Skip to content

Commit 7d20508

Browse files
committed
feat: SortedMap
1 parent 8312635 commit 7d20508

File tree

3 files changed

+194
-0
lines changed

3 files changed

+194
-0
lines changed
File renamed without changes.

src/utils/sorted-map.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict'
2+
3+
/**
4+
* SortedMap is a Map whose iterator order can be defined by the user
5+
*/
6+
class SortedMap extends Map {
7+
/**
8+
* @param {Array<k, v>} [entries]
9+
* @param {function(a, b)} [cmp]
10+
*/
11+
constructor (entries, cmp) {
12+
super(entries)
13+
this._cmp = cmp
14+
this._keys = []
15+
}
16+
17+
// Performance is O(N log(N)).
18+
// Could be O(log(N)) if we use a sorted array. Fix this if performance isn't
19+
// good enough.
20+
set (k, data) {
21+
super.set(k, data)
22+
const i = this._keys.indexOf(k)
23+
if (i < 0) {
24+
this._keys.push(k)
25+
this._keys.sort(this._kvCmp.bind(this))
26+
}
27+
}
28+
29+
clear () {
30+
super.clear()
31+
this._keys = []
32+
}
33+
34+
/**
35+
* Call update to manually trigger a sort.
36+
* For example if the compare function sorts by the priority field, and the
37+
* priority changes, call update.
38+
*
39+
* @param {Object} [k] the key corresponding to the entry whose position
40+
* should be updated.
41+
*/
42+
update (k) {
43+
this._keys.sort(this._kvCmp.bind(this))
44+
}
45+
46+
// Same performance comments as set()
47+
delete (k) {
48+
const i = this._keys.indexOf(k)
49+
if (i >= 0) {
50+
this._keys.splice(i, 1)
51+
super.delete(k)
52+
}
53+
}
54+
55+
* keys () {
56+
for (const k of this._keys) {
57+
yield k
58+
}
59+
}
60+
61+
* values () {
62+
for (const k of this._keys) {
63+
yield this.get(k)
64+
}
65+
}
66+
67+
* entries () {
68+
for (const k of this._keys) {
69+
yield [k, this.get(k)]
70+
}
71+
}
72+
73+
* [Symbol.iterator] () {
74+
yield * this.entries()
75+
}
76+
77+
forEach (cb, thisArg) {
78+
if (!cb) {
79+
return
80+
}
81+
82+
for (const k of this._keys) {
83+
cb.apply(thisArg, [[k, this.get(k)]])
84+
}
85+
}
86+
87+
_kvCmp (a, b) {
88+
// By default sort by built-in compare
89+
if (!this._cmp) {
90+
if (a < b) return -1
91+
if (b < a) return 1
92+
return 0
93+
}
94+
95+
return this._cmp(
96+
[a, this.get(a)],
97+
[b, this.get(b)]
98+
)
99+
}
100+
}
101+
102+
module.exports = SortedMap

test/utils.spec.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const BitswapMessageEntry = require('../src/types/message/entry')
1010
chai.use(require('dirty-chai'))
1111
const expect = chai.expect
1212
const { groupBy, uniqWith, pullAllWith, includesWith, sortBy, isMapEqual } = require('../src/utils')
13+
const SortedMap = require('../src/utils/sorted-map')
1314

1415
describe('utils spec', function () {
1516
it('groupBy', () => {
@@ -165,4 +166,95 @@ describe('utils spec', function () {
165166
)).to.be.true()
166167
})
167168
})
169+
170+
describe('SortedMap', () => {
171+
it('size', () => {
172+
const sm = new SortedMap()
173+
sm.set('one', 1)
174+
sm.set('two', 2)
175+
sm.set('three', 3)
176+
177+
expect(sm.size).to.eql(3)
178+
})
179+
180+
it('get / set', () => {
181+
const sm = new SortedMap()
182+
sm.set('one', 1)
183+
sm.set('two', 2)
184+
sm.set('three', 3)
185+
186+
expect(sm.get('one')).to.eql(1)
187+
expect(sm.get('two')).to.eql(2)
188+
expect(sm.get('three')).to.eql(3)
189+
})
190+
191+
it('delete', () => {
192+
const sm = new SortedMap()
193+
sm.set('one', 1)
194+
sm.set('two', 2)
195+
sm.set('three', 3)
196+
197+
expect(sm.get('two')).to.eql(2)
198+
199+
sm.delete('two')
200+
201+
expect(sm.get('two')).to.be.undefined()
202+
expect(sm.size).to.eql(2)
203+
})
204+
205+
it('clear', () => {
206+
const sm = new SortedMap()
207+
sm.set('one', 1)
208+
sm.set('two', 2)
209+
sm.set('three', 3)
210+
211+
expect(sm.get('two')).to.eql(2)
212+
213+
sm.clear('two')
214+
215+
expect(sm.get('two')).to.be.undefined()
216+
expect(sm.size).to.eql(0)
217+
expect([...sm.keys()]).to.eql([])
218+
})
219+
220+
it('default order', () => {
221+
const sm = new SortedMap()
222+
223+
sm.set(1, 'a')
224+
sm.set(3, 'c')
225+
sm.set(2, 'b')
226+
227+
expect(sm.size).to.eql(3)
228+
expect([...sm.keys()]).to.eql([1, 2, 3])
229+
expect([...sm.values()]).to.eql(['a', 'b', 'c'])
230+
expect([...sm.entries()]).to.eql([[1, 'a'], [2, 'b'], [3, 'c']])
231+
expect([...sm]).to.eql([...sm.entries()])
232+
233+
const collected = []
234+
sm.forEach(i => { collected.push(i) })
235+
expect(collected).to.eql([...sm])
236+
})
237+
238+
it('custom order', () => {
239+
const sm = new SortedMap([], (a, b) => b[1].priority - a[1].priority)
240+
241+
const data1 = { k: 'v1', priority: 1 }
242+
const data2 = { k: 'v2', priority: 3 }
243+
const data3 = { k: 'v3', priority: 2 }
244+
sm.set('one', data1)
245+
sm.set('two', data2)
246+
sm.set('three', data3)
247+
248+
expect([...sm.keys()]).to.eql(['two', 'three', 'one'])
249+
expect([...sm.values()].map(v => v.k)).to.eql(['v2', 'v3', 'v1'])
250+
251+
// After changing data that affects the sort order, need to call update
252+
// to actually trigger the sort
253+
data3.priority = 5
254+
sm.update('three')
255+
256+
expect([...sm.keys()]).to.eql(['three', 'two', 'one'])
257+
expect([...sm.values()].map(v => v.k)).to.eql(['v3', 'v2', 'v1'])
258+
})
259+
})
168260
})

0 commit comments

Comments
 (0)