/** * Booking-time 20ft weight-pairing rule. * * A container wagon holds two 20ft containers (2 TEU). When two 20ft ride the * same wagon their gross-weight difference must not exceed `maxPairDiffTons` * (global rule `max20ftPairWeightDiffTons`, default 10t) so the wagon load stays * balanced. 40ft containers occupy a whole wagon alone and never pair. * * At booking time the customer enters every 20ft container's weight but not its * wagon slot, so we auto-pair: sort the 20ft weights ascending and pair adjacent * (0-1, 2-3, …). Adjacent pairing minimises the diff of every pair, so if ANY * valid pairing exists this one finds it — a violation here means no balanced * pairing is possible and the booking must be blocked. An odd leftover 20ft is * fine: it has no partner in this booking and flows to consolidation. */ export interface Container20ftUnit { /** Human label for messages, e.g. the container number. */ label: string; grossWeightTons: number; } export interface PairingViolation { message: string; /** The two container labels whose pairing exceeds the diff cap. */ labels: [string, string]; diffTons: number; } const round2 = (n: number): number => Math.round(n * 100) / 100; /** * Validate that the given 20ft units can all be paired onto wagons within the * weight-difference cap. Returns one violation per over-cap adjacent pair (empty * when every wagon pair is balanced or there is nothing to pair). A single * leftover unit (odd count) is not a violation. */ export function validate20ftWeightPairing( units: Container20ftUnit[], maxPairDiffTons: number, ): PairingViolation[] { if (units.length < 2 || maxPairDiffTons == null) return []; // Ascending by weight: adjacent pairs have the smallest possible diffs. const sorted = [...units].sort((a, b) => a.grossWeightTons - b.grossWeightTons); const violations: PairingViolation[] = []; for (let i = 0; i + 1 < sorted.length; i += 2) { const a = sorted[i]; const b = sorted[i + 1]; const diff = Math.abs(a.grossWeightTons - b.grossWeightTons); if (diff > maxPairDiffTons) { violations.push({ message: `20ft containers ${a.label} (${round2(a.grossWeightTons)}T) and ` + `${b.label} (${round2(b.grossWeightTons)}T) cannot share a wagon: ` + `weight difference ${round2(diff)}T exceeds the ${maxPairDiffTons}T limit.`, labels: [a.label, b.label], diffTons: round2(diff), }); } } return violations; }