@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.
- 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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region Date formatting
|
|
4
|
+
// The two-stage compiler applied to date patterns: a pattern is scanned
|
|
5
|
+
// once into a chain of appenders, and calling the result only walks the
|
|
6
|
+
// chain. Re-scanning the pattern per call - the shape a `format(value,
|
|
7
|
+
// pattern)` helper naturally takes - measured 534 ns against 118 ns for
|
|
8
|
+
// the compiled form on `yyyy-MM-dd HH:mm:ss`.
|
|
9
|
+
//
|
|
10
|
+
// Tokens are Unicode LDML (the vocabulary `Intl` skeletons and CLDR use),
|
|
11
|
+
// not moment's: `yyyy-MM-dd`, never `YYYY-MM-DD`. That avoids moment's
|
|
12
|
+
// most reported footgun - `YYYY` there means the week-numbering year -
|
|
13
|
+
// and means a pattern copied from CLDR data behaves as its author meant.
|
|
14
|
+
// A literal is single-quoted (`'T'`), and `''` is a literal quote.
|
|
15
|
+
//
|
|
16
|
+
// Month, weekday and meridiem names are NOT built in: this module stays
|
|
17
|
+
// locale-free, so a pattern using a name token requires a `names`
|
|
18
|
+
// provider (@jarenjs/locales supplies one). Asking for `MMMM` without a
|
|
19
|
+
// provider is a compile error rather than a silent English fallback.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
daysFromCivil,
|
|
23
|
+
weekdayFromDays,
|
|
24
|
+
isoWeekdayFromDays,
|
|
25
|
+
isoWeekOfYear,
|
|
26
|
+
dayOfYear,
|
|
27
|
+
quarterOfYear,
|
|
28
|
+
} from './civil.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Locale names a pattern may need.
|
|
32
|
+
* @typedef {Object} DateNames
|
|
33
|
+
* @property {string[]} [months] - 12 wide month names, January first
|
|
34
|
+
* @property {string[]} [monthsShort] - 12 abbreviated month names
|
|
35
|
+
* @property {string[]} [weekdays] - 7 wide weekday names, Sunday first
|
|
36
|
+
* @property {string[]} [weekdaysShort] - 7 abbreviated weekday names
|
|
37
|
+
* @property {[string, string]} [meridiem] - the AM and PM markers
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const D2 = (n) => (n < 10 ? '0' + n : '' + n);
|
|
41
|
+
const D3 = (n) => (n < 10 ? '00' + n : n < 100 ? '0' + n : '' + n);
|
|
42
|
+
|
|
43
|
+
function pad(n, width) {
|
|
44
|
+
const s = '' + n;
|
|
45
|
+
return s.length >= width ? s : '0'.repeat(width - s.length) + s;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Signed offset in minutes to '+HH:MM' / '+HHMM' / 'Z'.
|
|
49
|
+
function offsetText(offset, colon, zForUtc) {
|
|
50
|
+
if (offset === null || offset === undefined)
|
|
51
|
+
return '';
|
|
52
|
+
if (offset === 0 && zForUtc)
|
|
53
|
+
return 'Z';
|
|
54
|
+
const sign = offset < 0 ? '-' : '+';
|
|
55
|
+
const abs = offset < 0 ? -offset : offset;
|
|
56
|
+
const hh = D2(Math.trunc(abs / 60));
|
|
57
|
+
const mm = D2(abs % 60);
|
|
58
|
+
return colon ? `${sign}${hh}:${mm}` : `${sign}${hh}${mm}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hour12(hours) {
|
|
62
|
+
const h = hours % 12;
|
|
63
|
+
return h === 0 ? 12 : h;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Every token, as a function of (parts, names). Longest match wins, so
|
|
67
|
+
// the compiler tries 5 characters down to 1.
|
|
68
|
+
const TOKENS = {
|
|
69
|
+
yyyy: (p) => pad(p.year < 0 ? -p.year : p.year, 4),
|
|
70
|
+
yy: (p) => D2((p.year % 100 + 100) % 100),
|
|
71
|
+
y: (p) => '' + p.year,
|
|
72
|
+
MMMM: (p, n) => n.months[p.month - 1],
|
|
73
|
+
MMM: (p, n) => n.monthsShort[p.month - 1],
|
|
74
|
+
MM: (p) => D2(p.month),
|
|
75
|
+
M: (p) => '' + p.month,
|
|
76
|
+
dd: (p) => D2(p.day),
|
|
77
|
+
d: (p) => '' + p.day,
|
|
78
|
+
EEEE: (p, n) => n.weekdays[weekdayFromDays(daysFromCivil(p.year, p.month, p.day))],
|
|
79
|
+
EEE: (p, n) => n.weekdaysShort[weekdayFromDays(daysFromCivil(p.year, p.month, p.day))],
|
|
80
|
+
E: (p) => '' + isoWeekdayFromDays(daysFromCivil(p.year, p.month, p.day)),
|
|
81
|
+
HH: (p) => D2(p.hours),
|
|
82
|
+
H: (p) => '' + p.hours,
|
|
83
|
+
hh: (p) => D2(hour12(p.hours)),
|
|
84
|
+
h: (p) => '' + hour12(p.hours),
|
|
85
|
+
mm: (p) => D2(p.minutes),
|
|
86
|
+
m: (p) => '' + p.minutes,
|
|
87
|
+
ss: (p) => D2(Math.trunc(p.seconds)),
|
|
88
|
+
s: (p) => '' + Math.trunc(p.seconds),
|
|
89
|
+
SSS: (p) => D3(Math.round((p.seconds - Math.trunc(p.seconds)) * 1000)),
|
|
90
|
+
S: (p) => '' + Math.trunc((p.seconds - Math.trunc(p.seconds)) * 10),
|
|
91
|
+
a: (p, n) => n.meridiem[p.hours < 12 ? 0 : 1],
|
|
92
|
+
XXX: (p) => offsetText(p.offset, true, true),
|
|
93
|
+
XX: (p) => offsetText(p.offset, false, true),
|
|
94
|
+
X: (p) => offsetText(p.offset, true, false),
|
|
95
|
+
DDD: (p) => D3(dayOfYear(p)),
|
|
96
|
+
D: (p) => '' + dayOfYear(p),
|
|
97
|
+
ww: (p) => D2(isoWeekOfYear(p).week),
|
|
98
|
+
w: (p) => '' + isoWeekOfYear(p).week,
|
|
99
|
+
Q: (p) => '' + quarterOfYear(p),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// tokens that need a names provider, and which array each one reads
|
|
103
|
+
const NEEDS_NAMES = Object.freeze({
|
|
104
|
+
MMMM: 'months', MMM: 'monthsShort',
|
|
105
|
+
EEEE: 'weekdays', EEE: 'weekdaysShort',
|
|
106
|
+
a: 'meridiem',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const MAX_TOKEN = 4;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Compile an LDML date pattern into a formatter.
|
|
113
|
+
*
|
|
114
|
+
* The returned function takes a parts record (`parseRFC3339Parts`) and
|
|
115
|
+
* returns a string. It closes over nothing mutable and is reusable.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} pattern - an LDML pattern, e.g. `"yyyy-MM-dd'T'HH:mm:ssXXX"`
|
|
118
|
+
* @param {DateNames} [names] - locale names, required only if the
|
|
119
|
+
* pattern uses `MMM`/`MMMM`/`EEE`/`EEEE`/`a`
|
|
120
|
+
* @returns {(parts: object) => string} the compiled formatter
|
|
121
|
+
* @throws {TypeError} on an unterminated quote, or a name token with no
|
|
122
|
+
* provider for it
|
|
123
|
+
* @example
|
|
124
|
+
* const iso = compileDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
125
|
+
* iso(parseRFC3339Parts('2026-07-27T14:30:05+02:00'));
|
|
126
|
+
* // '2026-07-27T14:30:05+02:00'
|
|
127
|
+
*/
|
|
128
|
+
export function compileDateFormat(pattern, names = undefined) {
|
|
129
|
+
if (typeof pattern !== 'string')
|
|
130
|
+
throw new TypeError('a date pattern must be a string');
|
|
131
|
+
const steps = [];
|
|
132
|
+
let literal = '';
|
|
133
|
+
const flushLiteral = () => {
|
|
134
|
+
if (literal !== '') {
|
|
135
|
+
const text = literal;
|
|
136
|
+
steps.push(() => text);
|
|
137
|
+
literal = '';
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < pattern.length;) {
|
|
142
|
+
const ch = pattern[i];
|
|
143
|
+
if (ch === "'") { // quoted literal, '' is one quote
|
|
144
|
+
if (pattern[i + 1] === "'") {
|
|
145
|
+
literal += "'";
|
|
146
|
+
i += 2;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const end = pattern.indexOf("'", i + 1);
|
|
150
|
+
if (end < 0)
|
|
151
|
+
throw new TypeError(`unterminated quoted literal in date pattern '${pattern}'`);
|
|
152
|
+
literal += pattern.slice(i + 1, end);
|
|
153
|
+
i = end + 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
let matched = null;
|
|
157
|
+
for (let len = MAX_TOKEN; len >= 1; len--) {
|
|
158
|
+
const candidate = pattern.slice(i, i + len);
|
|
159
|
+
if (candidate.length === len && TOKENS[candidate] !== undefined) {
|
|
160
|
+
matched = candidate;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (matched === null) {
|
|
165
|
+
literal += ch;
|
|
166
|
+
i += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const need = NEEDS_NAMES[matched];
|
|
170
|
+
if (need !== undefined && (names === undefined || names[need] === undefined)) {
|
|
171
|
+
throw new TypeError(`the '${matched}' token needs a '${need}' names provider`
|
|
172
|
+
+ ' (@jarenjs/core/dates is locale-free by design)');
|
|
173
|
+
}
|
|
174
|
+
flushLiteral();
|
|
175
|
+
const fn = TOKENS[matched];
|
|
176
|
+
steps.push(need === undefined ? fn : (p) => fn(p, names));
|
|
177
|
+
i += matched.length;
|
|
178
|
+
}
|
|
179
|
+
flushLiteral();
|
|
180
|
+
|
|
181
|
+
const count = steps.length;
|
|
182
|
+
if (count === 0)
|
|
183
|
+
return () => '';
|
|
184
|
+
if (count === 1)
|
|
185
|
+
return steps[0];
|
|
186
|
+
return (parts) => {
|
|
187
|
+
let out = '';
|
|
188
|
+
for (let i = 0; i < count; i++)
|
|
189
|
+
out += steps[i](parts);
|
|
190
|
+
return out;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// the two canonical shapes, compiled once at module load
|
|
195
|
+
const FORMAT_DATE = compileDateFormat('yyyy-MM-dd');
|
|
196
|
+
const FORMAT_DATE_TIME = compileDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
197
|
+
const FORMAT_TIME = compileDateFormat('HH:mm:ssXXX');
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Render a parts record back to RFC 3339, the inverse of
|
|
201
|
+
* `parseRFC3339Parts`. The value keeps the shape it had: a record with
|
|
202
|
+
* no time half comes back a `full-date`, one with no date half a
|
|
203
|
+
* `full-time`, and the offset is the record's own rather than UTC.
|
|
204
|
+
*
|
|
205
|
+
* The round trip preserves the *value*, not necessarily the spelling: a
|
|
206
|
+
* fractional second is emitted only when non-zero and without trailing
|
|
207
|
+
* zeros, so `…:05.250Z` comes back `…:05.25Z`. The parts record holds
|
|
208
|
+
* the fraction as a number, so the original digit count is not
|
|
209
|
+
* recoverable — a consumer that must reproduce the input byte for byte
|
|
210
|
+
* (JOSL, which round-trips TOML) has to keep the literal text itself.
|
|
211
|
+
*
|
|
212
|
+
* @param {object} parts - a parts record
|
|
213
|
+
* @returns {string} an RFC 3339 string
|
|
214
|
+
* @example
|
|
215
|
+
* formatRFC3339Parts(parseRFC3339Parts('2026-07-27')); // '2026-07-27'
|
|
216
|
+
*/
|
|
217
|
+
export function formatRFC3339Parts(parts) {
|
|
218
|
+
if (parts.hours < 0)
|
|
219
|
+
return FORMAT_DATE(parts);
|
|
220
|
+
const fraction = parts.seconds - Math.trunc(parts.seconds);
|
|
221
|
+
const base = parts.year < 0 ? FORMAT_TIME(parts) : FORMAT_DATE_TIME(parts);
|
|
222
|
+
if (fraction === 0)
|
|
223
|
+
return base;
|
|
224
|
+
// splice the fraction in after the seconds, before the offset
|
|
225
|
+
const digits = fraction.toFixed(3).slice(1).replace(/0+$/, '');
|
|
226
|
+
const cut = base.length - (FORMAT_OFFSET_LEN(parts));
|
|
227
|
+
return base.slice(0, cut) + digits + base.slice(cut);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// how many characters the rendered offset takes, so the fraction can be
|
|
231
|
+
// spliced in front of it
|
|
232
|
+
function FORMAT_OFFSET_LEN(parts) {
|
|
233
|
+
if (parts.offset === null || parts.offset === undefined)
|
|
234
|
+
return 0;
|
|
235
|
+
return parts.offset === 0 ? 1 : 6;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
//#region @jarenjs/core/dates
|
|
4
|
+
// The suite's date kernel. Dates are not a type here: they are the two
|
|
5
|
+
// forms JSON already has - an RFC 3339 **string** (lexical, what
|
|
6
|
+
// documents, schemas, forms and TOML actually contain) and **epoch
|
|
7
|
+
// milliseconds** (arithmetic, what a chart plots). A wrapper object,
|
|
8
|
+
// even an immutable one, could not be a query-engine item, a JSON Patch
|
|
9
|
+
// target or part of app state, which is why `canonicalizeJson` turns a
|
|
10
|
+
// `Date` into `{}`.
|
|
11
|
+
//
|
|
12
|
+
// The parts record produced by `parseRFC3339Parts` is the intermediate
|
|
13
|
+
// the calendar functions work on; it is deliberately plain data and is
|
|
14
|
+
// never handed to a consumer as an opaque handle.
|
|
15
|
+
//
|
|
16
|
+
// rfc3339.js validation, lexical decomposition, epoch conversion
|
|
17
|
+
// civil.js proleptic Gregorian arithmetic over integers
|
|
18
|
+
// format.js LDML pattern -> compiled formatter
|
|
19
|
+
// duration.js ISO 8601 duration decomposition and conversion
|
|
20
|
+
//
|
|
21
|
+
// Locale-dependent presentation (month and weekday names, relative
|
|
22
|
+
// phrasing) is NOT here: it belongs to @jarenjs/locales, so this module
|
|
23
|
+
// stays zero-dependency and free of data that would drift per language.
|
|
24
|
+
|
|
25
|
+
export * from './rfc3339.js';
|
|
26
|
+
export * from './civil.js';
|
|
27
|
+
export * from './format.js';
|
|
28
|
+
export * from './duration.js';
|
|
29
|
+
|
|
30
|
+
//#endregion
|