Functional Weave
Code in TypeScript

Suites · todo

Todo and task lists

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.

41 capabilities (16 core, 25 optional) and 8 gaps, in build order. Put each line in the file that calls it, then fune build.

1. Items and the list

The todo type and every change to a list as a pure reducer.

todo.item core
The Todo, TodoDraft and Recurrence types, with validators giving a message per field.
The to-do item type (Todo, TodoDraft, Recurrence) and its validators, with a message per field.
import { validateDraft, validateTodo } from "#fune/todo.item@^1";
todo.list core
Adds, edits, completes, removes, clears and reorders todos, each returning a new list.
The to-do list reducer: add, edit, complete, remove, clear and reorder todos, each returning a new list.
import { addTodo, updateTodo, toggleTodo, removeTodo, clearCompleted, toggleAll, moveTodo, toggleAllMode } from "#fune/todo.list@^1";

2. Quick entry

Turn what people type into todos.

todo.parse-quick-add core
Turns "Pay rent tomorrow !high
Turn one typed line like "Pay rent tomorrow !high #home every month" into a todo draft.
import { parseQuickAdd } from "#fune/todo.parse-quick-add@^1";
todo.parse-date-phrase optional
Reads "tomorrow", "next friday", "in 2 weeks" as a date relative to today.
Read a typed due date like "tomorrow", "next friday", "in 2 weeks" or "2026-10-01" as a date, relative to today.
import { parseDatePhrase } from "#fune/todo.parse-date-phrase@^1";
todo.parse-tags optional
Reads a tags box ("#Home, work") into normalised tags.
Read a tags box as typed, "#Home, work errands", into normalised tags: split on spaces and commas.
import { parseTags } from "#fune/todo.parse-tags@^1";
todo.normalise-tags optional
Cleans tags: trims, drops
Clean up tags as typed: trim, drop the #, lower-case slug form, drop empties and duplicates, keep the order.
import { normaliseTags } from "#fune/todo.normalise-tags@^1";
todo.parse-repeat optional
Reads a Repeat box such as "every 2 weeks" as a recurrence.
Read a Repeat box, "every 2 weeks" or "every monday on 2026-10-05", as a todo's recurrence, or null.
import { parseRepeat } from "#fune/todo.parse-repeat@^1";

3. Due dates and repeats

Say where each todo stands against its date, and roll repeating ones forward.

todo.due-status core
Done, overdue, today, tomorrow, this week, later or no date, for a todo today.
Where a todo stands against its due date today: done, overdue, today, tomorrow, this week, later or no date.
import { dueStatus } from "#fune/todo.due-status@^1";
todo.due-label core
The short label beside a todo: "2 days overdue", "Due tomorrow".
The short English label shown next to a todo: "2 days overdue", "Due today", "Due tomorrow", "Due Fri 2 Oct".
import { dueLabel } from "#fune/todo.due-label@^1";
todo.next-occurrence optional
The next due date of a repeating todo once done, month-end safe.
The next due date of a repeating todo once it is done: daily, weekdays, weekly, monthly or yearly, month-end safe.
import { nextOccurrence } from "#fune/todo.next-occurrence@^1";
todo.reschedule-recurrence optional
Moves a repeating todo's series when its due date is changed by hand.
When a repeating todo's due date is moved by hand, move its series with it: the new date becomes the anchor.
import { rescheduleRecurrence } from "#fune/todo.reschedule-recurrence@^1";
todo.recurrence-label optional
Text for a repeat chip: "Every weekday", "Every month on the 31st".
English text for a repeating todo's chip: "Every weekday", "Every 2 weeks on Monday", "Every month on the 31st".
import { recurrenceLabel } from "#fune/todo.recurrence-label@^1";

4. Views

The list as people see it: filtered, sorted, grouped and counted.

todo.filter core
All, active or completed, one tag and a search box.
The todos a list view shows: all, active or completed, optionally one tag, and a search box, in input order.
import { filterTodos } from "#fune/todo.filter@^1";
todo.sort core
Sorts by manual order, due date, priority, creation time or title, stably.
Sort todos by manual order, due date, priority, creation time or title, stably, with sensible tie-breakers.
import { sortTodos } from "#fune/todo.sort@^1";
todo.group-by-due optional
Overdue, Today, Tomorrow, This week, Later, No date and Done sections.
Split todos into Overdue, Today, Tomorrow, This week, Later, No date and Done sections, in that order.
import { groupByDue, groupByDueWithOrder } from "#fune/todo.group-by-due@^1";
todo.summary optional
Counts for the header or footer: "3 items left", percent done, overdue.
Counts for a todo list's header or footer: total, active, done, overdue, due today, percent done and "3 items left".
import { summariseTodos } from "#fune/todo.summary@^1";
todo.tag-counts optional
How many todos carry each tag, for a tag sidebar.
How many todos carry each tag, for a tag sidebar: most used first, open todos only unless asked.
import { tagCounts } from "#fune/todo.tag-counts@^1";

5. Import and export

Move todos in and out as CSV.

todo.export-csv optional
Writes todos as an RFC 4180 CSV file for backups and spreadsheets.
Write todos as a CSV file (RFC 4180) with one column per field, for backups, spreadsheets and other apps.
import { exportCsv } from "#fune/todo.export-csv@^1";
todo.import-csv optional
Reads that CSV back, checking every row with row-numbered errors.
Read todos from a CSV file (RFC 4180) as todo.export-csv writes it, checking every row, with row-numbered errors.
import { importCsv, validateTodoCsv } from "#fune/todo.import-csv@^1";
todo.merge optional
Merges imported todos into a list by id.
Merge imported todos into a list by id: an imported todo replaces the one with its id, new ids are appended.
import { mergeTodos } from "#fune/todo.merge@^1";

6. Calendar arithmetic from dates-basics

Move and measure ISO dates exactly, with no date library and no clock reads.

dates.add-days core
Shifts a date by whole days, for due dates and windows.
Shift an ISO date by a whole number of days, forwards or backwards, with exact calendar arithmetic.
import { addDays } from "#fune/dates.add-days@^1";
dates.add-months core
Adds months clamping to month end, so 31 January plus a month is the end of February.
Add calendar months to an ISO date, clamping to the month end: 31 Jan + 1 month is 28 or 29 Feb.
import { addMonths } from "#fune/dates.add-months@^1";
dates.days-between core
Signed calendar days between two dates, for ages of invoices and stays.
Calendar days from one ISO date to another, signed: negative when the second date is earlier.
import { daysBetween } from "#fune/dates.days-between@^1";
dates.month-boundaries core
First and last day of a date's month, for monthly periods and statements.
The first day, last day and length of the calendar month containing a date.
import { monthBoundaries } from "#fune/dates.month-boundaries@^1";
dates.day-of-week core
ISO weekday of a date, for weekend rules and opening hours.
The ISO 8601 day of the week for a date: 1 is Monday through 7 is Sunday.
import { dayOfWeek } from "#fune/dates.day-of-week@^1";
dates.is-leap-year optional
Whether a year is a leap year, for 365/366-day calculations.
Whether a year is a Gregorian leap year: every 4th, except centuries, except every 400th.
import { isLeapYear } from "#fune/dates.is-leap-year@^1";
dates.overlap optional
Days two date ranges share, for prorating and clash checks.
Days two date ranges have in common, with the end of each range stated as inclusive or exclusive.
import { overlapDays } from "#fune/dates.overlap@^1";
dates.age optional
Age in whole years on a date, handling 29 February birthdays.
Age in whole years on a given date, with 29 February birthdays ageing on 1 March in other years.
import { age } from "#fune/dates.age@^1";

7. Periods and reporting dates from dates-basics

Put dates into the periods reports are grouped by.

dates.quarter core
Calendar and fiscal quarter of a date for any year-start month.
Calendar quarter and fiscal quarter of a date, with the fiscal year and quarter date ranges, for any start month.
import { quarter } from "#fune/dates.quarter@^1";
dates.week-number optional
ISO week number and week-year, for weekly reports.
ISO 8601 week number and week-numbering year of a date: weeks start Monday, week 1 holds the first Thursday.
import { weekNumber } from "#fune/dates.week-number@^1";
dates.day-count-fraction optional
Year fraction by ACT/365F, ACT/360, 30/360 or ACT/ACT, for interest periods.
Year fraction between two dates by ACT/365F, ACT/360, 30/360, 30E/360 or ACT/ACT ISDA, as an exact fraction.
import { dayCountFraction } from "#fune/dates.day-count-fraction@^1";

8. Working days and holidays from dates-basics

Skip weekends and public holidays when counting or adding days.

dates.bank-holidays core
UK bank holidays for a region and year, as gov.uk publishes them.
UK bank and public holidays for a region and year, as published by gov.uk, including one-off holidays.
import { bankHolidays } from "#fune/dates.bank-holidays@^1";
dates.add-business-days core
Moves a date by working days, skipping weekends and a holiday list.
Move a date forward or back by working days, skipping weekends and a caller-supplied holiday list.
import { addBusinessDays } from "#fune/dates.add-business-days@^1";
dates.business-days-between core
Working days between two dates, for SLAs and notice periods.
Working days between two dates, excluding weekends and a caller-supplied holiday list.
import { businessDaysBetween } from "#fune/dates.business-days-between@^1";

9. Repeating schedules from dates-basics

Generate the dates of something that repeats.

dates.recurrence optional
The next n dates of a rule: monthly on the 31st, last Friday, every two weeks.
The next n dates of a repeating rule: monthly on a day, last weekday of the month, every k days or weeks, yearly.
import { recurrence } from "#fune/dates.recurrence@^1";

10. Times and durations from dates-basics

Work with times of day, durations and timestamps.

time.duration optional
Parses and adds durations like 1h30m in whole minutes, and formats the total.
Parse durations written as 1h30m, 90m or 01:30, add them up in whole minutes, and format the total.
import { duration } from "#fune/time.duration@^1";
time.minutes-between optional
Minutes between two local date-times, across midnight.
Minutes from one local date and time to another, correct across midnight; wall-clock time, no time zones.
import { minutesBetween } from "#fune/time.minutes-between@^1";
time.round-to-increment optional
Rounds minutes to a billing increment such as 6 or 15.
Round a duration in minutes to a billing increment such as 6 or 15 minutes, up, down or to nearest.
import { roundToIncrement } from "#fune/time.round-to-increment@^1";
time.iso-to-unix optional
RFC 3339 timestamps to Unix seconds.
An RFC 3339 timestamp with a Z or offset, such as 2026-09-26T13:00:00+01:00, to Unix time in whole seconds.
import { isoToUnix } from "#fune/time.iso-to-unix@^1";
time.unix-to-iso optional
Unix seconds to an ISO 8601 UTC timestamp.
Unix time in seconds to an ISO 8601 UTC timestamp such as 2026-09-26T12:00:00Z, for years 0001 to 9999.
import { unixToIso } from "#fune/time.unix-to-iso@^1";
time.countdown optional
Seconds left until a moment and a 4:05 style timer text.
Seconds left until a Unix time, whether it has passed, and a timer text like 4:05, for tokens or retry waits.
import { countdown } from "#fune/time.countdown@^1";

Gaps

What this kind of app usually needs that Functional Weave does not have yet: write these yourself, or use a service.

  • persistence Storing todos (localStorage, a database) and loading them: caps works on lists in memory.
  • sync Syncing between devices and resolving edits made offline on two of them.
  • notifications Reminders that fire at a time: push, email or OS notifications and their scheduler.
  • sharing Shared lists, assignees and permissions between users.
  • ui The list UI itself: Functional Weave has no todo components, only the logic and React form fields.
  • time-zones Time zones and daylight saving: caps works in ISO dates, UTC and wall-clock times; converting with the IANA tz rules is yours.
  • locale-formatting Showing dates in a user's language and format ("3 octobre 2026"): caps returns ISO dates, not localised text.
  • non-uk-holidays Public holidays outside the UK: dates.bank-holidays is UK only; pass another country's list to the working-day functions yourself.