@jarenjs/core 0.9.2 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/ARCHITECTURE.md +229 -26
  2. package/README.md +64 -10
  3. package/dist/types/array.d.ts +13 -0
  4. package/dist/types/cache.d.ts +166 -0
  5. package/dist/types/chunk.d.ts +102 -0
  6. package/dist/types/color.d.ts +64 -0
  7. package/dist/types/convert/convert.d.ts +33 -0
  8. package/dist/types/convert/currency.d.ts +47 -0
  9. package/dist/types/convert/index.d.ts +9 -0
  10. package/dist/types/convert/registry.d.ts +55 -0
  11. package/dist/types/dates/civil.d.ts +146 -0
  12. package/dist/types/dates/duration.d.ts +83 -0
  13. package/dist/types/dates/format.d.ts +59 -0
  14. package/dist/types/dates/index.d.ts +4 -0
  15. package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
  16. package/dist/types/errors.d.ts +77 -0
  17. package/dist/types/finance/amortization.d.ts +51 -0
  18. package/dist/types/finance/bond.d.ts +45 -0
  19. package/dist/types/finance/cashflow.d.ts +44 -0
  20. package/dist/types/finance/depreciation.d.ts +26 -0
  21. package/dist/types/finance/index.d.ts +16 -0
  22. package/dist/types/finance/indicators.d.ts +76 -0
  23. package/dist/types/finance/interest.d.ts +44 -0
  24. package/dist/types/finance/returns.d.ts +43 -0
  25. package/dist/types/finance/tvm.d.ts +50 -0
  26. package/dist/types/geo/angle.d.ts +4 -0
  27. package/dist/types/geo/bbox.d.ts +32 -0
  28. package/dist/types/geo/distance.d.ts +67 -0
  29. package/dist/types/geo/geohash.d.ts +63 -0
  30. package/dist/types/geo/geojson.d.ts +108 -0
  31. package/dist/types/geo/index-tree.d.ts +52 -0
  32. package/dist/types/geo/index.d.ts +11 -0
  33. package/dist/types/geo/mercator.d.ts +57 -0
  34. package/dist/types/geo/predicates.d.ts +44 -0
  35. package/dist/types/geo/ring.d.ts +80 -0
  36. package/dist/types/geo/simplify.d.ts +31 -0
  37. package/dist/types/geo/valid.d.ts +24 -0
  38. package/dist/types/geo/wkt.d.ts +16 -0
  39. package/dist/types/math/float64.d.ts +126 -3
  40. package/dist/types/math/format.d.ts +58 -0
  41. package/dist/types/math/index.d.ts +5 -0
  42. package/dist/types/math/mat4.d.ts +60 -0
  43. package/dist/types/math/project.d.ts +54 -0
  44. package/dist/types/math/solve.d.ts +67 -0
  45. package/dist/types/math/word.d.ts +82 -0
  46. package/dist/types/message.d.ts +52 -0
  47. package/dist/types/object.d.ts +151 -0
  48. package/dist/types/scan.d.ts +30 -0
  49. package/dist/types/schema.d.ts +24 -0
  50. package/dist/types/string.d.ts +96 -0
  51. package/dist/types/text/base64.d.ts +6 -4
  52. package/dist/types/text/email.d.ts +0 -1
  53. package/dist/types/text/host.d.ts +39 -8
  54. package/dist/types/text/i18n.d.ts +13 -1
  55. package/dist/types/text/index.d.ts +1 -0
  56. package/dist/types/text/misc.d.ts +15 -1
  57. package/dist/types/text/punycode.d.ts +45 -85
  58. package/docs/CONVERT.md +45 -0
  59. package/docs/DATES.md +78 -0
  60. package/docs/FINANCE.md +59 -0
  61. package/docs/GEO.md +114 -0
  62. package/docs/MATH.md +75 -0
  63. package/package.json +57 -4
  64. package/src/array.js +16 -0
  65. package/src/cache.js +206 -0
  66. package/src/chunk.js +159 -0
  67. package/src/color.js +125 -0
  68. package/src/convert/convert.js +59 -0
  69. package/src/convert/currency.js +74 -0
  70. package/src/convert/index.js +11 -0
  71. package/src/convert/registry.js +213 -0
  72. package/src/dates/civil.js +360 -0
  73. package/src/dates/duration.js +225 -0
  74. package/src/dates/format.js +238 -0
  75. package/src/dates/index.js +30 -0
  76. package/src/dates/rfc3339.js +621 -0
  77. package/src/errors.js +102 -0
  78. package/src/finance/amortization.js +50 -0
  79. package/src/finance/bond.js +100 -0
  80. package/src/finance/cashflow.js +122 -0
  81. package/src/finance/depreciation.js +54 -0
  82. package/src/finance/index.js +27 -0
  83. package/src/finance/indicators.js +207 -0
  84. package/src/finance/interest.js +63 -0
  85. package/src/finance/returns.js +90 -0
  86. package/src/finance/tvm.js +94 -0
  87. package/src/function.js +0 -2
  88. package/src/geo/angle.js +12 -0
  89. package/src/geo/bbox.js +80 -0
  90. package/src/geo/distance.js +155 -0
  91. package/src/geo/geohash.js +224 -0
  92. package/src/geo/geojson.js +361 -0
  93. package/src/geo/index-tree.js +294 -0
  94. package/src/geo/index.js +52 -0
  95. package/src/geo/mercator.js +124 -0
  96. package/src/geo/predicates.js +313 -0
  97. package/src/geo/ring.js +182 -0
  98. package/src/geo/simplify.js +130 -0
  99. package/src/geo/valid.js +142 -0
  100. package/src/geo/wkt.js +262 -0
  101. package/src/math/float64.js +228 -11
  102. package/src/math/format.js +157 -0
  103. package/src/math/index.js +5 -0
  104. package/src/math/mat4.js +131 -0
  105. package/src/math/project.js +49 -0
  106. package/src/math/solve.js +112 -0
  107. package/src/math/word.js +177 -0
  108. package/src/message.js +119 -0
  109. package/src/object.js +329 -0
  110. package/src/scan.js +42 -0
  111. package/src/schema.js +37 -0
  112. package/src/string.js +189 -0
  113. package/src/text/base64.js +15 -52
  114. package/src/text/email.js +0 -5
  115. package/src/text/host.js +515 -140
  116. package/src/text/i18n.js +13 -6
  117. package/src/text/identifiers.js +1 -1
  118. package/src/text/index.js +1 -0
  119. package/src/text/misc.js +81 -5
  120. package/src/text/punycode.js +235 -323
  121. package/src/dates.js +0 -371
package/ARCHITECTURE.md CHANGED
@@ -95,38 +95,70 @@ flowchart TB
95
95
  TextIndex["text/index.js"]
96
96
  Basic["basic.js<br/>Alpha, numeric, hex"]
97
97
  Email["email.js<br/>Email validation"]
98
- Host["host.js<br/>URL, IP, hostname"]
98
+ Host["host.js<br/>URL, IP, hostname, URI/IRI"]
99
99
  Identifiers["identifiers.js<br/>UUID, GUID"]
100
100
  Base64["base64.js<br/>Base64 validation"]
101
- Punycode["punycode.js<br/>IDN encoding"]
101
+ Punycode["punycode.js<br/>Punycode codec + IDNA"]
102
102
  I18n["i18n.js<br/>Unicode category checks"]
103
+ IRegexp["iregexp.js<br/>I-Regexp (RFC 9485)"]
104
+ TextMisc["misc.js<br/>Assorted text testers"]
103
105
  end
104
106
 
105
107
  subgraph DateModule["Date Processing"]
106
- Dates["dates.js<br/>RFC 3339 / ISO 8601"]
108
+ DatesIndex["dates/index.js"]
109
+ DatesRfc["rfc3339.js<br/>RFC 3339 / ISO 8601"]
110
+ DatesCivil["civil.js<br/>Gregorian day-number math"]
111
+ DatesFormat["format.js<br/>LDML pattern compiler"]
112
+ DatesDuration["duration.js<br/>ISO 8601 durations"]
113
+ end
114
+
115
+ subgraph GeoModule["Spatial"]
116
+ GeoIndex["geo/index.js"]
117
+ GeoPred["predicates.js<br/>Robust orientation"]
118
+ GeoDist["distance.js<br/>Great-circle measurement"]
119
+ GeoRing["ring.js<br/>Closure, winding, area, containment"]
120
+ GeoBbox["bbox.js<br/>Bounding boxes"]
121
+ GeoHash["geohash.js<br/>Base-32 cells"]
122
+ GeoJson["geojson.js<br/>Typed-geometry traversal"]
123
+ GeoValid["valid.js + wkt.js<br/>Validity testers"]
124
+ GeoTree["index-tree.js<br/>Packed-Hilbert box index"]
125
+ GeoMerc["mercator.js + simplify.js<br/>Projection out, simplification"]
107
126
  end
108
127
 
109
128
  subgraph MathModules["Mathematics"]
110
129
  MathIndex["math/index.js"]
111
130
  Int32Math["int32.js<br/>Fixed-point math"]
112
- Float64Math["float64.js<br/>Float64 utilities"]
131
+ Float64Math["float64.js<br/>Float64 utilities, remap, clamp01"]
113
132
  Vec2I32["vec2i32.js<br/>2D integer vectors"]
114
133
  Vec2F64["vec2f64.js<br/>2D float vectors"]
115
134
  Vec3F64["vec3f64.js<br/>3D float vectors"]
135
+ Mat4Math["mat4.js<br/>4x4 matrices"]
136
+ ProjectMath["project.js<br/>3D to 2D projection"]
137
+ SolveMath["solve.js<br/>Numeric root finders"]
138
+ WordMath["word.js<br/>BigInt fixed-width words"]
139
+ FormatMath["format.js<br/>Number format/parse"]
140
+ end
141
+
142
+ subgraph DomainModules["Domain Kernels"]
143
+ ConvertIndex["convert/index.js<br/>Unit and currency conversion"]
144
+ FinanceIndex["finance/index.js<br/>TVM, bonds, cash flow, returns"]
116
145
  end
117
146
 
118
147
  subgraph FunctionModule["Function Utilities"]
119
148
  FunctionUtil["function.js<br/>trueThat, falseThat"]
120
149
  end
121
150
 
122
- subgraph StringModule["String Utilities"]
123
- StringUtil["string.js<br/>RegExp, grapheme counting"]
151
+ subgraph StringModules["String &amp; Scanning"]
152
+ StringUtil["string.js<br/>RegExp, grapheme counting, fnv1a"]
153
+ ScanUtil["scan.js<br/>Char-code classes for parsers"]
154
+ MessageUtil["message.js<br/>Message template/catalog compiler"]
155
+ ColorUtil["color.js<br/>Mixing, relative luminance"]
124
156
  end
125
157
  end
126
158
 
127
159
  CoreModule --> ScalarModules
128
160
  CoreModule --> CollectionModules
129
- CoreModule --> StringModule
161
+ CoreModule --> StringModules
130
162
  CoreModule --> FunctionModule
131
163
 
132
164
  TextIndex --> Basic
@@ -136,18 +168,41 @@ flowchart TB
136
168
  TextIndex --> Base64
137
169
  TextIndex --> Punycode
138
170
  TextIndex --> I18n
171
+ TextIndex --> IRegexp
172
+ TextIndex --> TextMisc
139
173
 
140
174
  MathIndex --> Int32Math
141
175
  MathIndex --> Float64Math
142
176
  MathIndex --> Vec2I32
143
177
  MathIndex --> Vec2F64
144
178
  MathIndex --> Vec3F64
179
+ MathIndex --> Mat4Math
180
+ MathIndex --> ProjectMath
181
+ MathIndex --> SolveMath
182
+ MathIndex --> WordMath
183
+ MathIndex --> FormatMath
184
+
185
+ DatesIndex --> DatesRfc
186
+ DatesIndex --> DatesCivil
187
+ DatesIndex --> DatesFormat
188
+ DatesIndex --> DatesDuration
189
+
190
+ GeoIndex --> GeoPred
191
+ GeoIndex --> GeoDist
192
+ GeoIndex --> GeoRing
193
+ GeoIndex --> GeoBbox
194
+ GeoIndex --> GeoHash
195
+ GeoIndex --> GeoJson
196
+ GeoIndex --> GeoValid
197
+ GeoIndex --> GeoTree
198
+ GeoIndex --> GeoMerc
145
199
 
146
200
  style CorePackage fill:#e1f5fe
147
201
  style CoreModule fill:#bbdefb
148
202
  style TextModules fill:#c8e6c9
149
203
  style MathModules fill:#ffccbc
150
204
  style DateModule fill:#fff9c4
205
+ style GeoModule fill:#b2dfdb
151
206
  ```
152
207
 
153
208
  ---
@@ -206,7 +261,7 @@ flowchart TB
206
261
  | `@jarenjs/refs` | None | Schema reference data |
207
262
  | `jarenjs` (root) | All packages | Public API aggregation |
208
263
 
209
- > **Note:** For broader Jaren architecture, see the root [`ARCHITECTURE.md`](../../ARCHITECTURE.md). For development guides, see [`HOWTO.md`](../../HOWTO.md).
264
+ > **Note:** For broader Jaren architecture, see the root [`ARCHITECTURE.md`](../../docs/ARCHITECTURE.md). For development guides, see [`HOWTO.md`](../../docs/HOWTO.md).
210
265
 
211
266
  ---
212
267
 
@@ -383,9 +438,41 @@ getStringLength("👨‍👩‍👧‍👦", false); // 11 (code units)
383
438
  getStringLength("👨‍👩‍👧‍👦", true); // 1 (grapheme cluster)
384
439
  ```
385
440
 
386
- ### 5. Date Module (`dates.js`)
387
-
388
- RFC 3339 and ISO 8601 compliant date/time parsing.
441
+ ### 5. Date Module (`dates/`)
442
+
443
+ RFC 3339 and ISO 8601 validation, plus the suite's calendar kernel.
444
+
445
+ **Dates are not a type here.** They are the two forms JSON already has — an
446
+ RFC 3339 **string** (lexical, what documents, schemas, forms and TOML
447
+ contain) and **epoch milliseconds** (arithmetic, what a chart plots). A
448
+ wrapper object, even an immutable one, could not be a query-engine item, a
449
+ JSON Patch target or part of app state; it is why `canonicalizeJson` turns a
450
+ `Date` into `{}`. The parts record from `parseRFC3339Parts` is the working
451
+ intermediate and is plain data, never an opaque handle.
452
+
453
+ | Module | Owns |
454
+ |---|---|
455
+ | `rfc3339.js` | validation, lexical decomposition, epoch conversion |
456
+ | `civil.js` | proleptic Gregorian arithmetic over integers |
457
+ | `format.js` | LDML pattern → compiled formatter |
458
+ | `duration.js` | ISO 8601 duration decomposition and date arithmetic |
459
+
460
+ Calendar math goes through day numbers (`daysFromCivil`/`civilFromDays`),
461
+ never through `Date`: the conversions are ~10 integer operations and allocate
462
+ nothing, where an allocate-mutate-read `Date` round trip costs about 165× as
463
+ much for the same answer. For the same reason every `getDateTypeOf*` getter
464
+ has a `getEpochOf*` twin — the same validity check and the same engine parse,
465
+ returning epoch milliseconds instead of a `Date` — so a consumer comparing
466
+ against precomputed bounds (the `formatMinimum`/`formatMaximum` validators)
467
+ allocates nothing per value. Formatting is the two-stage compiler again — a
468
+ pattern is scanned once into a chain of appenders, which measured ~4.5×
469
+ against re-scanning it per call.
470
+
471
+ Locale-dependent presentation (month and weekday names, meridiem, relative
472
+ phrasing) is deliberately **not** here, so this package stays zero-dependency
473
+ and free of data that drifts per language: `compileDateFormat` takes a names
474
+ provider, and a pattern using `MMMM`/`EEEE`/`a` without one is a compile
475
+ error rather than a silent English fallback.
389
476
 
390
477
  ```mermaid
391
478
  flowchart TB
@@ -425,11 +512,130 @@ import {
425
512
  CONST_TICKS_HOUR, // 3600000
426
513
  CONST_TICKS_DAY, // 86400000
427
514
  CONST_RFC3339_DAYS, // Days per month array
428
- CONST_RFC3339_REGEX_ISDATE, // Date regex
429
- CONST_RFC3339_REGEX_ISTIME, // Time regex
430
515
  } from '@jarenjs/core/dates';
431
516
  ```
432
517
 
518
+ **Working with the calendar:**
519
+
520
+ ```javascript
521
+ import {
522
+ parseRFC3339Parts, addToParts, startOfParts, compileDateFormat,
523
+ } from '@jarenjs/core/dates';
524
+
525
+ const parts = parseRFC3339Parts('2026-01-31');
526
+ addToParts(parts, 1, 'month'); // 2026-02-28 — month math clamps
527
+ startOfParts(parts, 'week'); // the Monday of that week (ISO 8601)
528
+
529
+ const fmt = compileDateFormat("yyyy-'W'ww"); // compile once…
530
+ fmt(parts); // …call many: '2026-W05'
531
+ ```
532
+
533
+ ### 5b. Geo Module (`geo/`)
534
+
535
+ The spatial kernel. As with dates, **there is no geometry type**: the
536
+ representation is GeoJSON ([RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946)),
537
+ whose positions are `[longitude, latitude]` arrays and whose rings are arrays
538
+ of those. They are already JSON items, so they stay patchable, schema-checkable
539
+ and addressable; a wrapper class would break all three. Every function here
540
+ takes plain numbers and plain arrays, so a `Polygon`'s `coordinates` can be
541
+ passed straight in without the `type` discriminator being involved.
542
+
543
+ | Module | Owns |
544
+ |---|---|
545
+ | `predicates.js` | robust orientation — the sign every spatial test rests on |
546
+ | `distance.js` | great-circle measurement on the WGS 84 sphere |
547
+ | `ring.js` | ring closure, winding, area and containment |
548
+ | `bbox.js` | bounding boxes, the cheap half of every spatial test |
549
+ | `geohash.js` | the base-32 cell encoding, and its validity tester |
550
+ | `geojson.js` | the one layer that knows the `type` discriminator |
551
+ | `valid.js` | `isValidGeoJson` — the one-call structural judgment (rings must close), the boolean twin of the schema artifacts in `@jarenjs/json` |
552
+ | `wkt.js` | `isValidWkt` — a strict validity tester for Well-Known Text |
553
+ | `index-tree.js` | a static packed-Hilbert box index for spatial joins |
554
+ | `mercator.js` | Web Mercator, the projection *out* for anything that draws |
555
+ | `simplify.js` | Douglas-Peucker — dropping the vertices a drawing cannot show |
556
+
557
+ The three validity testers back the `geoFormats` group in
558
+ [`@jarenjs/formats`](../formats) (`geohash`, `wkt`, `geojson`).
559
+
560
+ Two decisions carry the module. **Orientation is computed exactly**, through
561
+ Shewchuk's adaptive precision arithmetic: a naive floating-point determinant
562
+ returns the *wrong sign* on near-collinear input, which makes a containment
563
+ test contradict itself and a clipper emit self-intersecting output. The test
564
+ suite pins real longitude/latitude fixtures on the San Francisco–Los Angeles
565
+ line where the naive form reports "collinear" and the truth is one side or the
566
+ other. The cheap filter runs first, so the exact path costs nothing until it is
567
+ needed.
568
+
569
+ **Distance is spherical, and the planar shortcut is refused.** A degree of
570
+ longitude spans ~111 km at the equator and ~68 km at 52°N, so a Euclidean norm
571
+ on raw degrees is 64% wrong over 1 km at Dutch latitudes. `haversineDistance`
572
+ is the answer (<0.5% anywhere); `equirectDistance` is the *screening* form
573
+ (0.02% at 430 km, 12.4% intercontinentally — a few multiply-adds against
574
+ haversine's transcendentals) for rejecting candidates before the real test —
575
+ the same build-then-probe shape the query engine's hash join uses. The
576
+ ellipsoid is deliberately not modelled.
577
+
578
+ RFC 7946 removed coordinate-reference-system support and mandates WGS 84, so
579
+ there is no SRID table and no reprojection: conformance removes the need rather
580
+ than an omission hiding it.
581
+
582
+ **Measured against the field** (`npm run benchmark:geo`, Node v22.22.2). Every
583
+ scenario asserts result equivalence before any timing, and the harness refuses
584
+ to print a table if the engines disagree: distance, area, length and bounding
585
+ box come out *bit-identical* to Turf, containment agrees on a 400-point sweep,
586
+ and the index returns exactly Flatbush's answer on 200 queries. Ratios are the
587
+ rival's time over this kernel's, so above 1 means Jaren is faster.
588
+
589
+ | scenario | Jaren | rival | ratio |
590
+ |---|---|---|---|
591
+ | distance, two positions | 26.2 ns | 128.5 ns (turf) | **4.9×** |
592
+ | distance vs an ellipsoidal library | 52.3 ns | 648.4 ns (geolib) | **12.4×** |
593
+ | line length, 500 positions | 20.5 µs | 63.6 µs (turf) | **3.1×** |
594
+ | point in polygon, 12 vertices | 228 ns | 327 ns (turf) | **1.4×** |
595
+ | bounding box, 2000 vertices | 15.3 µs | 18.1 µs (turf) | **1.2×** |
596
+ | polygon area, 2000 vertices | 18.8 µs | 20.4 µs (turf) | **1.1×** |
597
+ | centroid, 2000 vertices | 18.4 µs | 19.1 µs (turf) | 1.0× |
598
+ | index build, 100k boxes | 12.2 ms | 12.4 ms (flatbush) | 1.0× |
599
+ | index probe, 100k boxes | 529 ns | 529 ns (flatbush) | 1.0× |
600
+ | point in polygon, 2000 vertices | 8.2 µs | 3.7 µs (turf) | **0.5×** |
601
+
602
+ One row is a loss, and it is kept on purpose. **Point-in-polygon on a large
603
+ ring runs at about half Turf's speed** because every edge that could matter
604
+ goes through the exact orientation predicate, where Turf uses naive
605
+ floating-point arithmetic. That is the trade this module exists to make — it
606
+ is the difference between a containment test that is right on near-collinear
607
+ input and one that is merely fast. Cheap straddle/span tests already skip the
608
+ predicate on edges that cannot affect the answer, which took this from 0.2×
609
+ to 0.5×; the rest is the predicate itself.
610
+
611
+ Two former losses closed, and both closures carry a lesson about profiling
612
+ before fixing:
613
+
614
+ - **Index build** was 2.6× behind Flatbush, and the assumed cause — the leaf
615
+ sort permuting the four-wide bounds rows on every swap — turned out to be
616
+ wrong: fixing it moved nothing. A phase profile put ~60% of the build in
617
+ the *Hilbert distance* computation, a 16-step bisection loop with a float
618
+ division per step. The classical loop is now the bit-parallel transform
619
+ (identical values, verified exhaustively at the corners and over 200k
620
+ pseudo-random points), the sort orders a `Uint32Array` permutation with an
621
+ insertion-sort cutoff, and the bounds are written once, already in leaf
622
+ order. Build and probe are both level with Flatbush now.
623
+ - **Centroid** walked the same `eachPosition` as `bboxOf` yet lost where
624
+ `bboxOf` won. The difference was the callback body: accumulating doubles
625
+ into *closure variables* writes a boxed heap number per `+=` — two per
626
+ vertex — where `bboxOf`'s comparisons rarely write at all. The accumulators
627
+ are a small `Float64Array` now (raw double stores, no boxing), and the row
628
+ is level with Turf.
629
+
630
+ ```javascript
631
+ import { orient2d, haversineDistance, ringWinding, geohashEncode } from '@jarenjs/core/geo';
632
+
633
+ orient2d(0, 0, 1, 0, 0, 1); // > 0 — counter-clockwise, exactly
634
+ haversineDistance(4.9041, 52.3676, 2.3522, 48.8566); // 429_862 m
635
+ ringWinding([[0,0],[1,0],[1,1],[0,1],[0,0]]); // 1 — RFC 7946 exterior ring
636
+ geohashEncode(4.9041, 52.3676, 5); // 'u173z' — a string, so $starts-with is proximity
637
+ ```
638
+
433
639
  ### 6. Text Module (`text/`)
434
640
 
435
641
  Comprehensive string format validation organized by domain.
@@ -457,12 +663,11 @@ flowchart TB
457
663
 
458
664
  subgraph EmailModule["email.js"]
459
665
  Email["isValidEmail()<br/>Basic RFC 5322"]
460
- EmailFull["isValidEmailFull()<br/>Comprehensive"]
461
666
  IDNEmail["isValidIdnEmail()<br/>Internationalized"]
462
667
  end
463
668
 
464
669
  subgraph HostModule["host.js"]
465
- URL["isValidUrl() / isValidUrlFull()"]
670
+ URL["isValidUrl()<br/>Web-scheme URI"]
466
671
  URI["isValidUri() / isValidUriFull()"]
467
672
  IPv4["isValidIPv4()"]
468
673
  IPv6["isValidIPv6()"]
@@ -471,16 +676,14 @@ flowchart TB
471
676
  end
472
677
 
473
678
  subgraph Base64Module["base64.js"]
474
- B64["isValidBase64()<br/>Multiple implementations"]
475
- B64Fast["isValidBase64Fast()<br/>Optimized"]
476
- B64Full["isValidBase64Full()<br/>Strict padding"]
679
+ B64["isValidBase64()<br/>RFC 4648"]
477
680
  end
478
681
 
479
682
  subgraph PunycodeModule["punycode.js"]
480
- ToASCII["toASCII()<br/>Unicode Punycode"]
481
- ToUnicode["toUnicode()<br/>Punycode Unicode"]
482
- Encode["encode()<br/>Raw encoding"]
483
- Decode["decode()<br/>Raw decoding"]
683
+ Encode["punycodeEncode()<br/>One label, RFC 3492"]
684
+ Decode["punycodeDecode()<br/>One label, RFC 3492"]
685
+ ToASCII["domainToASCII()<br/>Whole name → ACE"]
686
+ ToUnicode["domainToUnicode()<br/>ACE → Unicode"]
484
687
  end
485
688
 
486
689
  subgraph I18nModule["i18n.js"]
@@ -490,13 +693,13 @@ flowchart TB
490
693
  Hebrew["isHebrew()"]
491
694
  Arabic["isArabicIndicDigit()"]
492
695
  CJK["isHiragana() / isKatakana() / isHan()"]
493
- Contextual["checkContextualRules()<br/>IDN label rules"]
696
+ Contextual["checkContextualRules()<br/>IDN label rules, over code points"]
494
697
  end
495
698
 
496
699
  subgraph Misc["misc.js"]
497
700
  ISBN["ISBN-10 / ISBN-13 validation"]
498
701
  Country["isValidCountryAlpha2()<br/>ISO 3166-1"]
499
- IBAN["isValidIBAN()"]
702
+ IBAN["isValidIBAN()<br/>ISO 13616 + MOD 97-10"]
500
703
  end
501
704
  end
502
705
  ```
@@ -531,7 +734,7 @@ flowchart TB
531
734
  Standard["Standard math<br/>sqrt, pow, sin, cos"]
532
735
  GCD["GCD calculation"]
533
736
  InverseSqrt["Fast inverse square root<br/>(Quake III algorithm)"]
534
- Interpolation["Interpolation<br/>lerp, norm, map"]
737
+ Interpolation["Normalization<br/>norm (free export: remap)"]
535
738
  end
536
739
 
537
740
  subgraph Vec2I32["vec2i32.js"]
@@ -797,4 +1000,4 @@ export function isNumberType(data) {
797
1000
 
798
1001
  When contributing, remember: this package is used by `@jarenjs/validate` and `@jarenjs/formats`. Changes here have downstream effects. Maintain backward compatibility, optimize for performance, and keep the API predictable.
799
1002
 
800
- For questions about the broader architecture, see the root [`ARCHITECTURE.md`](../../ARCHITECTURE.md). For development workflows, see [`HOWTO.md`](../../HOWTO.md).
1003
+ For questions about the broader architecture, see the root [`ARCHITECTURE.md`](../../docs/ARCHITECTURE.md). For development workflows, see [`HOWTO.md`](../../docs/HOWTO.md).
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 validator, the formats package and the forms package are built on lives here — type guards, Unicode-aware string handling, a large text-validation toolbox, number range helpers, and fixed-point and vector math.
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 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
 
@@ -10,18 +10,25 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
10
10
  |---|---|
11
11
  | `@jarenjs/core` | type guards and getters (`isStringType`, `isObjectClass`, `getIntegerType`, ...) |
12
12
  | `@jarenjs/core/array` | array helpers (`isUniqueArray`, `getUniqueArray`, `includesAll`, ...) |
13
- | `@jarenjs/core/object` | deep equality (`equalsDeep`, JSON-only `equalsJson`), map/set merging |
14
- | `@jarenjs/core/string` | Unicode string helpers (`countCodePoints`, `compareCodePoints`, ...), cached regex compilation |
13
+ | `@jarenjs/core/object` | deep equality (`equalsDeep`, JSON-only `equalsJson`), the `isJsonObject` predicate, `__proto__`-safe `setObjectMember`, `deepFreeze`, map/set merging |
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
+ | `@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/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 |
15
17
  | `@jarenjs/core/scan` | char-code constants and predicates for recursive-descent parsers |
18
+ | `@jarenjs/core/message` | the message template/catalog compiler shared by the validator and the form layer |
19
+ | `@jarenjs/core/color` | pure color math (`lerpColor` — hex `#rrggbb` interpolation) |
16
20
  | `@jarenjs/core/number` | boolean/number/integer coercion helpers (`isIntishType`, ...) |
17
21
  | `@jarenjs/core/integer` | `int8` ... `uint64` ranges and validators |
18
22
  | `@jarenjs/core/float` | `float16` ... `float64` constants, validators, increment/decrement |
19
23
  | `@jarenjs/core/bigint` | bigint helpers (`BigInt_min`, `BigInt_MinMax`, ...) |
20
- | `@jarenjs/core/dates` | RFC 3339 / ISO 8601 date-time parsing and validation |
24
+ | `@jarenjs/core/dates` | RFC 3339 / ISO 8601 validation, plus the calendar kernel: integer date arithmetic, compiled formatting, durations |
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 |
21
26
  | `@jarenjs/core/text` | text validators: emails, hostnames, IPs, URIs/IRIs, UUIDs, punycode, ... |
22
- | `@jarenjs/core/math` | int32/float64 math and 2D/3D vector classes |
27
+ | `@jarenjs/core/math` | int32/float64 math and 2D/3D vector classes; the linear `remap` and unit-interval `clamp01` |
28
+ | `@jarenjs/core/finance` | zero-dependency finance/trading formulas: TVM, cash flow, amortization, interest, depreciation, bonds, technical indicators, returns/risk |
29
+ | `@jarenjs/core/convert` | pure deterministic quantity conversion: affine dimensional units and rate-table currency |
23
30
 
24
- Deep imports work too (`@jarenjs/core/text/email`, `@jarenjs/core/math/vec2f64`, ...).
31
+ Deep imports work too (`@jarenjs/core/text/email`, `@jarenjs/core/math/vec2f64`, `@jarenjs/core/finance/tvm`, ...).
25
32
 
26
33
  ## Strings and Unicode
27
34
 
@@ -51,13 +58,60 @@ isValidIPv6('::ffff:192.168.0.1'); // true
51
58
  isValidUriTemplate('/users{/id}{?q}'); // true (RFC 6570)
52
59
  ```
53
60
 
54
- Grouped by file: `email` (RFC 5321 + internationalized addresses), `host` (hostnames, IDN hostnames, IPv4/IPv6, MAC, URI/URL/IRI and references, URI templates), `identifiers` (UUID, GUID, C/HTML/CSS identifiers), `iregexp` (I-Regexp / RFC 9485 validation and translation to `RegExp`: `isValidIRegexp`, `compileIRegexp`), `base64`, `basic` (alpha, numeric, hex, web colors), `misc` (ISBN-10/13, ISO 3166 country codes, IBAN), `i18n` (Unicode script tests and IDNA context checks) and a complete `punycode` implementation.
61
+ Grouped by file: `email` (RFC 5321 + internationalized addresses), `host` (hostnames, IDN hostnames, IPv4/IPv6, MAC, URI/URL/IRI and references, URI templates), `identifiers` (UUID, GUID, C/HTML/CSS identifiers), `iregexp` (I-Regexp / RFC 9485 validation and translation to `RegExp`: `isValidIRegexp`, `compileIRegexp`), `base64`, `basic` (alpha, numeric, hex, web colors), `misc` (ISBN-10/13, ISO 3166 country codes, ISO 13616 IBAN including its MOD 97-10 check digits), `i18n` (Unicode script tests and IDNA context checks) and `punycode` (the RFC 3492 codec, `punycodeEncode`/`punycodeDecode`, plus the domain-level `domainToASCII`/`domainToUnicode`).
62
+
63
+ `host` answers `uri`, `uri-reference`, `iri` and `iri-reference` from one character-code scanner: RFC 3987 is RFC 3986 with the unreserved class widened by `ucschar` and `iprivate` admitted in the query, so both grammars are the same walk with one flag. That is what makes every URI an IRI by construction rather than by coincidence.
55
64
 
56
65
  ## Dates, numbers and math
57
66
 
58
- - `dates` — RFC 3339 date/time/date-time validation and parsing into `Date` (leap years and month lengths included), ISO 8601 durations, and the more lenient ISO date-time forms.
67
+ - `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 turns an LDML pattern into a formatter once instead of re-scanning it per call. 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).
59
68
  - `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.
60
- - `math` — asm.js-style typed math (`Int32`, `Float64`) and vector classes (`Vec2i32`, `Vec2f64`, `Vec3f64`) with a fast integer sine approximation.
69
+ - `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).
70
+
71
+ ## Geospatial
72
+
73
+ `@jarenjs/core/geo` is the suite's spatial kernel, and its representation is GeoJSON itself ([RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946)) rather than a geometry class: positions are `[longitude, latitude]` arrays, a `Polygon`'s `coordinates` *is* an array of rings, so every value stays a plain JSON item — patchable, schema-checkable, addressable by pointer and path. RFC 7946 fixes the coordinate system to WGS 84, so there is no SRID table and no reprojection *in*; the one projection that exists goes *out*, for drawing.
74
+
75
+ ```javascript
76
+ import {
77
+ orient2d, haversineDistance, ringWinding, bboxOf,
78
+ geohashEncode, isValidWkt, createBboxIndex,
79
+ } from '@jarenjs/core/geo';
80
+
81
+ orient2d(0, 0, 1, 0, 0, 1); // > 0 — counter-clockwise, exactly
82
+ haversineDistance(4.9041, 52.3676, 2.3522, 48.8566); // 429_862 m (Amsterdam–Paris)
83
+ ringWinding([[0,0],[1,0],[1,1],[0,1],[0,0]]); // 1 — an RFC 7946 exterior ring
84
+ bboxOf({ type: 'Polygon', coordinates: [[[4,52],[5,52],[5,53],[4,52]]] }); // [4, 52, 5, 53]
85
+ geohashEncode(4.9041, 52.3676, 5); // 'u173z' — a string, so a prefix test is proximity
86
+ isValidWkt('POINT (4.9041 52.3676)'); // true — strict ISO 19125 grammar
87
+
88
+ const index = createBboxIndex(regions.map(bboxOf)); // packed-Hilbert, build once
89
+ for (const i of index.search(...bboxOf(point))) confirm(regions[i]); // candidates, then the exact test
90
+ ```
91
+
92
+ Three design decisions carry the module:
93
+
94
+ - **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).
95
+ - **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.
96
+ - **Validity is a separate concern from traversal.** `eachPosition`, `bboxOf`, `centroidOf` and friends measure without judging; `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).
97
+
98
+ 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).
99
+
100
+ ## Messages
101
+
102
+ `@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):
103
+
104
+ ```javascript
105
+ import { compileMessageTemplate, compileMessageCatalog } from '@jarenjs/core/message';
106
+
107
+ const t = compileMessageTemplate('must be {comparison} {limit}');
108
+ t({ comparison: '>=', limit: 18 }); // 'must be >= 18'
109
+ ```
110
+
111
+ ## Units, currency and finance
112
+
113
+ - `convert` — pure, deterministic quantity conversion over a registry of affine dimensional units (`convert(1, 'nmi', 'km')` → `1.852`; dimensions include length, area, volume, mass, temperature, time, speed, pressure, energy, angle and more, with `unitsOf`/`dimensionOf` for discovery). Currency is rate-table based: rates come in as data, never from a network call inside this package. Reference in [docs/CONVERT.md](./docs/CONVERT.md).
114
+ - `finance` — zero-dependency finance and trading formulas grouped by file: `tvm` (`pmt(0.05/12, 360, 250000)` → `-1342.05`, plus `fv`/`pv`/`nper`/`rate`), `cashflow` (NPV/IRR), `amortization`, `interest`, `depreciation`, `bond`, `returns` (returns and risk measures) and `indicators` (SMA/EMA/WMA, MACD, RSI, Bollinger bands — windowed values pad with `null` until the window fills, so outputs align with inputs). Reference in [docs/FINANCE.md](./docs/FINANCE.md).
61
115
 
62
116
  ## JSON addressing standards
63
117
 
@@ -65,4 +119,4 @@ The JSON addressing and query standards — JSON validation helpers, JSON Pointe
65
119
 
66
120
  ## Development
67
121
 
68
- Unit tests live in `test/core/` at the repository root (`npm run test:core`). This package's internals are described in its own [ARCHITECTURE](./ARCHITECTURE.md) document; see the repository [README](../../README.md) and [ARCHITECTURE](../../ARCHITECTURE.md) for the monorepo picture, and the [ROADMAP](../../ROADMAP.md) for planned work.
122
+ Unit tests live in `test/core/` at the repository root (`npm run test:core`). This package's internals are described in its own [ARCHITECTURE](./ARCHITECTURE.md) document, with per-module references under [docs/](./docs/) (`MATH`, `CONVERT`, `FINANCE`, `DATES`, `GEO`); see the repository [README](../../README.md) and [ARCHITECTURE](../../docs/ARCHITECTURE.md) for the monorepo picture, and the [ROADMAP](../../docs/ROADMAP.md) for planned work.
@@ -26,3 +26,16 @@ export declare function isUniqueArray(arr: any[]): boolean;
26
26
  * @returns {boolean} True if all 'values' are included in 'arr', false otherwise.
27
27
  */
28
28
  export declare function includesAll(arr: any[], values: any[]): boolean;
29
+ /**
30
+ * The value when it is one of `allowed`, otherwise `fallback`. The point
31
+ * is untrusted input: a value restored from storage, a share link or a
32
+ * stale state slice can be anything, and a UI that switches on it directly
33
+ * renders nothing at all for a name it does not know. Narrowing to a known
34
+ * set turns that blank into the fallback.
35
+ * @template T
36
+ * @param {unknown} value - The candidate, from anywhere.
37
+ * @param {readonly T[]} allowed - The values that may pass through.
38
+ * @param {T} fallback - Used when `value` is not one of `allowed`.
39
+ * @returns {T}
40
+ */
41
+ export declare function pickAllowed<T>(value: unknown, allowed: readonly T[], fallback: T): T;