diff --git a/packages/associative/README.md b/packages/associative/README.md index b0d4829551..3f18625cae 100644 --- a/packages/associative/README.md +++ b/packages/associative/README.md @@ -7,7 +7,7 @@ [![Mastodon Follow](https://img.shields.io/mastodon/follow/109331703950160316?domain=https%3A%2F%2Fmastodon.thi.ng&style=social)](https://mastodon.thi.ng/@toxi) > [!NOTE] -> This is one of 192 standalone projects, maintained as part +> This is one of 193 standalone projects, maintained as part > of the [@thi.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo > and anti-framework. > @@ -15,6 +15,7 @@ > GitHub](https://github.com/sponsors/postspectacular). Thank you! ❤️ - [About](#about) + - [Why?](#why) - [Comparison with ES6 native types](#comparison-with-es6-native-types) - [Status](#status) - [Installation](#installation) @@ -43,14 +44,11 @@ Alternative Map and Set implementations with customizable equality semantics & s > > - [@thi.ng/bidir-index](https://thi.ng/bidir-index) > - [@thi.ng/object-utils](https://thi.ng/object-utils) +> - [@thi.ng/sorted-map](https://thi.ng/sorted-map) > - [@thi.ng/trie](https://thi.ng/trie) -- Array based `ArraySet`, Linked List based `LLSet`, - [Skiplist](https://en.wikipedia.org/wiki/Skip_list) based `SortedMap` & - `SortedSet` and customizable `EquivMap` implement the full ES6 Map/Set APIs - and additional features: - - range query iterators (via `entries()`, `keys()`, `values()`) (sorted - types only) +- Array based `ArraySet`, Linked List based `LLSet` and customizable `EquivMap` + & `HashMap` implementing the full ES6 Map/Set APIs and additional features: - `ICopy`, `IEmpty` & `IEquiv` implementations - `ICompare` implementation for sorted types - multiple value additions / updates / deletions via `into()`, `dissoc()` @@ -71,31 +69,33 @@ Please see these packages for some example use cases: - [@thi.ng/ecs](https://github.com/thi-ng/umbrella/tree/develop/packages/ecs) - [@thi.ng/rstream-query](https://github.com/thi-ng/umbrella/tree/develop/packages/rstream-query) -The native ES6 implementations use object reference identity to -determine key containment, but often it's more practical and useful to -use equivalent value semantics for this purpose, especially when keys -are structured data (arrays / objects). +### Why? -**Note**: It's the user's responsibility to ensure the inserted keys are -kept immutable (even if technically they're not). +The native ES6 implementations use **object reference** identity to determine +key containment, but often it's **more practical and useful to use equivalent +value semantics** for this purpose, especially when keys are structured data +(arrays / objects). + +**Note**: It's the user's responsibility to ensure the inserted keys are kept +immutable (even if technically they're not). ### Comparison with ES6 native types ```ts // first two objects w/ equal values -a = [1, 2]; -b = [1, 2]; +const a = [1, 2]; +const b = [1, 2]; ``` Using native implementations ```ts -set = new Set(); +const set = new Set(); set.add(a); set.has(b); // false -map = new Map(); +const map = new Map(); map.set(a, "foo"); map.get(b); // undefined @@ -106,7 +106,7 @@ Using custom implementations: ```ts import { defArraySet } from "@thi.ng/associative"; -set = defArraySet(); +const set = defArraySet(); set.add(a); set.add({a: 1}); // ArraySet { [ 1, 2 ], { a: 1 } } @@ -114,56 +114,49 @@ set.has(b); // true set.has({a: 1}); // true +``` +```ts import { defLLSet } from "@thi.ng/associative"; -set = defLLSet(); +const set = defLLSet(); set.add(a); set.add({a: 1}); // LLSet { [ 1, 2 ], { a: 1 } } + set.has(b); // true + set.has({a: 1}); // true +``` -import { defEquivMap } from "@thi.ng/associative"; +```ts +import { defEquivMap, ArraySet } from "@thi.ng/associative"; // by default EquivMap uses ArraySet for its canonical keys -map = defEquivMap(); +// const map = defEquivMap(); // with custom implementation -map = defEquivMap(null, { keys: assoc.ArraySet }); +const map = defEquivMap(null, { keys: ArraySet }); map.set(a, "foo"); // EquivMap { [ 1, 2 ] => 'foo' } + map.get(b); // "foo" +``` -// Hash map w/ user supplied hash code function -// (here using `hash` function for arrays) +```ts import { defHashMap } from "@thi.ng/associative"; import { hash } from "@thi.ng/vectors" -m = defHashMap([], { hash }) -m.set([1, 2], "a"); -m.set([3, 4, 5], "b"); -m.set([1, 2], "c"); +// Hash map w/ user supplied hash code function +// (here using `hash` function for arrays) +const map = defHashMap([], { hash }) +map.set([1, 2], "a"); +map.set([3, 4, 5], "b"); +map.set([1, 2], "c"); // HashMap { [ 1, 2 ] => 'c', [ 3, 4, 5 ] => 'b' } - -import { defSortedSet, defSortedMap } from "@thi.ng/associative"; - -set = defSortedSet([a, [-1, 2], [-1, -2]]); -// SortedSet { [ -1, -2 ], [ -1, 2 ], [ 1, 2 ] } -set.has(b); -// true - -map = defSortedMap([[a, "foo"], [[-1,-2], "bar"]]); -// SortedMap { [ -1, -2 ] => 'bar', [ 1, 2 ] => 'foo' } -map.get(b); -// "foo" - -// key lookup w/ default value -map.get([3,4], "n/a"); -// "n/a" ``` ## Status @@ -198,7 +191,7 @@ For Node.js REPL: const assoc = await import("@thi.ng/associative"); ``` -Package sizes (brotli'd, pre-treeshake): ESM: 4.78 KB +Package sizes (brotli'd, pre-treeshake): ESM: 3.52 KB ## Dependencies @@ -206,12 +199,10 @@ Package sizes (brotli'd, pre-treeshake): ESM: 4.78 KB - [@thi.ng/arrays](https://github.com/thi-ng/umbrella/tree/develop/packages/arrays) - [@thi.ng/binary](https://github.com/thi-ng/umbrella/tree/develop/packages/binary) - [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks) -- [@thi.ng/compare](https://github.com/thi-ng/umbrella/tree/develop/packages/compare) - [@thi.ng/dcons](https://github.com/thi-ng/umbrella/tree/develop/packages/dcons) - [@thi.ng/equiv](https://github.com/thi-ng/umbrella/tree/develop/packages/equiv) - [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors) - [@thi.ng/object-utils](https://github.com/thi-ng/umbrella/tree/develop/packages/object-utils) -- [@thi.ng/random](https://github.com/thi-ng/umbrella/tree/develop/packages/random) - [@thi.ng/transducers](https://github.com/thi-ng/umbrella/tree/develop/packages/transducers) - [tslib](https://www.typescriptlang.org/) diff --git a/packages/associative/package.json b/packages/associative/package.json index 7f59a16b4f..53aed3cbe6 100644 --- a/packages/associative/package.json +++ b/packages/associative/package.json @@ -40,12 +40,10 @@ "@thi.ng/arrays": "^2.9.11", "@thi.ng/binary": "^3.4.29", "@thi.ng/checks": "^3.6.8", - "@thi.ng/compare": "^2.3.9", "@thi.ng/dcons": "^3.2.118", "@thi.ng/equiv": "^2.1.62", "@thi.ng/errors": "^2.5.12", "@thi.ng/object-utils": "^1.0.0", - "@thi.ng/random": "^3.8.5", "@thi.ng/transducers": "^9.0.10", "tslib": "^2.6.3" }, @@ -118,6 +116,12 @@ "./indexed": { "default": "./indexed.js" }, + "./internal/equiv": { + "default": "./internal/equiv.js" + }, + "./internal/inspect": { + "default": "./internal/inspect.js" + }, "./intersection": { "default": "./intersection.js" }, @@ -130,15 +134,6 @@ "./ll-set": { "default": "./ll-set.js" }, - "./sorted-map": { - "default": "./sorted-map.js" - }, - "./sorted-obj": { - "default": "./sorted-obj.js" - }, - "./sorted-set": { - "default": "./sorted-set.js" - }, "./sparse-set": { "default": "./sparse-set.js" }, diff --git a/packages/associative/src/api.ts b/packages/associative/src/api.ts index 835743e011..4d516ecb49 100644 --- a/packages/associative/src/api.ts +++ b/packages/associative/src/api.ts @@ -1,5 +1,4 @@ import type { - Comparator, Fn, IClear, ICopy, @@ -10,7 +9,6 @@ import type { Maybe, Predicate2, } from "@thi.ng/api"; -import type { IRandom } from "@thi.ng/random"; export interface IEquivSet extends Set, @@ -88,39 +86,3 @@ export interface HashMapOpts { */ cap?: number; } - -/** - * SortedMapOpts implementation config settings. - */ -export interface SortedMapOpts { - /** - * Key comparison function. Must follow standard comparator contract and - * return: - * - negative if `a < b` - * - positive if `a > b` - * - `0` if `a == b` - * - * Note: The {@link SortedMap} implementation only uses `<` and `==` style - * comparisons. - * - * @defaultValue - * [`compare()`](https://docs.thi.ng/umbrella/compare/functions/compare.html) - */ - compare: Comparator; - /** - * Probability for a value to exist in any express lane of the - * underlying Skip List implementation. - * - * @defaultValue `1 / Math.E` - */ - probability: number; - /** - * Random number generator for choosing new insertion levels. By default - * uses - * [`SYSTEM`](https://docs.thi.ng/umbrella/random/variables/SYSTEM.html) - * from thi.ng/random pkg. - */ - rnd: IRandom; -} - -export type SortedSetOpts = SortedMapOpts; diff --git a/packages/associative/src/index.ts b/packages/associative/src/index.ts index fd2ab0105b..a6ff811a9f 100644 --- a/packages/associative/src/index.ts +++ b/packages/associative/src/index.ts @@ -11,8 +11,5 @@ export * from "./intersection.js"; export * from "./into.js"; export * from "./join.js"; export * from "./ll-set.js"; -export * from "./sorted-map.js"; -export * from "./sorted-obj.js"; -export * from "./sorted-set.js"; export * from "./sparse-set.js"; export * from "./union.js"; diff --git a/packages/associative/src/internal/equiv.ts b/packages/associative/src/internal/equiv.ts index 4b6fbe75fc..9972fff1db 100644 --- a/packages/associative/src/internal/equiv.ts +++ b/packages/associative/src/internal/equiv.ts @@ -1,3 +1,4 @@ +// thing:export import { equiv } from "@thi.ng/equiv"; export const __equivMap = (a: Map, b: any) => { diff --git a/packages/associative/src/internal/inspect.ts b/packages/associative/src/internal/inspect.ts index b434f8f2d6..020d6bd7f8 100644 --- a/packages/associative/src/internal/inspect.ts +++ b/packages/associative/src/internal/inspect.ts @@ -1,3 +1,4 @@ +// thing:export import { mixin } from "@thi.ng/api/mixin"; import { isNode } from "@thi.ng/checks/is-node"; import { map } from "@thi.ng/transducers/map"; diff --git a/packages/associative/src/sorted-map.ts b/packages/associative/src/sorted-map.ts deleted file mode 100644 index 8ed10f3b7c..0000000000 --- a/packages/associative/src/sorted-map.ts +++ /dev/null @@ -1,389 +0,0 @@ -import type { Comparator, Fn3, IObjectOf, Maybe, Pair } from "@thi.ng/api"; -import { isPlainObject } from "@thi.ng/checks/is-plain-object"; -import { compare } from "@thi.ng/compare/compare"; -import type { IRandom } from "@thi.ng/random"; -import { SYSTEM } from "@thi.ng/random/system"; -import type { Reduced, ReductionFn } from "@thi.ng/transducers"; -import { map } from "@thi.ng/transducers/map"; -import { isReduced } from "@thi.ng/transducers/reduced"; -import type { SortedMapOpts } from "./api.js"; -import { dissoc } from "./dissoc.js"; -import { __equivMap } from "./internal/equiv.js"; -import { __inspectable } from "./internal/inspect.js"; -import { into } from "./into.js"; - -interface SortedMapState { - head: Node; - cmp: Comparator; - p: number; - rnd: IRandom; - size: number; -} - -/** @internal */ -class Node { - next: Maybe>; - prev: Maybe>; - up: Maybe>; - down: Maybe>; - - constructor(public k?: K, public v?: V, public level = 0) {} -} - -// stores private properties for all instances -// http://fitzgeraldnick.com/2014/01/13/hiding-implementation-details-with-e6-weakmaps.html -const __private = new WeakMap, SortedMapState>(); - -/** - * Sorted map implementation based on Skip List data structure. Supports any - * keys (other than `undefined`) which can be sorted via user-defined - * comparator, given as ctor option. - * - * @remarks - * v6.3.0 .set() & .delete() implementations rewritten, based on: - * - * - https://en.wikipedia.org/wiki/Skip_list - * - https://www.youtube.com/watch?v=kBwUoWpeH_Q (MIT courseware) - * - https://www.educba.com/skip-list-java/ - */ -@__inspectable -export class SortedMap extends Map { - constructor( - pairs?: Iterable> | null, - opts: Partial> = {} - ) { - super(); - __private.set(this, { - head: new Node(), - cmp: opts.compare || compare, - rnd: opts.rnd || SYSTEM, - p: opts.probability || 1 / Math.E, - size: 0, - }); - if (pairs) { - this.into(pairs); - } - } - - get size(): number { - return __private.get(this)!.size; - } - - get [Symbol.species]() { - return SortedMap; - } - - *[Symbol.iterator](): IterableIterator> { - let node: Maybe> = this.firstNode(); - while (node && node.k !== undefined) { - yield [node.k, node.v!]; - node = node.next; - } - } - - /** - * Yields iterator of sorted `[key, value]` pairs, optionally taking given - * `key` and `max` flag into account. - * - * @remarks - * If `key` is given and `max=false`, the key is used as minimum search key - * and the iterator will only yield pairs for which keys are `>=` given key. - * If `max=true`, the given is used as maximum and only yields pairs for - * which keys are `<=` given key. - * - * If **no** key is given, yields **all** pairs. - * - * @param key - * @param max - */ - *entries(key?: K, max = false): IterableIterator> { - if (key === undefined) { - yield* this; - return; - } - const { cmp, size } = __private.get(this)!; - if (!size) return; - if (max) { - let node: Maybe> = this.firstNode(); - while (node && node.k !== undefined && cmp(node.k, key) <= 0) { - yield [node.k!, node.v!]; - node = node.next; - } - } else { - let node: Maybe> = this.firstNode(); - while (node.down) node = node!.down; - while (node) { - if (node.k !== undefined && cmp(node.k, key) >= 0) { - yield [node.k!, node.v!]; - } - node = node.next; - } - } - } - - /** - * Similar to {@link SortedMap.entries}, but only yield sequence of keys. - * - * @param key - * @param max - */ - keys(key?: K, max = false): IterableIterator { - return map((p) => p[0], this.entries(key, max)); - } - - /** - * Similar to {@link SortedMap.entries}, but only yield sequence of values. - * - * @param key - * @param max - */ - values(key?: K, max = false): IterableIterator { - return map((p) => p[1], this.entries(key, max)); - } - - clear() { - const $this = __private.get(this)!; - $this.head = new Node(); - $this.size = 0; - } - - empty(): SortedMap { - return new SortedMap(null, this.opts()); - } - - copy(): SortedMap { - return new SortedMap(this, this.opts()); - } - - compare(o: Map) { - const n = this.size; - const m = o.size; - if (n < m) return -1; - if (n > m) return 1; - const i = this.entries(); - const j = o.entries(); - let x: IteratorResult>, y: IteratorResult>; - let c: number; - while (((x = i.next()), (y = j.next()), !x.done && !y.done)) { - if ( - (c = compare(x.value[0], y.value[0])) !== 0 || - (c = compare(x.value[1], y.value[1])) !== 0 - ) { - return c; - } - } - return 0; - } - - equiv(o: any) { - return __equivMap(this, o); - } - - first() { - const node = this.firstNode(); - return node && node.k !== undefined ? [node.k, node.v] : undefined; - } - - get(key: K, notFound?: V): Maybe { - const $this = __private.get(this)!; - const node = this.findNode(key); - return node.k !== undefined && $this.cmp(node.k, key) === 0 - ? node.v - : notFound; - } - - has(key: K): boolean { - const { cmp } = __private.get(this)!; - const node = this.findNode(key); - return node.k !== undefined && cmp(node.k, key) === 0; - } - - set(key: K, val: V) { - const $this = __private.get(this)!; - const { cmp, p, rnd } = $this; - let node: Maybe> = this.findNode(key); - if (node.k !== undefined && cmp(node.k, key) === 0) { - node.v = val; - while (node.down) { - node = node!.down; - node.v = val; - } - return this; - } - let newNode = new Node(key, val, node.level); - this.insertInLane(node, newNode); - let currLevel = node.level; - let headLevel = $this.head.level; - while (rnd.probability(p)) { - // check if new head (at a new level) is needed - if (currLevel >= headLevel) { - const newHead = new Node( - undefined, - undefined, - headLevel + 1 - ); - this.linkLanes(newHead, $this.head); - $this.head = newHead; - headLevel++; - } - // find nearest predecessor with up link - while (!node!.up) node = node!.prev; - node = node!.up; - // insert new link in express lane - const tmp = new Node(key, val, node.level); - this.insertInLane(node, tmp); - // connect with new node at base level - this.linkLanes(tmp, newNode); - newNode = tmp; - currLevel++; - } - $this.size++; - return this; - } - - delete(key: K) { - const $this = __private.get(this)!; - let node: Maybe> = this.findNode(key); - if (node.k === undefined || $this.cmp(node.k, key) !== 0) return false; - // descent to lowest level - while (node.down) node = node.down; - let prev: Maybe>; - let next: Maybe>; - // ascend & remove node from all levels - while (node) { - prev = node.prev; - next = node.next; - if (prev) prev.next = next; - if (next) next.prev = prev; - node = node.up; - } - // patch up head - while (!$this.head.next && $this.head.down) { - $this.head = $this.head.down; - $this.head.up = undefined; - } - $this.size--; - return true; - } - - into(pairs: Iterable>) { - return into(this, pairs); - } - - dissoc(keys: Iterable) { - return dissoc(this, keys); - } - - /** - * The key & value args given the callback `fn` MUST be treated as - * readonly/immutable. This could be enforced via TS, but would - * break ES6 Map interface contract. - * - * @param fn - - * @param thisArg - - */ - forEach(fn: Fn3, void>, thisArg?: any) { - for (let p of this) { - fn.call(thisArg, p[1], p[0], this); - } - } - - $reduce(rfn: ReductionFn, R>, acc: R | Reduced) { - let node: Maybe> = this.firstNode(); - while (node && node.k !== undefined && !isReduced(acc)) { - acc = rfn(acc, [node.k, node.v!]); - node = node.next; - } - return acc; - } - - opts(): SortedMapOpts { - const $this = __private.get(this)!; - return { - compare: $this.cmp, - probability: $this.p, - rnd: $this.rnd, - }; - } - - /** - * Inserts `b` as successor of `a` (in the same lane as `a`). - * - * @param a - * @param b - */ - protected insertInLane(a: Node, b: Node) { - b.prev = a; - b.next = a.next; - if (a.next) a.next.prev = b; - a.next = b; - } - - /** - * Links lanes by connecting `a` and `b` vertically. - * - * @param a - * @param b - */ - protected linkLanes(a: Node, b: Node) { - a.down = b; - b.up = a; - } - - /** - * Returns first node on lowest level. Unless the map is empty, this node - * will be the first data node (with the smallest key). - */ - protected firstNode() { - const { head } = __private.get(this)!; - let node: Maybe> = head; - while (node.down) node = node.down; - while (node.prev) node = node.prev; - if (node.next) node = node.next; - return node; - } - - /** - * Returns the first matching (or predecessor) node for given key (NOT - * necessarily at the lowest level). - * - * @param key - */ - protected findNode(key: K) { - let { cmp, head } = __private.get(this)!; - let node: Node = head; - let next: Maybe>; - let down: Maybe>; - let nodeKey: Maybe; - while (true) { - next = node.next; - while (next && cmp(next.k!, key) <= 0) { - node = next; - next = node.next; - } - nodeKey = node.k; - if (nodeKey !== undefined && cmp(nodeKey, key) === 0) break; - down = node.down; - if (!down) break; - node = down; - } - return node; - } -} - -export function defSortedMap( - pairs?: Iterable> | null, - opts?: Partial> -): SortedMap; -export function defSortedMap( - obj: IObjectOf, - opts?: Partial> -): SortedMap; -export function defSortedMap( - src: any, - opts?: Partial> -): SortedMap { - return isPlainObject(src) - ? new SortedMap(Object.entries(src), opts) - : new SortedMap(src, opts); -} diff --git a/packages/associative/src/sorted-obj.ts b/packages/associative/src/sorted-obj.ts deleted file mode 100644 index d6b70a867c..0000000000 --- a/packages/associative/src/sorted-obj.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IObjectOf } from "@thi.ng/api"; -import { compareByKey } from "@thi.ng/compare/keys"; -import { assocObj } from "@thi.ng/transducers/assoc-obj"; - -/** - * Takes an object and returns shallow copy with keys sorted. Useful for JSON - * serialization/pretty printing. - * - * @remarks - * Note: Object keys are not guaranteed to keep their order and behavior will - * depend on JS runtime and object size (number of keys). - * - * @param obj - - */ -export const sortedObject = (obj: IObjectOf) => - assocObj(Object.entries(obj).sort(compareByKey(0))); diff --git a/packages/associative/src/sorted-set.ts b/packages/associative/src/sorted-set.ts deleted file mode 100644 index a81d0e3e8f..0000000000 --- a/packages/associative/src/sorted-set.ts +++ /dev/null @@ -1,170 +0,0 @@ -import type { Fn3, ICompare, Maybe, Pair } from "@thi.ng/api"; -import { compare } from "@thi.ng/compare/compare"; -import type { IReducible, Reduced, ReductionFn } from "@thi.ng/transducers"; -import { map } from "@thi.ng/transducers/map"; -import type { IEquivSet, SortedSetOpts } from "./api.js"; -import { dissoc } from "./dissoc.js"; -import { __equivSet } from "./internal/equiv.js"; -import { __inspectable } from "./internal/inspect.js"; -import { into } from "./into.js"; -import { SortedMap } from "./sorted-map.js"; - -const __private = new WeakMap, SortedMap>(); - -/** - * Sorted set implementation with standard ES6 Set API, customizable value - * equality and comparison semantics and additional functionality: - * - * - range queries (via {@link SortedSet.entries}, {@link SortedSet.keys}, - * {@link SortedSet.values}) - * - multiple value addition/deletion via {@link SortedSet.into} and - * {@link SortedSet.disj} - * - * Furthermore, this class implements the - * [`ICopy`](https://docs.thi.ng/umbrella/api/interfaces/ICopy.html), IEmpty`, - * [`ICompare`](https://docs.thi.ng/umbrella/api/interfaces/ICompare.html) and - * [`IEquiv`](https://docs.thi.ng/umbrella/api/interfaces/IEquiv.html) - * interfaces defined by [`thi.ng/api`](https://thi.ng/api). The latter two - * allow instances to be used as keys themselves in other data types defined in - * this (and other) package(s). - * - * This set uses a {@link SortedMap} as backing store and therefore has the same - * resizing characteristics. - */ -@__inspectable -export class SortedSet - extends Set - implements IEquivSet, ICompare>, IReducible -{ - /** - * Creates new instance with optional given values and/or - * implementation options. The options are the same as used by - * {@link SortedMap}. - * - * @param values - input values - * @param opts - config options - */ - constructor(values?: Iterable | null, opts?: Partial>) { - super(); - __private.set( - this, - new SortedMap( - values ? map((x) => >[x, x], values) : null, - opts - ) - ); - } - - [Symbol.iterator](): IterableIterator { - return this.keys(); - } - - get [Symbol.species]() { - return SortedSet; - } - - get [Symbol.toStringTag]() { - return "SortedSet"; - } - - get size(): number { - return __private.get(this)!.size; - } - - copy(): SortedSet { - return new SortedSet(this.keys(), this.opts()); - } - - empty() { - return new SortedSet(null, this.opts()); - } - - compare(o: Set) { - const n = this.size; - const m = o.size; - if (n < m) return -1; - if (n > m) return 1; - const i = this.entries(); - const j = o.entries(); - let x: IteratorResult>, y: IteratorResult>; - let c: number; - while (((x = i.next()), (y = j.next()), !x.done && !y.done)) { - if ((c = compare(x.value[0], y.value[0])) !== 0) { - return c; - } - } - return 0; - } - - equiv(o: any) { - return __equivSet(this, o); - } - - $reduce(rfn: ReductionFn, acc: R | Reduced) { - return __private.get(this)!.$reduce((_acc, x) => rfn(_acc, x[0]), acc); - } - - entries(key?: T, max = false): IterableIterator> { - return __private.get(this)!.entries(key, max); - } - - keys(key?: T, max = false): IterableIterator { - return __private.get(this)!.keys(key, max); - } - - values(key?: T, max = false): IterableIterator { - return __private.get(this)!.values(key, max); - } - - add(key: T) { - __private.get(this)!.set(key, key); - return this; - } - - into(keys: Iterable) { - return into(this, keys); - } - - clear(): void { - __private.get(this)!.clear(); - } - - first(): T { - const first = __private.get(this)!.first(); - return first ? first[0] : undefined; - } - - delete(key: T): boolean { - return __private.get(this)!.delete(key); - } - - disj(keys: Iterable) { - return dissoc(this, keys); - } - - forEach( - fn: Fn3, Readonly, Set, void>, - thisArg?: any - ): void { - for (let p of this) { - fn.call(thisArg, p, p, this); - } - } - - has(key: T): boolean { - return __private.get(this)!.has(key); - } - - get(key: T, notFound?: T): Maybe { - return __private.get(this)!.get(key, notFound); - } - - opts(): SortedSetOpts { - return __private.get(this)!.opts(); - } -} - -export const defSortedSet = ( - vals?: Iterable | null, - opts?: Partial> -) => new SortedSet(vals, opts); diff --git a/packages/associative/test/sortedmap.test.ts b/packages/associative/test/sortedmap.test.ts deleted file mode 100644 index c3aaf32aee..0000000000 --- a/packages/associative/test/sortedmap.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { shuffle } from "@thi.ng/arrays"; -import { equiv } from "@thi.ng/equiv"; -import { XsAdd } from "@thi.ng/random"; -import { range, repeat, zip } from "@thi.ng/transducers"; -import { beforeEach, expect, test } from "bun:test"; -import { SortedMap, defSortedMap } from "../src/index.js"; - -let m: SortedMap; - -beforeEach(() => { - m = defSortedMap({ a: 1, b: 2, c: 3 }); -}); - -test("size", () => { - expect(m.size).toBe(3); - m.set("a", 10); - expect(m.size).toBe(3); - m.set("d", 10); - expect(m.size).toBe(4); - expect(m.delete("d")); - expect(m.size).toBe(3); - expect(!m.delete("d")); - expect(m.size).toBe(3); -}); - -test("clear", () => { - m.clear(); - expect(m.size).toBe(0); - expect([...m.entries()]).toEqual([]); - expect(m.delete("a")).toBeFalse(); - expect(m.size).toBe(0); -}); - -test("empty", () => { - const m2 = m.empty(); - expect(m.size).toBe(3); - expect(m2.size).toBe(0); - expect([...m2.entries()]).toEqual([]); -}); - -test("copy", () => { - expect(m.copy()).toEqual(m); -}); - -test("equiv", () => { - expect(equiv(m.copy(), m)).toBeTrue(); - expect(equiv(m, new SortedMap())).toBeFalse(); -}); - -test("has", () => { - expect(m.has("a")).toBeTrue(); - expect(m.has("b")).toBeTrue(); - expect(m.has("c")).toBeTrue(); - expect(m.has("aa")).toBeFalse(); - expect(m.has("d")).toBeFalse(); - expect(m.has("@")).toBeFalse(); -}); - -test("first", () => { - expect(["a", 1]).toEqual(m.first()!); - m.set("A", 10); - expect(["A", 10]).toEqual(m.first()!); -}); - -test("get", () => { - expect(m.get("a")).toBe(1); - expect(m.get("b")).toBe(2); - expect(m.get("c")).toBe(3); - expect(m.get("aa")).toBeUndefined(); - expect(m.get("d")).toBeUndefined(); - expect(m.get("@", -1)).toBe(-1); -}); - -test("entries", () => { - expect([...m]).toEqual([ - ["a", 1], - ["b", 2], - ["c", 3], - ]); -}); - -// "entries rev": () => { -// assert. deepStrictEqual([...m.entries(undefined, true)], [["c", 3], ["b", 2], ["a", 1]]); -// }, - -test("entries a", () => { - expect([...m.entries("a")]).toEqual([ - ["a", 1], - ["b", 2], - ["c", 3], - ]); -}); - -// "entries a rev": () => { -// assert. deepStrictEqual([...m.entries("a", true)], [["a", 1]]); -// }, - -test("entries aa", () => { - expect([...m.entries("aa")]).toEqual([ - ["b", 2], - ["c", 3], - ]); -}); - -// "entries aa rev": () => { -// assert. deepStrictEqual([...m.entries("aa", true)], [["a", 1]]); -// }, - -test("entries bb", () => { - expect([...m.entries("bb")]).toEqual([["c", 3]]); -}); - -// "entries bb rev": () => { -// assert. deepStrictEqual([...m.entries("bb", true)], [["b", 2], ["a", 1]]); -// }, - -test("entries c", () => { - expect([...m.entries("c")]).toEqual([["c", 3]]); -}); - -// "entries c rev": () => { -// assert. deepStrictEqual([...m.entries("c", true)], [["c", 3], ["b", 2], ["a", 1]]); -// }, - -test("entries 0", () => { - expect([...m.entries("0")]).toEqual([ - ["a", 1], - ["b", 2], - ["c", 3], - ]); -}); - -// "entries 0 rev": () => { -// assert. deepStrictEqual([...m.entries("0", true)], []); -// }); - -test("entries d", () => { - expect([...m.entries("d")]).toEqual([]); -}); - -// "entries d rev": () => { -// assert. deepStrictEqual([...m.entries("d", true)], [["c", 3], ["b", 2], ["a", 1]]); -// }, - -test("keys", () => { - expect([...m.keys()]).toEqual(["a", "b", "c"]); - m.set("aa", 0); - m.set("d", 0); - expect([...m.keys()]).toEqual(["a", "aa", "b", "c", "d"]); -}); - -test("values", () => { - expect([...m.values()]).toEqual([1, 2, 3]); - m.set("aa", 0); - m.set("d", 0); - expect([...m.values()]).toEqual([1, 0, 2, 3, 0]); -}); - -test("comparator", () => { - m = defSortedMap( - { a: 1, b: 2, c: 3 }, - { - compare: (a: string, b: string) => (a === b ? 0 : a < b ? 1 : -1), - } - ); - expect([ - ["c", 3], - ["b", 2], - ["a", 1], - ]).toEqual([...m.entries()]); -}); - -test("fuzz", () => { - const keys = [...range(32)]; - for (let i = 0; i < 1000; i++) { - m = new SortedMap(zip(shuffle(keys.slice()), repeat(1))); - expect([...m.keys()]).toEqual(keys); - } -}); - -test("fuzzSetDelete", () => { - const s = defSortedMap(null, { - compare: (a, b) => a - b, - }); - const N = 1e5; - const M = 1e4; - for (let i = 0; i < N; i++) { - s.set((Math.random() * M) | 0, i); - s.delete((Math.random() * M) | 0); - } -}); - -test("updateValue", () => { - m = defSortedMap( - [ - ["one", 1], - ["two", 2], - ["three", 3], - ], - { rnd: new XsAdd(0xdecafbad) } - ); - expect([...m.values()]).toEqual([1, 3, 2]); - m.set("one", 10); - m.set("three", 30); - expect([...m.values()]).toEqual([10, 30, 2]); -}); diff --git a/packages/associative/tpl.readme.md b/packages/associative/tpl.readme.md index d010336ffb..2da864801a 100644 --- a/packages/associative/tpl.readme.md +++ b/packages/associative/tpl.readme.md @@ -12,14 +12,11 @@ > > - [@thi.ng/bidir-index](https://thi.ng/bidir-index) > - [@thi.ng/object-utils](https://thi.ng/object-utils) +> - [@thi.ng/sorted-map](https://thi.ng/sorted-map) > - [@thi.ng/trie](https://thi.ng/trie) -- Array based `ArraySet`, Linked List based `LLSet`, - [Skiplist](https://en.wikipedia.org/wiki/Skip_list) based `SortedMap` & - `SortedSet` and customizable `EquivMap` implement the full ES6 Map/Set APIs - and additional features: - - range query iterators (via `entries()`, `keys()`, `values()`) (sorted - types only) +- Array based `ArraySet`, Linked List based `LLSet` and customizable `EquivMap` + & `HashMap` implementing the full ES6 Map/Set APIs and additional features: - `ICopy`, `IEmpty` & `IEquiv` implementations - `ICompare` implementation for sorted types - multiple value additions / updates / deletions via `into()`, `dissoc()` @@ -40,31 +37,33 @@ Please see these packages for some example use cases: - [@thi.ng/ecs](https://github.com/thi-ng/umbrella/tree/develop/packages/ecs) - [@thi.ng/rstream-query](https://github.com/thi-ng/umbrella/tree/develop/packages/rstream-query) -The native ES6 implementations use object reference identity to -determine key containment, but often it's more practical and useful to -use equivalent value semantics for this purpose, especially when keys -are structured data (arrays / objects). +### Why? -**Note**: It's the user's responsibility to ensure the inserted keys are -kept immutable (even if technically they're not). +The native ES6 implementations use **object reference** identity to determine +key containment, but often it's **more practical and useful to use equivalent +value semantics** for this purpose, especially when keys are structured data +(arrays / objects). + +**Note**: It's the user's responsibility to ensure the inserted keys are kept +immutable (even if technically they're not). ### Comparison with ES6 native types ```ts // first two objects w/ equal values -a = [1, 2]; -b = [1, 2]; +const a = [1, 2]; +const b = [1, 2]; ``` Using native implementations ```ts -set = new Set(); +const set = new Set(); set.add(a); set.has(b); // false -map = new Map(); +const map = new Map(); map.set(a, "foo"); map.get(b); // undefined @@ -75,7 +74,7 @@ Using custom implementations: ```ts import { defArraySet } from "@thi.ng/associative"; -set = defArraySet(); +const set = defArraySet(); set.add(a); set.add({a: 1}); // ArraySet { [ 1, 2 ], { a: 1 } } @@ -83,56 +82,49 @@ set.has(b); // true set.has({a: 1}); // true +``` +```ts import { defLLSet } from "@thi.ng/associative"; -set = defLLSet(); +const set = defLLSet(); set.add(a); set.add({a: 1}); // LLSet { [ 1, 2 ], { a: 1 } } + set.has(b); // true + set.has({a: 1}); // true +``` -import { defEquivMap } from "@thi.ng/associative"; +```ts +import { defEquivMap, ArraySet } from "@thi.ng/associative"; // by default EquivMap uses ArraySet for its canonical keys -map = defEquivMap(); +// const map = defEquivMap(); // with custom implementation -map = defEquivMap(null, { keys: assoc.ArraySet }); +const map = defEquivMap(null, { keys: ArraySet }); map.set(a, "foo"); // EquivMap { [ 1, 2 ] => 'foo' } + map.get(b); // "foo" +``` -// Hash map w/ user supplied hash code function -// (here using `hash` function for arrays) +```ts import { defHashMap } from "@thi.ng/associative"; import { hash } from "@thi.ng/vectors" -m = defHashMap([], { hash }) -m.set([1, 2], "a"); -m.set([3, 4, 5], "b"); -m.set([1, 2], "c"); +// Hash map w/ user supplied hash code function +// (here using `hash` function for arrays) +const map = defHashMap([], { hash }) +map.set([1, 2], "a"); +map.set([3, 4, 5], "b"); +map.set([1, 2], "c"); // HashMap { [ 1, 2 ] => 'c', [ 3, 4, 5 ] => 'b' } - -import { defSortedSet, defSortedMap } from "@thi.ng/associative"; - -set = defSortedSet([a, [-1, 2], [-1, -2]]); -// SortedSet { [ -1, -2 ], [ -1, 2 ], [ 1, 2 ] } -set.has(b); -// true - -map = defSortedMap([[a, "foo"], [[-1,-2], "bar"]]); -// SortedMap { [ -1, -2 ] => 'bar', [ 1, 2 ] => 'foo' } -map.get(b); -// "foo" - -// key lookup w/ default value -map.get([3,4], "n/a"); -// "n/a" ``` {{meta.status}}