@jarenjs/core 0.9.2 → 0.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +229 -26
- package/README.md +64 -10
- package/dist/types/array.d.ts +13 -0
- package/dist/types/cache.d.ts +166 -0
- package/dist/types/chunk.d.ts +102 -0
- package/dist/types/color.d.ts +64 -0
- package/dist/types/convert/convert.d.ts +33 -0
- package/dist/types/convert/currency.d.ts +47 -0
- package/dist/types/convert/index.d.ts +9 -0
- package/dist/types/convert/registry.d.ts +55 -0
- package/dist/types/dates/civil.d.ts +146 -0
- package/dist/types/dates/duration.d.ts +83 -0
- package/dist/types/dates/format.d.ts +59 -0
- package/dist/types/dates/index.d.ts +4 -0
- package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
- package/dist/types/errors.d.ts +77 -0
- package/dist/types/finance/amortization.d.ts +51 -0
- package/dist/types/finance/bond.d.ts +45 -0
- package/dist/types/finance/cashflow.d.ts +44 -0
- package/dist/types/finance/depreciation.d.ts +26 -0
- package/dist/types/finance/index.d.ts +16 -0
- package/dist/types/finance/indicators.d.ts +76 -0
- package/dist/types/finance/interest.d.ts +44 -0
- package/dist/types/finance/returns.d.ts +43 -0
- package/dist/types/finance/tvm.d.ts +50 -0
- package/dist/types/geo/angle.d.ts +4 -0
- package/dist/types/geo/bbox.d.ts +32 -0
- package/dist/types/geo/distance.d.ts +67 -0
- package/dist/types/geo/geohash.d.ts +63 -0
- package/dist/types/geo/geojson.d.ts +108 -0
- package/dist/types/geo/index-tree.d.ts +52 -0
- package/dist/types/geo/index.d.ts +11 -0
- package/dist/types/geo/mercator.d.ts +57 -0
- package/dist/types/geo/predicates.d.ts +44 -0
- package/dist/types/geo/ring.d.ts +80 -0
- package/dist/types/geo/simplify.d.ts +31 -0
- package/dist/types/geo/valid.d.ts +24 -0
- package/dist/types/geo/wkt.d.ts +16 -0
- package/dist/types/math/float64.d.ts +126 -3
- package/dist/types/math/format.d.ts +58 -0
- package/dist/types/math/index.d.ts +5 -0
- package/dist/types/math/mat4.d.ts +60 -0
- package/dist/types/math/project.d.ts +54 -0
- package/dist/types/math/solve.d.ts +67 -0
- package/dist/types/math/word.d.ts +82 -0
- package/dist/types/message.d.ts +52 -0
- package/dist/types/object.d.ts +151 -0
- package/dist/types/scan.d.ts +30 -0
- package/dist/types/schema.d.ts +24 -0
- package/dist/types/string.d.ts +96 -0
- package/dist/types/text/base64.d.ts +6 -4
- package/dist/types/text/email.d.ts +0 -1
- package/dist/types/text/host.d.ts +39 -8
- package/dist/types/text/i18n.d.ts +13 -1
- package/dist/types/text/index.d.ts +1 -0
- package/dist/types/text/misc.d.ts +15 -1
- package/dist/types/text/punycode.d.ts +45 -85
- package/docs/CONVERT.md +45 -0
- package/docs/DATES.md +78 -0
- package/docs/FINANCE.md +59 -0
- package/docs/GEO.md +114 -0
- package/docs/MATH.md +75 -0
- package/package.json +57 -4
- package/src/array.js +16 -0
- package/src/cache.js +206 -0
- package/src/chunk.js +159 -0
- package/src/color.js +125 -0
- package/src/convert/convert.js +59 -0
- package/src/convert/currency.js +74 -0
- package/src/convert/index.js +11 -0
- package/src/convert/registry.js +213 -0
- package/src/dates/civil.js +360 -0
- package/src/dates/duration.js +225 -0
- package/src/dates/format.js +238 -0
- package/src/dates/index.js +30 -0
- package/src/dates/rfc3339.js +621 -0
- package/src/errors.js +102 -0
- package/src/finance/amortization.js +50 -0
- package/src/finance/bond.js +100 -0
- package/src/finance/cashflow.js +122 -0
- package/src/finance/depreciation.js +54 -0
- package/src/finance/index.js +27 -0
- package/src/finance/indicators.js +207 -0
- package/src/finance/interest.js +63 -0
- package/src/finance/returns.js +90 -0
- package/src/finance/tvm.js +94 -0
- package/src/function.js +0 -2
- package/src/geo/angle.js +12 -0
- package/src/geo/bbox.js +80 -0
- package/src/geo/distance.js +155 -0
- package/src/geo/geohash.js +224 -0
- package/src/geo/geojson.js +361 -0
- package/src/geo/index-tree.js +294 -0
- package/src/geo/index.js +52 -0
- package/src/geo/mercator.js +124 -0
- package/src/geo/predicates.js +313 -0
- package/src/geo/ring.js +182 -0
- package/src/geo/simplify.js +130 -0
- package/src/geo/valid.js +142 -0
- package/src/geo/wkt.js +262 -0
- package/src/math/float64.js +228 -11
- package/src/math/format.js +157 -0
- package/src/math/index.js +5 -0
- package/src/math/mat4.js +131 -0
- package/src/math/project.js +49 -0
- package/src/math/solve.js +112 -0
- package/src/math/word.js +177 -0
- package/src/message.js +119 -0
- package/src/object.js +329 -0
- package/src/scan.js +42 -0
- package/src/schema.js +37 -0
- package/src/string.js +189 -0
- package/src/text/base64.js +15 -52
- package/src/text/email.js +0 -5
- package/src/text/host.js +515 -140
- package/src/text/i18n.js +13 -6
- package/src/text/identifiers.js +1 -1
- package/src/text/index.js +1 -0
- package/src/text/misc.js +81 -5
- package/src/text/punycode.js +235 -323
- package/src/dates.js +0 -371
package/src/geo/valid.js
ADDED
|
@@ -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
|
package/src/math/float64.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
];
|