Functional Weave
Code in TypeScript

education.academic-year@1.0.1

impl/python.py

2,254 bytes · the Python 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 re
from typing import Optional, Sequence

from .dates_add_days import add_days, days_in_month, format_iso_date, parse_iso_date  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_days_types import CivilDate
from .education_academic_year_types import AcademicYear, Term

MONTH_DAY = re.compile(r"([0-9]{2})-([0-9]{2})")


def academic_year(on_date: str, year_start: Optional[str], terms: Sequence[Term]) -> AcademicYear:
    """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.
    """
    on = parse_iso_date(on_date)
    start_text = "09-01" if year_start is None else year_start
    match = MONTH_DAY.fullmatch(start_text) if isinstance(start_text, str) else None
    month = int(match.group(1)) if match else 0
    day = int(match.group(2)) if match else 0
    if match is None or month < 1 or month > 12 or day < 1 or day > days_in_month(2001, month):
        raise ValueError('yearStart must be MM-DD on a day every year has, received "%s"' % (start_text,))

    ordered = sorted(terms, key=lambda t: t.start)
    for t in ordered:
        parse_iso_date(t.start)
        parse_iso_date(t.end)
        if t.end < t.start:
            raise ValueError("term %s ends (%s) before it starts (%s)" % (t.name, t.end, t.start))
    for i in range(1, len(ordered)):
        if ordered[i].start <= ordered[i - 1].end:
            raise ValueError("terms %s and %s overlap" % (ordered[i - 1].name, ordered[i].name))

    before_start = (on.month, on.day) < (month, day)
    start_year = on.year - 1 if before_start else on.year
    start = format_iso_date(CivilDate(year=start_year, month=month, day=day))
    end = add_days(format_iso_date(CivilDate(year=start_year + 1, month=month, day=day)), -1)
    end_year = int(end[0:4])
    label = str(start_year) if end_year == start_year else "%d/%02d" % (start_year, end_year % 100)

    term = next((t for t in ordered if t.start <= on_date <= t.end), None)
    return AcademicYear(
        label=label,
        start=start,
        end=end,
        term=None if term is None else term.name,
        term_start=None if term is None else term.start,
        term_end=None if term is None else term.end,
    )