/** * Flock「按密度估算货运等级」= NMFTA Docket 2025-1 十三档密度表(2025-07-19 生效) * * 权威来源(禁止臆造区间): * - NMFTA Disposition Bulletin / Docket 2025-1(13-sub density scale) * - Flock 官网 i 弹层:Total Volume / Density / Density-based Freight Class * 实测样例:6×48×40×48、2000 lb → 320 ft³、6.25 PCF → Class 125 * * 关系(与「参数越小等级越高」的正确表述): * - 密度 PCF = 总重(lb) ÷ 总体积(ft³) * - 体积越大或重量越轻 → PCF ↓ → 货运等级数字 ↑(越贵,占车容多) * - 体积越小或重量越重 → PCF ↑ → 货运等级数字 ↓(越便宜,装得下更多重量) * * 公式: * cuFt = (L_in × W_in × H_in × 件数) / 1728 * PCF = 总重_lb / cuFt * 等级 = 下表查表(区间均为「≥min 且 = [ { sub: 13, minPcf: 50, maxPcf: Number.POSITIVE_INFINITY, freightClass: "50", rangeLabelZh: "≥50", }, { sub: 12, minPcf: 35, maxPcf: 50, freightClass: "55", rangeLabelZh: "35 – <50", }, { sub: 11, minPcf: 30, maxPcf: 35, freightClass: "60", rangeLabelZh: "30 – <35", }, { sub: 10, minPcf: 22.5, maxPcf: 30, freightClass: "65", rangeLabelZh: "22.5 – <30", }, { sub: 9, minPcf: 15, maxPcf: 22.5, freightClass: "70", rangeLabelZh: "15 – <22.5", }, { sub: 8, minPcf: 12, maxPcf: 15, freightClass: "85", rangeLabelZh: "12 – <15", }, { sub: 7, minPcf: 10, maxPcf: 12, freightClass: "92.5", rangeLabelZh: "10 – <12", }, { sub: 6, minPcf: 8, maxPcf: 10, freightClass: "100", rangeLabelZh: "8 – <10", }, { sub: 5, minPcf: 6, maxPcf: 8, freightClass: "125", rangeLabelZh: "6 – <8", }, { sub: 4, minPcf: 4, maxPcf: 6, freightClass: "175", rangeLabelZh: "4 – <6", }, { sub: 3, minPcf: 2, maxPcf: 4, freightClass: "250", rangeLabelZh: "2 – <4", }, { sub: 2, minPcf: 1, maxPcf: 2, freightClass: "300", rangeLabelZh: "1 – <2", }, { sub: 1, minPcf: 0, maxPcf: 1, freightClass: "400", rangeLabelZh: "<1", }, ]; /** 密度公式绝不会给出的档(仅商品条件分等;Flock 下拉仍可手选) */ export const NMFC_NON_DENSITY_FREIGHT_CLASSES = [ "77.5", "110", "150", "200", "500", ] as const; export type FreightDensityInput = { lengthIn: number; widthIn: number; heightIn: number; quantity: number; totalWeightLb: number; }; export type FreightDensityResult = { cubicFeet: number; densityPcf: number; /** 查表得到的密度分等(仅 13 档之一) */ suggestedClass: string; rangeLabelZh: string; }; export function computeFreightDensity( input: FreightDensityInput, ): FreightDensityResult | null { const { lengthIn, widthIn, heightIn, quantity, totalWeightLb } = input; if ( !(lengthIn > 0) || !(widthIn > 0) || !(heightIn > 0) || !(quantity > 0) || !(totalWeightLb > 0) ) { return null; } const cubicInches = lengthIn * widthIn * heightIn * quantity; const cubicFeet = cubicInches / CUBIC_INCHES_PER_CU_FT; if (!(cubicFeet > 0)) return null; const densityPcf = totalWeightLb / cubicFeet; if (!Number.isFinite(densityPcf) || densityPcf <= 0) return null; const matched = matchDensityBand(densityPcf); if (!matched) return null; return { cubicFeet, densityPcf, suggestedClass: matched.freightClass, rangeLabelZh: matched.rangeLabelZh, }; } export function matchDensityBand(pcf: number) { if (!Number.isFinite(pcf) || pcf < 0) return null; for (const band of NMFC_DENSITY_CLASS_BANDS) { if (pcf >= band.minPcf && pcf < band.maxPcf) { return band; } } return null; } export function densityPcfToFreightClass(pcf: number): string | null { return matchDensityBand(pcf)?.freightClass ?? null; } /** 完整十三档表(密度↑ → 等级数字↓) */ export function formatNmfcDensityClassChartZh(): string { const rows = [...NMFC_DENSITY_CLASS_BANDS] .slice() .reverse() .map( (b) => `${b.rangeLabelZh} lbs/ft³ → 等级 ${b.freightClass}`, ); return [ "密度越高 → 等级数字越低(越便宜);体积大/重量轻 → 等级越高。", "NMFTA 2025 十三档(与 Flock 密度推算一致):", ...rows, "注:77.5 / 110 / 150 / 200 / 500 不能由密度算出,仅商品条件分等可选手选。", ].join("\n"); } /** 官网 i 弹层:有尺寸重量时三行明细;否则公式 + 完整分档表 */ export function formatFreightClassDensityTip( density: FreightDensityResult | null, ): string { if (!density) { return [ "总体积 = (长×宽×高×件数) ÷ 1728(立方英尺)", "密度 = 总重(磅) ÷ 总体积", formatNmfcDensityClassChartZh(), ].join("\n"); } return [ `总体积:${density.cubicFeet.toFixed(2)} ft³`, `密度:${density.densityPcf.toFixed(2)} lbs/ft³(区间 ${density.rangeLabelZh})`, `基于密度的货运等级:${density.suggestedClass}`, ].join("\n"); } /** 手选等级与密度推算不一致时返回提示;一致或「按密度估算」则 null */ export function freightClassDensityMismatchMessage( selectedClass: string, density: FreightDensityResult | null, ): string | null { if (!density) return null; if (!selectedClass || selectedClass === "density") return null; const selected = selectedClass.trim(); if (selected === density.suggestedClass) return null; return `根据货物尺寸与重量推算,请选择货运等级 ${density.suggestedClass}。`; } export function parsePositiveNumber(raw: string): number | null { const n = Number(String(raw).replace(/,/g, "").trim()); return Number.isFinite(n) && n > 0 ? n : null; }