Functional Weave
Code in Rust

education.academic-year@1.0.1

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,
  };
}