impl/typescript.ts
2,617 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { Z_SCORES } from "./inventory_safety_stock_data.ts"; ← this capability’s own data, compiled from data/z-scores.json into the same file by fune build
import { type SafetyStockInput } from "./inventory_safety_stock_types.ts";
function amount(name: string, value: number | null, method: string): number {
if (value === null || value === undefined) {
throw new RangeError(`${method} needs ${name}`);
}
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
throw new RangeError(`${name} must be a finite number, not negative, received ${value}`);
}
return value;
}
// Six decimal places, half away from zero, then up to whole units: float noise
// such as 55.00000000000001 must not cost a whole extra unit.
function wholeUnitsUp(x: number): number {
const y = x * 1e6;
let micro = Math.floor(y);
if (y - micro >= 0.5) micro += 1;
return Math.floor((micro + 999999) / 1e6);
}
/** Safety stock in whole units, by the service-level or max-minus-average method. */
export function safetyStock(input: SafetyStockInput): number {
const leadTime = amount("leadTime", input.leadTime, input.method);
if (input.method === "service-level") {
const level = input.serviceLevelBasisPoints;
if (level === null || level === undefined) throw new RangeError("service-level needs serviceLevelBasisPoints");
const row = Z_SCORES.find((r) => r.serviceLevelBasisPoints === level);
if (!row) {
throw new RangeError(`no z-score for a service level of ${level} basis points: use one of ${Z_SCORES.map((r) => r.serviceLevelBasisPoints).join(", ")}`);
}
const sd = amount("demandStdDev", input.demandStdDev, input.method);
let variance = leadTime * sd * sd;
const sl = input.leadTimeStdDev ?? 0;
amount("leadTimeStdDev", sl, input.method);
if (sl > 0) {
const d = amount("averageDemand", input.averageDemand, "service-level with a varying lead time");
variance = variance + d * d * sl * sl;
}
return wholeUnitsUp((row.zTenThousandths / 10000) * Math.sqrt(variance));
}
if (input.method === "max-minus-average") {
const maxDemand = amount("maxDemand", input.maxDemand, input.method);
const maxLeadTime = amount("maxLeadTime", input.maxLeadTime, input.method);
const averageDemand = amount("averageDemand", input.averageDemand, input.method);
const worst = maxDemand * maxLeadTime;
const usual = averageDemand * leadTime;
if (worst < usual) {
throw new RangeError(`maxDemand x maxLeadTime (${worst}) must not be less than averageDemand x leadTime (${usual})`);
}
return wholeUnitsUp(worst - usual);
}
throw new RangeError(`unknown safety stock method "${input.method}"`);
}