{
  "id": "todo",
  "title": "Todo and task lists",
  "summary": "A to-do list's logic as pure functions: the item type, a list reducer, quick-add parsing, due dates, repeats, tags, filters, sorting and CSV import/export.",
  "synonyms": [
    "todo",
    "to do",
    "to-do list",
    "tasks",
    "task list",
    "task manager",
    "checklist",
    "reminders",
    "productivity",
    "getting things done"
  ],
  "apps": [
    "todo app",
    "task manager",
    "to-do list app",
    "productivity app",
    "personal task tracker",
    "reminders app"
  ],
  "language": "typescript",
  "includes": [
    "dates-basics"
  ],
  "includedBy": [],
  "caveats": [],
  "sections": [
    {
      "key": "items",
      "title": "Items and the list",
      "purpose": "The todo type and every change to a list as a pure reducer.",
      "from": "todo",
      "members": [
        {
          "id": "todo.item",
          "tier": "core",
          "why": "The Todo, TodoDraft and Recurrence types, with validators giving a message per field.",
          "section": "items",
          "from": "todo",
          "version": "1.0.0",
          "summary": "The to-do item type (Todo, TodoDraft, Recurrence) and its validators, with a message per field.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "validateDraft",
            "validateTodo"
          ],
          "signatures": [
            "export function validateDraft(draft: TodoDraft): TodoValidation",
            "export function validateTodo(todo: Todo): TodoValidation"
          ],
          "reference": "import { validateDraft, validateTodo } from \"#fune/todo.item@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.item"
        },
        {
          "id": "todo.list",
          "tier": "core",
          "why": "Adds, edits, completes, removes, clears and reorders todos, each returning a new list.",
          "section": "items",
          "from": "todo",
          "version": "1.1.0",
          "summary": "The to-do list reducer: add, edit, complete, remove, clear and reorder todos, each returning a new list.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "addTodo",
            "updateTodo",
            "toggleTodo",
            "removeTodo",
            "clearCompleted",
            "toggleAll",
            "moveTodo",
            "toggleAllMode"
          ],
          "signatures": [
            "export function addTodo(todos: readonly Todo[], draft: TodoDraft, id: string, now: string): readonly Todo[]",
            "export function updateTodo(todos: readonly Todo[], id: string, draft: TodoDraft): readonly Todo[]",
            "export function toggleTodo(todos: readonly Todo[], id: string, now: string, today: string): readonly Todo[]",
            "export function removeTodo(todos: readonly Todo[], id: string): readonly Todo[]",
            "export function clearCompleted(todos: readonly Todo[]): readonly Todo[]",
            "export function toggleAll(todos: readonly Todo[], now: string, today: string): readonly Todo[]",
            "export function moveTodo(todos: readonly Todo[], id: string, toIndex: number): readonly Todo[]",
            "export function toggleAllMode(todos: readonly Todo[]): ToggleAllMode"
          ],
          "reference": "import { addTodo, updateTodo, toggleTodo, removeTodo, clearCompleted, toggleAll, moveTodo, toggleAllMode } from \"#fune/todo.list@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.list"
        }
      ],
      "gaps": []
    },
    {
      "key": "entry",
      "title": "Quick entry",
      "purpose": "Turn what people type into todos.",
      "from": "todo",
      "members": [
        {
          "id": "todo.parse-quick-add",
          "tier": "core",
          "why": "Turns \"Pay rent tomorrow !high",
          "section": "entry",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Turn one typed line like \"Pay rent tomorrow !high #home every month\" into a todo draft.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "parseQuickAdd"
          ],
          "signatures": [
            "export function parseQuickAdd(text: string, today: string): TodoDraft"
          ],
          "reference": "import { parseQuickAdd } from \"#fune/todo.parse-quick-add@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.parse-quick-add"
        },
        {
          "id": "todo.parse-date-phrase",
          "tier": "optional",
          "why": "Reads \"tomorrow\", \"next friday\", \"in 2 weeks\" as a date relative to today.",
          "section": "entry",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Read a typed due date like \"tomorrow\", \"next friday\", \"in 2 weeks\" or \"2026-10-01\" as a date, relative to today.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "parseDatePhrase"
          ],
          "signatures": [
            "export function parseDatePhrase(text: string, today: string): string | null"
          ],
          "reference": "import { parseDatePhrase } from \"#fune/todo.parse-date-phrase@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.parse-date-phrase"
        },
        {
          "id": "todo.parse-tags",
          "tier": "optional",
          "why": "Reads a tags box (\"#Home, work\") into normalised tags.",
          "section": "entry",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Read a tags box as typed, \"#Home, work  errands\", into normalised tags: split on spaces and commas.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "parseTags"
          ],
          "signatures": [
            "export function parseTags(text: string): readonly string[]"
          ],
          "reference": "import { parseTags } from \"#fune/todo.parse-tags@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.parse-tags"
        },
        {
          "id": "todo.normalise-tags",
          "tier": "optional",
          "why": "Cleans tags: trims, drops",
          "section": "entry",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Clean up tags as typed: trim, drop the #, lower-case slug form, drop empties and duplicates, keep the order.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "normaliseTags"
          ],
          "signatures": [
            "export function normaliseTags(tags: readonly string[]): readonly string[]"
          ],
          "reference": "import { normaliseTags } from \"#fune/todo.normalise-tags@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.normalise-tags"
        },
        {
          "id": "todo.parse-repeat",
          "tier": "optional",
          "why": "Reads a Repeat box such as \"every 2 weeks\" as a recurrence.",
          "section": "entry",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Read a Repeat box, \"every 2 weeks\" or \"every monday on 2026-10-05\", as a todo's recurrence, or null.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "parseRepeat"
          ],
          "signatures": [
            "export function parseRepeat(text: string, due: string | null, today: string): Recurrence | null"
          ],
          "reference": "import { parseRepeat } from \"#fune/todo.parse-repeat@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.parse-repeat"
        }
      ],
      "gaps": []
    },
    {
      "key": "due",
      "title": "Due dates and repeats",
      "purpose": "Say where each todo stands against its date, and roll repeating ones forward.",
      "from": "todo",
      "members": [
        {
          "id": "todo.due-status",
          "tier": "core",
          "why": "Done, overdue, today, tomorrow, this week, later or no date, for a todo today.",
          "section": "due",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Where a todo stands against its due date today: done, overdue, today, tomorrow, this week, later or no date.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "dueStatus"
          ],
          "signatures": [
            "export function dueStatus(todo: Todo, today: string): DueStatus"
          ],
          "reference": "import { dueStatus } from \"#fune/todo.due-status@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.due-status"
        },
        {
          "id": "todo.due-label",
          "tier": "core",
          "why": "The short label beside a todo: \"2 days overdue\", \"Due tomorrow\".",
          "section": "due",
          "from": "todo",
          "version": "1.0.0",
          "summary": "The short English label shown next to a todo: \"2 days overdue\", \"Due today\", \"Due tomorrow\", \"Due Fri 2 Oct\".",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "dueLabel"
          ],
          "signatures": [
            "export function dueLabel(todo: Todo, today: string): string"
          ],
          "reference": "import { dueLabel } from \"#fune/todo.due-label@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.due-label"
        },
        {
          "id": "todo.next-occurrence",
          "tier": "optional",
          "why": "The next due date of a repeating todo once done, month-end safe.",
          "section": "due",
          "from": "todo",
          "version": "1.0.0",
          "summary": "The next due date of a repeating todo once it is done: daily, weekdays, weekly, monthly or yearly, month-end safe.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "nextOccurrence"
          ],
          "signatures": [
            "export function nextOccurrence(recurrence: Recurrence, due: string, today: string): string"
          ],
          "reference": "import { nextOccurrence } from \"#fune/todo.next-occurrence@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.next-occurrence"
        },
        {
          "id": "todo.reschedule-recurrence",
          "tier": "optional",
          "why": "Moves a repeating todo's series when its due date is changed by hand.",
          "section": "due",
          "from": "todo",
          "version": "1.0.0",
          "summary": "When a repeating todo's due date is moved by hand, move its series with it: the new date becomes the anchor.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "rescheduleRecurrence"
          ],
          "signatures": [
            "export function rescheduleRecurrence(recurrence: Recurrence | null, fromDue: string | null, toDue: string | null): Recurrence | null"
          ],
          "reference": "import { rescheduleRecurrence } from \"#fune/todo.reschedule-recurrence@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.reschedule-recurrence"
        },
        {
          "id": "todo.recurrence-label",
          "tier": "optional",
          "why": "Text for a repeat chip: \"Every weekday\", \"Every month on the 31st\".",
          "section": "due",
          "from": "todo",
          "version": "1.0.0",
          "summary": "English text for a repeating todo's chip: \"Every weekday\", \"Every 2 weeks on Monday\", \"Every month on the 31st\".",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "recurrenceLabel"
          ],
          "signatures": [
            "export function recurrenceLabel(recurrence: Recurrence): string"
          ],
          "reference": "import { recurrenceLabel } from \"#fune/todo.recurrence-label@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.recurrence-label"
        }
      ],
      "gaps": []
    },
    {
      "key": "views",
      "title": "Views",
      "purpose": "The list as people see it: filtered, sorted, grouped and counted.",
      "from": "todo",
      "members": [
        {
          "id": "todo.filter",
          "tier": "core",
          "why": "All, active or completed, one tag and a search box.",
          "section": "views",
          "from": "todo",
          "version": "1.0.0",
          "summary": "The todos a list view shows: all, active or completed, optionally one tag, and a search box, in input order.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "filterTodos"
          ],
          "signatures": [
            "export function filterTodos(todos: readonly Todo[], filter: TodoFilter): readonly Todo[]"
          ],
          "reference": "import { filterTodos } from \"#fune/todo.filter@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.filter"
        },
        {
          "id": "todo.sort",
          "tier": "core",
          "why": "Sorts by manual order, due date, priority, creation time or title, stably.",
          "section": "views",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Sort todos by manual order, due date, priority, creation time or title, stably, with sensible tie-breakers.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "sortTodos"
          ],
          "signatures": [
            "export function sortTodos(todos: readonly Todo[], by: TodoSortKey, direction: SortDirection): readonly Todo[]"
          ],
          "reference": "import { sortTodos } from \"#fune/todo.sort@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.sort"
        },
        {
          "id": "todo.group-by-due",
          "tier": "optional",
          "why": "Overdue, Today, Tomorrow, This week, Later, No date and Done sections.",
          "section": "views",
          "from": "todo",
          "version": "1.1.0",
          "summary": "Split todos into Overdue, Today, Tomorrow, This week, Later, No date and Done sections, in that order.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "groupByDue",
            "groupByDueWithOrder"
          ],
          "signatures": [
            "export function groupByDue(todos: readonly Todo[], today: string): readonly DueGroup[]",
            "export function groupByDueWithOrder(todos: readonly Todo[], today: string, order: SectionOrder): readonly DueGroup[]"
          ],
          "reference": "import { groupByDue, groupByDueWithOrder } from \"#fune/todo.group-by-due@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.group-by-due"
        },
        {
          "id": "todo.summary",
          "tier": "optional",
          "why": "Counts for the header or footer: \"3 items left\", percent done, overdue.",
          "section": "views",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Counts for a todo list's header or footer: total, active, done, overdue, due today, percent done and \"3 items left\".",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "summariseTodos"
          ],
          "signatures": [
            "export function summariseTodos(todos: readonly Todo[], today: string): TodoSummary"
          ],
          "reference": "import { summariseTodos } from \"#fune/todo.summary@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.summary"
        },
        {
          "id": "todo.tag-counts",
          "tier": "optional",
          "why": "How many todos carry each tag, for a tag sidebar.",
          "section": "views",
          "from": "todo",
          "version": "1.0.0",
          "summary": "How many todos carry each tag, for a tag sidebar: most used first, open todos only unless asked.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "tagCounts"
          ],
          "signatures": [
            "export function tagCounts(todos: readonly Todo[], includeDone: boolean): readonly TagCount[]"
          ],
          "reference": "import { tagCounts } from \"#fune/todo.tag-counts@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.tag-counts"
        }
      ],
      "gaps": []
    },
    {
      "key": "import-export",
      "title": "Import and export",
      "purpose": "Move todos in and out as CSV.",
      "from": "todo",
      "members": [
        {
          "id": "todo.export-csv",
          "tier": "optional",
          "why": "Writes todos as an RFC 4180 CSV file for backups and spreadsheets.",
          "section": "import-export",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Write todos as a CSV file (RFC 4180) with one column per field, for backups, spreadsheets and other apps.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "exportCsv"
          ],
          "signatures": [
            "export function exportCsv(todos: readonly Todo[]): string"
          ],
          "reference": "import { exportCsv } from \"#fune/todo.export-csv@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.export-csv"
        },
        {
          "id": "todo.import-csv",
          "tier": "optional",
          "why": "Reads that CSV back, checking every row with row-numbered errors.",
          "section": "import-export",
          "from": "todo",
          "version": "1.1.0",
          "summary": "Read todos from a CSV file (RFC 4180) as todo.export-csv writes it, checking every row, with row-numbered errors.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "importCsv",
            "validateTodoCsv"
          ],
          "signatures": [
            "export function importCsv(csv: string): readonly Todo[]",
            "export function validateTodoCsv(csv: string): CsvValidation"
          ],
          "reference": "import { importCsv, validateTodoCsv } from \"#fune/todo.import-csv@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.import-csv"
        },
        {
          "id": "todo.merge",
          "tier": "optional",
          "why": "Merges imported todos into a list by id.",
          "section": "import-export",
          "from": "todo",
          "version": "1.0.0",
          "summary": "Merge imported todos into a list by id: an imported todo replaces the one with its id, new ids are appended.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "mergeTodos"
          ],
          "signatures": [
            "export function mergeTodos(current: readonly Todo[], incoming: readonly Todo[]): readonly Todo[]"
          ],
          "reference": "import { mergeTodos } from \"#fune/todo.merge@^1\";",
          "url": "http://www.functionalweave.com/functions/todo.merge"
        }
      ],
      "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": [
            "export function addDays(iso: string, days: number): string"
          ],
          "reference": "import { addDays } from \"#fune/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": [
            "export function addMonths(iso: string, months: number): string"
          ],
          "reference": "import { addMonths } from \"#fune/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": [
            "export function daysBetween(startIso: string, endIso: string): number"
          ],
          "reference": "import { daysBetween } from \"#fune/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": [
            "export function monthBoundaries(iso: string): MonthBoundaries"
          ],
          "reference": "import { monthBoundaries } from \"#fune/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": [
            "export function dayOfWeek(iso: string): number"
          ],
          "reference": "import { dayOfWeek } from \"#fune/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": [
            "export function isLeapYear(year: number): boolean"
          ],
          "reference": "import { isLeapYear } from \"#fune/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": [
            "export function overlapDays(aStart: string, aEnd: string, bStart: string, bEnd: string, endInclusive: boolean): number"
          ],
          "reference": "import { overlapDays } from \"#fune/dates.overlap@^1\";",
          "url": "http://www.functionalweave.com/functions/dates.overlap"
        },
        {
          "id": "dates.age",
          "tier": "optional",
          "why": "Age in whole years on a date, handling 29 February birthdays.",
          "section": "date-arithmetic",
          "from": "dates-basics",
          "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": [
            "export function age(birthDate: string, onDate: string): number"
          ],
          "reference": "import { age } from \"#fune/dates.age@^1\";",
          "url": "http://www.functionalweave.com/functions/dates.age"
        }
      ],
      "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": [
            "export function quarter(iso: string, fiscalYearStartMonth: number): Quarter"
          ],
          "reference": "import { quarter } from \"#fune/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": [
            "export function weekNumber(iso: string): IsoWeek"
          ],
          "reference": "import { weekNumber } from \"#fune/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": [
            "export function dayCountFraction(startIso: string, endIso: string, convention: DayCountConvention): YearFraction"
          ],
          "reference": "import { dayCountFraction } from \"#fune/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": [
            "export function bankHolidays(region: UkRegion, year: number): readonly BankHoliday[]"
          ],
          "reference": "import { bankHolidays } from \"#fune/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": [
            "export function addBusinessDays(iso: string, days: number, holidays: readonly string[]): string"
          ],
          "reference": "import { addBusinessDays } from \"#fune/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": [
            "export function businessDaysBetween(startIso: string, endIso: string, holidays: readonly string[]): number"
          ],
          "reference": "import { businessDaysBetween } from \"#fune/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": [
            "export function recurrence(rule: RecurrenceRule, startIso: string, count: number): readonly string[]"
          ],
          "reference": "import { recurrence } from \"#fune/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": [
            "export function duration(parts: readonly string[]): Duration"
          ],
          "reference": "import { duration } from \"#fune/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": [
            "export function minutesBetween(startDate: string, startTime: string, endDate: string, endTime: string): number"
          ],
          "reference": "import { minutesBetween } from \"#fune/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": [
            "export function roundToIncrement(minutes: number, increment: number, mode: RoundingMode): number"
          ],
          "reference": "import { roundToIncrement } from \"#fune/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": [
            "export function isoToUnix(timestamp: string): number"
          ],
          "reference": "import { isoToUnix } from \"#fune/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": [
            "export function unixToIso(seconds: number): string"
          ],
          "reference": "import { unixToIso } from \"#fune/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": [
            "export function countdown(until: number, now: number): Countdown"
          ],
          "reference": "import { countdown } from \"#fune/time.countdown@^1\";",
          "url": "http://www.functionalweave.com/functions/time.countdown"
        }
      ],
      "gaps": []
    }
  ],
  "gaps": [
    {
      "key": "persistence",
      "text": "Storing todos (localStorage, a database) and loading them: caps works on lists in memory.",
      "section": null,
      "from": "todo"
    },
    {
      "key": "sync",
      "text": "Syncing between devices and resolving edits made offline on two of them.",
      "section": null,
      "from": "todo"
    },
    {
      "key": "notifications",
      "text": "Reminders that fire at a time: push, email or OS notifications and their scheduler.",
      "section": null,
      "from": "todo"
    },
    {
      "key": "sharing",
      "text": "Shared lists, assignees and permissions between users.",
      "section": null,
      "from": "todo"
    },
    {
      "key": "ui",
      "text": "The list UI itself: Functional Weave has no todo components, only the logic and React form fields.",
      "section": null,
      "from": "todo"
    },
    {
      "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": 41,
    "core": 16,
    "optional": 25,
    "unreviewed": 0,
    "deprecated": [],
    "unavailable": [],
    "missing": [],
    "gaps": 8
  },
  "warnings": [],
  "url": "http://www.functionalweave.com/suites/todo",
  "owner": null
}
