Functional Weave
Code in Rust

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.

pub fn regular_giving_schedule(frequency: &str, collection_day: i64, sign_up_date: &str, lead_days: i64, end_date: Option<&str>, count: i64) -> Vec<String>
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 is a string in Rust, one of: "monthly", "quarterly", "annually".
// Parameters take it as &str and results hold it as String.

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

fune!(charity.regular-giving-schedule@^1);  // then call regular_giving_schedule(…)
impl/rust.rs · 72 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{add_days, parse_iso_date};  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_recurrence::{recurrence, RecurrenceRule};  ← from dates.recurrence ^1.0.0 · built alongside by fune

/// 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.
///
/// # Panics
/// Panics on an unknown frequency, a day outside 1-31, negative lead days or
/// count, or a malformed date.
pub fn regular_giving_schedule(
    frequency: &str,
    collection_day: i64,
    sign_up_date: &str,
    lead_days: i64,
    end_date: Option<&str>,
    count: i64,
) -> Vec<String> {
    let interval = match frequency {
        "monthly" => 1,
        "quarterly" => 3,
        "annually" => 12,
        _ => panic!("unknown giving frequency \"{}\": expected monthly, quarterly or annually", frequency),
    };
    if !(1..=31).contains(&collection_day) {
        panic!("collectionDay must be 1-31, received {}", collection_day);
    }
    if lead_days < 0 {
        panic!("leadDays must not be negative, received {}", lead_days);
    }
    if count < 0 {
        panic!("count must be a non-negative integer, received {}", count);
    }
    if let Some(end) = end_date {
        parse_iso_date(end);
    }
    let first = add_days(sign_up_date, lead_days);
    if let Some(end) = end_date {
        if end < first.as_str() {
            return Vec::new();
        }
    }
    let rule = RecurrenceRule {
        kind: "monthly-on-day".to_string(),
        interval,
        day: Some(collection_day),
        weekday: None,
    };
    recurrence(&rule, &first, count)
        .into_iter()
        .filter(|d| end_date.map_or(true, |end| d.as_str() <= end))
        .collect()
}

fn int_arg(v: &Value, name: &str, message: &str) -> i64 {
    if let Value::Float(f) = v {
        if f.fract() != 0.0 {
            panic!("{} {}, received {}", name, message, f);
        }
    }
    v.as_i64()
}

pub fn fune_vector(args: &[Value]) -> Value {
    let day = int_arg(&args[1], "collectionDay", "must be 1-31");
    let lead = int_arg(&args[3], "leadDays", "must not be negative");
    let count = int_arg(&args[5], "count", "must be a non-negative integer");
    let end = if args[4].is_null() { None } else { Some(args[4].as_str()) };
    let dates = regular_giving_schedule(args[0].as_str(), day, args[2].as_str(), lead, end, count);
    Value::Arr(dates.iter().map(|d| Value::str(d)).collect())
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add charity.regular-giving-schedule
Download for Rust charity.regular-giving-schedule-1.0.0-rust.fune · 9,544 bytes sha256 164945ea2039dcf6f941c408a82aedd857a03f10d5e11268b17679f53e496435

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

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