Functional Weave
Code in TypeScript

charity.regular-giving-schedule

Collection dates for a regular gift: monthly, quarterly or annual on a day, clamped to short months, after a lead time.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

What it does

The dates a regular gift (a Direct Debit, standing order or card subscription) will be collected: monthly, quarterly or annually on a chosen day of the month.

- **The first collection** is the first collection day on or after `signUpDate + leadDays`. `leadDays` is whatever notice your collection method needs (Direct Debit advance notice, a processing cut-off); 0 means the sign-up day itself can be the first collection. - **Short months clamp from the chosen day every time**: a gift on the 31st is collected on 30 April, 28 (or 29) February and 31 March again, never drifting down to the 28th. Quarterly and annual gifts count their months from the first collection. This reuses dates.recurrence (`monthly-on-day` every 1, 3 or 12 months) rather than re-implementing it. - **`endDate` is inclusive** and may cut the list short of `count`; a pledge with no end passes `null`.

For example

  • regularGivingSchedule(monthly, 1, 2026-09-23, 10, —, 3) → 2026-11-01, 2026-12-01, 2027-01-01 monthly on the 1st with 10 days' notice: October's date is too soon
  • regularGivingSchedule(monthly, 31, 2026-01-01, 0, —, 4) → 2026-01-31, 2026-02-28, 2026-03-31, 2026-04-30 monthly on the 31st clamps each month from the 31st, not from February's 28th
  • regularGivingSchedule(monthly, 30, 2028-01-31, 0, —, 3) → 2028-02-29, 2028-03-30, 2028-04-30 monthly on the 30th in a leap year gives 29 February

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function regularGivingSchedule(frequency: GivingFrequency, collectionDay: number, signUpDate: string, leadDays: number, endDate: string | null, count: number): readonly string[]
frequencyGivingFrequencyhow often the gift is collected
collectionDayintday of the month, 1 to 31; clamped to the last day of a shorter month
signUpDatedatethe date the donor set up the gift
leadDaysintcalendar days needed before the first collection, e.g. for Direct Debit advance notice; 0 or more
endDatedate?last date a collection may fall on, inclusive; null for an open-ended gift
countintmost dates to return, 0 or more
returnsdate[]ascending: the first collection is the first collection day on or after signUpDate + leadDays

The type it declares, generated into your project

export type GivingFrequency = "monthly" | "quarterly" | "annually";

Your code names it in one line, in the file that uses it

import { regularGivingSchedule } from "#fune/charity.regular-giving-schedule@^1";
impl/typescript.ts · 36 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { addDays, parseIsoDate } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { recurrence } from "./dates_recurrence.ts";  ← from dates.recurrence ^1.0.0 · built alongside by fune
import { type RecurrenceRule } from "./dates_recurrence_types.ts";
import { type GivingFrequency } from "./charity_regular_giving_schedule_types.ts";

const INTERVAL: Record<string, number> = { monthly: 1, quarterly: 3, annually: 12 };

/**
 * Collection dates for a regular gift. Each date is clamped from the chosen
 * day, not from the previous date (dates.recurrence), so a gift on the 31st
 * is taken on 28 February and on 31 March again, rather than drifting to the
 * 28th for ever after February.
 */
export function regularGivingSchedule(
  frequency: GivingFrequency,
  collectionDay: number,
  signUpDate: string,
  leadDays: number,
  endDate: string | null,
  count: number
): readonly string[] {
  const interval = INTERVAL[frequency];
  if (interval === undefined) {
    throw new RangeError(`unknown giving frequency "${frequency}": expected monthly, quarterly or annually`);
  }
  if (!Number.isInteger(collectionDay) || collectionDay < 1 || collectionDay > 31) {
    throw new RangeError(`collectionDay must be 1-31, received ${collectionDay}`);
  }
  if (!Number.isInteger(leadDays) || leadDays < 0) throw new RangeError(`leadDays must not be negative, received ${leadDays}`);
  if (!Number.isInteger(count) || count < 0) throw new RangeError(`count must be a non-negative integer, received ${count}`);
  if (endDate !== null) parseIsoDate(endDate);
  const first = addDays(signUpDate, leadDays);
  const rule: RecurrenceRule = { kind: "monthly-on-day", interval, day: collectionDay, weekday: null };
  if (endDate !== null && endDate < first) return [];
  return recurrence(rule, first, count).filter((d) => endDate === null || d <= endDate);
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add charity.regular-giving-schedule
Download for TypeScript charity.regular-giving-schedule-1.0.0-typescript.fune · 8,800 bytes sha256 b2cdfc66edc3ff25f707deeafef69280e1b7523e8d83abe40c9b5e03e942fe0b

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./charity.regular-giving-schedule-1.0.0-typescript.fune, or fetch it from a terminal with fune pull charity.regular-giving-schedule@1.0.0:typescript.

The whole function, every language, is one file too: charity.regular-giving-schedule-1.0.0.fune, 13,309 bytes, sha256 8c444f46d2bfebf41aa575d4afe0af1fbfa55ef813325a5987629057027978fa. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before charity.regular-giving-schedule

after — your function gets the result and the arguments, and returns the final result.

// fune: after charity.regular-giving-schedule

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace dates.add-days in charity.regular-giving-schedule
// fune: replace dates.recurrence in charity.regular-giving-schedule

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show charity.regular-giving-schedule --steps.

// fune: step charity.regular-giving-schedule after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
monthly on the 1st with 10 days' notice: October's date is too soon monthly, 1, 2026-09-23, 10, —, 3 → 2026-11-01, 2026-12-01, 2027-01-01
monthly on the 31st clamps each month from the 31st, not from February's 28th monthly, 31, 2026-01-01, 0, —, 4 → 2026-01-31, 2026-02-28, 2026-03-31, 2026-04-30
monthly on the 30th in a leap year gives 29 February monthly, 30, 2028-01-31, 0, —, 3 → 2028-02-29, 2028-03-30, 2028-04-30
the sign-up day itself is the first collection when there is no lead time monthly, 23, 2026-09-23, 0, —, 2 → 2026-09-23, 2026-10-23
quarterly on the 15th quarterly, 15, 2026-09-23, 5, —, 4 → 2026-10-15, 2027-01-15, 2027-04-15, 2027-07-15
annually on 6 April annually, 6, 2026-04-01, 3, —, 3 → 2026-04-06, 2027-04-06, 2028-04-06
annually on the 29th from February 2028: 28 February in ordinary years annually, 29, 2028-02-01, 0, —, 3 → 2028-02-29, 2029-02-28, 2030-02-28
the lead time crosses a year end monthly, 5, 2026-12-25, 10, —, 2 → 2027-01-05, 2027-02-05
an end date is inclusive and cuts the schedule short monthly, 1, 2026-09-01, 0, 2026-12-01, 12 → 2026-09-01, 2026-10-01, 2026-11-01, 2026-12-01
an end date before the first possible collection gives nothing monthly, 1, 2026-09-23, 10, 2026-10-01, 12 →
Show the other 9 tests
CaseArgumentsExpected
a count of zero is an empty schedule monthly, 1, 2026-09-23, 0, —, 0 →
an unknown frequency is an error weekly, 1, 2026-09-23, 0, —, 3 → error: unknown giving frequency "weekly"
day 0 is an error monthly, 0, 2026-09-23, 0, —, 3 → error: collectionDay must be 1-31
day 32 is an error monthly, 32, 2026-09-23, 0, —, 3 → error: collectionDay must be 1-31
a fractional day is an error monthly, 1.5, 2026-09-23, 0, —, 3 → error: collectionDay must be 1-31
negative lead days are an error monthly, 1, 2026-09-23, -1, —, 3 → error: leadDays must not be negative
a negative count is an error monthly, 1, 2026-09-23, 0, —, -1 → error: count must be a non-negative integer
a malformed sign-up date is an error monthly, 1, 23/09/2026, 0, —, 3 → error: is not an ISO date
an impossible end date is an error monthly, 1, 2026-09-23, 0, 2026-02-30, 3 → error: is not a real calendar date

More from the author

Dates are ISO strings and are not moved for weekends or bank holidays: banks move a collection that falls on a non-working day themselves, and a caller who wants the processing date can pass each date to banking.bacs-processing-date or dates.add-business-days.

Files

PathBytes
README.md1,193
impl/python.py1,840
impl/rust.rs2,477
impl/typescript.ts1,779
vectors.json2,987