react-native-roster 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -168,7 +168,7 @@ and "Availability may be incomplete"; both are localizable props.
168
168
  | Import | Shipped surface |
169
169
  | --- | --- |
170
170
  | `react-native-roster` | `Roster`, `Schedule`, hooks, slot components, `RosterSelectionPopover`, `PortalHost`, `Portal`, and all core exports. |
171
- | `react-native-roster/core` | Types, `layoutLane`, `coverageFor`, `flagFor`, axis helpers, comparators, and counters. Standard JavaScript and `Intl` only. |
171
+ | `react-native-roster/core` | Types, `layoutLane`, `coverageFor`, `flagFor`, `extentOf`, `intersectionOf`, `unionOf`, axis helpers, comparators, and counters. Standard JavaScript and `Intl` only. |
172
172
  | `react-native-roster/rrule` | `expandRuleSet`, `envelopeFor`, types, and expansion counters and caches. Uses pinned `rrule-temporal` and `@js-temporal/polyfill`. |
173
173
  | `react-native-roster/nativewind` | Registers `Roster` and `Schedule` with NativeWind so their `className` props resolve; re-exports the registered components. |
174
174
 
@@ -180,6 +180,51 @@ importing one function from the root costs the whole root bundle; import from
180
180
  `/core` for a core-only bundle. Cache keys, counters, and clearing are in
181
181
  [caches](./docs/caches.md).
182
182
 
183
+ ## Core
184
+
185
+ ### Interval helpers
186
+
187
+ `unionOf(spans: readonly Window[]): Window[]` merges overlapping or touching
188
+ windows and returns them sorted by start, keeping disjoint windows separate.
189
+ Empty input returns `[]`. Every returned window is a new object; the input array
190
+ and its windows remain untouched. Like `extentOf` and `intersectionOf`, it uses
191
+ end-exclusive epoch milliseconds and throws `RangeError` for non-finite bounds
192
+ or `end <= start`.
193
+
194
+ Collapse each person's segments with `extentOf`, then use `intersectionOf` to
195
+ find the shared time across those windows. Each extent bridges gaps between
196
+ segments, so the result describes the collapsed windows.
197
+
198
+ ```ts
199
+ import type { Window } from 'react-native-roster/core';
200
+ import { extentOf, intersectionOf } from 'react-native-roster/core';
201
+
202
+ const people: { name: string; segments: Window[] }[] = [
203
+ {
204
+ name: 'Alex',
205
+ segments: [
206
+ { start: 1_000, end: 3_000 },
207
+ { start: 4_000, end: 8_000 },
208
+ ],
209
+ },
210
+ {
211
+ name: 'Sam',
212
+ segments: [
213
+ { start: 2_000, end: 5_000 },
214
+ { start: 6_000, end: 9_000 },
215
+ ],
216
+ },
217
+ ];
218
+
219
+ const windows = people.map((person): Window => {
220
+ const extent = extentOf(person.segments);
221
+ if (extent === null) throw new Error(`${person.name} has no segments`);
222
+ return extent;
223
+ });
224
+ const sharedTime = intersectionOf(windows); // { start: 2_000, end: 8_000 }
225
+ // A null result from intersectionOf means there is no shared time.
226
+ ```
227
+
183
228
  ## Navigation and interaction
184
229
 
185
230
  A `WindowSpec` is `{ span: 'day' | 'week' | 'month', anchorDate, timezone, wkst? }`
@@ -9,5 +9,6 @@ export type { LaneComparator } from './order';
9
9
  export { byCoverage, byLabel } from './order';
10
10
  export { snapToStep, timeAtX, timeAtY } from './snap';
11
11
  export { hasSource } from './source';
12
+ export { extentOf, intersectionOf, unionOf } from './spans';
12
13
  export type { Coverage, DayColumn, Gap, Interval, Lane, LaneFlag, LaneGeometry, Layer, LayerRole, LayerStyle, Projection, Rect, Source, Transition, Weekday, Window, } from './types';
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,YAAY,EACV,QAAQ,EACR,SAAS,EACT,GAAG,EACH,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,KAAK,EACL,SAAS,EACT,UAAU,EACV,UAAU,EACV,IAAI,EACJ,MAAM,EACN,UAAU,EACV,OAAO,EACP,MAAM,GACP,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC5D,YAAY,EACV,QAAQ,EACR,SAAS,EACT,GAAG,EACH,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,KAAK,EACL,SAAS,EACT,UAAU,EACV,UAAU,EACV,IAAI,EACJ,MAAM,EACN,UAAU,EACV,OAAO,EACP,MAAM,GACP,MAAM,SAAS,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasSource = exports.timeAtY = exports.timeAtX = exports.snapToStep = exports.byLabel = exports.byCoverage = exports.layoutLane = exports.flagFor = exports.coverageFor = exports.dayColumnsFor = exports.resetStats = exports.layoutStats = exports.coverageStats = exports.clearLayoutCache = exports.clearCoverageCache = exports.windowFor = exports.today = exports.prev = exports.next = void 0;
3
+ exports.unionOf = exports.intersectionOf = exports.extentOf = exports.hasSource = exports.timeAtY = exports.timeAtX = exports.snapToStep = exports.byLabel = exports.byCoverage = exports.layoutLane = exports.flagFor = exports.coverageFor = exports.dayColumnsFor = exports.resetStats = exports.layoutStats = exports.coverageStats = exports.clearLayoutCache = exports.clearCoverageCache = exports.windowFor = exports.today = exports.prev = exports.next = void 0;
4
4
  var axis_1 = require("./axis");
5
5
  Object.defineProperty(exports, "next", { enumerable: true, get: function () { return axis_1.next; } });
6
6
  Object.defineProperty(exports, "prev", { enumerable: true, get: function () { return axis_1.prev; } });
@@ -29,4 +29,8 @@ Object.defineProperty(exports, "timeAtX", { enumerable: true, get: function () {
29
29
  Object.defineProperty(exports, "timeAtY", { enumerable: true, get: function () { return snap_1.timeAtY; } });
30
30
  var source_1 = require("./source");
31
31
  Object.defineProperty(exports, "hasSource", { enumerable: true, get: function () { return source_1.hasSource; } });
32
+ var spans_1 = require("./spans");
33
+ Object.defineProperty(exports, "extentOf", { enumerable: true, get: function () { return spans_1.extentOf; } });
34
+ Object.defineProperty(exports, "intersectionOf", { enumerable: true, get: function () { return spans_1.intersectionOf; } });
35
+ Object.defineProperty(exports, "unionOf", { enumerable: true, get: function () { return spans_1.unionOf; } });
32
36
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/index.ts"],"names":[],"mappings":";;;AACA,+BAAsD;AAA7C,4FAAA,IAAI,OAAA;AAAE,4FAAA,IAAI,OAAA;AAAE,6FAAA,KAAK,OAAA;AAAE,iGAAA,SAAS,OAAA;AACrC,iCAMiB;AALf,2GAAA,kBAAkB,OAAA;AAClB,yGAAA,gBAAgB,OAAA;AAChB,sGAAA,aAAa,OAAA;AACb,oGAAA,WAAW,OAAA;AACX,mGAAA,UAAU,OAAA;AAEZ,qCAA0C;AAAjC,wGAAA,aAAa,OAAA;AACtB,uCAAyC;AAAhC,uGAAA,WAAW,OAAA;AACpB,+BAAiC;AAAxB,+FAAA,OAAO,OAAA;AAChB,mCAAsC;AAA7B,oGAAA,UAAU,OAAA;AAEnB,iCAA8C;AAArC,mGAAA,UAAU,OAAA;AAAE,gGAAA,OAAO,OAAA;AAC5B,+BAAsD;AAA7C,kGAAA,UAAU,OAAA;AAAE,+FAAA,OAAO,OAAA;AAAE,+FAAA,OAAO,OAAA;AACrC,mCAAqC;AAA5B,mGAAA,SAAS,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/index.ts"],"names":[],"mappings":";;;AACA,+BAAsD;AAA7C,4FAAA,IAAI,OAAA;AAAE,4FAAA,IAAI,OAAA;AAAE,6FAAA,KAAK,OAAA;AAAE,iGAAA,SAAS,OAAA;AACrC,iCAMiB;AALf,2GAAA,kBAAkB,OAAA;AAClB,yGAAA,gBAAgB,OAAA;AAChB,sGAAA,aAAa,OAAA;AACb,oGAAA,WAAW,OAAA;AACX,mGAAA,UAAU,OAAA;AAEZ,qCAA0C;AAAjC,wGAAA,aAAa,OAAA;AACtB,uCAAyC;AAAhC,uGAAA,WAAW,OAAA;AACpB,+BAAiC;AAAxB,+FAAA,OAAO,OAAA;AAChB,mCAAsC;AAA7B,oGAAA,UAAU,OAAA;AAEnB,iCAA8C;AAArC,mGAAA,UAAU,OAAA;AAAE,gGAAA,OAAO,OAAA;AAC5B,+BAAsD;AAA7C,kGAAA,UAAU,OAAA;AAAE,+FAAA,OAAO,OAAA;AAAE,+FAAA,OAAO,OAAA;AACrC,mCAAqC;AAA5B,mGAAA,SAAS,OAAA;AAClB,iCAA4D;AAAnD,iGAAA,QAAQ,OAAA;AAAE,uGAAA,cAAc,OAAA;AAAE,gGAAA,OAAO,OAAA"}
@@ -1,3 +1,22 @@
1
1
  import type { Interval, Window } from './types';
2
2
  export declare function sourceSpans(intervals: Interval[], window: Window): Interval[];
3
+ /**
4
+ * Earliest start through latest end in epoch milliseconds, end-exclusive; empty input returns null.
5
+ * Throws RangeError for non-finite bounds or end <= start.
6
+ * Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
7
+ */
8
+ export declare function extentOf(segments: readonly Window[]): Window | null;
9
+ /**
10
+ * Shared epoch milliseconds, end-exclusive; empty input or an empty intersection returns null.
11
+ * Throws RangeError for non-finite bounds or end <= start.
12
+ * Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
13
+ */
14
+ export declare function intersectionOf(spans: readonly Window[]): Window | null;
15
+ /**
16
+ * Merged end-exclusive epoch windows sorted by start; empty input returns an empty array.
17
+ * Overlapping or touching spans merge; disjoint spans remain separate.
18
+ * Throws RangeError for non-finite bounds or end <= start.
19
+ * Returns new objects without mutating inputs.
20
+ */
21
+ export declare function unionOf(spans: readonly Window[]): Window[];
3
22
  //# sourceMappingURL=spans.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spans.d.ts","sourceRoot":"","sources":["../../../src/core/spans.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAU,MAAM,EAAE,MAAM,SAAS,CAAC;AAIxD,wBAAgB,WAAW,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CAmD7E"}
1
+ {"version":3,"file":"spans.d.ts","sourceRoot":"","sources":["../../../src/core/spans.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAU,MAAM,EAAE,MAAM,SAAS,CAAC;AAIxD,wBAAgB,WAAW,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CAmD7E;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CASnE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAUtE;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAgB1D"}
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sourceSpans = sourceSpans;
4
+ exports.extentOf = extentOf;
5
+ exports.intersectionOf = intersectionOf;
6
+ exports.unionOf = unionOf;
4
7
  function sourceSpans(intervals, window) {
5
8
  const boundaries = [];
6
9
  for (const interval of intervals) {
@@ -57,4 +60,65 @@ function sourceSpans(intervals, window) {
57
60
  }
58
61
  return spans;
59
62
  }
63
+ /**
64
+ * Earliest start through latest end in epoch milliseconds, end-exclusive; empty input returns null.
65
+ * Throws RangeError for non-finite bounds or end <= start.
66
+ * Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
67
+ */
68
+ function extentOf(segments) {
69
+ let start = Infinity;
70
+ let end = -Infinity;
71
+ for (const segment of segments) {
72
+ validateSpan(segment);
73
+ start = Math.min(start, segment.start);
74
+ end = Math.max(end, segment.end);
75
+ }
76
+ return start < end ? { start, end } : null;
77
+ }
78
+ /**
79
+ * Shared epoch milliseconds, end-exclusive; empty input or an empty intersection returns null.
80
+ * Throws RangeError for non-finite bounds or end <= start.
81
+ * Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
82
+ */
83
+ function intersectionOf(spans) {
84
+ if (spans.length === 0)
85
+ return null;
86
+ let start = -Infinity;
87
+ let end = Infinity;
88
+ for (const span of spans) {
89
+ validateSpan(span);
90
+ start = Math.max(start, span.start);
91
+ end = Math.min(end, span.end);
92
+ }
93
+ return start < end ? { start, end } : null;
94
+ }
95
+ /**
96
+ * Merged end-exclusive epoch windows sorted by start; empty input returns an empty array.
97
+ * Overlapping or touching spans merge; disjoint spans remain separate.
98
+ * Throws RangeError for non-finite bounds or end <= start.
99
+ * Returns new objects without mutating inputs.
100
+ */
101
+ function unionOf(spans) {
102
+ const sorted = spans.map((span) => {
103
+ validateSpan(span);
104
+ return { start: span.start, end: span.end };
105
+ });
106
+ sorted.sort((a, b) => a.start - b.start);
107
+ const merged = [];
108
+ for (const span of sorted) {
109
+ const last = merged.at(-1);
110
+ if (last && span.start <= last.end) {
111
+ last.end = Math.max(last.end, span.end);
112
+ }
113
+ else {
114
+ merged.push(span);
115
+ }
116
+ }
117
+ return merged;
118
+ }
119
+ function validateSpan(span) {
120
+ if (!Number.isFinite(span.start) || !Number.isFinite(span.end) || span.end <= span.start) {
121
+ throw new RangeError('Span bounds must be finite with end greater than start');
122
+ }
123
+ }
60
124
  //# sourceMappingURL=spans.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"spans.js","sourceRoot":"","sources":["../../../src/core/spans.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"spans.js","sourceRoot":"","sources":["../../../src/core/spans.ts"],"names":[],"mappings":"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"}
package/llms.txt CHANGED
@@ -145,10 +145,27 @@ show the incompleteLabel; do not describe incomplete empty time as definitive.
145
145
  | Import | Runtime exports and purpose |
146
146
  | --- | --- |
147
147
  | react-native-roster | Roster, Schedule, hooks, default slot components, RosterSelectionPopover, PortalHost, Portal, and core re-exports. |
148
- | react-native-roster/core | layoutLane, coverageFor, flagFor, windowFor, prev, next, today, dayColumnsFor, timeAtX, timeAtY, snapToStep, byLabel, byCoverage, and core counters/caches. |
148
+ | react-native-roster/core | layoutLane, coverageFor, flagFor, extentOf, intersectionOf, unionOf, windowFor, prev, next, today, dayColumnsFor, timeAtX, timeAtY, snapToStep, byLabel, byCoverage, and core counters/caches. |
149
149
  | react-native-roster/rrule | expandRuleSet, envelopeFor, expandStats, resetExpandStats, clearExpandCache; RuleSet, RosterRule, RosterDate, ExpandOptions, ExpandResult, and ExpandStats types. |
150
150
  | react-native-roster/nativewind | Side-effect registration of Roster and Schedule with NativeWind's cssInterop, plus the registered Roster and Schedule and the rosterClassNames/scheduleClassNames mappings. Imports the optional nativewind peer. |
151
151
 
152
+ ## Interval helpers
153
+
154
+ extentOf(segments: readonly Window[]): Window | null returns earliest start through
155
+ latest end, bridging disjoint segments. intersectionOf(spans: readonly Window[]):
156
+ Window | null returns latest start through earliest end. Both use end-exclusive
157
+ epoch milliseconds and return null for empty input.
158
+ Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
159
+ An empty intersection, including touching bounds, returns null. Every input is
160
+ validated; non-finite bounds or end <= start throw RangeError. Window is the bare
161
+ { start, end } type; Span remains the axis choice type.
162
+
163
+ unionOf(spans: readonly Window[]): Window[] merges overlapping or touching
164
+ end-exclusive windows, sorts them by start, and keeps disjoint windows separate.
165
+ Empty input returns []; every returned window is a new object. The input array
166
+ and its windows remain untouched. Every span is validated with the same
167
+ RangeError rules as extentOf and intersectionOf.
168
+
152
169
  No default exports. Root exports renderer inputs/models/slot input types as well as
153
170
  core types. Core imports standard JS and Intl only. Only /rrule imports
154
171
  rrule-temporal 1.6.0 and @js-temporal/polyfill 0.5.1. Those are installed runtime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-roster",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Layered intervals with provenance across lanes and a shared time axis for React Native.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -6,6 +6,9 @@ no React Native, no dependencies.
6
6
  - Public subpath: `react-native-roster/core`; the root entry re-exports it
7
7
  - Authoritative types: `types.ts` (`Lane`, `Layer`, `Interval`, `Gap`,
8
8
  `Source`, `Window`, `Projection`, `Rect`, `Coverage`, `LaneGeometry`)
9
+ - Interval helpers: `extentOf(segments: readonly Window[]): Window | null`,
10
+ `intersectionOf(spans: readonly Window[]): Window | null`, and
11
+ `unionOf(spans: readonly Window[]): Window[]`
9
12
  - Barrel: `index.ts` only
10
13
 
11
14
  | File | Role |
@@ -14,7 +17,7 @@ no React Native, no dependencies.
14
17
  | `layout.ts` | rect geometry for one lane in either projection |
15
18
  | `coverage.ts` | covered and removed minutes per lane |
16
19
  | `source.ts` | `hasSource(sources, source)` matches kind and id, ignoring labels |
17
- | `spans.ts` | the provenance sweep: exact sources per merged span |
20
+ | `spans.ts` | `extentOf`, `intersectionOf`, `unionOf`, and the provenance sweep with exact sources per merged span |
18
21
  | `columns.ts` | day columns, transitions, and skipped dates for the columns projection |
19
22
  | `zone.ts` | Intl-only timezone math (offsets, first instants, transitions) |
20
23
  | `scale.ts`, `snap.ts` | pixel and time conversion, pointer snapping |
@@ -22,6 +25,16 @@ no React Native, no dependencies.
22
25
  | `order.ts`, `flag.ts` | lane comparators and lane flags |
23
26
  | `cache.ts`, `hash.ts` | layout and coverage caches keyed by lane content |
24
27
 
28
+ All interval helpers reuse the bare `{ start, end }` `Window` type with
29
+ end-exclusive epoch millisecond bounds. `extentOf` bridges disjoint segments;
30
+ `intersectionOf` returns only their shared span. Both return `null` for empty
31
+ input; an empty intersection, including touching bounds, also returns `null`.
32
+ `unionOf` merges overlapping or touching windows, sorts by start, and keeps
33
+ disjoint windows separate. It returns `[]` for empty input and new window objects.
34
+ Every input must have finite bounds and `end > start`; otherwise the helpers throw `RangeError`.
35
+ Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
36
+ The existing `Span` type names axis choices only.
37
+
25
38
  Reference: [design](../../docs/design.md), [timezones](../../docs/timezones.md),
26
39
  [caches](../../docs/caches.md). Tests: `test/core`.
27
40
 
package/src/core/index.ts CHANGED
@@ -15,6 +15,7 @@ export type { LaneComparator } from './order';
15
15
  export { byCoverage, byLabel } from './order';
16
16
  export { snapToStep, timeAtX, timeAtY } from './snap';
17
17
  export { hasSource } from './source';
18
+ export { extentOf, intersectionOf, unionOf } from './spans';
18
19
  export type {
19
20
  Coverage,
20
21
  DayColumn,
package/src/core/spans.ts CHANGED
@@ -1,5 +1,6 @@
1
- // Sweep boundaries once; reference counts preserve provenance through nested
2
- // intervals and equal-time ends/starts. Labels never affect source identity.
1
+ // Provenance sweep logic and the extentOf, intersectionOf, and unionOf interval helpers.
2
+ // The sweep uses reference counts to preserve provenance through nested intervals
3
+ // and equal-time ends/starts. Labels never affect source identity.
3
4
  import type { Interval, Source, Window } from './types';
4
5
 
5
6
  type Boundary = { at: number; delta: number; sources: Source[] };
@@ -56,3 +57,66 @@ export function sourceSpans(intervals: Interval[], window: Window): Interval[] {
56
57
  }
57
58
  return spans;
58
59
  }
60
+
61
+ /**
62
+ * Earliest start through latest end in epoch milliseconds, end-exclusive; empty input returns null.
63
+ * Throws RangeError for non-finite bounds or end <= start.
64
+ * Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
65
+ */
66
+ export function extentOf(segments: readonly Window[]): Window | null {
67
+ let start = Infinity;
68
+ let end = -Infinity;
69
+ for (const segment of segments) {
70
+ validateSpan(segment);
71
+ start = Math.min(start, segment.start);
72
+ end = Math.max(end, segment.end);
73
+ }
74
+ return start < end ? { start, end } : null;
75
+ }
76
+
77
+ /**
78
+ * Shared epoch milliseconds, end-exclusive; empty input or an empty intersection returns null.
79
+ * Throws RangeError for non-finite bounds or end <= start.
80
+ * Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
81
+ */
82
+ export function intersectionOf(spans: readonly Window[]): Window | null {
83
+ if (spans.length === 0) return null;
84
+ let start = -Infinity;
85
+ let end = Infinity;
86
+ for (const span of spans) {
87
+ validateSpan(span);
88
+ start = Math.max(start, span.start);
89
+ end = Math.min(end, span.end);
90
+ }
91
+ return start < end ? { start, end } : null;
92
+ }
93
+
94
+ /**
95
+ * Merged end-exclusive epoch windows sorted by start; empty input returns an empty array.
96
+ * Overlapping or touching spans merge; disjoint spans remain separate.
97
+ * Throws RangeError for non-finite bounds or end <= start.
98
+ * Returns new objects without mutating inputs.
99
+ */
100
+ export function unionOf(spans: readonly Window[]): Window[] {
101
+ const sorted = spans.map((span) => {
102
+ validateSpan(span);
103
+ return { start: span.start, end: span.end };
104
+ });
105
+ sorted.sort((a, b) => a.start - b.start);
106
+ const merged: Window[] = [];
107
+ for (const span of sorted) {
108
+ const last = merged.at(-1);
109
+ if (last && span.start <= last.end) {
110
+ last.end = Math.max(last.end, span.end);
111
+ } else {
112
+ merged.push(span);
113
+ }
114
+ }
115
+ return merged;
116
+ }
117
+
118
+ function validateSpan(span: Window): void {
119
+ if (!Number.isFinite(span.start) || !Number.isFinite(span.end) || span.end <= span.start) {
120
+ throw new RangeError('Span bounds must be finite with end greater than start');
121
+ }
122
+ }