@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/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 must
18
- * match). Not the same as `equalsJson`, which compares JSON values
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
- if (target.constructor !== source.constructor)
38
+ const constructor = Object.getPrototypeOf(target)?.constructor;
39
+ if (constructor !== Object.getPrototypeOf(source)?.constructor)
38
40
  return false;
39
41
 
40
- if (target.constructor === Object) {
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 (target.constructor === Map) {
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 (target.constructor === Array) {
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 (target.constructor === Set) {
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 (target.constructor === RegExp) {
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 past the last element. A caller that may hold such a
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
+ }
@@ -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).