todo.reschedule-recurrence
When a repeating todo's due date is moved by hand, move its series with it: the new date becomes the anchor.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 14 tests, run in TypeScript, Python and Rust.
What it does
When someone edits a repeating todo and changes its due date, the series moves with it: the new date becomes the recurrence's anchor.
todo.item's `Recurrence` counts every occurrence from its `anchor`, the series' first date, and `validateDraft` refuses a due date before the anchor. So an edit form that simply stored the new date would either be refused (moved earlier) or keep repeating on the old rhythm (moved later). Moving the anchor is what a person means by "move this to Friday".
For example
reschedule_recurrence(frequency monthly, interval 1, anchor 2026-09-29, 2026-09-29, 2026-10-05)→ frequency monthly, interval 1, anchor 2026-10-05 moving a monthly todo's date moves the series to the new datereschedule_recurrence(frequency weekly, interval 1, anchor 2026-10-01, 2026-10-01, 2026-09-28)→ frequency weekly, interval 1, anchor 2026-09-28 moving it earlier than the anchor, which todo.item would otherwise refusereschedule_recurrence(frequency monthly, interval 1, anchor 2026-01-31, 2026-02-28, 2026-02-28)→ frequency monthly, interval 1, anchor 2026-01-31 an unchanged date keeps a month-end anchor after the series rolled forward
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 reschedule_recurrence(recurrence: Option<&Recurrence>, from_due: Option<&str>, to_due: Option<&str>) -> Option<Recurrence>
| recurrence | Recurrence? | the todo's repeat as stored, or null for a one-off todo |
| from_due | date? | the due date it had |
| to_due | date? | the due date the person gave it |
| returns | Recurrence? | anchored on toDue when the date moved; otherwise unchanged |
Your code names it in one line, in the file that uses it
fune!(todo.reschedule-recurrence@^1); // then call reschedule_recurrence(…)
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; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::todo_item::Recurrence; ← from todo.item ^1.0.0 · built alongside by fune
use super::todo_item_validate_draft::{recurrence_from_value, recurrence_to_value};
/// The repeat a todo keeps when its due date is edited: a moved date becomes
/// the anchor; an unchanged date keeps it (so a month-end series that has
/// rolled forward stays on the month end).
///
/// # Panics
/// On a malformed or impossible date (dates.add-days's message).
pub fn reschedule_recurrence(recurrence: Option<&Recurrence>, from_due: Option<&str>, to_due: Option<&str>) -> Option<Recurrence> {
if let Some(date) = from_due {
add_days(date, 0);
}
if let Some(date) = to_due {
add_days(date, 0);
}
let rule = recurrence?;
add_days(&rule.anchor, 0);
match to_due {
Some(date) if Some(date) != from_due => Some(Recurrence { anchor: date.to_string(), ..rule.clone() }),
_ => Some(rule.clone()),
}
}
fn opt_date(v: &Value) -> Option<&str> {
if v.is_null() { None } else { Some(v.as_str()) }
}
pub fn fune_vector(args: &[Value]) -> Value {
let rule = recurrence_from_value(&args[0]);
recurrence_to_value(&reschedule_recurrence(rule.as_ref(), opt_date(&args[1]), opt_date(&args[2])))
}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 todo.reschedule-recurrence
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./todo.reschedule-recurrence-1.0.0-rust.fune, or fetch it from a terminal with fune pull todo.reschedule-recurrence@1.0.0:rust.
The whole function, every language, is one file too: todo.reschedule-recurrence-1.0.0.fune, 9,618 bytes, sha256 32bbf5607d31a2d70a36741adcf309620dd29de47922d0b5e4045c5976eb5451. 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 todo.reschedule-recurrence
after — your function gets the result and the arguments, and returns the final result.
// fune: after todo.reschedule-recurrence
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 todo.reschedule-recurrence
// fune: replace todo.item in todo.reschedule-recurrence
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 todo.reschedule-recurrence --steps.
// fune: step todo.reschedule-recurrence 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.
| Case | Arguments | Expected | |
|---|---|---|---|
| moving a monthly todo's date moves the series to the new date | frequency monthly, interval 1, anchor 2026-09-29, 2026-09-29, 2026-10-05 | → | frequency monthly, interval 1, anchor 2026-10-05 |
| moving it earlier than the anchor, which todo.item would otherwise refuse | frequency weekly, interval 1, anchor 2026-10-01, 2026-10-01, 2026-09-28 | → | frequency weekly, interval 1, anchor 2026-09-28 |
| an unchanged date keeps a month-end anchor after the series rolled forward | frequency monthly, interval 1, anchor 2026-01-31, 2026-02-28, 2026-02-28 | → | frequency monthly, interval 1, anchor 2026-01-31 |
| a rolled-forward series moved by two days is anchored on the new date, not the old anchor | frequency monthly, interval 1, anchor 2026-01-31, 2026-02-28, 2026-03-02 | → | frequency monthly, interval 1, anchor 2026-03-02 |
| frequency and interval are kept | frequency weekly, interval 2, anchor 2026-09-28, 2026-09-28, 2026-10-02 | → | frequency weekly, interval 2, anchor 2026-10-02 |
| a one-off todo stays one-off | —, 2026-09-28, 2026-10-01 | → | — |
| clearing the date leaves the repeat for todo.item to report | frequency daily, interval 1, anchor 2026-09-28, 2026-09-28, — | → | frequency daily, interval 1, anchor 2026-09-28 |
| a repeat that had no date gets one: the new date is the anchor | frequency daily, interval 1, anchor 2026-09-28, —, 2026-10-01 | → | frequency daily, interval 1, anchor 2026-10-01 |
| a weekday series moved to a Saturday is anchored there as given | frequency weekdays, interval 1, anchor 2026-09-28, 2026-09-30, 2026-10-03 | → | frequency weekdays, interval 1, anchor 2026-10-03 |
| no dates at all and no repeat | —, —, — | → | — |
Show the other 4 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| an impossible new date is an error | frequency daily, interval 1, anchor 2026-09-28, 2026-09-28, 2026-02-30 | → | error: is not a real calendar date |
| a malformed old date is an error | frequency daily, interval 1, anchor 2026-09-28, 28/09/2026, 2026-10-01 | → | error: is not an ISO date |
| a malformed anchor is an error | frequency monthly, interval 1, anchor 2026-13-01, 2026-09-28, 2026-10-01 | → | error: is not a real calendar date |
| a bad date is an error even for a one-off todo | —, 2026-09-28, 2026-9-30 | → | error: is not an ISO date |
More from the author
| recurrence | fromDue → toDue | result | | --- | --- | --- | | null | anything | null | | any | toDue null | unchanged (validateDraft then says a repeating todo needs a due date) | | any | toDue = fromDue | unchanged | | any | toDue ≠ fromDue | the same frequency and interval, anchored on toDue |
- **An unchanged date keeps the anchor.** A monthly series from 31 January is due 28 February after one roll forward; saving an edit to its title must not re-anchor it on the 28th, or it would lose the month end. - A new date is taken as given, even a Saturday for a weekday series: todo.next-occurrence decides the following dates from there. - Every date given (`fromDue`, `toDue`, the anchor) must be a real ISO date, with or without a recurrence; the messages are dates.add-days's (`"2026-02-30" is not a real calendar date`).
Files
| Path | Bytes |
|---|---|
| README.md | 1,367 |
| impl/python.py | 833 |
| impl/rust.rs | 1,257 |
| impl/typescript.ts | 1,015 |
| vectors.json | 2,954 |