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(),
)
}