@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
@@ -0,0 +1,142 @@
1
+ //@ts-check
2
+
3
+ //#region GeoJSON validity
4
+ // The one-call structural judgment over a GeoJSON value: the boolean
5
+ // twin of the schema artifacts in `@jarenjs/json`. The portable
6
+ // meta-schema states the same grammar declaratively and reports *where*
7
+ // a document fails; the Jaren variant adds winding through `$query`.
8
+ // This predicate exists for the callers that only need yes-or-no — a
9
+ // `format` tester, a guard before an expensive walk — and it includes
10
+ // the invariant the portable schema provably cannot express: that every
11
+ // linear ring closes.
12
+ //
13
+ // The traversal helpers in geojson.js stay judgment-free on purpose (a
14
+ // measurement that also validated would report the same defect twice);
15
+ // this module is where the judgment lives.
16
+
17
+ import { isRingClosed } from './ring.js';
18
+
19
+ /**
20
+ * A position: an array of 2 or 3 finite numbers, longitude in
21
+ * `[-180, 180]` and latitude in `[-90, 90]` — the same bounds the
22
+ * meta-schema states, valid because RFC 7946 fixes the reference system
23
+ * to WGS 84.
24
+ */
25
+ function isStrictPosition(value) {
26
+ if (!Array.isArray(value) || value.length < 2 || value.length > 3)
27
+ return false;
28
+ const lon = value[0];
29
+ const lat = value[1];
30
+ if (typeof lon !== 'number' || !(lon >= -180 && lon <= 180))
31
+ return false;
32
+ if (typeof lat !== 'number' || !(lat >= -90 && lat <= 90))
33
+ return false;
34
+ return value.length === 2
35
+ || (typeof value[2] === 'number' && Number.isFinite(value[2]));
36
+ }
37
+
38
+ function everyPosition(list) {
39
+ if (!Array.isArray(list))
40
+ return false;
41
+ for (let i = 0; i < list.length; i++) {
42
+ if (!isStrictPosition(list[i]))
43
+ return false;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ /** A line: two or more positions. */
49
+ function isLine(list) {
50
+ return Array.isArray(list) && list.length >= 2 && everyPosition(list);
51
+ }
52
+
53
+ /** A linear ring: four or more positions, first equal to last. */
54
+ function isRing(list) {
55
+ return Array.isArray(list) && list.length >= 4 && everyPosition(list)
56
+ && isRingClosed(list);
57
+ }
58
+
59
+ function everyOf(list, test) {
60
+ if (!Array.isArray(list))
61
+ return false;
62
+ for (let i = 0; i < list.length; i++) {
63
+ if (!test(list[i]))
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+
69
+ function isRings(list) {
70
+ return everyOf(list, isRing);
71
+ }
72
+
73
+ function isGeometryValue(value, depth) {
74
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
75
+ return false;
76
+ switch (value.type) {
77
+ case 'Point':
78
+ return isStrictPosition(value.coordinates);
79
+ case 'MultiPoint':
80
+ return everyPosition(value.coordinates);
81
+ case 'LineString':
82
+ return isLine(value.coordinates);
83
+ case 'MultiLineString':
84
+ return everyOf(value.coordinates, isLine);
85
+ case 'Polygon':
86
+ return isRings(value.coordinates);
87
+ case 'MultiPolygon':
88
+ return everyOf(value.coordinates, isRings);
89
+ case 'GeometryCollection':
90
+ // a nesting guard, not a spec rule: RFC 7946 merely discourages
91
+ // nested collections, but unbounded recursion on hostile input is
92
+ // a different problem than an unusual document
93
+ return depth < 8
94
+ && everyOf(value.geometries, (g) => isGeometryValue(g, depth + 1));
95
+ default:
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Whether a value is a structurally valid GeoJSON object: one of the
102
+ * seven geometry types, a Feature, or a FeatureCollection, with the
103
+ * coordinate nesting its `type` requires, positions of 2 or 3 numbers
104
+ * inside the WGS 84 bounds, and **every linear ring closed** — the
105
+ * invariant a JSON Schema cannot state.
106
+ *
107
+ * This is the shallow twin of the schema artifacts in `@jarenjs/json`:
108
+ * it answers yes or no in one call, where the meta-schema reports what
109
+ * failed and where, and the Jaren-extended variant also checks winding.
110
+ * Foreign members and a `bbox` are ignored rather than judged, exactly
111
+ * as the meta-schema leaves them open.
112
+ *
113
+ * @param {any} value
114
+ * @returns {boolean}
115
+ * @example
116
+ * isValidGeoJson({ type: 'Point', coordinates: [4.9, 52.4] }); // true
117
+ * isValidGeoJson({ type: 'Polygon',
118
+ * coordinates: [[[0,0],[1,0],[1,1],[0,0]]] }); // true
119
+ * isValidGeoJson({ type: 'Polygon',
120
+ * coordinates: [[[0,0],[1,0],[1,1],[2,2]]] }); // false (open ring)
121
+ * isValidGeoJson({ type: 'Point', coordinates: [52.4, 4.9, 0, 0] }); // false (4 axes)
122
+ */
123
+ export function isValidGeoJson(value) {
124
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
125
+ return false;
126
+ if (value.type === 'Feature') {
127
+ // `properties` and `geometry` are required members; both may be null
128
+ if (!('properties' in value) || !('geometry' in value))
129
+ return false;
130
+ if (value.properties !== null
131
+ && (typeof value.properties !== 'object' || Array.isArray(value.properties)))
132
+ return false;
133
+ return value.geometry === null || isGeometryValue(value.geometry, 0);
134
+ }
135
+ if (value.type === 'FeatureCollection') {
136
+ return everyOf(value.features, (f) => f !== null && typeof f === 'object'
137
+ && !Array.isArray(f) && f.type === 'Feature' && isValidGeoJson(f));
138
+ }
139
+ return isGeometryValue(value, 0);
140
+ }
141
+
142
+ //#endregion
package/src/geo/wkt.js ADDED
@@ -0,0 +1,262 @@
1
+ //@ts-check
2
+
3
+ //#region Well-Known Text
4
+ // A validity tester for WKT (ISO 19125 / OGC Simple Features), the text
5
+ // encoding every spatial database speaks. Nothing here *parses into* a
6
+ // geometry — the suite's representation is GeoJSON, and a WKT string in
7
+ // a JSON document is interchange data passing through — but a schema can
8
+ // still assert that such a string is well-formed, which is what a
9
+ // `format` needs: a plain predicate over the string itself.
10
+ //
11
+ // The grammar is validated strictly: the seven geometry tags, an
12
+ // optional Z/M/ZM dimension modifier, `EMPTY` or a parenthesized body,
13
+ // and a consistent coordinate count per geometry. The dimension rule
14
+ // follows the field rather than the letter of SFA: an unmodified tag
15
+ // accepts 2 or 3 coordinates per point (PostGIS reads `POINT(1 2 3)` as
16
+ // 3D), `Z` and `M` require exactly 3, `ZM` exactly 4 — but whichever
17
+ // count the first point establishes, every following point must match.
18
+ // Polygon rings must close and carry at least four points, as the spec
19
+ // requires. No leading or trailing text is tolerated: a format that
20
+ // trims would accept strings a consumer then fails on.
21
+
22
+ import {
23
+ isAsciiLetterCode,
24
+ isDigitCode,
25
+ isWhitespaceCode,
26
+ } from '../scan.js';
27
+
28
+ // character codes
29
+ const LPAREN = 0x28;
30
+ const RPAREN = 0x29;
31
+ const COMMA = 0x2C;
32
+ const PLUS = 0x2B;
33
+ const MINUS = 0x2D;
34
+ const DOT = 0x2E;
35
+
36
+ function skipWs(text, at) {
37
+ while (at < text.length && isWhitespaceCode(text.charCodeAt(at)))
38
+ at++;
39
+ return at;
40
+ }
41
+
42
+ /** Read a run of letters, uppercased; returns [word, next] or null. */
43
+ function readWord(text, at) {
44
+ let end = at;
45
+ while (end < text.length && isAsciiLetterCode(text.charCodeAt(end)))
46
+ end++;
47
+ return end === at ? null : [text.slice(at, end).toUpperCase(), end];
48
+ }
49
+
50
+ /** Advance past one signed decimal number, or return -1. */
51
+ function scanNumber(text, at) {
52
+ let i = at;
53
+ const c = i < text.length ? text.charCodeAt(i) : 0;
54
+ if (c === PLUS || c === MINUS)
55
+ i++;
56
+ let digits = 0;
57
+ while (i < text.length && isDigitCode(text.charCodeAt(i))) {
58
+ i++;
59
+ digits++;
60
+ }
61
+ if (i < text.length && text.charCodeAt(i) === DOT) {
62
+ i++;
63
+ while (i < text.length && isDigitCode(text.charCodeAt(i))) {
64
+ i++;
65
+ digits++;
66
+ }
67
+ }
68
+ if (digits === 0)
69
+ return -1;
70
+ const e = i < text.length ? text.charCodeAt(i) : 0;
71
+ if (e === 0x45 || e === 0x65) { // E e
72
+ let j = i + 1;
73
+ const sign = j < text.length ? text.charCodeAt(j) : 0;
74
+ if (sign === PLUS || sign === MINUS)
75
+ j++;
76
+ let exp = 0;
77
+ while (j < text.length && isDigitCode(text.charCodeAt(j))) {
78
+ j++;
79
+ exp++;
80
+ }
81
+ if (exp === 0)
82
+ return -1;
83
+ i = j;
84
+ }
85
+ return i;
86
+ }
87
+
88
+ /**
89
+ * One point: `dim.n` coordinates separated by whitespace. A zero `dim.n`
90
+ * means the modifier allowed 2 or 3, and the first point decides which.
91
+ * Returns the position after the point, or -1.
92
+ */
93
+ function scanPoint(text, at, dim) {
94
+ let i = scanNumber(text, skipWs(text, at));
95
+ if (i < 0)
96
+ return -1;
97
+ let count = 1;
98
+ for (;;) {
99
+ const j = skipWs(text, i);
100
+ if (j === i)
101
+ break; // numbers must be whitespace-separated
102
+ const k = scanNumber(text, j);
103
+ if (k < 0)
104
+ break;
105
+ i = k;
106
+ count++;
107
+ }
108
+ if (dim.n === 0) {
109
+ if (count !== 2 && count !== 3)
110
+ return -1;
111
+ dim.n = count;
112
+ }
113
+ else if (count !== dim.n) {
114
+ return -1;
115
+ }
116
+ return i;
117
+ }
118
+
119
+ /**
120
+ * A parenthesized, comma-separated list validated by `scanItem`, with at
121
+ * least `min` items. `first` may record where each item's scan started —
122
+ * the hook ring closure uses. Returns the position after `)`, or -1.
123
+ */
124
+ function scanList(text, at, dim, min, scanItem, starts) {
125
+ let i = skipWs(text, at);
126
+ if (i >= text.length || text.charCodeAt(i) !== LPAREN)
127
+ return -1;
128
+ i++;
129
+ let count = 0;
130
+ for (;;) {
131
+ const from = skipWs(text, i);
132
+ if (starts !== null)
133
+ starts.push(from);
134
+ i = scanItem(text, from, dim);
135
+ if (i < 0)
136
+ return -1;
137
+ count++;
138
+ i = skipWs(text, i);
139
+ const c = i < text.length ? text.charCodeAt(i) : 0;
140
+ if (c === COMMA) {
141
+ i++;
142
+ continue;
143
+ }
144
+ if (c === RPAREN)
145
+ return count >= min ? i + 1 : -1;
146
+ return -1;
147
+ }
148
+ }
149
+
150
+ /** A point that may also be wrapped in its own parens (MULTIPOINT). */
151
+ function scanMultiPointItem(text, at, dim) {
152
+ if (at < text.length && text.charCodeAt(at) === LPAREN) {
153
+ const i = scanPoint(text, at + 1, dim);
154
+ if (i < 0)
155
+ return -1;
156
+ const j = skipWs(text, i);
157
+ return j < text.length && text.charCodeAt(j) === RPAREN ? j + 1 : -1;
158
+ }
159
+ return scanPoint(text, at, dim);
160
+ }
161
+
162
+ function scanLineStringBody(text, at, dim) {
163
+ return scanList(text, at, dim, 2, scanPoint, null);
164
+ }
165
+
166
+ /** A ring: four or more points, and the first equals the last. */
167
+ function scanRing(text, at, dim) {
168
+ const starts = [];
169
+ const i = scanList(text, at, dim, 4, scanPoint, starts);
170
+ if (i < 0)
171
+ return -1;
172
+ // compare the first and last point textually by re-scanning both spans
173
+ const first = pointText(text, starts[0], dim);
174
+ const last = pointText(text, starts[starts.length - 1], dim);
175
+ return first !== null && first === last ? i : -1;
176
+ }
177
+
178
+ /** The point's coordinates as a normalized string, for closure tests. */
179
+ function pointText(text, at, dim) {
180
+ const end = scanPoint(text, at, dim);
181
+ if (end < 0)
182
+ return null;
183
+ const parts = text.slice(at, end).trim().split(/\s+/);
184
+ return parts.map(Number).join(',');
185
+ }
186
+
187
+ function scanPolygonBody(text, at, dim) {
188
+ return scanList(text, at, dim, 1, scanRing, null);
189
+ }
190
+
191
+ /**
192
+ * One tagged geometry: `TAG [Z|M|ZM] (EMPTY | body)`. Returns the
193
+ * position after it, or -1.
194
+ */
195
+ function scanGeometry(text, at) {
196
+ const word = readWord(text, skipWs(text, at));
197
+ if (word === null)
198
+ return -1;
199
+ const [tag, afterTag] = word;
200
+ let i = afterTag;
201
+ // an exact count from the modifier, or 0 for "2 or 3, first point decides"
202
+ const dim = { n: 0 };
203
+ const mod = readWord(text, skipWs(text, i));
204
+ if (mod !== null && (mod[0] === 'Z' || mod[0] === 'M' || mod[0] === 'ZM')) {
205
+ dim.n = mod[0] === 'ZM' ? 4 : 3;
206
+ i = mod[1];
207
+ }
208
+ const empty = readWord(text, skipWs(text, i));
209
+ if (empty !== null && empty[0] === 'EMPTY')
210
+ return empty[1];
211
+
212
+ switch (tag) {
213
+ case 'POINT': {
214
+ const j = skipWs(text, i);
215
+ if (j >= text.length || text.charCodeAt(j) !== LPAREN)
216
+ return -1;
217
+ const k = scanPoint(text, j + 1, dim);
218
+ if (k < 0)
219
+ return -1;
220
+ const l = skipWs(text, k);
221
+ return l < text.length && text.charCodeAt(l) === RPAREN ? l + 1 : -1;
222
+ }
223
+ case 'LINESTRING':
224
+ return scanLineStringBody(text, i, dim);
225
+ case 'POLYGON':
226
+ return scanPolygonBody(text, i, dim);
227
+ case 'MULTIPOINT':
228
+ return scanList(text, i, dim, 1, scanMultiPointItem, null);
229
+ case 'MULTILINESTRING':
230
+ return scanList(text, i, dim, 1, scanLineStringBody, null);
231
+ case 'MULTIPOLYGON':
232
+ return scanList(text, i, dim, 1, scanPolygonBody, null);
233
+ case 'GEOMETRYCOLLECTION':
234
+ // members are whole tagged geometries with modifiers of their own
235
+ return scanList(text, i, dim, 1, (t, a) => scanGeometry(t, a), null);
236
+ default:
237
+ return -1;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Whether a string is a well-formed WKT geometry: one of the seven
243
+ * tagged types, `EMPTY` or a body whose nesting matches the tag, a
244
+ * coordinate count consistent with the `Z`/`M`/`ZM` modifier and with
245
+ * itself, closed polygon rings, and nothing before or after.
246
+ *
247
+ * @param {string} text
248
+ * @returns {boolean}
249
+ * @example
250
+ * isValidWkt('POINT (4.9041 52.3676)'); // true
251
+ * isValidWkt('POLYGON ((0 0, 4 0, 4 4, 0 0))'); // true (closed)
252
+ * isValidWkt('POLYGON ((0 0, 4 0, 4 4, 1 1))'); // false (open ring)
253
+ * isValidWkt('POINT Z (1 2)'); // false (Z wants 3)
254
+ * isValidWkt('LINESTRING (0 0, 1 1 1)'); // false (mixed dimension)
255
+ */
256
+ export function isValidWkt(text) {
257
+ if (typeof text !== 'string' || text.length === 0 || isWhitespaceCode(text.charCodeAt(0)))
258
+ return false;
259
+ return scanGeometry(text, 0) === text.length;
260
+ }
261
+
262
+ //#endregion
@@ -9,6 +9,24 @@ export const mathf64_cos = Math.cos;
9
9
  export const mathf64_atan2 = Math.atan2;
10
10
  export const mathf64_asin = Math.asin;
11
11
 
12
+ // Transcendental completeness. Free `Math.*` aliases, in the
13
+ // module's existing naming style, so downstream packages bind them by
14
+ // import instead of reaching for the global `Math`.
15
+ export const mathf64_tan = Math.tan;
16
+ export const mathf64_acos = Math.acos;
17
+ export const mathf64_atan = Math.atan;
18
+ export const mathf64_sinh = Math.sinh;
19
+ export const mathf64_cosh = Math.cosh;
20
+ export const mathf64_tanh = Math.tanh;
21
+ export const mathf64_cbrt = Math.cbrt;
22
+ export const mathf64_log = Math.log;
23
+ export const mathf64_log2 = Math.log2;
24
+ export const mathf64_log10 = Math.log10;
25
+ export const mathf64_exp = Math.exp;
26
+ export const mathf64_expm1 = Math.expm1;
27
+ export const mathf64_hypot = Math.hypot;
28
+ export const mathf64_sign = Math.sign;
29
+
12
30
  export const mathf64_ceil = Math.ceil;
13
31
  export const mathf64_floor = Math.floor;
14
32
  export const mathf64_round = Math.round;
@@ -21,6 +39,11 @@ export const mathf64_EPSILON = +0.000001;
21
39
 
22
40
  export const mathf64_SQRTFIVE = +mathf64_sqrt(5);
23
41
 
42
+ export const mathf64_E = +Math.E;
43
+ export const mathf64_LN2 = +Math.LN2;
44
+ export const mathf64_LN10 = +Math.LN10;
45
+ export const mathf64_PHI = +((1 + mathf64_sqrt(5)) / 2);
46
+
24
47
  export const mathf64_PI = +Math.PI;
25
48
  export const mathf64_PI2 = +(mathf64_PI * 2);
26
49
  export const mathf64_PI1H = +(mathf64_PI / 2);
@@ -151,16 +174,6 @@ export class Float64 {
151
174
  return +((value - min) / (max - min));
152
175
  }
153
176
 
154
- static lerp(norm = 0.0, min = 0.0, max = 0.0) {
155
- norm = +norm; min = +min; max = +max;
156
- return +((max - min) * (norm + min));
157
- }
158
-
159
- static map(value = 0.0, smin = 0.0, smax = 0.0, dmin = 0.0, dmax = 0.0) {
160
- value = +value; smin = +smin; smax = +smax; dmin = +dmin; dmax = +dmax;
161
- return +Float64.lerp(+Float64.norm(value, smin, smax), dmin, dmax);
162
- }
163
-
164
177
  /**
165
178
  * Clamps a value between a checked boundary.
166
179
  * and can therefor handle swapped min/max arguments
@@ -239,6 +252,7 @@ export class Float64 {
239
252
  }
240
253
 
241
254
  static cosHp(r = 0.0) {
255
+ // High-precision polynomial cosine (Nick's approximation).
242
256
  // template<typename T>
243
257
  // inline T cos(T x) noexcept
244
258
  // {
@@ -251,7 +265,13 @@ export class Float64 {
251
265
  // #endif
252
266
  // return x;
253
267
  // }
254
- throw new Error('float64_cosHp is not implemented! r=' + String(r));
268
+ r = +r;
269
+ const tp = +(1.0 / +mathf64_PI2);
270
+ let x = +(+r * tp);
271
+ x = +(x - +(0.25 + +mathf64_floor(+(x + 0.25))));
272
+ x = +(x * +(16.0 * +(+mathf64_abs(x) - 0.5)));
273
+ x = +(x + +(0.225 * x * +(+mathf64_abs(x) - 1.0)));
274
+ return +x;
255
275
  }
256
276
 
257
277
  static sinMpEx(r = 0.0) {
@@ -313,4 +333,201 @@ export class Float64 {
313
333
 
314
334
  //#endregion
315
335
 
336
+ //#region transcendental completeness
337
+
338
+ /**
339
+ * The base-e logarithm of `x` in an arbitrary base.
340
+ * @param {number} base
341
+ * @param {number} x
342
+ * @returns {number}
343
+ */
344
+ static logBase(base = 0.0, x = 0.0) {
345
+ return +(+mathf64_log(+x) / +mathf64_log(+base));
346
+ }
347
+
348
+ /**
349
+ * The sign of `x` (-1, 0 or +1); preserves ±0 and NaN like `Math.sign`.
350
+ * @param {number} x
351
+ * @returns {number}
352
+ */
353
+ static sign(x = 0.0) {
354
+ return +mathf64_sign(+x);
355
+ }
356
+
357
+ /**
358
+ * The Euclidean length of any number of components, overflow-safe.
359
+ * @param {...number} args
360
+ * @returns {number}
361
+ */
362
+ static hypot(...args) {
363
+ return +mathf64_hypot(...args);
364
+ }
365
+
366
+ /**
367
+ * The real `n`-th root of `x` (odd roots of negatives handled).
368
+ * @param {number} x
369
+ * @param {number} n
370
+ * @returns {number}
371
+ */
372
+ static nthroot(x = 0.0, n = 0.0) {
373
+ x = +x; n = +n;
374
+ if (x < 0.0 && (n % 2.0) !== 0.0) {
375
+ return +(-mathf64_pow(-x, +(1.0 / n)));
376
+ }
377
+ return +mathf64_pow(x, +(1.0 / n));
378
+ }
379
+
380
+ /**
381
+ * Round `value` to `digits` decimal places (banker-free, half-up).
382
+ * @param {number} value
383
+ * @param {number} [digits]
384
+ * @returns {number}
385
+ */
386
+ static roundTo(value = 0.0, digits = 0.0) {
387
+ value = +value; digits = +digits | 0;
388
+ if (!isFinite(value)) return +value;
389
+ const f = +mathf64_pow(10.0, digits);
390
+ return +(+mathf64_round(+(value * f)) / f);
391
+ }
392
+
393
+ /**
394
+ * Round `value` to `digits` significant figures (1–100) — the numeric
395
+ * value of `Number.prototype.toPrecision`, without its exponential
396
+ * string formatting.
397
+ * @param {number} value
398
+ * @param {number} [digits]
399
+ * @returns {number}
400
+ */
401
+ static roundToPrecision(value = 0.0, digits = 1.0) {
402
+ value = +value; digits = +digits | 0;
403
+ if (!isFinite(value)) return +value;
404
+ return +Number(value.toPrecision(digits));
405
+ }
406
+
407
+ /**
408
+ * The Lanczos approximation of the Gamma function, valid for the whole
409
+ * real line (poles at non-positive integers return ±Infinity/NaN).
410
+ * @param {number} x
411
+ * @returns {number}
412
+ */
413
+ static gamma(x = 0.0) {
414
+ x = +x;
415
+ // Reflection for the left half-plane: Γ(x)Γ(1-x) = π / sin(πx).
416
+ if (x < 0.5) {
417
+ return +(mathf64_PI / +(mathf64_sin(+(mathf64_PI * x)) * +Float64.gamma(+(1.0 - x))));
418
+ }
419
+ x -= 1.0;
420
+ // g=7, n=9 Lanczos coefficients.
421
+ const g = 7.0;
422
+ const c = _LANCZOS_G7;
423
+ let a = +c[0];
424
+ const t = +(x + g + 0.5);
425
+ for (let i = 1; i < c.length; i++) {
426
+ a += +(c[i] / +(x + i));
427
+ }
428
+ return +(+mathf64_sqrt(+(2.0 * mathf64_PI)) * +mathf64_pow(t, +(x + 0.5)) * +mathf64_exp(-t) * a);
429
+ }
430
+
431
+ /**
432
+ * The factorial `n!`. Integer `n` uses an exact product; non-integers
433
+ * are lifted to `gamma(n + 1)`. Negative integers return NaN.
434
+ * @param {number} n
435
+ * @returns {number}
436
+ */
437
+ static factorial(n = 0.0) {
438
+ n = +n;
439
+ if (n < 0.0 && mathf64_floor(n) === n) return NaN;
440
+ if (mathf64_floor(n) === n) {
441
+ let acc = 1.0;
442
+ for (let i = 2.0; i <= n; i += 1.0) acc *= i;
443
+ return +acc;
444
+ }
445
+ return +Float64.gamma(+(n + 1.0));
446
+ }
447
+
448
+ //#endregion
449
+
450
+ }
451
+
452
+ /**
453
+ * Linear remap of `v` from the source range `[smin, smax]` to the destination
454
+ * range `[dmin, dmax]`: `dmin + t·(dmax - dmin)` with
455
+ * `t = (v - smin)/(smax - smin)`. A degenerate source range collapses to
456
+ * `dmin` rather than dividing by zero, so a constant-valued axis still maps to
457
+ * a drawable coordinate. Inverted destination ranges are supported, which is
458
+ * what screen-space y-flips need.
459
+ *
460
+ * @param {number} v
461
+ * @param {number} smin
462
+ * @param {number} smax
463
+ * @param {number} dmin
464
+ * @param {number} dmax
465
+ * @returns {number}
466
+ */
467
+ export function remap(v, smin, smax, dmin, dmax) {
468
+ if (smax === smin) return dmin;
469
+ return dmin + ((v - smin) / (smax - smin)) * (dmax - dmin);
316
470
  }
471
+
472
+ /**
473
+ * The "nice" axis step — 1, 2 or 5 times a power of ten — that covers `span`
474
+ * in roughly `count` steps, so a tick sequence reads as 0/2/4/6 rather than
475
+ * 0/1.7/3.4. `count` is floored at 1; pass a pre-divided per-step span and
476
+ * leave it at its default to get the ladder alone.
477
+ *
478
+ * The ladder is pure arithmetic with no domain guard: it is the caller who
479
+ * knows what a non-positive or non-finite `span` means for its axis, and
480
+ * different axes answer that differently.
481
+ *
482
+ * @param {number} span the domain span to cover
483
+ * @param {number} [count] desired number of steps (floored at 1)
484
+ * @returns {number}
485
+ */
486
+ export function niceStep(span, count = 1) {
487
+ const raw = span / Math.max(1, count);
488
+ const base = Math.pow(10, Math.floor(Math.log10(raw)));
489
+ const unit = raw / base;
490
+ const factor = unit < 1.5 ? 1 : unit < 3 ? 2 : unit < 7 ? 5 : 10;
491
+ return factor * base;
492
+ }
493
+
494
+ /**
495
+ * Clamp a value into the unit interval `[0, 1]` — the fraction every
496
+ * unit-space geometry stage emits. `NaN` passes through as `NaN` rather
497
+ * than collapsing to a boundary, so a non-finite input stays visible to
498
+ * the caller instead of silently rendering at an edge; callers that want a
499
+ * drawable coordinate regardless guard with `Number.isFinite` first.
500
+ *
501
+ * @param {number} v
502
+ * @returns {number}
503
+ */
504
+ export function clamp01(v) {
505
+ return v < 0 ? 0 : v > 1 ? 1 : v;
506
+ }
507
+
508
+ /**
509
+ * Geometric mean — the honest average of ratios (a 10× and a 0.1×
510
+ * average to parity, where an arithmetic mean would claim 5×).
511
+ * Non-finite and non-positive entries are skipped; `null` when nothing
512
+ * usable remains.
513
+ * @param {number[]} values
514
+ * @returns {number|null}
515
+ */
516
+ export function geoMean(values) {
517
+ const usable = values.filter((v) => Number.isFinite(v) && v > 0);
518
+ if (usable.length === 0) return null;
519
+ return Math.exp(usable.reduce((sum, v) => sum + Math.log(v), 0) / usable.length);
520
+ }
521
+
522
+ /** Lanczos g=7 coefficients (shared, allocation-free). */
523
+ const _LANCZOS_G7 = [
524
+ 0.99999999999980993,
525
+ 676.5203681218851,
526
+ -1259.1392167224028,
527
+ 771.32342877765313,
528
+ -176.61502916214059,
529
+ 12.507343278686905,
530
+ -0.13857109526572012,
531
+ 9.9843695780195716e-6,
532
+ 1.5056327351493116e-7,
533
+ ];