logistics.freight-rate
Freight charge from a carrier's dated weight-break tariff for a zone, with the fuel surcharge in force on the ship date.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 26 tests, run in TypeScript, Python and Rust.
What it does
Prices a consignment on a carrier's tariff: find the zone's weight break that covers the chargeable weight on the ship date, price it, check whether a heavier break would be cheaper, and add the fuel surcharge in force that day.
## The tariff is an argument
For example
freight_rate(rates ×8, fuel surcharges ×3, DE, 20,000, 2026-05-10)→ zone DE, rated grams 20,000, break from grams 1, freight £90.00, fuel surcharge basis points 18.5%, fuel surcharge £16.65, total £106.65 per-kilogram break: 20 kg at 4.50/kg plus 18.5% fuelfreight_rate(rates ×8, fuel surcharges ×3, DE, 20,000, 2026-07-01)→ zone DE, rated grams 20,000, break from grams 1, freight £90.00, fuel surcharge basis points 21.25%, fuel surcharge £19.13, total £109.13 the fuel surcharge changes on 1 July and rounds 1912.5 upfreight_rate(rates ×8, fuel surcharges ×3, DE, 20,000, 2026-06-30)→ zone DE, rated grams 20,000, break from grams 1, freight £90.00, fuel surcharge basis points 18.5%, fuel surcharge £16.65, total £106.65 the last day of the old surcharge
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn freight_rate(rates: &[FreightRate], fuel_surcharges: &[FuelSurcharge], zone: &str, chargeable_grams: i64, ship_date: &str) -> FreightQuote
| rates | FreightRate[] | the carrier's tariff, every zone and date; contracts are private, so the caller supplies it |
| fuel_surcharges | FuelSurcharge[] | the carrier's published fuel surcharge by date; empty for none |
| zone | string | |
| chargeable_grams | int | 1 or more: the greater of actual and volumetric weight, e.g. from retail.volumetric-weight |
| ship_date | date | the collection date, which decides the tariff and surcharge in force |
| returns | FreightQuote |
The types it declares, generated into your project
/// One weight break of a tariff for one zone.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FreightRate {
pub zone: String,
/// lightest chargeable weight in the break, inclusive
pub from_grams: i64,
/// heaviest, inclusive; null for no upper limit
pub to_grams: Option<i64>,
/// the weight is rounded up to a multiple of this before pricing; 1 for none
pub step_grams: i64,
/// fixed charge for the break; zero for a pure per-kilogram rate
pub base: Money,
/// minor units per kilogram of rated weight; 0 for a flat-priced band
pub per_kg_minor: i64,
/// the least the break charges; null for none
pub minimum: Option<Money>,
pub valid_from: String,
/// last day in force, inclusive; null while current
pub valid_to: Option<String>,
}
/// A fuel surcharge percentage and the dates it applies.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FuelSurcharge {
/// 1850 = 18.5% of the freight charge
pub basis_points: i64,
pub valid_from: String,
/// last day in force, inclusive; null while current
pub valid_to: Option<String>,
}
/// The freight charge, the surcharge on it, and what decided them.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FreightQuote {
pub zone: String,
/// the weight priced: rounded up to the step, or a heavier break's first weight when that was cheaper
pub rated_grams: i64,
/// fromGrams of the break that priced it
pub break_from_grams: i64,
pub freight: Money,
pub fuel_surcharge_basis_points: i64,
pub fuel_surcharge: Money,
pub total: Money,
}
Your code names it in one line, in the file that uses it
fune!(logistics.freight-rate@^1); // then call freight_rate(…)
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()))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add logistics.freight-rate
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./logistics.freight-rate-1.0.1-rust.fune, or fetch it from a terminal with fune pull logistics.freight-rate@1.0.1:rust.
The whole function, every language, is one file too: logistics.freight-rate-1.0.1.fune, 77,267 bytes, sha256 23349243b0c363ec3d796e3aa555df9607f22fdd26c6b21ea95fb440639411d4. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before logistics.freight-rate
after — your function gets the result and the arguments, and returns the final result.
// fune: after logistics.freight-rate
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace math.round-div in logistics.freight-rate
// fune: replace money.add in logistics.freight-rate
// fune: replace money.amount in logistics.freight-rate
// fune: replace money.apply-rate in logistics.freight-rate
// fune: replace money.compare in logistics.freight-rate
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show logistics.freight-rate --steps.
// fune: step logistics.freight-rate after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| per-kilogram break: 20 kg at 4.50/kg plus 18.5% fuel | rates ×8, fuel surcharges ×3, DE, 20,000, 2026-05-10 | → | zone DE, rated grams 20,000, break from grams 1, freight £90.00, fuel surcharge basis points 18.5%, fuel surcharge £16.65, total £106.65 |
| the fuel surcharge changes on 1 July and rounds 1912.5 up | rates ×8, fuel surcharges ×3, DE, 20,000, 2026-07-01 | → | zone DE, rated grams 20,000, break from grams 1, freight £90.00, fuel surcharge basis points 21.25%, fuel surcharge £19.13, total £109.13 |
| the last day of the old surcharge | rates ×8, fuel surcharges ×3, DE, 20,000, 2026-06-30 | → | zone DE, rated grams 20,000, break from grams 1, freight £90.00, fuel surcharge basis points 18.5%, fuel surcharge £16.65, total £106.65 |
| 40 kg is cheaper charged as 45 kg at the next break (pricing only the 40 kg break gives 180.00) | rates ×8, fuel surcharges ×3, DE, 40,000, 2026-05-10 | → | zone DE, rated grams 45,000, break from grams 45,000, freight £171.00, fuel surcharge basis points 18.5%, fuel surcharge £31.64, total £202.64 |
| 90 kg is cheaper charged as 100 kg | rates ×8, fuel surcharges ×3, DE, 90,000, 2026-05-10 | → | zone DE, rated grams 100,000, break from grams 100,000, freight £320.00, fuel surcharge basis points 18.5%, fuel surcharge £59.20, total £379.20 |
| a light consignment pays the break's minimum charge | rates ×8, fuel surcharges ×3, DE, 5,000, 2026-05-10 | → | zone DE, rated grams 5,000, break from grams 1, freight £50.00, fuel surcharge basis points 18.5%, fuel surcharge £9.25, total £59.25 |
| the weight is rounded up to the break's 500 g step | rates ×8, fuel surcharges ×3, DE, 12,345, 2026-05-10 | → | zone DE, rated grams 12,500, break from grams 1, freight £56.25, fuel surcharge basis points 18.5%, fuel surcharge £10.41, total £66.66 |
| exactly on a break boundary uses that break | rates ×8, fuel surcharges ×3, DE, 45,000, 2026-05-10 | → | zone DE, rated grams 45,000, break from grams 45,000, freight £171.00, fuel surcharge basis points 18.5%, fuel surcharge £31.64, total £202.64 |
| last year's tariff and surcharge for a 2025 shipment | rates ×8, fuel surcharges ×3, DE, 20,000, 2025-11-01 | → | zone DE, rated grams 20,000, break from grams 1, freight £80.00, fuel surcharge basis points 17%, fuel surcharge £13.60, total £93.60 |
| flat-priced band: a heavier band is dearer, so the band stands | rates ×8, fuel surcharges ×3, FR, 1,900, 2026-05-10 | → | zone FR, rated grams 1,900, break from grams 1, freight £8.95, fuel surcharge basis points 18.5%, fuel surcharge £1.66, total £10.61 |
Show the other 16 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| base plus per kilogram on a 1 kg step | rates ×8, fuel surcharges ×3, FR, 15,200, 2026-05-10 | → | zone FR, rated grams 16,000, break from grams 10,001, freight £26.55, fuel surcharge basis points 18.5%, fuel surcharge £4.91, total £31.46 |
| the per-kilogram charge 499.5 rounds half up | rates ×8, fuel surcharges ×3, IT, 1,500, 2026-05-10 | → | zone IT, rated grams 1,500, break from grams 1, freight £5.00, fuel surcharge basis points 18.5%, fuel surcharge £0.93, total £5.93 |
| an empty surcharge table means no surcharge | rates ×8, , DE, 20,000, 2026-05-10 | → | zone DE, rated grams 20,000, break from grams 1, freight £90.00, fuel surcharge basis points 0%, fuel surcharge £0.00, total £90.00 |
| the latest-starting surcharge row in force wins | rates ×8, fuel surcharges ×4, DE, 20,000, 2026-05-10 | → | zone DE, rated grams 20,000, break from grams 1, freight £90.00, fuel surcharge basis points 20%, fuel surcharge £18.00, total £108.00 |
| no break for the zone | rates ×8, fuel surcharges ×3, ES, 20,000, 2026-05-10 | → | error: no freight rate for zone "ES" on 2026-05-10 covers 20000 g |
| a weight above the zone's last break | rates ×8, fuel surcharges ×3, FR, 30,001, 2026-05-10 | → | error: no freight rate for zone "FR" on 2026-05-10 covers 30001 g |
| no break in force before the tariff starts | rates ×8, fuel surcharges ×3, DE, 20,000, 2024-12-31 | → | error: no freight rate for zone "DE" on 2024-12-31 covers 20000 g |
| a surcharge table that does not reach the date | rates ×8, fuel surcharges ×3, IT, 1,500, 2024-06-01 | → | error: no fuel surcharge in force on 2024-06-01 |
| two breaks covering the same weight | rates ×2, , DE, 5,000, 2026-05-10 | → | error: two freight rates for zone "DE" on 2026-05-10 cover 5000 g |
| zero weight | rates ×8, fuel surcharges ×3, DE, 0, 2026-05-10 | → | error: chargeableGrams must be 1 or more |
| a step below 1 g | rates ×1, , DE, 5,000, 2026-05-10 | → | error: stepGrams must be 1 or more |
| a negative per-kilogram rate | rates ×1, , DE, 5,000, 2026-05-10 | → | error: perKgMinor must not be negative |
| breaks in two currencies | rates ×2, , DE, 5,000, 2026-05-10 | → | error: currency mismatch |
| a malformed ship date | rates ×8, fuel surcharges ×3, DE, 20,000, 10/05/2026 | → | error: shipDate must be an ISO date |
| a ship date with a trailing newline | rates ×8, fuel surcharges ×3, DE, 20,000, 2026-05-10 | → | error: shipDate must be an ISO date |
| a ship date in Arabic-Indic digits | rates ×8, fuel surcharges ×3, DE, 20,000, ٢٠٢٦-05-10 | → | error: shipDate must be an ISO date |
More from the author
Freight rates are private contracts between a shipper and a carrier, so the tariff is passed in (`rates`), not shipped as registry data. Each row still carries `validFrom` and `validTo`, so one table can hold last year's rates and this year's and the ship date picks between them. Carriers publish their fuel surcharge as a percentage that changes weekly or monthly; that is the second table. The figures in the vectors are illustrative, not any carrier's.
## Pricing one break
ratedGrams = chargeableGrams rounded up to stepGrams
freight = base + ratedGrams x perKgMinor / 1000 (rounded half up to the minor unit)
freight = max(freight, minimum)That one shape covers the common tariffs: a flat price per weight band (`perKgMinor` 0), a per-kilogram rate by weight break with a minimum charge (air freight's M / N / +45 / +100 ...), and a base plus a per-kilogram element. The break is chosen on the chargeable weight as given, before the break's own rounding; bands must not overlap.
## A heavier break can be cheaper
Per-kilogram rates fall as the weight rises, so 40 kg at 4.50/kg (180.00) costs more than 45 kg at 3.80/kg (171.00). Carriers charge the lower figure by pricing the consignment at the first weight of the heavier break, and so does this: every heavier break of the zone in force on the date is tried at its `fromGrams`, and the cheapest wins (on a tie, the lighter rated weight). `ratedGrams` and `breakFromGrams` say what happened. Pricing only the break the weight falls in is the mistake this vector catches.
## Fuel surcharge
`fuelSurcharge = freight x basisPoints / 10000`, rounded half up, applied to the freight charge only. Of the rows in force on the ship date, the one with the latest `validFrom` wins, so a new week's figure can be appended without closing the previous row. An empty table means no surcharge; a non-empty table with no row for the date is an error, because it almost always means the table has not been updated.
## Errors
No break for the zone and date covering the weight, two breaks covering it, a weight below 1 g, a step below 1, a negative rate, and mixed currencies are all errors, each naming what it found.
1.0.1 fixes Python accepting non-ASCII digits in shipDate; adds tests.
Files
| Path | Bytes |
|---|---|
| README.md | 2,545 |
| impl/python.py | 4,541 |
| impl/rust.rs | 6,828 |
| impl/typescript.ts | 4,212 |
| vectors.json | 46,782 |