@naturalcycles/js-lib 15.45.0 → 15.46.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/dist/env.d.ts +6 -0
- package/dist/env.js +8 -0
- package/dist/object/keySortedMap.d.ts +18 -14
- package/dist/object/keySortedMap.js +74 -56
- package/dist/object/keySortedMap2.d.ts +72 -0
- package/dist/object/keySortedMap2.js +184 -0
- package/dist/object/map2.d.ts +1 -0
- package/dist/object/map2.js +3 -0
- package/dist/object/set2.d.ts +1 -1
- package/dist/object/set2.js +2 -2
- package/dist/string/stringify.js +45 -43
- package/package.json +2 -2
- package/src/env.ts +9 -0
- package/src/object/keySortedMap.ts +84 -59
- package/src/object/keySortedMap2.ts +221 -0
- package/src/object/map2.ts +4 -0
- package/src/object/set2.ts +2 -2
- package/src/string/stringify.ts +54 -52
package/dist/string/stringify.js
CHANGED
|
@@ -57,58 +57,29 @@ export function _stringify(obj, opt = {}) {
|
|
|
57
57
|
return _stringify(obj.error, opt);
|
|
58
58
|
}
|
|
59
59
|
if (obj instanceof Error || _isErrorLike(obj)) {
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
// Error or ErrorLike
|
|
63
|
-
//
|
|
64
|
-
// Omit "default" error name as non-informative
|
|
65
|
-
// UPD: no, it's still important to understand that we're dealing with Error and not just some string
|
|
66
|
-
// if (obj?.name === 'Error') {
|
|
67
|
-
// s = obj.message
|
|
68
|
-
// }
|
|
69
|
-
// if (_isErrorObject(obj) && _isHttpErrorObject(obj)) {
|
|
70
|
-
// // Printing (0) to avoid ambiguity
|
|
71
|
-
// s = `${obj.name}(${obj.data.backendResponseStatusCode}): ${obj.message}`
|
|
72
|
-
// }
|
|
73
|
-
s = [obj.name, obj.message].filter(Boolean).join(': ');
|
|
74
|
-
if (typeof obj.code === 'string') {
|
|
75
|
-
// Error that has no `data`, but has `code` property
|
|
76
|
-
s += `\ncode: ${obj.code}`;
|
|
77
|
-
}
|
|
78
|
-
if (opt.includeErrorData && _isErrorObject(obj) && Object.keys(obj.data).length) {
|
|
79
|
-
s += '\n' + _stringify(obj.data, opt);
|
|
80
|
-
}
|
|
81
|
-
if (opt.includeErrorStack && obj.stack) {
|
|
82
|
-
// Here we're using the previously-generated "title line" (e.g "Error: some_message"),
|
|
83
|
-
// concatenating it with the Stack (but without the title line of the Stack)
|
|
84
|
-
// This is to fix the rare error (happened with Got) where `err.message` was changed,
|
|
85
|
-
// but err.stack had "old" err.message
|
|
86
|
-
// This should "fix" that
|
|
87
|
-
const sLines = s.split('\n').length;
|
|
88
|
-
s = [s, ...obj.stack.split('\n').slice(sLines)].join('\n');
|
|
89
|
-
}
|
|
90
|
-
if (supportsAggregateError && obj instanceof AggregateError && obj.errors.length) {
|
|
91
|
-
s = [
|
|
92
|
-
s,
|
|
93
|
-
`${obj.errors.length} error(s):`,
|
|
94
|
-
...obj.errors.map((err, i) => `${i + 1}. ${_stringify(err, opt)}`),
|
|
95
|
-
].join('\n');
|
|
96
|
-
}
|
|
97
|
-
if (obj.cause && includeErrorCause) {
|
|
98
|
-
s = s + '\nCaused by: ' + _stringify(obj.cause, opt);
|
|
99
|
-
}
|
|
60
|
+
s = stringifyErrorLike(obj, opt);
|
|
100
61
|
}
|
|
101
62
|
else if (typeof obj === 'string') {
|
|
102
|
-
//
|
|
103
|
-
// String
|
|
104
|
-
//
|
|
105
63
|
s = obj.trim() || 'empty_string';
|
|
64
|
+
// todo: think about it more
|
|
65
|
+
// Stringifying it like a JSON would.
|
|
66
|
+
// To highlight that it's a String (and not a Number) - using double-quotes, JSON-like.
|
|
67
|
+
// s = `"${obj}"`
|
|
68
|
+
}
|
|
69
|
+
else if (typeof obj === 'number') {
|
|
70
|
+
s = String(obj);
|
|
71
|
+
// todo: support RegExp and Date, when split between Browser and Node stringification is implemented
|
|
72
|
+
// } else if (obj instanceof RegExp) {
|
|
73
|
+
// s = String(obj)
|
|
74
|
+
// } else if (obj instanceof Date) {
|
|
75
|
+
// s = `Date (${obj.toISOString()})`
|
|
106
76
|
}
|
|
107
77
|
else {
|
|
108
78
|
//
|
|
109
79
|
// Other
|
|
110
80
|
//
|
|
111
81
|
if (obj instanceof Map) {
|
|
82
|
+
// todo: double-check it, maybe Node's inspect has good built-in stringification
|
|
112
83
|
obj = Object.fromEntries(obj);
|
|
113
84
|
}
|
|
114
85
|
else if (obj instanceof Set) {
|
|
@@ -132,3 +103,34 @@ export function _stringify(obj, opt = {}) {
|
|
|
132
103
|
}
|
|
133
104
|
return s;
|
|
134
105
|
}
|
|
106
|
+
function stringifyErrorLike(obj, opt) {
|
|
107
|
+
const { includeErrorCause = true } = opt;
|
|
108
|
+
let s = [obj.name, obj.message].filter(Boolean).join(': ');
|
|
109
|
+
if (typeof obj.code === 'string') {
|
|
110
|
+
// Error that has no `data`, but has `code` property
|
|
111
|
+
s += `\ncode: ${obj.code}`;
|
|
112
|
+
}
|
|
113
|
+
if (opt.includeErrorData && _isErrorObject(obj) && Object.keys(obj.data).length) {
|
|
114
|
+
s += '\n' + _stringify(obj.data, opt);
|
|
115
|
+
}
|
|
116
|
+
if (opt.includeErrorStack && obj.stack) {
|
|
117
|
+
// Here we're using the previously-generated "title line" (e.g "Error: some_message"),
|
|
118
|
+
// concatenating it with the Stack (but without the title line of the Stack)
|
|
119
|
+
// This is to fix the rare error (happened with Got) where `err.message` was changed,
|
|
120
|
+
// but err.stack had "old" err.message
|
|
121
|
+
// This should "fix" that
|
|
122
|
+
const sLines = s.split('\n').length;
|
|
123
|
+
s = [s, ...obj.stack.split('\n').slice(sLines)].join('\n');
|
|
124
|
+
}
|
|
125
|
+
if (supportsAggregateError && obj instanceof AggregateError && obj.errors.length) {
|
|
126
|
+
s = [
|
|
127
|
+
s,
|
|
128
|
+
`${obj.errors.length} error(s):`,
|
|
129
|
+
...obj.errors.map((err, i) => `${i + 1}. ${_stringify(err, opt)}`),
|
|
130
|
+
].join('\n');
|
|
131
|
+
}
|
|
132
|
+
if (obj.cause && includeErrorCause) {
|
|
133
|
+
s = s + '\nCaused by: ' + _stringify(obj.cause, opt);
|
|
134
|
+
}
|
|
135
|
+
return s;
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/js-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.46.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"tslib": "^2",
|
|
7
7
|
"undici": "^7",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"@types/semver": "^7",
|
|
14
14
|
"crypto-js": "^4",
|
|
15
15
|
"dayjs": "^1",
|
|
16
|
-
"@naturalcycles/dev-lib": "
|
|
16
|
+
"@naturalcycles/dev-lib": "20.8.0"
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./dist/index.js",
|
package/src/env.ts
CHANGED
|
@@ -18,3 +18,12 @@ export function isClientSide(): boolean {
|
|
|
18
18
|
// oxlint-disable-next-line unicorn/prefer-global-this
|
|
19
19
|
return typeof window !== 'undefined' && !!window?.document
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Almost the same as isServerSide()
|
|
24
|
+
* (isServerSide should return true for Node),
|
|
25
|
+
* but detects Node specifically (not Deno, not Bun, etc).
|
|
26
|
+
*/
|
|
27
|
+
export function isNode(): boolean {
|
|
28
|
+
return typeof process !== 'undefined' && process?.release?.name === 'node'
|
|
29
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _assert } from '../error/index.js'
|
|
1
2
|
import type { Comparator } from '../types.js'
|
|
2
3
|
|
|
3
4
|
export interface KeySortedMapOptions<K> {
|
|
@@ -23,17 +24,17 @@ export interface KeySortedMapOptions<K> {
|
|
|
23
24
|
*/
|
|
24
25
|
export class KeySortedMap<K, V> implements Map<K, V> {
|
|
25
26
|
private readonly map: Map<K, V>
|
|
26
|
-
|
|
27
|
+
readonly #sortedKeys: K[]
|
|
27
28
|
|
|
28
|
-
constructor(
|
|
29
|
-
|
|
30
|
-
public opt: KeySortedMapOptions<K> = {},
|
|
31
|
-
) {
|
|
29
|
+
constructor(entries: [K, V][] = [], opt: KeySortedMapOptions<K> = {}) {
|
|
30
|
+
this.#comparator = opt.comparator
|
|
32
31
|
this.map = new Map(entries)
|
|
33
|
-
this
|
|
32
|
+
this.#sortedKeys = [...this.map.keys()]
|
|
34
33
|
this.sortKeys()
|
|
35
34
|
}
|
|
36
35
|
|
|
36
|
+
readonly #comparator: Comparator<K> | undefined
|
|
37
|
+
|
|
37
38
|
/**
|
|
38
39
|
* Convenience way to create KeySortedMap from object.
|
|
39
40
|
*/
|
|
@@ -47,7 +48,7 @@ export class KeySortedMap<K, V> implements Map<K, V> {
|
|
|
47
48
|
|
|
48
49
|
clear(): void {
|
|
49
50
|
this.map.clear()
|
|
50
|
-
this
|
|
51
|
+
this.#sortedKeys.length = 0
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
has(key: K): boolean {
|
|
@@ -64,7 +65,7 @@ export class KeySortedMap<K, V> implements Map<K, V> {
|
|
|
64
65
|
setMany(obj: Record<any, V>): this {
|
|
65
66
|
for (const [k, v] of Object.entries(obj)) {
|
|
66
67
|
this.map.set(k as K, v)
|
|
67
|
-
this
|
|
68
|
+
this.#sortedKeys.push(k as K)
|
|
68
69
|
}
|
|
69
70
|
// Resort all at once
|
|
70
71
|
this.sortKeys()
|
|
@@ -84,7 +85,7 @@ export class KeySortedMap<K, V> implements Map<K, V> {
|
|
|
84
85
|
// Find insertion index (lower_bound).
|
|
85
86
|
const i = this.lowerBound(key)
|
|
86
87
|
// Only insert into keys when actually new.
|
|
87
|
-
this
|
|
88
|
+
this.#sortedKeys.splice(i, 0, key)
|
|
88
89
|
this.map.set(key, value)
|
|
89
90
|
return this
|
|
90
91
|
}
|
|
@@ -98,13 +99,13 @@ export class KeySortedMap<K, V> implements Map<K, V> {
|
|
|
98
99
|
// Remove from keys using binary search to avoid O(n) find.
|
|
99
100
|
const i = this.lowerBound(key)
|
|
100
101
|
// Because key existed, it must be at i.
|
|
101
|
-
if (i < this
|
|
102
|
-
this
|
|
102
|
+
if (i < this.#sortedKeys.length && this.#sortedKeys[i] === key) {
|
|
103
|
+
this.#sortedKeys.splice(i, 1)
|
|
103
104
|
} else {
|
|
104
105
|
// Extremely unlikely if external mutation happened; safe guard.
|
|
105
106
|
// Fall back to linear search (shouldn't happen).
|
|
106
|
-
const j = this
|
|
107
|
-
if (j !== -1) this
|
|
107
|
+
const j = this.#sortedKeys.indexOf(key)
|
|
108
|
+
if (j !== -1) this.#sortedKeys.splice(j, 1)
|
|
108
109
|
}
|
|
109
110
|
return true
|
|
110
111
|
}
|
|
@@ -113,20 +114,19 @@ export class KeySortedMap<K, V> implements Map<K, V> {
|
|
|
113
114
|
* Iterables (Map-compatible), all in sorted order.
|
|
114
115
|
*/
|
|
115
116
|
*keys(): MapIterator<K> {
|
|
116
|
-
for (
|
|
117
|
-
yield
|
|
117
|
+
for (const key of this.#sortedKeys) {
|
|
118
|
+
yield key
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
*values(): MapIterator<V> {
|
|
122
|
-
for (
|
|
123
|
-
yield this.map.get(
|
|
123
|
+
for (const key of this.#sortedKeys) {
|
|
124
|
+
yield this.map.get(key)!
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
*entries(): MapIterator<[K, V]> {
|
|
128
|
-
for (
|
|
129
|
-
const k = this.sortedKeys[i]!
|
|
129
|
+
for (const k of this.#sortedKeys) {
|
|
130
130
|
yield [k, this.map.get(k)!]
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -135,64 +135,82 @@ export class KeySortedMap<K, V> implements Map<K, V> {
|
|
|
135
135
|
return this.entries()
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
toString(): string {
|
|
139
|
+
console.log('toString called !!!!!!!!!!!!!!!!!!!!!')
|
|
140
|
+
return 'abc'
|
|
141
|
+
}
|
|
142
|
+
|
|
138
143
|
[Symbol.toStringTag] = 'KeySortedMap'
|
|
139
144
|
|
|
140
145
|
/**
|
|
141
146
|
* Zero-allocation callbacks over sorted data (faster than spreading to arrays).
|
|
142
147
|
*/
|
|
143
148
|
forEach(cb: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
|
|
144
|
-
const
|
|
145
|
-
for (
|
|
146
|
-
|
|
147
|
-
cb.call(thisArg, m.get(k)!, k, this)
|
|
149
|
+
const { map } = this
|
|
150
|
+
for (const k of this.#sortedKeys) {
|
|
151
|
+
cb.call(thisArg, map.get(k)!, k, this)
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
* These allocate; use iterators/forEach for maximum performance.
|
|
154
|
-
*/
|
|
155
|
-
keysArray(): K[] {
|
|
156
|
-
return this.sortedKeys.slice()
|
|
155
|
+
firstKeyOrUndefined(): K | undefined {
|
|
156
|
+
return this.#sortedKeys[0]
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
for (let i = 0; i < this.sortedKeys.length; i++) {
|
|
163
|
-
a[i] = this.map.get(this.sortedKeys[i]!)!
|
|
164
|
-
}
|
|
165
|
-
return a
|
|
159
|
+
firstKey(): K {
|
|
160
|
+
_assert(this.#sortedKeys.length, 'Map.firstKey called on empty map')
|
|
161
|
+
return this.#sortedKeys[0]!
|
|
166
162
|
}
|
|
167
163
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return out
|
|
164
|
+
lastKeyOrUndefined(): K | undefined {
|
|
165
|
+
return this.#sortedKeys.length ? this.#sortedKeys[this.#sortedKeys.length - 1] : undefined
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lastKey(): K {
|
|
169
|
+
_assert(this.#sortedKeys.length, 'Map.lastKey called on empty map')
|
|
170
|
+
return this.#sortedKeys[this.#sortedKeys.length - 1]!
|
|
176
171
|
}
|
|
177
172
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return this.sortedKeys[0]
|
|
173
|
+
firstValueOrUndefined(): V | undefined {
|
|
174
|
+
return this.map.get(this.#sortedKeys[0]!)
|
|
181
175
|
}
|
|
182
176
|
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
firstValue(): V {
|
|
178
|
+
_assert(this.#sortedKeys.length, 'Map.firstValue called on empty map')
|
|
179
|
+
return this.map.get(this.#sortedKeys[0]!)!
|
|
185
180
|
}
|
|
186
181
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
lastValueOrUndefined(): V | undefined {
|
|
183
|
+
return this.#sortedKeys.length
|
|
184
|
+
? this.map.get(this.#sortedKeys[this.#sortedKeys.length - 1]!)
|
|
185
|
+
: undefined
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
lastValue(): V {
|
|
189
|
+
_assert(this.#sortedKeys.length, 'Map.lastValue called on empty map')
|
|
190
|
+
return this.map.get(this.#sortedKeys[this.#sortedKeys.length - 1]!)!
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
firstEntryOrUndefined(): [K, V] | undefined {
|
|
194
|
+
if (!this.#sortedKeys.length) return
|
|
195
|
+
const k = this.#sortedKeys[0]!
|
|
196
|
+
return [k, this.map.get(k)!]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
firstEntry(): [K, V] {
|
|
200
|
+
_assert(this.#sortedKeys.length, 'Map.firstEntry called on empty map')
|
|
201
|
+
const k = this.#sortedKeys[0]!
|
|
190
202
|
return [k, this.map.get(k)!]
|
|
191
203
|
}
|
|
192
204
|
|
|
193
|
-
|
|
194
|
-
if (!this
|
|
195
|
-
const k = this
|
|
205
|
+
lastEntryOrUndefined(): [K, V] | undefined {
|
|
206
|
+
if (!this.#sortedKeys.length) return
|
|
207
|
+
const k = this.#sortedKeys[this.#sortedKeys.length - 1]!
|
|
208
|
+
return [k, this.map.get(k)!]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
lastEntry(): [K, V] {
|
|
212
|
+
_assert(this.#sortedKeys.length, 'Map.lastEntry called on empty map')
|
|
213
|
+
const k = this.#sortedKeys[this.#sortedKeys.length - 1]!
|
|
196
214
|
return [k, this.map.get(k)!]
|
|
197
215
|
}
|
|
198
216
|
|
|
@@ -201,7 +219,14 @@ export class KeySortedMap<K, V> implements Map<K, V> {
|
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
toObject(): Record<string, V> {
|
|
204
|
-
return Object.fromEntries(this.
|
|
222
|
+
return Object.fromEntries(this.entries())
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Clones the KeySortedMap into ordinary Map.
|
|
227
|
+
*/
|
|
228
|
+
toMap(): Map<K, V> {
|
|
229
|
+
return new Map(this.entries())
|
|
205
230
|
}
|
|
206
231
|
|
|
207
232
|
/**
|
|
@@ -209,11 +234,11 @@ export class KeySortedMap<K, V> implements Map<K, V> {
|
|
|
209
234
|
*/
|
|
210
235
|
private lowerBound(target: K): number {
|
|
211
236
|
let lo = 0
|
|
212
|
-
let hi = this
|
|
237
|
+
let hi = this.#sortedKeys.length
|
|
213
238
|
while (lo < hi) {
|
|
214
239
|
// oxlint-disable-next-line no-bitwise
|
|
215
240
|
const mid = (lo + hi) >>> 1
|
|
216
|
-
if (this
|
|
241
|
+
if (this.#sortedKeys[mid]! < target) {
|
|
217
242
|
lo = mid + 1
|
|
218
243
|
} else {
|
|
219
244
|
hi = mid
|
|
@@ -223,6 +248,6 @@ export class KeySortedMap<K, V> implements Map<K, V> {
|
|
|
223
248
|
}
|
|
224
249
|
|
|
225
250
|
private sortKeys(): void {
|
|
226
|
-
this
|
|
251
|
+
this.#sortedKeys.sort(this.#comparator)
|
|
227
252
|
}
|
|
228
253
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { _assert } from '../error/index.js'
|
|
2
|
+
import type { Comparator } from '../types.js'
|
|
3
|
+
|
|
4
|
+
export interface KeySortedMapOptions<K> {
|
|
5
|
+
/**
|
|
6
|
+
* Defaults to undefined.
|
|
7
|
+
* Undefined (default comparator) works well for String keys.
|
|
8
|
+
* For Number keys - use comparators.numericAsc (or desc),
|
|
9
|
+
* otherwise sorting will be wrong (lexicographic).
|
|
10
|
+
*/
|
|
11
|
+
comparator?: Comparator<K>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Maintains sorted array of keys.
|
|
16
|
+
* Sorts **data access**, not on insertion.
|
|
17
|
+
*
|
|
18
|
+
* @experimental
|
|
19
|
+
*/
|
|
20
|
+
export class KeySortedMap2<K, V> implements Map<K, V> {
|
|
21
|
+
private readonly map: Map<K, V>
|
|
22
|
+
private readonly maybeSortedKeys: K[]
|
|
23
|
+
private keysAreSorted = false
|
|
24
|
+
|
|
25
|
+
constructor(entries: [K, V][] = [], opt: KeySortedMapOptions<K> = {}) {
|
|
26
|
+
this.#comparator = opt.comparator
|
|
27
|
+
this.map = new Map(entries)
|
|
28
|
+
this.maybeSortedKeys = [...this.map.keys()]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
readonly #comparator: Comparator<K> | undefined
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convenience way to create KeySortedMap from object.
|
|
35
|
+
*/
|
|
36
|
+
static of<V>(obj: Record<any, V>): KeySortedMap2<string, V> {
|
|
37
|
+
return new KeySortedMap2(Object.entries(obj))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get size(): number {
|
|
41
|
+
return this.map.size
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear(): void {
|
|
45
|
+
this.map.clear()
|
|
46
|
+
this.maybeSortedKeys.length = 0
|
|
47
|
+
this.keysAreSorted = true
|
|
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.maybeSortedKeys.push(k as K)
|
|
65
|
+
}
|
|
66
|
+
this.keysAreSorted = false
|
|
67
|
+
return this
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Insert or update. Keeps keys array sorted at all times.
|
|
72
|
+
* Returns this (Map-like).
|
|
73
|
+
*/
|
|
74
|
+
set(key: K, value: V): this {
|
|
75
|
+
if (!this.map.has(key)) {
|
|
76
|
+
this.maybeSortedKeys.push(key)
|
|
77
|
+
this.keysAreSorted = false
|
|
78
|
+
}
|
|
79
|
+
this.map.set(key, value)
|
|
80
|
+
return this
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Delete by key. Returns boolean like Map.delete.
|
|
85
|
+
*/
|
|
86
|
+
delete(key: K): boolean {
|
|
87
|
+
if (!this.map.has(key)) return false
|
|
88
|
+
this.map.delete(key)
|
|
89
|
+
// Delete operation keeps the array **as-is**, it may have been sorted or not.
|
|
90
|
+
const j = this.maybeSortedKeys.indexOf(key)
|
|
91
|
+
if (j !== -1) this.maybeSortedKeys.splice(j, 1)
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Iterables (Map-compatible), all in sorted order.
|
|
97
|
+
*/
|
|
98
|
+
*keys(): MapIterator<K> {
|
|
99
|
+
for (const key of this.getSortedKeys()) {
|
|
100
|
+
yield key
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
*values(): MapIterator<V> {
|
|
105
|
+
for (const key of this.getSortedKeys()) {
|
|
106
|
+
yield this.map.get(key)!
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
*entries(): MapIterator<[K, V]> {
|
|
111
|
+
for (const k of this.getSortedKeys()) {
|
|
112
|
+
yield [k, this.map.get(k)!]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
[Symbol.iterator](): MapIterator<[K, V]> {
|
|
117
|
+
return this.entries()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
[Symbol.toStringTag] = 'KeySortedMap'
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Zero-allocation callbacks over sorted data (faster than spreading to arrays).
|
|
124
|
+
*/
|
|
125
|
+
forEach(cb: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
|
|
126
|
+
const { map } = this
|
|
127
|
+
for (const k of this.getSortedKeys()) {
|
|
128
|
+
cb.call(thisArg, map.get(k)!, k, this)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
firstKeyOrUndefined(): K | undefined {
|
|
133
|
+
return this.getSortedKeys()[0]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
firstKey(): K {
|
|
137
|
+
_assert(this.maybeSortedKeys.length, 'Map.firstKey called on empty map')
|
|
138
|
+
return this.getSortedKeys()[0]!
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lastKeyOrUndefined(): K | undefined {
|
|
142
|
+
if (!this.maybeSortedKeys.length) return
|
|
143
|
+
const keys = this.getSortedKeys()
|
|
144
|
+
return keys[keys.length - 1]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
lastKey(): K {
|
|
148
|
+
const k = this.lastKeyOrUndefined()
|
|
149
|
+
_assert(k, 'Map.lastKey called on empty map')
|
|
150
|
+
return k
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
firstValueOrUndefined(): V | undefined {
|
|
154
|
+
if (!this.maybeSortedKeys.length) return
|
|
155
|
+
return this.map.get(this.getSortedKeys()[0]!)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
firstValue(): V {
|
|
159
|
+
const v = this.firstValueOrUndefined()
|
|
160
|
+
_assert(v, 'Map.firstValue called on empty map')
|
|
161
|
+
return v
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
lastValueOrUndefined(): V | undefined {
|
|
165
|
+
if (!this.maybeSortedKeys.length) return
|
|
166
|
+
const keys = this.getSortedKeys()
|
|
167
|
+
return this.map.get(keys[keys.length - 1]!)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
lastValue(): V {
|
|
171
|
+
const v = this.lastValueOrUndefined()
|
|
172
|
+
_assert(v, 'Map.lastValue called on empty map')
|
|
173
|
+
return v
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
firstEntryOrUndefined(): [K, V] | undefined {
|
|
177
|
+
if (!this.maybeSortedKeys.length) return
|
|
178
|
+
const k = this.getSortedKeys()[0]!
|
|
179
|
+
return [k, this.map.get(k)!]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
firstEntry(): [K, V] {
|
|
183
|
+
const e = this.firstEntryOrUndefined()
|
|
184
|
+
_assert(e, 'Map.firstEntry called on empty map')
|
|
185
|
+
return e
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
lastEntryOrUndefined(): [K, V] | undefined {
|
|
189
|
+
if (!this.maybeSortedKeys.length) return
|
|
190
|
+
const keys = this.getSortedKeys()
|
|
191
|
+
const k = keys[keys.length - 1]!
|
|
192
|
+
return [k, this.map.get(k)!]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
lastEntry(): [K, V] {
|
|
196
|
+
const e = this.firstEntryOrUndefined()
|
|
197
|
+
_assert(e, 'Map.lastEntry called on empty map')
|
|
198
|
+
return e
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
toJSON(): Record<string, V> {
|
|
202
|
+
return this.toObject()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
toObject(): Record<string, V> {
|
|
206
|
+
return Object.fromEntries(this.entries())
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private getSortedKeys(): K[] {
|
|
210
|
+
if (!this.keysAreSorted) {
|
|
211
|
+
return this.sortKeys()
|
|
212
|
+
}
|
|
213
|
+
return this.maybeSortedKeys
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private sortKeys(): K[] {
|
|
217
|
+
this.maybeSortedKeys.sort(this.#comparator)
|
|
218
|
+
this.keysAreSorted = true
|
|
219
|
+
return this.maybeSortedKeys
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/object/map2.ts
CHANGED
|
@@ -31,5 +31,9 @@ export class Map2<K = any, V = any> extends Map<K, V> {
|
|
|
31
31
|
return Object.fromEntries(this)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
override toString(): string {
|
|
35
|
+
return `Map2(${this.size}) ${JSON.stringify(Object.fromEntries(this))}`
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
// consider more helpful .toString() ?
|
|
35
39
|
}
|
package/src/object/set2.ts
CHANGED
|
@@ -41,8 +41,8 @@ export class Set2<T = any> extends Set<T> {
|
|
|
41
41
|
return [...this]
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
override
|
|
45
|
-
return
|
|
44
|
+
override toString(): string {
|
|
45
|
+
return `Set2(${this.size}) ${JSON.stringify([...this])}`
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// todo: consider more helpful .toString() ?
|