@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.
- package/ARCHITECTURE.md +122 -9
- package/README.md +50 -3
- package/dist/types/async.d.ts +39 -0
- 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/random.d.ts +80 -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/dist/types/stats.d.ts +73 -0
- package/docs/DATES.md +77 -1
- package/docs/GEO.md +2 -2
- package/docs/SERIES.md +342 -0
- package/package.json +21 -1
- package/src/async.js +78 -0
- 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/random.js +125 -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/stats.js +133 -0
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"]
|
|
@@ -151,6 +161,15 @@ flowchart TB
|
|
|
151
161
|
|
|
152
162
|
subgraph FunctionModule["Function Utilities"]
|
|
153
163
|
FunctionUtil["function.js<br/>trueThat, falseThat"]
|
|
164
|
+
AsyncUtil["async.js<br/>Bounded ordered asynchronous map"]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
subgraph RandomModule["Randomness"]
|
|
168
|
+
RandomUtil["random.js<br/>Seeded generator, integer draw, shuffle, distinct draw"]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
subgraph StatsModule["Statistics"]
|
|
172
|
+
StatsUtil["stats.js<br/>Mean, sample variance, median, named quantile"]
|
|
154
173
|
end
|
|
155
174
|
|
|
156
175
|
subgraph StringModules["String & Scanning"]
|
|
@@ -191,6 +210,7 @@ flowchart TB
|
|
|
191
210
|
DatesIndex --> DatesCivil
|
|
192
211
|
DatesIndex --> DatesFormat
|
|
193
212
|
DatesIndex --> DatesDuration
|
|
213
|
+
DatesIndex --> DatesTicks
|
|
194
214
|
|
|
195
215
|
GeoIndex --> GeoPred
|
|
196
216
|
GeoIndex --> GeoDist
|
|
@@ -462,7 +482,9 @@ intermediate and is plain data, never an opaque handle.
|
|
|
462
482
|
| `rfc3339.js` | validation, lexical decomposition, epoch conversion |
|
|
463
483
|
| `civil.js` | proleptic Gregorian arithmetic over integers |
|
|
464
484
|
| `format.js` | LDML pattern → compiled formatter |
|
|
485
|
+
| `parse.js` | LDML pattern → compiled strict parser (the formatter's inverse) |
|
|
465
486
|
| `duration.js` | ISO 8601 duration decomposition and date arithmetic |
|
|
487
|
+
| `ticks.js` | the time-axis step ladder and its calendar boundaries |
|
|
466
488
|
|
|
467
489
|
Calendar math goes through day numbers (`daysFromCivil`/`civilFromDays`),
|
|
468
490
|
never through `Date`: the conversions are ~10 integer operations and allocate
|
|
@@ -475,6 +497,23 @@ allocates nothing per value. Formatting is the two-stage compiler again — a
|
|
|
475
497
|
pattern is scanned once into a chain of appenders, which measured ~4.5×
|
|
476
498
|
against re-scanning it per call.
|
|
477
499
|
|
|
500
|
+
Parsing is the same compiler run backwards. `compileDateParser` scans a
|
|
501
|
+
pattern once into a chain of readers and returns a function from text to the
|
|
502
|
+
same parts record `parseRFC3339Parts` produces, so every calendar function
|
|
503
|
+
takes its output unchanged. It is strict in three ways that a permissive
|
|
504
|
+
reader is not: trailing input fails, an impossible civil date (31 February,
|
|
505
|
+
hour 24) fails rather than rolling forward into the next month, and a
|
|
506
|
+
fixed-width token reads exactly its width. Failure is `null` — malformed
|
|
507
|
+
*text* is data — while a malformed *pattern* throws at compile time. The
|
|
508
|
+
tokens that only ever format, because they are derived from a date rather
|
|
509
|
+
than fields of one (`EEEE`, `DDD`, `ww`, `Q`), are compile errors with a
|
|
510
|
+
message saying which and why, rather than tokens that quietly read nothing.
|
|
511
|
+
A consumer whose patterns are not LDML — Mermaid's Gantt `dateFormat` is
|
|
512
|
+
moment's grammar and its `axisFormat` is strftime — adapts its own tokens
|
|
513
|
+
onto this vocabulary rather than handing its spelling to this compiler:
|
|
514
|
+
LDML's `YYYY` is the week-numbering year, so `YYYY-MM-DD` read as LDML
|
|
515
|
+
answers 2019 for 2018-12-31.
|
|
516
|
+
|
|
478
517
|
Locale-dependent presentation (month and weekday names, meridiem, relative
|
|
479
518
|
phrasing) is deliberately **not** here, so this package stays zero-dependency
|
|
480
519
|
and free of data that drifts per language: `compileDateFormat` takes a names
|
|
@@ -532,11 +571,31 @@ import {
|
|
|
532
571
|
const parts = parseRFC3339Parts('2026-01-31');
|
|
533
572
|
addToParts(parts, 1, 'month'); // 2026-02-28 — month math clamps
|
|
534
573
|
startOfParts(parts, 'week'); // the Monday of that week (ISO 8601)
|
|
574
|
+
addToParts(parts, 3, 'hour'); // TypeError — a full-date has no clock
|
|
535
575
|
|
|
536
576
|
const fmt = compileDateFormat("yyyy-'W'ww"); // compile once…
|
|
537
577
|
fmt(parts); // …call many: '2026-W05'
|
|
578
|
+
|
|
579
|
+
const read = compileDateParser('dd-MM-yyyy');
|
|
580
|
+
read('06-01-2014'); // { year: 2014, month: 1, day: 6, … }
|
|
581
|
+
read('06-01-2014x'); // null — trailing input
|
|
582
|
+
read('31-02-2014'); // null — February has no 31st
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**Ticks a caller chose, or ticks this module chose:**
|
|
586
|
+
|
|
587
|
+
```javascript
|
|
588
|
+
import { axisTicksTime, timeTicksEvery } from '@jarenjs/core/dates';
|
|
589
|
+
|
|
590
|
+
axisTicksTime(from, to, 4); // the ladder picks the step
|
|
591
|
+
timeTicksEvery(from, to, 'week', 1, { weekStart: 7 }); // Sundays
|
|
538
592
|
```
|
|
539
593
|
|
|
594
|
+
`axisTicksTime` is the chart axis's planner; `timeTicksEvery` is the same
|
|
595
|
+
boundary rule with the step supplied, which is what a document declaring its
|
|
596
|
+
own interval (Mermaid's `tickInterval 1week`) needs. One ladder, so a chart
|
|
597
|
+
and a timeline cannot disagree about where a tick falls.
|
|
598
|
+
|
|
540
599
|
### 5b. Geo Module (`geo/`)
|
|
541
600
|
|
|
542
601
|
The spatial kernel. As with dates, **there is no geometry type**: the
|
|
@@ -578,9 +637,9 @@ direction is *not* claimed, because WKT whitespace, the `M` measure and number
|
|
|
578
637
|
spelling are normalized on the way through.
|
|
579
638
|
|
|
580
639
|
Against [`wellknown`](https://www.npmjs.com/package/wellknown), the established
|
|
581
|
-
WKT↔GeoJSON converter (`npm run benchmark:geo`, Node <!--
|
|
640
|
+
WKT↔GeoJSON converter (`npm run benchmark:geo`, Node <!--fact:geo.node-->v24.19.0<!--/fact-->):
|
|
582
641
|
|
|
583
|
-
<!--
|
|
642
|
+
<!--fact:geo.wktTable-->
|
|
584
643
|
| scenario | Jaren | rival | ratio |
|
|
585
644
|
|---|---|---|---|
|
|
586
645
|
| wkt parse (POINT) | 431.1 ns | 1.31 µs (wellknown) | **3.1×** |
|
|
@@ -588,7 +647,7 @@ WKT↔GeoJSON converter (`npm run benchmark:geo`, Node <!--bm:geo.node-->v24.19.
|
|
|
588
647
|
| wkt stringify (2000-vertex polygon) | 163.40 µs | 229.70 µs (wellknown) | **1.4×** |
|
|
589
648
|
| wkt validate (POINT) | 242.7 ns | 1.28 µs (wellknown) | **5.3×** |
|
|
590
649
|
| wkt validate (2000-vertex polygon) | 496.00 µs | 1.90 ms (wellknown) | **3.8×** |
|
|
591
|
-
<!--/
|
|
650
|
+
<!--/fact-->
|
|
592
651
|
|
|
593
652
|
The last two rows are the ones the sink exists for: `wellknown` has no
|
|
594
653
|
predicate, so validating means parsing and throwing the geometry away. And the
|
|
@@ -627,7 +686,7 @@ RFC 7946 removed coordinate-reference-system support and mandates WGS 84, so
|
|
|
627
686
|
there is no SRID table and no reprojection: conformance removes the need rather
|
|
628
687
|
than an omission hiding it.
|
|
629
688
|
|
|
630
|
-
**Measured against the field** (`npm run benchmark:geo`, Node <!--
|
|
689
|
+
**Measured against the field** (`npm run benchmark:geo`, Node <!--fact:geo.node-->v24.19.0<!--/fact-->). Every
|
|
631
690
|
scenario asserts result equivalence before any timing, and the harness refuses
|
|
632
691
|
to print a table if the engines disagree: distance, area, length and bounding
|
|
633
692
|
box come out *bit-identical* to Turf, containment agrees on a 400-point sweep,
|
|
@@ -635,7 +694,7 @@ and the index returns exactly Flatbush's answer on 200 queries. Ratios are the
|
|
|
635
694
|
rival's time over this kernel's, so above 1 means Jaren is faster; the rows
|
|
636
695
|
below derive from the committed measurement and are refreshed with it.
|
|
637
696
|
|
|
638
|
-
<!--
|
|
697
|
+
<!--fact:geo.table-->
|
|
639
698
|
| scenario | Jaren | rival | ratio |
|
|
640
699
|
|---|---|---|---|
|
|
641
700
|
| distance (two positions) | 28.3 ns | 111.0 ns (turf) | **3.9×** |
|
|
@@ -650,11 +709,11 @@ below derive from the committed measurement and are refreshed with it.
|
|
|
650
709
|
| index build (100k boxes) | 12.70 ms | 10.62 ms (flatbush) | 0.8× |
|
|
651
710
|
| index probe (100k boxes) | 602.3 ns | 668.9 ns (flatbush) | **1.1×** |
|
|
652
711
|
| linear scan (100k boxes, no index) | 141.50 µs | — | — |
|
|
653
|
-
<!--/
|
|
712
|
+
<!--/fact-->
|
|
654
713
|
|
|
655
|
-
The losses are kept, and named by the same measurement: on the committed run <!--
|
|
714
|
+
The losses are kept, and named by the same measurement: on the committed run <!--fact:geo.losses-->three rows lose to a rival: point in polygon (2000-vertex) at 0.5× (turf), bounding box (2000-vertex) at 0.9× (turf), index build (100k boxes) at 0.8× (flatbush)<!--/fact-->.
|
|
656
715
|
**Point-in-polygon on a large ring runs at about half Turf's speed**
|
|
657
|
-
(<!--
|
|
716
|
+
(<!--fact:geo.pip2000-->0.5×<!--/fact--> at 2000 vertices) because every edge that could
|
|
658
717
|
matter goes through the exact orientation predicate, where Turf uses naive
|
|
659
718
|
floating-point arithmetic. That is the trade this module exists to make — it
|
|
660
719
|
is the difference between a containment test that is right on near-collinear
|
|
@@ -663,7 +722,7 @@ predicate on edges that cannot affect the answer, which took this from 0.2×
|
|
|
663
722
|
to about 0.5×; the rest is the predicate itself, and the campaign that made
|
|
664
723
|
geography reachable across the suite never traded it away. The bounding box
|
|
665
724
|
of the same ring and the index build against Flatbush sit within a few tenths
|
|
666
|
-
of level (<!--
|
|
725
|
+
of level (<!--fact:geo.bbox2000-->0.9×<!--/fact--> and <!--fact:geo.indexBuild-->0.8×<!--/fact-->) and move
|
|
667
726
|
between runs and Node versions; they are published as measured rather than
|
|
668
727
|
rounded to a win.
|
|
669
728
|
|
|
@@ -721,6 +780,60 @@ their reason to exist; they guarantee a finite answer. The fixed-arity vector
|
|
|
721
780
|
classes in `math/` (`Vec2f64`, `Vec3f64`) are 2D/3D geometry with a
|
|
722
781
|
different convention and a different job, and they stay where they are.
|
|
723
782
|
|
|
783
|
+
### 5d. Series Module (`series/`)
|
|
784
|
+
|
|
785
|
+
The temporal kernel: one meaning for an instant, one for a sorted series of
|
|
786
|
+
readings, one for an interval, and the set algebra over them. As with dates,
|
|
787
|
+
**there is no type** — an instant is epoch milliseconds or an RFC 3339 string,
|
|
788
|
+
a sample is `{ at, value }`, an interval is `{ start, end }` — and, as with
|
|
789
|
+
dates, **there is no `now`**: every bound is data, so every answer here is
|
|
790
|
+
reproducible and cacheable.
|
|
791
|
+
|
|
792
|
+
| File | Owns |
|
|
793
|
+
|---|---|
|
|
794
|
+
| `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 |
|
|
795
|
+
| `normalize.js` | `toEpoch`, `normalizeSeries`, `normalizeIntervals` and the `lowerBoundTime`/`upperBoundTime` cuts a sorted array is read through |
|
|
796
|
+
| `interval.js` | `containsInstant`, `overlapsInterval`, `intersectInterval`, `mergeIntervals`, `subtractIntervals`, `gapsWithin`, `coverageOf`, `findSlots` |
|
|
797
|
+
| `interval-index.js` | `createIntervalIndex` — build once, query many, by point or by range |
|
|
798
|
+
|
|
799
|
+
Four decisions carry it. **Half-open `[start, end)`**: an interval holds its
|
|
800
|
+
start and not its end, so touching intervals neither overlap nor double-count
|
|
801
|
+
and a boundary instant belongs to exactly one of them — while `mergeIntervals`
|
|
802
|
+
*joins* touching spans by default, because availability means continuous cover,
|
|
803
|
+
with `{ adjacent: false }` as the explicit other answer. **Nothing is dropped**:
|
|
804
|
+
a member that names no instant, or an interval that is empty, reversed or
|
|
805
|
+
non-finite, is a refusal naming the row rather than a silently shorter result,
|
|
806
|
+
and the sort is stable so duplicate instants keep their order and both count.
|
|
807
|
+
**No clock**: `gapsWithin` and `coverageOf` derive their window from the input's
|
|
808
|
+
own hull when given none, because the only other default would be "now".
|
|
809
|
+
|
|
810
|
+
And **the index cuts on both ends**. Sorting by start alone is the bug: a span
|
|
811
|
+
that began long before a query sits far to the left of the query's own
|
|
812
|
+
neighbourhood and still overlaps it, so a binary search around the query loses
|
|
813
|
+
it while looking plausible. `createIntervalIndex` therefore carries a prefix
|
|
814
|
+
maximum end beside the starts — non-decreasing by construction, hence
|
|
815
|
+
binary-searchable — and the first position where it passes the query's start is
|
|
816
|
+
the first position where anything can still be live. A query is two binary cuts
|
|
817
|
+
and a walk between them, O(log n + k), returning the caller's own rows in a
|
|
818
|
+
fresh array. Like the packed-Hilbert box index it is **static**: bounds are
|
|
819
|
+
copied into flat typed arrays at build time, so a query reads no source object
|
|
820
|
+
and a row mutated afterwards cannot change what the index answers.
|
|
821
|
+
|
|
822
|
+
Buckets, resampling, rolling windows and as-of joins are the layer above this
|
|
823
|
+
one; named zones, recurrence grammars and scheduling solvers are outside it.
|
|
824
|
+
`findSlots` enumerates where a fixed-width span fits — choosing among the
|
|
825
|
+
answers is a solver's job, deliberately not this kernel's.
|
|
826
|
+
|
|
827
|
+
```javascript
|
|
828
|
+
import { createIntervalIndex, mergeIntervals, findSlots } from '@jarenjs/core/series';
|
|
829
|
+
|
|
830
|
+
const shifts = [{ start: 0, end: 4 * 3600_000 }, { start: 4 * 3600_000, end: 8 * 3600_000 }];
|
|
831
|
+
mergeIntervals(shifts); // one span — touching IS continuous cover
|
|
832
|
+
mergeIntervals(shifts, { adjacent: false }); // two — a handover is two shifts
|
|
833
|
+
findSlots(shifts, { duration: 'PT30M' }).length; // 16
|
|
834
|
+
createIntervalIndex(shifts).at(4 * 3600_000); // the second shift only: [start, end)
|
|
835
|
+
```
|
|
836
|
+
|
|
724
837
|
### 6. Text Module (`text/`)
|
|
725
838
|
|
|
726
839
|
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
|
|
|
@@ -13,6 +13,9 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
|
|
|
13
13
|
| `@jarenjs/core/object` | deep equality (`equalsDeep`, JSON-only `equalsJson`), the `isJsonObject` and deep `isJsonValue` predicates, `__proto__`-safe `setObjectMember`, `deepFreeze`, map/set merging |
|
|
14
14
|
| `@jarenjs/core/string` | Unicode string helpers (`countCodePoints`, `compareCodePoints`, ...), cached regex compilation, the suite's one content hash (`fnv1a` and the `hashContent` fingerprint over it) and `kebabCase` |
|
|
15
15
|
| `@jarenjs/core/cache` | the bounded LRU (`createBoundedCache`), the reference-keyed `createWeakCache`, and `createSemanticCache` — keyed by what a value IS, for caches whose entries decide a result |
|
|
16
|
+
| `@jarenjs/core/random` | the suite's one seeded generator (`mulberry32`, pinned sequence, ToUint32 seed) and the draws built on it: `randomInt` over a half-open range, in-place Fisher–Yates `shuffle`, and `drawDistinct` — `k` distinct indices from one stream |
|
|
17
|
+
| `@jarenjs/core/stats` | descriptive statistics over a sample: `mean`, sample `variance`/`stddev`, the midpoint `median`, and `quantile(values, p, { method })` — `p` on 0..1 under a NAMED rule, `'nearest-rank'` or `'linear'`, because a default would decide silently; an empty sample answers `undefined`, never `0` |
|
|
18
|
+
| `@jarenjs/core/async` | `mapConcurrent(items, limit, worker, { signal })` — the bounded ordered asynchronous map: never more than `limit` workers in flight, results in input order, a rejection or an abort stops dispatch and drains the lanes before the map rejects, so nothing is still running when it settles |
|
|
16
19
|
| `@jarenjs/core/chunk` | cutting a value down to size: `sizeOf` (the suite's one size rule — a string is its length, anything else its JSON), `excerpt`, `truncate`, and `chunkText` by size, line or separator with offsets that locate a piece in its source |
|
|
17
20
|
| `@jarenjs/core/scan` | char-code constants and predicates for recursive-descent parsers |
|
|
18
21
|
| `@jarenjs/core/message` | the message template/catalog compiler shared by the validator and the form layer |
|
|
@@ -24,6 +27,7 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
|
|
|
24
27
|
| `@jarenjs/core/dates` | RFC 3339 / ISO 8601 validation, plus the calendar kernel: integer date arithmetic, compiled formatting, durations |
|
|
25
28
|
| `@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
29
|
| `@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`) |
|
|
30
|
+
| `@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
31
|
| `@jarenjs/core/text` | text validators: emails, hostnames, IPs, URIs/IRIs, UUIDs, punycode, ... |
|
|
28
32
|
| `@jarenjs/core/math` | int32/float64 math and 2D/3D vector classes; the linear `remap` and unit-interval `clamp01` |
|
|
29
33
|
| `@jarenjs/core/finance` | zero-dependency finance/trading formulas: TVM, cash flow, amortization, interest, depreciation, bonds, technical indicators, returns/risk |
|
|
@@ -65,7 +69,7 @@ Grouped by file: `email` (RFC 5321 + internationalized addresses), `host` (hostn
|
|
|
65
69
|
|
|
66
70
|
## Dates, numbers and math
|
|
67
71
|
|
|
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
|
|
72
|
+
- `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
73
|
- `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
74
|
- `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
75
|
|
|
@@ -97,7 +101,7 @@ Four design decisions carry the module:
|
|
|
97
101
|
- **Orientation is computed exactly** (Shewchuk's adaptive predicates): a naive floating-point determinant returns the *wrong sign* on near-collinear input, which makes containment contradict itself. Every ring winding and point-in-polygon answer rests on this sign, and the deliberate cost is on the [benchmark page](https://jklarenbeek.github.io/jarenjs/#/benchmarks?suite=geo).
|
|
98
102
|
- **Measurement is spherical, drawing is projected, and the two never mix.** A Euclidean norm on raw degrees is 64% wrong over 1 km at Dutch latitudes, so `haversineDistance`/`sphericalRingArea` work on the sphere (`equirectDistance` is the cheap screening form for rejecting candidates first), while `projectMercator`/`fitMercator` and `simplifyLine`/`simplifyRing` exist for renderers — never measure on a projected coordinate.
|
|
99
103
|
- **Validity is a separate concern from traversal, and a box is refused rather than made too small.** `eachPosition`, `bboxOf`, `centroidOf` and friends measure without judging (a ring is read as closed whether or not it is); a box over any non-finite coordinate is `null`, never a plausible box that misses its input; and boxes never cross the antimeridian — cut the geometry at ±180° as RFC 7946 §3.1.9 asks, and every containment test and index probe is right. `isValidGeoJson` (structure plus the ring closure a JSON Schema provably cannot express), `isValidWkt` and `isValidGeohash` are the one-call judgments that back the `geoFormats` group in [`@jarenjs/formats`](../formats), next to the full GeoJSON meta-schema artifacts in [`@jarenjs/json`](../json).
|
|
100
|
-
- **WKT is one grammar walk with two entry points.** `isValidWkt` and `wktToGeoJson` run the *same* scan, parameterized by a sink that is absent for the predicate and present for the parser — so the `wkt` format tester (which runs per value in the validator and per keystroke in the form layer) allocates nothing, and the two cannot drift apart. A committed corpus asserts `isValidWkt(s) === (wktToGeoJson(s) !== null)` for all 181 entries, malformed half included. `geoJsonToWkt` writes the string back, and `wktToGeoJson(geoJsonToWkt(g))` returns `g`; the text direction is *not* claimed, because whitespace, the `M` measure and number spelling are normalized. Against [`wellknown`](https://www.npmjs.com/package/wellknown) the parse is <!--
|
|
104
|
+
- **WKT is one grammar walk with two entry points.** `isValidWkt` and `wktToGeoJson` run the *same* scan, parameterized by a sink that is absent for the predicate and present for the parser — so the `wkt` format tester (which runs per value in the validator and per keystroke in the form layer) allocates nothing, and the two cannot drift apart. A committed corpus asserts `isValidWkt(s) === (wktToGeoJson(s) !== null)` for all 181 entries, malformed half included. `geoJsonToWkt` writes the string back, and `wktToGeoJson(geoJsonToWkt(g))` returns `g`; the text direction is *not* claimed, because whitespace, the `M` measure and number spelling are normalized. Against [`wellknown`](https://www.npmjs.com/package/wellknown) the parse is <!--fact:geo.wktParsePoint-->3.1×<!--/fact--> faster on a `POINT` and the yes-or-no answer <!--fact:geo.wktValidatePoint-->5.3×<!--/fact--> — and wellknown *validates less* while being slower, which is the honest framing; the table and every language difference are in [ARCHITECTURE](./ARCHITECTURE.md). The kernel's own losses are published there too, derived from the same run: <!--fact:geo.losses-->three rows lose to a rival: point in polygon (2000-vertex) at 0.5× (turf), bounding box (2000-vertex) at 0.9× (turf), index build (100k boxes) at 0.8× (flatbush)<!--/fact-->.
|
|
101
105
|
|
|
102
106
|
The spatial query operators (`$distance`, `$within`, `$geohash`, spatial joins over the box index) live in the query engine in [`@jarenjs/json`](../json); the streaming map chart that draws a FeatureCollection with bounded memory lives in [`@jarenjs/charts`](../../components/charts). The full module reference is [docs/GEO.md](./docs/GEO.md).
|
|
103
107
|
|
|
@@ -123,6 +127,49 @@ Three rules, kept by every function so that no caller has to check them again:
|
|
|
123
127
|
|
|
124
128
|
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
129
|
|
|
130
|
+
## Intervals and series
|
|
131
|
+
|
|
132
|
+
`@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.
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
import { createIntervalIndex, mergeIntervals, gapsWithin, findSlots } from '@jarenjs/core/series';
|
|
136
|
+
|
|
137
|
+
const shifts = [
|
|
138
|
+
{ from: '2026-03-02T09:00:00Z', to: '2026-03-02T13:00:00Z', who: 'ada' },
|
|
139
|
+
{ from: '2026-03-02T13:00:00Z', to: '2026-03-02T17:00:00Z', who: 'grace' },
|
|
140
|
+
];
|
|
141
|
+
const index = createIntervalIndex(shifts, { start: 'from', end: 'to' });
|
|
142
|
+
index.at('2026-03-02T13:00:00Z'); // [grace] — half-open, so the handover belongs to one shift
|
|
143
|
+
|
|
144
|
+
const cover = shifts.map((s) => ({ start: s.from, end: s.to }));
|
|
145
|
+
mergeIntervals(cover); // one span, 09:00–17:00 — touching IS continuous cover
|
|
146
|
+
findSlots(cover, { duration: 'PT30M' }); // sixteen half-hour slots, one straddling the handover
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
import { resampleSeries, rollingSeries, asOfJoin, downsampleSeries } from '@jarenjs/core/series';
|
|
151
|
+
|
|
152
|
+
resampleSeries(readings, { every: 'PT1H', aggregate: 'mean', fill: 'linear' });
|
|
153
|
+
resampleSeries(readings, { every: 'P1M', zone: 'Europe/Amsterdam', provider });
|
|
154
|
+
rollingSeries(readings, { width: 'PT5M', aggregate: 'max', minPeriods: 3 });
|
|
155
|
+
asOfJoin(trades, quotes, { direction: 'nearest', tolerance: 'PT1S', key: 'symbol' });
|
|
156
|
+
downsampleSeries(readings, { target: 2000 }); // → { points, sourceCount, renderedCount, method }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Seven decisions carry the module:
|
|
160
|
+
|
|
161
|
+
- **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.
|
|
162
|
+
- **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.
|
|
163
|
+
- **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.
|
|
164
|
+
- **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.
|
|
165
|
+
|
|
166
|
+
- **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.
|
|
167
|
+
- **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.
|
|
168
|
+
- **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.
|
|
169
|
+
- **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.
|
|
170
|
+
|
|
171
|
+
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).
|
|
172
|
+
|
|
126
173
|
## Messages
|
|
127
174
|
|
|
128
175
|
`@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):
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The bounded ordered asynchronous map: run a worker over a list
|
|
3
|
+
* with never more than `limit` calls in flight, and answer the results
|
|
4
|
+
* in the list's order. Before this file the same twelve lines lived in
|
|
5
|
+
* the AI package's program runner and in the benchmark harness, and a
|
|
6
|
+
* downstream consumer had written them a third time; a pool that exists
|
|
7
|
+
* once is one whose edge behavior can be pinned once.
|
|
8
|
+
*
|
|
9
|
+
* The contract, in full:
|
|
10
|
+
*
|
|
11
|
+
* - results are in INPUT order, whatever order the workers finish in;
|
|
12
|
+
* - never more than `limit` workers are in flight; `limit` must be a
|
|
13
|
+
* number of at least 1 (`Infinity` is allowed and means unbounded) —
|
|
14
|
+
* anything else is a `TypeError`, never a silent clamp, because a
|
|
15
|
+
* limit of 0 is a bug in the caller and "sequential" is spelled 1;
|
|
16
|
+
* - a worker rejection stops dispatch: no item starts after it, the
|
|
17
|
+
* workers already in flight are awaited, and only then does the map
|
|
18
|
+
* reject with that first rejection. A worker that throws
|
|
19
|
+
* synchronously is a rejection;
|
|
20
|
+
* - an abort does the same, rejecting with the signal's reason; a signal
|
|
21
|
+
* that is already aborted rejects before any worker runs;
|
|
22
|
+
* - so when the returned promise settles, NO worker is still running —
|
|
23
|
+
* the caller can close whatever the workers were using.
|
|
24
|
+
*
|
|
25
|
+
* The map does not retry, rate-limit, delay per origin or know anything
|
|
26
|
+
* about what the worker does; those are the caller's policies around it.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* @template T, R
|
|
30
|
+
* @param {readonly T[]} items
|
|
31
|
+
* @param {number} limit - workers in flight at once; a number >= 1, `Infinity` for unbounded
|
|
32
|
+
* @param {(item: T, index: number) => Promise<R> | R} worker
|
|
33
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
34
|
+
* @returns {Promise<R[]>} the results, in input order
|
|
35
|
+
* @throws {TypeError} (as a rejection) when `limit` is not a number >= 1
|
|
36
|
+
*/
|
|
37
|
+
export declare function mapConcurrent<T, R>(items: readonly T[], limit: number, worker: (item: T, index: number) => Promise<R> | R, options?: {
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}): Promise<R[]>;
|
|
@@ -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
|