Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { addDays, daysBetween, parseIsoDate } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { addMonths } from "./dates_add_months.ts"; ← from dates.add-months ^1.0.0 · built alongside by fune
import { type CpdEntry, type CpdProgress, type CpdRequirement } from "./education_cpd_hours_types.ts";
/**
* CPD minutes logged in the period that applies on onDate, against the
* requirement. Fixed period boundaries are anchor + k x periodMonths, each
* worked out from the anchor so month-end clamping never accumulates.
*/
export function cpdHours(entries: readonly CpdEntry[], requirement: CpdRequirement, onDate: string): CpdProgress {
const on = parseIsoDate(onDate);
const { requiredMinutes, periodMonths, rolling, anchor } = requirement;
if (!Number.isInteger(requiredMinutes) || requiredMinutes < 0) {
throw new RangeError(`requiredMinutes must be a whole number of 0 or more, received ${requiredMinutes}`);
}
if (!Number.isInteger(periodMonths) || periodMonths < 1) {
throw new RangeError(`periodMonths must be a whole number of 1 or more, received ${periodMonths}`);
}
let periodStart: string;
let periodEnd: string;
if (rolling) {
if (anchor !== null) throw new RangeError("a rolling period takes no anchor");
periodEnd = onDate;
periodStart = addDays(addMonths(onDate, -periodMonths), 1);
} else {
if (anchor === null) throw new RangeError("a fixed period needs an anchor date");
const a = parseIsoDate(anchor);
const monthsApart = (on.year - a.year) * 12 + (on.month - a.month);
let k = Math.floor(monthsApart / periodMonths);
while (addMonths(anchor, k * periodMonths) > onDate) k -= 1;
while (addMonths(anchor, (k + 1) * periodMonths) <= onDate) k += 1;
periodStart = addMonths(anchor, k * periodMonths);
periodEnd = addDays(addMonths(anchor, (k + 1) * periodMonths), -1);
}
let logged = 0;
let counted = 0;
for (const e of entries) {
parseIsoDate(e.date);
if (!Number.isInteger(e.minutes) || e.minutes < 0) {
throw new RangeError(`minutes must be a whole number of 0 or more, received ${e.minutes} on ${e.date}`);
}
if (e.date >= periodStart && e.date <= periodEnd && e.date <= onDate) {
logged += e.minutes;
counted += 1;
}
}
const shortfall = Math.max(0, requiredMinutes - logged);
return {
periodStart,
periodEnd,
loggedMinutes: logged,
entriesCounted: counted,
requiredMinutes,
shortfallMinutes: shortfall,
met: shortfall === 0,
daysRemaining: daysBetween(onDate, periodEnd),
};
}