Functional Weave
Code in TypeScript

subscriptions.next-billing-date@1.0.0

impl/rust.rs

2,312 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::{add_days, epoch_day_from_iso, 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

/// The first billing date strictly after `after_date`: anchor + k intervals
/// for the smallest k >= 0 that lands after it. Each candidate is computed
/// from the anchor, never from the previous billing date, so the 31st stays on
/// month ends instead of decaying to the 28th after February.
///
/// # Panics
/// Panics if `interval_count` is below 1, the interval is unknown, or either
/// date is not a real ISO date.
pub fn next_billing_date(anchor_date: &str, interval: &str, interval_count: i64, after_date: &str) -> String {
    if interval_count < 1 {
        panic!("interval count must be 1 or more, received {}", interval_count);
    }
    let (days, months): (i64, i64) = match interval {
        "day" => (1, 0),
        "week" => (7, 0),
        "month" => (0, 1),
        "quarter" => (0, 3),
        "year" => (0, 12),
        other => panic!(
            "unknown billing interval \"{}\": expected day, week, month, quarter or year",
            other
        ),
    };
    let anchor = epoch_day_from_iso(anchor_date);
    let after = epoch_day_from_iso(after_date);
    if after < anchor {
        return anchor_date.to_string();
    }

    if days > 0 {
        let step = days * interval_count;
        let k = (after - anchor).div_euclid(step) + 1;
        return add_days(anchor_date, k * step);
    }

    // Jump straight to the period containing after_date's month; the clamp can
    // only put that candidate on or before after_date, so at most one or two
    // steps forward are needed from there.
    let step = months * interval_count;
    let a = parse_iso_date(anchor_date);
    let b = parse_iso_date(after_date);
    let months_apart = (b.year - a.year) * 12 + (b.month - a.month);
    let mut k = months_apart.div_euclid(step);
    let mut candidate = add_months(anchor_date, k * step);
    while epoch_day_from_iso(&candidate) <= after {
        k += 1;
        candidate = add_months(anchor_date, k * step);
    }
    candidate
}

pub fn fune_vector(args: &[Value]) -> Value {
    Value::str(&next_billing_date(
        args[0].as_str(),
        args[1].as_str(),
        args[2].as_i64(),
        args[3].as_str(),
    ))
}