Shipping quotes look simple until you try to calculate one in code. A carton is billed on whichever is larger, its real weight or its size, and every mode of transport converts size to weight with a different number. Get one divisor wrong and an invoice check is off by 20 percent.
These are the small, pure functions I use behind the shipping calculators on TradingForms. Every snippet below is plain JavaScript with no dependencies, and the test at the end runs in Node as is.
Why the "billable weight" matters
Carriers sell space as well as weight. A box of pillows weighs little but fills a truck, so carriers turn volume into a "dimensional" or "volumetric" weight and charge the greater of the two. Your code needs three ideas:
- Volume in one unit (cubic metres or cubic inches).
- A divisor that turns volume into weight for that mode.
- A rule for picking and rounding the weight that gets billed.
Cubic metres (CBM) from any unit
Convert each side to metres first, then multiply. Converting once at the end is a common source of bugs when people mix inches and centimetres.
const M_PER = { mm: 0.001, cm: 0.01, m: 1, in: 0.0254, ft: 0.3048 };
function cbm(l, w, h, unit) {
const f = M_PER[unit];
return l * f * (w * f) * (h * f);
}
cbm(120, 80, 100, "cm"); // 0.96
cbm(48, 40, 48, "in"); // 1.5104...
A 48 x 40 inch pallet stacked 48 inches high is about 1.51 CBM, or 53.3 cubic feet. The CBM calculator on our site does the same sum for many carton lines at once.
Dimensional weight and the divisor table
Dimensional weight is volume divided by a divisor. Metric divisors give kilograms from cubic centimetres; imperial divisors give pounds from cubic inches.
const DIVISORS = {
iata: 6000, // air cargo, cm³ per kg
express: 5000, // many express couriers, cm³ per kg
road: 3000, // common road freight factor, cm³ per kg
us139: 139, // FedEx / UPS US daily rates, in³ per lb
ups166: 166, // UPS retail US domestic, in³ per lb
};
const dimWeight = (l, w, h, divisor) => (l * w * h) / divisor;
dimWeight(60, 50, 40, DIVISORS.iata); // 20 kg
dimWeight(24, 18, 12, DIVISORS.us139); // 37.29... lb
Divisors change between carriers, services and contract rates, so keep them as data, not as magic numbers inside formulas. Carriers also have their own rounding rules for measurements and the final weight, so read the service guide for the carrier you bill against. Our dimensional weight calculator lists the common divisors side by side.
Chargeable weight for air cargo
For air freight, the airline charges the greater of gross weight and volume weight, and the result is usually rounded up to the next half kilogram. Handle every piece, then compare the totals.
function chargeableWeight(pieces, divisor = 6000) {
let volKg = 0;
let gross = 0;
for (const p of pieces) {
if (!(p.qty > 0) || !(p.l > 0 && p.w > 0 && p.h > 0)) continue;
volKg += ((p.l * p.w * p.h) / divisor) * p.qty;
gross += (Number.isFinite(p.kg) ? p.kg : 0) * p.qty;
}
const raw = Math.max(volKg, gross);
// 1e-9 keeps 40.0000000001 from rounding up to 40.5
const chargeable = Math.ceil(raw * 2 - 1e-9) / 2;
return { volKg, gross, chargeable, by: volKg > gross ? "volume" : "gross" };
}
chargeableWeight([{ l: 60, w: 50, h: 40, qty: 2, kg: 15 }]);
// { volKg: 40, gross: 30, chargeable: 40, by: "volume" }
The 1e-9 matters. Floating point turns some exact totals into values a hair above the true number, and without the nudge Math.ceil bills an extra half kilo. That tiny guard is the difference between a calculator that matches the airline's invoice and one that does not. Try it with your own pieces in the chargeable weight calculator.
Sea freight: weight or measure
Less than container load (LCL) sea freight is billed per "revenue ton": the greater of the weight in tonnes and the volume in cubic metres. One CBM counts as 1,000 kg.
const revenueTons = (cbmTotal, kgTotal) => Math.max(cbmTotal, kgTotal / 1000);
revenueTons(2.4, 900); // 2.4 (billed on volume)
revenueTons(1.2, 1800); // 1.8 (billed on weight)
Freight density for US LTL
US less than truckload pricing leans on density: pounds per cubic foot. One cubic foot is 1,728 cubic inches.
const CUIN_PER_CUFT = 1728;
const densityLbFt3 = (lIn, wIn, hIn, lb, pieces = 1) =>
(lb * pieces) / ((lIn * wIn * hIn * pieces) / CUIN_PER_CUFT);
densityLbFt3(48, 40, 48, 500); // 9.375 lb/ft³
Measure the pallet, not just the cartons on it. Density feeds the freight class, and the class tables changed with the NMFC update of July 2025, so keep class lookups in a data file you can update. Our freight density calculator shows the class for the current scale.
Fuel surcharges: per mile and percentage
Truckload contracts often charge fuel per mile: the gap between the current diesel price and a base price, divided by the truck's miles per gallon. Never let it go negative.
function fuelSurchargePerMile(current, base, mpg, miles = NaN) {
const perMile = mpg > 0 ? Math.max(0, current - base) / mpg : NaN;
return { perMile, total: Number.isFinite(miles) ? perMile * miles : NaN };
}
fuelSurchargePerMile(4.1, 3, 6, 600); // { perMile: 0.1833..., total: 110 }
Percentage tables work in whole steps above the base, so floor the step count before you multiply.
function fuelSurchargePct(current, base, step, stepPct, startPct = 0) {
if (current < base) return { steps: 0, pct: 0 };
if (!(step > 0)) return { steps: 0, pct: NaN };
const steps = Math.floor((current - base) / step + 1e-9);
return { steps, pct: startPct + steps * stepPct };
}
fuelSurchargePct(1.47, 1.2, 0.05, 0.5); // { steps: 5, pct: 2.5 }
Note the same 1e-9 trick in reverse. In JavaScript, (1.4 - 1.2) / 0.05 is 3.999999999999999, not 4, so a plain Math.floor drops a whole step and undercharges by one bracket. I wrote up the business side, with worked examples, in how to calculate a fuel surcharge.
Test it
Save everything above in one file with this at the bottom and run node freight.mjs:
import assert from "node:assert/strict";
const near = (a, b) => assert.ok(Math.abs(a - b) < 1e-9, `${a} != ${b}`);
near(cbm(120, 80, 100, "cm"), 0.96);
near(dimWeight(60, 50, 40, DIVISORS.iata), 20);
const cw = chargeableWeight([{ l: 60, w: 50, h: 40, qty: 2, kg: 15 }]);
near(cw.chargeable, 40);
assert.equal(cw.by, "volume");
near(chargeableWeight([{ l: 10, w: 10, h: 10, qty: 1, kg: 12.2 }]).chargeable, 12.5);
near(revenueTons(1.2, 1800), 1.8);
near(densityLbFt3(48, 40, 48, 500), 9.375);
near(fuelSurchargePerMile(4.1, 3, 6, 600).total, 110);
near(fuelSurchargePct(1.47, 1.2, 0.05, 0.5).pct, 2.5);
assert.equal(fuelSurchargePct(1.4, 1.2, 0.05, 1).steps, 4);
console.log("all freight checks passed");
Takeaways
- Convert units first, multiply second.
- Keep divisors, densities and class tables as data, because carriers change them.
- Guard every
Math.ceilandMath.flooragainst floating point drift. - Treat results as estimates and check them against the carrier's rate sheet before you bill.
If you would rather not code it, the calculators on TradingForms are free and run in the browser.
Top comments (0)