UNPKG

@itwin/core-bentley

Version:

Bentley JavaScript core components

427 lines • 19.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Ids */ import { assert } from "./Assert"; import { Id64 } from "./Id"; import { OrderedId64Iterable } from "./OrderedId64Iterable"; import { SortedArray } from "./SortedArray"; /** A compact string representation of an [[Id64Set]]. Such a representation is useful when serializing potentially very large * sets of Ids. * @see [[CompressedId64Set.iterable]] to efficiently iterate the Ids represented by a compact string. * @see [[CompressedId64Set.compressSet]] and [[CompressedId64Set.compressArray]] to produce a compact string from a collection of Ids. * @see [[CompressedId64Set.decompressSet]] and [[CompressedId64Set.decompressArray]] to produce a collection of Ids from a compact string. * @see [[OrderedId64Iterable]] for a generic representation of an ordered set of Ids (compressed or otherwise). * @see [[MutableCompressedId64Set]] for a mutable version. * @public */ export var CompressedId64Set; (function (CompressedId64Set) { function isHexDigit(ch) { // ascii values: // '0' = 48 // '9' = 57 // 'a' = 65 // 'f' = 70 return (ch >= 48 && ch <= 57) || (ch >= 65 && ch <= 70); } function compactRange(increment, length) { assert(length > 0); const inc = `+${increment.toString()}`; if (length <= 1) return inc; const len = length.toString(16).toUpperCase(); return `${inc}*${len}`; } /** Given a set of [[Id64String]]s, produce a compact string representation. Useful when serializing potentially large sets of Ids. * @note Invalid Ids are ignored. * @see [[CompressedId64Set.sortAndCompress]] to compress any unordered collection of Ids. * @see [[CompressedId64Set.compressArray]] to perform the same operation on an [[Id64Array]]. * @see [[CompressedId64Set.decompressSet]] to perform the inverse operation. */ function compressSet(ids) { return sortAndCompress(ids); } CompressedId64Set.compressSet = compressSet; /** Create a sorted array from `ids`, then return a compact string representation of those Ids. * @see [[CompressedId64Set.compressIds]] if `ids` is known to already be sorted. */ function sortAndCompress(ids) { // `string` is an Iterable<string>. In that case assume caller passed a single Id64String. const arr = typeof ids === "string" ? [ids] : Array.from(ids); OrderedId64Iterable.sortArray(arr); return compressArray(arr); } CompressedId64Set.sortAndCompress = sortAndCompress; /** Give a **numerically-ordered** array of [[Id64String]]s, produce a compact string representation. Useful when serializing potentially large sets of Ids. * Duplicate Ids are included only once in the string representation. * @throws Error if two consecutive Ids `x` and `y` exist such that the numerical value of `x` is greater than that of `y` - i.e., the array is not properly sorted. * @note The array must be sorted according to the 64-bit numerical value of each Id. * @note Invalid Ids are ignored. * @see [[CompressedId64Set.decompressArray]] to perform the inverse operation. * @see [[OrderedId64Iterable.sortArray]] to ensure the Ids are properly sorted. * @see [[CompressedId64Set.sortAndCompress]] to compress any unordered collection of Ids. */ function compressArray(ids) { return compressIds(ids); } CompressedId64Set.compressArray = compressArray; /** Give a **numerically-ordered** collection of [[Id64String]]s, produce a compact string representation. Useful when serializing potentially large sets of Ids. * Duplicate Ids are included only once in the string representation. * @throws Error if two consecutive Ids `x` and `y` exist such that the numerical value of `x` is greater than that of `y` - i.e., the collection is not properly sorted. * @note The collection must be sorted according to the 64-bit numerical value of each Id. * @note Invalid Ids are ignored. * @see [[CompressedId64Set.iterable]] to perform the inverse operation. * @see [[OrderedId64Iterable.sortArray]] or [[OrderedId64Iterable.compare]] to ensure the Ids are properly sorted. * @see [[CompressedId64Set.sortAndCompress]] to compress any unordered collection of Ids. */ function compressIds(ids) { if ("string" === typeof ids) return ids; let str = ""; const prevId = new Uint64(); const rangeIncrement = new Uint64(); let rangeLen = 0; const curId = new Uint64(); const curIncrement = new Uint64(); for (const id of ids) { if (!Id64.isValidId64(id)) continue; // ignore garbage and invalid Ids ("0") curId.setFromId(id); curIncrement.setFromDifference(curId, prevId); const cmp = prevId.compare(curId); if (0 === cmp) continue; // ignore duplicates else if (cmp > 0) throw new Error("CompressedId64Set.compressArray requires a sorted array as input"); prevId.copyFrom(curId); if (0 === rangeLen) { rangeIncrement.copyFrom(curIncrement); rangeLen = 1; } else if (curIncrement.equals(rangeIncrement)) { ++rangeLen; } else { str += compactRange(rangeIncrement, rangeLen); rangeIncrement.copyFrom(curIncrement); rangeLen = 1; } } if (0 < rangeLen) str += compactRange(rangeIncrement, rangeLen); return str; } CompressedId64Set.compressIds = compressIds; /** This exists strictly for the purposes of compressed sets of 64-bit Ids, to avoid the overhead of BigInt for handling 64-bit integers. */ class Uint64 { lower; upper; static _base = 0x100000000; static assertUint32(num) { assert(num >= 0); assert(num < Uint64._base); assert(Math.floor(num) === num); } assertConstraints() { Uint64.assertUint32(this.lower); Uint64.assertUint32(this.upper); } constructor(lower = 0, upper = 0) { this.lower = lower; this.upper = upper; this.assertConstraints(); } compare(rhs) { const diff = this.upper - rhs.upper; return 0 === diff ? this.lower - rhs.lower : diff; } equals(rhs) { return 0 === this.compare(rhs); } isLessThan(rhs) { return this.compare(rhs) < 0; } isGreaterThan(rhs) { return this.compare(rhs) > 0; } get isZero() { return 0 === this.lower && 0 === this.upper; } setFromDifference(lhs, rhs) { assert(!rhs.isGreaterThan(lhs)); this.lower = lhs.lower - rhs.lower; this.upper = lhs.upper - rhs.upper; if (this.lower < 0) { this.lower += Uint64._base; this.upper -= 1; } } add(rhs) { let lower = rhs.lower; let upper = rhs.upper; if (lower + this.lower >= Uint64._base) { lower -= Uint64._base; upper += 1; } this.lower += lower; this.upper += upper; this.assertConstraints(); } setFromId(id) { Id64.getUint32Pair(id, this); } copyFrom(other) { this.lower = other.lower; this.upper = other.upper; } toString() { if (0 === this.upper) return this.lower.toString(16).toUpperCase(); const upper = this.upper.toString(16); const lower = this.lower.toString(16).padStart(8, "0"); assert(lower.length === 8); return `${upper}${lower}`.toUpperCase(); } toId64String() { return Id64.fromUint32Pair(this.lower, this.upper); } } /** Supplies an iterator over the [[Id64String]]s in a [[CompressedId64Set]]. * The Ids are iterated in ascending order based on their unsigned 64-bit integer values. */ function* iterator(ids) { if (0 === ids.length) return; // empty set. if ("+" !== ids[0]) throw new Error("Invalid CompressedId64Set"); let curIndex = 1; // skip the leading '+' const curId = new Uint64(); function parseUint32() { let value = 0; let nChars = 0; while (curIndex < ids.length && nChars < 8) { ++nChars; const ch = ids.charCodeAt(curIndex); if (!isHexDigit(ch)) break; // not a hex digit in [0..9] or [A..F] value <<= 4; value |= (ch >= 65 ? ch - 65 + 10 : ch - 48); // ch - 'A' + 10 or ch - '0' value = value >>> 0; // restore unsignedness because silly javascript. ++curIndex; } return value; } function parseUint64(uint64) { let lower = 0; let upper = 0; // Read up to the first 8 digits. const startIndex = curIndex; const first = parseUint32(); const nFirstDigits = curIndex - startIndex; assert(nFirstDigits <= 8); if (8 === nFirstDigits && curIndex + 1 < ids.length && isHexDigit(ids.charCodeAt(curIndex + 1))) { // We've got up to 8 more digits remaining const secondIndex = curIndex; const second = parseUint32(); // Transfer excess digits from upper to lower. const nSecondDigits = curIndex - secondIndex; assert(nSecondDigits > 0 && nSecondDigits <= 8); const nDigitsToTransfer = 8 - nSecondDigits; upper = first >>> (4 * nDigitsToTransfer); const transfer = first - ((upper << (4 * nDigitsToTransfer)) >>> 0); lower = (second | ((transfer << (4 * nSecondDigits)) >>> 0)) >>> 0; } else { lower = first; } uint64.lower = lower; uint64.upper = upper; } const increment = new Uint64(); while (curIndex < ids.length) { let multiplier = 1; parseUint64(increment); if (increment.isZero) throw new Error("Invalid CompressedId64Set"); if (curIndex < ids.length) { switch (ids[curIndex++]) { case "*": multiplier = parseUint32(); if (0 === multiplier) throw new Error("Invalid CompressedId64Set"); if (curIndex !== ids.length && ids[curIndex++] !== "+") return; break; case "+": break; default: throw new Error("Invalid CompressedId64Set"); } } for (let i = 0; i < multiplier; i++) { curId.add(increment); yield curId.toId64String(); } } } CompressedId64Set.iterator = iterator; /** Supplies an iterable over the [[Id64String]]s in a [[CompressedId64Set]]. * The Ids are iterated in ascending order based on their unsigned 64-bit integer values. */ function iterable(ids) { return { [Symbol.iterator]: () => iterator(ids), }; } CompressedId64Set.iterable = iterable; /** Decompress the compact string representation of an [[Id64Set]] into an [[Id64Set]]. * @param compressedIds The compact string representation. * @param out If supplied, the Ids will be inserted into this set rather than allocating and returning a new set. * @returns The set containing the decompressed Ids. * @throws Error if `compressedIds` is not a well-formed [[CompressedId64Set]]. * @see [[CompressedId64Set.compressSet]] to perform the inverse operation. * @see [[CompressedId64Set.decompressArray]] to decompress as an [[Id64Array]] instead. * @see [[CompressedId64Set.iterable]] to efficiently iterate the Ids. */ function decompressSet(compressedIds, out) { const set = out ?? new Set(); for (const id of iterable(compressedIds)) set.add(id); return set; } CompressedId64Set.decompressSet = decompressSet; /** Decompress the compact string representation of an [[Id64Set]] into an [[Id64Array]]. * @param compressedIds The compact string representation. * @param out If supplied, the Ids will be appended to this array rather than allocating and returning a new array. * @returns The array containing the decompressed Ids. * @throws Error if `compressedIds` is not a well-formed [[CompressedId64Set]]. * @note The Ids are decompressed and appended to the array in ascending order based on their 64-bit numerical values. * @see [[CompressedId64Set.compressArray]] to perform the inverse operation. * @see [[CompressedId64Set.decompressSet]] to decompress as an [[Id64Set]] instead. * @see [[CompressedId64Set.iterable]] to efficiently iterate the Ids. */ function decompressArray(compressedIds, out) { const arr = out ?? []; for (const id of iterable(compressedIds)) arr.push(id); return arr; } CompressedId64Set.decompressArray = decompressArray; })(CompressedId64Set || (CompressedId64Set = {})); /** A [[SortedArray]] of unique [[Id64String]]s sorted in ascending order by the 64-bit unsigned integer values of the Ids. * @see [[CompressedId64Set]] for an immutable compact string representation. * @public */ export class OrderedId64Array extends SortedArray { /** Construct a new, empty array. */ constructor() { super((lhs, rhs) => OrderedId64Iterable.compare(lhs, rhs)); } /** An iterable that iterates over the Ids in sorted order. */ get ids() { return this._array; } /** The underlying array of Ids. */ get array() { return this._array; } } /** A mutable set of valid [[Id64String]]s sorted in ascending order by the 64-bit unsigned integer value of the Ids. * Internally the set of Ids is maintained as a [[CompressedId64Set]] string representation. * Insertions and removals are buffered until the string representation needs to be recomputed. The string representation is recomputed by every public method except [[add]] and [[delete]] - * therefore, if multiple removals and/or insertions are required, it is most efficient to perform them all before invoking other methods. * @public */ export class MutableCompressedId64Set { _ids; _inserted = new OrderedId64Array(); _deleted = new OrderedId64Array(); /** Construct a new set, optionally initialized to contain the Ids represented by `ids`. */ constructor(ids) { this._ids = ids ?? ""; } /** Obtain the compact string representation of the contents of this set. If any insertions or removals are pending, they will be applied and the string recomputed. */ get ids() { this.updateIds(); return this._ids; } /** Add the specified Id to the set. * @throws Error if `id` is not a valid [[Id64String]]. */ add(id) { if (!Id64.isValidId64(id)) throw new Error("MutableCompressedId64Set.add: invalid Id"); this._deleted.remove(id); this._inserted.insert(id); } /** Remove the specified Id from the set. * @throws Error if `id` is not a valid [[Id64String]]. */ delete(id) { if (!Id64.isValidId64(id)) throw new Error("MutableCompressedId64Set.delete: invalid Id"); this._inserted.remove(id); this._deleted.insert(id); } /** Remove all Ids from the set. */ clear() { this._ids = ""; this._inserted.clear(); this._deleted.clear(); } /** Remove all Ids from the set, then add the specified Ids. */ reset(ids) { this.clear(); this._ids = ids ?? ""; } /** Obtain an iterator over the Ids in this set. The Ids are returned in ascending order based on their unsigned 64-bit integer values. */ [Symbol.iterator]() { return CompressedId64Set.iterator(this.ids); } /** Compute a compact string representation of the union of this and another set of Ids - i.e., those Ids present in either this and/or the other set. */ computeUnion(ids) { if (this.isEmpty) return CompressedId64Set.compressIds(ids); else if (OrderedId64Iterable.isEmptySet(ids) || this.equals(ids)) return this.ids; return CompressedId64Set.compressIds(OrderedId64Iterable.union(this, ids)); } /** Compute a compact string representation of the intersection of this and another set of Ids - i.e., those Ids present in both this and the other set. */ computeIntersection(ids) { if (this.equals(ids)) return this.ids; else if (this.isEmpty || OrderedId64Iterable.isEmptySet(ids)) return ""; return CompressedId64Set.compressIds(OrderedId64Iterable.intersection(this, ids)); } /** Compute a compact string representation of the difference between this and another set - i.e., those Ids present in this but not in the other set. */ computeDifference(ids) { if (this.isEmpty || this.equals(ids)) return ""; return CompressedId64Set.compressIds(OrderedId64Iterable.difference(this, ids)); } /** Return true if this set contains no Ids. */ get isEmpty() { return OrderedId64Iterable.isEmptySet(this.ids); } /** Return true if the set of Ids represented by `other` is identical to those in this set. * @note This considers only the **distinct** Ids in `other` - duplicates are ignored. */ equals(other) { if (other instanceof MutableCompressedId64Set) { if (other === this) return true; if (typeof other !== "string") other = other.ids; } if (typeof other === "string") return other === this.ids; this.updateIds(); return OrderedId64Iterable.areEqualSets(this, other); } get _isDirty() { return !this._inserted.isEmpty || !this._deleted.isEmpty; } updateIds() { if (!this._isDirty) return; const difference = OrderedId64Iterable.difference(CompressedId64Set.iterable(this._ids), this._deleted.ids); const union = { [Symbol.iterator]: () => OrderedId64Iterable.unionIterator(difference, this._inserted.ids) }; this._ids = CompressedId64Set.compressIds(union); this._inserted.clear(); this._deleted.clear(); } } //# sourceMappingURL=CompressedId64Set.js.map