Functional Weave
Code in Python

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

  • regular_giving_schedule(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
  • regular_giving_schedule(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
  • regular_giving_schedule(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.

def regular_giving_schedule(frequency: GivingFrequency, collection_day: int, sign_up_date: str, lead_days: int, end_date: Optional[str], count: int) -> List[str]
frequencyGivingFrequencyhow often the gift is collected
collection_dayintday of the month, 1 to 31; clamped to the last day of a shorter month
sign_up_datedatethe date the donor set up the gift
lead_daysintcalendar days needed before the first collection, e.g. for Direct Debit advance notice; 0 or more
end_datedate?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

GivingFrequency = Literal["monthly", "quarterly", "annually"]

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

from fune.charity.regular_giving_schedule import regular_giving_schedule  # charity.regular-giving-schedule@^1
impl/python.py · 42 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.

from typing import List, Optional

from .charity_regular_giving_schedule_types import GivingFrequency
from .dates_add_days import add_days, parse_iso_date  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_recurrence import recurrence  ← from dates.recurrence ^1.0.0 · built alongside by fune
from .dates_recurrence_types import RecurrenceRule

INTERVAL = {"monthly": 1, "quarterly": 3, "annually": 12}


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def regular_giving_schedule(
    frequency: GivingFrequency,
    collection_day: int,
    sign_up_date: str,
    lead_days: int,
    end_date: Optional[str],
    count: int,
) -> List[str]:
    """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.
    """
    interval = INTERVAL.get(frequency) if isinstance(frequency, str) else None
    if interval is None:
        raise ValueError('unknown giving frequency "%s": expected monthly, quarterly or annually' % (frequency,))
    if not _is_int(collection_day) or collection_day < 1 or collection_day > 31:
        raise ValueError("collectionDay must be 1-31, received %s" % (collection_day,))
    if not _is_int(lead_days) or lead_days < 0:
        raise ValueError("leadDays must not be negative, received %s" % (lead_days,))
    if not _is_int(count) or count < 0:
        raise ValueError("count must be a non-negative integer, received %s" % (count,))
    if end_date is not None:
        parse_iso_date(end_date)
    first = add_days(sign_up_date, lead_days)
    rule = RecurrenceRule(kind="monthly-on-day", interval=interval, day=collection_day, weekday=None)
    if end_date is not None and end_date < first:
        return []
    return [d for d in recurrence(rule, first, count) if end_date is None or d <= end_date]

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Python 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 Python charity.regular-giving-schedule-1.0.0-python.fune · 8,869 bytes sha256 cadbc71b8bb7025334f1dbc7533624ce4a281ff5f9520c5eb26203f847404d0a

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

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