Functional Weave
Code in Rust

dates.recurrence@1.0.0

impl/rust.rs

4,775 bytes · the Rust implementation · view 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::{days_from_civil, days_in_month, epoch_day_from_iso, iso_from_epoch_day, parse_iso_date};  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_add_months::add_months;  ← from dates.add-months ^1.0.0 · built alongside by fune
use super::dates_day_of_week::day_of_week;  ← from dates.day-of-week ^1.0.0 · built alongside by fune

fn check_range(name: &str, value: i64, low: i64, high: i64, kind: &str) {
    if value < low || value > high {
        panic!("{} must be {}-{} for {}, received {}", name, low, high, kind, value);
    }
}

/// The date of the `day`-th (clamped) or last `weekday` in a month index counted from year 0.
fn date_in_month(index: i64, rule: &RecurrenceRule) -> i64 {
    let year = index.div_euclid(12);
    let month = index.rem_euclid(12) + 1;
    if year > 9999 {
        panic!("the schedule runs past 9999-12-31, outside the supported range 0001-01-01 to 9999-12-31");
    }
    let length = days_in_month(year, month);
    if rule.kind == "monthly-on-day" {
        return days_from_civil(year, month, rule.day.unwrap().min(length));
    }
    let last = days_from_civil(year, month, length);
    let last_weekday = day_of_week(&iso_from_epoch_day(last));
    last - (last_weekday - rule.weekday.unwrap()).rem_euclid(7)
}

/// The first `count` dates of a rule on or after `start_iso`. Monthly dates are
/// clamped from the rule's day every time, never from the previous date, so the
/// 31st does not decay to the 28th after February.
///
/// # Panics
/// Panics on an invalid rule, a malformed start date, a negative count, or a
/// schedule running past 9999-12-31.
pub fn recurrence(rule: &RecurrenceRule, start_iso: &str, count: i64) -> Vec<String> {
    if count < 0 {
        panic!("count must be a non-negative integer, received {}", count);
    }
    if rule.interval < 1 {
        panic!("interval must be a positive integer, received {}", rule.interval);
    }
    let kind = rule.kind.as_str();
    match kind {
        "monthly-on-day" => {
            match rule.day {
                None => panic!("day must be 1-31 for monthly-on-day, received null"),
                Some(day) => check_range("day", day, 1, 31, kind),
            }
            if rule.weekday.is_some() {
                panic!("weekday does not apply to monthly-on-day");
            }
        }
        "last-weekday-of-month" => {
            match rule.weekday {
                None => panic!("weekday must be 1-7 for last-weekday-of-month, received null"),
                Some(weekday) => check_range("weekday", weekday, 1, 7, kind),
            }
            if rule.day.is_some() {
                panic!("day does not apply to last-weekday-of-month");
            }
        }
        "every-days" | "every-weeks" | "yearly" => {
            if rule.day.is_some() {
                panic!("day does not apply to {}", kind);
            }
            if rule.weekday.is_some() {
                panic!("weekday does not apply to {}", kind);
            }
        }
        _ => panic!(
            "unknown recurrence kind \"{}\": expected monthly-on-day, last-weekday-of-month, every-days, every-weeks or yearly",
            kind
        ),
    }

    let start = epoch_day_from_iso(start_iso);
    let mut out: Vec<String> = Vec::new();
    match kind {
        "every-days" | "every-weeks" => {
            let step = if kind == "every-weeks" { 7 * rule.interval } else { rule.interval };
            for k in 0..count {
                out.push(iso_from_epoch_day(start + k * step));
            }
        }
        "yearly" => {
            // Always from the anchor, so 29 February comes back in leap years.
            for k in 0..count {
                out.push(add_months(start_iso, 12 * rule.interval * k));
            }
        }
        _ => {
            let date = parse_iso_date(start_iso);
            let mut first = date.year * 12 + (date.month - 1);
            if count > 0 && date_in_month(first, rule) < start {
                first += 1;
            }
            for k in 0..count {
                out.push(iso_from_epoch_day(date_in_month(first + k * rule.interval, rule)));
            }
        }
    }
    out
}

fn optional_int(value: &Value) -> Option<i64> {
    if value.is_null() {
        None
    } else {
        Some(value.as_i64())
    }
}

pub fn recurrence_rule_from_value(value: &Value) -> RecurrenceRule {
    RecurrenceRule {
        kind: value.get("kind").as_str().to_string(),
        interval: value.get("interval").as_i64(),
        day: optional_int(value.get("day")),
        weekday: optional_int(value.get("weekday")),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let rule = recurrence_rule_from_value(&args[0]);
    Value::Arr(
        recurrence(&rule, args[1].as_str(), args[2].as_i64())
            .iter()
            .map(|d| Value::str(d))
            .collect(),
    )
}