Skip to content

Commit 4d6b518

Browse files
committed
fix: fixing size when deleting children
If deleSubTree is true, size was not ending up correctly. Fixing it.
1 parent 7001699 commit 4d6b518

2 files changed

Lines changed: 86 additions & 24 deletions

File tree

libs/js-tuple/src/nested-map.ts

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { IterationOptions, PartialKey, TraverseMode, YieldMode } from './types';
33

44
const VAL = Symbol('value');
55
const SET = Symbol('valueSet');
6+
const COUNT = Symbol('count');
67
const MAP = Symbol('objectMap');
78

89
type CacheNode = {
910
[MAP]?: Map<unknown, CacheNode>;
1011
[SET]?: number;
1112
[VAL]?: unknown;
13+
[COUNT]: number;
1214
};
1315

1416
type PathArray = readonly unknown[];
@@ -198,9 +200,9 @@ const traverser = {
198200
* ```
199201
*/
200202
export class NestedMap<K, V> {
201-
private readonly _root: CacheNode = {};
202-
203-
private _size = 0;
203+
private readonly _root: CacheNode = {
204+
[COUNT]: 0,
205+
};
204206

205207
/**
206208
* Creates a new NestedMap instance.
@@ -218,24 +220,43 @@ export class NestedMap<K, V> {
218220
* Gets the number of key-value pairs in the map.
219221
*/
220222
get size(): number {
221-
return this._size;
223+
return this._root[COUNT];
222224
}
223225

224-
private _getOrCreateNode(nestedKey: K) {
226+
private _getOrCreateNode(nestedKey: K, path: CacheNode[]) {
225227
const keyArray = treatKey(nestedKey);
226228
let current = this._root;
229+
path.push(current);
227230

228231
// Navigate/create the path
229232
for (let i = 0; i < keyArray.length; i++) {
230233
const key = keyArray[i];
231234
const map = (current[MAP] ??= new Map());
232-
let next = map.get(key);
233-
if (!next) map.set(key, (next = {}));
235+
let next: CacheNode = map.get(key);
236+
if (!next) {
237+
next = {
238+
[COUNT]: 0,
239+
};
240+
map.set(key, next);
241+
}
242+
path.push(next);
234243
current = next;
235244
}
236245
return current;
237246
}
238247

248+
#internalSet(current: CacheNode, value: V, path: CacheNode[]) {
249+
const isNewEntry = current[SET] !== 1;
250+
251+
// Store the actual value using the special VALUE_KEY
252+
current[VAL] = value;
253+
254+
if (isNewEntry) {
255+
path.forEach((p) => p[COUNT]++);
256+
current[SET] = 1;
257+
}
258+
}
259+
239260
/**
240261
* Stores a value with the given array key.
241262
*
@@ -244,18 +265,11 @@ export class NestedMap<K, V> {
244265
* @returns This NestedMap instance for chaining
245266
*/
246267
set(nestedKey: K, value: V): this {
247-
const current = this._getOrCreateNode(nestedKey);
268+
const path: CacheNode[] = [];
269+
const current = this._getOrCreateNode(nestedKey, path);
248270

249271
// Check if this is a new entry by seeing if VALUE_KEY already exists
250-
const isNewEntry = current[SET] !== 1;
251-
252-
// Store the actual value using the special VALUE_KEY
253-
current[VAL] = value;
254-
255-
if (isNewEntry) {
256-
this._size++;
257-
current[SET] = 1;
258-
}
272+
this.#internalSet(current, value, path);
259273

260274
return this;
261275
}
@@ -282,10 +296,11 @@ export class NestedMap<K, V> {
282296
* @returns The existing or newly created value
283297
*/
284298
getOrSet<T extends V>(nestedKey: K, getNewValue: (nestedKey: K) => T): T {
285-
const node = this._getOrCreateNode(nestedKey);
286-
if (node?.[VAL] !== undefined) return node[VAL] as T;
299+
const path: CacheNode[] = [];
300+
const node = this._getOrCreateNode(nestedKey, path);
301+
if (node[VAL] !== undefined) return node[VAL] as T;
287302
const value = getNewValue(nestedKey);
288-
this.set(nestedKey, value);
303+
this.#internalSet(node, value, path);
289304
return value;
290305
}
291306

@@ -336,19 +351,24 @@ export class NestedMap<K, V> {
336351
}
337352
// Remove the value
338353
current[VAL] = undefined;
354+
let decrement = current[SET] ? 1 : 0;
339355
current[SET] = 0;
340-
this._size--;
341356

342357
// Clean up empty Maps from the bottom up
343358
if (current[MAP]?.size) {
344-
if (!deleteSubTree) return true;
359+
if (!deleteSubTree) {
360+
this._root[COUNT] -= decrement;
361+
return true;
362+
}
363+
decrement = current[COUNT];
345364
current[MAP] = undefined;
346365
}
347366
for (let i = path.length - 1; i >= 0; i--) {
348367
const pathItem = path[i] as Required<(typeof path)[0]>;
349368
const nodeMap = pathItem.map[MAP] as Map<unknown, CacheNode>;
350369
const { map, key } = pathItem;
351370
nodeMap.delete(key);
371+
pathItem.map[COUNT] -= decrement;
352372
if (nodeMap.size) break;
353373
map[MAP] = undefined;
354374
}
@@ -361,7 +381,7 @@ export class NestedMap<K, V> {
361381
*/
362382
clear(): void {
363383
this._root[MAP]?.clear();
364-
this._size = 0;
384+
this._root[COUNT] = 0;
365385
}
366386

367387
/**
@@ -394,7 +414,7 @@ export class NestedMap<K, V> {
394414
root = this._getNode(basePath);
395415
if (!root) {
396416
return traverser[TraverseMode.BreadthFirst][yieldMode](
397-
{},
417+
{ [COUNT]: 0 },
398418
EMPTY,
399419
justValue,
400420
);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { NestedMap } from '../src/nested-map';
2+
3+
describe('NestedMap.delete with deleteSubTree', () => {
4+
it('should decrement size by the number of subnodes when deleting a subtree', () => {
5+
// Arrange
6+
const map = new NestedMap<number[], string>();
7+
map.set([1, 2, 3], 'a');
8+
map.set([1, 2, 4], 'b');
9+
map.set([1, 2, 5], 'c');
10+
expect(map.size).toBe(3);
11+
12+
// Act
13+
const deleted = map.delete([1, 2], true);
14+
15+
// Assert
16+
expect(deleted).toBe(true);
17+
expect(map.size).toBe(0);
18+
expect(map.has([1, 2, 3])).toBe(false);
19+
expect(map.has([1, 2, 4])).toBe(false);
20+
expect(map.has([1, 2, 5])).toBe(false);
21+
});
22+
it('should decrement size by the number of subnodes plus parent when parent also has a value', () => {
23+
// Arrange
24+
const map = new NestedMap<number[], string>();
25+
map.set([1, 2], 'parent');
26+
map.set([1, 2, 3], 'a');
27+
map.set([1, 2, 4], 'b');
28+
map.set([1, 2, 5], 'c');
29+
expect(map.size).toBe(4);
30+
31+
// Act
32+
const deleted = map.delete([1, 2], true);
33+
34+
// Assert
35+
expect(deleted).toBe(true);
36+
expect(map.size).toBe(0);
37+
expect(map.has([1, 2])).toBe(false);
38+
expect(map.has([1, 2, 3])).toBe(false);
39+
expect(map.has([1, 2, 4])).toBe(false);
40+
expect(map.has([1, 2, 5])).toBe(false);
41+
});
42+
});

0 commit comments

Comments
 (0)