@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
214 lines (213 loc) • 6.18 kB
JavaScript
import { _assert } from '../error/index.js';
/**
* Maintains sorted array of keys.
* Sorts **on insertion**, not on retrieval.
*
* - set(): O(log n) search + O(n) splice only when inserting a NEW key
* - get/has: O(1)
* - delete: O(log n) search + O(n) splice if present
* - iteration: O(n) over pre-sorted keys (no sorting at iteration time)
*
* @experimental
*/
export class KeySortedMap {
map;
#sortedKeys;
constructor(entries = [], opt = {}) {
this.#comparator = opt.comparator;
this.map = new Map(entries);
this.#sortedKeys = [...this.map.keys()];
this.sortKeys();
}
#comparator;
/**
* Convenience way to create KeySortedMap from object.
*/
static of(obj) {
return new KeySortedMap(Object.entries(obj));
}
get size() {
return this.map.size;
}
clear() {
this.map.clear();
this.#sortedKeys.length = 0;
}
has(key) {
return this.map.has(key);
}
get(key) {
return this.map.get(key);
}
/**
* Allows to set multiple key-value pairs at once.
*/
setMany(obj) {
for (const [k, v] of Object.entries(obj)) {
this.map.set(k, v);
this.#sortedKeys.push(k);
}
// Resort all at once
this.sortKeys();
return this;
}
/**
* Insert or update. Keeps keys array sorted at all times.
* Returns this (Map-like).
*/
set(key, value) {
if (this.map.has(key)) {
// Update only; position unchanged.
this.map.set(key, value);
return this;
}
// Find insertion index (lower_bound).
const i = this.lowerBound(key);
// Only insert into keys when actually new.
this.#sortedKeys.splice(i, 0, key);
this.map.set(key, value);
return this;
}
/**
* Delete by key. Returns boolean like Map.delete.
*/
delete(key) {
if (!this.map.has(key))
return false;
this.map.delete(key);
// Remove from keys using binary search to avoid O(n) find.
const i = this.lowerBound(key);
// Because key existed, it must be at i.
if (i < this.#sortedKeys.length && this.#sortedKeys[i] === key) {
this.#sortedKeys.splice(i, 1);
}
else {
// Extremely unlikely if external mutation happened; safe guard.
// Fall back to linear search (shouldn't happen).
const j = this.#sortedKeys.indexOf(key);
if (j !== -1)
this.#sortedKeys.splice(j, 1);
}
return true;
}
/**
* Iterables (Map-compatible), all in sorted order.
*/
*keys() {
for (const key of this.#sortedKeys) {
yield key;
}
}
*values() {
for (const key of this.#sortedKeys) {
yield this.map.get(key);
}
}
*entries() {
for (const k of this.#sortedKeys) {
yield [k, this.map.get(k)];
}
}
[Symbol.iterator]() {
return this.entries();
}
toString() {
console.log('toString called !!!!!!!!!!!!!!!!!!!!!');
return 'abc';
}
[Symbol.toStringTag] = 'KeySortedMap';
/**
* Zero-allocation callbacks over sorted data (faster than spreading to arrays).
*/
forEach(cb, thisArg) {
const { map } = this;
for (const k of this.#sortedKeys) {
cb.call(thisArg, map.get(k), k, this);
}
}
firstKeyOrUndefined() {
return this.#sortedKeys[0];
}
firstKey() {
_assert(this.#sortedKeys.length, 'Map.firstKey called on empty map');
return this.#sortedKeys[0];
}
lastKeyOrUndefined() {
return this.#sortedKeys.length ? this.#sortedKeys[this.#sortedKeys.length - 1] : undefined;
}
lastKey() {
_assert(this.#sortedKeys.length, 'Map.lastKey called on empty map');
return this.#sortedKeys[this.#sortedKeys.length - 1];
}
firstValueOrUndefined() {
return this.map.get(this.#sortedKeys[0]);
}
firstValue() {
_assert(this.#sortedKeys.length, 'Map.firstValue called on empty map');
return this.map.get(this.#sortedKeys[0]);
}
lastValueOrUndefined() {
return this.#sortedKeys.length
? this.map.get(this.#sortedKeys[this.#sortedKeys.length - 1])
: undefined;
}
lastValue() {
_assert(this.#sortedKeys.length, 'Map.lastValue called on empty map');
return this.map.get(this.#sortedKeys[this.#sortedKeys.length - 1]);
}
firstEntryOrUndefined() {
if (!this.#sortedKeys.length)
return;
const k = this.#sortedKeys[0];
return [k, this.map.get(k)];
}
firstEntry() {
_assert(this.#sortedKeys.length, 'Map.firstEntry called on empty map');
const k = this.#sortedKeys[0];
return [k, this.map.get(k)];
}
lastEntryOrUndefined() {
if (!this.#sortedKeys.length)
return;
const k = this.#sortedKeys[this.#sortedKeys.length - 1];
return [k, this.map.get(k)];
}
lastEntry() {
_assert(this.#sortedKeys.length, 'Map.lastEntry called on empty map');
const k = this.#sortedKeys[this.#sortedKeys.length - 1];
return [k, this.map.get(k)];
}
toJSON() {
return this.toObject();
}
toObject() {
return Object.fromEntries(this.entries());
}
/**
* Clones the KeySortedMap into ordinary Map.
*/
toMap() {
return new Map(this.entries());
}
/**
* lowerBound: first index i s.t. keys[i] >= target
*/
lowerBound(target) {
let lo = 0;
let hi = this.#sortedKeys.length;
while (lo < hi) {
// oxlint-disable-next-line no-bitwise
const mid = (lo + hi) >>> 1;
if (this.#sortedKeys[mid] < target) {
lo = mid + 1;
}
else {
hi = mid;
}
}
return lo;
}
sortKeys() {
this.#sortedKeys.sort(this.#comparator);
}
}