Functional Weave
Code in TypeScript

logistics.demurrage@1.0.0

impl/rust.rs

4,964 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;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_days_between::days_between;  ← from dates.days-between ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_sum::sum_money;  ← from money.sum ^1.0.0 · built alongside by fune

/// Demurrage or detention for one container: calendar days from start_date
/// to end_date inclusive, the first free_days free, every later day at the
/// rate of the tier whose day numbers contain it.
///
/// # Panics
/// Panics on bad dates, negative free days, and a tariff with no tiers,
/// overlapping or backwards tiers, negative rates, mixed currencies or a gap
/// that a chargeable day falls in.
pub fn demurrage(start_date: &str, end_date: &str, free_days: i64, tiers: &[DemurrageTier]) -> DemurrageCharge {
    if free_days < 0 {
        panic!("freeDays must not be negative, received {}", free_days);
    }
    let total_days = days_between(start_date, end_date) + 1;
    if total_days < 1 {
        panic!("endDate must not be before startDate: {} to {}", start_date, end_date);
    }

    if tiers.is_empty() {
        panic!("tiers must not be empty");
    }
    for tier in tiers {
        if tier.from_day < 1 {
            panic!("tier fromDay must be 1 or more, received {}", tier.from_day);
        }
        if let Some(to) = tier.to_day {
            if to < tier.from_day {
                panic!("tier toDay must not be before fromDay, received {} to {}", tier.from_day, to);
            }
        }
        if tier.daily_rate.minor < 0 {
            panic!("dailyRate must not be negative, received {}", tier.daily_rate.minor);
        }
        assert_same_currency(&tiers[0].daily_rate, &tier.daily_rate);
    }
    let mut sorted: Vec<&DemurrageTier> = tiers.iter().collect();
    sorted.sort_by_key(|t| t.from_day);
    for i in 1..sorted.len() {
        let overlaps = match sorted[i - 1].to_day {
            None => true,
            Some(to) => to >= sorted[i].from_day,
        };
        if overlaps {
            panic!("tiers overlap at day {}", sorted[i].from_day);
        }
    }

    let mut lines: Vec<DemurrageLine> = Vec::new();
    let mut day = free_days + 1;
    for tier in &sorted {
        if day > total_days {
            break;
        }
        if let Some(to) = tier.to_day {
            if to < day {
                continue; // wholly inside free time
            }
        }
        // A gap is a mistake in the tariff; charging nothing for it would hide it.
        if tier.from_day > day {
            panic!("no rate for day {}: no tier covers it", day);
        }
        let last = match tier.to_day {
            None => total_days,
            Some(to) => to.min(total_days),
        };
        let days = last - day + 1;
        lines.push(DemurrageLine {
            from_day: day,
            to_day: last,
            days,
            daily_rate: tier.daily_rate.clone(),
            amount: money(tier.daily_rate.minor * days, &tier.daily_rate.currency),
        });
        day = last + 1;
    }
    if day <= total_days {
        panic!("no rate for day {}: no tier covers it", day);
    }

    let amounts: Vec<Money> = lines.iter().map(|l| l.amount.clone()).collect();
    DemurrageCharge {
        total_days,
        free_days_used: free_days.min(total_days),
        chargeable_days: (total_days - free_days).max(0),
        free_time_ends: if free_days > 0 { Some(add_days(start_date, free_days - 1)) } else { None },
        total: sum_money(&amounts, &tiers[0].daily_rate.currency),
        lines,
    }
}

pub fn demurrage_tier_from_value(v: &Value) -> DemurrageTier {
    DemurrageTier {
        from_day: v.get("fromDay").as_i64(),
        to_day: if v.get("toDay").is_null() { None } else { Some(v.get("toDay").as_i64()) },
        daily_rate: money_from_value(v.get("dailyRate")),
    }
}

pub fn demurrage_line_to_value(l: &DemurrageLine) -> Value {
    Value::obj(vec![
        ("fromDay", Value::Int(l.from_day)),
        ("toDay", Value::Int(l.to_day)),
        ("days", Value::Int(l.days)),
        ("dailyRate", money_to_value(&l.daily_rate)),
        ("amount", money_to_value(&l.amount)),
    ])
}

pub fn demurrage_charge_to_value(c: &DemurrageCharge) -> Value {
    Value::obj(vec![
        ("totalDays", Value::Int(c.total_days)),
        ("freeDaysUsed", Value::Int(c.free_days_used)),
        ("chargeableDays", Value::Int(c.chargeable_days)),
        (
            "freeTimeEnds",
            match &c.free_time_ends {
                Some(d) => Value::str(d),
                None => Value::Null,
            },
        ),
        ("lines", Value::Arr(c.lines.iter().map(demurrage_line_to_value).collect())),
        ("total", money_to_value(&c.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let tiers: Vec<DemurrageTier> = args[3].as_arr().iter().map(demurrage_tier_from_value).collect();
    demurrage_charge_to_value(&demurrage(args[0].as_str(), args[1].as_str(), args[2].as_i64(), &tiers))
}