ts-data-forge
Version:
[](https://www.npmjs.com/package/ts-data-forge) [](https://www.npmjs.com/package/ts-data-forge) [ • 16.7 kB
JavaScript
import { Optional } from '../functional/optional.mjs';
import { pipe } from '../functional/pipe.mjs';
import '../functional/result.mjs';
import '../number/branded-types/finite-number.mjs';
import '../number/branded-types/int.mjs';
import '../number/branded-types/int16.mjs';
import '../number/branded-types/int32.mjs';
import '../number/branded-types/non-negative-finite-number.mjs';
import '../number/branded-types/non-negative-int16.mjs';
import '../number/branded-types/non-negative-int32.mjs';
import '../number/branded-types/non-zero-finite-number.mjs';
import '../number/branded-types/non-zero-int.mjs';
import '../number/branded-types/non-zero-int16.mjs';
import '../number/branded-types/non-zero-int32.mjs';
import '../number/branded-types/non-zero-safe-int.mjs';
import '../number/branded-types/non-zero-uint16.mjs';
import '../number/branded-types/non-zero-uint32.mjs';
import '../number/branded-types/positive-finite-number.mjs';
import '../number/branded-types/positive-int.mjs';
import '../number/branded-types/positive-int16.mjs';
import '../number/branded-types/positive-int32.mjs';
import '../number/branded-types/positive-safe-int.mjs';
import '../number/branded-types/positive-uint16.mjs';
import '../number/branded-types/positive-uint32.mjs';
import '../number/branded-types/safe-int.mjs';
import '../number/branded-types/safe-uint.mjs';
import '../number/branded-types/uint.mjs';
import '../number/branded-types/uint16.mjs';
import { asUint32 } from '../number/branded-types/uint32.mjs';
import '../number/enum/int8.mjs';
import '../number/enum/uint8.mjs';
import '../number/num.mjs';
import '../number/refined-number-utils.mjs';
import { tp } from '../others/tuple.mjs';
/**
* Provides utility functions for IMapMapped.
*/
var IMapMapped;
(function (IMapMapped) {
/**
* Creates a new IMapMapped instance with custom key transformation functions.
*
* This factory function creates an immutable map that can use complex objects as keys
* by providing bidirectional transformation functions. The `toKey` function converts
* your custom key type to a primitive type that can be efficiently stored, while
* `fromKey` reconstructs the original key type for iteration and access.
*
* **Performance:** O(n) where n is the number of entries in the iterable.
*
* @template K The type of the custom keys.
* @template V The type of the values.
* @template KM The type of the mapped primitive keys.
* @param iterable An iterable of key-value pairs using the custom key type.
* @param toKey A function that converts a custom key `K` to a primitive key `KM`.
* This function must be deterministic and produce unique values for unique keys.
* @param fromKey A function that converts a primitive key `KM` back to the custom key `K`.
* This should be the inverse of `toKey`.
* @returns A new IMapMapped instance containing all entries from the iterable.
*
* @example
* ```typescript
* // Example 1: Geographic coordinates as keys
* type Coordinate = { lat: number; lng: number };
* type LocationInfo = { name: string; population: number };
*
* const coordToString = (coord: Coordinate): string => `${coord.lat},${coord.lng}`;
* const stringToCoord = (str: string): Coordinate => {
* const [lat, lng] = str.split(',').map(Number);
* return { lat, lng };
* };
*
* const locationMap = IMapMapped.create<Coordinate, LocationInfo, string>(
* [
* [{ lat: 40.7128, lng: -74.0060 }, { name: "New York", population: 8000000 }],
* [{ lat: 34.0522, lng: -118.2437 }, { name: "Los Angeles", population: 4000000 }]
* ],
* coordToString,
* stringToCoord
* );
*
* const nyCoord = { lat: 40.7128, lng: -74.0060 };
* console.log(locationMap.get(nyCoord).unwrap().name); // Output: "New York"
*
* // Example 2: Multi-part business keys
* type OrderId = { customerId: string; year: number; orderNumber: number };
*
* const orderIdToKey = (id: OrderId): string =>
* `${id.customerId}:${id.year}:${id.orderNumber}`;
*
* const keyToOrderId = (key: string): OrderId => {
* const [customerId, yearStr, orderNumStr] = key.split(':');
* return {
* customerId,
* year: Number(yearStr),
* orderNumber: Number(orderNumStr)
* };
* };
*
* const orderMap = IMapMapped.create<OrderId, Order, string>(
* [],
* orderIdToKey,
* keyToOrderId
* );
*
* // Example 3: Simple case with string keys (identity transformation)
* const simpleMap = IMapMapped.create<string, number, string>(
* [["key1", 100], ["key2", 200]],
* (s) => s, // identity function
* (s) => s // identity function
* );
*
* // Example 4: From existing data structures
* const existingEntries = new Map([
* [{ id: 1, type: "user" }, { name: "Alice", active: true }],
* [{ id: 2, type: "user" }, { name: "Bob", active: false }]
* ]);
*
* type EntityKey = { id: number; type: string };
* const entityKeyToString = (key: EntityKey): string => `${key.type}_${key.id}`;
* const stringToEntityKey = (str: string): EntityKey => {
* const [type, idStr] = str.split('_');
* return { type, id: Number(idStr) };
* };
*
* const entityMap = IMapMapped.create<EntityKey, Entity, string>(
* existingEntries,
* entityKeyToString,
* stringToEntityKey
* );
* ```
*/
IMapMapped.create = (iterable, toKey, fromKey) => new IMapMappedClass(iterable, toKey, fromKey);
/**
* Checks if two IMapMapped instances are structurally equal.
*
* Two IMapMapped instances are considered equal if they have the same size and contain
* exactly the same key-value pairs. The comparison is performed on the underlying mapped
* keys and values, so the transformation functions themselves don't need to be identical.
* Values are compared using JavaScript's `===` operator.
*
* **Performance:** O(n) where n is the size of the smaller map.
*
* @template K The type of the custom keys.
* @template V The type of the values.
* @template KM The type of the mapped primitive keys.
* @param a The first IMapMapped instance to compare.
* @param b The second IMapMapped instance to compare.
* @returns `true` if the maps contain exactly the same key-value pairs, `false` otherwise.
*
* @example
* ```typescript
* // Example with coordinate keys
* type Point = { x: number; y: number };
* const pointToString = (p: Point): string => `${p.x},${p.y}`;
* const stringToPoint = (s: string): Point => {
* const [x, y] = s.split(',').map(Number);
* return { x, y };
* };
*
* const map1 = IMapMapped.create<Point, string, string>(
* [[{ x: 1, y: 2 }, "point1"], [{ x: 3, y: 4 }, "point2"]],
* pointToString,
* stringToPoint
* );
*
* const map2 = IMapMapped.create<Point, string, string>(
* [[{ x: 1, y: 2 }, "point1"], [{ x: 3, y: 4 }, "point2"]], // Same content
* pointToString,
* stringToPoint
* );
*
* const map3 = IMapMapped.create<Point, string, string>(
* [[{ x: 1, y: 2 }, "point1"], [{ x: 3, y: 4 }, "different"]], // Different value
* pointToString,
* stringToPoint
* );
*
* console.log(IMapMapped.equal(map1, map2)); // true
* console.log(IMapMapped.equal(map1, map3)); // false (different value)
*
* // Order doesn't matter for equality
* const map4 = IMapMapped.create<Point, string, string>(
* [[{ x: 3, y: 4 }, "point2"], [{ x: 1, y: 2 }, "point1"]], // Different order
* pointToString,
* stringToPoint
* );
*
* console.log(IMapMapped.equal(map1, map4)); // true
*
* // Different transformation functions but same logical content
* const alternativePointToString = (p: Point): string => `(${p.x},${p.y})`; // Different format
* const alternativeStringToPoint = (s: string): Point => {
* const match = s.match(/\((\d+),(\d+)\)/);
* return { x: Number(match![1]), y: Number(match![2]) };
* };
*
* const map5 = IMapMapped.create<Point, string, string>(
* [[{ x: 1, y: 2 }, "point1"], [{ x: 3, y: 4 }, "point2"]],
* alternativePointToString,
* alternativeStringToPoint
* );
*
* // This would be false because the underlying mapped keys are different
* // even though the logical content is the same
* console.log(IMapMapped.equal(map1, map5)); // false
*
* // Empty maps
* const empty1 = IMapMapped.create<Point, string, string>([], pointToString, stringToPoint);
* const empty2 = IMapMapped.create<Point, string, string>([], pointToString, stringToPoint);
* console.log(IMapMapped.equal(empty1, empty2)); // true
* ```
*/
IMapMapped.equal = (a, b) => a.size === b.size && a.every((v, k) => b.get(k) === v);
})(IMapMapped || (IMapMapped = {}));
/**
* Internal class implementation for IMapMapped providing immutable map operations with key transformation.
*
* This class implements the IMapMapped interface by maintaining a JavaScript Map with primitive keys
* internally while exposing an API that works with custom key types. The transformation between
* custom and primitive keys is handled transparently through the provided `toKey` and `fromKey` functions.
*
* **Implementation Details:**
* - Uses ReadonlyMap<KM, V> internally where KM is the primitive key type
* - Stores transformation functions for bidirectional key conversion
* - Implements copy-on-write semantics for efficiency
* - Provides optional debug messaging for development
*
* @template K The type of the custom keys.
* @template V The type of the values.
* @template KM The type of the mapped primitive keys.
* @implements IMapMapped
* @implements Iterable
* @internal This class should not be used directly. Use IMapMapped.create() instead.
*/
class IMapMappedClass {
#map;
#toKey;
#fromKey;
#showNotFoundMessage;
/**
* Constructs an IMapMappedClass instance with custom key transformation.
*
* @param iterable An iterable of key-value pairs using the custom key type K.
* @param toKey A function that converts a custom key K to a primitive key KM.
* Must be deterministic and produce unique values for unique keys.
* @param fromKey A function that converts a primitive key KM back to the custom key K.
* Should be the inverse of the toKey function.
* @param showNotFoundMessage Whether to log warning messages when operations
* are performed on non-existent keys. Useful for debugging.
* Defaults to false for production use.
* @internal Use IMapMapped.create() instead of calling this constructor directly.
*/
constructor(iterable, toKey, fromKey, showNotFoundMessage = false) {
this.#map = new Map(Array.from(iterable, ([k, v]) => [toKey(k), v]));
this.#toKey = toKey;
this.#fromKey = fromKey;
this.#showNotFoundMessage = showNotFoundMessage;
}
/** @inheritdoc */
get size() {
return asUint32(this.#map.size);
}
/** @inheritdoc */
has(key) {
return this.#map.has(this.#toKey(key));
}
/** @inheritdoc */
get(key) {
if (!this.has(key))
return Optional.none;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return Optional.some(this.#map.get(this.#toKey(key)));
}
/** @inheritdoc */
every(predicate) {
for (const [k, v] of this.entries()) {
if (!predicate(v, k))
return false;
}
return true;
}
/** @inheritdoc */
some(predicate) {
for (const [k, v] of this.entries()) {
if (predicate(v, k))
return true;
}
return false;
}
/** @inheritdoc */
delete(key) {
if (!this.has(key)) {
if (this.#showNotFoundMessage) {
console.warn(`IMapMapped.delete: key not found: ${String(this.#toKey(key))}`);
}
return this;
}
const keyMapped = this.#toKey(key);
return IMapMapped.create(Array.from(this.#map)
.filter(([km]) => !Object.is(km, keyMapped))
.map(([km, v]) => tp(this.#fromKey(km), v)), this.#toKey, this.#fromKey);
}
/** @inheritdoc */
set(key, value) {
if (value === this.get(key))
return this; // has no changes
const keyMapped = this.#toKey(key);
if (!this.has(key)) {
return IMapMapped.create([...this.#map, tp(keyMapped, value)].map(([km, v]) => tp(this.#fromKey(km), v)), this.#toKey, this.#fromKey);
}
else {
return IMapMapped.create(Array.from(this.#map, ([km, v]) => tp(this.#fromKey(km), Object.is(km, keyMapped) ? value : v)), this.#toKey, this.#fromKey);
}
}
/** @inheritdoc */
update(key, updater) {
const curr = this.get(key);
if (Optional.isNone(curr)) {
if (this.#showNotFoundMessage) {
console.warn(`IMapMapped.update: key not found: ${String(this.#toKey(key))}`);
}
return this;
}
const keyMapped = this.#toKey(key);
return IMapMapped.create(Array.from(this.#map.entries(), (keyValue) => pipe(keyValue)
.map(([km, v]) => tp(km, Object.is(km, keyMapped) ? updater(curr.value) : v))
.map(([km, v]) => tp(this.#fromKey(km), v)).value), this.#toKey, this.#fromKey);
}
/** @inheritdoc */
withMutations(actions) {
const mut_result = new Map(this.#map);
for (const action of actions) {
const key = this.#toKey(action.key);
switch (action.type) {
case 'delete':
mut_result.delete(key);
break;
case 'set':
mut_result.set(key, action.value);
break;
case 'update': {
const curr = mut_result.get(key);
if (!mut_result.has(key) || curr === undefined) {
if (this.#showNotFoundMessage) {
console.warn(`IMapMapped.withMutations::update: key not found: ${String(key)}`);
}
break;
}
mut_result.set(key, action.updater(curr));
break;
}
}
}
return IMapMapped.create(Array.from(mut_result, ([k, v]) => [this.#fromKey(k), v]), this.#toKey, this.#fromKey);
}
/** @inheritdoc */
map(mapFn) {
return IMapMapped.create(this.toArray().map(([k, v]) => tp(k, mapFn(v, k))), this.#toKey, this.#fromKey);
}
/** @inheritdoc */
mapKeys(mapFn) {
return IMapMapped.create(this.toArray().map(([k, v]) => tp(mapFn(k), v)), this.#toKey, this.#fromKey);
}
/** @inheritdoc */
mapEntries(mapFn) {
return IMapMapped.create(this.toArray().map(mapFn), this.#toKey, this.#fromKey);
}
/** @inheritdoc */
forEach(callbackfn) {
for (const [km, value] of this.#map.entries()) {
callbackfn(value, this.#fromKey(km));
}
}
/**
* @inheritdoc
*/
*[Symbol.iterator]() {
for (const e of this.entries()) {
yield e;
}
}
/** @inheritdoc */
*keys() {
for (const km of this.#map.keys()) {
yield this.#fromKey(km);
}
}
/** @inheritdoc */
*values() {
for (const v of this.#map.values()) {
yield v;
}
}
/** @inheritdoc */
*entries() {
for (const [km, v] of this.#map.entries()) {
yield [this.#fromKey(km), v];
}
}
/** @inheritdoc */
toKeysArray() {
return Array.from(this.keys());
}
/** @inheritdoc */
toValuesArray() {
return Array.from(this.values());
}
/** @inheritdoc */
toEntriesArray() {
return Array.from(this.entries());
}
/** @inheritdoc */
toArray() {
return Array.from(this.entries());
}
/** @inheritdoc */
toRawMap() {
return this.#map;
}
}
export { IMapMapped };
//# sourceMappingURL=imap-mapped.mjs.map