@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
185 lines (184 loc) • 4.99 kB
JavaScript
import { _assert } from '../error/index.js';
/**
* Maintains sorted array of keys.
* Sorts **data access**, not on insertion.
*
* @experimental
*/
export class LazyKeySortedMap {
map;
maybeSortedKeys;
keysAreSorted = false;
constructor(entries = [], opt = {}) {
this.#comparator = opt.comparator;
this.map = new Map(entries);
this.maybeSortedKeys = [...this.map.keys()];
}
#comparator;
/**
* Convenience way to create KeySortedMap from object.
*/
static of(obj) {
return new LazyKeySortedMap(Object.entries(obj));
}
get size() {
return this.map.size;
}
clear() {
this.map.clear();
this.maybeSortedKeys.length = 0;
this.keysAreSorted = true;
}
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.maybeSortedKeys.push(k);
}
this.keysAreSorted = false;
return this;
}
/**
* Insert or update. Keeps keys array sorted at all times.
* Returns this (Map-like).
*/
set(key, value) {
if (!this.map.has(key)) {
this.maybeSortedKeys.push(key);
this.keysAreSorted = false;
}
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);
// Delete operation keeps the array **as-is**, it may have been sorted or not.
const j = this.maybeSortedKeys.indexOf(key);
if (j !== -1)
this.maybeSortedKeys.splice(j, 1);
return true;
}
/**
* Iterables (Map-compatible), all in sorted order.
*/
*keys() {
for (const key of this.getSortedKeys()) {
yield key;
}
}
*values() {
for (const key of this.getSortedKeys()) {
yield this.map.get(key);
}
}
*entries() {
for (const k of this.getSortedKeys()) {
yield [k, this.map.get(k)];
}
}
[Symbol.iterator]() {
return this.entries();
}
[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.getSortedKeys()) {
cb.call(thisArg, map.get(k), k, this);
}
}
firstKeyOrUndefined() {
return this.getSortedKeys()[0];
}
firstKey() {
_assert(this.maybeSortedKeys.length, 'Map.firstKey called on empty map');
return this.getSortedKeys()[0];
}
lastKeyOrUndefined() {
if (!this.maybeSortedKeys.length)
return;
const keys = this.getSortedKeys();
return keys[keys.length - 1];
}
lastKey() {
const k = this.lastKeyOrUndefined();
_assert(k, 'Map.lastKey called on empty map');
return k;
}
firstValueOrUndefined() {
if (!this.maybeSortedKeys.length)
return;
return this.map.get(this.getSortedKeys()[0]);
}
firstValue() {
const v = this.firstValueOrUndefined();
_assert(v, 'Map.firstValue called on empty map');
return v;
}
lastValueOrUndefined() {
if (!this.maybeSortedKeys.length)
return;
const keys = this.getSortedKeys();
return this.map.get(keys[keys.length - 1]);
}
lastValue() {
const v = this.lastValueOrUndefined();
_assert(v, 'Map.lastValue called on empty map');
return v;
}
firstEntryOrUndefined() {
if (!this.maybeSortedKeys.length)
return;
const k = this.getSortedKeys()[0];
return [k, this.map.get(k)];
}
firstEntry() {
const e = this.firstEntryOrUndefined();
_assert(e, 'Map.firstEntry called on empty map');
return e;
}
lastEntryOrUndefined() {
if (!this.maybeSortedKeys.length)
return;
const keys = this.getSortedKeys();
const k = keys[keys.length - 1];
return [k, this.map.get(k)];
}
lastEntry() {
const e = this.firstEntryOrUndefined();
_assert(e, 'Map.lastEntry called on empty map');
return e;
}
toJSON() {
return this.toObject();
}
toObject() {
return Object.fromEntries(this.entries());
}
getSortedKeys() {
if (!this.keysAreSorted) {
return this.sortKeys();
}
return this.maybeSortedKeys;
}
sortKeys() {
this.maybeSortedKeys.sort(this.#comparator);
this.keysAreSorted = true;
return this.maybeSortedKeys;
}
}