@logistics-ts/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,423 @@
1
+ /**
2
+ * The {@link Explained} result wrapper — the cross-cutting contract that every
3
+ * numeric output in logistics-ts returns. Instead of a bare number, callers get
4
+ * the value alongside a machine-readable account of how it was derived, so that
5
+ * humans and AI agents can inspect the method, the inputs, and the reasoning.
6
+ */
7
+ /**
8
+ * A computed result paired with a machine-readable explanation of how it was
9
+ * derived.
10
+ *
11
+ * @typeParam T - The type of the computed value.
12
+ */
13
+ interface Explained<T> {
14
+ /** The computed value. */
15
+ value: T;
16
+ /** Identifier of the method used, e.g. `"king-formula"`. */
17
+ method: string;
18
+ /** Every input value that fed the computation, keyed by name. */
19
+ inputs: Record<string, number | string>;
20
+ /** Human- and agent-readable bullet points explaining the result. */
21
+ reasoning: string[];
22
+ /** Optional literature citations backing the method. */
23
+ citations?: string[];
24
+ /** Optional caveats, e.g. low sample size or an undefined-metric condition. */
25
+ warnings?: string[];
26
+ }
27
+ /** The explanation metadata of an {@link Explained} result, without its value. */
28
+ type Explanation<T> = Omit<Explained<T>, 'value'>;
29
+ /**
30
+ * Constructs an {@link Explained} result from a value and its explanation.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * explain(120, {
35
+ * method: 'king-formula',
36
+ * inputs: { serviceLevel: 0.95, leadTimeDays: 14 },
37
+ * reasoning: ['95% service level', 'demand variability dominates'],
38
+ * })
39
+ * ```
40
+ */
41
+ declare function explain<T>(value: T, explanation: Explanation<T>): Explained<T>;
42
+
43
+ /**
44
+ * The canonical record types every logistics-ts analysis speaks. Loaders map
45
+ * arbitrary user data onto these shapes (see {@link ./table/loader}); all
46
+ * algorithms consume them rather than raw user objects.
47
+ *
48
+ * Dates are treated as **calendar dates**, never instants. A `Date` or an ISO
49
+ * date string (`"2026-01-31"`) is accepted for a record's date field. Records
50
+ * preserve whatever `DateInput` you pass; date arithmetic (bucketization,
51
+ * lead-time spans) converts to an integer epoch-day at computation time — it
52
+ * does not rewrite the stored value (see {@link ./time/epoch-day}).
53
+ */
54
+ /** A date at the API boundary: a `Date` or an ISO `YYYY-MM-DD` string. */
55
+ type DateInput = Date | string;
56
+ /**
57
+ * A quantity of an item demanded (sold/shipped/consumed) on a date. The atomic
58
+ * input to forecasting and demand classification.
59
+ */
60
+ interface DemandRecord {
61
+ /** Stable identifier of the item (SKU). */
62
+ itemId: string;
63
+ /** Calendar date the demand occurred. */
64
+ date: DateInput;
65
+ /** Quantity demanded. Must be finite and non-negative. */
66
+ quantity: number;
67
+ /** Optional stocking location, for location-aware analysis. */
68
+ locationId?: string;
69
+ /** Optional unit price at the time, used for value-based ABC classification. */
70
+ unitPrice?: number;
71
+ }
72
+ /** A snapshot of on-hand stock for an item. */
73
+ interface StockRecord {
74
+ /** Stable identifier of the item (SKU). */
75
+ itemId: string;
76
+ /** Quantity on hand. Must be finite and non-negative. */
77
+ quantity: number;
78
+ /** Optional stocking location. */
79
+ locationId?: string;
80
+ /** Optional unit cost, used for value-based classification and coverage. */
81
+ unitCost?: number;
82
+ /** Optional time the snapshot was taken. */
83
+ timestamp?: DateInput;
84
+ }
85
+ /**
86
+ * An observed replenishment lead time for an item — one record per receipt or
87
+ * purchase order — so that lead-time variability (σ_LT) can be estimated.
88
+ */
89
+ interface LeadTimeRecord {
90
+ /** Stable identifier of the item (SKU). */
91
+ itemId: string;
92
+ /** Observed lead time in days. Must be finite and non-negative. */
93
+ leadTimeDays: number;
94
+ /** Optional date the receipt/order was observed. */
95
+ date?: DateInput;
96
+ }
97
+
98
+ /**
99
+ * Input abstractions for the loaders. logistics-ts accepts three shapes of
100
+ * tabular input and normalises them to a single row reader:
101
+ *
102
+ * - **row input** — an array of plain objects (`{ sku, qty, ... }[]`);
103
+ * - **columnar input** — an object of parallel arrays (`{ sku: [...], qty: [...] }`);
104
+ * - **{@link TableSource}** — the seam a future DuckDB/Parquet adapter implements
105
+ * to hand column-oriented data to the loaders without materialising objects.
106
+ */
107
+ /** A column-oriented source of tabular data. The adapter seam. */
108
+ interface TableSource {
109
+ /** Number of rows. */
110
+ readonly numRows: number;
111
+ /** Names of the available columns. */
112
+ readonly columnNames: readonly string[];
113
+ /** Returns the values of a column as an indexable, row-aligned sequence. */
114
+ getColumn(name: string): ArrayLike<unknown>;
115
+ }
116
+ /**
117
+ * An array of row objects keyed by column name. Typed to `object` so that
118
+ * typed record arrays (interfaces, which TypeScript does not consider
119
+ * assignable to an index signature) are accepted without a cast.
120
+ */
121
+ type RowInput = ReadonlyArray<object>;
122
+ /** An object of parallel, row-aligned column arrays. */
123
+ type ColumnarInput = Record<string, ArrayLike<unknown>>;
124
+ /** Any accepted tabular input. */
125
+ type TableInput = RowInput | ColumnarInput | TableSource;
126
+
127
+ /**
128
+ * Loaders that map arbitrary tabular input onto the canonical record types via a
129
+ * column mapping, coercing CSV-style string cells and collecting per-row
130
+ * validation issues rather than throwing on the first bad row.
131
+ *
132
+ * Structural problems throw immediately — that is a configuration mistake. A
133
+ * structural problem is a required column missing entirely, or a column the
134
+ * caller explicitly named in the mapping that does not exist in the input.
135
+ * (Empty input is not structural: it loads as zero records.)
136
+ *
137
+ * Per-row data problems are collected as {@link LoadIssue}s, unless
138
+ * `throwOnIssue` is set. An invalid **required** field (a non-numeric quantity,
139
+ * an unparseable date) skips the row; an invalid **optional** field (a
140
+ * non-numeric unit price) keeps the row and omits the field — but is still
141
+ * recorded as an issue. A genuinely absent optional cell is not an issue.
142
+ */
143
+
144
+ /** A single per-row validation problem. */
145
+ interface LoadIssue {
146
+ /** Zero-based row index in the input. */
147
+ row: number;
148
+ /** The source column the problem concerns. */
149
+ column: string;
150
+ /** Human- and agent-readable description of the problem. */
151
+ problem: string;
152
+ }
153
+ /** The result of a load: the valid records plus any collected issues. */
154
+ interface LoadResult<T> {
155
+ records: T[];
156
+ issues: LoadIssue[];
157
+ }
158
+ interface LoadOptions {
159
+ /** Throw on the first data issue instead of collecting it and skipping the row. */
160
+ throwOnIssue?: boolean;
161
+ }
162
+ /** Maps canonical {@link DemandRecord} fields to source column names. */
163
+ interface DemandColumnMap {
164
+ itemId?: string;
165
+ date?: string;
166
+ quantity?: string;
167
+ locationId?: string;
168
+ unitPrice?: string;
169
+ }
170
+ /** Maps canonical {@link StockRecord} fields to source column names. */
171
+ interface StockColumnMap {
172
+ itemId?: string;
173
+ quantity?: string;
174
+ locationId?: string;
175
+ unitCost?: string;
176
+ timestamp?: string;
177
+ }
178
+ /** Maps canonical {@link LeadTimeRecord} fields to source column names. */
179
+ interface LeadTimeColumnMap {
180
+ itemId?: string;
181
+ leadTimeDays?: string;
182
+ date?: string;
183
+ }
184
+ /** Loads and validates {@link DemandRecord}s from tabular input. */
185
+ declare function loadDemand(input: TableInput, mapping?: DemandColumnMap, options?: LoadOptions): LoadResult<DemandRecord>;
186
+ /** Loads and validates {@link StockRecord}s from tabular input. */
187
+ declare function loadStock(input: TableInput, mapping?: StockColumnMap, options?: LoadOptions): LoadResult<StockRecord>;
188
+ /** Loads and validates {@link LeadTimeRecord}s from tabular input. */
189
+ declare function loadLeadTimes(input: TableInput, mapping?: LeadTimeColumnMap, options?: LoadOptions): LoadResult<LeadTimeRecord>;
190
+
191
+ /**
192
+ * Calendar-date handling. logistics-ts treats every date as a **calendar date**
193
+ * (never an instant with a timezone), represented internally as an integer
194
+ * "epoch day" — the number of whole days since 1970-01-01 in UTC. This keeps
195
+ * bucketization deterministic and free of daylight-saving/timezone drift, with
196
+ * no date-library dependency.
197
+ */
198
+
199
+ /**
200
+ * Converts a {@link DateInput} to an integer epoch day (days since 1970-01-01,
201
+ * UTC). ISO strings are read by their calendar `YYYY-MM-DD` part (a trailing
202
+ * time part is accepted and ignored); `Date` values are read by their UTC
203
+ * year/month/day. Throws on an unparseable value.
204
+ */
205
+ declare function toEpochDay(input: DateInput): number;
206
+ /**
207
+ * Converts an epoch day back to a UTC-midnight `Date`. An epoch day is an
208
+ * integer count of days; a non-integer input is rejected so the midnight
209
+ * contract holds.
210
+ */
211
+ declare function fromEpochDay(epochDay: number): Date;
212
+ /** Formats an epoch day as an ISO `YYYY-MM-DD` calendar date. */
213
+ declare function formatEpochDay(epochDay: number): string;
214
+ /** ISO weekday of an epoch day, Monday = 0 … Sunday = 6. */
215
+ declare function isoWeekday(epochDay: number): number;
216
+
217
+ /**
218
+ * Aggregates raw demand records into dense, chronological, **zero-filled** time
219
+ * series per item. Zero-filling is essential: intermittent-demand statistics
220
+ * (ADI, CV²) and forecasting models are only meaningful when the periods with no
221
+ * demand are present as explicit zeros rather than missing rows.
222
+ */
223
+
224
+ /** Time granularity for bucketization. */
225
+ type Granularity = 'day' | 'week' | 'month';
226
+ /** A single dense time bucket. */
227
+ interface DemandBucket {
228
+ /**
229
+ * Canonical period label: `YYYY-MM-DD` for `day`, the Monday's `YYYY-MM-DD`
230
+ * for `week`, and `YYYY-MM` for `month`.
231
+ */
232
+ period: string;
233
+ /** Total demand in the period (0 for zero-filled gaps). */
234
+ quantity: number;
235
+ }
236
+ /** A dense, chronological, zero-filled demand series for one item. */
237
+ interface DemandSeries {
238
+ itemId: string;
239
+ granularity: Granularity;
240
+ buckets: DemandBucket[];
241
+ }
242
+ interface BucketizeOptions {
243
+ /**
244
+ * Inclusive range start. When omitted, each item's series starts at its own
245
+ * earliest demand. Provide this to align every item to a common calendar.
246
+ */
247
+ start?: DateInput;
248
+ /** Inclusive range end. When omitted, each item's series ends at its latest demand. */
249
+ end?: DateInput;
250
+ }
251
+ /**
252
+ * Buckets demand records into dense, zero-filled series — one per item, sorted
253
+ * by `itemId`, each chronologically ordered.
254
+ *
255
+ * @param records - Raw demand records; multiple records in the same period are summed.
256
+ * @param granularity - `day`, `week`, or `month`.
257
+ * @param options - Optional common calendar range (see {@link BucketizeOptions}).
258
+ */
259
+ declare function bucketize(records: readonly DemandRecord[], granularity: Granularity, options?: BucketizeOptions): DemandSeries[];
260
+
261
+ /**
262
+ * Descriptive statistics used throughout logistics-ts. Sample statistics use the
263
+ * unbiased (n − 1) denominator by default; pass `population: true` for the
264
+ * biased (n) form where the data is the whole population.
265
+ */
266
+ /** Arithmetic mean. Returns `NaN` for an empty input. */
267
+ declare function mean(values: readonly number[]): number;
268
+ /**
269
+ * Variance. Sample (n − 1) by default; population (n) when `population` is true.
270
+ * Returns `NaN` for the sample form given fewer than two values, and for the
271
+ * population form given an empty input.
272
+ */
273
+ declare function variance(values: readonly number[], population?: boolean): number;
274
+ /** Standard deviation — the square root of {@link variance}. */
275
+ declare function standardDeviation(values: readonly number[], population?: boolean): number;
276
+ /**
277
+ * Coefficient of variation: standard deviation divided by the mean. A unitless
278
+ * measure of relative dispersion. Returns `NaN` when the mean is zero.
279
+ */
280
+ declare function coefficientOfVariation(values: readonly number[], population?: boolean): number;
281
+ /**
282
+ * Squared coefficient of variation (CV²) of the **non-zero** values only.
283
+ *
284
+ * This is the demand-lumpiness axis of the Syntetos–Boylan–Croston
285
+ * classification: CV² is computed over demand *sizes* (the periods with demand),
286
+ * ignoring the zero periods. Returns `NaN` when there are fewer than two
287
+ * non-zero values.
288
+ *
289
+ * @see Syntetos, A.A., Boylan, J.E. & Croston, J.D. (2005). On the categorization
290
+ * of demand patterns. Journal of the Operational Research Society, 56(5).
291
+ */
292
+ declare function squaredCvOfNonZero(values: readonly number[]): number;
293
+ /**
294
+ * Average Demand Interval (ADI): the mean number of periods between consecutive
295
+ * non-zero demands, measured across the full series length.
296
+ *
297
+ * Defined as `periods / numberOfNonZeroDemands`. This is the demand-intermittence
298
+ * axis of the Syntetos–Boylan–Croston classification. Returns `NaN` when there
299
+ * are no non-zero demands.
300
+ *
301
+ * @param series - Demand per period, including zero periods.
302
+ * @see Syntetos, Boylan & Croston (2005).
303
+ */
304
+ declare function averageDemandInterval(series: readonly number[]): number;
305
+
306
+ /**
307
+ * Standard-normal distribution helpers. These back service-level → z-score
308
+ * conversions (safety stock) and the unit normal loss integral (fill-rate).
309
+ */
310
+ /** Standard-normal probability density function, φ(z). */
311
+ declare function normalPdf(z: number): number;
312
+ /**
313
+ * Standard-normal cumulative distribution function, Φ(z). Accuracy follows the
314
+ * underlying erf approximation — absolute error ≈ 1.5 × 10⁻⁷, coarser than
315
+ * {@link inverseNormalCdf}'s 1.15 × 10⁻⁹ and inherited by
316
+ * {@link normalLossFunction}.
317
+ */
318
+ declare function normalCdf(z: number): number;
319
+ /**
320
+ * Inverse standard-normal CDF (the quantile / probit function): returns the z
321
+ * such that Φ(z) = p.
322
+ *
323
+ * Uses Acklam's rational approximation, with a relative error below
324
+ * 1.15 × 10⁻⁹ across the open interval (0, 1). Returns `-Infinity` at p = 0,
325
+ * `+Infinity` at p = 1, and `NaN` outside [0, 1].
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * inverseNormalCdf(0.95) // ≈ 1.6448536 (95% service level)
330
+ * inverseNormalCdf(0.975) // ≈ 1.9599640
331
+ * ```
332
+ * @see Acklam, P.J. (2003). An algorithm for computing the inverse normal
333
+ * cumulative distribution function.
334
+ */
335
+ declare function inverseNormalCdf(p: number): number;
336
+ /**
337
+ * Unit normal loss function, E(z) = φ(z) − z · (1 − Φ(z)): the expected shortfall
338
+ * per unit of standard deviation for a standard-normal demand. Used to translate
339
+ * a target **fill rate** (units satisfied) into safety stock — as distinct from
340
+ * a cycle service level (order cycles without stockout).
341
+ *
342
+ * @see Silver, E.A., Pyke, D.F. & Thomas, D.J. (2017). Inventory and Production
343
+ * Management in Supply Chains, 4th ed.
344
+ */
345
+ declare function normalLossFunction(z: number): number;
346
+
347
+ /**
348
+ * Nelder–Mead simplex minimiser — a derivative-free optimiser used to fit
349
+ * smoothing parameters (e.g. α, β, γ for exponential smoothing) by minimising a
350
+ * sum-of-squared-errors objective. Dependency-free.
351
+ *
352
+ * The search is unconstrained; to impose bounds (such as 0 ≤ α ≤ 1), have the
353
+ * objective return a large penalty outside the feasible region.
354
+ *
355
+ * @see Nelder, J.A. & Mead, R. (1965). A simplex method for function
356
+ * minimization. The Computer Journal, 7(4), 308–313.
357
+ */
358
+ /** A scalar objective over an n-dimensional parameter vector. */
359
+ type Objective = (x: readonly number[]) => number;
360
+ interface NelderMeadOptions {
361
+ /** Initial simplex edge length per dimension. Default 0.1. */
362
+ step?: number;
363
+ /** Maximum iterations before giving up. Default 200. */
364
+ maxIterations?: number;
365
+ /**
366
+ * Convergence tolerance on the spread of objective values across the simplex.
367
+ * Default 1e-8.
368
+ */
369
+ tolerance?: number;
370
+ }
371
+ interface NelderMeadResult {
372
+ /** The best parameter vector found. */
373
+ x: number[];
374
+ /** The objective value at {@link NelderMeadResult.x}. */
375
+ fx: number;
376
+ /** Iterations performed. */
377
+ iterations: number;
378
+ /** Whether the tolerance was met before `maxIterations`. */
379
+ converged: boolean;
380
+ }
381
+ /** Minimises `f` starting from `x0` using the Nelder–Mead simplex method. */
382
+ declare function nelderMead(f: Objective, x0: readonly number[], options?: NelderMeadOptions): NelderMeadResult;
383
+
384
+ /**
385
+ * Deterministic synthetic supply-chain data. Given a seed, this produces a
386
+ * repeatable catalogue of demand history, on-hand stock, and observed lead
387
+ * times — so that examples, demos, and AI agents can exercise the library with
388
+ * zero setup and realistic demand shapes.
389
+ */
390
+
391
+ /** The statistical character of an item's demand. */
392
+ type DemandProfile = 'smooth' | 'seasonal' | 'trending' | 'intermittent' | 'lumpy' | 'mixed';
393
+ interface GenerateOptions {
394
+ /** Number of items (SKUs) to generate. Default 10. */
395
+ items?: number;
396
+ /** Number of periods of history per item. Default 24. */
397
+ periods?: number;
398
+ /** Demand profile applied to every item, or `'mixed'` to vary it. Default `'mixed'`. */
399
+ profile?: DemandProfile;
400
+ /** Spacing between periods. Default `'month'`. */
401
+ granularity?: Granularity;
402
+ /** Date of the first period. Default `'2024-01-01'`. */
403
+ startDate?: DateInput;
404
+ /** Seed for the PRNG. The same seed always yields the same dataset. Default 1. */
405
+ seed?: number;
406
+ }
407
+ /** A self-contained synthetic dataset. */
408
+ interface ExampleDataset {
409
+ demand: DemandRecord[];
410
+ stock: StockRecord[];
411
+ leadTimes: LeadTimeRecord[];
412
+ }
413
+ /**
414
+ * Generates a deterministic synthetic dataset.
415
+ *
416
+ * @example
417
+ * ```ts
418
+ * const { demand, stock, leadTimes } = generateExampleData({ items: 20, periods: 36 })
419
+ * ```
420
+ */
421
+ declare function generateExampleData(options?: GenerateOptions): ExampleDataset;
422
+
423
+ export { type BucketizeOptions, type ColumnarInput, type DateInput, type DemandBucket, type DemandColumnMap, type DemandProfile, type DemandRecord, type DemandSeries, type ExampleDataset, type Explained, type Explanation, type GenerateOptions, type Granularity, type LeadTimeColumnMap, type LeadTimeRecord, type LoadIssue, type LoadOptions, type LoadResult, type NelderMeadOptions, type NelderMeadResult, type Objective, type RowInput, type StockColumnMap, type StockRecord, type TableInput, type TableSource, averageDemandInterval, bucketize, coefficientOfVariation, explain, formatEpochDay, fromEpochDay, generateExampleData, inverseNormalCdf, isoWeekday, loadDemand, loadLeadTimes, loadStock, mean, nelderMead, normalCdf, normalLossFunction, normalPdf, squaredCvOfNonZero, standardDeviation, toEpochDay, variance };
package/dist/index.js ADDED
@@ -0,0 +1,602 @@
1
+ // src/explained.ts
2
+ function explain(value, explanation) {
3
+ return { value, ...explanation };
4
+ }
5
+
6
+ // src/time/epoch-day.ts
7
+ var MS_PER_DAY = 864e5;
8
+ var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
9
+ function toEpochDay(input) {
10
+ if (input instanceof Date) {
11
+ if (Number.isNaN(input.getTime())) throw new RangeError("Invalid Date");
12
+ return Math.floor(
13
+ Date.UTC(input.getUTCFullYear(), input.getUTCMonth(), input.getUTCDate()) / MS_PER_DAY
14
+ );
15
+ }
16
+ const match = ISO_DATE.exec(input);
17
+ if (!match) throw new RangeError(`Unparseable date: ${JSON.stringify(input)}`);
18
+ const year = Number(match[1]);
19
+ const month = Number(match[2]) - 1;
20
+ const day = Number(match[3]);
21
+ const utc = Date.UTC(year, month, day);
22
+ const d = new Date(utc);
23
+ if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month || d.getUTCDate() !== day) {
24
+ throw new RangeError(`Invalid calendar date: ${input}`);
25
+ }
26
+ return Math.floor(utc / MS_PER_DAY);
27
+ }
28
+ function fromEpochDay(epochDay) {
29
+ if (!Number.isInteger(epochDay)) {
30
+ throw new RangeError(`Epoch day must be an integer, got ${epochDay}`);
31
+ }
32
+ return new Date(epochDay * MS_PER_DAY);
33
+ }
34
+ function formatEpochDay(epochDay) {
35
+ return fromEpochDay(epochDay).toISOString().slice(0, 10);
36
+ }
37
+ function isoWeekday(epochDay) {
38
+ return ((epochDay % 7 + 4) % 7 + 6) % 7;
39
+ }
40
+
41
+ // src/table/table-source.ts
42
+ function isTableSource(input) {
43
+ const candidate = input;
44
+ return !Array.isArray(input) && typeof candidate.getColumn === "function" && typeof candidate.numRows === "number" && Array.isArray(candidate.columnNames);
45
+ }
46
+ function normalizeInput(input) {
47
+ if (Array.isArray(input)) {
48
+ const rows = input;
49
+ return {
50
+ numRows: rows.length,
51
+ // Scan for the column rather than trusting the first row — rows may be
52
+ // sparse. `some` short-circuits, so the homogeneous case stays O(1). Use an
53
+ // own-property check so inherited members (`toString`, …) are not columns.
54
+ hasColumn: (name) => rows.some((r) => Object.hasOwn(r, name)),
55
+ getCell: (row, column) => rows[row]?.[column]
56
+ };
57
+ }
58
+ if (isTableSource(input)) {
59
+ const columns2 = new Set(input.columnNames);
60
+ const cache = /* @__PURE__ */ new Map();
61
+ const column = (name) => {
62
+ let col = cache.get(name);
63
+ if (!col) {
64
+ col = input.getColumn(name);
65
+ cache.set(name, col);
66
+ }
67
+ return col;
68
+ };
69
+ return {
70
+ numRows: input.numRows,
71
+ hasColumn: (name) => columns2.has(name),
72
+ getCell: (row, name) => columns2.has(name) ? column(name)[row] : void 0
73
+ };
74
+ }
75
+ const columnar = input;
76
+ const names = Object.keys(columnar);
77
+ let numRows = 0;
78
+ for (const name of names) numRows = Math.max(numRows, columnar[name]?.length ?? 0);
79
+ const columns = new Set(names);
80
+ return {
81
+ numRows,
82
+ hasColumn: (name) => columns.has(name),
83
+ getCell: (row, name) => columnar[name]?.[row]
84
+ };
85
+ }
86
+
87
+ // src/table/loader.ts
88
+ function toStringCell(value) {
89
+ if (value === null || value === void 0) return null;
90
+ if (typeof value === "string") {
91
+ const trimmed = value.trim();
92
+ return trimmed === "" ? null : trimmed;
93
+ }
94
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
95
+ return null;
96
+ }
97
+ function toNumberCell(value) {
98
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
99
+ if (typeof value === "string") {
100
+ const trimmed = value.trim();
101
+ if (trimmed === "") return null;
102
+ const n = Number(trimmed);
103
+ return Number.isFinite(n) ? n : null;
104
+ }
105
+ return null;
106
+ }
107
+ function toDateCell(value) {
108
+ if (!(value instanceof Date) && typeof value !== "string") return null;
109
+ try {
110
+ toEpochDay(value);
111
+ return value;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+ function raise(ctx, column, problem) {
117
+ const issue = { row: ctx.row, column, problem };
118
+ if (ctx.throwOnIssue) {
119
+ throw new Error(`Row ${ctx.row}, column "${column}": ${problem}`);
120
+ }
121
+ ctx.issues.push(issue);
122
+ return void 0;
123
+ }
124
+ function isAbsent(value) {
125
+ return value === null || value === void 0 || typeof value === "string" && value.trim() === "";
126
+ }
127
+ function requireQuantity(value, column, ctx) {
128
+ const n = toNumberCell(value);
129
+ if (n === null) return raise(ctx, column, "expected a finite number");
130
+ if (n < 0) return raise(ctx, column, "must not be negative");
131
+ return n;
132
+ }
133
+ function optionalNumber(value, column, ctx) {
134
+ if (isAbsent(value)) return void 0;
135
+ return requireQuantity(value, column, ctx);
136
+ }
137
+ function optionalDate(value, column, ctx) {
138
+ if (isAbsent(value)) return void 0;
139
+ const date = toDateCell(value);
140
+ if (date === null) return raise(ctx, column, "expected a calendar date (Date or ISO YYYY-MM-DD)");
141
+ return date;
142
+ }
143
+ function optionalString(value, column, ctx) {
144
+ if (isAbsent(value)) return void 0;
145
+ const s = toStringCell(value);
146
+ if (s === null) return raise(ctx, column, "expected a string");
147
+ return s;
148
+ }
149
+ function requireColumns(reader, columns) {
150
+ if (reader.numRows === 0) return;
151
+ const missing = columns.filter((c) => c !== void 0 && !reader.hasColumn(c));
152
+ if (missing.length > 0) {
153
+ throw new Error(`Input is missing expected column(s): ${missing.join(", ")}`);
154
+ }
155
+ }
156
+ function loadDemand(input, mapping = {}, options = {}) {
157
+ const reader = normalizeInput(input);
158
+ const col = {
159
+ itemId: mapping.itemId ?? "itemId",
160
+ date: mapping.date ?? "date",
161
+ quantity: mapping.quantity ?? "quantity",
162
+ locationId: mapping.locationId ?? "locationId",
163
+ unitPrice: mapping.unitPrice ?? "unitPrice"
164
+ };
165
+ requireColumns(reader, [
166
+ col.itemId,
167
+ col.date,
168
+ col.quantity,
169
+ mapping.locationId,
170
+ mapping.unitPrice
171
+ ]);
172
+ const throwOnIssue = options.throwOnIssue ?? false;
173
+ const records = [];
174
+ const issues = [];
175
+ for (let row = 0; row < reader.numRows; row++) {
176
+ const ctx = { row, issues, throwOnIssue };
177
+ const itemId = toStringCell(reader.getCell(row, col.itemId));
178
+ if (itemId === null) {
179
+ raise(ctx, col.itemId, "expected a non-empty item id");
180
+ continue;
181
+ }
182
+ const date = toDateCell(reader.getCell(row, col.date));
183
+ if (date === null) {
184
+ raise(ctx, col.date, "expected a calendar date (Date or ISO YYYY-MM-DD)");
185
+ continue;
186
+ }
187
+ const quantity = requireQuantity(reader.getCell(row, col.quantity), col.quantity, ctx);
188
+ if (quantity === void 0) continue;
189
+ const record = { itemId, date, quantity };
190
+ const locationId = optionalString(reader.getCell(row, col.locationId), col.locationId, ctx);
191
+ if (locationId !== void 0) record.locationId = locationId;
192
+ const unitPrice = optionalNumber(reader.getCell(row, col.unitPrice), col.unitPrice, ctx);
193
+ if (unitPrice !== void 0) record.unitPrice = unitPrice;
194
+ records.push(record);
195
+ }
196
+ return { records, issues };
197
+ }
198
+ function loadStock(input, mapping = {}, options = {}) {
199
+ const reader = normalizeInput(input);
200
+ const col = {
201
+ itemId: mapping.itemId ?? "itemId",
202
+ quantity: mapping.quantity ?? "quantity",
203
+ locationId: mapping.locationId ?? "locationId",
204
+ unitCost: mapping.unitCost ?? "unitCost",
205
+ timestamp: mapping.timestamp ?? "timestamp"
206
+ };
207
+ requireColumns(reader, [
208
+ col.itemId,
209
+ col.quantity,
210
+ mapping.locationId,
211
+ mapping.unitCost,
212
+ mapping.timestamp
213
+ ]);
214
+ const throwOnIssue = options.throwOnIssue ?? false;
215
+ const records = [];
216
+ const issues = [];
217
+ for (let row = 0; row < reader.numRows; row++) {
218
+ const ctx = { row, issues, throwOnIssue };
219
+ const itemId = toStringCell(reader.getCell(row, col.itemId));
220
+ if (itemId === null) {
221
+ raise(ctx, col.itemId, "expected a non-empty item id");
222
+ continue;
223
+ }
224
+ const quantity = requireQuantity(reader.getCell(row, col.quantity), col.quantity, ctx);
225
+ if (quantity === void 0) continue;
226
+ const record = { itemId, quantity };
227
+ const locationId = optionalString(reader.getCell(row, col.locationId), col.locationId, ctx);
228
+ if (locationId !== void 0) record.locationId = locationId;
229
+ const unitCost = optionalNumber(reader.getCell(row, col.unitCost), col.unitCost, ctx);
230
+ if (unitCost !== void 0) record.unitCost = unitCost;
231
+ const timestamp = optionalDate(reader.getCell(row, col.timestamp), col.timestamp, ctx);
232
+ if (timestamp !== void 0) record.timestamp = timestamp;
233
+ records.push(record);
234
+ }
235
+ return { records, issues };
236
+ }
237
+ function loadLeadTimes(input, mapping = {}, options = {}) {
238
+ const reader = normalizeInput(input);
239
+ const col = {
240
+ itemId: mapping.itemId ?? "itemId",
241
+ leadTimeDays: mapping.leadTimeDays ?? "leadTimeDays",
242
+ date: mapping.date ?? "date"
243
+ };
244
+ requireColumns(reader, [col.itemId, col.leadTimeDays, mapping.date]);
245
+ const throwOnIssue = options.throwOnIssue ?? false;
246
+ const records = [];
247
+ const issues = [];
248
+ for (let row = 0; row < reader.numRows; row++) {
249
+ const ctx = { row, issues, throwOnIssue };
250
+ const itemId = toStringCell(reader.getCell(row, col.itemId));
251
+ if (itemId === null) {
252
+ raise(ctx, col.itemId, "expected a non-empty item id");
253
+ continue;
254
+ }
255
+ const leadTimeDays = requireQuantity(
256
+ reader.getCell(row, col.leadTimeDays),
257
+ col.leadTimeDays,
258
+ ctx
259
+ );
260
+ if (leadTimeDays === void 0) continue;
261
+ const record = { itemId, leadTimeDays };
262
+ const date = optionalDate(reader.getCell(row, col.date), col.date, ctx);
263
+ if (date !== void 0) record.date = date;
264
+ records.push(record);
265
+ }
266
+ return { records, issues };
267
+ }
268
+
269
+ // src/time/bucketize.ts
270
+ function bucketKey(epochDay, granularity) {
271
+ switch (granularity) {
272
+ case "day":
273
+ return epochDay;
274
+ case "week":
275
+ return epochDay - isoWeekday(epochDay);
276
+ case "month": {
277
+ const d = fromEpochDay(epochDay);
278
+ return d.getUTCFullYear() * 12 + d.getUTCMonth();
279
+ }
280
+ }
281
+ }
282
+ function keyStep(granularity) {
283
+ return granularity === "week" ? 7 : 1;
284
+ }
285
+ function keyToLabel(key, granularity) {
286
+ if (granularity === "month") {
287
+ const year = Math.floor(key / 12);
288
+ const month = key % 12;
289
+ return `${year}-${String(month + 1).padStart(2, "0")}`;
290
+ }
291
+ return formatEpochDay(key);
292
+ }
293
+ function bucketize(records, granularity, options = {}) {
294
+ const step = keyStep(granularity);
295
+ const rangeStart = options.start !== void 0 ? bucketKey(toEpochDay(options.start), granularity) : void 0;
296
+ const rangeEnd = options.end !== void 0 ? bucketKey(toEpochDay(options.end), granularity) : void 0;
297
+ if (rangeStart !== void 0 && rangeEnd !== void 0 && rangeStart > rangeEnd) {
298
+ throw new RangeError(
299
+ `bucketize range start (${String(options.start)}) must not be after end (${String(options.end)})`
300
+ );
301
+ }
302
+ const perItem = /* @__PURE__ */ new Map();
303
+ for (const record of records) {
304
+ const key = bucketKey(toEpochDay(record.date), granularity);
305
+ let entry = perItem.get(record.itemId);
306
+ if (!entry) {
307
+ entry = { totals: /* @__PURE__ */ new Map(), min: key, max: key };
308
+ perItem.set(record.itemId, entry);
309
+ }
310
+ entry.totals.set(key, (entry.totals.get(key) ?? 0) + record.quantity);
311
+ if (key < entry.min) entry.min = key;
312
+ if (key > entry.max) entry.max = key;
313
+ }
314
+ const series = [];
315
+ for (const itemId of [...perItem.keys()].sort()) {
316
+ const entry = perItem.get(itemId);
317
+ const from = rangeStart ?? entry.min;
318
+ const to = rangeEnd ?? entry.max;
319
+ const buckets = [];
320
+ for (let key = from; key <= to; key += step) {
321
+ buckets.push({ period: keyToLabel(key, granularity), quantity: entry.totals.get(key) ?? 0 });
322
+ }
323
+ series.push({ itemId, granularity, buckets });
324
+ }
325
+ return series;
326
+ }
327
+
328
+ // src/numerics/stats.ts
329
+ function mean(values) {
330
+ if (values.length === 0) return Number.NaN;
331
+ let sum = 0;
332
+ for (const v of values) sum += v;
333
+ return sum / values.length;
334
+ }
335
+ function variance(values, population = false) {
336
+ const n = values.length;
337
+ if (population ? n === 0 : n < 2) return Number.NaN;
338
+ const m = mean(values);
339
+ let sumSq = 0;
340
+ for (const v of values) {
341
+ const d = v - m;
342
+ sumSq += d * d;
343
+ }
344
+ return sumSq / (population ? n : n - 1);
345
+ }
346
+ function standardDeviation(values, population = false) {
347
+ return Math.sqrt(variance(values, population));
348
+ }
349
+ function coefficientOfVariation(values, population = false) {
350
+ const m = mean(values);
351
+ if (m === 0) return Number.NaN;
352
+ return standardDeviation(values, population) / m;
353
+ }
354
+ function squaredCvOfNonZero(values) {
355
+ const nonZero = values.filter((v) => v !== 0);
356
+ const cv = coefficientOfVariation(nonZero);
357
+ return cv * cv;
358
+ }
359
+ function averageDemandInterval(series) {
360
+ if (series.length === 0) return Number.NaN;
361
+ let nonZeroCount = 0;
362
+ for (const v of series) if (v !== 0) nonZeroCount++;
363
+ if (nonZeroCount === 0) return Number.NaN;
364
+ return series.length / nonZeroCount;
365
+ }
366
+
367
+ // src/numerics/normal.ts
368
+ function normalPdf(z) {
369
+ return Math.exp(-0.5 * z * z) / Math.sqrt(2 * Math.PI);
370
+ }
371
+ function erf(x) {
372
+ const sign = Math.sign(x);
373
+ const ax = Math.abs(x);
374
+ const t = 1 / (1 + 0.3275911 * ax);
375
+ const y = 1 - ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-ax * ax);
376
+ return sign * y;
377
+ }
378
+ function normalCdf(z) {
379
+ return 0.5 * (1 + erf(z / Math.SQRT2));
380
+ }
381
+ function inverseNormalCdf(p) {
382
+ if (Number.isNaN(p) || p < 0 || p > 1) return Number.NaN;
383
+ if (p === 0) return Number.NEGATIVE_INFINITY;
384
+ if (p === 1) return Number.POSITIVE_INFINITY;
385
+ const [a1, a2, a3, a4, a5, a6] = [
386
+ -39.69683028665376,
387
+ 220.9460984245205,
388
+ -275.9285104469687,
389
+ 138.357751867269,
390
+ -30.66479806614716,
391
+ 2.506628277459239
392
+ ];
393
+ const [b1, b2, b3, b4, b5] = [
394
+ -54.47609879822406,
395
+ 161.5858368580409,
396
+ -155.6989798598866,
397
+ 66.80131188771972,
398
+ -13.28068155288572
399
+ ];
400
+ const [c1, c2, c3, c4, c5, c6] = [
401
+ -0.007784894002430293,
402
+ -0.3223964580411365,
403
+ -2.400758277161838,
404
+ -2.549732539343734,
405
+ 4.374664141464968,
406
+ 2.938163982698783
407
+ ];
408
+ const [d1, d2, d3, d4] = [
409
+ 0.007784695709041462,
410
+ 0.3224671290700398,
411
+ 2.445134137142996,
412
+ 3.754408661907416
413
+ ];
414
+ const pLow = 0.02425;
415
+ const pHigh = 1 - pLow;
416
+ if (p < pLow) {
417
+ const q2 = Math.sqrt(-2 * Math.log(p));
418
+ return (((((c1 * q2 + c2) * q2 + c3) * q2 + c4) * q2 + c5) * q2 + c6) / ((((d1 * q2 + d2) * q2 + d3) * q2 + d4) * q2 + 1);
419
+ }
420
+ if (p <= pHigh) {
421
+ const q2 = p - 0.5;
422
+ const r = q2 * q2;
423
+ return (((((a1 * r + a2) * r + a3) * r + a4) * r + a5) * r + a6) * q2 / (((((b1 * r + b2) * r + b3) * r + b4) * r + b5) * r + 1);
424
+ }
425
+ const q = Math.sqrt(-2 * Math.log(1 - p));
426
+ return -(((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) / ((((d1 * q + d2) * q + d3) * q + d4) * q + 1);
427
+ }
428
+ function normalLossFunction(z) {
429
+ return normalPdf(z) - z * (1 - normalCdf(z));
430
+ }
431
+
432
+ // src/numerics/optimize.ts
433
+ function nelderMead(f, x0, options = {}) {
434
+ const { step = 0.1, maxIterations = 200, tolerance = 1e-8 } = options;
435
+ const n = x0.length;
436
+ if (n === 0) throw new Error("nelderMead requires a non-empty initial vector");
437
+ const alpha = 1;
438
+ const gamma = 2;
439
+ const rho = 0.5;
440
+ const sigma = 0.5;
441
+ const simplex = [{ x: [...x0], fx: f(x0) }];
442
+ for (let i = 0; i < n; i++) {
443
+ const x = [...x0];
444
+ x[i] = (x[i] ?? 0) + step;
445
+ simplex.push({ x, fx: f(x) });
446
+ }
447
+ const shrink = () => {
448
+ const best2 = simplex[0];
449
+ for (let i = 1; i <= n; i++) {
450
+ const v = simplex[i];
451
+ const x = v.x.map((xi, j) => best2.x[j] + sigma * (xi - best2.x[j]));
452
+ simplex[i] = evaluate(f, x);
453
+ }
454
+ };
455
+ let iterations = 0;
456
+ let converged = false;
457
+ for (; iterations < maxIterations; iterations++) {
458
+ simplex.sort((p, q) => p.fx - q.fx);
459
+ const best2 = simplex[0];
460
+ const worst = simplex[n];
461
+ const secondWorst = simplex[n - 1];
462
+ if (Math.abs(worst.fx - best2.fx) <= tolerance) {
463
+ converged = true;
464
+ break;
465
+ }
466
+ const centroid = new Array(n).fill(0);
467
+ for (let i = 0; i < n; i++) {
468
+ const v = simplex[i];
469
+ for (let j = 0; j < n; j++) centroid[j] = centroid[j] + v.x[j];
470
+ }
471
+ for (let j = 0; j < n; j++) centroid[j] = centroid[j] / n;
472
+ const reflected = evaluate(f, combine(centroid, worst.x, alpha));
473
+ if (reflected.fx < best2.fx) {
474
+ const expanded = evaluate(f, combine(centroid, worst.x, alpha * gamma));
475
+ simplex[n] = expanded.fx < reflected.fx ? expanded : reflected;
476
+ } else if (reflected.fx < secondWorst.fx) {
477
+ simplex[n] = reflected;
478
+ } else if (reflected.fx < worst.fx) {
479
+ const contracted = evaluate(f, combine(centroid, worst.x, rho));
480
+ if (contracted.fx <= reflected.fx) simplex[n] = contracted;
481
+ else shrink();
482
+ } else {
483
+ const contracted = evaluate(f, combine(centroid, worst.x, -rho));
484
+ if (contracted.fx < worst.fx) simplex[n] = contracted;
485
+ else shrink();
486
+ }
487
+ }
488
+ simplex.sort((p, q) => p.fx - q.fx);
489
+ const best = simplex[0];
490
+ return { x: best.x, fx: best.fx, iterations, converged };
491
+ }
492
+ function combine(centroid, worst, coefficient) {
493
+ return centroid.map((c, j) => c + coefficient * (c - worst[j]));
494
+ }
495
+ function evaluate(f, x) {
496
+ return { x, fx: f(x) };
497
+ }
498
+
499
+ // src/synthetic/generate.ts
500
+ var BASE_PROFILES = [
501
+ "smooth",
502
+ "seasonal",
503
+ "trending",
504
+ "intermittent",
505
+ "lumpy"
506
+ ];
507
+ function mulberry32(seed) {
508
+ let a = seed >>> 0;
509
+ return () => {
510
+ a = a + 1831565813 | 0;
511
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
512
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
513
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
514
+ };
515
+ }
516
+ function gaussian(rng) {
517
+ let u = 0;
518
+ while (u === 0) u = rng();
519
+ const v = rng();
520
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
521
+ }
522
+ function periodDate(startEpochDay, index, granularity) {
523
+ if (granularity !== "month") {
524
+ const step = granularity === "week" ? 7 : 1;
525
+ return formatEpochDay(startEpochDay + index * step);
526
+ }
527
+ const start = fromEpochDay(startEpochDay);
528
+ const monthOrdinal = start.getUTCFullYear() * 12 + start.getUTCMonth() + index;
529
+ const year = Math.floor(monthOrdinal / 12);
530
+ const month = monthOrdinal % 12;
531
+ const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
532
+ const day = Math.min(start.getUTCDate(), daysInMonth);
533
+ return `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
534
+ }
535
+ function demandForPeriod(profile, level, period, periods, rng) {
536
+ switch (profile) {
537
+ case "smooth":
538
+ return Math.max(0, Math.round(level + gaussian(rng) * level * 0.1));
539
+ case "seasonal": {
540
+ const seasonal = 1 + 0.5 * Math.sin(2 * Math.PI * period / 12);
541
+ return Math.max(0, Math.round(level * seasonal + gaussian(rng) * level * 0.1));
542
+ }
543
+ case "trending": {
544
+ const growth = 1 + 0.8 * period / Math.max(1, periods - 1);
545
+ return Math.max(0, Math.round(level * growth + gaussian(rng) * level * 0.1));
546
+ }
547
+ case "intermittent":
548
+ return rng() < 0.3 ? Math.max(1, Math.round(level + gaussian(rng) * level * 0.15)) : 0;
549
+ case "lumpy":
550
+ return rng() < 0.25 ? Math.max(1, Math.round(level * (0.2 + rng() * 2.8))) : 0;
551
+ case "mixed":
552
+ return demandForPeriod("smooth", level, period, periods, rng);
553
+ }
554
+ }
555
+ function generateExampleData(options = {}) {
556
+ const {
557
+ items = 10,
558
+ periods = 24,
559
+ profile = "mixed",
560
+ granularity = "month",
561
+ startDate = "2024-01-01",
562
+ seed = 1
563
+ } = options;
564
+ const rng = mulberry32(seed);
565
+ const startEpochDay = toEpochDay(startDate);
566
+ const demand = [];
567
+ const stock = [];
568
+ const leadTimes = [];
569
+ for (let i = 0; i < items; i++) {
570
+ const itemId = `SKU-${String(i + 1).padStart(4, "0")}`;
571
+ const itemProfile = profile === "mixed" ? BASE_PROFILES[i % BASE_PROFILES.length] : profile;
572
+ const level = Math.round(20 + rng() * 180);
573
+ const unitPrice = Math.round((5 + rng() * 95) * 100) / 100;
574
+ const unitCost = Math.round(unitPrice * (0.4 + rng() * 0.3) * 100) / 100;
575
+ for (let period = 0; period < periods; period++) {
576
+ const quantity = demandForPeriod(itemProfile, level, period, periods, rng);
577
+ if (quantity > 0) {
578
+ demand.push({
579
+ itemId,
580
+ date: periodDate(startEpochDay, period, granularity),
581
+ quantity,
582
+ unitPrice
583
+ });
584
+ }
585
+ }
586
+ stock.push({
587
+ itemId,
588
+ quantity: Math.round(level * (0.5 + rng() * 2.5)),
589
+ unitCost
590
+ });
591
+ const leadTimeMean = Math.round(7 + rng() * 23);
592
+ for (let k = 0; k < 5; k++) {
593
+ leadTimes.push({
594
+ itemId,
595
+ leadTimeDays: Math.max(1, Math.round(leadTimeMean + gaussian(rng) * leadTimeMean * 0.2))
596
+ });
597
+ }
598
+ }
599
+ return { demand, stock, leadTimes };
600
+ }
601
+
602
+ export { averageDemandInterval, bucketize, coefficientOfVariation, explain, formatEpochDay, fromEpochDay, generateExampleData, inverseNormalCdf, isoWeekday, loadDemand, loadLeadTimes, loadStock, mean, nelderMead, normalCdf, normalLossFunction, normalPdf, squaredCvOfNonZero, standardDeviation, toEpochDay, variance };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@logistics-ts/core",
3
+ "version": "0.0.1",
4
+ "description": "Core types, column store, data loading, and shared numerics for logistics-ts.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/adam-drag/logistics-ts.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "homepage": "https://github.com/adam-drag/logistics-ts/tree/main/packages/core",
13
+ "sideEffects": false,
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "main": "./dist/index.js",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "typecheck": "tsc --noEmit"
32
+ }
33
+ }