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