-
-
Notifications
You must be signed in to change notification settings - Fork 152
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(associative): add BidirIndex & tests
- Loading branch information
1 parent
6a76c75
commit 26f749f
Showing
4 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,280 @@ | ||
import { isString } from "@thi.ng/checks/is-string"; | ||
|
||
export interface SerializedBidirIndex<T> { | ||
pairs: [T, number][]; | ||
nextID: number; | ||
} | ||
|
||
export interface BidirIndexOpts<T> { | ||
/** | ||
* Custom `key -> id` map implementation (e.g. {@link EquivMap} or | ||
* {@link HashMap}). If omitted, a native JS `Map` will be used. | ||
*/ | ||
map: Map<T, number>; | ||
/** | ||
* Start ID for indexing new keys. | ||
* | ||
* @defaultValue 0 | ||
*/ | ||
start: number; | ||
} | ||
|
||
/** | ||
* Bi-directional index to map arbitrary keys to numeric IDs and vice versa. | ||
*/ | ||
export class BidirIndex<T> { | ||
fwd: Map<T, number>; | ||
rev: Map<number, T>; | ||
nextID: number; | ||
|
||
constructor( | ||
keys?: Iterable<T> | null, | ||
opts: Partial<BidirIndexOpts<T>> = {} | ||
) { | ||
this.nextID = opts.start || 0; | ||
this.fwd = opts.map || new Map(); | ||
this.rev = new Map(); | ||
keys && this.addAll(keys); | ||
} | ||
|
||
get size() { | ||
return this.fwd.size; | ||
} | ||
|
||
/** | ||
* Yields same result as {@link BidirIndex.entries}. | ||
*/ | ||
[Symbol.iterator]() { | ||
return this.entries(); | ||
} | ||
|
||
/** | ||
* Returns iterator of `[key,id]` pairs. | ||
*/ | ||
entries() { | ||
return this.fwd.entries(); | ||
} | ||
|
||
/** | ||
* Returns iterator of all indexed keys. | ||
*/ | ||
keys() { | ||
return this.fwd.keys(); | ||
} | ||
|
||
/** | ||
* Returns iterator of all indexed IDs. | ||
*/ | ||
values() { | ||
return this.fwd.values(); | ||
} | ||
|
||
/** | ||
* Returns true if given `key` is known/indexed. | ||
* | ||
* @param key | ||
*/ | ||
has(key: T) { | ||
return this.fwd.has(key); | ||
} | ||
|
||
/** | ||
* Returns true if given `id` has a corresponding known/indexed key. | ||
* | ||
* @param id | ||
*/ | ||
hasID(id: number) { | ||
return this.rev.has(id); | ||
} | ||
|
||
/** | ||
* Reverse lookup of {@link BidirIndex.getID}. Returns the matching ID for | ||
* given `key` or undefined if the key is not known. | ||
* | ||
* @param key | ||
*/ | ||
get(key: T) { | ||
return this.fwd.get(key); | ||
} | ||
|
||
/** | ||
* Reverse lookup of {@link BidirIndex.get}. Returns the matching key for | ||
* given `id` or undefined if the ID is not known. | ||
* | ||
* @param id | ||
*/ | ||
getID(id: number) { | ||
return this.rev.get(id); | ||
} | ||
|
||
/** | ||
* Indexes given `key` and assigns & returns a new ID. If `key` is already | ||
* known/indexed, returns its existing ID. | ||
* | ||
* @param key | ||
*/ | ||
add(key: T) { | ||
let id = this.fwd.get(key); | ||
if (id === undefined) { | ||
this.fwd.set(key, this.nextID); | ||
this.rev.set(this.nextID, key); | ||
id = this.nextID++; | ||
} | ||
return id; | ||
} | ||
|
||
/** | ||
* Batch version of {@link BidirIndex.add}. Indexes all given keys and | ||
* returns array of their corresponding IDs. | ||
* | ||
* @param keys | ||
*/ | ||
addAll(keys: Iterable<T>) { | ||
const res: number[] = []; | ||
for (let k of keys) { | ||
res.push(this.add(k)); | ||
} | ||
return res; | ||
} | ||
|
||
/** | ||
* Removes bi-directional mapping for given `key` from the index. Returns | ||
* true if successful. | ||
* | ||
* @param key | ||
*/ | ||
delete(key: T) { | ||
const fwd = this.fwd; | ||
const id = fwd.get(key); | ||
if (id !== undefined) { | ||
fwd.delete(key); | ||
this.rev.delete(id); | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Removes bi-directional mapping for given `id` from the index. Returns | ||
* true if successful. | ||
* | ||
* @param id | ||
*/ | ||
deleteID(id: number) { | ||
const rev = this.rev; | ||
const k = rev.get(id); | ||
if (k !== undefined) { | ||
rev.delete(id); | ||
this.fwd.delete(k); | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Batch version of {@link BidirIndex.delete}. | ||
* | ||
* @param keys | ||
*/ | ||
deleteAll(keys: Iterable<T>) { | ||
for (let k of keys) this.delete(k); | ||
} | ||
|
||
/** | ||
* Batch version of {@link BidirIndex.deleteID}. | ||
* | ||
* @param ids | ||
*/ | ||
deleteAllIDs(ids: Iterable<number>) { | ||
for (let id of ids) this.deleteID(id); | ||
} | ||
|
||
/** | ||
* Returns array of IDs for all given keys. If `fail` is true (default: | ||
* false), throws error if any of the given keys is unknown/unindexed (use | ||
* {@link BidirIndex.add} or {@link BidirIndex.addAll} first). | ||
* | ||
* @param keys | ||
* @param fail | ||
*/ | ||
getAll(keys: Iterable<T>, fail = false) { | ||
const index = this.fwd; | ||
const res: number[] = []; | ||
for (let k of keys) { | ||
const id = index.get(k); | ||
if (id === undefined) { | ||
if (fail) throw new Error(`unknown key: ${k}`); | ||
} else { | ||
res.push(id); | ||
} | ||
} | ||
return res; | ||
} | ||
|
||
/** | ||
* Returns array of matching keys for all given IDs. If `fail` is true | ||
* (default: false), throws error if any of the given IDs is | ||
* unknown/unindexed (use {@link BidirIndex.add} or | ||
* {@link BidirIndex.addAll} first). | ||
* | ||
* @param ids | ||
* @param fail | ||
*/ | ||
getAllIDs(ids: Iterable<number>, fail = false) { | ||
const index = this.rev; | ||
const res: T[] = []; | ||
for (let id of ids) { | ||
const k = index.get(id); | ||
if (k === undefined) { | ||
if (fail) throw new Error(`unknwon ID: ${id}`); | ||
} else { | ||
res.push(k); | ||
} | ||
} | ||
return res; | ||
} | ||
|
||
/** | ||
* Returns a compact JSON serializable version of the index. Use | ||
* {@link bidirIndexFromJSON} to instantiate an index from such a JSON | ||
* serialization. | ||
*/ | ||
toJSON(): SerializedBidirIndex<T> { | ||
return { | ||
pairs: [...this.entries()], | ||
nextID: this.nextID, | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* Factory function wrapper for {@link BidirIndex}. | ||
* | ||
* @param keys | ||
* @param opts | ||
*/ | ||
export const defBidirIndex = <T>( | ||
keys?: Iterable<T>, | ||
opts?: Partial<BidirIndexOpts<T>> | ||
) => new BidirIndex<T>(keys, opts); | ||
|
||
/** | ||
* Instantiates a {@link BidirIndex} from given JSON serialization. The optional | ||
* `map` arg can be used to provide a customized `key -> id` map implementation | ||
* (same use as {@link BidirIndexOpts.map}). | ||
* | ||
* @param src | ||
* @param map | ||
*/ | ||
export const bidirIndexFromJSON = <T>( | ||
src: string | SerializedBidirIndex<T>, | ||
map?: Map<T, number> | ||
) => { | ||
const $src = isString(src) ? <SerializedBidirIndex<T>>JSON.parse(src) : src; | ||
const res = new BidirIndex(null, { map, start: $src.nextID }); | ||
$src.pairs.forEach(([k, id]) => { | ||
res.fwd.set(k, id); | ||
res.rev.set(id, k); | ||
}); | ||
return res; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { group } from "@thi.ng/testament"; | ||
import * as assert from "assert"; | ||
import { defBidirIndex } from "../src/index.js"; | ||
|
||
group("BidirIndex", { | ||
addAll: () => { | ||
const idx = defBidirIndex("abc", { start: 100 }); | ||
assert.deepStrictEqual( | ||
idx.fwd, | ||
new Map([ | ||
["a", 100], | ||
["b", 101], | ||
["c", 102], | ||
]) | ||
); | ||
assert.deepStrictEqual( | ||
idx.rev, | ||
new Map([ | ||
[100, "a"], | ||
[101, "b"], | ||
[102, "c"], | ||
]) | ||
); | ||
}, | ||
getAll: () => { | ||
const idx = defBidirIndex("abc"); | ||
assert.deepStrictEqual(idx.getAll("cba"), [2, 1, 0]); | ||
assert.deepStrictEqual(idx.getAllIDs([2, 1, 0]), ["c", "b", "a"]); | ||
}, | ||
deleteAll: () => { | ||
const idx = defBidirIndex("abcd", { start: 100 }); | ||
idx.deleteAll("bd"); | ||
assert.deepStrictEqual( | ||
idx.fwd, | ||
new Map([ | ||
["a", 100], | ||
["c", 102], | ||
]) | ||
); | ||
assert.deepStrictEqual( | ||
idx.rev, | ||
new Map([ | ||
[100, "a"], | ||
[102, "c"], | ||
]) | ||
); | ||
}, | ||
}); |