@yookue/ts-multi-map
Version:
Multiple key/value map & range map for typescript
585 lines (583 loc) • 16.1 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/util/RangeMap.ts
var RangeMap_exports = {};
__export(RangeMap_exports, {
RangeMap: () => RangeMap
});
module.exports = __toCommonJS(RangeMap_exports);
var import_object_hash = __toESM(require("object-hash"));
var RangeMap = class {
/**
* Construct a range map instance
*
* @param entries the map entries
* @param validation whether to compare the ranges to all the ranges previously, to determine if there are any conflicts
*
* @example
* ```ts
* const map = new RangeMap([
* [[1, 30], 'green'],
* [[30, 60], 'blue'],
* [[60, 90], 'orange'],
* [[90, 100, true, true], 'red']
* ]);
* ```
*/
constructor(entries, validation = true) {
this.keyMap = /* @__PURE__ */ new Map();
this.valueMap = /* @__PURE__ */ new Map();
entries == null ? void 0 : entries.forEach((entry) => {
const [k, v] = entry;
this.set(k, v, validation);
});
}
/**
* Construct a range map instance
*
* @param entries the map entries
* @param validation whether to compare the ranges to all the ranges previously, to determine if there are any conflicts
*
* @returns a range map instance
*
* @example
* ```ts
* const map = RangeMap.of([
* [[1, 30], 'green'],
* [[30, 60], 'blue'],
* [[60, 90], 'orange'],
* [[90, 100, true, true], 'red']
* ]);
* ```
*/
static of(entries, validation = true) {
return new RangeMap(entries, validation);
}
/**
* Returns the value of the given key
*
* @param key the key to retrieve
* @param defaults the default value if not found
*
* @returns the value of the given key
*
* @example
* ```ts
* const map = RangeMap.of([
* [[1, 50, true, true], 'white'],
* [[51, 100, false, true], 'black']
* ]);
* map.get([1, 50, true, true]); // 'white'
* map.get([51, 100, false, true]); // 'black'
* ```
*/
get(key, defaults) {
const hash = (0, import_object_hash.default)(this.toInternalKey(key));
return this.valueMap.get(hash) ?? defaults;
}
/**
* Returns the value that associated to the number, by determining which bound contains the given number
*
* @param digit the number to retrieve
* @param defaults the default value if not found
*
* @returns the value that associated to the number, by determining which bound contains the given number
*
* @example
* ```ts
* const map = RangeMap.of([
* [[1, 50, true, true], 'white'],
* [[51, 100, false, true], 'black']
* ]);
* map.getByDigit(25); // 'white'
* map.getByDigit(50); // 'white'
* map.getByDigit(51); // undefined
* map.getByDigit(52); // 'black'
* map.getByDigit(200); // undefined
* ```
*/
getByDigit(digit, defaults) {
if (this.isEmpty()) {
return defaults;
}
for (const [k, v] of this.keyMap.entries()) {
if ((v.startInclusive ? digit >= v.start : digit > v.start) && (v.endInclusive ? digit <= v.end : digit < v.end)) {
return this.valueMap.get(k);
}
}
return defaults;
}
/**
* Sets the value of the given key
*
* @param key the key to set
* @param value the value to set
* @param validation whether to compare the range to all the ranges previously, to determine if there are any conflicts
*
* @example
* ```ts
* map.set([60, 80, true, true], 'volcano');
* ```
*/
set(key, value, validation = true) {
const alias = this.toInternalKey(key);
if (alias.start > alias.end || alias.start === alias.end && !alias.startInclusive && !alias.endInclusive) {
throw RangeError(`Range start ${alias.start} must be less than or equal to it's end ${alias.end}`);
}
if (validation) {
this.checkRangeConflict(alias);
}
const hash = (0, import_object_hash.default)(alias);
this.keyMap.set(hash, alias);
this.valueMap.set(hash, value);
}
/**
* Clears the map
*/
clear() {
this.keyMap.clear();
this.valueMap.clear();
}
/**
* Returns the keys of the map
*
* @returns the keys of the map
*/
keys() {
return [...this.keyMap.values()];
}
/**
* Returns the value array of the map
*
* @returns the value array of the map
*/
values() {
return [...this.valueMap.values()];
}
/**
* Returns the key/value entries of the map
*
* @returns the key/value entries of the map
*/
entries() {
if (this.isEmpty()) {
return [];
}
if (this.keyMap.size !== this.valueMap.size) {
throw EvalError(`Internal maps size mismatch!`);
}
const result = [];
for (const [k, v] of this.valueMap) {
result.push([this.keyMap.get(k), v]);
}
return result;
}
/**
* Deletes the entry with the given key
*
* @param key the key to delete
*
* @returns whether the entry has been deleted
*
* @example
* ```ts
* map.deleteByKey([1, 50]);
* ```
*/
deleteByKey(key) {
if (this.isEmpty()) {
return false;
}
const hash = (0, import_object_hash.default)(this.toInternalKey(key));
return this.keyMap.delete(hash) && this.valueMap.delete(hash);
}
/**
* Deletes all the entries with the given keys
*
* @param keys the keys to delete
*
* @returns whether any of the entries has been deleted
*
* @example
* ```ts
* map.deleteByKeys([[1, 30], [30, 50]]);
* ```
*/
deleteByKeys(keys) {
if (!keys.length || this.isEmpty()) {
return false;
}
let result = false;
for (const key of keys) {
if (this.deleteByKey(key)) {
result = true;
}
}
return result;
}
/**
* Deletes the entry/entries with the given value
*
* @param value the value to delete
*
* @returns whether the entry/entries has been deleted
*
* @example
* ```ts
* map.deleteByValue('red');
* ```
*/
deleteByValue(value) {
if (this.isEmpty()) {
return false;
}
let result = false;
for (const [k, v] of this.valueMap.entries()) {
if (v === value && this.keyMap.delete(k) && this.valueMap.delete(k)) {
result = true;
}
}
return result;
}
/**
* Deletes all the entries with the given values
*
* @param values the values to delete
*
* @returns whether any of the entries has been deleted
*
* @example
* ```ts
* map.deleteByValues(['green', 'blue']);
* ```
*/
deleteByValues(values) {
if (!values.length || this.isEmpty()) {
return false;
}
let result = false;
for (const value of values) {
if (this.deleteByValue(value)) {
result = true;
}
}
return result;
}
/**
* Processes each entry in the map
*
* @param callback a callback function that processes each entry
* @param thisArg any instance to retrieve 'this' reference in the callback function
*
* @example
* ```ts
* map.forEach((value, key) => {
* console.log(value);
* });
* ```
*/
forEach(callback, thisArg) {
this.entries().forEach((entry) => {
const [k, v] = entry;
callback(v, k);
}, thisArg);
}
/**
* Processes each entry in the map with index capability
*
* @param callback a callback function that processes each entry
* @param thisArg any instance to retrieve 'this' reference in the callback function
*
* @example
* ```ts
* map.forEachIndexing((value, key, index) => {
* console.log(index);
* });
* ```
*/
forEachIndexing(callback, thisArg) {
let index = 0;
this.entries().forEach((entry) => {
const [k, v] = entry;
callback(v, k, index++);
}, thisArg);
}
/**
* Processes each entry in the map with breakable capability
*
* @param callback a callback function that processes each entry. Returning false indicates to break the map iteration
* @param thisArg any instance to retrieve 'this' reference in the callback function
*
* @example
* ```ts
* map.forEachBreakable((value, key) => {
* return true;
* });
* ```
*/
forEachBreakable(callback, thisArg) {
this.entries().forEach((entry) => {
const [k, v] = entry;
if (!callback(v, k)) {
return;
}
}, thisArg);
}
/**
* Returns whether the map contains the given key
*
* @param key the key to check
*
* @returns whether the map contains the given key
*
* @example
* ```ts
* map.hasKey([1, 30]);
* ```
*/
hasKey(key) {
return this.isNotEmpty() && this.keyMap.has((0, import_object_hash.default)(this.toInternalKey(key)));
}
/**
* Returns whether the map contains the given key/value pair
*
* @param key the key to check
* @param value the value to check
*
* @returns whether the map contains the given key/value pair
*
* @example
* ```ts
* const map = RangeMap.of([
* [[1, 50], 'white']
* ]);
* map.hasKeyValue([1, 50], 'white'); // true
* map.hasKeyValue([1, 50], 'black'); // false
* ```
*/
hasKeyValue(key, value) {
return this.isNotEmpty() && this.get(key) === value;
}
/**
* Returns whether the map contains any of the given keys
*
* @param keys the keys to check
*
* @returns whether the map contains any of the given keys
*
* @example
* ```ts
* const map = RangeMap.of([
* [[1, 50, true, true], 'white'],
* [[51, 100, false, true], 'black']
* ]);
* map.hasAnyKeys([[1, 50, true, true], [20, 60]]); // true
* ```
*/
hasAnyKeys(keys) {
return this.isNotEmpty() && keys.length > 0 && keys.some((item) => this.hasKey(item));
}
/**
* Returns whether the map contains all the given keys
*
* @param keys the keys to check
*
* @returns whether the map contains all the given keys
*
* @example
* ```ts
* map.hasAllKeys([[1, 50], [51, 100]]);
* ```
*/
hasAllKeys(keys) {
return this.isNotEmpty() && keys.length > 0 && keys.every((item) => this.hasKey(item));
}
/**
* Returns whether any entries of the map that contains the given values
*
* @param value the value to check
*
* @returns whether any entries of the map that contains the given values
*
* @example
* ```ts
* const map = RangeMap.of([
* [[1, 50, true, true], 'white'],
* [[51, 100, false, true], 'black']
* ]);
* map.hasValue('white'); // true
* map.hasValue('blue'); // false
* ```
*/
hasValue(value) {
return this.isNotEmpty() && this.values().includes(value);
}
/**
* Returns whether the map contains any of the given values
*
* @param values the values to check
*
* @returns whether the map contains any of the given values
*
* @example
* ```ts
* const map = RangeMap.of([
* [[1, 50, true, true], 'white'],
* [[51, 100, false, true], 'black']
* ]);
* map.hasAnyValues(['red', 'black']); // true
* map.hasAnyValues(['top', 'right']); // false
* ```
*/
hasAnyValues(values) {
return this.isNotEmpty() && values.length > 0 && values.some((item) => this.hasValue(item));
}
/**
* Returns whether the map contains all the given values, matching exactly
*
* @param values the values to check
*
* @returns whether the map contains all the given values, matching exactly
*
* @example
* ```ts
* map.hasAllValues(['red', 'green', 'blue']);
* ```
*/
hasAllValues(values) {
return this.isNotEmpty() && values.length > 0 && values.every((item) => this.hasValue(item));
}
/**
* Returns whether the map is empty
*
* @returns whether the map is empty
*/
isEmpty() {
return !this.valueMap.size;
}
/**
* Returns whether the map is not empty
*
* @returns whether the map is not empty
*/
isNotEmpty() {
return this.valueMap.size > 0;
}
/**
* Response for returning the list of key/value to iterate
*
* @example
* ```ts
* for (const [key, value] of map) {
* console.log(value);
* }
* ```
*/
// @ts-ignore
[Symbol.iterator]() {
return this.entries()[Symbol.iterator]();
}
/**
* Returns the size of map
*
* @returns the size of map
*/
get size() {
return this.valueMap.size;
}
/**
* Returns the string representation of the map identifier ('RangeMap')
*
* @returns the string representation of the map identifier
*/
get [Symbol.toStringTag]() {
return "RangeMap";
}
/**
* Returns the string representation of the map elements
*
* @returns the string representation of the map elements
*
* @example
* ```ts
* const map = RangeMap.of([
* [[1, 50, true, true], 'white'],
* [[51, 100, false, true], 'black']
* ]);
* console.log(map.toString()); // '[start:1,end:50,startInclusive:true,endInclusive:true]:white;[start:51,end:100,startInclusive:false,endInclusive:true]:black'
* ```
*/
toString() {
return [...this].map((entry) => {
const [k, v] = entry;
const ks = Object.keys(k).map((item) => `${item}:${k[item]}`).join();
return `[${ks}]:${v}`;
}).join(";");
}
/**
* Converts the given key to internal one, for storage
*
* @param key the key to convert
*
* @returns the converted key
*/
toInternalKey(key) {
return !Array.isArray(key) ? key : {
start: key.length > 0 ? key[0] : 0,
end: key.length > 1 ? key[1] : 0,
startInclusive: key.length > 2 ? key[2] : true,
endInclusive: key.length > 3 ? key[3] : false
};
}
/**
* Checks the given key is conflicting with the ranges previously
*
* @param key the key to inspect
*/
checkRangeConflict(key) {
if (this.isEmpty()) {
return;
}
const keyExpress = `${key.startInclusive ? "[" : "("}${key.start},${key.end}${key.endInclusive ? "]" : ")"}`;
for (const k of this.keyMap.values()) {
const kExpress = `${k.startInclusive ? "[" : "("}${k.start},${k.end}${k.endInclusive ? "]" : ")"}`;
if (key.start <= k.start && key.end >= k.end) {
throw RangeError(`Range ${keyExpress} must not override the range ${kExpress}`);
}
const badStart = (key.startInclusive ? key.start >= k.start : key.start > k.start) && (key.startInclusive && k.endInclusive ? key.start <= k.end : key.start < k.end);
const badEnd = (key.endInclusive && k.startInclusive ? key.end >= k.start : key.end > k.start) && key.end <= k.end;
if (badStart || badEnd) {
throw RangeError(`Range ${keyExpress} must not intersect the range ${kExpress}`);
}
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
RangeMap
});