{
  "id": "health-clinic",
  "title": "Health clinic and practice management",
  "summary": "A clinic or practice: appointment slots, patient identifiers and ages, waiting times, and clinical calculators (BMI, BSA, eGFR, NEWS2, weight-based doses, gestational age).",
  "synonyms": [
    "clinic",
    "medical practice",
    "GP surgery",
    "healthcare",
    "patient management",
    "practice management",
    "physiotherapy",
    "dental practice",
    "private clinic",
    "NHS",
    "clinical calculator",
    "patient records"
  ],
  "apps": [
    "clinic booking system",
    "patient management system",
    "practice management software",
    "appointment booking for a clinic",
    "clinical calculator app",
    "electronic health record"
  ],
  "language": "rust",
  "includes": [
    "dates-basics",
    "validation-basics"
  ],
  "includedBy": [],
  "caveats": [
    "Not a medical device and not clinical advice. Every clinical calculator here needs sign-off by a qualified clinician before it is used for real patients, and software that informs diagnosis or treatment may need UKCA/CE marking as a medical device (MHRA).",
    "UK first: NHS numbers, NHS England RTT waits and NEWS2 follow UK rules.",
    "Patient data is special category data under UK GDPR: storage, access control and audit are yours."
  ],
  "sections": [
    {
      "key": "appointments",
      "title": "Appointments",
      "purpose": "Offer bookable slots and keep track of how long patients wait.",
      "from": "health-clinic",
      "members": [
        {
          "id": "health.appointment-slots",
          "tier": "core",
          "why": "Bookable slots from clinic sessions, a slot length, breaks and existing bookings.",
          "section": "appointments",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "Bookable appointment slots from clinic sessions, a slot length, breaks and existing bookings.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "appointmentSlots"
          ],
          "signatures": [
            "pub fn appointment_slots(sessions: &[ClinicSession], slot_minutes: i64, breaks: &[ClinicBreak], bookings: &[Booking]) -> Vec<AppointmentSlot>"
          ],
          "reference": "fune!(health.appointment-slots@^1);  // then call appointment_slots(…)",
          "url": "http://www.functionalweave.com/functions/health.appointment-slots"
        },
        {
          "id": "health.waiting-time",
          "tier": "optional",
          "why": "NHS England referral-to-treatment wait in days and weekly bands, with the 18-week breach flag.",
          "section": "appointments",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "NHS England referral-to-treatment (RTT) wait in days and weekly bands, with the 18-week breach flag.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "rttWaitingTime"
          ],
          "signatures": [
            "pub fn rtt_waiting_time(clock_start: &str, clock_stop: Option<&str>, on_date: &str) -> RttWait"
          ],
          "reference": "fune!(health.waiting-time@^1);  // then call rtt_waiting_time(…)",
          "url": "http://www.functionalweave.com/functions/health.waiting-time"
        }
      ],
      "gaps": [
        {
          "key": "appointment-reminders",
          "text": "Reminder texts and emails, and handling did-not-attend and cancellations.",
          "section": "appointments",
          "from": "health-clinic"
        },
        {
          "key": "clinician-calendars",
          "text": "Clinicians' rotas, rooms and leave that make the sessions the slots are cut from.",
          "section": "appointments",
          "from": "health-clinic"
        }
      ]
    },
    {
      "key": "patients",
      "title": "Patients",
      "purpose": "Identify patients and record their age correctly.",
      "from": "health-clinic",
      "members": [
        {
          "id": "validation.nhs-number",
          "tier": "core",
          "why": "Checks an NHS number's modulus 11 check digit and normalises it to ten digits.",
          "section": "patients",
          "from": "health-clinic",
          "version": "2.0.0",
          "summary": "Check an NHS number's modulus 11 check digit and normalise it to ten digits.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateNhsNumber"
          ],
          "signatures": [
            "pub fn validate_nhs_number(value: &str) -> NhsNumber"
          ],
          "reference": "fune!(validation.nhs-number@^2);  // then call validate_nhs_number(…)",
          "url": "http://www.functionalweave.com/functions/validation.nhs-number"
        },
        {
          "id": "health.nhs-number-format",
          "tier": "core",
          "why": "Displays an NHS number as \"943 476 5919\" after checking it.",
          "section": "patients",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "Display an NHS number in the 3-3-4 format (\"943 476 5919\") after checking its modulus 11 check digit.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "formatNhsNumber"
          ],
          "signatures": [
            "pub fn format_nhs_number(value: &str) -> String"
          ],
          "reference": "fune!(health.nhs-number-format@^1);  // then call format_nhs_number(…)",
          "url": "http://www.functionalweave.com/functions/health.nhs-number-format"
        },
        {
          "id": "dates.age",
          "tier": "core",
          "why": "Age in whole years on a date, for adult records and age-based rules.",
          "section": "patients",
          "from": "health-clinic",
          "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": [
            "pub fn age(birth_date: &str, on_date: &str) -> i64"
          ],
          "reference": "fune!(dates.age@^1);  // then call age(…)",
          "url": "http://www.functionalweave.com/functions/dates.age"
        },
        {
          "id": "health.age-in-months",
          "tier": "optional",
          "why": "Age in completed months and days, for paediatric charts and doses.",
          "section": "patients",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "Age in completed months and days on a given date, for paediatric charts and doses; month ends handled like birthdays.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "ageInMonths"
          ],
          "signatures": [
            "pub fn age_in_months(birth_date: &str, on_date: &str) -> AgeInMonths"
          ],
          "reference": "fune!(health.age-in-months@^1);  // then call age_in_months(…)",
          "url": "http://www.functionalweave.com/functions/health.age-in-months"
        }
      ],
      "gaps": [
        {
          "key": "patient-lookup",
          "text": "Looking a patient up on the NHS Spine (PDS) or another master index is network work and not in caps.",
          "section": "patients",
          "from": "health-clinic"
        },
        {
          "key": "clinical-records",
          "text": "Clinical notes, coding (SNOMED CT), consent and the audit trail of who saw what.",
          "section": "patients",
          "from": "health-clinic"
        }
      ]
    },
    {
      "key": "calculators",
      "title": "Clinical calculators",
      "purpose": "Common bedside and consulting-room calculations, each pinned by tests.",
      "from": "health-clinic",
      "members": [
        {
          "id": "health.bmi",
          "tier": "core",
          "why": "Adult BMI to one decimal place and its NICE/WHO weight category.",
          "section": "calculators",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "Adult body mass index (kg/m², 1 decimal place) and its NICE/WHO weight category.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "bmi"
          ],
          "signatures": [
            "pub fn bmi(weight_kg: f64, height_cm: f64) -> Bmi"
          ],
          "reference": "fune!(health.bmi@^1);  // then call bmi(…)",
          "url": "http://www.functionalweave.com/functions/health.bmi"
        },
        {
          "id": "health.bsa",
          "tier": "optional",
          "why": "Body surface area by the Mosteller and DuBois formulas.",
          "section": "calculators",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "Body surface area in m² by the Mosteller and the DuBois & DuBois formulas, to 2 decimal places.",
          "status": "unreviewed",
          "statusNote": "not for clinical use; regulatory review pending",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "bodySurfaceArea"
          ],
          "signatures": [
            "pub fn body_surface_area(weight_kg: f64, height_cm: f64) -> BodySurfaceArea"
          ],
          "reference": "fune!(health.bsa@^1);  // then call body_surface_area(…)",
          "url": "http://www.functionalweave.com/functions/health.bsa"
        },
        {
          "id": "health.egfr",
          "tier": "optional",
          "why": "eGFR by the race-free CKD-EPI 2021 creatinine equation.",
          "section": "calculators",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "eGFR in mL/min/1.73 m² by the race-free CKD-EPI 2021 creatinine equation, from creatinine in µmol/L or mg/dL.",
          "status": "unreviewed",
          "statusNote": "not for clinical use; regulatory review pending",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "egfrCkdEpi2021"
          ],
          "signatures": [
            "pub fn egfr_ckd_epi2021(creatinine: f64, unit: &str, age_years: i64, sex: &str) -> Egfr"
          ],
          "reference": "fune!(health.egfr@^1);  // then call egfr_ckd_epi2021(…)",
          "url": "http://www.functionalweave.com/functions/health.egfr"
        },
        {
          "id": "health.news2",
          "tier": "optional",
          "why": "NEWS2 score from one set of observations, with its clinical risk band.",
          "section": "calculators",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "NEWS2 early warning score from one set of observations: parameter scores, aggregate and RCP clinical risk band.",
          "status": "unreviewed",
          "statusNote": "not for clinical use; regulatory review pending",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "news2Score"
          ],
          "signatures": [
            "pub fn news2_score(observations: &News2Observations) -> News2Score"
          ],
          "reference": "fune!(health.news2@^1);  // then call news2_score(…)",
          "url": "http://www.functionalweave.com/functions/health.news2"
        },
        {
          "id": "health.dose-weight-based",
          "tier": "optional",
          "why": "Weight-based dose capped at a maximum and rounded to a measurable volume.",
          "section": "calculators",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "Weight-based dose (per kg) capped at a maximum single dose and rounded to a measurable volume, in exact micro-units.",
          "status": "unreviewed",
          "statusNote": "not for clinical use; regulatory review pending",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "weightBasedDose"
          ],
          "signatures": [
            "pub fn weight_based_dose(weight_grams: i64, dose_per_kg_micrograms: i64, max_single_dose_micrograms: Option<i64>, strength_micrograms: i64, strength_volume_microlitres: i64, measuring_increment_microlitres: i64) -> WeightBasedDose"
          ],
          "reference": "fune!(health.dose-weight-based@^1);  // then call weight_based_dose(…)",
          "url": "http://www.functionalweave.com/functions/health.dose-weight-based"
        },
        {
          "id": "health.gestational-age",
          "tier": "optional",
          "why": "Gestational age and estimated due date from the last menstrual period (Naegele's rule).",
          "section": "calculators",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "Gestational age (weeks+days) and estimated due date from the last menstrual period by Naegele's rule, cycle-adjusted.",
          "status": "unreviewed",
          "statusNote": "not for clinical use; regulatory review pending",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "gestationalAge"
          ],
          "signatures": [
            "pub fn gestational_age(lmp_date: &str, on_date: &str, cycle_length_days: i64) -> GestationalAge"
          ],
          "reference": "fune!(health.gestational-age@^1);  // then call gestational_age(…)",
          "url": "http://www.functionalweave.com/functions/health.gestational-age"
        },
        {
          "id": "health.qrisk-inputs-validate",
          "tier": "optional",
          "why": "Checks a QRISK3 input set against the calculator's published ranges; it does not compute the risk.",
          "section": "calculators",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "Check a QRISK3 cardiovascular risk input set against the calculator's published ranges; does not compute risk.",
          "status": "unreviewed",
          "statusNote": "not for clinical use; regulatory review pending",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateQriskInputs"
          ],
          "signatures": [
            "pub fn validate_qrisk_inputs(inputs: &QriskInputs) -> QriskInputsValidation"
          ],
          "reference": "fune!(health.qrisk-inputs-validate@^1);  // then call validate_qrisk_inputs(…)",
          "url": "http://www.functionalweave.com/functions/health.qrisk-inputs-validate"
        }
      ],
      "gaps": [
        {
          "key": "qrisk-score",
          "text": "The QRISK3 cardiovascular risk itself: use the licensed calculator; Functional Weave only checks its inputs.",
          "section": "calculators",
          "from": "health-clinic"
        },
        {
          "key": "prescribing",
          "text": "Drug interactions, formularies and e-prescribing (EPS): not in caps.",
          "section": "calculators",
          "from": "health-clinic"
        }
      ]
    },
    {
      "key": "billing",
      "title": "Billing (private practice)",
      "purpose": "Charge for private appointments.",
      "from": "health-clinic",
      "members": [
        {
          "id": "finance.invoice.calculate",
          "tier": "optional",
          "why": "Totals an invoice for a private appointment, with VAT per line where it applies.",
          "section": "billing",
          "from": "health-clinic",
          "version": "1.0.1",
          "summary": "Calculate a complete invoice: line nets, VAT per line, totals and a VAT summary grouped by rate.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "calculateInvoice"
          ],
          "signatures": [
            "pub fn calculate_invoice(lines: &[InvoiceLine], jurisdiction: &str, invoice_date: &str) -> Invoice"
          ],
          "reference": "fune!(finance.invoice.calculate@^1);  // then call calculate_invoice(…)",
          "url": "http://www.functionalweave.com/functions/finance.invoice.calculate"
        },
        {
          "id": "money.format",
          "tier": "optional",
          "why": "Shows fees with the currency's real decimal places.",
          "section": "billing",
          "from": "health-clinic",
          "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": [
            "pub fn format_money(amount: &Money) -> String"
          ],
          "reference": "fune!(money.format@^1);  // then call format_money(…)",
          "url": "http://www.functionalweave.com/functions/money.format"
        }
      ],
      "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": [
            "pub fn add_days(iso: &str, days: i64) -> String"
          ],
          "reference": "fune!(dates.add-days@^1);  // then call add_days(…)",
          "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": [
            "pub fn add_months(iso: &str, months: i64) -> String"
          ],
          "reference": "fune!(dates.add-months@^1);  // then call add_months(…)",
          "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": [
            "pub fn days_between(start_iso: &str, end_iso: &str) -> i64"
          ],
          "reference": "fune!(dates.days-between@^1);  // then call days_between(…)",
          "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": [
            "pub fn month_boundaries(iso: &str) -> MonthBoundaries"
          ],
          "reference": "fune!(dates.month-boundaries@^1);  // then call month_boundaries(…)",
          "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": [
            "pub fn day_of_week(iso: &str) -> i64"
          ],
          "reference": "fune!(dates.day-of-week@^1);  // then call day_of_week(…)",
          "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": [
            "pub fn is_leap_year(year: i64) -> bool"
          ],
          "reference": "fune!(dates.is-leap-year@^1);  // then call is_leap_year(…)",
          "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": [
            "pub fn overlap_days(a_start: &str, a_end: &str, b_start: &str, b_end: &str, end_inclusive: bool) -> i64"
          ],
          "reference": "fune!(dates.overlap@^1);  // then call overlap_days(…)",
          "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": [
            "pub fn quarter(iso: &str, fiscal_year_start_month: i64) -> Quarter"
          ],
          "reference": "fune!(dates.quarter@^1);  // then call quarter(…)",
          "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": [
            "pub fn week_number(iso: &str) -> IsoWeek"
          ],
          "reference": "fune!(dates.week-number@^1);  // then call week_number(…)",
          "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": [
            "pub fn day_count_fraction(start_iso: &str, end_iso: &str, convention: &str) -> YearFraction"
          ],
          "reference": "fune!(dates.day-count-fraction@^1);  // then call day_count_fraction(…)",
          "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": [
            "pub fn bank_holidays(region: &str, year: i64) -> Vec<BankHoliday>"
          ],
          "reference": "fune!(dates.bank-holidays@^1);  // then call bank_holidays(…)",
          "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": [
            "pub fn add_business_days(iso: &str, days: i64, holidays: &[String]) -> String"
          ],
          "reference": "fune!(dates.add-business-days@^1);  // then call add_business_days(…)",
          "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": [
            "pub fn business_days_between(start_iso: &str, end_iso: &str, holidays: &[String]) -> i64"
          ],
          "reference": "fune!(dates.business-days-between@^1);  // then call business_days_between(…)",
          "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": [
            "pub fn recurrence(rule: &RecurrenceRule, start_iso: &str, count: i64) -> Vec<String>"
          ],
          "reference": "fune!(dates.recurrence@^1);  // then call recurrence(…)",
          "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": [
            "pub fn duration(parts: &[String]) -> Duration"
          ],
          "reference": "fune!(time.duration@^1);  // then call duration(…)",
          "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": [
            "pub fn minutes_between(start_date: &str, start_time: &str, end_date: &str, end_time: &str) -> i64"
          ],
          "reference": "fune!(time.minutes-between@^1);  // then call minutes_between(…)",
          "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": [
            "pub fn round_to_increment(minutes: i64, increment: i64, mode: &RoundingMode) -> i64"
          ],
          "reference": "fune!(time.round-to-increment@^1);  // then call round_to_increment(…)",
          "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": [
            "pub fn iso_to_unix(timestamp: &str) -> i64"
          ],
          "reference": "fune!(time.iso-to-unix@^1);  // then call iso_to_unix(…)",
          "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": [
            "pub fn unix_to_iso(seconds: i64) -> String"
          ],
          "reference": "fune!(time.unix-to-iso@^1);  // then call unix_to_iso(…)",
          "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": [
            "pub fn countdown(until: i64, now: i64) -> Countdown"
          ],
          "reference": "fune!(time.countdown@^1);  // then call countdown(…)",
          "url": "http://www.functionalweave.com/functions/time.countdown"
        }
      ],
      "gaps": []
    },
    {
      "key": "contact",
      "title": "Contact details",
      "purpose": "Check and normalise a person's contact details before storing them.",
      "from": "validation-basics",
      "members": [
        {
          "id": "validation.email",
          "tier": "core",
          "why": "Is an email address plausible, by a documented subset of RFC 5322.",
          "section": "contact",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Is this a plausible email address? A documented, pragmatic subset of RFC 5322, not the full grammar.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isEmail"
          ],
          "signatures": [
            "pub fn is_email(value: &str) -> bool"
          ],
          "reference": "fune!(validation.email@^1);  // then call is_email(…)",
          "url": "http://www.functionalweave.com/functions/validation.email"
        },
        {
          "id": "validation.phone-e164",
          "tier": "core",
          "why": "Normalises a phone number to +44… E.164 for a default country.",
          "section": "contact",
          "from": "validation-basics",
          "version": "2.0.0",
          "summary": "Normalise a phone number to E.164 (+447700900123) using a default country's trunk and dialling prefixes.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validatePhoneE164"
          ],
          "signatures": [
            "pub fn validate_phone_e164(value: &str, default_country: &str) -> PhoneE164"
          ],
          "reference": "fune!(validation.phone-e164@^2);  // then call validate_phone_e164(…)",
          "url": "http://www.functionalweave.com/functions/validation.phone-e164"
        },
        {
          "id": "validation.uk-postcode",
          "tier": "core",
          "why": "Validates a UK postcode and puts it in canonical form.",
          "section": "contact",
          "from": "validation-basics",
          "version": "2.0.0",
          "summary": "Validate a UK postcode and normalise it to canonical upper case with one space before the inward code.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkPostcode"
          ],
          "signatures": [
            "pub fn validate_uk_postcode(value: &str) -> UkPostcode"
          ],
          "reference": "fune!(validation.uk-postcode@^2);  // then call validate_uk_postcode(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-postcode"
        },
        {
          "id": "text.normalise-name",
          "tier": "optional",
          "why": "Tidies a typed name: spacing and title case with Mc, O' and hyphen rules.",
          "section": "contact",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Tidy a personal name: trim, collapse whitespace and title-case, with Mc, Mac, O', hyphen and van/de rules.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "normaliseName"
          ],
          "signatures": [
            "pub fn normalise_name(value: &str) -> String"
          ],
          "reference": "fune!(text.normalise-name@^1);  // then call normalise_name(…)",
          "url": "http://www.functionalweave.com/functions/text.normalise-name"
        },
        {
          "id": "auth.normalise-email",
          "tier": "optional",
          "why": "Lower-cases an email's domain for storage and lookup.",
          "section": "contact",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Trim an email address and lower-case its domain for storage and lookup, or null if it is not a plausible address.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "normaliseEmail"
          ],
          "signatures": [
            "pub fn normalise_email(value: &str) -> Option<String>"
          ],
          "reference": "fune!(auth.normalise-email@^1);  // then call normalise_email(…)",
          "url": "http://www.functionalweave.com/functions/auth.normalise-email"
        }
      ],
      "gaps": []
    },
    {
      "key": "bank",
      "title": "Bank details",
      "purpose": "Check payee and customer bank details before money moves.",
      "from": "validation-basics",
      "members": [
        {
          "id": "validation.iban",
          "tier": "core",
          "why": "Checks an IBAN's mod-97 checksum and its country's length.",
          "section": "bank",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check an IBAN against the ISO 13616 mod-97-10 checksum and the registered length for its country.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isIban"
          ],
          "signatures": [
            "pub fn is_iban(value: &str) -> bool"
          ],
          "reference": "fune!(validation.iban@^1);  // then call is_iban(…)",
          "url": "http://www.functionalweave.com/functions/validation.iban"
        },
        {
          "id": "validation.bic",
          "tier": "optional",
          "why": "Checks a SWIFT/BIC code's format and splits it.",
          "section": "bank",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check the format of a SWIFT/BIC code (8 or 11 characters) and split it into its parts.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateBic"
          ],
          "signatures": [
            "pub fn validate_bic(value: &str) -> BicCheck"
          ],
          "reference": "fune!(validation.bic@^1);  // then call validate_bic(…)",
          "url": "http://www.functionalweave.com/functions/validation.bic"
        },
        {
          "id": "validation.uk-sort-code-account",
          "tier": "optional",
          "why": "Checks a UK sort code and account number by the Vocalink modulus rules, against a table you supply.",
          "section": "bank",
          "from": "validation-basics",
          "version": "3.0.0",
          "summary": "Check a UK sort code and account number with the Vocalink/Pay.UK modulus rules, against a table you supply.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkSortCodeAccount"
          ],
          "signatures": [
            "pub fn validate_uk_sort_code_account(sort_code: &str, account_number: &str, table: &UkModulusTable) -> UkSortCodeAccount"
          ],
          "reference": "fune!(validation.uk-sort-code-account@^3);  // then call validate_uk_sort_code_account(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-sort-code-account"
        },
        {
          "id": "validation.uk-modulus-table",
          "tier": "optional",
          "why": "Parses Vocalink's VALACDOS and SCSUBTAB files into that table.",
          "section": "bank",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Parse Vocalink's VALACDOS.txt and SCSUBTAB.txt into the table UK sort code modulus checking needs.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "parseUkModulusTable"
          ],
          "signatures": [
            "pub fn parse_uk_modulus_table(valacdos: &str, scsubtab: &str) -> UkModulusTable"
          ],
          "reference": "fune!(validation.uk-modulus-table@^1);  // then call parse_uk_modulus_table(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-modulus-table"
        }
      ],
      "gaps": []
    },
    {
      "key": "check-digits",
      "title": "Identifiers and check digits",
      "purpose": "Catch mistyped reference numbers by their check digits.",
      "from": "validation-basics",
      "members": [
        {
          "id": "validation.luhn",
          "tier": "optional",
          "why": "Luhn mod-10 check, for card numbers and many reference numbers.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check a digit string against the Luhn mod-10 checksum used by payment cards and many identifiers.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isLuhn"
          ],
          "signatures": [
            "pub fn is_luhn(value: &str) -> bool"
          ],
          "reference": "fune!(validation.luhn@^1);  // then call is_luhn(…)",
          "url": "http://www.functionalweave.com/functions/validation.luhn"
        },
        {
          "id": "validation.uk-company-number",
          "tier": "optional",
          "why": "Checks a Companies House number's format and prefix.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "2.0.1",
          "summary": "Check a Companies House company number's format and prefix (SC, NI, OC, LP...) and zero-pad it to eight characters.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkCompanyNumber"
          ],
          "signatures": [
            "pub fn validate_uk_company_number(value: &str) -> UkCompanyNumber"
          ],
          "reference": "fune!(validation.uk-company-number@^2);  // then call validate_uk_company_number(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-company-number"
        },
        {
          "id": "validation.lei",
          "tier": "optional",
          "why": "Checks a Legal Entity Identifier's check digits.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check a 20-character Legal Entity Identifier against its ISO 17442 / ISO 7064 MOD 97-10 check digits.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateLei"
          ],
          "signatures": [
            "pub fn validate_lei(value: &str) -> LeiCheck"
          ],
          "reference": "fune!(validation.lei@^1);  // then call validate_lei(…)",
          "url": "http://www.functionalweave.com/functions/validation.lei"
        },
        {
          "id": "validation.gtin",
          "tier": "optional",
          "why": "Checks an EAN/UPC/GTIN barcode number.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "1.1.0",
          "summary": "Check an EAN-8, UPC-A, EAN-13 or GTIN-14 barcode number against the GS1 mod-10 check digit.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateGtin"
          ],
          "signatures": [
            "pub fn validate_gtin(value: &str) -> GtinCheck"
          ],
          "reference": "fune!(validation.gtin@^1);  // then call validate_gtin(…)",
          "url": "http://www.functionalweave.com/functions/validation.gtin"
        }
      ],
      "gaps": []
    },
    {
      "key": "masking",
      "title": "Displaying sensitive values",
      "purpose": "Show stored identifiers without exposing them.",
      "from": "validation-basics",
      "members": [
        {
          "id": "text.mask",
          "tier": "optional",
          "why": "Masks all but the last digits of a card, account or phone number.",
          "section": "masking",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Mask all but the last n characters of a card, account or phone number, optionally keeping separators.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "mask"
          ],
          "signatures": [
            "pub fn mask(value: &str, visible: i64, mask_char: &str, keep_separators: bool) -> String"
          ],
          "reference": "fune!(text.mask@^1);  // then call mask(…)",
          "url": "http://www.functionalweave.com/functions/text.mask"
        }
      ],
      "gaps": []
    }
  ],
  "gaps": [
    {
      "key": "appointment-reminders",
      "text": "Reminder texts and emails, and handling did-not-attend and cancellations.",
      "section": "appointments",
      "from": "health-clinic"
    },
    {
      "key": "clinician-calendars",
      "text": "Clinicians' rotas, rooms and leave that make the sessions the slots are cut from.",
      "section": "appointments",
      "from": "health-clinic"
    },
    {
      "key": "patient-lookup",
      "text": "Looking a patient up on the NHS Spine (PDS) or another master index is network work and not in caps.",
      "section": "patients",
      "from": "health-clinic"
    },
    {
      "key": "clinical-records",
      "text": "Clinical notes, coding (SNOMED CT), consent and the audit trail of who saw what.",
      "section": "patients",
      "from": "health-clinic"
    },
    {
      "key": "qrisk-score",
      "text": "The QRISK3 cardiovascular risk itself: use the licensed calculator; Functional Weave only checks its inputs.",
      "section": "calculators",
      "from": "health-clinic"
    },
    {
      "key": "prescribing",
      "text": "Drug interactions, formularies and e-prescribing (EPS): not in caps.",
      "section": "calculators",
      "from": "health-clinic"
    },
    {
      "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"
    },
    {
      "key": "address-lookup",
      "text": "Turning a postcode into a list of addresses (Royal Mail PAF or a lookup API) is network work and not in caps.",
      "section": null,
      "from": "validation-basics"
    },
    {
      "key": "email-deliverability",
      "text": "Whether a mailbox exists (MX lookups, a confirmation email): validation.email checks the syntax only.",
      "section": null,
      "from": "validation-basics"
    },
    {
      "key": "vocalink-data",
      "text": "The Vocalink modulus tables themselves: download them from Pay.UK under its terms; caps parses them but does not ship them.",
      "section": null,
      "from": "validation-basics"
    }
  ],
  "counts": {
    "members": 49,
    "core": 18,
    "optional": 31,
    "unreviewed": 11,
    "deprecated": [],
    "unavailable": [],
    "missing": [],
    "gaps": 12
  },
  "warnings": [
    "3 core capabilities are unreviewed - they pass their tests but await an expert's sign-off; tell the user before relying on them, and never use one silently for tax, payroll, legal, clinical or other regulated rules: health.appointment-slots, health.nhs-number-format, health.bmi",
    "8 optional capabilities are unreviewed too: health.waiting-time, health.age-in-months, health.bsa, health.egfr, health.news2, health.dose-weight-based, health.gestational-age, health.qrisk-inputs-validate",
    "Not a medical device and not clinical advice. Every clinical calculator here needs sign-off by a qualified clinician before it is used for real patients, and software that informs diagnosis or treatment may need UKCA/CE marking as a medical device (MHRA).",
    "UK first: NHS numbers, NHS England RTT waits and NEWS2 follow UK rules.",
    "Patient data is special category data under UK GDPR: storage, access control and audit are yours."
  ],
  "url": "http://www.functionalweave.com/suites/health-clinic",
  "owner": null
}
