impl/typescript.ts
2,180 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { addDays, daysInMonth, formatIsoDate, parseIsoDate } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { type AcademicYear, type Term } from "./education_academic_year_types.ts";
const MONTH_DAY = /^(\d{2})-(\d{2})$/;
/**
* The academic year and term for a date.
*
* The end is the day before the next year's start, worked out by date
* arithmetic, so a 1 March start ends on 29 February in a leap year.
*/
export function academicYear(onDate: string, yearStart: string | null, terms: readonly Term[]): AcademicYear {
const on = parseIsoDate(onDate);
const startText = yearStart === null ? "09-01" : yearStart;
const match = MONTH_DAY.exec(startText);
const month = match === null ? 0 : Number(match[1]);
const day = match === null ? 0 : Number(match[2]);
if (match === null || month < 1 || month > 12 || day < 1 || day > daysInMonth(2001, month)) {
throw new RangeError(`yearStart must be MM-DD on a day every year has, received "${startText}"`);
}
const sorted = [...terms].sort((a, b) => (a.start < b.start ? -1 : a.start > b.start ? 1 : 0));
for (const t of sorted) {
parseIsoDate(t.start);
parseIsoDate(t.end);
if (t.end < t.start) {
throw new RangeError(`term ${t.name} ends (${t.end}) before it starts (${t.start})`);
}
}
for (let i = 1; i < sorted.length; i++) {
if (sorted[i].start <= sorted[i - 1].end) {
throw new RangeError(`terms ${sorted[i - 1].name} and ${sorted[i].name} overlap`);
}
}
const beforeStart = on.month < month || (on.month === month && on.day < day);
const startYear = beforeStart ? on.year - 1 : on.year;
const start = formatIsoDate({ year: startYear, month, day });
const end = addDays(formatIsoDate({ year: startYear + 1, month, day }), -1);
const endYear = Number(end.slice(0, 4));
const label = endYear === startYear ? String(startYear) : `${startYear}/${String(endYear % 100).padStart(2, "0")}`;
const term = sorted.find((t) => t.start <= onDate && onDate <= t.end) ?? null;
return {
label,
start,
end,
term: term === null ? null : term.name,
termStart: term === null ? null : term.start,
termEnd: term === null ? null : term.end,
};
}