Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{add_days, days_between, parse_iso_date}; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_add_months::add_months; ← from dates.add-months ^1.0.0 · built alongside by fune
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
use super::payroll_holiday_entitlement_data::{HolidayRule, HOLIDAY_RULES, HOLIDAY_RULES_HISTORY}; ← this capability’s own data, compiled from data/holiday-rules.json into the same file by fune build
fn rule_for(basis: &str, leave_year_start: &str) -> &'static HolidayRule {
let mut best: Option<&'static HolidayRule> = None;
for rule in HOLIDAY_RULES {
if rule.basis != basis || leave_year_start < rule.valid_from {
continue;
}
if let Some(to) = rule.valid_to {
if leave_year_start > to {
continue;
}
}
if best.map_or(true, |b| rule.valid_from > b.valid_from) {
best = Some(rule);
}
}
match best {
Some(rule) => rule,
None => {
let what = if basis == "accrual" { "12.07% accrual rule" } else { "5.6-week rule" };
if HOLIDAY_RULES_HISTORY != "full" {
panic!(
"no holiday {} for a leave year beginning {}: this build was installed with history={}. Reinstall with history=full for older leave years.",
what, leave_year_start, HOLIDAY_RULES_HISTORY
);
}
panic!("no holiday {} for a leave year beginning {}", what, leave_year_start)
}
}
}
/// Statutory annual leave under the Working Time Regulations 1998.
///
/// Regular workers get 5.6 weeks of their working days, capped at 28 days, and
/// a proportion of it for a leave year they only partly work. Irregular-hours
/// and part-year workers, for leave years from 1 April 2024, instead accrue
/// 12.07% of the hours they work each pay period, to the nearest whole hour.
///
/// # Panics
/// Panics on an out-of-range input, a malformed date, or a leave year no rule covers.
pub fn holiday_entitlement(worker: &HolidayWorker, leave_year_start: &str) -> HolidayEntitlement {
parse_iso_date(leave_year_start);
if worker.kind == "irregular-hours" || worker.kind == "part-year" {
let rule = rule_for("accrual", leave_year_start);
if worker.minutes_worked < 0 {
panic!(
"minutesWorked must be a whole number of minutes, not negative, received {}",
worker.minutes_worked
);
}
// reg 15B(5): under 30 minutes of a fraction is dropped, 30 or more is a whole hour.
let scaled = worker.minutes_worked * rule.accrual_basis_points;
let hour = 60 * 10000;
let mut hours = scaled / hour;
if scaled - hours * hour >= hour / 2 {
hours += 1;
}
return HolidayEntitlement { kind: worker.kind.clone(), days_x100: 0, capped: false, hours_accrued: hours };
}
if worker.kind != "regular" {
panic!("unknown kind of worker \"{}\"", worker.kind);
}
let rule = rule_for("weeks", leave_year_start);
let per_week = worker.days_per_week_x100;
if !(1..=700).contains(&per_week) {
panic!("daysPerWeekX100 must be 1 to 700, received {}", per_week);
}
// In thousandths of a day: 5.6 weeks of 5 days is 28000.
let raw = rule.weeks_x10 * per_week;
let cap = rule.cap_days * 1000;
let full = raw.min(cap);
let next_year = add_months(leave_year_start, 12);
let year_end = add_days(&next_year, -1);
let from = match &worker.employment_start {
Some(s) if s.as_str() > leave_year_start => s.clone(),
_ => leave_year_start.to_string(),
};
let to = match &worker.employment_end {
Some(e) if *e < year_end => e.clone(),
_ => year_end.clone(),
};
parse_iso_date(&from);
parse_iso_date(&to);
if to < from {
panic!("employment {} to {} does not overlap the leave year", from, to);
}
let worked = days_between(&from, &to) + 1;
let year_days = days_between(leave_year_start, &next_year);
HolidayEntitlement {
kind: "regular".to_string(),
days_x100: round_div(full * worked, year_days * 10, "up"),
capped: raw > cap,
hours_accrued: 0,
}
}
pub fn holiday_entitlement_to_value(e: &HolidayEntitlement) -> Value {
Value::obj(vec![
("kind", Value::str(&e.kind)),
("daysX100", Value::Int(e.days_x100)),
("capped", Value::Bool(e.capped)),
("hoursAccrued", Value::Int(e.hours_accrued)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let w = &args[0];
let optional = |key: &str| {
let v = w.get(key);
if v.is_null() {
None
} else {
Some(v.as_str().to_string())
}
};
let worker = HolidayWorker {
kind: w.get("kind").as_str().to_string(),
days_per_week_x100: w.get("daysPerWeekX100").as_i64(),
employment_start: optional("employmentStart"),
employment_end: optional("employmentEnd"),
minutes_worked: w.get("minutesWorked").as_i64(),
};
holiday_entitlement_to_value(&holiday_entitlement(&worker, args[1].as_str()))
}