Functional Weave
Code in Rust

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] }));
}