Functional Weave
Code in TypeScript

logistics.freight-rate@1.0.1

impl/rust.rs

6,828 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::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_add::add_money;  ← from money.add ^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_apply_rate::apply_rate;  ← from money.apply-rate ^1.0.0 · built alongside by fune
use super::money_compare::compare_money;  ← from money.compare ^1.0.0 · built alongside by fune

fn is_iso_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() == 10
        && bytes[4] == b'-'
        && bytes[7] == b'-'
        && bytes
            .iter()
            .enumerate()
            .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}

fn in_force(valid_from: &str, valid_to: &Option<String>, on_date: &str) -> bool {
    on_date >= valid_from
        && match valid_to {
            None => true,
            Some(to) => on_date <= to.as_str(),
        }
}

fn round_up_to(grams: i64, step: i64) -> i64 {
    round_div(grams, step, "up") * step
}

/// The break's charge at a weight: base plus the per-kilogram element, at least the minimum.
fn charge(rate: &FreightRate, rated_grams: i64) -> Money {
    let variable = round_div(rated_grams * rate.per_kg_minor, 1000, "half-up");
    let freight = money(rate.base.minor + variable, &rate.base.currency);
    if let Some(minimum) = &rate.minimum {
        if compare_money(&freight, minimum) < 0 {
            return minimum.clone();
        }
    }
    freight
}

/// Freight for a consignment on a caller-supplied tariff: the zone's break
/// covering the weight on the ship date, or a heavier break when that is
/// cheaper, plus the fuel surcharge in force that day.
///
/// # Panics
/// Panics on a weight below 1 g, a malformed date, a bad break, mixed
/// currencies, no break or two breaks covering the weight, or a surcharge
/// table with no row for the date.
pub fn freight_rate(
    rates: &[FreightRate],
    fuel_surcharges: &[FuelSurcharge],
    zone: &str,
    chargeable_grams: i64,
    ship_date: &str,
) -> FreightQuote {
    if chargeable_grams < 1 {
        panic!("chargeableGrams must be 1 or more, received {}", chargeable_grams);
    }
    if !is_iso_date(ship_date) {
        panic!("shipDate must be an ISO date (YYYY-MM-DD), received \"{}\"", ship_date);
    }

    let candidates: Vec<&FreightRate> = rates
        .iter()
        .filter(|r| r.zone == zone && in_force(&r.valid_from, &r.valid_to, ship_date))
        .collect();
    for rate in &candidates {
        if rate.step_grams < 1 {
            panic!("stepGrams must be 1 or more, received {}", rate.step_grams);
        }
        if rate.per_kg_minor < 0 {
            panic!("perKgMinor must not be negative, received {}", rate.per_kg_minor);
        }
        assert_same_currency(&candidates[0].base, &rate.base);
    }

    let covering: Vec<&&FreightRate> = candidates
        .iter()
        .filter(|r| chargeable_grams >= r.from_grams && r.to_grams.map_or(true, |t| chargeable_grams <= t))
        .collect();
    if covering.is_empty() {
        panic!("no freight rate for zone \"{}\" on {} covers {} g", zone, ship_date, chargeable_grams);
    }
    if covering.len() > 1 {
        panic!("two freight rates for zone \"{}\" on {} cover {} g", zone, ship_date, chargeable_grams);
    }

    let mut best: &FreightRate = covering[0];
    let mut best_grams = round_up_to(chargeable_grams, best.step_grams);
    let mut best_freight = charge(best, best_grams);
    // Rates per kilogram fall as weight rises, so a heavier break charged at
    // its first weight can undercut the break the consignment falls in.
    for rate in &candidates {
        if rate.from_grams <= chargeable_grams {
            continue;
        }
        let grams = round_up_to(rate.from_grams, rate.step_grams);
        let freight = charge(rate, grams);
        let order = compare_money(&freight, &best_freight);
        if order < 0 || (order == 0 && grams < best_grams) {
            best = rate;
            best_grams = grams;
            best_freight = freight;
        }
    }

    let mut basis_points = 0;
    if !fuel_surcharges.is_empty() {
        let mut found: Option<&FuelSurcharge> = None;
        for row in fuel_surcharges {
            if !in_force(&row.valid_from, &row.valid_to, ship_date) {
                continue;
            }
            if found.map_or(true, |f| row.valid_from > f.valid_from) {
                found = Some(row);
            }
        }
        // A table that stops short of the date has usually not been updated.
        match found {
            None => panic!("no fuel surcharge in force on {}", ship_date),
            Some(row) => basis_points = row.basis_points,
        }
    }
    let fuel_surcharge = apply_rate(&best_freight, basis_points, "half-up");
    FreightQuote {
        zone: zone.to_string(),
        rated_grams: best_grams,
        break_from_grams: best.from_grams,
        total: add_money(&best_freight, &fuel_surcharge),
        freight: best_freight,
        fuel_surcharge_basis_points: basis_points,
        fuel_surcharge,
    }
}

fn opt_str(v: &Value) -> Option<String> {
    if v.is_null() {
        None
    } else {
        Some(v.as_str().to_string())
    }
}

pub fn freight_rate_from_value(v: &Value) -> FreightRate {
    FreightRate {
        zone: v.get("zone").as_str().to_string(),
        from_grams: v.get("fromGrams").as_i64(),
        to_grams: if v.get("toGrams").is_null() { None } else { Some(v.get("toGrams").as_i64()) },
        step_grams: v.get("stepGrams").as_i64(),
        base: money_from_value(v.get("base")),
        per_kg_minor: v.get("perKgMinor").as_i64(),
        minimum: if v.get("minimum").is_null() { None } else { Some(money_from_value(v.get("minimum"))) },
        valid_from: v.get("validFrom").as_str().to_string(),
        valid_to: opt_str(v.get("validTo")),
    }
}

pub fn fuel_surcharge_from_value(v: &Value) -> FuelSurcharge {
    FuelSurcharge {
        basis_points: v.get("basisPoints").as_i64(),
        valid_from: v.get("validFrom").as_str().to_string(),
        valid_to: opt_str(v.get("validTo")),
    }
}

pub fn freight_quote_to_value(q: &FreightQuote) -> Value {
    Value::obj(vec![
        ("zone", Value::str(&q.zone)),
        ("ratedGrams", Value::Int(q.rated_grams)),
        ("breakFromGrams", Value::Int(q.break_from_grams)),
        ("freight", money_to_value(&q.freight)),
        ("fuelSurchargeBasisPoints", Value::Int(q.fuel_surcharge_basis_points)),
        ("fuelSurcharge", money_to_value(&q.fuel_surcharge)),
        ("total", money_to_value(&q.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let rates: Vec<FreightRate> = args[0].as_arr().iter().map(freight_rate_from_value).collect();
    let fuel: Vec<FuelSurcharge> = args[1].as_arr().iter().map(fuel_surcharge_from_value).collect();
    freight_quote_to_value(&freight_rate(&rates, &fuel, args[2].as_str(), args[3].as_i64(), args[4].as_str()))
}