diff --git a/packages/common/src/models/collection.ts b/packages/common/src/models/collection.ts index 1ea44899..d66f46b4 100644 --- a/packages/common/src/models/collection.ts +++ b/packages/common/src/models/collection.ts @@ -3,6 +3,21 @@ import { isDefined } from "../utils"; export type CollectionAddOptions = { index?: number }; +/** + * Collection abstract model + * + * @example + * Define a new collection class with internal type `number` and external type `string` + * ``` + * class TestCollection extends Collection { + * protected _map(item: string | number): number { + * return Number(item); + * } + * // Some other methods + * } + * ``` + * + */ export abstract class Collection implements Iterable { protected readonly _items: InternalType[]; @@ -27,14 +42,25 @@ export abstract class Collection implements Iterable }; } + /** + * Number of items in the collection + */ public get length(): number { return this._items.length; } + /** + * True if the collection is empty + */ public get isEmpty(): boolean { return this.length === 0; } + /** + * Get item at index + * @param index + * @throws RangeError if index is out of bounds + */ public at(index: number): InternalType { if (this._isIndexOutOfBounds(index)) { throw new RangeError(`Index '${index}' is out of range.`); @@ -43,12 +69,23 @@ export abstract class Collection implements Iterable return this._items[index]; } + /** + * Add item to the collection + * @param items + * @param options + * @returns The new length of the collection + */ public add(items: OneOrMore, options?: CollectionAddOptions): number { return this._addOneOrMore(items, options); } abstract remove(item: unknown): number; + /** + * Map external type to internal type + * @param item + * @protected + */ protected abstract _map(item: ExternalType | InternalType): InternalType; protected _addOne(item: InternalType | ExternalType, options?: CollectionAddOptions): number { @@ -89,6 +126,9 @@ export abstract class Collection implements Iterable return this.length; } + /** + * Get the collection as an array + */ public toArray(): InternalType[] { return [...this._items]; } diff --git a/packages/common/src/utils/array.ts b/packages/common/src/utils/array.ts index 52d4bd1c..db632462 100644 --- a/packages/common/src/utils/array.ts +++ b/packages/common/src/utils/array.ts @@ -3,6 +3,11 @@ import { assert, isEmpty } from "./assertions"; type ObjectSelector = (item: T) => T[keyof T]; +/** + * Returns the first element of an array. + * @param array + * @throws an error if the array is empty. + */ export function first(array: undefined): undefined; export function first(array: ArrayLike): T; export function first(array: ArrayLike | undefined): T | number | undefined { @@ -12,6 +17,11 @@ export function first(array: ArrayLike | undefined): T | number | undefine return array[0]; } +/** + * Returns the last element of an array. + * @param array + * @throws an error if the array is empty. + */ export function last(array: undefined): undefined; export function last(array: ArrayLike): T; export function last(array: ArrayLike | undefined): T | undefined { @@ -21,6 +31,11 @@ export function last(array: ArrayLike | undefined): T | undefined { return at(array, -1); } +/** + * Returns the element at the specified index. Negative indices are counted from the end of the array. + * @param array + * @param index + */ export function at(array: undefined, index: number): undefined; export function at(array: ArrayLike, index: number): T; export function at(array: ArrayLike | undefined, index: number): T | undefined { @@ -52,6 +67,19 @@ export function hasDuplicatesBy(array: T[], selector: ObjectSelector): boo }); } +/** + * Turns an array into chunks of the specified size + * @param array + * @param size + * + * @example + * ``` + * const array = [1, 2, 3, 4, 5]; + * const chunks = chunk(array, 2); + * console.log(chunks); + * // [[1, 2], [3, 4], [5]] + * ``` + */ export function chunk(array: T[], size: number): T[][] { if (array.length <= size) { return [array]; @@ -65,6 +93,20 @@ export function chunk(array: T[], size: number): T[][] { return chunks; } +/** + * Sorts an array of objects by the specified property + * @param array + * @param iteratee + * @param order + * + * @example + * ``` + * const array = [{ name: "John", age: 25 }, { name: "Jane", age: 30 }]; + * const sorted = orderBy(array, (item) => item.age, "desc"); + * console.log(sorted); + * // [{ name: "Jane", age: 30 }, { name: "John", age: 25 }] + * ``` + */ export function orderBy( array: T[], iteratee: SortingSelector, @@ -81,6 +123,22 @@ export function orderBy( }); } +/** + * Checks if arrays are equal + * @param array1 + * @param array2 + * + * @example + * ``` + * const array1 = [1, 2, 3]; + * const array2 = [1, 2, 3]; + * const array3 = [1, 2, 4]; + * const array4 = [1, 2, 3, 4]; + * areEqual(array1, array2); // true + * areEqual(array1, array3); // false + * areEqual(array1, array4); // false + * ``` + */ export function areEqual(array1: ArrayLike, array2: ArrayLike): boolean { if (array1 === array2) { return true; @@ -99,6 +157,22 @@ export function areEqual(array1: ArrayLike, array2: ArrayLike): boolean return true; } +/** + * Checks if arrays are equal by the specified property + * @param array1 + * @param array2 + * @param selector + * + * @example + * ``` + * const array1 = [{ name: "John", age: 25 }, { name: "Jane", age: 30 }]; + * const array2 = [{ name: "John", age: 25 }, { name: "Jane", age: 30 }]; + * const array3 = [{ name: "John", age: 25 }, { name: "Jane", age: 31 }]; + * + * areEqualBy(array1, array2, (item) => item.age); // true + * areEqualBy(array1, array3, (item) => item.age); // false + * ``` + */ export function areEqualBy( array1: ArrayLike, array2: ArrayLike, @@ -121,6 +195,21 @@ export function areEqualBy( return true; } +/** + * Checks if the array starts with the specified target + * @param array + * @param target + * + * @example + * ``` + * const array = [1, 2, 3, 4, 5]; + * const target1 = [1, 2]; + * const target2 = [1, 3]; + * + * startsWith(array, target1); // true + * startsWith(array, target2); // false + * ``` + */ export function startsWith(array: ArrayLike, target: ArrayLike): boolean { if (array === target) { return true; @@ -139,6 +228,21 @@ export function startsWith(array: ArrayLike, target: ArrayLike): boolea return true; } +/** + * Checks if the array ends with the specified target + * @param array + * @param target + * + * @example + * ``` + * const array = [1, 2, 3, 4, 5]; + * const target1 = [4, 5]; + * const target2 = [3, 5]; + * + * endsWith(array, target1); // true + * endsWith(array, target2); // false + * ``` + */ export function endsWith(array: ArrayLike, target: ArrayLike): boolean { if (array === target) { return true; @@ -159,6 +263,18 @@ export function endsWith(array: ArrayLike, target: ArrayLike): boolean return true; } +/** + * Makes an array unique by removing duplicate elements + * @param array + * + * @example + * ``` + * const array = [1, 2, 3, 3, 4, 5, 5]; + * const unique = uniq(array); + * console.log(unique); + * // [1, 2, 3, 4, 5] + * ``` + */ export function uniq(array: Array): Array { if (isEmpty(array)) { return array; @@ -167,6 +283,20 @@ export function uniq(array: Array): Array { return Array.from(new Set(array)); } +/** + * Makes an array unique by removing duplicate elements using the specified property + * @param array + * @param selector + * @param selection + * + * @example + * ``` + * const array = [{ name: "John", age: 25 }, { name: "Jane", age: 30 }, { name: "John", age: 30 }]; + * const unique = uniqBy(array, (item) => item.name); + * console.log(unique); + * // [{ name: "John", age: 25 }, { name: "Jane", age: 30 }] + * ``` + */ export function uniqBy( array: Array, selector: ObjectSelector, @@ -191,6 +321,17 @@ export function uniqBy( ); } +/** + * Returns the depth of an array + * @param array + * + * @example + * ``` + * const array = [1, 2, 3, [4, 5, [6, 7]]]; + * const depth = depthOf(array); + * console.log(depth); + * // 3 + */ export function depthOf(array: unknown | unknown[]): number { return Array.isArray(array) ? 1 + Math.max(0, ...array.map(depthOf)) : 0; } diff --git a/packages/common/src/utils/bigInt.ts b/packages/common/src/utils/bigInt.ts index 3ecdb300..8cf6c073 100644 --- a/packages/common/src/utils/bigInt.ts +++ b/packages/common/src/utils/bigInt.ts @@ -12,6 +12,10 @@ export const _63n = BigInt(63); export const _127n = BigInt(127); export const _128n = BigInt(128); +/** + * Ensure that the given value is a bigint + * @param number + */ export function ensureBigInt(number: NumberLike): bigint { return typeof number === "bigint" ? number : BigInt(number); } @@ -29,6 +33,16 @@ type ParsingOptions = { decimalMark?: string; }; +/** + * Parse a decimal string into a bigint with options + * @param decimalStr + * @param options + * + * @example + * undecimalize("129.8379183", { decimals: 9 }) // 129837918300n + * undecimalize("1", { decimals: 2 }) // 100n + * undecimalize("1", 2) // 100n + */ export function undecimalize(decimalStr: string, options?: ParsingOptions | number): bigint { if (!decimalStr) { return _0n; @@ -60,6 +74,10 @@ export function undecimalize(decimalStr: string, options?: ParsingOptions | numb return BigInt(negative + _stripNonDigits(integer + decimal)); } +/** + * Strip all non-digits from a string + * @param value + */ function _stripNonDigits(value: string): string { return value.replace(/\D/g, ""); } @@ -82,6 +100,15 @@ type FormattingOptions = { decimalMark?: string; }; +/** + * Format a bigint into a decimal string with options + * @param value + * @param options + * + * @example + * decimalize(129837918300n, { decimals: 9 }) // "129.8379183" + * decimalize(100n, { decimals: 2 }) // "1" + */ export function decimalize(value: Amount, options?: FormattingOptions | number): string { value = ensureBigInt(value); if (!options) { @@ -99,6 +126,20 @@ export function decimalize(value: Amount, options?: FormattingOptions | number): return _buildFormattedDecimal(integer.toString(10), decimal.toString(10), options); } +/** + * Format a bigint percentage into a decimal string with options + * @param value + * @param percentage + * @param precision + * + * @example + * ``` + * percent(3498n, 1n) // 34n(1%) + * percent(3498n, 2n) // 69n(2%) + * percent(3498n, 10n) // 349n(10%) + * ``` + * + */ export function percent(value: bigint, percentage: bigint, precision = 2n) { return (value * percentage) / 10n ** precision; } @@ -142,6 +183,25 @@ function _removeLeadingZeros(value: string): string { return value.replace(/^0+\.?/, ""); } +/** + * Sum a collection of numbers by a given iteratee + * @param collection + * @param iteratee + * @param condition + * + * @example + * ``` + * const values = [ + * { key: 1, value: 100n }, + * { key: 2, value: 200n }, + * { key: 3, value: 300n }, + * { key: 4, value: 400n }, + * ]; + * + * sumBy(values, x => x.value) // 1000n + * sumBy(values, x => x.value, x => x.key < 0) // 0n + * sumBy(values, x => x.value, x => x.key % 2 === 0) // 600n + */ export function sumBy( collection: readonly T[], iteratee: (value: T) => bigint, @@ -161,6 +221,10 @@ export function sumBy( return acc; } +/** + * Get the minimum value from a collection of numbers + * @param numbers + */ export function min(...numbers: T[]): T { let min = first(numbers); @@ -173,6 +237,10 @@ export function min(...numbers: T[]): T { return min; } +/** + * Get the maximum value from a collection of numbers + * @param numbers + */ export function max(...numbers: T[]): T { let max = first(numbers); diff --git a/packages/common/src/utils/object.ts b/packages/common/src/utils/object.ts index 7c1fe47c..9dc2f569 100644 --- a/packages/common/src/utils/object.ts +++ b/packages/common/src/utils/object.ts @@ -1,5 +1,16 @@ import { isEmpty, isUndefined } from "./assertions"; +/** + * Remove undefined values from an object + * @param value + * + * @example + * ``` + * const obj = { a: 1, b: undefined }; + * const result = clearUndefined(obj); + * console.log(result); // { a: 1 } + * ``` + */ export function clearUndefined(value: Record) { const result: Record = {}; for (const key in value) { @@ -12,6 +23,19 @@ export function clearUndefined(value: Record) { return result; } +/** + * Ensure that the options object has all the default values + * @param options + * @param defaults + * + * @example + * ``` + * const options = { a: 1 }; + * const defaults = { a: 2, b: 3 }; + * const result = ensureDefaults(options, defaults); + * console.log(result); // { a: 1, b: 3 } + * ``` + */ export function ensureDefaults( options: T | undefined, defaults: R diff --git a/packages/common/src/utils/utxo.ts b/packages/common/src/utils/utxo.ts index bf984d3e..746f8205 100644 --- a/packages/common/src/utils/utxo.ts +++ b/packages/common/src/utils/utxo.ts @@ -13,6 +13,27 @@ import { _0n } from "./bigInt"; const NANOERGS_TOKEN_ID = "nanoErgs"; +/** + * Calculates the sum of all nanoErgs and tokens in the given boxes. + * @param boxes + * + * @example + * ``` + * const boxes = [ + * { + * value: 10, + * assets: [{ tokenId: "test", amount: 20 }] + * }, { + * value: 20, + * assets: [{ tokenId: "test", amount: 30 }] + * } + * ]; + * const sum = utxoSum(boxes); + * console.log(sum); + * // { nanoErgs: 30n, tokens: [{ tokenId: "test", amount: 50n }] } + * ``` + * + */ export function utxoSum(boxes: MinimalBoxAmounts): BoxSummary; export function utxoSum(boxes: MinimalBoxAmounts, tokenId: TokenId): bigint; export function utxoSum(boxes: MinimalBoxAmounts, tokenId?: TokenId): BoxSummary | bigint { @@ -46,6 +67,25 @@ export function utxoSum(boxes: MinimalBoxAmounts, tokenId?: TokenId): BoxSummary }; } +/** + * Calculates the difference between two utxos or utxo sets. + * @param minuend + * @param subtrahend + * + * @example + * ``` + * const minuend = { + * nanoErgs: 30n, + * tokens: [{ tokenId: "test", amount: 50n }] + * }; + * const subtrahend = { + * nanoErgs: 10n, + * tokens: [{ tokenId: "test", amount: 20n }] + * }; + * const diff = utxoDiff(minuend, subtrahend); + * console.log(diff); + * // { nanoErgs: 20n, tokens: [{ tokenId: "test", amount: 30n }] } + */ export function utxoDiff( minuend: BoxSummary | Box[], subtrahend: BoxSummary | Box[] @@ -76,6 +116,21 @@ export function utxoDiff( const MIN_NON_MANDATORY_REGISTER_INDEX = 4; const MAX_NON_MANDATORY_REGISTER_INDEX = 9; +/** + * Checks if the given registers are densely packed. + * @param registers + * + * @example + * ``` + * const registers = { + * R4: "0x0000000000", + * R6: "0x0000000000", + * R7: "0x0000000000", + * }; + * const result = areRegistersDenselyPacked(registers); + * console.log(result); + * // false + */ export function areRegistersDenselyPacked(registers: NonMandatoryRegisters): boolean { let lastIndex = 0; for (let i = MIN_NON_MANDATORY_REGISTER_INDEX; i <= MAX_NON_MANDATORY_REGISTER_INDEX; i++) { @@ -97,6 +152,11 @@ export function areRegistersDenselyPacked(registers: NonMandatoryRegisters): boo return true; } +/** + * Filters the given utxos by the given filter parameters. + * @param utxos + * @param filterParams + */ export function utxoFilter( utxos: Box[], filterParams: UTxOFilterParams @@ -167,6 +227,10 @@ export type MinimalBoxAmounts = readonly { assets: TokenAmount[]; }[]; +/** + * Ensures that the given box candidate has big integer values. + * @param box + */ export function ensureUTxOBigInt(box: Box): Box; export function ensureUTxOBigInt(candidate: BoxCandidate): BoxCandidate; export function ensureUTxOBigInt(