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.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 24 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
freightRate(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% fuelfreightRate(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 upfreightRate(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.
export function freightRate(rates: readonly FreightRate[], fuelSurcharges: readonly FuelSurcharge[], zone: string, chargeableGrams: number, shipDate: string): FreightQuote
| rates | FreightRate[] | the carrier's tariff, every zone and date; contracts are private, so the caller supplies it |
| fuelSurcharges | FuelSurcharge[] | the carrier's published fuel surcharge by date; empty for none |
| zone | string | |
| chargeableGrams | int | 1 or more: the greater of actual and volumetric weight, e.g. from retail.volumetric-weight |
| shipDate | 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. */
export interface FreightRate {
readonly zone: string;
/** lightest chargeable weight in the break, inclusive */
readonly fromGrams: number;
/** heaviest, inclusive; null for no upper limit */
readonly toGrams: number | null;
/** the weight is rounded up to a multiple of this before pricing; 1 for none */
readonly stepGrams: number;
/** fixed charge for the break; zero for a pure per-kilogram rate */
readonly base: Money;
/** minor units per kilogram of rated weight; 0 for a flat-priced band */
readonly perKgMinor: number;
/** the least the break charges; null for none */
readonly minimum: Money | null;
readonly validFrom: string;
/** last day in force, inclusive; null while current */
readonly validTo: string | null;
}
/** A fuel surcharge percentage and the dates it applies. */
export interface FuelSurcharge {
/** 1850 = 18.5% of the freight charge */
readonly basisPoints: number;
readonly validFrom: string;
/** last day in force, inclusive; null while current */
readonly validTo: string | null;
}
/** The freight charge, the surcharge on it, and what decided them. */
export interface FreightQuote {
readonly zone: string;
/** the weight priced: rounded up to the step, or a heavier break's first weight when that was cheaper */
readonly ratedGrams: number;
/** fromGrams of the break that priced it */
readonly breakFromGrams: number;
readonly freight: Money;
readonly fuelSurchargeBasisPoints: number;
readonly fuelSurcharge: Money;
readonly total: Money;
}
Your code names it in one line, in the file that uses it
import { freightRate } from "#fune/logistics.freight-rate@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { addMoney } from "./money_add.ts"; ← from money.add ^1.0.0 · built alongside by fune
import { type Money, assertSameCurrency, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { applyRate } from "./money_apply_rate.ts"; ← from money.apply-rate ^1.0.0 · built alongside by fune
import { compareMoney } from "./money_compare.ts"; ← from money.compare ^1.0.0 · built alongside by fune
import { type FreightRate, type FuelSurcharge, type FreightQuote } from "./logistics_freight_rate_types.ts";
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
function inForce(validFrom: string, validTo: string | null, onDate: string): boolean {
return onDate >= validFrom && (validTo === null || onDate <= validTo);
}
function roundUpTo(grams: number, step: number): number {
return roundDiv(grams, step, "up") * step;
}
/** The break's charge at a weight: base plus the per-kilogram element, at least the minimum. */
function charge(rate: FreightRate, ratedGrams: number): Money {
const variable = roundDiv(ratedGrams * rate.perKgMinor, 1000, "half-up");
const freight = money(rate.base.minor + variable, rate.base.currency);
if (rate.minimum !== null && compareMoney(freight, rate.minimum) < 0) return rate.minimum;
return 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.
*/
export function freightRate(
rates: readonly FreightRate[],
fuelSurcharges: readonly FuelSurcharge[],
zone: string,
chargeableGrams: number,
shipDate: string,
): FreightQuote {
if (!Number.isInteger(chargeableGrams) || chargeableGrams < 1) {
throw new RangeError(`chargeableGrams must be 1 or more, received ${chargeableGrams}`);
}
if (!ISO_DATE.test(shipDate)) throw new RangeError(`shipDate must be an ISO date (YYYY-MM-DD), received "${shipDate}"`);
const candidates = rates.filter((r) => r.zone === zone && inForce(r.validFrom, r.validTo, shipDate));
for (const rate of candidates) {
if (!Number.isInteger(rate.stepGrams) || rate.stepGrams < 1) {
throw new RangeError(`stepGrams must be 1 or more, received ${rate.stepGrams}`);
}
if (!Number.isInteger(rate.perKgMinor) || rate.perKgMinor < 0) {
throw new RangeError(`perKgMinor must not be negative, received ${rate.perKgMinor}`);
}
assertSameCurrency(candidates[0].base, rate.base);
}
const covering = candidates.filter(
(r) => chargeableGrams >= r.fromGrams && (r.toGrams === null || chargeableGrams <= r.toGrams),
);
if (covering.length === 0) {
throw new RangeError(`no freight rate for zone "${zone}" on ${shipDate} covers ${chargeableGrams} g`);
}
if (covering.length > 1) {
throw new RangeError(`two freight rates for zone "${zone}" on ${shipDate} cover ${chargeableGrams} g`);
}
let best = covering[0];
let bestGrams = roundUpTo(chargeableGrams, best.stepGrams);
let bestFreight = charge(best, bestGrams);
// 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 (const rate of candidates) {
if (rate.fromGrams <= chargeableGrams) continue;
const grams = roundUpTo(rate.fromGrams, rate.stepGrams);
const freight = charge(rate, grams);
const order = compareMoney(freight, bestFreight);
if (order < 0 || (order === 0 && grams < bestGrams)) {
best = rate;
bestGrams = grams;
bestFreight = freight;
}
}
let basisPoints = 0;
if (fuelSurcharges.length > 0) {
let found: FuelSurcharge | null = null;
for (const row of fuelSurcharges) {
if (!inForce(row.validFrom, row.validTo, shipDate)) continue;
if (found === null || row.validFrom > found.validFrom) found = row;
}
// A table that stops short of the date has usually not been updated.
if (found === null) throw new RangeError(`no fuel surcharge in force on ${shipDate}`);
basisPoints = found.basisPoints;
}
const fuelSurcharge = applyRate(bestFreight, basisPoints, "half-up");
return {
zone,
ratedGrams: bestGrams,
breakFromGrams: best.fromGrams,
freight: bestFreight,
fuelSurchargeBasisPoints: basisPoints,
fuelSurcharge,
total: addMoney(bestFreight, fuelSurcharge),
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the TypeScript 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. 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 TypeScript implementation. Install it without the registry with fune add ./logistics.freight-rate-1.0.0-typescript.fune, or fetch it from a terminal with fune pull logistics.freight-rate@1.0.0:typescript.
The whole function, every language, is one file too: logistics.freight-rate-1.0.0.fune, 72,801 bytes, sha256 8bea10f04f6f84cc8fdf6cedc848e6c79afef95634eae21234454081c74ab780. 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 14 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 |
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.
Files
| Path | Bytes |
|---|---|
| README.md | 2,473 |
| impl/python.py | 4,534 |
| impl/rust.rs | 6,828 |
| impl/typescript.ts | 4,212 |
| vectors.json | 42,945 |