|
|
/**
|
|
|
* 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 且 <max」,末档 ≥50 → 50)
|
|
|
*
|
|
|
* 密度推算只会落到下表 13 档。下拉中的 77.5 / 110 / 150 / 200 / 500
|
|
|
* 属于商品条件分等(handling/stowability/liability),不能由密度公式算出。
|
|
|
*/
|
|
|
|
|
|
export const CUBIC_INCHES_PER_CU_FT = 1728;
|
|
|
|
|
|
/**
|
|
|
* NMFTA 2025 十三档:自高密度→低密度。
|
|
|
* 文案口径:Sub N · min – <max pcf → Class C(与 NMFTA “X but less than Y” 一致)
|
|
|
*/
|
|
|
export const NMFC_DENSITY_CLASS_BANDS: ReadonlyArray<{
|
|
|
sub: number;
|
|
|
minPcf: number;
|
|
|
maxPcf: number;
|
|
|
freightClass: string;
|
|
|
/** 中文区间说明,供 tip / 单测展示 */
|
|
|
rangeLabelZh: string;
|
|
|
}> = [
|
|
|
{
|
|
|
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;
|
|
|
}
|