@jarenjs/core 0.46.4 → 0.49.2
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/ARCHITECTURE.md +104 -0
- package/README.md +46 -2
- package/dist/types/dates/civil.d.ts +24 -5
- package/dist/types/dates/index.d.ts +2 -0
- package/dist/types/dates/parse.d.ts +47 -0
- package/dist/types/dates/ticks.d.ts +59 -0
- package/dist/types/math/float64.d.ts +13 -0
- package/dist/types/series/asof.d.ts +88 -0
- package/dist/types/series/bucket.d.ts +206 -0
- package/dist/types/series/downsample.d.ts +53 -0
- package/dist/types/series/index.d.ts +8 -0
- package/dist/types/series/interval-index.d.ts +47 -0
- package/dist/types/series/interval.d.ts +170 -0
- package/dist/types/series/normalize.d.ts +190 -0
- package/dist/types/series/rolling.d.ts +67 -0
- package/dist/types/series/selector.d.ts +29 -0
- package/dist/types/series/zone.d.ts +59 -0
- package/docs/DATES.md +77 -1
- package/docs/SERIES.md +342 -0
- package/package.json +9 -1
- package/src/dates/civil.js +136 -41
- package/src/dates/index.js +4 -0
- package/src/dates/parse.js +410 -0
- package/src/dates/ticks.js +184 -0
- package/src/math/float64.js +29 -0
- package/src/series/asof.js +276 -0
- package/src/series/bucket.js +542 -0
- package/src/series/downsample.js +343 -0
- package/src/series/index.js +86 -0
- package/src/series/interval-index.js +181 -0
- package/src/series/interval.js +374 -0
- package/src/series/normalize.js +330 -0
- package/src/series/rolling.js +229 -0
- package/src/series/selector.js +104 -0
- package/src/series/zone.js +188 -0
- package/src/string.js +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -109,7 +109,9 @@ flowchart TB
|
|
|
109
109
|
DatesRfc["rfc3339.js<br/>RFC 3339 / ISO 8601"]
|
|
110
110
|
DatesCivil["civil.js<br/>Gregorian day-number math"]
|
|
111
111
|
DatesFormat["format.js<br/>LDML pattern compiler"]
|
|
112
|
+
DatesParse["parse.js<br/>LDML pattern parser"]
|
|
112
113
|
DatesDuration["duration.js<br/>ISO 8601 durations"]
|
|
114
|
+
DatesTicks["ticks.js<br/>Time-axis step ladder"]
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
subgraph GeoModule["Spatial"]
|
|
@@ -130,6 +132,14 @@ flowchart TB
|
|
|
130
132
|
VectorIndex["vector/index.js<br/>Similarity kernels, normalization, packed form"]
|
|
131
133
|
end
|
|
132
134
|
|
|
135
|
+
subgraph SeriesModule["Temporal"]
|
|
136
|
+
SeriesIndex["series/index.js"]
|
|
137
|
+
SeriesSelector["selector.js<br/>Where a member lives in a row"]
|
|
138
|
+
SeriesNormalize["normalize.js<br/>Instants, stable sort, binary bounds"]
|
|
139
|
+
SeriesInterval["interval.js<br/>Half-open set algebra"]
|
|
140
|
+
SeriesTree["interval-index.js<br/>Prefix-max-end interval index"]
|
|
141
|
+
end
|
|
142
|
+
|
|
133
143
|
subgraph MathModules["Mathematics"]
|
|
134
144
|
MathIndex["math/index.js"]
|
|
135
145
|
Int32Math["int32.js<br/>Fixed-point math"]
|
|
@@ -191,6 +201,7 @@ flowchart TB
|
|
|
191
201
|
DatesIndex --> DatesCivil
|
|
192
202
|
DatesIndex --> DatesFormat
|
|
193
203
|
DatesIndex --> DatesDuration
|
|
204
|
+
DatesIndex --> DatesTicks
|
|
194
205
|
|
|
195
206
|
GeoIndex --> GeoPred
|
|
196
207
|
GeoIndex --> GeoDist
|
|
@@ -462,7 +473,9 @@ intermediate and is plain data, never an opaque handle.
|
|
|
462
473
|
| `rfc3339.js` | validation, lexical decomposition, epoch conversion |
|
|
463
474
|
| `civil.js` | proleptic Gregorian arithmetic over integers |
|
|
464
475
|
| `format.js` | LDML pattern → compiled formatter |
|
|
476
|
+
| `parse.js` | LDML pattern → compiled strict parser (the formatter's inverse) |
|
|
465
477
|
| `duration.js` | ISO 8601 duration decomposition and date arithmetic |
|
|
478
|
+
| `ticks.js` | the time-axis step ladder and its calendar boundaries |
|
|
466
479
|
|
|
467
480
|
Calendar math goes through day numbers (`daysFromCivil`/`civilFromDays`),
|
|
468
481
|
never through `Date`: the conversions are ~10 integer operations and allocate
|
|
@@ -475,6 +488,23 @@ allocates nothing per value. Formatting is the two-stage compiler again — a
|
|
|
475
488
|
pattern is scanned once into a chain of appenders, which measured ~4.5×
|
|
476
489
|
against re-scanning it per call.
|
|
477
490
|
|
|
491
|
+
Parsing is the same compiler run backwards. `compileDateParser` scans a
|
|
492
|
+
pattern once into a chain of readers and returns a function from text to the
|
|
493
|
+
same parts record `parseRFC3339Parts` produces, so every calendar function
|
|
494
|
+
takes its output unchanged. It is strict in three ways that a permissive
|
|
495
|
+
reader is not: trailing input fails, an impossible civil date (31 February,
|
|
496
|
+
hour 24) fails rather than rolling forward into the next month, and a
|
|
497
|
+
fixed-width token reads exactly its width. Failure is `null` — malformed
|
|
498
|
+
*text* is data — while a malformed *pattern* throws at compile time. The
|
|
499
|
+
tokens that only ever format, because they are derived from a date rather
|
|
500
|
+
than fields of one (`EEEE`, `DDD`, `ww`, `Q`), are compile errors with a
|
|
501
|
+
message saying which and why, rather than tokens that quietly read nothing.
|
|
502
|
+
A consumer whose patterns are not LDML — Mermaid's Gantt `dateFormat` is
|
|
503
|
+
moment's grammar and its `axisFormat` is strftime — adapts its own tokens
|
|
504
|
+
onto this vocabulary rather than handing its spelling to this compiler:
|
|
505
|
+
LDML's `YYYY` is the week-numbering year, so `YYYY-MM-DD` read as LDML
|
|
506
|
+
answers 2019 for 2018-12-31.
|
|
507
|
+
|
|
478
508
|
Locale-dependent presentation (month and weekday names, meridiem, relative
|
|
479
509
|
phrasing) is deliberately **not** here, so this package stays zero-dependency
|
|
480
510
|
and free of data that drifts per language: `compileDateFormat` takes a names
|
|
@@ -532,11 +562,31 @@ import {
|
|
|
532
562
|
const parts = parseRFC3339Parts('2026-01-31');
|
|
533
563
|
addToParts(parts, 1, 'month'); // 2026-02-28 — month math clamps
|
|
534
564
|
startOfParts(parts, 'week'); // the Monday of that week (ISO 8601)
|
|
565
|
+
addToParts(parts, 3, 'hour'); // TypeError — a full-date has no clock
|
|
535
566
|
|
|
536
567
|
const fmt = compileDateFormat("yyyy-'W'ww"); // compile once…
|
|
537
568
|
fmt(parts); // …call many: '2026-W05'
|
|
569
|
+
|
|
570
|
+
const read = compileDateParser('dd-MM-yyyy');
|
|
571
|
+
read('06-01-2014'); // { year: 2014, month: 1, day: 6, … }
|
|
572
|
+
read('06-01-2014x'); // null — trailing input
|
|
573
|
+
read('31-02-2014'); // null — February has no 31st
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Ticks a caller chose, or ticks this module chose:**
|
|
577
|
+
|
|
578
|
+
```javascript
|
|
579
|
+
import { axisTicksTime, timeTicksEvery } from '@jarenjs/core/dates';
|
|
580
|
+
|
|
581
|
+
axisTicksTime(from, to, 4); // the ladder picks the step
|
|
582
|
+
timeTicksEvery(from, to, 'week', 1, { weekStart: 7 }); // Sundays
|
|
538
583
|
```
|
|
539
584
|
|
|
585
|
+
`axisTicksTime` is the chart axis's planner; `timeTicksEvery` is the same
|
|
586
|
+
boundary rule with the step supplied, which is what a document declaring its
|
|
587
|
+
own interval (Mermaid's `tickInterval 1week`) needs. One ladder, so a chart
|
|
588
|
+
and a timeline cannot disagree about where a tick falls.
|
|
589
|
+
|
|
540
590
|
### 5b. Geo Module (`geo/`)
|
|
541
591
|
|
|
542
592
|
The spatial kernel. As with dates, **there is no geometry type**: the
|
|
@@ -721,6 +771,60 @@ their reason to exist; they guarantee a finite answer. The fixed-arity vector
|
|
|
721
771
|
classes in `math/` (`Vec2f64`, `Vec3f64`) are 2D/3D geometry with a
|
|
722
772
|
different convention and a different job, and they stay where they are.
|
|
723
773
|
|
|
774
|
+
### 5d. Series Module (`series/`)
|
|
775
|
+
|
|
776
|
+
The temporal kernel: one meaning for an instant, one for a sorted series of
|
|
777
|
+
readings, one for an interval, and the set algebra over them. As with dates,
|
|
778
|
+
**there is no type** — an instant is epoch milliseconds or an RFC 3339 string,
|
|
779
|
+
a sample is `{ at, value }`, an interval is `{ start, end }` — and, as with
|
|
780
|
+
dates, **there is no `now`**: every bound is data, so every answer here is
|
|
781
|
+
reproducible and cacheable.
|
|
782
|
+
|
|
783
|
+
| File | Owns |
|
|
784
|
+
|---|---|
|
|
785
|
+
| `selector.js` | where a member lives in a caller's row — a property name or a function, so `on`/`recorded_at`/`from`/`to` are read where they are |
|
|
786
|
+
| `normalize.js` | `toEpoch`, `normalizeSeries`, `normalizeIntervals` and the `lowerBoundTime`/`upperBoundTime` cuts a sorted array is read through |
|
|
787
|
+
| `interval.js` | `containsInstant`, `overlapsInterval`, `intersectInterval`, `mergeIntervals`, `subtractIntervals`, `gapsWithin`, `coverageOf`, `findSlots` |
|
|
788
|
+
| `interval-index.js` | `createIntervalIndex` — build once, query many, by point or by range |
|
|
789
|
+
|
|
790
|
+
Four decisions carry it. **Half-open `[start, end)`**: an interval holds its
|
|
791
|
+
start and not its end, so touching intervals neither overlap nor double-count
|
|
792
|
+
and a boundary instant belongs to exactly one of them — while `mergeIntervals`
|
|
793
|
+
*joins* touching spans by default, because availability means continuous cover,
|
|
794
|
+
with `{ adjacent: false }` as the explicit other answer. **Nothing is dropped**:
|
|
795
|
+
a member that names no instant, or an interval that is empty, reversed or
|
|
796
|
+
non-finite, is a refusal naming the row rather than a silently shorter result,
|
|
797
|
+
and the sort is stable so duplicate instants keep their order and both count.
|
|
798
|
+
**No clock**: `gapsWithin` and `coverageOf` derive their window from the input's
|
|
799
|
+
own hull when given none, because the only other default would be "now".
|
|
800
|
+
|
|
801
|
+
And **the index cuts on both ends**. Sorting by start alone is the bug: a span
|
|
802
|
+
that began long before a query sits far to the left of the query's own
|
|
803
|
+
neighbourhood and still overlaps it, so a binary search around the query loses
|
|
804
|
+
it while looking plausible. `createIntervalIndex` therefore carries a prefix
|
|
805
|
+
maximum end beside the starts — non-decreasing by construction, hence
|
|
806
|
+
binary-searchable — and the first position where it passes the query's start is
|
|
807
|
+
the first position where anything can still be live. A query is two binary cuts
|
|
808
|
+
and a walk between them, O(log n + k), returning the caller's own rows in a
|
|
809
|
+
fresh array. Like the packed-Hilbert box index it is **static**: bounds are
|
|
810
|
+
copied into flat typed arrays at build time, so a query reads no source object
|
|
811
|
+
and a row mutated afterwards cannot change what the index answers.
|
|
812
|
+
|
|
813
|
+
Buckets, resampling, rolling windows and as-of joins are the layer above this
|
|
814
|
+
one; named zones, recurrence grammars and scheduling solvers are outside it.
|
|
815
|
+
`findSlots` enumerates where a fixed-width span fits — choosing among the
|
|
816
|
+
answers is a solver's job, deliberately not this kernel's.
|
|
817
|
+
|
|
818
|
+
```javascript
|
|
819
|
+
import { createIntervalIndex, mergeIntervals, findSlots } from '@jarenjs/core/series';
|
|
820
|
+
|
|
821
|
+
const shifts = [{ start: 0, end: 4 * 3600_000 }, { start: 4 * 3600_000, end: 8 * 3600_000 }];
|
|
822
|
+
mergeIntervals(shifts); // one span — touching IS continuous cover
|
|
823
|
+
mergeIntervals(shifts, { adjacent: false }); // two — a handover is two shifts
|
|
824
|
+
findSlots(shifts, { duration: 'PT30M' }).length; // 16
|
|
825
|
+
createIntervalIndex(shifts).at(4 * 3600_000); // the second shift only: [start, end)
|
|
826
|
+
```
|
|
827
|
+
|
|
724
828
|
### 6. Text Module (`text/`)
|
|
725
829
|
|
|
726
830
|
Comprehensive string format validation organized by domain.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @jarenjs/core
|
|
2
2
|
|
|
3
|
-
The zero-dependency foundation of [Jaren](https://github.com/jklarenbeek/jarenjs). Everything the rest of the suite is built on lives here — type guards, Unicode-aware string handling, a large text-validation toolbox, number range helpers, fixed-point and vector math, the calendar kernel, the spatial kernel, the vector kernel, the message-catalog compiler, unit/currency conversion and a finance library.
|
|
3
|
+
The zero-dependency foundation of [Jaren](https://github.com/jklarenbeek/jarenjs). Everything the rest of the suite is built on lives here — type guards, Unicode-aware string handling, a large text-validation toolbox, number range helpers, fixed-point and vector math, the calendar kernel, the spatial kernel, the vector kernel, the interval kernel, the message-catalog compiler, unit/currency conversion and a finance library.
|
|
4
4
|
|
|
5
5
|
None of it depends on JSON Schema: every module can be used standalone in any JavaScript project.
|
|
6
6
|
|
|
@@ -24,6 +24,7 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
|
|
|
24
24
|
| `@jarenjs/core/dates` | RFC 3339 / ISO 8601 validation, plus the calendar kernel: integer date arithmetic, compiled formatting, durations |
|
|
25
25
|
| `@jarenjs/core/geo` | the spatial kernel over GeoJSON: robust orientation, great-circle measurement, rings, bounding boxes, geohash, GeoJSON/WKT validity, a packed-Hilbert box index, Web Mercator and Douglas-Peucker simplification |
|
|
26
26
|
| `@jarenjs/core/vector` | the vector kernel over plain arrays: dot, cosine and Euclidean similarity (higher-is-better; a malformed pair scores 0), l2 normalization, the packed little-endian Float32 form and the one shape guard (`isVector`) |
|
|
27
|
+
| `@jarenjs/core/series` | the temporal kernel over plain records: instant/sample/interval normalization with a stable sort, half-open `[start, end)` set algebra (overlap, merge, subtract, gaps, coverage, slot enumeration), a static interval index, and the series operations built on them — fixed and calendar buckets with five fill policies, time-width rolling aggregates, as-of joins and gap-aware downsampling, on an injected zone seam |
|
|
27
28
|
| `@jarenjs/core/text` | text validators: emails, hostnames, IPs, URIs/IRIs, UUIDs, punycode, ... |
|
|
28
29
|
| `@jarenjs/core/math` | int32/float64 math and 2D/3D vector classes; the linear `remap` and unit-interval `clamp01` |
|
|
29
30
|
| `@jarenjs/core/finance` | zero-dependency finance/trading formulas: TVM, cash flow, amortization, interest, depreciation, bonds, technical indicators, returns/risk |
|
|
@@ -65,7 +66,7 @@ Grouped by file: `email` (RFC 5321 + internationalized addresses), `host` (hostn
|
|
|
65
66
|
|
|
66
67
|
## Dates, numbers and math
|
|
67
68
|
|
|
68
|
-
- `dates` — RFC 3339 date/time/date-time validation and parsing (leap years and month lengths included), the more lenient ISO date-time forms, and the suite's calendar kernel: proleptic Gregorian arithmetic over integer day numbers (`addToParts`, `startOfParts`, `endOfParts` — month math clamps, so 31 Jan plus a month is 28 Feb), ISO 8601 duration decomposition, and `compileDateFormat`, which
|
|
69
|
+
- `dates` — RFC 3339 date/time/date-time validation and parsing (leap years and month lengths included), the more lenient ISO date-time forms, and the suite's calendar kernel: proleptic Gregorian arithmetic over integer day numbers (`addToParts`, `startOfParts`, `endOfParts` — month math clamps, so 31 Jan plus a month is 28 Feb, and an operation reading a half the value has not got is refused rather than guessed), ISO 8601 duration decomposition, the locale-free time-axis step ladder (`niceTimeStep`, `axisTicksTime`, and `timeTicksEvery` for a step the caller declares) a chart and a timeline both read, and the two-stage pattern compilers `compileDateFormat` and its strict inverse `compileDateParser`, which turn an LDML pattern into a formatter or a parser once instead of re-scanning it per call — the parser refuses trailing input, an impossible civil date, and the tokens that only format because they are derived from a date rather than fields of one. Dates stay JSON: an RFC 3339 string or epoch milliseconds, never a wrapper object. Locale names live in `@jarenjs/locales`, so a pattern needing `MMMM` takes a names provider. The full module reference is [docs/DATES.md](./docs/DATES.md); the interval algebra built on top of it is [`series`](#intervals-and-series).
|
|
69
70
|
- `integer`/`float`/`bigint` — range constants and validators for every fixed-width type from `int8` to `uint64` and `float16` to `float64`, including float increment/decrement in representable steps.
|
|
70
71
|
- `math` — asm.js-style typed math (`Int32`, `Float64`) and vector classes (`Vec2i32`, `Vec2f64`, `Vec3f64`) with a fast integer sine approximation; reference in [docs/MATH.md](./docs/MATH.md).
|
|
71
72
|
|
|
@@ -123,6 +124,49 @@ Three rules, kept by every function so that no caller has to check them again:
|
|
|
123
124
|
|
|
124
125
|
The packed form is `4·d` bytes of little-endian binary32 — the value a database column stores; components round to `Math.fround` and come back exactly. Unpacking aligned bytes on a little-endian host is a *view*, not a copy, which is what a sweep over ten thousand fetched rows is paid for by; misaligned bytes (a pooled `Buffer`, an odd offset into a record) and big-endian hosts take the copy path to the same values. The client that produces embeddings — and a deterministic reference embedder for tests — lives in [`@jarenjs/ai`](../ai/README.md#embeddings).
|
|
125
126
|
|
|
127
|
+
## Intervals and series
|
|
128
|
+
|
|
129
|
+
`@jarenjs/core/series` is the suite's temporal kernel: one meaning for an interval, one meaning for a sorted series of timestamped readings, the set algebra over them, and the five operations every consumer of a timeline otherwise rebuilds by hand — bucket, fill, roll, join as-of, downsample. As with dates, there is no type — an instant is epoch milliseconds or an RFC 3339 string, a sample is `{ at, value }`, an interval is `{ start, end }` — so every value stays a plain JSON item, and nothing here reads a clock.
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
import { createIntervalIndex, mergeIntervals, gapsWithin, findSlots } from '@jarenjs/core/series';
|
|
133
|
+
|
|
134
|
+
const shifts = [
|
|
135
|
+
{ from: '2026-03-02T09:00:00Z', to: '2026-03-02T13:00:00Z', who: 'ada' },
|
|
136
|
+
{ from: '2026-03-02T13:00:00Z', to: '2026-03-02T17:00:00Z', who: 'grace' },
|
|
137
|
+
];
|
|
138
|
+
const index = createIntervalIndex(shifts, { start: 'from', end: 'to' });
|
|
139
|
+
index.at('2026-03-02T13:00:00Z'); // [grace] — half-open, so the handover belongs to one shift
|
|
140
|
+
|
|
141
|
+
const cover = shifts.map((s) => ({ start: s.from, end: s.to }));
|
|
142
|
+
mergeIntervals(cover); // one span, 09:00–17:00 — touching IS continuous cover
|
|
143
|
+
findSlots(cover, { duration: 'PT30M' }); // sixteen half-hour slots, one straddling the handover
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
import { resampleSeries, rollingSeries, asOfJoin, downsampleSeries } from '@jarenjs/core/series';
|
|
148
|
+
|
|
149
|
+
resampleSeries(readings, { every: 'PT1H', aggregate: 'mean', fill: 'linear' });
|
|
150
|
+
resampleSeries(readings, { every: 'P1M', zone: 'Europe/Amsterdam', provider });
|
|
151
|
+
rollingSeries(readings, { width: 'PT5M', aggregate: 'max', minPeriods: 3 });
|
|
152
|
+
asOfJoin(trades, quotes, { direction: 'nearest', tolerance: 'PT1S', key: 'symbol' });
|
|
153
|
+
downsampleSeries(readings, { target: 2000 }); // → { points, sourceCount, renderedCount, method }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Seven decisions carry the module:
|
|
157
|
+
|
|
158
|
+
- **Half-open, everywhere.** `[start, end)` holds its start and not its end, so a day ends exactly where the next begins, a boundary instant belongs to exactly one of two touching intervals, and nothing is counted twice. Touching intervals therefore do *not* overlap — back-to-back bookings are not a double booking — while `mergeIntervals` joins them by default, because availability asks whether there is continuous cover. `{ adjacent: false }` is the other answer, spelled out rather than guessed.
|
|
159
|
+
- **A row is never dropped, and a duplicate is never merged away.** A member that cannot become a finite instant is a refusal naming the row, not a silently shorter result; an empty (`[t, t)`), reversed or non-finite interval is refused at the point it was written. The sort is stable, so two readings in the same millisecond keep their input order and both count.
|
|
160
|
+
- **Nothing reads a clock.** `gapsWithin` and `coverageOf` derive their window from the input's own hull when given none, because the only other default would be "now" — and an operation that read the clock could not be cached, reproduced or run in a test twice.
|
|
161
|
+
- **The index cuts on both ends, and cannot lose a long span.** Sorting by start alone is the bug: a conference week that began before an hourly meeting sits far to the left of that meeting's neighbourhood and still overlaps it. `createIntervalIndex` carries a prefix maximum end beside the starts — non-decreasing, so binary-searchable — and a query becomes two binary cuts and a walk between them, O(log n + k), returning the caller's own rows. It is static: bounds are copied at build time, so a query reads no source object and a row mutated afterwards changes nothing.
|
|
162
|
+
|
|
163
|
+
- **Bucketing is arithmetic; filling is a policy.** An average over an empty hour is not zero, and it is not yesterday's average, and it is not nothing — it is whichever of `omit | null | zero | locf | linear` the caller asked for, and the aggregate had no opinion. `count` reports source rows (duplicates and measured gaps included) beside every value, so `null` and "nobody reported" are distinguishable. Neither `locf` nor `linear` invents a value at the leading edge.
|
|
164
|
+
- **A rolling window is a duration, not a row count.** Sixty rows of a sensor reporting every second is a minute; sixty rows of a sensor that dropped half its readings is two minutes. The window is `(at − width, at]`, so two readings in the same millisecond share it and therefore share an answer. `sum`/`mean`/`count` carry a running total, `min`/`max` use a monotone deque, `first`/`last` are forward-only pointers — the complexity is structural, and the tests count the reads rather than the milliseconds.
|
|
165
|
+
- **Time zones are injected, never bundled.** UTC and fixed offsets work with no setup; a named zone takes the caller's own `provider`, because a bundled tzdb is megabytes that go stale on a government's timetable. A local time that never happened, or happened twice, is a refusal unless `disambiguation` says `earlier` or `later`. A sampler never bridges a gap, never moves an end, and refuses a target too small to hold both rather than drawing a prettier line.
|
|
166
|
+
- **A specification is closed.** Every kernel that takes one publishes the members it admits, and anything else is a refusal naming the near miss — because `minPeriod` for `minPeriods` quietly ignored is a window with no minimum and a plausible number to show for it, and `timezone` for `zone` is precisely the silent fall back to UTC the injected clock exists to prevent. The query language reads the same lists, minus the `provider`, which is a pair of functions and therefore not something a document can carry.
|
|
167
|
+
|
|
168
|
+
The kernel is measured against one-pass loops written for one question and against the query vocabulary a consumer had instead, and both ratios are published: it costs about twice a hand-written loop and answers about a hundred times faster than the generic route. Named time zones' data, recurrence grammars and scheduling solvers are deliberately outside it. The full module reference is [docs/SERIES.md](./docs/SERIES.md).
|
|
169
|
+
|
|
126
170
|
## Messages
|
|
127
171
|
|
|
128
172
|
`@jarenjs/core/message` is the template/catalog compiler behind the validator's uniform error messages and the form layer's labels — msgid plus parameters in, rendered text out, so every message is translatable by swapping a catalog ([`@jarenjs/locales`](../locales) ships eleven language packs over it):
|
|
@@ -111,36 +111,55 @@ export declare function fixedUnitMs(unit: string): number;
|
|
|
111
111
|
/**
|
|
112
112
|
* Add a signed amount of calendar units to a parts record, returning a
|
|
113
113
|
* new one. The input is never mutated and its lexical shape is kept: a
|
|
114
|
-
* full-date stays a full-date,
|
|
115
|
-
* rather than being normalized.
|
|
114
|
+
* full-date stays a full-date, a full-time stays a full-time, and a
|
|
115
|
+
* value keeps its own UTC offset rather than being normalized.
|
|
116
116
|
*
|
|
117
117
|
* Month and year arithmetic **clamps** to the end of the target month —
|
|
118
118
|
* 2026-01-31 plus one month is 2026-02-28 — which is the rule every
|
|
119
119
|
* mainstream date library uses, because the alternative (overflowing
|
|
120
120
|
* into March) makes `add(1, 'month')` non-monotonic.
|
|
121
121
|
*
|
|
122
|
+
* A fraction is a quantity only where the unit has an exact conversion.
|
|
123
|
+
* A fixed-width fraction becomes whole milliseconds, so `1.5 day` is
|
|
124
|
+
* thirty-six hours; a calendar fraction is refused unless it lands on a
|
|
125
|
+
* whole month, because half of January is not a length. The clock those
|
|
126
|
+
* milliseconds land on has to exist: a full-date can be moved by whole
|
|
127
|
+
* days but not by half of one.
|
|
128
|
+
*
|
|
122
129
|
* @param {object} parts - a parts record from `parseRFC3339Parts`
|
|
123
|
-
* @param {number} amount - signed count,
|
|
124
|
-
*
|
|
130
|
+
* @param {number} amount - signed count, fractional only where the unit
|
|
131
|
+
* converts exactly and the value has the half to carry it
|
|
125
132
|
* @param {string} unit - a {@link DATE_UNITS} member
|
|
126
133
|
* @returns {object} a new parts record
|
|
134
|
+
* @throws {TypeError} for an unknown unit, a fraction with no exact
|
|
135
|
+
* conversion, or a value missing a half the operation reads
|
|
127
136
|
*/
|
|
128
137
|
export declare function addToParts(parts: object, amount: number, unit: string): object;
|
|
129
138
|
/**
|
|
130
139
|
* Truncate a parts record to the start of a calendar unit, returning a
|
|
131
140
|
* new one. `week` starts on Monday (ISO 8601).
|
|
141
|
+
*
|
|
142
|
+
* `day` and coarser name a boundary the calendar has, so the start of
|
|
143
|
+
* the day of a full-time is midnight. A sub-day unit names a boundary
|
|
144
|
+
* inside a day, which a value with no clock does not have, and is
|
|
145
|
+
* refused rather than answered from a clock that is not there.
|
|
146
|
+
*
|
|
132
147
|
* @param {object} parts - a parts record
|
|
133
148
|
* @param {string} unit - a {@link DATE_UNITS} member
|
|
134
149
|
* @returns {object} a new parts record
|
|
150
|
+
* @throws {TypeError} for an unknown unit, or a value missing a half
|
|
151
|
+
* the truncation reads
|
|
135
152
|
*/
|
|
136
153
|
export declare function startOfParts(parts: object, unit: string): object;
|
|
137
154
|
/**
|
|
138
155
|
* The last representable instant inside a calendar unit: the start of
|
|
139
156
|
* the next unit less one millisecond. A value with no time half is
|
|
140
157
|
* truncated to the unit's last *day* instead, so a full-date stays a
|
|
141
|
-
* full-date.
|
|
158
|
+
* full-date, and it refuses the same sub-day units `startOfParts` does.
|
|
142
159
|
* @param {object} parts - a parts record
|
|
143
160
|
* @param {string} unit - a {@link DATE_UNITS} member
|
|
144
161
|
* @returns {object} a new parts record
|
|
162
|
+
* @throws {TypeError} for an unknown unit, or a value missing a half
|
|
163
|
+
* the truncation reads
|
|
145
164
|
*/
|
|
146
165
|
export declare function endOfParts(parts: object, unit: string): object;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type Reader = (text: string, i: number, out: any) => number;
|
|
2
|
+
/**
|
|
3
|
+
* Compile an LDML date pattern into a strict parser.
|
|
4
|
+
*
|
|
5
|
+
* The returned function takes a string and returns a parts record in
|
|
6
|
+
* exactly the shape `parseRFC3339Parts` produces — so
|
|
7
|
+
* `epochOfRFC3339Parts`, `formatRFC3339Parts`, `addToParts` and every
|
|
8
|
+
* other calendar function accept it unchanged — or `null` when the text
|
|
9
|
+
* does not match. Failure is `null`, never a throw and never a partial
|
|
10
|
+
* record: a malformed *pattern* is the programmer's error and throws at
|
|
11
|
+
* compile time; malformed *text* is data and is reported as no match.
|
|
12
|
+
*
|
|
13
|
+
* Strict means three things:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Full consumption.** Trailing text fails; `yyyy-MM-dd` does not
|
|
16
|
+
* read `2026-01-02T03:04`.
|
|
17
|
+
* 2. **Impossible civil dates fail.** 31 February and hour 24 are not
|
|
18
|
+
* silently rolled forward into March and the next day.
|
|
19
|
+
* 3. **Fixed-width tokens are fixed width.** `MM` reads exactly two
|
|
20
|
+
* digits, `M` one or two, and neither reads three.
|
|
21
|
+
*
|
|
22
|
+
* The lexical family of the result follows the pattern: a pattern with
|
|
23
|
+
* only date tokens yields a full-date (`hours`/`minutes`/`seconds` are
|
|
24
|
+
* the contract's `-1`), one with only time tokens a full-time
|
|
25
|
+
* (`year`/`month`/`day` are `-1`), and one with both a date-time. A
|
|
26
|
+
* pattern with no field token at all is a compile error — it could only
|
|
27
|
+
* ever return the same empty record.
|
|
28
|
+
*
|
|
29
|
+
* Two tokens are not strict inverses of an arbitrary value and are
|
|
30
|
+
* documented rather than refused: `yy` reads 00–68 as 2000–2068 and
|
|
31
|
+
* 69–99 as 1969–1999 (the POSIX pivot moment uses), and `h`/`hh`
|
|
32
|
+
* without an `a` in the same pattern read as the morning, so 12 is
|
|
33
|
+
* midnight. Both round-trip their own spelling exactly.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} pattern - an LDML pattern, e.g. `'yyyy-MM-dd'`
|
|
36
|
+
* @param {import('./format.js').DateNames} [names] - locale names, required only if the
|
|
37
|
+
* pattern uses `MMM`/`MMMM`/`a`
|
|
38
|
+
* @returns {(text: string) => object | null} the compiled parser
|
|
39
|
+
* @throws {TypeError} on an unterminated quote, a derived token, a name
|
|
40
|
+
* token with no provider, or a pattern that reads no field
|
|
41
|
+
* @example
|
|
42
|
+
* const read = compileDateParser('dd-MM-yyyy');
|
|
43
|
+
* read('06-01-2014'); // { year: 2014, month: 1, day: 6, hours: -1, … }
|
|
44
|
+
* read('06-01-2014x'); // null — trailing input
|
|
45
|
+
* read('31-02-2014'); // null — February has no 31st
|
|
46
|
+
*/
|
|
47
|
+
export declare function compileDateParser(pattern: string, names?: import('./format.js').DateNames): (text: string) => object | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choose the calendar step closest to covering `span` in `count` ticks.
|
|
3
|
+
*
|
|
4
|
+
* A year is the coarsest unit a calendar has, so above one year the
|
|
5
|
+
* ladder continues as whole years on the same 1/2/5×10^k steps — 2, 5,
|
|
6
|
+
* 10, 50, 1000 years. Without that continuation a millennial domain
|
|
7
|
+
* asks for one tick per year and runs out of axis long before it runs
|
|
8
|
+
* out of domain.
|
|
9
|
+
*
|
|
10
|
+
* @param {number} span - Domain width in milliseconds
|
|
11
|
+
* @param {number} count - Desired tick count
|
|
12
|
+
* @returns {[string, number]} a [unit, amount] pair
|
|
13
|
+
*/
|
|
14
|
+
export declare function niceTimeStep(span: number, count: number): [string, number];
|
|
15
|
+
/**
|
|
16
|
+
* Time ticks on calendar boundaries: the first tick is the start of a
|
|
17
|
+
* `[unit, amount]` step at or after `min`, and each following one is a
|
|
18
|
+
* whole step later. A multi-unit step lands on a multiple of its own
|
|
19
|
+
* amount — months on 1, 4, 7, 10 and years on 1900, 1950, 2000 — so an
|
|
20
|
+
* axis reads as a calendar rather than as offsets from wherever the
|
|
21
|
+
* data happened to start.
|
|
22
|
+
*
|
|
23
|
+
* @param {number} min - Domain minimum, epoch milliseconds
|
|
24
|
+
* @param {number} max - Domain maximum, epoch milliseconds
|
|
25
|
+
* @param {number} [count] - Desired tick count (approximate)
|
|
26
|
+
* @returns {number[]} tick values in epoch milliseconds
|
|
27
|
+
*/
|
|
28
|
+
export declare function axisTicksTime(min: number, max: number, count?: number): number[];
|
|
29
|
+
/**
|
|
30
|
+
* Time ticks on a step the CALLER chose, rather than one this module
|
|
31
|
+
* picked for a target count: the first tick is the start of the
|
|
32
|
+
* `amount`-wide `unit` at or after `min`, and each following one is a
|
|
33
|
+
* whole step later. This is the boundary rule {@link axisTicksTime}
|
|
34
|
+
* uses once it has decided a step, exposed on its own because a
|
|
35
|
+
* consumer whose document declares its own interval — Mermaid's Gantt
|
|
36
|
+
* `tickInterval 1week` is the one in this repo — needs the same
|
|
37
|
+
* boundaries without the ladder choosing for it.
|
|
38
|
+
*
|
|
39
|
+
* A multi-unit step lands on a multiple of its own amount, so months
|
|
40
|
+
* fall on 1, 4, 7, 10 and years on 1900, 1950, 2000. A `week` step
|
|
41
|
+
* starts on Monday unless `weekStart` names another ISO weekday, which
|
|
42
|
+
* is what a document that says "weeks begin on Sunday" means.
|
|
43
|
+
*
|
|
44
|
+
* @param {number} min - Domain minimum, epoch milliseconds
|
|
45
|
+
* @param {number} max - Domain maximum, epoch milliseconds
|
|
46
|
+
* @param {string} unit - a `DATE_UNITS` member (`'day'`, `'week'`, …)
|
|
47
|
+
* @param {number} [amount] - whole steps per tick, at least 1
|
|
48
|
+
* @param {{ weekStart?: number, limit?: number }} [options] -
|
|
49
|
+
* `weekStart` is an ISO weekday, 1 (Monday) to 7 (Sunday);
|
|
50
|
+
* `limit` caps the tick count (default 1000)
|
|
51
|
+
* @returns {number[]} tick values in epoch milliseconds
|
|
52
|
+
* @example
|
|
53
|
+
* timeTicksEvery(Date.UTC(2024, 0, 3), Date.UTC(2024, 0, 20), 'week', 1);
|
|
54
|
+
* // the Mondays of 2024-01-08 and 2024-01-15
|
|
55
|
+
*/
|
|
56
|
+
export declare function timeTicksEvery(min: number, max: number, unit: string, amount?: number, options?: {
|
|
57
|
+
weekStart?: number;
|
|
58
|
+
limit?: number;
|
|
59
|
+
}): number[];
|
|
@@ -222,6 +222,19 @@ export declare function remap(v: number, smin: number, smax: number, dmin: numbe
|
|
|
222
222
|
* @returns {number}
|
|
223
223
|
*/
|
|
224
224
|
export declare function niceStep(span: number, count?: number): number;
|
|
225
|
+
/**
|
|
226
|
+
* Ticks on the {@link niceStep} ladder inside `[min, max]`: every whole
|
|
227
|
+
* multiple of the step the domain contains, rounded to the step's own
|
|
228
|
+
* precision so an axis never shows `0.30000000000000004`. A degenerate
|
|
229
|
+
* domain yields a single tick, a reversed one reads the same as its
|
|
230
|
+
* forward twin, and a non-finite bound yields nothing.
|
|
231
|
+
*
|
|
232
|
+
* @param {number} min - Domain minimum
|
|
233
|
+
* @param {number} max - Domain maximum
|
|
234
|
+
* @param {number} [count] - Desired tick count (approximate)
|
|
235
|
+
* @returns {number[]}
|
|
236
|
+
*/
|
|
237
|
+
export declare function axisTicksLinear(min: number, max: number, count?: number): number[];
|
|
225
238
|
/**
|
|
226
239
|
* Clamp a value into the unit interval `[0, 1]` — the fraction every
|
|
227
240
|
* unit-space geometry stage emits. `NaN` passes through as `NaN` rather
|
|
@@ -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[];
|