UNPKG

@oazmi/kitchensink

Version:

a collection of personal utility functions

538 lines (537 loc) 24.4 kB
/** utility functions for 1d arrays. * * @module */ import "./_dnt.polyfills.js"; import { array_isEmpty, math_min, math_random, number_POSITIVE_INFINITY, symbol_iterator } from "./alias.js"; import { bind_array_map, bind_array_push } from "./binder.js"; import { absolute, max, min, modulo, roundFloat, sign } from "./numericmethods.js"; export function resolveRange(start, end, length, offset) { start ??= 0; offset ??= 0; if (length === undefined) { return [start + offset, end === undefined ? end : end + offset, length]; } end ??= length; start += start >= 0 ? 0 : length; end += end >= 0 ? 0 : length; length = end - start; return [start + offset, end + offset, max(0, length)]; } /** mutate and rotate the given array by the specified amount to the right. * * given an array `arr`, this function would rotate its rows by the specified `amount`. * a positive `amount` would rotate the rows to the right, and a negative `amount` would rotate it to the left. * * @param arr the array to be rotated. * @param amount The number of indexes to rotate the major-axis to the right. * positive values rotate right, while negative values rotate left. * @returns The original array is returned back after the rotation. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const arr: Array<number> = [1, 2, 3, 4, 5] * * rotateArray(arr, 2) * assertEquals(arr, [4, 5, 1, 2, 3]) * * rotateArray(arr, -3) * assertEquals(arr, [2, 3, 4, 5, 1]) * ``` */ export const rotateArray = (arr, amount) => { const len = arr.length; // compute the effective right-rotation amount so that it handles negative values and full rotations amount = modulo(amount, len === 0 ? 1 : len); // there is nothing to rotate if the effective amount is zero if (amount === 0) { return arr; } const right_removed_rows = arr.splice(len - amount, amount); arr.splice(0, 0, ...right_removed_rows); return arr; }; /** shuffle a 1D array via mutation. the ordering of elements will be randomized by the end. * * ```ts * import { assertEquals, assertNotEquals } from "jsr:@std/assert" * * const * range_100 = Array(100).fill(0).map((_, i) => (i)), // sequntially numbered array * my_arr = range_100.slice() * shuffleArray(my_arr) // shuffling our array via mutation * * // the shuffled array is very unlikely to equal to the original unshuffled form * assertNotEquals(my_arr, range_100) * // sort the shuffled array to assert the preservation of the contained items * assertEquals(my_arr.toSorted((a, b) => (a - b)), range_100) * ``` */ export const shuffleArray = (arr) => { const len = arr.length, rand_int = () => (math_random() * len) | 0, swap = (i1, i2) => { const temp = arr[i1]; arr[i1] = arr[i2]; arr[i2] = temp; }; for (let i = 0; i < len; i++) { swap(i, rand_int()); } return arr; }; /** a generator that shuffles your 1D array via mutation, then yields randomly selected non-repeating elements out of it, one by one, * until all elements have been yielded, at which a new cycle begins, and the items in the array are re-shuffled again. * i.e. after every new cycle, the ordering of the randomly yielded elements will differ from the ordering of the previous cycle. * * moreover, you can call the iterator with an optional number argument that specifies if you wish to skip ahead or go back a certain number of elements. * - `1`: go to next element (default behavior) * - `0`: receive the same element as before * - `-1`: go to previous next element * - `+ve number`: skip to next `number` of elements * - `-ve number`: go back `number` of elements * * note that once a cycle is complete, going back won't restore the correct element from the previous cycle, because the info about the previous cycle gets lost. * * ```ts * import { assert, assertEquals, assertNotEquals } from "jsr:@std/assert" * * const * my_playlist = ["song1", "song2", "song3", "song4"], * my_queue = my_playlist.slice(), * track_iter = shuffledDeque(my_queue) // shuffles our play queue via mutation, and then indefinitely provides unique items each cycle * * const * track1 = track_iter.next().value, * track2 = track_iter.next(1).value, * track3 = track_iter.next().value * * assertEquals(track1, track_iter.next(-2).value) * assertEquals(track2, track_iter.next(1).value) * assertEquals(track3, track_iter.next().value) * * const track4 = track_iter.next().value // final track of the current queue * const track5 = track_iter.next().value // the queue has been reset, and re-shuffled * * assert([track1, track2, track3].includes(track4) === false) * assert([track1, track2, track3, track4].includes(track5) === true) * ``` */ export const shuffledDeque = function* (arr) { let i = arr.length; // this is only temporary. `i` immediately becomes `0` when the while loop begins while (!array_isEmpty(arr)) { if (i >= arr.length) { i = 0; shuffleArray(arr); } i = max(i + ((yield arr[i]) ?? 1), 0); } }; /** a function to splice any stack (see the {@link GenericStack} interface). * * splicing alone lets you effectively implement all sorts of array mutation methods, such as * `push`, `pop`, `unshift`, `shift`, `insert`, `rotate`, and many more. * * > [!note] * > the `length` property of your `stack` is not mutated/assigned by this function. * > you will have to do that manually yourself if your `stack` does not modify the `length` property upon the `push` and `pop` operations. * * @param stack the generic stack object to splice. * @param start the starting index to begin splicing from. * you must provide only positive starting index values. * defaults to `0`. * @param deleteCount the number of elements to remove from the `start` index (inclusive). * if it is set to `undefined`, then all elements until the end of the generic stack array will be removed. * defaults to `undefined`. * @param items insert items at the `start` index, so that the first inserted item will occupy the `start` index _after_ the splicing. * @returns an array of deleted items in the generic stack will be returned. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const * fn = spliceGenericStack, * eq = assertEquals * * const my_stack = [0, 1, 2, 3, 4, 5, 6, 7] * * eq(fn(my_stack, 4), [4, 5, 6, 7]) * eq(my_stack, [0, 1, 2, 3]) * * eq(fn(my_stack, 0, 0, -3, -2, -1), []) * eq(my_stack, [-3, -2, -1, 0, 1, 2, 3]) * * eq(fn(my_stack, 4, 2, 0.1, 0.2, 0.3), [1, 2]) * eq(my_stack, [-3, -2, -1, 0, 0.1, 0.2, 0.3, 3]) * * eq(fn(my_stack), [-3, -2, -1, 0, 0.1, 0.2, 0.3, 3]) * eq(my_stack, []) * ``` */ export const spliceGenericStack = (stack, start = 0, deleteCount, ...items) => { const initial_length = stack.length, maxDeleteCount = initial_length - start; deleteCount ??= maxDeleteCount; deleteCount = min(deleteCount, maxDeleteCount); const end = start + deleteCount, retained_items = [], removed_items = [], retained_items_push = bind_array_push(retained_items), removed_items_push = bind_array_push(removed_items); // collect the items that will be be retained and re-pushed back into the `arr` later on for (let i = initial_length; i > end; i--) { retained_items_push(stack.pop()); } // collect the items that will be removed for (let i = end; i > start; i--) { removed_items_push(stack.pop()); } // then push the new `items`, followed by the reverse of `retained_items` stack.push(...items, ...retained_items.toReversed()); // `toReversed()` is faster than the `reverse()` method for large arrays. return removed_items.toReversed(); }; /** generate a numeric array with sequentially increasing value, within a specific range interval. * similar to python's `range` function. * * however, unlike python's `range`, you **must** always supply the starting index **and** the ending index, * even if the start index is supposed to be `0`, you cannot substitute the first argument with the ending index. * only the {@link step} argument is optional. moreover, the {@link step} argument must always be a positive number. * * > [!note] * > there is also an iterator generator variant of this function that is also capable of indefinite sequences. * > check out {@link rangeIterator} for details. * * @param start the initial number to begin the output range sequence from. * @param end the final exclusive number to end the output range sequence at. its value will **not** be in the output array. * @param step a **positive** number, dictating how large each step from the `start` to the `end` should be. * for safety, so that a user doesn't run into an infinite loop by providing a negative step value, * we always take the absolute value of this parameter. * defaults to `1`. * @param decimal_precision an integer that specifies the number of decimal places to which the output * numbers should be rounded to, in order to nullify floating point arithmetic inaccuracy. * for instance, in javascript `0.1 + 0.2 = 0.30000000000000004` instead of `0.3`. * now, you'd certainly not want to see this kind of number in our output, which is why we round it so that it becomes `0.3`. * defaults to `6` (6 decimal places; i.e. rounds to the closest micro-number (10**(-6))). * @returns a numeric array with sequentially increasing value from the `start` to the `end` interval, with steps of size `step`. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const * fn = rangeArray, * eq = assertEquals * * eq(fn(0, 5), [0, 1, 2, 3, 4]) * eq(fn(-2, 3), [-2, -1, 0, 1, 2]) * eq(fn(2, 7), [2, 3, 4, 5, 6]) * eq(fn(2, 7.1), [2, 3, 4, 5, 6, 7]) * eq(fn(0, 1, 0.2), [0, 0.2, 0.4, 0.6, 0.8]) * eq(fn(0, 100, 20), [0, 20, 40, 60, 80]) * eq(fn(2, -3), [2, 1, 0, -1, -2]) * eq(fn(2, -7, 2), [2, 0, -2, -4, -6]) * eq(fn(2, -7, -2), [2, 0, -2, -4, -6]) // as a protective measure, only the `abs(step)` value is ever taken. * eq(fn(2, 7, -1), [2, 3, 4, 5, 6]) // as a protective measure, only the `abs(step)` value is ever taken. * ``` */ export const rangeArray = (start, end, step = 1, decimal_precision = 6) => { return [...rangeIterator(start, end, step, decimal_precision)]; }; /** this function is the iterator version of {@link rangeArray}, mimicking python's `range` function. * * you can iterate indefinitely with this function if you set the {@link end} parameter to `undefined`, * and then define the direction of the step increments with the {@link step} parameter. * (a negative `step` will result in a decreasing sequence of numbers). * * @param start the initial number to begin the output range sequence from. defaults to `0`. * @param end the final exclusive number to end the output range sequence at. its value will **not** be in the last output number. * if left `undefined`, then it will be assumed to be `Number.POSITIVE_INFINITY` if `step` is a positive number (default), * or it will become `Number.NEGATIVE_INFINITY` if `step` is a negative number. * defaults to `undefined`. * @param step a number, dictating how large each step from the `start` to the `end` should be. defaults to `1`. * @param decimal_precision an integer that specifies the number of decimal places to which the output * numbers should be rounded to, in order to nullify floating point arithmetic inaccuracy. * defaults to `6` (6 decimal places; i.e. rounds to the closest micro-number (10**(-6))). * @yields a number in the sequence of the given range. * @returns the total number of elements that were outputted. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const * fn = rangeIterator, * eq = assertEquals * * eq([...fn(0, 5)], [0, 1, 2, 3, 4]) * eq([...fn(-2, 3)], [-2, -1, 0, 1, 2]) * eq([...fn(2, 7)], [2, 3, 4, 5, 6]) * eq([...fn(2, 7.1)], [2, 3, 4, 5, 6, 7]) * eq([...fn(0, 1, 0.2)], [0, 0.2, 0.4, 0.6, 0.8]) * eq([...fn(1, -1, 0.4)], [1, 0.6, 0.2, -0.2, -0.6]) * eq([...fn(1, -1, -0.4)], [1, 0.6, 0.2, -0.2, -0.6]) * * // indefinite sequence in the positive direction * const * loop_limit = 10, * accumulation_arr: number[] = [] * for (const v of fn(0)) { * if (v >= loop_limit) { break } * accumulation_arr.push(v) * } * eq(accumulation_arr, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) * accumulation_arr.splice(0) // clearing our array for the next test * * // indefinite sequence in the negative direction * for (const v of fn(0, undefined, -1)) { * if (v <= -loop_limit) { break } * accumulation_arr.push(v) * } * eq(accumulation_arr, [0, -1, -2, -3, -4, -5, -6, -7, -8, -9]) * ``` */ export const rangeIterator = function* (start = 0, end, step = 1, decimal_precision = 6) { end ??= sign(step) * number_POSITIVE_INFINITY; const delta = end - start, signed_step = absolute(step) * sign(delta), end_index = delta / signed_step; let i = 0; for (; i < end_index; i++) { yield roundFloat(start + i * signed_step, decimal_precision); } return i; }; /** zip together a list of input arrays as tuples, similar to python's `zip` function. * * > [!note] * > if one of the input arrays is shorter in length than all the other input arrays, * > then this zip function will only generate tuples up until the shortest array is expended, * > similar to how python's `zip` function behaves. * > in a sense, this feature is what sets it apart from the 2d array transpose function {@link transposeArray2D}, * > which decides its output length based on the first array's length. * * > [!tip] * > applying the zip function twice will give you back the original arrays (assuming they all had the same length). * > so in a sense, to unzip the output of `zipArrays`, you simply apply `zipArrays` to again (after performing an array spread operation). * * > [!important] * > this function only accepts array inputs to zip, and **not** iterators. * > to zip a sequence of iterators, use the {@link zipIterators} function (which has a slightly slower performance). * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * type MyObj = { key: string } * type MyTuple = [number, boolean, MyObj] * * const * my_num_arr: number[] = [100, 101, 102, 103, 104], * my_bool_arr: boolean[] = [true, false, false, true, false], * my_obj_arr: MyObj[] = [{ key: "a" }, { key: "b" }, { key: "c" }, { key: "d" }] * // notice that `my_obj_arr` is shorter than the other two arrays. (i.e. has a length of `4`, while others are `5`) * // this would mean that zipping them together would only generate a 3-tuple array of `4` elements. * * const my_tuples_arr: MyTuple[] = zipArrays<[number, boolean, MyObj]>(my_num_arr, my_bool_arr, my_obj_arr) * assertEquals(my_tuples_arr, [ * [100, true, { key: "a" }], * [101, false, { key: "b" }], * [102, false, { key: "c" }], * [103, true, { key: "d" }], * ]) * * // to unzip the array of tuples, and receive back the original (trimmed) arrays, simply apply `zipArrays` again. * const my_arrs = [ * [ 1, 2, 3, 4], * [true, false, true, true], * [ "w", "x", "y", "z"], * ] * assertEquals(zipArrays(...zipArrays(...my_arrs)), my_arrs) * * // zipping no input arrays should not iterate infinitely. * assertEquals(zipArrays(), []) * ``` */ export const zipArrays = (...arrays) => { const output = [], output_push = bind_array_push(output), // NOTE: `math_min()` returns `Infinity` when no input is given! // thus we must check for zero sized `arrays` in order to not loop infinitely. (learned it the hard way) min_len = array_isEmpty(arrays) ? 0 : math_min(...arrays.map((arr) => (arr.length))); for (let i = 0; i < min_len; i++) { // TODO: CONSIDER: honestly, using `arrays.map` doesn't seem too performant. // I feel like using array indexing would be faster, but that will turn this // function to basically `transposeArray2D`, implementation wise. output_push(arrays.map((arr) => arr[i])); } return output; }; /** zip together a list of input iterators or iterable objects as tuples, similar to python's `zip` function. * * > [!note] * > this zip function stops yielding as soon as one of its input iterators is "done" iterating (i.e. out of elements). * * if all of your input `iterators` are arrays, then use the {@link zipArrays} function, which is more performant (and smaller in footprint). * * @param iterators the list of iterators/iterable objects which should be zipped. * @yields a tuple of each entry from the given list of `iterators`, until one of the iterators is "done" iterating (i.e. out of elements). * @returns the number of items that were yielded/iterated (i.e. length of iterator). * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * type MyObj = { key: string } * type MyTuple = [number, boolean, MyObj] * * const * my_num_iter: Iterable<number> = rangeIterator(100), // infnite iterable, with values `[100, 101, 102, ...]` * my_bool_iter: Iterator<boolean> = [true, false, false, true, false][Symbol.iterator](), * my_obj_iter: Iterable<MyObj> = [{ key: "a" }, { key: "b" }, { key: "c" }, { key: "d" }] * // notice that `my_obj_iter` is shorter than the other two arrays. (i.e. has a length of `4`) * // this would mean that zipping them together would only generate a 3-tuple array of `4` elements. * * const my_tuples_iter: Iterator<MyTuple> = zipIterators<[number, boolean, MyObj]>(my_num_iter, my_bool_iter, my_obj_iter) * assertEquals(my_tuples_iter.next(), { value: [100, true, { key: "a" }], done: false }) * assertEquals(my_tuples_iter.next(), { value: [101, false, { key: "b" }], done: false }) * assertEquals(my_tuples_iter.next(), { value: [102, false, { key: "c" }], done: false }) * assertEquals(my_tuples_iter.next(), { value: [103, true, { key: "d" }], done: false }) * assertEquals(my_tuples_iter.next(), { value: 4, done: true }) // the return value of the iterator dictates its length. * * * // since the actual output of `zipIterators` is an `IterableIterator`, * // so we may even use it in a for-of loop, or do an array spreading with the output. * const my_tuples_iter2 = zipIterators<[number, boolean]>(my_num_iter, [false, true, false, false, true]) * my_tuples_iter2 satisfies Iterable<[number, boolean]> * * // IMPORTANT: notice that the first tuple is not `[104, false]`, but instead `[105, false]`. * // this is because our first zip iterator (`my_tuples_iter`) utilized the `my_num_iter` iterable one additional time * // before realizing that one of the input iterables (the `my_bool_iter`) had gone out of elements to provide. * // thus, the ordering of the iterators do matter, and it is possible to have one iterated value to disappear into the void. * assertEquals([...my_tuples_iter2], [ * [105, false], * [106, true ], * [107, false], * [108, false], * [109, true ], * ]) * * // zipping with zero sized input iterators should not yield anything. * assertEquals([...zipIterators([], [], [])], []) * * // zipping with no input iterators at all should not iterate infinitely. * assertEquals([...zipIterators()], []) * ``` */ export const zipIterators = function* (...iterators) { // if there are no `iterators`, then we should return immediately, otherwise we will be stuck in an infinitely yielding loop. if (array_isEmpty(iterators)) { return 0; } // first we convert all potential `Iterable` entries to an `Iterator`. const pure_iterators = iterators.map((iter) => { return iter instanceof Iterator ? iter : iter[symbol_iterator](); }), pure_iterators_map = bind_array_map(pure_iterators); let length = 0, continue_iterating = true; const iterator_map_fn = (iter) => { const { value, done } = iter.next(); if (done) { continue_iterating = false; } return value; }; for (let tuple_values = pure_iterators_map(iterator_map_fn); continue_iterating; tuple_values = pure_iterators_map(iterator_map_fn)) { length++; yield tuple_values; } return length; }; /** create a mapping function that operates on a list of iterable/iterator inputs, that are zipped together as tuples, * and then passed on to the {@link map_fn} for transformation, one by one. * * > [!note] * > if one of the input arrays or iterators is shorter in length than all the rest, * > then the mapping function will only operate up till the shortest array/iterator. * > similar to how python's `zip` function generates tuples up till the end of the shortest input array. * * @param map_fn a function that maps each tuple `T` (from the collection of input iterators) to some type `V`. * @returns a generator function that will accept a list of iterators as its input, * and that yields back the result of each zipped tuple being mapped via `map_fn`. * the return value of the generator (after it concludes) is the length of the number of items that it had yielded. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * type MyObj = { key: string } * type MyTuple = [number, boolean, MyObj] * * const myTupleMapper = zipIteratorsMapperFactory((tuple: MyTuple, index: number): string => { * const [my_number, my_boolean, my_object] = tuple * return `${index}-${my_object.key}/${my_number}/${my_boolean}` * }) * * myTupleMapper satisfies ((number_arr: number[], boolean_arr: boolean[], object_arr: MyObj[]) => IterableIterator<string>) * * const * my_num_iter = rangeIterator(100), // infnite iterable, with values `[100, 101, 102, ...]` * my_bool_arr = [true, false, false, true, false], * my_obj_arr = [{ key: "a" }, { key: "b" }, { key: "c" }, { key: "d" }] * // notice that `my_obj_arr` is shorter than the other two arrays. (i.e has a length of `4`). * // this would mean that `myTupleMapper` would only operate on the first `4` elements of all the 3 arrays. * * const outputs_iter: Iterable<string> = myTupleMapper(my_num_iter, my_bool_arr, my_obj_arr) * assertEquals([...outputs_iter], [ * "0-a/100/true", * "1-b/101/false", * "2-c/102/false", * "3-d/103/true", * ]) * * // zipping with zero sized input iterators should not yield anything. * assertEquals([...myTupleMapper([], [], [])], []) * * // for safety, map-zipping with no input iterators should not yield anything. * assertEquals([...myTupleMapper()], []) * ``` */ export const zipIteratorsMapperFactory = (map_fn) => { return function* (...iterators) { let i = 0; for (const tuple of zipIterators(...iterators)) { yield map_fn(tuple, i); i++; } return i; }; }; /** a generator function that slices your input `array` to smaller chunks of your desired `chunk_size`. * * note that the final chunk that gets yielded may be smaller than your `chunk_size` if it does not divide `array.length` precisely. * * @param chunk_size a **positive** integer dictating the length of each chunk that gets yielded. * @param array your input array that needs to be yielded in chunks. * @yields a chunk of length `chunk_size` from your input `array`. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const my_arr = rangeArray(0, 30) // equals to `[0, 1, 2, ..., 28, 29]` * * // below, we split `my_arr` into smaller array chunks of size `8`, except for the last chunk, which is smaller. * assertEquals([...chunkGenerator(8, my_arr)], [ * [ 0, 1, 2, 3, 4, 5, 6, 7 ], * [ 8, 9, 10, 11, 12, 13, 14, 15], * [16, 17, 18, 19, 20, 21, 22, 23], * [24, 25, 26, 27, 28, 29], * ]) * * // chunking zero length array will not yield anything * assertEquals([...chunkGenerator(8, [])], []) * ``` */ export const chunkGenerator = function* (chunk_size, array) { const len = array.length; for (let i = 0; i < len; i += chunk_size) { yield array.slice(i, i + chunk_size); } };