You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chajia/lib/flock/freight-class-density.ts

248 lines
6.5 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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;
}