inventory.cycle-count-schedule@1.0.0
impl/typescript.ts
2,662 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 { epochDayFromIso, isoFromEpochDay } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { dayOfWeek } from "./dates_day_of_week.ts"; ← from dates.day-of-week ^1.0.0 · built alongside by fune
import { type CountDay, type CountItem } from "./inventory_cycle_count_schedule_types.ts";
function byCode(a: string, b: string): number {
return a < b ? -1 : a > b ? 1 : 0;
}
/**
* Spread each item's counts evenly over the working days of a period: a class
* counted f times gets f even windows, and its items are spread evenly within
* each window.
*/
export function cycleCountSchedule(
items: readonly CountItem[],
countsPerPeriod: Readonly<Record<string, number>>,
startDate: string,
endDate: string,
holidays: readonly string[],
): readonly CountDay[] {
const first = epochDayFromIso(startDate);
const last = epochDayFromIso(endDate);
if (last < first) throw new RangeError(`endDate ${endDate} is before startDate ${startDate}`);
const closed = new Set<number>(holidays.map((h) => epochDayFromIso(h)));
const days: string[] = [];
for (let day = first; day <= last; day++) {
const iso = isoFromEpochDay(day);
if (dayOfWeek(iso) <= 5 && !closed.has(day)) days.push(iso);
}
const classes = Object.keys(countsPerPeriod).sort(byCode);
for (const cls of classes) {
const f = countsPerPeriod[cls];
if (!Number.isInteger(f) || f < 0) {
throw new RangeError(`countsPerPeriod for class "${cls}" must be a whole number, not negative, received ${f}`);
}
}
const byClass = new Map<string, string[]>();
const seen = new Set<string>();
for (const item of items) {
if (seen.has(item.sku)) throw new RangeError(`duplicate sku "${item.sku}"`);
seen.add(item.sku);
if (!Object.prototype.hasOwnProperty.call(countsPerPeriod, item.abcClass)) {
throw new RangeError(`no count frequency for class "${item.abcClass}" (sku "${item.sku}")`);
}
const list = byClass.get(item.abcClass) ?? [];
list.push(item.sku);
byClass.set(item.abcClass, list);
}
const n = days.length;
const schedule: string[][] = days.map(() => []);
for (const cls of classes) {
const skus = (byClass.get(cls) ?? []).slice().sort(byCode);
const f = countsPerPeriod[cls];
if (skus.length === 0 || f === 0) continue;
if (f > n) {
throw new RangeError(`class "${cls}" is counted ${f} times but the period has only ${n} working days`);
}
for (let k = 0; k < f; k++) {
const start = Math.floor((k * n) / f);
const length = Math.floor(((k + 1) * n) / f) - start;
skus.forEach((sku, j) => schedule[start + Math.floor((j * length) / skus.length)].push(sku));
}
}
return days.map((date, i) => ({ date, skus: schedule[i] }));
}