@naturalcycles/js-lib 15.41.0 → 15.42.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/dist/array/range.js +0 -1
- package/dist/json-schema/jsonSchemaBuilder.d.ts +5 -3
- package/dist/json-schema/jsonSchemaBuilder.js +1 -1
- package/dist/object/index.d.ts +1 -0
- package/dist/object/index.js +1 -0
- package/dist/object/keySortedMap.d.ts +77 -0
- package/dist/object/keySortedMap.js +205 -0
- package/dist/object/object.util.js +1 -4
- package/package.json +1 -1
- package/src/array/range.ts +0 -1
- package/src/json-schema/jsonSchemaBuilder.ts +10 -6
- package/src/object/index.ts +1 -0
- package/src/object/keySortedMap.ts +234 -0
- package/src/object/object.util.ts +0 -2
package/dist/array/range.js
CHANGED
|
@@ -20,7 +20,6 @@ export function _range(fromIncl, toExcl, step = 1) {
|
|
|
20
20
|
* If it was an object - it'll paste the same object reference, which can create bugs.
|
|
21
21
|
*/
|
|
22
22
|
export function _arrayFilled(length, fill) {
|
|
23
|
-
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
24
23
|
return Array(length).fill(fill);
|
|
25
24
|
}
|
|
26
25
|
export function _rangeIterable(fromIncl, toExcl, step = 1) {
|
|
@@ -19,7 +19,7 @@ export declare const j: {
|
|
|
19
19
|
integer<T extends number = number>(): JsonSchemaNumberBuilder<T, false>;
|
|
20
20
|
string<T extends string = string>(): JsonSchemaStringBuilder<T, false>;
|
|
21
21
|
object: typeof object;
|
|
22
|
-
dbEntity<
|
|
22
|
+
dbEntity<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(props: P): JsonSchemaObjectBuilder<BaseDBEntity & { [K in keyof P]: P[K] extends JsonSchemaAnyBuilder<infer U, any, any> ? U : never; }>;
|
|
23
23
|
rootObject<T extends AnyObject>(props: { [K in keyof T]: JsonSchemaAnyBuilder<T[K]>; }): JsonSchemaObjectBuilder<T, false>;
|
|
24
24
|
array<T extends JsonSchemaAnyBuilder<any>>(itemSchema: T): JsonSchemaArrayBuilder<T["infer"], false>;
|
|
25
25
|
tuple<T extends any[] = unknown[]>(items: JsonSchemaAnyBuilder[]): JsonSchemaTupleBuilder<T>;
|
|
@@ -139,12 +139,14 @@ export declare class JsonSchemaArrayBuilder<ITEM, Opt extends boolean = false> e
|
|
|
139
139
|
constructor(itemsSchema: JsonSchemaBuilder<ITEM>);
|
|
140
140
|
min(minItems: number): this;
|
|
141
141
|
max(maxItems: number): this;
|
|
142
|
-
unique(uniqueItems
|
|
142
|
+
unique(uniqueItems?: boolean): this;
|
|
143
143
|
}
|
|
144
144
|
export declare class JsonSchemaTupleBuilder<T extends any[]> extends JsonSchemaAnyBuilder<T, JsonSchemaTuple<T>> {
|
|
145
145
|
constructor(items: JsonSchemaBuilder[]);
|
|
146
146
|
}
|
|
147
|
-
declare function object<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(props?:
|
|
147
|
+
declare function object<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(props?: {
|
|
148
|
+
[K in keyof P]: P[K] & JsonSchemaAnyBuilder<any, any, any>;
|
|
149
|
+
}): JsonSchemaObjectBuilder<{
|
|
148
150
|
[K in keyof P as P[K] extends JsonSchemaAnyBuilder<any, any, infer Opt> ? Opt extends true ? never : K : never]: P[K] extends JsonSchemaAnyBuilder<infer U, any, any> ? U : never;
|
|
149
151
|
} & {
|
|
150
152
|
[K in keyof P as P[K] extends JsonSchemaAnyBuilder<any, any, infer Opt> ? Opt extends true ? K : never : never]?: P[K] extends JsonSchemaAnyBuilder<infer U, any, any> ? U : never;
|
|
@@ -392,7 +392,7 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
|
|
|
392
392
|
Object.assign(this.schema, { maxItems });
|
|
393
393
|
return this;
|
|
394
394
|
}
|
|
395
|
-
unique(uniqueItems) {
|
|
395
|
+
unique(uniqueItems = true) {
|
|
396
396
|
Object.assign(this.schema, { uniqueItems });
|
|
397
397
|
return this;
|
|
398
398
|
}
|
package/dist/object/index.d.ts
CHANGED
package/dist/object/index.js
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface KeySortedMapOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Defaults to false.
|
|
4
|
+
* Set to true if your keys are numeric,
|
|
5
|
+
* so it would sort correctly.
|
|
6
|
+
*/
|
|
7
|
+
numericKeys?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Maintains sorted array of keys.
|
|
11
|
+
* Sorts **on insertion**, not on retrieval.
|
|
12
|
+
*
|
|
13
|
+
* - set(): O(log n) search + O(n) splice only when inserting a NEW key
|
|
14
|
+
* - get/has: O(1)
|
|
15
|
+
* - delete: O(log n) search + O(n) splice if present
|
|
16
|
+
* - iteration: O(n) over pre-sorted keys (no sorting at iteration time)
|
|
17
|
+
*
|
|
18
|
+
* @experimental
|
|
19
|
+
*/
|
|
20
|
+
export declare class KeySortedMap<K, V> implements Map<K, V> {
|
|
21
|
+
opt: KeySortedMapOptions;
|
|
22
|
+
private readonly map;
|
|
23
|
+
private readonly sortedKeys;
|
|
24
|
+
constructor(entries?: [K, V][], opt?: KeySortedMapOptions);
|
|
25
|
+
/**
|
|
26
|
+
* Convenience way to create KeySortedMap from object.
|
|
27
|
+
*/
|
|
28
|
+
static of<V>(obj: Record<any, V>): KeySortedMap<string, V>;
|
|
29
|
+
get size(): number;
|
|
30
|
+
clear(): void;
|
|
31
|
+
has(key: K): boolean;
|
|
32
|
+
get(key: K): V | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Allows to set multiple key-value pairs at once.
|
|
35
|
+
*/
|
|
36
|
+
setMany(obj: Record<any, V>): this;
|
|
37
|
+
/**
|
|
38
|
+
* Insert or update. Keeps keys array sorted at all times.
|
|
39
|
+
* Returns this (Map-like).
|
|
40
|
+
*/
|
|
41
|
+
set(key: K, value: V): this;
|
|
42
|
+
/**
|
|
43
|
+
* Delete by key. Returns boolean like Map.delete.
|
|
44
|
+
*/
|
|
45
|
+
delete(key: K): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Iterables (Map-compatible), all in sorted order.
|
|
48
|
+
*/
|
|
49
|
+
keys(): MapIterator<K>;
|
|
50
|
+
values(): MapIterator<V>;
|
|
51
|
+
entries(): MapIterator<[K, V]>;
|
|
52
|
+
[Symbol.iterator](): MapIterator<[K, V]>;
|
|
53
|
+
[Symbol.toStringTag]: string;
|
|
54
|
+
/**
|
|
55
|
+
* Zero-allocation callbacks over sorted data (faster than spreading to arrays).
|
|
56
|
+
*/
|
|
57
|
+
forEach(cb: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;
|
|
58
|
+
/**
|
|
59
|
+
* Convenience methods that MATERIALIZE arrays (if you really want arrays).
|
|
60
|
+
* These allocate; use iterators/forEach for maximum performance.
|
|
61
|
+
*/
|
|
62
|
+
keysArray(): K[];
|
|
63
|
+
valuesArray(): V[];
|
|
64
|
+
entriesArray(): [K, V][];
|
|
65
|
+
/** Fast helpers */
|
|
66
|
+
firstKey(): K | undefined;
|
|
67
|
+
lastKey(): K | undefined;
|
|
68
|
+
firstEntry(): [K, V] | undefined;
|
|
69
|
+
lastEntry(): [K, V] | undefined;
|
|
70
|
+
toJSON(): Record<string, V>;
|
|
71
|
+
toObject(): Record<string, V>;
|
|
72
|
+
/**
|
|
73
|
+
* lowerBound: first index i s.t. keys[i] >= target
|
|
74
|
+
*/
|
|
75
|
+
private lowerBound;
|
|
76
|
+
private sortKeys;
|
|
77
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maintains sorted array of keys.
|
|
3
|
+
* Sorts **on insertion**, not on retrieval.
|
|
4
|
+
*
|
|
5
|
+
* - set(): O(log n) search + O(n) splice only when inserting a NEW key
|
|
6
|
+
* - get/has: O(1)
|
|
7
|
+
* - delete: O(log n) search + O(n) splice if present
|
|
8
|
+
* - iteration: O(n) over pre-sorted keys (no sorting at iteration time)
|
|
9
|
+
*
|
|
10
|
+
* @experimental
|
|
11
|
+
*/
|
|
12
|
+
export class KeySortedMap {
|
|
13
|
+
opt;
|
|
14
|
+
map;
|
|
15
|
+
sortedKeys;
|
|
16
|
+
constructor(entries = [], opt = {}) {
|
|
17
|
+
this.opt = opt;
|
|
18
|
+
this.map = new Map(entries);
|
|
19
|
+
this.sortedKeys = [...this.map.keys()];
|
|
20
|
+
this.sortKeys();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Convenience way to create KeySortedMap from object.
|
|
24
|
+
*/
|
|
25
|
+
static of(obj) {
|
|
26
|
+
return new KeySortedMap(Object.entries(obj));
|
|
27
|
+
}
|
|
28
|
+
get size() {
|
|
29
|
+
return this.map.size;
|
|
30
|
+
}
|
|
31
|
+
clear() {
|
|
32
|
+
this.map.clear();
|
|
33
|
+
this.sortedKeys.length = 0;
|
|
34
|
+
}
|
|
35
|
+
has(key) {
|
|
36
|
+
return this.map.has(key);
|
|
37
|
+
}
|
|
38
|
+
get(key) {
|
|
39
|
+
return this.map.get(key);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Allows to set multiple key-value pairs at once.
|
|
43
|
+
*/
|
|
44
|
+
setMany(obj) {
|
|
45
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
46
|
+
this.map.set(k, v);
|
|
47
|
+
this.sortedKeys.push(k);
|
|
48
|
+
}
|
|
49
|
+
// Resort all at once
|
|
50
|
+
this.sortKeys();
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Insert or update. Keeps keys array sorted at all times.
|
|
55
|
+
* Returns this (Map-like).
|
|
56
|
+
*/
|
|
57
|
+
set(key, value) {
|
|
58
|
+
if (this.map.has(key)) {
|
|
59
|
+
// Update only; position unchanged.
|
|
60
|
+
this.map.set(key, value);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
// Find insertion index (lower_bound).
|
|
64
|
+
const i = this.lowerBound(key);
|
|
65
|
+
// Only insert into keys when actually new.
|
|
66
|
+
this.sortedKeys.splice(i, 0, key);
|
|
67
|
+
this.map.set(key, value);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Delete by key. Returns boolean like Map.delete.
|
|
72
|
+
*/
|
|
73
|
+
delete(key) {
|
|
74
|
+
if (!this.map.has(key))
|
|
75
|
+
return false;
|
|
76
|
+
this.map.delete(key);
|
|
77
|
+
// Remove from keys using binary search to avoid O(n) find.
|
|
78
|
+
const i = this.lowerBound(key);
|
|
79
|
+
// Because key existed, it must be at i.
|
|
80
|
+
if (i < this.sortedKeys.length && this.sortedKeys[i] === key) {
|
|
81
|
+
this.sortedKeys.splice(i, 1);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Extremely unlikely if external mutation happened; safe guard.
|
|
85
|
+
// Fall back to linear search (shouldn't happen).
|
|
86
|
+
const j = this.sortedKeys.indexOf(key);
|
|
87
|
+
if (j !== -1)
|
|
88
|
+
this.sortedKeys.splice(j, 1);
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Iterables (Map-compatible), all in sorted order.
|
|
94
|
+
*/
|
|
95
|
+
*keys() {
|
|
96
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
97
|
+
yield this.sortedKeys[i];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
*values() {
|
|
101
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
102
|
+
yield this.map.get(this.sortedKeys[i]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
*entries() {
|
|
106
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
107
|
+
const k = this.sortedKeys[i];
|
|
108
|
+
yield [k, this.map.get(k)];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
[Symbol.iterator]() {
|
|
112
|
+
return this.entries();
|
|
113
|
+
}
|
|
114
|
+
[Symbol.toStringTag] = 'KeySortedMap';
|
|
115
|
+
/**
|
|
116
|
+
* Zero-allocation callbacks over sorted data (faster than spreading to arrays).
|
|
117
|
+
*/
|
|
118
|
+
forEach(cb, thisArg) {
|
|
119
|
+
const m = this.map;
|
|
120
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
121
|
+
const k = this.sortedKeys[i];
|
|
122
|
+
cb.call(thisArg, m.get(k), k, this);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Convenience methods that MATERIALIZE arrays (if you really want arrays).
|
|
127
|
+
* These allocate; use iterators/forEach for maximum performance.
|
|
128
|
+
*/
|
|
129
|
+
keysArray() {
|
|
130
|
+
return this.sortedKeys.slice();
|
|
131
|
+
}
|
|
132
|
+
valuesArray() {
|
|
133
|
+
// oxlint-disable-next-line unicorn/no-new-array
|
|
134
|
+
const a = Array(this.sortedKeys.length);
|
|
135
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
136
|
+
a[i] = this.map.get(this.sortedKeys[i]);
|
|
137
|
+
}
|
|
138
|
+
return a;
|
|
139
|
+
}
|
|
140
|
+
entriesArray() {
|
|
141
|
+
// oxlint-disable-next-line unicorn/no-new-array
|
|
142
|
+
const out = Array(this.sortedKeys.length);
|
|
143
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
144
|
+
const k = this.sortedKeys[i];
|
|
145
|
+
out[i] = [k, this.map.get(k)];
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** Fast helpers */
|
|
150
|
+
firstKey() {
|
|
151
|
+
return this.sortedKeys[0];
|
|
152
|
+
}
|
|
153
|
+
lastKey() {
|
|
154
|
+
return this.sortedKeys.length ? this.sortedKeys[this.sortedKeys.length - 1] : undefined;
|
|
155
|
+
}
|
|
156
|
+
firstEntry() {
|
|
157
|
+
if (!this.sortedKeys.length)
|
|
158
|
+
return;
|
|
159
|
+
const k = this.sortedKeys[0];
|
|
160
|
+
return [k, this.map.get(k)];
|
|
161
|
+
}
|
|
162
|
+
lastEntry() {
|
|
163
|
+
if (!this.sortedKeys.length)
|
|
164
|
+
return;
|
|
165
|
+
const k = this.sortedKeys[this.sortedKeys.length - 1];
|
|
166
|
+
return [k, this.map.get(k)];
|
|
167
|
+
}
|
|
168
|
+
toJSON() {
|
|
169
|
+
return this.toObject();
|
|
170
|
+
}
|
|
171
|
+
toObject() {
|
|
172
|
+
return Object.fromEntries(this.map);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* lowerBound: first index i s.t. keys[i] >= target
|
|
176
|
+
*/
|
|
177
|
+
lowerBound(target) {
|
|
178
|
+
let lo = 0;
|
|
179
|
+
let hi = this.sortedKeys.length;
|
|
180
|
+
while (lo < hi) {
|
|
181
|
+
// oxlint-disable-next-line no-bitwise
|
|
182
|
+
const mid = (lo + hi) >>> 1;
|
|
183
|
+
if (this.sortedKeys[mid] < target) {
|
|
184
|
+
lo = mid + 1;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
hi = mid;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return lo;
|
|
191
|
+
}
|
|
192
|
+
sortKeys() {
|
|
193
|
+
if (this.opt.numericKeys) {
|
|
194
|
+
;
|
|
195
|
+
this.sortedKeys.sort(numericAscCompare);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Default sort - fastest for Strings
|
|
199
|
+
this.sortedKeys.sort();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function numericAscCompare(a, b) {
|
|
204
|
+
return a - b;
|
|
205
|
+
}
|
|
@@ -354,7 +354,6 @@ export function _get(obj = {}, path = '') {
|
|
|
354
354
|
* Based on: https://stackoverflow.com/a/54733755/4919972
|
|
355
355
|
*/
|
|
356
356
|
export function _set(obj, path, value) {
|
|
357
|
-
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
358
357
|
if (!obj || Object(obj) !== obj || !path)
|
|
359
358
|
return obj; // When obj is not an object
|
|
360
359
|
// If not yet an array, get the keys from the string-path
|
|
@@ -366,9 +365,7 @@ export function _set(obj, path, value) {
|
|
|
366
365
|
}
|
|
367
366
|
// oxlint-disable-next-line unicorn/no-array-reduce
|
|
368
367
|
;
|
|
369
|
-
path.slice(0, -1).reduce((a, c, i) =>
|
|
370
|
-
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
371
|
-
Object(a[c]) === a[c] // Does the key exist and is its value an object?
|
|
368
|
+
path.slice(0, -1).reduce((a, c, i) => Object(a[c]) === a[c] // Does the key exist and is its value an object?
|
|
372
369
|
? // Yes: then follow that path
|
|
373
370
|
a[c]
|
|
374
371
|
: // No: create the key. Is the next key a potential array-index?
|
package/package.json
CHANGED
package/src/array/range.ts
CHANGED
|
@@ -36,7 +36,6 @@ export function _range(fromIncl: Integer, toExcl?: Integer, step = 1): number[]
|
|
|
36
36
|
* If it was an object - it'll paste the same object reference, which can create bugs.
|
|
37
37
|
*/
|
|
38
38
|
export function _arrayFilled<T extends Primitive>(length: Integer, fill: T): T[] {
|
|
39
|
-
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
40
39
|
return Array(length).fill(fill)
|
|
41
40
|
}
|
|
42
41
|
|
|
@@ -119,14 +119,18 @@ export const j = {
|
|
|
119
119
|
|
|
120
120
|
// complex types
|
|
121
121
|
object,
|
|
122
|
-
dbEntity<
|
|
122
|
+
dbEntity<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(props: P) {
|
|
123
123
|
return j
|
|
124
124
|
.object<BaseDBEntity>({
|
|
125
125
|
id: j.string(),
|
|
126
126
|
created: j.integer().unixTimestamp2000(),
|
|
127
127
|
updated: j.integer().unixTimestamp2000(),
|
|
128
128
|
})
|
|
129
|
-
.extend(j.object(props))
|
|
129
|
+
.extend(j.object(props)) as JsonSchemaObjectBuilder<
|
|
130
|
+
BaseDBEntity & {
|
|
131
|
+
[K in keyof P]: P[K] extends JsonSchemaAnyBuilder<infer U, any, any> ? U : never
|
|
132
|
+
}
|
|
133
|
+
>
|
|
130
134
|
},
|
|
131
135
|
|
|
132
136
|
rootObject<T extends AnyObject>(props: {
|
|
@@ -541,7 +545,7 @@ export class JsonSchemaArrayBuilder<ITEM, Opt extends boolean = false> extends J
|
|
|
541
545
|
return this
|
|
542
546
|
}
|
|
543
547
|
|
|
544
|
-
unique(uniqueItems
|
|
548
|
+
unique(uniqueItems = true): this {
|
|
545
549
|
Object.assign(this.schema, { uniqueItems })
|
|
546
550
|
return this
|
|
547
551
|
}
|
|
@@ -561,9 +565,9 @@ export class JsonSchemaTupleBuilder<T extends any[]> extends JsonSchemaAnyBuilde
|
|
|
561
565
|
}
|
|
562
566
|
}
|
|
563
567
|
|
|
564
|
-
function object<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(
|
|
565
|
-
|
|
566
|
-
): JsonSchemaObjectBuilder<
|
|
568
|
+
function object<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(props?: {
|
|
569
|
+
[K in keyof P]: P[K] & JsonSchemaAnyBuilder<any, any, any>
|
|
570
|
+
}): JsonSchemaObjectBuilder<
|
|
567
571
|
{
|
|
568
572
|
[K in keyof P as P[K] extends JsonSchemaAnyBuilder<any, any, infer Opt>
|
|
569
573
|
? Opt extends true
|
package/src/object/index.ts
CHANGED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
export interface KeySortedMapOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Defaults to false.
|
|
4
|
+
* Set to true if your keys are numeric,
|
|
5
|
+
* so it would sort correctly.
|
|
6
|
+
*/
|
|
7
|
+
numericKeys?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maintains sorted array of keys.
|
|
12
|
+
* Sorts **on insertion**, not on retrieval.
|
|
13
|
+
*
|
|
14
|
+
* - set(): O(log n) search + O(n) splice only when inserting a NEW key
|
|
15
|
+
* - get/has: O(1)
|
|
16
|
+
* - delete: O(log n) search + O(n) splice if present
|
|
17
|
+
* - iteration: O(n) over pre-sorted keys (no sorting at iteration time)
|
|
18
|
+
*
|
|
19
|
+
* @experimental
|
|
20
|
+
*/
|
|
21
|
+
export class KeySortedMap<K, V> implements Map<K, V> {
|
|
22
|
+
private readonly map: Map<K, V>
|
|
23
|
+
private readonly sortedKeys: K[]
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
entries: [K, V][] = [],
|
|
27
|
+
public opt: KeySortedMapOptions = {},
|
|
28
|
+
) {
|
|
29
|
+
this.map = new Map(entries)
|
|
30
|
+
this.sortedKeys = [...this.map.keys()]
|
|
31
|
+
this.sortKeys()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convenience way to create KeySortedMap from object.
|
|
36
|
+
*/
|
|
37
|
+
static of<V>(obj: Record<any, V>): KeySortedMap<string, V> {
|
|
38
|
+
return new KeySortedMap(Object.entries(obj))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get size(): number {
|
|
42
|
+
return this.map.size
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear(): void {
|
|
46
|
+
this.map.clear()
|
|
47
|
+
this.sortedKeys.length = 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
has(key: K): boolean {
|
|
51
|
+
return this.map.has(key)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get(key: K): V | undefined {
|
|
55
|
+
return this.map.get(key)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Allows to set multiple key-value pairs at once.
|
|
60
|
+
*/
|
|
61
|
+
setMany(obj: Record<any, V>): this {
|
|
62
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
63
|
+
this.map.set(k as K, v)
|
|
64
|
+
this.sortedKeys.push(k as K)
|
|
65
|
+
}
|
|
66
|
+
// Resort all at once
|
|
67
|
+
this.sortKeys()
|
|
68
|
+
return this
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Insert or update. Keeps keys array sorted at all times.
|
|
73
|
+
* Returns this (Map-like).
|
|
74
|
+
*/
|
|
75
|
+
set(key: K, value: V): this {
|
|
76
|
+
if (this.map.has(key)) {
|
|
77
|
+
// Update only; position unchanged.
|
|
78
|
+
this.map.set(key, value)
|
|
79
|
+
return this
|
|
80
|
+
}
|
|
81
|
+
// Find insertion index (lower_bound).
|
|
82
|
+
const i = this.lowerBound(key)
|
|
83
|
+
// Only insert into keys when actually new.
|
|
84
|
+
this.sortedKeys.splice(i, 0, key)
|
|
85
|
+
this.map.set(key, value)
|
|
86
|
+
return this
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Delete by key. Returns boolean like Map.delete.
|
|
91
|
+
*/
|
|
92
|
+
delete(key: K): boolean {
|
|
93
|
+
if (!this.map.has(key)) return false
|
|
94
|
+
this.map.delete(key)
|
|
95
|
+
// Remove from keys using binary search to avoid O(n) find.
|
|
96
|
+
const i = this.lowerBound(key)
|
|
97
|
+
// Because key existed, it must be at i.
|
|
98
|
+
if (i < this.sortedKeys.length && this.sortedKeys[i] === key) {
|
|
99
|
+
this.sortedKeys.splice(i, 1)
|
|
100
|
+
} else {
|
|
101
|
+
// Extremely unlikely if external mutation happened; safe guard.
|
|
102
|
+
// Fall back to linear search (shouldn't happen).
|
|
103
|
+
const j = this.sortedKeys.indexOf(key)
|
|
104
|
+
if (j !== -1) this.sortedKeys.splice(j, 1)
|
|
105
|
+
}
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Iterables (Map-compatible), all in sorted order.
|
|
111
|
+
*/
|
|
112
|
+
*keys(): MapIterator<K> {
|
|
113
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
114
|
+
yield this.sortedKeys[i]!
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
*values(): MapIterator<V> {
|
|
119
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
120
|
+
yield this.map.get(this.sortedKeys[i]!)!
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
*entries(): MapIterator<[K, V]> {
|
|
125
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
126
|
+
const k = this.sortedKeys[i]!
|
|
127
|
+
yield [k, this.map.get(k)!]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
[Symbol.iterator](): MapIterator<[K, V]> {
|
|
132
|
+
return this.entries()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[Symbol.toStringTag] = 'KeySortedMap'
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Zero-allocation callbacks over sorted data (faster than spreading to arrays).
|
|
139
|
+
*/
|
|
140
|
+
forEach(cb: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
|
|
141
|
+
const m = this.map
|
|
142
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
143
|
+
const k = this.sortedKeys[i]!
|
|
144
|
+
cb.call(thisArg, m.get(k)!, k, this)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convenience methods that MATERIALIZE arrays (if you really want arrays).
|
|
150
|
+
* These allocate; use iterators/forEach for maximum performance.
|
|
151
|
+
*/
|
|
152
|
+
keysArray(): K[] {
|
|
153
|
+
return this.sortedKeys.slice()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
valuesArray(): V[] {
|
|
157
|
+
// oxlint-disable-next-line unicorn/no-new-array
|
|
158
|
+
const a = Array<V>(this.sortedKeys.length)
|
|
159
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
160
|
+
a[i] = this.map.get(this.sortedKeys[i]!)!
|
|
161
|
+
}
|
|
162
|
+
return a
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
entriesArray(): [K, V][] {
|
|
166
|
+
// oxlint-disable-next-line unicorn/no-new-array
|
|
167
|
+
const out = Array<[K, V]>(this.sortedKeys.length)
|
|
168
|
+
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
169
|
+
const k = this.sortedKeys[i]!
|
|
170
|
+
out[i] = [k, this.map.get(k)!]
|
|
171
|
+
}
|
|
172
|
+
return out
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Fast helpers */
|
|
176
|
+
firstKey(): K | undefined {
|
|
177
|
+
return this.sortedKeys[0]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lastKey(): K | undefined {
|
|
181
|
+
return this.sortedKeys.length ? this.sortedKeys[this.sortedKeys.length - 1] : undefined
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
firstEntry(): [K, V] | undefined {
|
|
185
|
+
if (!this.sortedKeys.length) return
|
|
186
|
+
const k = this.sortedKeys[0]!
|
|
187
|
+
return [k, this.map.get(k)!]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lastEntry(): [K, V] | undefined {
|
|
191
|
+
if (!this.sortedKeys.length) return
|
|
192
|
+
const k = this.sortedKeys[this.sortedKeys.length - 1]!
|
|
193
|
+
return [k, this.map.get(k)!]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
toJSON(): Record<string, V> {
|
|
197
|
+
return this.toObject()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
toObject(): Record<string, V> {
|
|
201
|
+
return Object.fromEntries(this.map)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* lowerBound: first index i s.t. keys[i] >= target
|
|
206
|
+
*/
|
|
207
|
+
private lowerBound(target: K): number {
|
|
208
|
+
let lo = 0
|
|
209
|
+
let hi = this.sortedKeys.length
|
|
210
|
+
while (lo < hi) {
|
|
211
|
+
// oxlint-disable-next-line no-bitwise
|
|
212
|
+
const mid = (lo + hi) >>> 1
|
|
213
|
+
if (this.sortedKeys[mid]! < target) {
|
|
214
|
+
lo = mid + 1
|
|
215
|
+
} else {
|
|
216
|
+
hi = mid
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return lo
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private sortKeys(): void {
|
|
223
|
+
if (this.opt.numericKeys) {
|
|
224
|
+
;(this.sortedKeys as number[]).sort(numericAscCompare)
|
|
225
|
+
} else {
|
|
226
|
+
// Default sort - fastest for Strings
|
|
227
|
+
this.sortedKeys.sort()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function numericAscCompare(a: number, b: number): number {
|
|
233
|
+
return a - b
|
|
234
|
+
}
|
|
@@ -421,7 +421,6 @@ type PropertyPath = Many<PropertyKey>
|
|
|
421
421
|
* Based on: https://stackoverflow.com/a/54733755/4919972
|
|
422
422
|
*/
|
|
423
423
|
export function _set<T extends AnyObject>(obj: T, path: PropertyPath, value: any): T {
|
|
424
|
-
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
425
424
|
if (!obj || Object(obj) !== obj || !path) return obj as any // When obj is not an object
|
|
426
425
|
|
|
427
426
|
// If not yet an array, get the keys from the string-path
|
|
@@ -438,7 +437,6 @@ export function _set<T extends AnyObject>(obj: T, path: PropertyPath, value: any
|
|
|
438
437
|
c,
|
|
439
438
|
i, // Iterate all of them except the last one
|
|
440
439
|
) =>
|
|
441
|
-
// biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
|
|
442
440
|
Object(a[c]) === a[c] // Does the key exist and is its value an object?
|
|
443
441
|
? // Yes: then follow that path
|
|
444
442
|
a[c]
|