Skip to content

Commit c64ea6c

Browse files
committed
fix: prevent TreeMultiSet comparator-equal object corruption
1 parent 92e357b commit c64ea6c

4 files changed

Lines changed: 51 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ All notable changes to this project will be documented in this file.
88
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
99
- [`auto-changelog`](https://github.com/CookPete/auto-changelog)
1010

11-
## [v2.6.0](https://github.com/zrwusa/data-structure-typed/compare/v2.5.3...main) (upcoming)
11+
## [v2.6.2](https://github.com/zrwusa/data-structure-typed/compare/v2.6.1...v2.6.2) (24 June 2026)
12+
13+
### Changes
14+
15+
- fix(TreeMultiSet): preserve counts for comparator-equal object keys [`#131`](https://github.com/zrwusa/data-structure-typed/issues/131)
16+
17+
## [v2.6.1](https://github.com/zrwusa/data-structure-typed/compare/v2.6.0...v2.6.1) (2 April 2026)
18+
19+
## [v2.6.0](https://github.com/zrwusa/data-structure-typed/compare/v2.5.3...v2.6.0) (31 March 2026)
1220

1321
## [v2.5.3](https://github.com/zrwusa/data-structure-typed/compare/v2.5.1...v2.5.3) (31 March 2026)
1422

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "data-structure-typed",
3-
"version": "2.6.1",
3+
"version": "2.6.2",
44
"description": "Production-ready TypeScript data structures: Heap, Deque, Trie, Graph, Red-Black Tree, TreeMap, TreeSet, and more. Zero dependencies, type-safe, with getRank/getByRank/rangeByRank support.",
55
"browser": "dist/umd/data-structure-typed.min.js",
66
"umd:main": "dist/umd/data-structure-typed.min.js",

src/data-structures/binary-tree/tree-multi-set.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export class TreeMultiSet<K = any, R = K> implements Iterable<K> {
113113
*/
114114
count(key: K): number {
115115
this._validateKey(key);
116-
return this.#core.get(key) ?? 0;
116+
return this.#core.getNode(key)?.value ?? 0;
117117
}
118118

119119
/**
@@ -133,9 +133,10 @@ export class TreeMultiSet<K = any, R = K> implements Iterable<K> {
133133
this._validateKey(key);
134134
this._validateCount(n);
135135
if (n === 0) return false;
136-
const old = this.#core.get(key) ?? 0;
136+
const node = this.#core.getNode(key);
137+
const old = node?.value ?? 0;
137138
const next = old + n;
138-
this.#core.set(key, next);
139+
this.#core.set(node?.key ?? key, next);
139140
this._size += n;
140141
return true;
141142
}
@@ -153,12 +154,13 @@ export class TreeMultiSet<K = any, R = K> implements Iterable<K> {
153154
setCount(key: K, n: number): boolean {
154155
this._validateKey(key);
155156
this._validateCount(n);
156-
const old = this.#core.get(key) ?? 0;
157+
const node = this.#core.getNode(key);
158+
const old = node?.value ?? 0;
157159
if (old === n) return false;
158160
if (n === 0) {
159-
if (old !== 0) this.#core.delete(key);
161+
if (node) this.#core.delete(node);
160162
} else {
161-
this.#core.set(key, n);
163+
this.#core.set(node?.key ?? key, n);
162164
}
163165
this._size += n - old;
164166
return true;
@@ -179,12 +181,13 @@ export class TreeMultiSet<K = any, R = K> implements Iterable<K> {
179181
this._validateKey(key);
180182
this._validateCount(n);
181183
if (n === 0) return false;
182-
const old = this.#core.get(key) ?? 0;
184+
const node = this.#core.getNode(key);
185+
const old = node?.value ?? 0;
183186
if (old === 0) return false;
184187
const removed = Math.min(old, n);
185188
const next = old - removed;
186-
if (next === 0) this.#core.delete(key);
187-
else this.#core.set(key, next);
189+
if (next === 0) this.#core.delete(node);
190+
else this.#core.set(node?.key ?? key, next);
188191
this._size -= removed;
189192
return true;
190193
}
@@ -202,9 +205,10 @@ export class TreeMultiSet<K = any, R = K> implements Iterable<K> {
202205
*/
203206
deleteAll(key: K): boolean {
204207
this._validateKey(key);
205-
const old = this.#core.get(key) ?? 0;
208+
const node = this.#core.getNode(key);
209+
const old = node?.value ?? 0;
206210
if (old === 0) return false;
207-
this.#core.delete(key);
211+
this.#core.delete(node);
208212
this._size -= old;
209213
return true;
210214
}

test/unit/data-structures/binary-tree/tree-multi-set.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,32 @@ describe('TreeMultiSet', () => {
499499
expect(ms.size).toBe(3);
500500
});
501501

502+
it('handles comparator-equal non-identical objects without corrupting size', () => {
503+
const ms = new TreeMultiSet<{ key: number }>([], {
504+
comparator: (a, b) => a.key - b.key
505+
});
506+
const nodeA = { key: 1 };
507+
const nodeB = { key: 1 };
508+
509+
ms.add(nodeA);
510+
ms.add(nodeB);
511+
512+
expect(ms.size).toBe(2);
513+
expect(ms.distinctSize).toBe(1);
514+
expect(ms.count(nodeA)).toBe(2);
515+
expect(ms.count(nodeB)).toBe(2);
516+
expect(ms.toArray()).toEqual([nodeA, nodeA]);
517+
518+
while (ms.size > 0) {
519+
const node = ms.first();
520+
expect(node).toBe(nodeA);
521+
expect(ms.delete(node!)).toBe(true);
522+
}
523+
524+
expect(ms.first()).toBeUndefined();
525+
expect(ms.distinctSize).toBe(0);
526+
});
527+
502528
it('rejects non-primitive types without comparator', () => {
503529
const ms = new TreeMultiSet<{ id: number }>();
504530
expect(() => ms.add({ id: 1 })).toThrow(TypeError);

0 commit comments

Comments
 (0)