froebel
Version:
TypeScript utility library
116 lines (98 loc) • 3.13 kB
JavaScript
/**
* Behaves like a regular JavaScript `Map`, but its iteration order is dependant
* on the `compare` function supplied in the constructor.
*
* Note: The item's sort position is only computed automatically on insertion.
* If you update one of the values that the `compare` function depends on, you
* must call the `update(key)` method afterwards to ensure the map stays sorted.
*/
export default class SortedMap {
constructor(compare, entries) {
this.#cmp = compare;
if (!(entries !== null && entries !== void 0 && entries.length)) return;
const sorted = [...entries].sort(([ka, va], [kb, vb]) => compare(va, vb, ka, kb));
for (let i = 0; i < entries.length; i++) {
this.#map.set(...sorted[i]);
this.#order.push(i);
}
}
#cmp;
#map = new Map();
#order = [];
#bind = method => this.#map[method].bind(this.#map);
clear = this.#bind("clear");
get = this.#bind("get");
has = this.#bind("has");
set(key, value) {
if (this.#map.has(key)) this.delete(key);
let i = 0;
for (const [k, v] of this) {
if (this.#cmp(value, v, key, k) < 0) break;
i++;
}
this.#order.splice(i, 0, this.size);
this.#map.set(key, value);
return this;
}
delete(key) {
if (this.#map.has(key)) {
const i = [...this.#map.keys()].indexOf(key);
this.#order.splice(this.#order.indexOf(i), 1);
this.#order = this.#order.map(n => n < i ? n : n - 1);
}
return this.#map.delete(key);
}
/**
* Update the sort position of the element at `key` if necessary.
*
* This method should be called to notify the SortedMap that one of the
* parameters that the `compare` function depends on has been updated and
* consequently the sort order must be verified/updated.
*
* @returns `true` if the sort position of the element with `key` had to be
* updated, `false` if not.
*/
update(key) {
const entries = [...this.#map.entries()];
const i = entries.findIndex(([k]) => k === key);
const oi = this.#order.indexOf(i);
const e = entries[i];
const l = entries[this.#order[oi - 1]];
const r = entries[this.#order[oi + 1]];
if ((!l || this.#cmp(l[1], e[1], l[0], e[0]) <= 0) && (!r || this.#cmp(e[1], r[1], e[0], r[0]) <= 0)) {
return false;
}
this.set(...e);
return true;
}
forEach(callback) {
for (const [k, v] of this.entries()) callback(v, k, this);
}
map(callback) {
return [...this].map(([k, v]) => callback(v, k, this));
}
get size() {
return this.#map.size;
}
#orderIter(iter) {
const items = [...iter];
let i = -1;
return {
next: () => {
i++;
return {
done: i >= items.length,
value: items[this.#order[i]]
};
},
[Symbol.iterator]() {
return this;
}
};
}
[Symbol.iterator] = () => this.#orderIter(this.#map);
entries = () => this.#orderIter(this.#map.entries());
keys = () => this.#orderIter(this.#map.keys());
values = () => this.#orderIter(this.#map.values());
[Symbol.toStringTag] = this.#map[Symbol.toStringTag];
}