@jarenjs/core 0.46.5 → 0.56.0

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.
Files changed (42) hide show
  1. package/ARCHITECTURE.md +122 -9
  2. package/README.md +50 -3
  3. package/dist/types/async.d.ts +39 -0
  4. package/dist/types/dates/civil.d.ts +24 -5
  5. package/dist/types/dates/index.d.ts +2 -0
  6. package/dist/types/dates/parse.d.ts +47 -0
  7. package/dist/types/dates/ticks.d.ts +59 -0
  8. package/dist/types/math/float64.d.ts +13 -0
  9. package/dist/types/random.d.ts +80 -0
  10. package/dist/types/series/asof.d.ts +88 -0
  11. package/dist/types/series/bucket.d.ts +206 -0
  12. package/dist/types/series/downsample.d.ts +53 -0
  13. package/dist/types/series/index.d.ts +8 -0
  14. package/dist/types/series/interval-index.d.ts +47 -0
  15. package/dist/types/series/interval.d.ts +170 -0
  16. package/dist/types/series/normalize.d.ts +190 -0
  17. package/dist/types/series/rolling.d.ts +67 -0
  18. package/dist/types/series/selector.d.ts +29 -0
  19. package/dist/types/series/zone.d.ts +59 -0
  20. package/dist/types/stats.d.ts +73 -0
  21. package/docs/DATES.md +77 -1
  22. package/docs/GEO.md +2 -2
  23. package/docs/SERIES.md +342 -0
  24. package/package.json +21 -1
  25. package/src/async.js +78 -0
  26. package/src/dates/civil.js +136 -41
  27. package/src/dates/index.js +4 -0
  28. package/src/dates/parse.js +410 -0
  29. package/src/dates/ticks.js +184 -0
  30. package/src/math/float64.js +29 -0
  31. package/src/random.js +125 -0
  32. package/src/series/asof.js +276 -0
  33. package/src/series/bucket.js +542 -0
  34. package/src/series/downsample.js +343 -0
  35. package/src/series/index.js +86 -0
  36. package/src/series/interval-index.js +181 -0
  37. package/src/series/interval.js +374 -0
  38. package/src/series/normalize.js +330 -0
  39. package/src/series/rolling.js +229 -0
  40. package/src/series/selector.js +104 -0
  41. package/src/series/zone.js +188 -0
  42. package/src/stats.js +133 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @file The suite's one seeded generator, and the three draws built on
3
+ * it. Every seeded corpus, oracle and property test in this repository
4
+ * needs the same thing: a stream of numbers that is identical on every
5
+ * host for a given seed, so that a benchmark can state a delta and a
6
+ * failing property test can be replayed. Before this file that stream
7
+ * was written ten times; a generator that exists once is one whose
8
+ * sequence can be pinned once.
9
+ *
10
+ * The algorithm is mulberry32 — a 32-bit state, one multiply-xorshift
11
+ * round per draw, a period of 2^32. It is named by its algorithm rather
12
+ * than by its role because the SEQUENCE is the contract: a corpus
13
+ * generated from seed 20260825 must regenerate byte-for-byte, and a
14
+ * "better" generator under the same name would silently change every
15
+ * fixture that trusts it. A second algorithm gets a second name.
16
+ *
17
+ * Nothing here is cryptographic, and nothing here reads `Math.random`.
18
+ */
19
+ /**
20
+ * A seeded generator: uniform in `[0, 1)`, identical on every host for
21
+ * the same seed.
22
+ *
23
+ * The seed is taken as an unsigned 32-bit integer (`seed >>> 0`, the
24
+ * ToUint32 conversion): `1.5` seeds as `1`, `-1` as `4294967295`,
25
+ * `2^32 + 5` as `5`, and `NaN` as `0`. Two seeds that agree modulo 2^32
26
+ * are one stream — say so wherever a seed is published.
27
+ * @param {number} seed
28
+ * @returns {() => number} the stream; each call is the next draw
29
+ */
30
+ export declare function mulberry32(seed: number): () => number;
31
+ /**
32
+ * One integer draw over the half-open range `[min, max)`: `min +
33
+ * floor(random() * (max - min))`.
34
+ *
35
+ * This is the floor draw and deliberately so. Every committed corpus in
36
+ * the suite was generated with exactly this arithmetic, and a generator
37
+ * whose integer draw changed would regenerate every one of them
38
+ * differently. It is uniform up to a bias bounded by `(max - min) /
39
+ * 2^32` — under one part in a million for a span of four thousand, and
40
+ * far below anything a benchmark row can resolve. A draw that rejects
41
+ * to remove even that bias would be a different function under a
42
+ * different name, not a change to this one.
43
+ * @param {() => number} random - the stream, from {@link mulberry32}
44
+ * @param {number} min - inclusive integer lower bound
45
+ * @param {number} max - exclusive integer upper bound; must exceed `min`
46
+ * @returns {number} an integer in `[min, max)`
47
+ * @throws {RangeError} when a bound is not an integer or `max <= min`
48
+ */
49
+ export declare function randomInt(random: () => number, min: number, max: number): number;
50
+ /**
51
+ * Fisher–Yates, in place, from the given stream: for `i` from the last
52
+ * index down to 1, swap `i` with a uniform `j` in `[0, i]`. Returns the
53
+ * same array. An empty or one-element list draws nothing.
54
+ *
55
+ * The descending form is the one the seeded corpora were generated
56
+ * with; the ascending form is a different permutation of the same
57
+ * stream and must not be substituted.
58
+ * @template T
59
+ * @param {() => number} random - the stream, from {@link mulberry32}
60
+ * @param {T[]} list - reordered in place
61
+ * @returns {T[]} `list`
62
+ */
63
+ export declare function shuffle<T>(random: () => number, list: T[]): T[];
64
+ /**
65
+ * `k` distinct indices from `[0, n)`, uniformly, as a partial forward
66
+ * Fisher–Yates over a fresh index pool: the first `k` positions of a
67
+ * shuffle, without paying for the rest. Asked for more than `n` it
68
+ * answers `n` — a draw cannot invent a member the population does not
69
+ * hold; asked for nothing, or from nothing, it answers `[]`.
70
+ *
71
+ * The stream is the caller's, so one stream can serve many draws
72
+ * (a policy that draws per question from one seeded closure stays
73
+ * reproducible across the whole run).
74
+ * @param {() => number} random - the stream, from {@link mulberry32}
75
+ * @param {number} n - the population size (a non-negative integer)
76
+ * @param {number} k - how many to draw (a non-negative integer)
77
+ * @returns {number[]} `min(k, n)` distinct indices, in draw order
78
+ * @throws {RangeError} when `n` or `k` is not a non-negative integer
79
+ */
80
+ export declare function drawDistinct(random: () => number, n: number, k: number): number[];
@@ -0,0 +1,88 @@
1
+ export type Sample = import('./normalize.js').Sample;
2
+ export type AsOfMatch = {
3
+ /**
4
+ * - the canonical left sample
5
+ */
6
+ left: any;
7
+ /**
8
+ * - the canonical right sample, or `null`
9
+ */
10
+ right: any | null;
11
+ /**
12
+ * - milliseconds between the two
13
+ * instants, never negative; `null` when there was no match
14
+ */
15
+ distance: number | null;
16
+ };
17
+ /**
18
+ * `asOfJoin`'s closed specification: which way to look, how far, what
19
+ * makes two rows comparable, and where each side keeps its members.
20
+ *
21
+ * This is the one series kernel whose spelling a query DOCUMENT cannot
22
+ * reuse: `left`/`right` are nested selector records, and §8.16 flattens
23
+ * them to `by`, `leftAt` and `rightAt` so the whole spec stays a
24
+ * literal. `@jarenjs/json` therefore keeps its own list, and says so.
25
+ */
26
+ export declare const ASOF_MEMBERS: readonly string[];
27
+ /**
28
+ * Join each left sample to the right sample that was current for it.
29
+ *
30
+ * The result is one record **per left row**, in the left series'
31
+ * normalized order — sorted by instant, with rows sharing an instant in
32
+ * the order they arrived. Both `left` and `right` are canonical samples:
33
+ * shallow copies carrying every member of the source row plus a numeric
34
+ * `at` and `value`, so a consumer reads `match.right.at` rather than
35
+ * parsing a timestamp a second time.
36
+ *
37
+ * | direction | the right row chosen |
38
+ * |---|---|
39
+ * | `backward` | the last one at or before the left instant (default) |
40
+ * | `forward` | the last one at or after it |
41
+ * | `nearest` | whichever is closer; a tie chooses `backward` |
42
+ *
43
+ * At an equal instant the **last** right-side row wins in every
44
+ * direction: duplicates are two readings in the same millisecond, and
45
+ * "as of" means the later one.
46
+ *
47
+ * `tolerance` is the furthest a match may be, in milliseconds or as a
48
+ * fixed duration. Beyond it there is no match — not a distant one.
49
+ *
50
+ * `key` joins within groups: a property name or a function, applied to
51
+ * both sides (or `left.key`/`right.key` when the two sides spell it
52
+ * differently). The right side is partitioned **once**; no left row ever
53
+ * filters it.
54
+ *
55
+ * @param {any[]} left
56
+ * @param {any[]} right
57
+ * @param {Object} [spec]
58
+ * @param {'backward'|'forward'|'nearest'} [spec.direction] default `'backward'`
59
+ * @param {number | string} [spec.tolerance] - the furthest a match may be
60
+ * @param {string | ((item: any, index: number) => any)} [spec.key] - the
61
+ * group both sides join within
62
+ * @param {{ at?: any, value?: any, key?: any }} [spec.left] - where the
63
+ * left side's members live
64
+ * @param {{ at?: any, value?: any, key?: any }} [spec.right] - where the
65
+ * right side's members live
66
+ * @returns {AsOfMatch[]}
67
+ * @throws {TypeError} for an unknown direction, a tolerance that is not
68
+ * a non-negative fixed width, or a row that is not a canonical sample
69
+ * @example
70
+ * asOfJoin(trades, quotes); // the last quote at or before
71
+ * asOfJoin(alarms, shifts, { direction: 'nearest', tolerance: 'PT1H' });
72
+ * asOfJoin(readings, calibrations, { key: 'sensor' }); // per sensor
73
+ */
74
+ export declare function asOfJoin(left: any[], right: any[], spec?: {
75
+ direction?: 'backward' | 'forward' | 'nearest';
76
+ tolerance?: number | string;
77
+ key?: string | ((item: any, index: number) => any);
78
+ left?: {
79
+ at?: any;
80
+ value?: any;
81
+ key?: any;
82
+ };
83
+ right?: {
84
+ at?: any;
85
+ value?: any;
86
+ key?: any;
87
+ };
88
+ }): AsOfMatch[];
@@ -0,0 +1,206 @@
1
+ export type Sample = import('./normalize.js').Sample;
2
+ export type Clock = import('./zone.js').Clock;
3
+ export type CompiledBuckets = {
4
+ /**
5
+ * - the width as it was written
6
+ */
7
+ every: number | string;
8
+ /**
9
+ * - whether boundaries need the calendar
10
+ */
11
+ calendar: boolean;
12
+ /**
13
+ * - `'millisecond'`, `'day'` or `'month'`
14
+ */
15
+ unit: string;
16
+ /**
17
+ * - the count of that unit
18
+ */
19
+ amount: number;
20
+ /**
21
+ * - fixed width in milliseconds, or 0 when
22
+ * the ladder is a calendar one
23
+ */
24
+ width: number;
25
+ /**
26
+ * - the clock the boundaries fall on
27
+ */
28
+ zone: string;
29
+ /**
30
+ * - the anchor, in epoch milliseconds
31
+ */
32
+ origin: number;
33
+ /**
34
+ * - the boundary at a
35
+ * ladder position; position 0 is `origin`
36
+ */
37
+ startOf: (index: number) => number;
38
+ /**
39
+ * - the ladder position
40
+ * holding an instant
41
+ */
42
+ indexOf: (at: number) => number;
43
+ /**
44
+ * - the boundary of the bucket
45
+ * holding an instant
46
+ */
47
+ floor: (at: number) => number;
48
+ };
49
+ /**
50
+ * A duration taken apart on a clock: which family it belongs to, how
51
+ * much of that family's unit it is, and the clock itself.
52
+ *
53
+ * The seam between the two kernels that need the same answer. A bucket
54
+ * ladder and a rolling window both have to know whether `P1M` is
55
+ * arithmetic or a calendar question, and whether `P1D` is 86,400,000
56
+ * milliseconds (it is, on UTC and on a fixed offset) or a day that might
57
+ * be 23 hours long (it is, on a named zone).
58
+ *
59
+ * @param {number | string} every
60
+ * @param {Object} [options] - the clock, as {@link resolveClock} takes it
61
+ * @param {string} [options.zone]
62
+ * @param {number} [options.offset]
63
+ * @param {import('./zone.js').ZoneProvider} [options.provider]
64
+ * @param {'reject' | 'earlier' | 'later'} [options.disambiguation]
65
+ * @returns {{ calendar: boolean, unit: string, amount: number, width: number, clock: Clock }}
66
+ * @throws {TypeError} for a span that is not one positive whole family,
67
+ * or a named zone with no provider
68
+ */
69
+ export declare function compileSpan(every: number | string, options?: {
70
+ zone?: string;
71
+ offset?: number;
72
+ provider?: import('./zone.js').ZoneProvider;
73
+ disambiguation?: 'reject' | 'earlier' | 'later';
74
+ }): {
75
+ calendar: boolean;
76
+ unit: string;
77
+ amount: number;
78
+ width: number;
79
+ clock: Clock;
80
+ };
81
+ /**
82
+ * The bucket ladder for a specification, validated once and returned as
83
+ * the four boundary operations everything downstream needs.
84
+ *
85
+ * `spec` may be the width alone (`compileBuckets('PT15M')`) or a record
86
+ * with `every` and an optional `origin`. `options` carries the clock —
87
+ * nothing, `{ offset }`, or `{ zone, provider }` — and defaults to UTC.
88
+ *
89
+ * The default `origin` is local `1970-01-01T00:00:00` on that clock, so
90
+ * a daily bucket in `+02:00` falls on local midnight rather than on
91
+ * UTC's, and a monthly bucket falls on the first of the month.
92
+ *
93
+ * @param {number | string | { every: number | string, origin?: number | string }} spec
94
+ * @param {Object} [options] - the clock, as {@link resolveClock} takes it
95
+ * @param {string} [options.zone]
96
+ * @param {number} [options.offset]
97
+ * @param {import('./zone.js').ZoneProvider} [options.provider]
98
+ * @param {'reject' | 'earlier' | 'later'} [options.disambiguation]
99
+ * @returns {CompiledBuckets}
100
+ * @throws {TypeError} for a width that is not a positive whole span, a
101
+ * width mixing calendar and fixed units, an origin naming no instant,
102
+ * or a named zone with no provider
103
+ * @example
104
+ * const b = compileBuckets('PT15M');
105
+ * b.floor(Date.UTC(2026, 0, 1, 9, 7)); // 09:00
106
+ * b.startOf(b.indexOf(0) + 1); // the boundary after the epoch
107
+ */
108
+ export declare function compileBuckets(spec: number | string | {
109
+ every: number | string;
110
+ origin?: number | string;
111
+ }, options?: {
112
+ zone?: string;
113
+ offset?: number;
114
+ provider?: import('./zone.js').ZoneProvider;
115
+ disambiguation?: 'reject' | 'earlier' | 'later';
116
+ }): CompiledBuckets;
117
+ /**
118
+ * `resampleSeries`' closed specification: the D5 bucket contract, the
119
+ * clock it reads and where a row keeps its instant and its reading.
120
+ */
121
+ export declare const RESAMPLE_MEMBERS: readonly string[];
122
+ /**
123
+ * Bucket a series, reduce each bucket to one number, and say what the
124
+ * empty ones mean.
125
+ *
126
+ * The result is ascending `{ at, value, count }` records labelled at
127
+ * their bucket's **start**, which is the only label that is a boundary
128
+ * rather than a summary of where the rows happened to land.
129
+ *
130
+ * `count` is the number of **source rows** the bucket held — duplicates
131
+ * and measured gaps included — so it is the honest denominator of what
132
+ * was seen, not of what could be added up. The six value aggregates
133
+ * (`sum`, `mean`, `min`, `max`, `first`, `last`) all skip `null`
134
+ * readings, so `value` is `null` exactly when the bucket had nothing to
135
+ * measure, and `count` tells you whether that was because nobody
136
+ * reported or because everybody reported a gap. `aggregate: 'count'`
137
+ * returns that same row count as the value.
138
+ *
139
+ * The window, when `start`/`end` are not given, is the data's own: the
140
+ * bucket holding the first sample through the bucket holding the last.
141
+ * Pass them when an empty edge matters — an empty Monday is only a
142
+ * missing Monday once the caller says the week starts then.
143
+ *
144
+ * The five fill policies decide what an **empty** bucket says, and
145
+ * nothing else — a bucket that held rows and no numbers reports `null`
146
+ * because that is a measurement:
147
+ *
148
+ * | fill | an empty bucket |
149
+ * |---|---|
150
+ * | `omit` | is not emitted (the default: a gap is not a row) |
151
+ * | `null` | is emitted as `null` |
152
+ * | `zero` | is emitted as `0` |
153
+ * | `locf` | repeats the last value before it |
154
+ * | `linear` | is interpolated between its two neighbours |
155
+ *
156
+ * Neither `locf` nor `linear` invents a value at the leading edge, and
157
+ * `linear` needs a value on **both** sides: with no anchor to carry or
158
+ * to interpolate from, the bucket stays `null`. To seed one, widen the
159
+ * window until the earlier reading falls inside it — the seed is then a
160
+ * bucket with data, which is the only kind of anchor this function will
161
+ * extrapolate from. (With `aggregate: 'count'` an empty bucket is `0` by
162
+ * definition, so fill only decides whether it appears at all.)
163
+ *
164
+ * @param {any[]} rows - the samples, in any order
165
+ * @param {Object} spec
166
+ * @param {number | string} spec.every - the bucket width
167
+ * @param {number | string} [spec.origin] - where a boundary falls
168
+ * (default: local `1970-01-01T00:00:00` on the clock)
169
+ * @param {number | string} [spec.start] - the half-open window's start
170
+ * @param {number | string} [spec.end] - the half-open window's end
171
+ * @param {'sum'|'mean'|'min'|'max'|'first'|'last'|'count'} [spec.aggregate]
172
+ * default `'mean'`
173
+ * @param {'omit'|'null'|'zero'|'locf'|'linear'} [spec.fill] default `'omit'`
174
+ * @param {string} [spec.zone] - a named zone, needing `provider`
175
+ * @param {number} [spec.offset] - minutes east of UTC
176
+ * @param {import('./zone.js').ZoneProvider} [spec.provider]
177
+ * @param {'reject'|'earlier'|'later'} [spec.disambiguation]
178
+ * @param {string | ((item: any, index: number) => any)} [spec.at] - where
179
+ * the instant lives in a source row (default `'at'`)
180
+ * @param {string | ((item: any, index: number) => any)} [spec.value]
181
+ * where the reading lives (default `'value'`)
182
+ * @returns {{ at: number, value: number | null, count: number }[]}
183
+ * @throws {TypeError} for a bad width, origin, window, aggregate or fill,
184
+ * or a row that is not a canonical sample
185
+ * @example
186
+ * resampleSeries(readings, { every: 'PT1H', aggregate: 'mean', fill: 'linear' });
187
+ * resampleSeries(readings, { every: 'P1M', zone: 'Europe/Amsterdam', provider });
188
+ */
189
+ export declare function resampleSeries(rows: any[], spec: {
190
+ every: number | string;
191
+ origin?: number | string;
192
+ start?: number | string;
193
+ end?: number | string;
194
+ aggregate?: 'sum' | 'mean' | 'min' | 'max' | 'first' | 'last' | 'count';
195
+ fill?: 'omit' | 'null' | 'zero' | 'locf' | 'linear';
196
+ zone?: string;
197
+ offset?: number;
198
+ provider?: import('./zone.js').ZoneProvider;
199
+ disambiguation?: 'reject' | 'earlier' | 'later';
200
+ at?: string | ((item: any, index: number) => any);
201
+ value?: string | ((item: any, index: number) => any);
202
+ }): {
203
+ at: number;
204
+ value: number | null;
205
+ count: number;
206
+ }[];
@@ -0,0 +1,53 @@
1
+ export type Sample = import('./normalize.js').Sample;
2
+ export type Downsampled = {
3
+ /**
4
+ * - the kept
5
+ * samples, ascending, with every gap still a gap
6
+ */
7
+ points: (Sample & Record<string, any>)[];
8
+ /**
9
+ * - how many samples went in
10
+ */
11
+ sourceCount: number;
12
+ /**
13
+ * - how many came out; never more than
14
+ * `target`, and less when a bucket's two extremes were one point
15
+ */
16
+ renderedCount: number;
17
+ /**
18
+ * - which strategy chose them
19
+ */
20
+ method: string;
21
+ };
22
+ /**
23
+ * `downsampleSeries`' closed specification: how many points may come
24
+ * back, which strategy chooses them, and where a row keeps its
25
+ * instant and its reading.
26
+ */
27
+ export declare const DOWNSAMPLE_MEMBERS: readonly string[];
28
+ /**
29
+ * Reduce a series to at most `target` points without bridging a gap or
30
+ * moving an end.
31
+ *
32
+ * @param {any[]} rows - the samples, in any order
33
+ * @param {Object} spec
34
+ * @param {number} spec.target - the most points to return, at least the
35
+ * mandatory endpoints and gap markers
36
+ * @param {'lttb'|'minmax'} [spec.method] default `'lttb'`
37
+ * @param {string | ((item: any, index: number) => any)} [spec.at]
38
+ * @param {string | ((item: any, index: number) => any)} [spec.value]
39
+ * @returns {Downsampled}
40
+ * @throws {TypeError} for an unknown method, a target that is not a
41
+ * positive whole number, or a row that is not a canonical sample
42
+ * @throws {RangeError} when `target` cannot hold the segment endpoints
43
+ * and gap markers the data requires
44
+ * @example
45
+ * const { points, sourceCount, renderedCount } =
46
+ * downsampleSeries(readings, { target: 800 });
47
+ */
48
+ export declare function downsampleSeries(rows: any[], spec: {
49
+ target: number;
50
+ method?: 'lttb' | 'minmax';
51
+ at?: string | ((item: any, index: number) => any);
52
+ value?: string | ((item: any, index: number) => any);
53
+ }): Downsampled;
@@ -0,0 +1,8 @@
1
+ export { toEpoch, normalizeSeries, canonicalSeries, normalizeIntervals, lowerBoundTime, upperBoundTime, } from './normalize.js';
2
+ export { containsInstant, overlapsInterval, intersectInterval, mergeIntervals, subtractIntervals, gapsWithin, coverageOf, findSlots, MERGE_MEMBERS, SLOTS_MEMBERS, } from './interval.js';
3
+ export { createIntervalIndex } from './interval-index.js';
4
+ export { resolveClock, CLOCK_MEMBERS } from './zone.js';
5
+ export { compileBuckets, resampleSeries, RESAMPLE_MEMBERS } from './bucket.js';
6
+ export { rollingSeries, ROLLING_MEMBERS } from './rolling.js';
7
+ export { asOfJoin, ASOF_MEMBERS } from './asof.js';
8
+ export { downsampleSeries, DOWNSAMPLE_MEMBERS } from './downsample.js';
@@ -0,0 +1,47 @@
1
+ export type IntervalIndex = {
2
+ /**
3
+ * - how many intervals were indexed
4
+ */
5
+ size: number;
6
+ /**
7
+ * - the items whose
8
+ * `[start, end)` contains this instant
9
+ */
10
+ at: (at: number | string) => any[];
11
+ /**
12
+ * - the items sharing an instant with `[start, end)`
13
+ */
14
+ overlapping: (start: number | string, end: number | string) => any[];
15
+ };
16
+ /**
17
+ * Build a queryable index over `[start, end)` intervals.
18
+ *
19
+ * Both queries answer with the caller's own items, ascending by start
20
+ * and — for items sharing a start — in the order they were given. Every
21
+ * result is a fresh array, so a caller can sort or splice it without
22
+ * reaching into the index.
23
+ *
24
+ * Bounds are read once at build time through the selectors, as epoch
25
+ * milliseconds or RFC 3339 strings, and an interval that is empty,
26
+ * reversed or names no instant is refused here rather than being
27
+ * skipped: an index quietly holding fewer rows than it was given
28
+ * answers every later question wrongly.
29
+ *
30
+ * @param {any[]} items - the rows, in any order
31
+ * @param {Object} [selectors]
32
+ * @param {string | ((item: any, index: number) => any)} [selectors.start]
33
+ * where the lower bound lives (default `'start'`)
34
+ * @param {string | ((item: any, index: number) => any)} [selectors.end]
35
+ * where the upper bound lives (default `'end'`)
36
+ * @returns {IntervalIndex}
37
+ * @throws {TypeError} for a non-array, a row that is not an object, a
38
+ * bound that names no instant, or `end <= start`
39
+ * @example
40
+ * const index = createIntervalIndex(bookings, { start: 'from', end: 'to' });
41
+ * index.at('2026-03-01T10:00:00Z'); // who is booked then
42
+ * index.overlapping(dayStart, dayEnd); // everything touching today
43
+ */
44
+ export declare function createIntervalIndex(items: any[], selectors?: {
45
+ start?: string | ((item: any, index: number) => any);
46
+ end?: string | ((item: any, index: number) => any);
47
+ }): IntervalIndex;
@@ -0,0 +1,170 @@
1
+ export type Interval = import('./normalize.js').Interval;
2
+ /**
3
+ * Does `interval` contain the instant `at`?
4
+ *
5
+ * Half-open: the start is in, the end is out. An instant on a boundary
6
+ * belongs to exactly one of two touching intervals, which is what makes
7
+ * "which shift is this event in" answerable.
8
+ *
9
+ * @param {Interval} interval
10
+ * @param {number | string} at - epoch milliseconds or RFC 3339
11
+ * @returns {boolean}
12
+ * @throws {TypeError} for an empty, reversed or non-finite interval
13
+ * @example
14
+ * containsInstant({ start: 0, end: 10 }, 0); // true
15
+ * containsInstant({ start: 0, end: 10 }, 10); // false
16
+ */
17
+ export declare function containsInstant(interval: Interval, at: number | string): boolean;
18
+ /**
19
+ * Do two intervals share at least one instant?
20
+ *
21
+ * Touching intervals do not: `[0, 10)` and `[10, 20)` have no instant
22
+ * in common, so consecutive bookings never read as a conflict.
23
+ *
24
+ * @param {Interval} a
25
+ * @param {Interval} b
26
+ * @returns {boolean}
27
+ * @throws {TypeError} for an empty, reversed or non-finite interval
28
+ * @example
29
+ * overlapsInterval({ start: 0, end: 10 }, { start: 10, end: 20 }); // false
30
+ * overlapsInterval({ start: 0, end: 10 }, { start: 9, end: 20 }); // true
31
+ */
32
+ export declare function overlapsInterval(a: Interval, b: Interval): boolean;
33
+ /**
34
+ * The span two intervals share, or `null` when they share none.
35
+ *
36
+ * `null` rather than an empty interval, because `[t, t)` is not a value
37
+ * this algebra has — the absence is the answer, and it cannot then be
38
+ * fed back in as if it were a span.
39
+ *
40
+ * @param {Interval} a
41
+ * @param {Interval} b
42
+ * @returns {Interval | null} a new record
43
+ * @throws {TypeError} for an empty, reversed or non-finite interval
44
+ * @example
45
+ * intersectInterval({ start: 0, end: 10 }, { start: 5, end: 20 });
46
+ * // { start: 5, end: 10 }
47
+ */
48
+ export declare function intersectInterval(a: Interval, b: Interval): Interval | null;
49
+ /**
50
+ * `mergeIntervals`' closed specification: whether spans that touch
51
+ * join, which availability normally wants and a handover does not.
52
+ */
53
+ export declare const MERGE_MEMBERS: readonly string[];
54
+ /**
55
+ * The union of `intervals`, as the fewest disjoint spans that cover the
56
+ * same instants, ascending.
57
+ *
58
+ * Touching spans are joined by default, because continuous cover is
59
+ * what availability means; `{ adjacent: false }` keeps them apart, which
60
+ * is what a handover between two shifts means. Overlapping spans always
61
+ * join, under both settings.
62
+ *
63
+ * @param {Interval[]} intervals - in any order
64
+ * @param {Object} [options]
65
+ * @param {boolean} [options.adjacent] - join touching spans (default `true`)
66
+ * @returns {Interval[]} new `{ start, end }` records
67
+ * @throws {TypeError} for an empty, reversed or non-finite interval
68
+ * @example
69
+ * mergeIntervals([{ start: 0, end: 10 }, { start: 10, end: 20 }]);
70
+ * // [{ start: 0, end: 20 }]
71
+ * mergeIntervals([{ start: 0, end: 10 }, { start: 10, end: 20 }],
72
+ * { adjacent: false });
73
+ * // [{ start: 0, end: 10 }, { start: 10, end: 20 }]
74
+ */
75
+ export declare function mergeIntervals(intervals: Interval[], options?: {
76
+ adjacent?: boolean;
77
+ }): Interval[];
78
+ /**
79
+ * The instants in `from` that `remove` does not cover, as disjoint
80
+ * ascending spans.
81
+ *
82
+ * Both sides are merged first, so the result is the set difference and
83
+ * nothing depends on the order the arguments arrived in. A cut through
84
+ * the middle of a span **splits** it into two; a cut that covers a span
85
+ * removes it entirely.
86
+ *
87
+ * @param {Interval[]} from - the spans being reduced
88
+ * @param {Interval[]} remove - the spans taken out of them
89
+ * @returns {Interval[]} new `{ start, end }` records
90
+ * @throws {TypeError} for an empty, reversed or non-finite interval
91
+ * @example
92
+ * subtractIntervals([{ start: 0, end: 100 }], [{ start: 40, end: 60 }]);
93
+ * // [{ start: 0, end: 40 }, { start: 60, end: 100 }]
94
+ */
95
+ export declare function subtractIntervals(from: Interval[], remove: Interval[]): Interval[];
96
+ /**
97
+ * The spans inside `within` that `intervals` leaves uncovered.
98
+ *
99
+ * `within` defaults to the hull of the intervals themselves — the gaps
100
+ * *between* them — because the alternative default would be a clock,
101
+ * and this kernel has none. Passing it explicitly is what reports a
102
+ * missing edge: an empty morning before the first booking is only a gap
103
+ * if the caller says the day starts at nine.
104
+ *
105
+ * @param {Interval[]} intervals - in any order
106
+ * @param {Interval} [within] - the window to look inside
107
+ * @returns {Interval[]} new `{ start, end }` records
108
+ * @throws {TypeError} for an empty, reversed or non-finite interval
109
+ * @example
110
+ * gapsWithin([{ start: 0, end: 10 }, { start: 30, end: 40 }]);
111
+ * // [{ start: 10, end: 30 }]
112
+ * gapsWithin([{ start: 10, end: 20 }], { start: 0, end: 30 });
113
+ * // [{ start: 0, end: 10 }, { start: 20, end: 30 }]
114
+ */
115
+ export declare function gapsWithin(intervals: Interval[], within?: Interval): Interval[];
116
+ /**
117
+ * How many milliseconds `intervals` cover, counting an instant once
118
+ * however many spans hold it.
119
+ *
120
+ * Restricted to `within` when given, which is what turns it into a
121
+ * ratio: `coverageOf(shifts, day) / (day.end - day.start)` is the
122
+ * fraction of the day that is staffed.
123
+ *
124
+ * @param {Interval[]} intervals - in any order
125
+ * @param {Interval} [within] - clip to this window first
126
+ * @returns {number} milliseconds
127
+ * @throws {TypeError} for an empty, reversed or non-finite interval
128
+ * @example
129
+ * coverageOf([{ start: 0, end: 10 }, { start: 5, end: 20 }]); // 20
130
+ */
131
+ export declare function coverageOf(intervals: Interval[], within?: Interval): number;
132
+ /**
133
+ * `findSlots`' closed specification: how long a slot is and how far
134
+ * apart two of them start. Enumeration, never a constraint solver.
135
+ */
136
+ export declare const SLOTS_MEMBERS: readonly string[];
137
+ /**
138
+ * Every place a span of `duration` fits inside `availability`.
139
+ *
140
+ * Availability is merged first — two touching windows are one window,
141
+ * so a meeting may straddle the seam — and each merged window is then
142
+ * walked from its own start in `step` increments (default: back to
143
+ * back), keeping every span that still ends inside the window. A window
144
+ * exactly one duration wide yields exactly one slot.
145
+ *
146
+ * This is enumeration, not scheduling: it answers "where could this
147
+ * go", and choosing among the answers, weighing preferences or
148
+ * assigning people is a solver's job, deliberately not this one.
149
+ *
150
+ * `duration` and `step` are fixed widths — a number of milliseconds or
151
+ * a fixed ISO 8601 duration (`'PT30M'`). A calendar duration is refused
152
+ * rather than approximated, because "every month" needs a calendar and
153
+ * a zone to say where its boundaries fall.
154
+ *
155
+ * @param {Interval[]} availability - in any order
156
+ * @param {Object} spec
157
+ * @param {number | string} spec.duration - how long the span is
158
+ * @param {number | string} [spec.step] - the spacing between starts
159
+ * (default: `duration`)
160
+ * @returns {Interval[]} new `{ start, end }` records, ascending
161
+ * @throws {TypeError} for an empty, reversed or non-finite interval, or
162
+ * a duration/step that is not a positive fixed width
163
+ * @example
164
+ * findSlots([{ start: 0, end: 90 }], { duration: 60, step: 30 });
165
+ * // [{ start: 0, end: 60 }, { start: 30, end: 90 }]
166
+ */
167
+ export declare function findSlots(availability: Interval[], spec: {
168
+ duration: number | string;
169
+ step?: number | string;
170
+ }): Interval[];