{
  "id": "education",
  "title": "Education and training",
  "summary": "Schools, colleges and training: academic years and terms, year groups, attendance, grades and weighted marks, GPA and degree classes, timetable clashes, fees and CPD.",
  "synonyms": [
    "school",
    "college",
    "university",
    "academy",
    "education",
    "training",
    "learning",
    "student information system",
    "MIS",
    "gradebook",
    "tutoring",
    "courses",
    "e-learning",
    "LMS"
  ],
  "apps": [
    "school management system",
    "student information system",
    "gradebook app",
    "attendance tracker",
    "timetabling tool",
    "training provider portal",
    "CPD tracker",
    "tuition fees app"
  ],
  "language": "python",
  "includes": [
    "dates-basics"
  ],
  "includedBy": [],
  "caveats": [
    "UK first: year groups, academic years, attendance thresholds and degree classification follow UK conventions; check your institution's own regulations."
  ],
  "sections": [
    {
      "key": "calendar",
      "title": "Academic calendar",
      "purpose": "Put dates into academic years and terms, and pupils into year groups.",
      "from": "education",
      "members": [
        {
          "id": "education.academic-year",
          "tier": "core",
          "why": "The academic year and term a date falls in, terms supplied by you.",
          "section": "calendar",
          "from": "education",
          "version": "1.0.1",
          "summary": "The academic year and term a date falls in: 1 September to 31 August by default, terms from the caller.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "academicYear"
          ],
          "signatures": [
            "def academic_year(on_date: str, year_start: Optional[str], terms: Sequence[Term]) -> AcademicYear"
          ],
          "reference": "from fune.education.academic_year import academic_year  # education.academic-year@^1",
          "url": "http://www.functionalweave.com/functions/education.academic-year"
        },
        {
          "id": "education.school-year-group",
          "tier": "core",
          "why": "School year group from date of birth, by UK nation.",
          "section": "calendar",
          "from": "education",
          "version": "1.0.1",
          "summary": "School year group from date of birth on a date, by UK nation: Reception to Year 13, P1 to S6, P1 to Year 14.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "schoolYearGroup"
          ],
          "signatures": [
            "def school_year_group(birth_date: str, on_date: str, nation: UkNation) -> SchoolYearGroup"
          ],
          "reference": "from fune.education.school_year_group import school_year_group  # education.school-year-group@^1",
          "url": "http://www.functionalweave.com/functions/education.school-year-group"
        },
        {
          "id": "dates.age",
          "tier": "optional",
          "why": "Age in whole years on a date.",
          "section": "calendar",
          "from": "education",
          "version": "1.0.0",
          "summary": "Age in whole years on a given date, with 29 February birthdays ageing on 1 March in other years.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "age"
          ],
          "signatures": [
            "def age(birth_date: str, on_date: str) -> int"
          ],
          "reference": "from fune.dates.age import age  # dates.age@^1",
          "url": "http://www.functionalweave.com/functions/dates.age"
        }
      ],
      "gaps": []
    },
    {
      "key": "attendance",
      "title": "Attendance",
      "purpose": "Track attendance and spot persistent absence.",
      "from": "education",
      "members": [
        {
          "id": "education.attendance-rate",
          "tier": "core",
          "why": "Attendance and absence rates with the DfE persistent and severe absence flags.",
          "section": "attendance",
          "from": "education",
          "version": "1.0.1",
          "summary": "Pupil attendance and absence rates in basis points, with the DfE persistent and severe absence flags.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "attendanceRate"
          ],
          "signatures": [
            "def attendance_rate(possible_sessions: int, missed_sessions: int, on_date: str) -> AttendanceRate"
          ],
          "reference": "from fune.education.attendance_rate import attendance_rate  # education.attendance-rate@^1",
          "url": "http://www.functionalweave.com/functions/education.attendance-rate"
        }
      ],
      "gaps": [
        {
          "key": "registers",
          "text": "Taking registers (marks, codes per session) and storing them is yours.",
          "section": "attendance",
          "from": "education"
        }
      ]
    },
    {
      "key": "assessment",
      "title": "Assessment and grades",
      "purpose": "Turn marks into grades and results.",
      "from": "education",
      "members": [
        {
          "id": "education.grade-boundaries",
          "tier": "core",
          "why": "Grade for a mark from your boundary table.",
          "section": "assessment",
          "from": "education",
          "version": "1.0.0",
          "summary": "Grade for a mark from the caller's boundary table: inclusive lower bounds, ungraded below the lowest.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "gradeForMark"
          ],
          "signatures": [
            "def grade_for_mark(mark: int, max_mark: int, boundaries: Sequence[GradeBoundary]) -> GradeResult"
          ],
          "reference": "from fune.education.grade_boundaries import grade_for_mark  # education.grade-boundaries@^1",
          "url": "http://www.functionalweave.com/functions/education.grade-boundaries"
        },
        {
          "id": "education.weighted-grade",
          "tier": "core",
          "why": "Weighted percentage across coursework and exam components.",
          "section": "assessment",
          "from": "education",
          "version": "1.0.0",
          "summary": "Weighted percentage across coursework and exam components, exact as a fraction, then rounded once.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "weightedGrade"
          ],
          "signatures": [
            "def weighted_grade(components: Sequence[GradeComponent], decimals: int, mode: RoundingMode) -> WeightedGrade"
          ],
          "reference": "from fune.education.weighted_grade import weighted_grade  # education.weighted-grade@^1",
          "url": "http://www.functionalweave.com/functions/education.weighted-grade"
        },
        {
          "id": "education.gpa",
          "tier": "optional",
          "why": "GPA on a 4.0 scale from letter grades and credit hours.",
          "section": "assessment",
          "from": "education",
          "version": "1.0.0",
          "summary": "Grade point average on a 4.0 scale from letter grades and credit hours, weighted by credits and rounded once.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "gpa"
          ],
          "signatures": [
            "def gpa(courses: Sequence[GpaCourse], grade_points: Optional[Sequence[GradePoint]], mode: RoundingMode) -> GpaResult"
          ],
          "reference": "from fune.education.gpa import gpa  # education.gpa@^1",
          "url": "http://www.functionalweave.com/functions/education.gpa"
        },
        {
          "id": "education.degree-classification",
          "tier": "optional",
          "why": "UK honours degree class from module marks and credits, weighted by level.",
          "section": "assessment",
          "from": "education",
          "version": "1.0.0",
          "summary": "UK honours degree class (First, 2:1, 2:2, Third) from module marks and credits, weighted by level.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "degreeClassification"
          ],
          "signatures": [
            "def degree_classification(modules: Sequence[ModuleMark], level_weights: Sequence[LevelWeight], decimals: int, mode: RoundingMode) -> DegreeClassification"
          ],
          "reference": "from fune.education.degree_classification import degree_classification  # education.degree-classification@^1",
          "url": "http://www.functionalweave.com/functions/education.degree-classification"
        },
        {
          "id": "stats.mean-median",
          "tier": "optional",
          "why": "Class averages and medians, exactly.",
          "section": "assessment",
          "from": "education",
          "version": "1.0.0",
          "summary": "Mean, median and mode of a list of integers, with the mean and median as exact fractions.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "meanMedianMode"
          ],
          "signatures": [
            "def mean_median_mode(values: Sequence[int]) -> CentralTendency"
          ],
          "reference": "from fune.stats.mean_median import mean_median_mode  # stats.mean-median@^1",
          "url": "http://www.functionalweave.com/functions/stats.mean-median"
        }
      ],
      "gaps": [
        {
          "key": "exam-board-data",
          "text": "Exam board results and boundaries (via A2C/EDI) are not in caps; you supply the tables.",
          "section": "assessment",
          "from": "education"
        }
      ]
    },
    {
      "key": "timetable",
      "title": "Timetabling",
      "purpose": "Check a timetable for clashes.",
      "from": "education",
      "members": [
        {
          "id": "education.timetable-clash",
          "tier": "core",
          "why": "Finds overlapping sessions that share a room, a teacher or a student group.",
          "section": "timetable",
          "from": "education",
          "version": "1.0.0",
          "summary": "Find timetable clashes: overlapping sessions that share a room, a teacher or a student group, in a fixed order.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "timetableClashes"
          ],
          "signatures": [
            "def timetable_clashes(sessions: Sequence[TimetableSession]) -> List[TimetableClash]"
          ],
          "reference": "from fune.education.timetable_clash import timetable_clashes  # education.timetable-clash@^1",
          "url": "http://www.functionalweave.com/functions/education.timetable-clash"
        }
      ],
      "gaps": [
        {
          "key": "timetable-generation",
          "text": "Building a timetable (constraint solving) is not in caps; it only finds clashes.",
          "section": "timetable",
          "from": "education"
        }
      ]
    },
    {
      "key": "fees",
      "title": "Fees and finance",
      "purpose": "Bill fees in instalments.",
      "from": "education",
      "members": [
        {
          "id": "education.fee-instalments",
          "tier": "optional",
          "why": "Tuition fee instalment schedule from a total, shares and due dates, to the penny.",
          "section": "fees",
          "from": "education",
          "version": "1.0.0",
          "summary": "Tuition fee instalment schedule from a total, shares and due dates, exact to the penny.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "feeInstalments"
          ],
          "signatures": [
            "def fee_instalments(total: Money, shares: Sequence[int], due_dates: Sequence[str]) -> List[FeeInstalment]"
          ],
          "reference": "from fune.education.fee_instalments import fee_instalments  # education.fee-instalments@^1",
          "url": "http://www.functionalweave.com/functions/education.fee-instalments"
        },
        {
          "id": "education.student-loan-repayment",
          "tier": "optional",
          "why": "A graduate's student loan repayment for a tax year.",
          "section": "fees",
          "from": "education",
          "version": "1.0.1",
          "summary": "A graduate's student or postgraduate loan repayment for a tax year, as Self Assessment works it out.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "studentLoanRepayment"
          ],
          "signatures": [
            "def student_loan_repayment(earned_income: Money, unearned_income: Money, plan: StudentLoanPlan, tax_year_date: str, deducted_through_paye: Money) -> StudentLoanRepayment"
          ],
          "reference": "from fune.education.student_loan_repayment import student_loan_repayment  # education.student-loan-repayment@^1",
          "url": "http://www.functionalweave.com/functions/education.student-loan-repayment"
        },
        {
          "id": "money.format",
          "tier": "optional",
          "why": "Shows fees with the currency's decimal places.",
          "section": "fees",
          "from": "education",
          "version": "1.0.0",
          "summary": "Render a monetary amount as text, using the currency's real minor-unit precision.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "formatMoney"
          ],
          "signatures": [
            "def format_money(amount: Money) -> str"
          ],
          "reference": "from fune.money.format import format_money  # money.format@^1",
          "url": "http://www.functionalweave.com/functions/money.format"
        }
      ],
      "gaps": []
    },
    {
      "key": "cpd",
      "title": "Professional development",
      "purpose": "Track CPD against a requirement.",
      "from": "education",
      "members": [
        {
          "id": "education.cpd-hours",
          "tier": "optional",
          "why": "CPD minutes logged against a requirement for a period, shortfall and days left.",
          "section": "cpd",
          "from": "education",
          "version": "1.0.0",
          "summary": "CPD logged against a requirement for a rolling or fixed period: minutes counted, shortfall and days left.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "cpdHours"
          ],
          "signatures": [
            "def cpd_hours(entries: Sequence[CpdEntry], requirement: CpdRequirement, on_date: str) -> CpdProgress"
          ],
          "reference": "from fune.education.cpd_hours import cpd_hours  # education.cpd-hours@^1",
          "url": "http://www.functionalweave.com/functions/education.cpd-hours"
        }
      ],
      "gaps": []
    },
    {
      "key": "date-arithmetic",
      "title": "Calendar arithmetic",
      "purpose": "Move and measure ISO dates exactly, with no date library and no clock reads.",
      "from": "dates-basics",
      "members": [
        {
          "id": "dates.add-days",
          "tier": "core",
          "why": "Shifts a date by whole days, for due dates and windows.",
          "section": "date-arithmetic",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Shift an ISO date by a whole number of days, forwards or backwards, with exact calendar arithmetic.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "addDays"
          ],
          "signatures": [
            "def add_days(iso: str, days: int) -> str"
          ],
          "reference": "from fune.dates.add_days import add_days  # dates.add-days@^1",
          "url": "http://www.functionalweave.com/functions/dates.add-days"
        },
        {
          "id": "dates.add-months",
          "tier": "core",
          "why": "Adds months clamping to month end, so 31 January plus a month is the end of February.",
          "section": "date-arithmetic",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Add calendar months to an ISO date, clamping to the month end: 31 Jan + 1 month is 28 or 29 Feb.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "addMonths"
          ],
          "signatures": [
            "def add_months(iso: str, months: int) -> str"
          ],
          "reference": "from fune.dates.add_months import add_months  # dates.add-months@^1",
          "url": "http://www.functionalweave.com/functions/dates.add-months"
        },
        {
          "id": "dates.days-between",
          "tier": "core",
          "why": "Signed calendar days between two dates, for ages of invoices and stays.",
          "section": "date-arithmetic",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Calendar days from one ISO date to another, signed: negative when the second date is earlier.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "daysBetween"
          ],
          "signatures": [
            "def days_between(start_iso: str, end_iso: str) -> int"
          ],
          "reference": "from fune.dates.days_between import days_between  # dates.days-between@^1",
          "url": "http://www.functionalweave.com/functions/dates.days-between"
        },
        {
          "id": "dates.month-boundaries",
          "tier": "core",
          "why": "First and last day of a date's month, for monthly periods and statements.",
          "section": "date-arithmetic",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "The first day, last day and length of the calendar month containing a date.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "monthBoundaries"
          ],
          "signatures": [
            "def month_boundaries(iso: str) -> MonthBoundaries"
          ],
          "reference": "from fune.dates.month_boundaries import month_boundaries  # dates.month-boundaries@^1",
          "url": "http://www.functionalweave.com/functions/dates.month-boundaries"
        },
        {
          "id": "dates.day-of-week",
          "tier": "core",
          "why": "ISO weekday of a date, for weekend rules and opening hours.",
          "section": "date-arithmetic",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "The ISO 8601 day of the week for a date: 1 is Monday through 7 is Sunday.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "dayOfWeek"
          ],
          "signatures": [
            "def day_of_week(iso: str) -> int"
          ],
          "reference": "from fune.dates.day_of_week import day_of_week  # dates.day-of-week@^1",
          "url": "http://www.functionalweave.com/functions/dates.day-of-week"
        },
        {
          "id": "dates.is-leap-year",
          "tier": "optional",
          "why": "Whether a year is a leap year, for 365/366-day calculations.",
          "section": "date-arithmetic",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Whether a year is a Gregorian leap year: every 4th, except centuries, except every 400th.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isLeapYear"
          ],
          "signatures": [
            "def is_leap_year(year: int) -> bool"
          ],
          "reference": "from fune.dates.is_leap_year import is_leap_year  # dates.is-leap-year@^1",
          "url": "http://www.functionalweave.com/functions/dates.is-leap-year"
        },
        {
          "id": "dates.overlap",
          "tier": "optional",
          "why": "Days two date ranges share, for prorating and clash checks.",
          "section": "date-arithmetic",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Days two date ranges have in common, with the end of each range stated as inclusive or exclusive.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "overlapDays"
          ],
          "signatures": [
            "def overlap_days(a_start: str, a_end: str, b_start: str, b_end: str, end_inclusive: bool) -> int"
          ],
          "reference": "from fune.dates.overlap import overlap_days  # dates.overlap@^1",
          "url": "http://www.functionalweave.com/functions/dates.overlap"
        }
      ],
      "gaps": []
    },
    {
      "key": "reporting-periods",
      "title": "Periods and reporting dates",
      "purpose": "Put dates into the periods reports are grouped by.",
      "from": "dates-basics",
      "members": [
        {
          "id": "dates.quarter",
          "tier": "core",
          "why": "Calendar and fiscal quarter of a date for any year-start month.",
          "section": "reporting-periods",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Calendar quarter and fiscal quarter of a date, with the fiscal year and quarter date ranges, for any start month.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "quarter"
          ],
          "signatures": [
            "def quarter(iso: str, fiscal_year_start_month: int) -> Quarter"
          ],
          "reference": "from fune.dates.quarter import quarter  # dates.quarter@^1",
          "url": "http://www.functionalweave.com/functions/dates.quarter"
        },
        {
          "id": "dates.week-number",
          "tier": "optional",
          "why": "ISO week number and week-year, for weekly reports.",
          "section": "reporting-periods",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "ISO 8601 week number and week-numbering year of a date: weeks start Monday, week 1 holds the first Thursday.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "weekNumber"
          ],
          "signatures": [
            "def week_number(iso: str) -> IsoWeek"
          ],
          "reference": "from fune.dates.week_number import week_number  # dates.week-number@^1",
          "url": "http://www.functionalweave.com/functions/dates.week-number"
        },
        {
          "id": "dates.day-count-fraction",
          "tier": "optional",
          "why": "Year fraction by ACT/365F, ACT/360, 30/360 or ACT/ACT, for interest periods.",
          "section": "reporting-periods",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Year fraction between two dates by ACT/365F, ACT/360, 30/360, 30E/360 or ACT/ACT ISDA, as an exact fraction.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "dayCountFraction"
          ],
          "signatures": [
            "def day_count_fraction(start_iso: str, end_iso: str, convention: DayCountConvention) -> YearFraction"
          ],
          "reference": "from fune.dates.day_count_fraction import day_count_fraction  # dates.day-count-fraction@^1",
          "url": "http://www.functionalweave.com/functions/dates.day-count-fraction"
        }
      ],
      "gaps": []
    },
    {
      "key": "working-days",
      "title": "Working days and holidays",
      "purpose": "Skip weekends and public holidays when counting or adding days.",
      "from": "dates-basics",
      "members": [
        {
          "id": "dates.bank-holidays",
          "tier": "core",
          "why": "UK bank holidays for a region and year, as gov.uk publishes them.",
          "section": "working-days",
          "from": "dates-basics",
          "version": "1.0.1",
          "summary": "UK bank and public holidays for a region and year, as published by gov.uk, including one-off holidays.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "bankHolidays"
          ],
          "signatures": [
            "def bank_holidays(region: UkRegion, year: int) -> List[BankHoliday]"
          ],
          "reference": "from fune.dates.bank_holidays import bank_holidays  # dates.bank-holidays@^1",
          "url": "http://www.functionalweave.com/functions/dates.bank-holidays"
        },
        {
          "id": "dates.add-business-days",
          "tier": "core",
          "why": "Moves a date by working days, skipping weekends and a holiday list.",
          "section": "working-days",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Move a date forward or back by working days, skipping weekends and a caller-supplied holiday list.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "addBusinessDays"
          ],
          "signatures": [
            "def add_business_days(iso: str, days: int, holidays: Sequence[str]) -> str"
          ],
          "reference": "from fune.dates.add_business_days import add_business_days  # dates.add-business-days@^1",
          "url": "http://www.functionalweave.com/functions/dates.add-business-days"
        },
        {
          "id": "dates.business-days-between",
          "tier": "core",
          "why": "Working days between two dates, for SLAs and notice periods.",
          "section": "working-days",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Working days between two dates, excluding weekends and a caller-supplied holiday list.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "businessDaysBetween"
          ],
          "signatures": [
            "def business_days_between(start_iso: str, end_iso: str, holidays: Sequence[str]) -> int"
          ],
          "reference": "from fune.dates.business_days_between import business_days_between  # dates.business-days-between@^1",
          "url": "http://www.functionalweave.com/functions/dates.business-days-between"
        }
      ],
      "gaps": []
    },
    {
      "key": "repeating-dates",
      "title": "Repeating schedules",
      "purpose": "Generate the dates of something that repeats.",
      "from": "dates-basics",
      "members": [
        {
          "id": "dates.recurrence",
          "tier": "optional",
          "why": "The next n dates of a rule: monthly on the 31st, last Friday, every two weeks.",
          "section": "repeating-dates",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "The next n dates of a repeating rule: monthly on a day, last weekday of the month, every k days or weeks, yearly.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "recurrence"
          ],
          "signatures": [
            "def recurrence(rule: RecurrenceRule, start_iso: str, count: int) -> List[str]"
          ],
          "reference": "from fune.dates.recurrence import recurrence  # dates.recurrence@^1",
          "url": "http://www.functionalweave.com/functions/dates.recurrence"
        }
      ],
      "gaps": []
    },
    {
      "key": "times",
      "title": "Times and durations",
      "purpose": "Work with times of day, durations and timestamps.",
      "from": "dates-basics",
      "members": [
        {
          "id": "time.duration",
          "tier": "optional",
          "why": "Parses and adds durations like 1h30m in whole minutes, and formats the total.",
          "section": "times",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Parse durations written as 1h30m, 90m or 01:30, add them up in whole minutes, and format the total.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "duration"
          ],
          "signatures": [
            "def duration(parts: Sequence[str]) -> Duration"
          ],
          "reference": "from fune.time.duration import duration  # time.duration@^1",
          "url": "http://www.functionalweave.com/functions/time.duration"
        },
        {
          "id": "time.minutes-between",
          "tier": "optional",
          "why": "Minutes between two local date-times, across midnight.",
          "section": "times",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Minutes from one local date and time to another, correct across midnight; wall-clock time, no time zones.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "minutesBetween"
          ],
          "signatures": [
            "def minutes_between(start_date: str, start_time: str, end_date: str, end_time: str) -> int"
          ],
          "reference": "from fune.time.minutes_between import minutes_between  # time.minutes-between@^1",
          "url": "http://www.functionalweave.com/functions/time.minutes-between"
        },
        {
          "id": "time.round-to-increment",
          "tier": "optional",
          "why": "Rounds minutes to a billing increment such as 6 or 15.",
          "section": "times",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Round a duration in minutes to a billing increment such as 6 or 15 minutes, up, down or to nearest.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "roundToIncrement"
          ],
          "signatures": [
            "def round_to_increment(minutes: int, increment: int, mode: RoundingMode) -> int"
          ],
          "reference": "from fune.time.round_to_increment import round_to_increment  # time.round-to-increment@^1",
          "url": "http://www.functionalweave.com/functions/time.round-to-increment"
        },
        {
          "id": "time.iso-to-unix",
          "tier": "optional",
          "why": "RFC 3339 timestamps to Unix seconds.",
          "section": "times",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "An RFC 3339 timestamp with a Z or offset, such as 2026-09-26T13:00:00+01:00, to Unix time in whole seconds.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isoToUnix"
          ],
          "signatures": [
            "def iso_to_unix(timestamp: str) -> int"
          ],
          "reference": "from fune.time.iso_to_unix import iso_to_unix  # time.iso-to-unix@^1",
          "url": "http://www.functionalweave.com/functions/time.iso-to-unix"
        },
        {
          "id": "time.unix-to-iso",
          "tier": "optional",
          "why": "Unix seconds to an ISO 8601 UTC timestamp.",
          "section": "times",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Unix time in seconds to an ISO 8601 UTC timestamp such as 2026-09-26T12:00:00Z, for years 0001 to 9999.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "unixToIso"
          ],
          "signatures": [
            "def unix_to_iso(seconds: int) -> str"
          ],
          "reference": "from fune.time.unix_to_iso import unix_to_iso  # time.unix-to-iso@^1",
          "url": "http://www.functionalweave.com/functions/time.unix-to-iso"
        },
        {
          "id": "time.countdown",
          "tier": "optional",
          "why": "Seconds left until a moment and a 4:05 style timer text.",
          "section": "times",
          "from": "dates-basics",
          "version": "1.0.0",
          "summary": "Seconds left until a Unix time, whether it has passed, and a timer text like 4:05, for tokens or retry waits.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "countdown"
          ],
          "signatures": [
            "def countdown(until: int, now: int) -> Countdown"
          ],
          "reference": "from fune.time.countdown import countdown  # time.countdown@^1",
          "url": "http://www.functionalweave.com/functions/time.countdown"
        }
      ],
      "gaps": []
    }
  ],
  "gaps": [
    {
      "key": "registers",
      "text": "Taking registers (marks, codes per session) and storing them is yours.",
      "section": "attendance",
      "from": "education"
    },
    {
      "key": "exam-board-data",
      "text": "Exam board results and boundaries (via A2C/EDI) are not in caps; you supply the tables.",
      "section": "assessment",
      "from": "education"
    },
    {
      "key": "timetable-generation",
      "text": "Building a timetable (constraint solving) is not in caps; it only finds clashes.",
      "section": "timetable",
      "from": "education"
    },
    {
      "key": "safeguarding",
      "text": "Safeguarding records, parental contacts and consent are yours, with their data protection duties.",
      "section": null,
      "from": "education"
    },
    {
      "key": "learning-content",
      "text": "Course content, quizzes and an LMS (SCORM, video) are not in caps.",
      "section": null,
      "from": "education"
    },
    {
      "key": "time-zones",
      "text": "Time zones and daylight saving: caps works in ISO dates, UTC and wall-clock times; converting with the IANA tz rules is yours.",
      "section": null,
      "from": "dates-basics"
    },
    {
      "key": "locale-formatting",
      "text": "Showing dates in a user's language and format (\"3 octobre 2026\"): caps returns ISO dates, not localised text.",
      "section": null,
      "from": "dates-basics"
    },
    {
      "key": "non-uk-holidays",
      "text": "Public holidays outside the UK: dates.bank-holidays is UK only; pass another country's list to the working-day functions yourself.",
      "section": null,
      "from": "dates-basics"
    }
  ],
  "counts": {
    "members": 34,
    "core": 15,
    "optional": 19,
    "unreviewed": 0,
    "deprecated": [],
    "unavailable": [],
    "missing": [],
    "gaps": 8
  },
  "warnings": [
    "UK first: year groups, academic years, attendance thresholds and degree classification follow UK conventions; check your institution's own regulations."
  ],
  "url": "http://www.functionalweave.com/suites/education",
  "owner": null
}
