@jarenjs/core 0.49.2 → 0.66.1
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 +52 -9
- package/README.md +109 -1
- package/dist/types/async.d.ts +142 -0
- package/dist/types/dates/format.d.ts +2 -1
- package/dist/types/object.d.ts +4 -3
- package/dist/types/random.d.ts +80 -0
- package/dist/types/runtime.d.ts +89 -0
- package/dist/types/series/index.d.ts +7 -0
- package/dist/types/stats.d.ts +73 -0
- package/dist/types/string.d.ts +13 -0
- package/docs/DATES.md +3 -1
- package/docs/GEO.md +2 -2
- package/docs/SERIES.md +28 -13
- package/package.json +17 -1
- package/src/async.js +285 -0
- package/src/dates/format.js +15 -5
- package/src/dates/rfc3339.js +5 -7
- package/src/object.js +15 -13
- package/src/random.js +125 -0
- package/src/runtime.js +130 -0
- package/src/series/index.js +7 -0
- package/src/stats.js +133 -0
- package/src/string.js +31 -0
package/src/object.js
CHANGED
|
@@ -14,8 +14,9 @@ const hasOwn = Object.hasOwn;
|
|
|
14
14
|
* Deep equality comparison for arbitrary values.
|
|
15
15
|
*
|
|
16
16
|
* Generic JavaScript equality: understands Maps, Sets, RegExps,
|
|
17
|
-
* functions, typed arrays and class instances (constructors
|
|
18
|
-
* match). Not the same as
|
|
17
|
+
* functions, typed arrays and class instances (prototype constructors
|
|
18
|
+
* must match; an own `constructor` member is data). Not the same as
|
|
19
|
+
* `equalsJson`, which compares JSON values
|
|
19
20
|
* only and is the hot-path variant — keep both.
|
|
20
21
|
* @param {any} target
|
|
21
22
|
* @param {any} source
|
|
@@ -34,23 +35,24 @@ export function equalsDeep(target, source) {
|
|
|
34
35
|
if (isScalarType(target))
|
|
35
36
|
return false;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
const constructor = Object.getPrototypeOf(target)?.constructor;
|
|
39
|
+
if (constructor !== Object.getPrototypeOf(source)?.constructor)
|
|
38
40
|
return false;
|
|
39
41
|
|
|
40
|
-
if (
|
|
42
|
+
if (constructor === Object) {
|
|
41
43
|
const tks = Object.keys(target);
|
|
42
44
|
const sks = Object.keys(source);
|
|
43
45
|
if (tks.length !== sks.length)
|
|
44
46
|
return false;
|
|
45
47
|
for (let i = 0; i < tks.length; ++i) {
|
|
46
48
|
const key = tks[i];
|
|
47
|
-
if (!equalsDeep(target[key], source[key]))
|
|
49
|
+
if (!hasOwn(source, key) || !equalsDeep(target[key], source[key]))
|
|
48
50
|
return false;
|
|
49
51
|
}
|
|
50
52
|
return true;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
if (
|
|
55
|
+
if (constructor === Map) {
|
|
54
56
|
if (target.size !== source.size)
|
|
55
57
|
return false;
|
|
56
58
|
for (const [key, value] of target) {
|
|
@@ -62,7 +64,7 @@ export function equalsDeep(target, source) {
|
|
|
62
64
|
return true;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
if (
|
|
67
|
+
if (constructor === Array) {
|
|
66
68
|
if (target.length !== source.length)
|
|
67
69
|
return false;
|
|
68
70
|
for (let i = 0; i < target.length; ++i) {
|
|
@@ -72,7 +74,7 @@ export function equalsDeep(target, source) {
|
|
|
72
74
|
return true;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
if (
|
|
77
|
+
if (constructor === Set) {
|
|
76
78
|
if (target.size !== source.size)
|
|
77
79
|
return false;
|
|
78
80
|
for (const value of target) {
|
|
@@ -82,7 +84,7 @@ export function equalsDeep(target, source) {
|
|
|
82
84
|
return true;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
if (
|
|
87
|
+
if (constructor === RegExp) {
|
|
86
88
|
return target.toString() === source.toString();
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -104,7 +106,7 @@ export function equalsDeep(target, source) {
|
|
|
104
106
|
if (tkeys.length === 0) return true;
|
|
105
107
|
for (let i = 0; i < tkeys.length; ++i) {
|
|
106
108
|
const key = tkeys[i];
|
|
107
|
-
if (!equalsDeep(target[key], source[key]))
|
|
109
|
+
if (!hasOwn(source, key) || !equalsDeep(target[key], source[key]))
|
|
108
110
|
return false;
|
|
109
111
|
}
|
|
110
112
|
return true;
|
|
@@ -375,7 +377,7 @@ function semanticToken(value, path, open) {
|
|
|
375
377
|
// an own property beyond the elements would vanish positionally
|
|
376
378
|
for (const key of Object.keys(items)) {
|
|
377
379
|
const index = Number(key);
|
|
378
|
-
if (!Number.isInteger(index) || index < 0 || index >= items.length)
|
|
380
|
+
if (!Number.isInteger(index) || index < 0 || index >= items.length || String(index) !== key)
|
|
379
381
|
refuse(`the extra array property ${JSON.stringify(key)}`);
|
|
380
382
|
}
|
|
381
383
|
out = '[';
|
|
@@ -422,7 +424,7 @@ function semanticToken(value, path, open) {
|
|
|
422
424
|
* functions, symbols, cycles, and non-plain objects (a `Date`, `Map`,
|
|
423
425
|
* `RegExp` or class instance, all of which serialize to `{}`), plus the
|
|
424
426
|
* two members a serialization cannot show — a symbol key, and an own
|
|
425
|
-
* array property
|
|
427
|
+
* array property that is not an element index. A caller that may hold such a
|
|
426
428
|
* value must treat the refusal as "not cacheable" and compute afresh —
|
|
427
429
|
* never as "reuse whatever shares the key".
|
|
428
430
|
*
|
|
@@ -634,4 +636,4 @@ export function mergeSet(set, ...iterables) {
|
|
|
634
636
|
set.add(item);
|
|
635
637
|
}
|
|
636
638
|
}
|
|
637
|
-
}
|
|
639
|
+
}
|
package/src/random.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The suite's one seeded generator, and the three draws built on
|
|
4
|
+
* it. Every seeded corpus, oracle and property test in this repository
|
|
5
|
+
* needs the same thing: a stream of numbers that is identical on every
|
|
6
|
+
* host for a given seed, so that a benchmark can state a delta and a
|
|
7
|
+
* failing property test can be replayed. Before this file that stream
|
|
8
|
+
* was written ten times; a generator that exists once is one whose
|
|
9
|
+
* sequence can be pinned once.
|
|
10
|
+
*
|
|
11
|
+
* The algorithm is mulberry32 — a 32-bit state, one multiply-xorshift
|
|
12
|
+
* round per draw, a period of 2^32. It is named by its algorithm rather
|
|
13
|
+
* than by its role because the SEQUENCE is the contract: a corpus
|
|
14
|
+
* generated from seed 20260825 must regenerate byte-for-byte, and a
|
|
15
|
+
* "better" generator under the same name would silently change every
|
|
16
|
+
* fixture that trusts it. A second algorithm gets a second name.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here is cryptographic, and nothing here reads `Math.random`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A seeded generator: uniform in `[0, 1)`, identical on every host for
|
|
23
|
+
* the same seed.
|
|
24
|
+
*
|
|
25
|
+
* The seed is taken as an unsigned 32-bit integer (`seed >>> 0`, the
|
|
26
|
+
* ToUint32 conversion): `1.5` seeds as `1`, `-1` as `4294967295`,
|
|
27
|
+
* `2^32 + 5` as `5`, and `NaN` as `0`. Two seeds that agree modulo 2^32
|
|
28
|
+
* are one stream — say so wherever a seed is published.
|
|
29
|
+
* @param {number} seed
|
|
30
|
+
* @returns {() => number} the stream; each call is the next draw
|
|
31
|
+
*/
|
|
32
|
+
export function mulberry32(seed) {
|
|
33
|
+
let a = seed >>> 0;
|
|
34
|
+
return function random() {
|
|
35
|
+
a = (a + 0x6D2B79F5) >>> 0;
|
|
36
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
37
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
38
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {number} value
|
|
44
|
+
* @param {string} what
|
|
45
|
+
*/
|
|
46
|
+
function assertInteger(value, what) {
|
|
47
|
+
if (!Number.isInteger(value))
|
|
48
|
+
throw new RangeError(`${what} must be an integer, got ${String(value)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One integer draw over the half-open range `[min, max)`: `min +
|
|
53
|
+
* floor(random() * (max - min))`.
|
|
54
|
+
*
|
|
55
|
+
* This is the floor draw and deliberately so. Every committed corpus in
|
|
56
|
+
* the suite was generated with exactly this arithmetic, and a generator
|
|
57
|
+
* whose integer draw changed would regenerate every one of them
|
|
58
|
+
* differently. It is uniform up to a bias bounded by `(max - min) /
|
|
59
|
+
* 2^32` — under one part in a million for a span of four thousand, and
|
|
60
|
+
* far below anything a benchmark row can resolve. A draw that rejects
|
|
61
|
+
* to remove even that bias would be a different function under a
|
|
62
|
+
* different name, not a change to this one.
|
|
63
|
+
* @param {() => number} random - the stream, from {@link mulberry32}
|
|
64
|
+
* @param {number} min - inclusive integer lower bound
|
|
65
|
+
* @param {number} max - exclusive integer upper bound; must exceed `min`
|
|
66
|
+
* @returns {number} an integer in `[min, max)`
|
|
67
|
+
* @throws {RangeError} when a bound is not an integer or `max <= min`
|
|
68
|
+
*/
|
|
69
|
+
export function randomInt(random, min, max) {
|
|
70
|
+
assertInteger(min, 'min');
|
|
71
|
+
assertInteger(max, 'max');
|
|
72
|
+
if (max <= min) throw new RangeError(`randomInt needs max > min, got [${min}, ${max})`);
|
|
73
|
+
return min + Math.floor(random() * (max - min));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fisher–Yates, in place, from the given stream: for `i` from the last
|
|
78
|
+
* index down to 1, swap `i` with a uniform `j` in `[0, i]`. Returns the
|
|
79
|
+
* same array. An empty or one-element list draws nothing.
|
|
80
|
+
*
|
|
81
|
+
* The descending form is the one the seeded corpora were generated
|
|
82
|
+
* with; the ascending form is a different permutation of the same
|
|
83
|
+
* stream and must not be substituted.
|
|
84
|
+
* @template T
|
|
85
|
+
* @param {() => number} random - the stream, from {@link mulberry32}
|
|
86
|
+
* @param {T[]} list - reordered in place
|
|
87
|
+
* @returns {T[]} `list`
|
|
88
|
+
*/
|
|
89
|
+
export function shuffle(random, list) {
|
|
90
|
+
for (let i = list.length - 1; i > 0; i--) {
|
|
91
|
+
const j = Math.floor(random() * (i + 1));
|
|
92
|
+
[list[i], list[j]] = [list[j], list[i]];
|
|
93
|
+
}
|
|
94
|
+
return list;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `k` distinct indices from `[0, n)`, uniformly, as a partial forward
|
|
99
|
+
* Fisher–Yates over a fresh index pool: the first `k` positions of a
|
|
100
|
+
* shuffle, without paying for the rest. Asked for more than `n` it
|
|
101
|
+
* answers `n` — a draw cannot invent a member the population does not
|
|
102
|
+
* hold; asked for nothing, or from nothing, it answers `[]`.
|
|
103
|
+
*
|
|
104
|
+
* The stream is the caller's, so one stream can serve many draws
|
|
105
|
+
* (a policy that draws per question from one seeded closure stays
|
|
106
|
+
* reproducible across the whole run).
|
|
107
|
+
* @param {() => number} random - the stream, from {@link mulberry32}
|
|
108
|
+
* @param {number} n - the population size (a non-negative integer)
|
|
109
|
+
* @param {number} k - how many to draw (a non-negative integer)
|
|
110
|
+
* @returns {number[]} `min(k, n)` distinct indices, in draw order
|
|
111
|
+
* @throws {RangeError} when `n` or `k` is not a non-negative integer
|
|
112
|
+
*/
|
|
113
|
+
export function drawDistinct(random, n, k) {
|
|
114
|
+
assertInteger(n, 'n');
|
|
115
|
+
assertInteger(k, 'k');
|
|
116
|
+
if (n < 0 || k < 0) throw new RangeError(`drawDistinct needs n >= 0 and k >= 0, got n=${n} k=${k}`);
|
|
117
|
+
const count = Math.min(k, n);
|
|
118
|
+
if (count === 0) return [];
|
|
119
|
+
const pool = Array.from({ length: n }, (_, i) => i);
|
|
120
|
+
for (let i = 0; i < count; i++) {
|
|
121
|
+
const j = i + Math.floor(random() * (pool.length - i));
|
|
122
|
+
[pool[i], pool[j]] = [pool[j], pool[i]];
|
|
123
|
+
}
|
|
124
|
+
return pool.slice(0, count);
|
|
125
|
+
}
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The runtime record: the host facts every subsystem that needs
|
|
4
|
+
* one used to take separately — the clock, secure identifiers,
|
|
5
|
+
* randomness and the zone provider — as one frozen record a host builds
|
|
6
|
+
* once and hands to the store, the jobs engine, the migration runner and
|
|
7
|
+
* the http binding. Nothing here IS a clock, a random source or a zone
|
|
8
|
+
* database: the defaults are the platform's own (`Date.now`,
|
|
9
|
+
* `crypto.randomUUID`, `Math.random`, and no zone provider, which keeps
|
|
10
|
+
* a named zone the refusal it always was), so adopting the record changes
|
|
11
|
+
* nothing a consumer can observe. What it buys is that a deterministic
|
|
12
|
+
* run — a fixed clock, a seeded generator, a counting identifier — is
|
|
13
|
+
* configured in one place, and that four subsystems can no longer
|
|
14
|
+
* disagree about what time it is.
|
|
15
|
+
*
|
|
16
|
+
* Precedence is fixed: a subsystem's own explicit option wins over the
|
|
17
|
+
* record's member, which wins over the built-in default. The options are
|
|
18
|
+
* published surface, and a record that silently overrode them would be a
|
|
19
|
+
* breaking change dressed as an ergonomic.
|
|
20
|
+
*
|
|
21
|
+
* The record reaches hosts, never query compilation. A compiled query is
|
|
22
|
+
* cached by document identity and saved as a rule, so the current instant
|
|
23
|
+
* enters it as data — an external — and there is no `now` operator for
|
|
24
|
+
* this record to feed.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The record every host-facing subsystem takes as `runtime`.
|
|
29
|
+
* @typedef {Object} Runtime
|
|
30
|
+
* @property {() => number} now - the clock, in epoch milliseconds
|
|
31
|
+
* @property {() => string} uuid - a fresh identifier; secure by default
|
|
32
|
+
* @property {() => number} random - uniform in `[0, 1)`
|
|
33
|
+
* @property {import('./series/zone.js').ZoneProvider | null} zoneProvider
|
|
34
|
+
* - the tzdb a named zone is read through, or `null`: a named zone is
|
|
35
|
+
* then a refusal, never a quiet UTC
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The four members a runtime record carries, so a subsystem, a test and
|
|
40
|
+
* a document all spell the host facts the same way.
|
|
41
|
+
*/
|
|
42
|
+
export const RUNTIME_MEMBERS = Object.freeze(['now', 'uuid', 'random', 'zoneProvider']);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The platform's own answers — exactly what every subsystem fell back
|
|
46
|
+
* to before the record existed, member for member, so a run that never
|
|
47
|
+
* builds a record behaves as it always did.
|
|
48
|
+
* @type {Readonly<Runtime>}
|
|
49
|
+
*/
|
|
50
|
+
const DEFAULT_RUNTIME = Object.freeze({
|
|
51
|
+
now: Date.now,
|
|
52
|
+
// `randomUUID` is a method of the platform's Crypto object and refuses
|
|
53
|
+
// to run unbound, so the default is a call rather than a reference
|
|
54
|
+
uuid: () => globalThis.crypto.randomUUID(),
|
|
55
|
+
random: Math.random,
|
|
56
|
+
zoneProvider: null,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether a value is the pair of functions the temporal kernel's clock
|
|
61
|
+
* seam takes for a named zone.
|
|
62
|
+
* @param {any} value
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function isZoneProvider(value) {
|
|
66
|
+
return value !== null && typeof value === 'object'
|
|
67
|
+
&& typeof value.toParts === 'function' && typeof value.toEpoch === 'function';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a runtime record: the platform defaults, with any member
|
|
72
|
+
* overridden. The result is frozen, so a subsystem that was handed one
|
|
73
|
+
* can hand it on without a copy.
|
|
74
|
+
*
|
|
75
|
+
* The record is closed: a member it does not have is a refusal naming
|
|
76
|
+
* the four it does, because `clock` for `now` quietly ignored would be a
|
|
77
|
+
* deterministic run that is not.
|
|
78
|
+
*
|
|
79
|
+
* @param {Partial<Runtime>} [overrides]
|
|
80
|
+
* @returns {Readonly<Runtime>}
|
|
81
|
+
* @throws {TypeError} for a member that is not a function, a
|
|
82
|
+
* `zoneProvider` that is neither `null` nor a provider, or a member
|
|
83
|
+
* the record does not have
|
|
84
|
+
* @example
|
|
85
|
+
* createRuntime(); // the platform's own
|
|
86
|
+
* createRuntime({ now: () => 1_700_000_000_000 }); // a fixed clock, the rest default
|
|
87
|
+
* createRuntime({ uuid: () => `id-${++n}`, random: mulberry32(1), zoneProvider });
|
|
88
|
+
*/
|
|
89
|
+
export function createRuntime(overrides = undefined) {
|
|
90
|
+
if (overrides === undefined || overrides === null)
|
|
91
|
+
return DEFAULT_RUNTIME;
|
|
92
|
+
if (typeof overrides !== 'object')
|
|
93
|
+
throw new TypeError('a runtime record is an object');
|
|
94
|
+
/** @type {any} */
|
|
95
|
+
const record = { ...DEFAULT_RUNTIME };
|
|
96
|
+
for (const key of Object.keys(overrides)) {
|
|
97
|
+
if (!RUNTIME_MEMBERS.includes(key)) {
|
|
98
|
+
throw new TypeError(`a runtime record has ${
|
|
99
|
+
RUNTIME_MEMBERS.map((m) => `'${m}'`).join(', ')}, not '${key}'`);
|
|
100
|
+
}
|
|
101
|
+
const value = /** @type {any} */ (overrides)[key];
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
if (key === 'zoneProvider') {
|
|
105
|
+
if (value !== null && !isZoneProvider(value)) {
|
|
106
|
+
throw new TypeError('runtime.zoneProvider is null or a provider with'
|
|
107
|
+
+ ' toParts(epoch, zone) and toEpoch(parts, zone, disambiguation)');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (typeof value !== 'function') {
|
|
111
|
+
throw new TypeError(`runtime.${key} is a function`);
|
|
112
|
+
}
|
|
113
|
+
record[key] = value;
|
|
114
|
+
}
|
|
115
|
+
return Object.freeze(record);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The record a subsystem reads its `runtime` option through: nothing
|
|
120
|
+
* given is the platform default, and anything given is validated and
|
|
121
|
+
* frozen — so every member a subsystem reads is a function, and a
|
|
122
|
+
* malformed record is refused where it was passed rather than where it
|
|
123
|
+
* was first called.
|
|
124
|
+
* @param {Partial<Runtime> | undefined | null} [candidate] - a subsystem's `options.runtime`
|
|
125
|
+
* @returns {Readonly<Runtime>}
|
|
126
|
+
* @throws {TypeError} as `createRuntime` does
|
|
127
|
+
*/
|
|
128
|
+
export function resolveRuntime(candidate = undefined) {
|
|
129
|
+
return candidate === DEFAULT_RUNTIME ? DEFAULT_RUNTIME : createRuntime(candidate);
|
|
130
|
+
}
|
package/src/series/index.js
CHANGED
|
@@ -75,6 +75,13 @@ export { createIntervalIndex } from './interval-index.js';
|
|
|
75
75
|
|
|
76
76
|
export { resolveClock, CLOCK_MEMBERS } from './zone.js';
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The wall clock a caller supplies for a named zone - the shape a host
|
|
80
|
+
* passes as `provider`, named here so it can be spelled where the seam
|
|
81
|
+
* is crossed.
|
|
82
|
+
* @typedef {import('./zone.js').ZoneProvider} ZoneProvider
|
|
83
|
+
*/
|
|
84
|
+
|
|
78
85
|
export { compileBuckets, resampleSeries, RESAMPLE_MEMBERS } from './bucket.js';
|
|
79
86
|
|
|
80
87
|
export { rollingSeries, ROLLING_MEMBERS } from './rolling.js';
|
package/src/stats.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Descriptive statistics over a sample of numbers: the mean, the
|
|
4
|
+
* sample variance and its root, the median, and a quantile that will
|
|
5
|
+
* not answer until told which quantile it is being asked for.
|
|
6
|
+
*
|
|
7
|
+
* Before this file the suite computed these in two places with two
|
|
8
|
+
* quantile rules — linear interpolation in the query layer's statistics
|
|
9
|
+
* pack, nearest rank in the benchmark harness — and each was the right
|
|
10
|
+
* rule for its consumer: an interpolated percentile is what an analyst
|
|
11
|
+
* expects of `$percentile`, while a benchmark row of eleven readings
|
|
12
|
+
* should not publish a latency nobody measured. Both rules stay; the
|
|
13
|
+
* definitions move here so that there is one of each, and `quantile`
|
|
14
|
+
* makes the caller name its method, because "the 95th percentile" of a
|
|
15
|
+
* small sample is a different number under every one of the seven
|
|
16
|
+
* common definitions and a default would decide silently.
|
|
17
|
+
*
|
|
18
|
+
* Every function answers `undefined` for a sample it cannot summarize —
|
|
19
|
+
* an empty one, or fewer than two values for a variance — rather than
|
|
20
|
+
* `NaN` or `0`: a number that was not measured must not format as one.
|
|
21
|
+
* The caller's array is never reordered; a quantile sorts a copy.
|
|
22
|
+
* Values are numbers by contract and are not checked one by one.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The arithmetic mean.
|
|
27
|
+
* @param {readonly number[]} values
|
|
28
|
+
* @returns {number | undefined} `undefined` for an empty sample
|
|
29
|
+
*/
|
|
30
|
+
export function mean(values) {
|
|
31
|
+
if (values.length === 0) return undefined;
|
|
32
|
+
let sum = 0;
|
|
33
|
+
for (const value of values) sum += value;
|
|
34
|
+
return sum / values.length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The SAMPLE variance, with Bessel's correction (`n − 1`): the sample is
|
|
39
|
+
* taken as drawn from a population it did not enumerate, which is what a
|
|
40
|
+
* benchmark's rounds and a query's rows both are.
|
|
41
|
+
* @param {readonly number[]} values
|
|
42
|
+
* @returns {number | undefined} `undefined` for fewer than two values
|
|
43
|
+
*/
|
|
44
|
+
export function variance(values) {
|
|
45
|
+
if (values.length < 2) return undefined;
|
|
46
|
+
const m = /** @type {number} */ (mean(values));
|
|
47
|
+
let sum = 0;
|
|
48
|
+
for (const value of values) sum += (value - m) * (value - m);
|
|
49
|
+
return sum / (values.length - 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The sample standard deviation — the root of {@link variance}.
|
|
54
|
+
* @param {readonly number[]} values
|
|
55
|
+
* @returns {number | undefined} `undefined` where the variance is
|
|
56
|
+
*/
|
|
57
|
+
export function stddev(values) {
|
|
58
|
+
const v = variance(values);
|
|
59
|
+
return v === undefined ? undefined : Math.sqrt(v);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {readonly number[]} values
|
|
64
|
+
* @returns {number[]} an ascending copy
|
|
65
|
+
*/
|
|
66
|
+
function ascending(values) {
|
|
67
|
+
return [...values].sort((a, b) => a - b);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The median: the middle value, or the mean of the two middle values
|
|
72
|
+
* when the sample has an even count.
|
|
73
|
+
*
|
|
74
|
+
* This is NOT `quantile(values, 0.5, …)` under either method, and on an
|
|
75
|
+
* even count the three disagree: for `[1, 2, 3, 4]` the median is `2.5`,
|
|
76
|
+
* the nearest-rank p50 is `2`, and the linear p50 is `2.5` only because
|
|
77
|
+
* that sample happens to be evenly spaced. A consumer publishing a "p50"
|
|
78
|
+
* beside a "p95" wants `quantile` with its method named; a consumer
|
|
79
|
+
* asking for the median wants this.
|
|
80
|
+
* @param {readonly number[]} values
|
|
81
|
+
* @returns {number | undefined} `undefined` for an empty sample
|
|
82
|
+
*/
|
|
83
|
+
export function median(values) {
|
|
84
|
+
if (values.length === 0) return undefined;
|
|
85
|
+
const sorted = ascending(values);
|
|
86
|
+
const mid = sorted.length >> 1;
|
|
87
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The quantile methods this module knows.
|
|
92
|
+
*
|
|
93
|
+
* - `'nearest-rank'`: the value at rank `max(1, ceil(p · n))` of the
|
|
94
|
+
* ascending sample — always a value that was measured, never one
|
|
95
|
+
* invented between two. The benchmark rule.
|
|
96
|
+
* - `'linear'`: rank `(n − 1) · p`, interpolated linearly between the
|
|
97
|
+
* values at `floor` and `ceil` of it (Hyndman–Fan type 7, the default
|
|
98
|
+
* of R, NumPy and spreadsheets). The analyst's rule.
|
|
99
|
+
* @typedef {'nearest-rank' | 'linear'} QuantileMethod
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/** @type {readonly QuantileMethod[]} */
|
|
103
|
+
const METHODS = ['nearest-rank', 'linear'];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The `p`-quantile of a sample, `p` on `[0, 1]`, under a NAMED method.
|
|
107
|
+
*
|
|
108
|
+
* The method is required, not defaulted: on a small sample the common
|
|
109
|
+
* definitions disagree by whole values, and a caller who did not say
|
|
110
|
+
* which one it wanted has published a number it cannot explain.
|
|
111
|
+
* @param {readonly number[]} values
|
|
112
|
+
* @param {number} p - the probability, `0` (the minimum) to `1` (the maximum)
|
|
113
|
+
* @param {{ method: QuantileMethod }} options
|
|
114
|
+
* @returns {number | undefined} `undefined` for an empty sample
|
|
115
|
+
* @throws {TypeError} when `method` is absent or not one of {@link QuantileMethod}
|
|
116
|
+
* @throws {RangeError} when `p` is not a number in `[0, 1]`
|
|
117
|
+
*/
|
|
118
|
+
export function quantile(values, p, options) {
|
|
119
|
+
const method = options?.method;
|
|
120
|
+
if (!METHODS.includes(/** @type {any} */ (method)))
|
|
121
|
+
throw new TypeError(`quantile needs { method: 'nearest-rank' | 'linear' }, got ${JSON.stringify(method)}`);
|
|
122
|
+
if (typeof p !== 'number' || !(p >= 0 && p <= 1))
|
|
123
|
+
throw new RangeError(`quantile needs p in [0, 1], got ${String(p)}`);
|
|
124
|
+
const n = values.length;
|
|
125
|
+
if (n === 0) return undefined;
|
|
126
|
+
const sorted = ascending(values);
|
|
127
|
+
if (method === 'nearest-rank') return sorted[Math.max(1, Math.ceil(p * n)) - 1];
|
|
128
|
+
const rank = (n - 1) * p;
|
|
129
|
+
const lo = Math.floor(rank);
|
|
130
|
+
const hi = Math.ceil(rank);
|
|
131
|
+
if (lo === hi) return sorted[lo];
|
|
132
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (rank - lo);
|
|
133
|
+
}
|
package/src/string.js
CHANGED
|
@@ -166,6 +166,37 @@ export function countCharCode(str, code, start = 0, end = str.length) {
|
|
|
166
166
|
return n;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* The UTF-8 byte length of a slice of a string — the one measure every
|
|
171
|
+
* byte bound in the suite counts (a body limit, a page, a change
|
|
172
|
+
* record, a parser's record or token limit) — computed without encoding
|
|
173
|
+
* a copy: one byte below U+0080, two below U+0800, four for a surrogate
|
|
174
|
+
* pair (one code point), three otherwise; a lone surrogate counts three,
|
|
175
|
+
* as its replacement would.
|
|
176
|
+
* @param {string} str
|
|
177
|
+
* @param {number} [start] - Inclusive start offset (defaults to 0)
|
|
178
|
+
* @param {number} [end] - Exclusive end offset (defaults to full length)
|
|
179
|
+
* @returns {number} The UTF-8 bytes of `str[start, end)`
|
|
180
|
+
*/
|
|
181
|
+
export function utf8ByteLength(str, start = 0, end = str.length) {
|
|
182
|
+
let bytes = 0;
|
|
183
|
+
for (let i = start; i < end; i++) {
|
|
184
|
+
const code = str.charCodeAt(i);
|
|
185
|
+
if (code < 0x80) bytes += 1;
|
|
186
|
+
else if (code < 0x800) bytes += 2;
|
|
187
|
+
else if (code >= 0xD800 && code <= 0xDBFF && i + 1 < end) {
|
|
188
|
+
const next = str.charCodeAt(i + 1);
|
|
189
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
190
|
+
bytes += 4;
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
else bytes += 3;
|
|
194
|
+
}
|
|
195
|
+
else bytes += 3;
|
|
196
|
+
}
|
|
197
|
+
return bytes;
|
|
198
|
+
}
|
|
199
|
+
|
|
169
200
|
/**
|
|
170
201
|
* Count the Unicode code points of a string (surrogate-pair aware;
|
|
171
202
|
* a lone surrogate counts as one code point).
|