« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/util/sugar.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/sugar.js')
-rw-r--r--src/util/sugar.js849
1 files changed, 0 insertions, 849 deletions
diff --git a/src/util/sugar.js b/src/util/sugar.js
deleted file mode 100644
index 7dd173a0..00000000
--- a/src/util/sugar.js
+++ /dev/null
@@ -1,849 +0,0 @@
-// Syntactic sugar! (Mostly.)
-// Generic functions - these are useful just a8out everywhere.
-//
-// Friendly(!) disclaimer: these utility functions haven't 8een tested all that
-// much. Do not assume it will do exactly what you want it to do in all cases.
-// It will likely only do exactly what I want it to, and only in the cases I
-// decided were relevant enough to 8other handling.
-
-import {colors} from './cli.js';
-
-// Apparently JavaScript doesn't come with a function to split an array into
-// chunks! Weird. Anyway, this is an awesome place to use a generator, even
-// though we don't really make use of the 8enefits of generators any time we
-// actually use this. 8ut it's still awesome, 8ecause I say so.
-export function* splitArray(array, fn) {
-  let lastIndex = 0;
-  while (lastIndex < array.length) {
-    let nextIndex = array.findIndex((item, index) => index >= lastIndex && fn(item));
-    if (nextIndex === -1) {
-      nextIndex = array.length;
-    }
-    yield array.slice(lastIndex, nextIndex);
-    // Plus one because we don't want to include the dividing line in the
-    // next array we yield.
-    lastIndex = nextIndex + 1;
-  }
-}
-
-// Null-accepting function to check if an array or set is empty. Accepts null
-// (which is treated as empty) as a shorthand for "hey, check if this property
-// is an array with/without stuff in it" for objects where properties that are
-// PRESENT but don't currently have a VALUE are null (rather than undefined).
-export function empty(value) {
-  if (value === null) {
-    return true;
-  }
-
-  if (Array.isArray(value)) {
-    return value.length === 0;
-  }
-
-  if (value instanceof Set) {
-    return value.size === 0;
-  }
-
-  throw new Error(`Expected array, set, or null`);
-}
-
-// Repeats all the items of an array a number of times.
-export function repeat(times, array) {
-  if (times === 0) return [];
-  if (array === null || array === undefined) return [];
-  if (Array.isArray(array) && empty(array)) return [];
-
-  const out = [];
-
-  for (let n = 1; n <= times; n++) {
-    const value =
-      (typeof array === 'function'
-        ? array()
-        : array);
-
-    if (Array.isArray(value)) out.push(...value);
-    else out.push(value);
-  }
-
-  return out;
-}
-
-// Gets a random item from an array.
-export function pick(array) {
-  return array[Math.floor(Math.random() * array.length)];
-}
-
-// Gets the item at an index relative to another index.
-export function atOffset(array, index, offset, {
-  wrap = false,
-  valuePastEdge = null,
-} = {}) {
-  if (index === -1) {
-    return valuePastEdge;
-  }
-
-  if (offset === 0) {
-    return array[index];
-  }
-
-  if (wrap) {
-    return array[(index + offset) % array.length];
-  }
-
-  if (offset > 0 && index + offset > array.length - 1) {
-    return valuePastEdge;
-  }
-
-  if (offset < 0 && index + offset < 0) {
-    return valuePastEdge;
-  }
-
-  return array[index + offset];
-}
-
-// Gets the index of the first item that satisfies the provided function,
-// or, if none does, returns the length of the array (the index just past the
-// final item).
-export function findIndexOrEnd(array, fn) {
-  const index = array.findIndex(fn);
-  if (index >= 0) {
-    return index;
-  } else {
-    return array.length;
-  }
-}
-
-// Sums the values in an array, optionally taking a function which maps each
-// item to a number (handy for accessing a certain property on an array of like
-// objects). This also coalesces null values to zero, so if the mapping function
-// returns null (or values in the array are nullish), they'll just be skipped in
-// the sum.
-export function accumulateSum(array, fn = x => x) {
-  return array.reduce(
-    (accumulator, value, index, array) =>
-      accumulator +
-        fn(value, index, array) ?? 0,
-    0);
-}
-
-// Stitches together the items of separate arrays into one array of objects
-// whose keys are the corresponding items from each array at that index.
-// This is mostly useful for iterating over multiple arrays at once!
-export function stitchArrays(keyToArray) {
-  const errors = [];
-
-  for (const [key, value] of Object.entries(keyToArray)) {
-    if (value === null) continue;
-    if (Array.isArray(value)) continue;
-    errors.push(new TypeError(`(${key}) Expected array or null, got ${typeAppearance(value)}`));
-  }
-
-  if (!empty(errors)) {
-    throw new AggregateError(errors, `Expected arrays or null`);
-  }
-
-  const keys = Object.keys(keyToArray);
-  const arrays = Object.values(keyToArray).filter(val => Array.isArray(val));
-  const length = Math.max(...arrays.map(({length}) => length));
-  const results = [];
-
-  for (let i = 0; i < length; i++) {
-    const object = {};
-    for (const key of keys) {
-      object[key] =
-        (Array.isArray(keyToArray[key])
-          ? keyToArray[key][i]
-          : null);
-    }
-    results.push(object);
-  }
-
-  return results;
-}
-
-// Like Map.groupBy! Collects the items of an unsorted array into buckets
-// according to a per-item computed value.
-export function groupArray(items, fn) {
-  const buckets = new Map();
-
-  for (const [index, item] of Array.prototype.entries.call(items)) {
-    const key = fn(item, index);
-    if (buckets.has(key)) {
-      buckets.get(key).push(item);
-    } else {
-      buckets.set(key, [item]);
-    }
-  }
-
-  return buckets;
-}
-
-// Turns this:
-//
-//   [
-//     [123, 'orange', null],
-//     [456, 'apple', true],
-//     [789, 'banana', false],
-//     [1000, 'pear', undefined],
-//   ]
-//
-// Into this:
-//
-//   [
-//     [123, 456, 789, 1000],
-//     ['orange', 'apple', 'banana', 'pear'],
-//     [null, true, false, undefined],
-//   ]
-//
-// And back again, if you call it again on its results.
-export function transposeArrays(arrays) {
-  if (empty(arrays)) {
-    return [];
-  }
-
-  const length = arrays[0].length;
-  const results = new Array(length).fill(null).map(() => []);
-
-  for (const array of arrays) {
-    for (let i = 0; i < length; i++) {
-      results[i].push(array[i]);
-    }
-  }
-
-  return results;
-}
-
-export const mapInPlace = (array, fn) =>
-  array.splice(0, array.length, ...array.map(fn));
-
-export const unique = (arr) => Array.from(new Set(arr));
-
-export const compareArrays = (arr1, arr2, {checkOrder = true} = {}) =>
-  arr1.length === arr2.length &&
-  (checkOrder
-    ? arr1.every((x, i) => arr2[i] === x)
-    : arr1.every((x) => arr2.includes(x)));
-
-export function compareObjects(obj1, obj2, {
-  checkOrder = false,
-  checkSymbols = true,
-} = {}) {
-  const keys1 = Object.keys(obj1);
-  const keys2 = Object.keys(obj2);
-  if (!compareArrays(keys1, keys2, {checkOrder})) return false;
-
-  let syms1, syms2;
-  if (checkSymbols) {
-    syms1 = Object.getOwnPropertySymbols(obj1);
-    syms2 = Object.getOwnPropertySymbols(obj2);
-    if (!compareArrays(syms1, syms2, {checkOrder})) return false;
-  }
-
-  for (const key of keys1) {
-    if (obj2[key] !== obj1[key]) return false;
-  }
-
-  if (checkSymbols) {
-    for (const sym of syms1) {
-      if (obj2[sym] !== obj1[sym]) return false;
-    }
-  }
-
-  return true;
-}
-
-// Stolen from jq! Which pro8a8ly stole the concept from other places. Nice.
-export const withEntries = (obj, fn) => {
-  const result = fn(Object.entries(obj));
-  if (result instanceof Promise) {
-    return result.then(entries => Object.fromEntries(entries));
-  } else {
-    return Object.fromEntries(result);
-  }
-}
-
-export function setIntersection(set1, set2) {
-  const intersection = new Set();
-  for (const item of set1) {
-    if (set2.has(item)) {
-      intersection.add(item);
-    }
-  }
-  return intersection;
-}
-
-export function filterProperties(object, properties, {
-  preserveOriginalOrder = false,
-} = {}) {
-  if (typeof object !== 'object' || object === null) {
-    throw new TypeError(`Expected object to be an object, got ${typeAppearance(object)}`);
-  }
-
-  if (!Array.isArray(properties)) {
-    throw new TypeError(`Expected properties to be an array, got ${typeAppearance(properties)}`);
-  }
-
-  const filteredObject = {};
-
-  if (preserveOriginalOrder) {
-    for (const property of Object.keys(object)) {
-      if (properties.includes(property)) {
-        filteredObject[property] = object[property];
-      }
-    }
-  } else {
-    for (const property of properties) {
-      if (Object.hasOwn(object, property)) {
-        filteredObject[property] = object[property];
-      }
-    }
-  }
-
-  return filteredObject;
-}
-
-export function queue(array, max = 50) {
-  if (max === 0) {
-    return array.map((fn) => fn());
-  }
-
-  const begin = [];
-  let current = 0;
-  const ret = array.map(
-    (fn) =>
-      new Promise((resolve, reject) => {
-        begin.push(() => {
-          current++;
-          Promise.resolve(fn()).then((value) => {
-            current--;
-            if (current < max && begin.length) {
-              begin.shift()();
-            }
-            resolve(value);
-          }, reject);
-        });
-      })
-  );
-
-  for (let i = 0; i < max && begin.length; i++) {
-    begin.shift()();
-  }
-
-  return ret;
-}
-
-export function delay(ms) {
-  return new Promise((res) => setTimeout(res, ms));
-}
-
-export function promiseWithResolvers() {
-  let obj = {};
-
-  obj.promise =
-    new Promise((...opts) =>
-      ([obj.resolve, obj.reject] = opts));
-
-  return obj;
-}
-
-// Stolen from here: https://stackoverflow.com/a/3561711
-//
-// There's a proposal for a native JS function like this, 8ut it's not even
-// past stage ~~1~~ 2 yet: https://github.com/tc39/proposal-regex-escaping
-export function escapeRegex(string) {
-  return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
-}
-
-export function splitKeys(key) {
-  return key.split(/(?<=(?<!\\)(?:\\\\)*)\./);
-}
-
-// Follows a key path like 'foo.bar.baz' to get an item nested deeply inside
-// an object.
-export function getNestedProp(obj, key) {
-  const recursive = (o, k) =>
-    (k.length === 1
-      ? o[k[0]]
-      : recursive(o[k[0]], k.slice(1)));
-
-  return recursive(obj, splitKeys(key));
-}
-
-// Gets the "look" of some arbitrary value. It's like typeof, but smarter.
-// Don't use this for actually validating types - it's only suitable for
-// inclusion in error messages.
-export function typeAppearance(value) {
-  if (value === null) return 'null';
-  if (value === undefined) return 'undefined';
-  if (Array.isArray(value)) return 'array';
-  return typeof value;
-}
-
-// Limits a string to the desired length, filling in an ellipsis at the end
-// if it cuts any text off.
-export function cut(text, length = 40) {
-  if (text.length >= length) {
-    const index = Math.max(1, length - 3);
-    return text.slice(0, index) + '...';
-  } else {
-    return text;
-  }
-}
-
-// Limits a string to the desired length, filling in an ellipsis at the start
-// if it cuts any text off.
-export function cutStart(text, length = 40) {
-  if (text.length >= length) {
-    const index = Math.min(text.length - 1, text.length - length + 3);
-    return '...' + text.slice(index);
-  } else {
-    return text;
-  }
-}
-
-// Wrapper function around wrap(), ha, ha - this requires the Node module
-// 'node-wrap'.
-export function indentWrap(str, {
-  wrap,
-  spaces = 0,
-  width = 60,
-  bullet = false,
-}) {
-  const wrapped =
-    wrap(str, {
-      width: width - spaces,
-      indent: ' '.repeat(spaces),
-    });
-
-  if (bullet) {
-    return wrapped.trimStart();
-  } else {
-    return wrapped;
-  }
-}
-
-// Annotates {index, length} results from another iterator with contextual
-// details, including:
-//
-// * its line and column numbers;
-// * if `formatWhere` is true (the default), a pretty-formatted,
-//   human-readable indication of the match's placement in the string;
-// * if `getContainingLine` is true, the entire line (or multiple lines)
-//   of text containing the match.
-//
-export function* iterateMultiline(content, iterator, {
-  formatWhere = true,
-  getContainingLine = false,
-} = {}) {
-  const lineRegexp = /\n/g;
-  const isMultiline = content.includes('\n');
-
-  let lineNumber = 0;
-  let startOfLine = 0;
-  let previousIndex = 0;
-
-  const countLineBreaks = (index, length) => {
-    const range = content.slice(index, index + length);
-    const lineBreaks = Array.from(range.matchAll(lineRegexp));
-    if (!empty(lineBreaks)) {
-      lineNumber += lineBreaks.length;
-      startOfLine = index + lineBreaks.at(-1).index + 1;
-    }
-  };
-
-  for (const result of iterator) {
-    const {index, length} = result;
-
-    countLineBreaks(previousIndex, index - previousIndex);
-
-    const matchStartOfLine = startOfLine;
-
-    previousIndex = index + length;
-
-    const columnNumber = index - startOfLine;
-
-    let where = null;
-    if (formatWhere) {
-      where =
-        colors.yellow(
-          (isMultiline
-            ? `line: ${lineNumber + 1}, col: ${columnNumber + 1}`
-            : `pos: ${index + 1}`));
-    }
-
-    countLineBreaks(index, length);
-
-    let containingLine = null;
-    if (getContainingLine) {
-      const nextLineResult =
-        content
-          .slice(previousIndex)
-          .matchAll(lineRegexp)
-          .next();
-
-      const nextStartOfLine =
-        (nextLineResult.done
-          ? content.length
-          : previousIndex + nextLineResult.value.index);
-
-      containingLine =
-        content.slice(matchStartOfLine, nextStartOfLine);
-    }
-
-    yield {
-      ...result,
-      lineNumber,
-      columnNumber,
-      where,
-      containingLine,
-    };
-  }
-}
-
-// Iterates over regular expression matches within a single- or multiline
-// string, yielding each match as well as contextual details; this accepts
-// the same options (and provides the same context) as iterateMultiline.
-export function* matchMultiline(content, matchRegexp, options) {
-  const matchAllIterator =
-    content.matchAll(matchRegexp);
-
-  const cleanMatchAllIterator =
-    (function*() {
-      for (const match of matchAllIterator) {
-        yield {
-          index: match.index,
-          length: match[0].length,
-          match,
-        };
-      }
-    })();
-
-  const multilineIterator =
-    iterateMultiline(content, cleanMatchAllIterator, options);
-
-  yield* multilineIterator;
-}
-
-// Binds default values for arguments in a {key: value} type function argument
-// (typically the second argument, but may be overridden by providing a
-// [bindOpts.bindIndex] argument). Typically useful for preparing a function for
-// reuse within one or multiple other contexts, which may not be aware of
-// required or relevant values provided in the initial context.
-//
-// This function also passes the identity of `this` through (the returned value
-// is not an arrow function), though note it's not a true bound function either
-// (since Function.prototype.bind only supports positional arguments, not
-// "options" specified via key/value).
-//
-export function bindOpts(fn, bind) {
-  const bindIndex = bind[bindOpts.bindIndex] ?? 1;
-
-  const bound = function (...args) {
-    const opts = args[bindIndex] ?? {};
-    return Reflect.apply(fn, this, [
-      ...args.slice(0, bindIndex),
-      {...bind, ...opts}
-    ]);
-  };
-
-  annotateFunction(bound, {
-    name: fn,
-    trait: 'options-bound',
-  });
-
-  for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(fn))) {
-    if (key === 'length') continue;
-    if (key === 'name') continue;
-    if (key === 'arguments') continue;
-    if (key === 'caller') continue;
-    if (key === 'prototype') continue;
-    Object.defineProperty(bound, key, descriptor);
-  }
-
-  return bound;
-}
-
-bindOpts.bindIndex = Symbol();
-
-// Sorts multiple arrays by an arbitrary function (which is the last argument).
-// Paired values from each array are provided to the callback sequentially:
-//
-//   (a_fromFirstArray, b_fromFirstArray,
-//    a_fromSecondArray, b_fromSecondArray,
-//    a_fromThirdArray, b_fromThirdArray) =>
-//     relative positioning (negative, positive, or zero)
-//
-// Like native single-array sort, this is a mutating function.
-export function sortMultipleArrays(...args) {
-  const arrays = args.slice(0, -1);
-  const fn = args.at(-1);
-
-  const length = arrays[0].length;
-  const symbols = new Array(length).fill(null).map(() => Symbol());
-  const indexes = Object.fromEntries(symbols.map((symbol, index) => [symbol, index]));
-
-  symbols.sort((a, b) => {
-    const indexA = indexes[a];
-    const indexB = indexes[b];
-
-    const args = [];
-    for (let i = 0; i < arrays.length; i++) {
-      args.push(arrays[i][indexA]);
-      args.push(arrays[i][indexB]);
-    }
-
-    return fn(...args);
-  });
-
-  for (const array of arrays) {
-    // Note: We're mutating this array pulling values from itself, but only all
-    // at once after all those values have been pulled.
-    array.splice(0, array.length, ...symbols.map(symbol => array[indexes[symbol]]));
-  }
-
-  return arrays;
-}
-
-// Filters multiple arrays by an arbitrary function (which is the last argument).
-// Values from each array are provided to the callback sequentially:
-//
-//   (value_fromFirstArray,
-//    value_fromSecondArray,
-//    value_fromThirdArray,
-//    index,
-//    [firstArray, secondArray, thirdArray]) =>
-//      true or false
-//
-// Please be aware that this is a mutating function, unlike native single-array
-// filter. The mutated arrays are returned. Also attached under `.removed` are
-// corresponding arrays of items filtered out.
-export function filterMultipleArrays(...args) {
-  const arrays = args.slice(0, -1);
-  const fn = args.at(-1);
-
-  const removed = new Array(arrays.length).fill(null).map(() => []);
-
-  for (let i = arrays[0].length - 1; i >= 0; i--) {
-    const args = arrays.map(array => array[i]);
-    args.push(i, arrays);
-
-    if (!fn(...args)) {
-      for (let j = 0; j < arrays.length; j++) {
-        const item = arrays[j][i];
-        arrays[j].splice(i, 1);
-        removed[j].unshift(item);
-      }
-    }
-  }
-
-  Object.assign(arrays, {removed});
-  return arrays;
-}
-
-// Corresponding filter function for sortByCount. By default, items whose
-// corresponding count is zero will be removed.
-export function filterByCount(data, counts, {
-  min = 1,
-  max = Infinity,
-} = {}) {
-  filterMultipleArrays(data, counts, (data, count) =>
-    count >= min && count <= max);
-}
-
-// Reduces multiple arrays with an arbitrary function (which is the last
-// argument). Note that this reduces into multiple accumulators, one for
-// each input array, not just a single value. That's reflected in both the
-// callback parameters:
-//
-//   (accumulator1,
-//    accumulator2,
-//    value_fromFirstArray,
-//    value_fromSecondArray,
-//    index,
-//    [firstArray, secondArray]) =>
-//      [newAccumulator1, newAccumulator2]
-//
-// As well as the final return value of reduceMultipleArrays:
-//
-//   [finalAccumulator1, finalAccumulator2]
-//
-// This is not a mutating function.
-export function reduceMultipleArrays(...args) {
-  const [arrays, fn, initialAccumulators] =
-    (typeof args.at(-1) === 'function'
-      ? [args.slice(0, -1), args.at(-1), null]
-      : [args.slice(0, -2), args.at(-2), args.at(-1)]);
-
-  if (empty(arrays[0])) {
-    throw new TypeError(`Reduce of empty arrays with no initial value`);
-  }
-
-  let [accumulators, i] =
-    (initialAccumulators
-      ? [initialAccumulators, 0]
-      : [arrays.map(array => array[0]), 1]);
-
-  for (; i < arrays[0].length; i++) {
-    const args = [...accumulators, ...arrays.map(array => array[i])];
-    args.push(i, arrays);
-    accumulators = fn(...args);
-  }
-
-  return accumulators;
-}
-
-export function chunkByConditions(array, conditions) {
-  if (empty(array)) {
-    return [];
-  }
-
-  if (empty(conditions)) {
-    return [array];
-  }
-
-  const out = [];
-  let cur = [array[0]];
-  for (let i = 1; i < array.length; i++) {
-    const item = array[i];
-    const prev = array[i - 1];
-    let chunk = false;
-    for (const condition of conditions) {
-      if (condition(item, prev)) {
-        chunk = true;
-        break;
-      }
-    }
-    if (chunk) {
-      out.push(cur);
-      cur = [item];
-    } else {
-      cur.push(item);
-    }
-  }
-  out.push(cur);
-  return out;
-}
-
-export function chunkByProperties(array, properties) {
-  return chunkByConditions(
-    array,
-    properties.map((p) => (a, b) => {
-      if (a[p] instanceof Date && b[p] instanceof Date) return +a[p] !== +b[p];
-
-      if (a[p] !== b[p]) return true;
-
-      // Not sure if this line is still necessary with the specific check for
-      // d8tes a8ove, 8ut, uh, keeping it anyway, just in case....?
-      if (a[p] != b[p]) return true;
-
-      return false;
-    })
-  ).map((chunk) => ({
-    ...Object.fromEntries(properties.map((p) => [p, chunk[0][p]])),
-    chunk,
-  }));
-}
-
-export function chunkMultipleArrays(...args) {
-  const arrays = args.slice(0, -1);
-  const fn = args.at(-1);
-
-  if (arrays[0].length === 0) {
-    return [];
-  }
-
-  const newChunk = index => arrays.map(array => [array[index]]);
-  const results = [newChunk(0)];
-
-  for (let i = 1; i < arrays[0].length; i++) {
-    const current = results.at(-1);
-
-    const args = [];
-    for (let j = 0; j < arrays.length; j++) {
-      const item = arrays[j][i];
-      const previous = current[j].at(-1);
-      args.push(item, previous);
-    }
-
-    if (fn(...args)) {
-      results.push(newChunk(i));
-      continue;
-    }
-
-    for (let j = 0; j < arrays.length; j++) {
-      current[j].push(arrays[j][i]);
-    }
-  }
-
-  return results;
-}
-
-// Delicious function annotations, such as:
-//
-//   (*bound) soWeAreBackInTheMine
-//   (data *unfulfilled) generateShrekTwo
-//
-export function annotateFunction(fn, {
-  name: nameOrFunction = null,
-  description: newDescription,
-  trait: newTrait,
-}) {
-  let name;
-
-  if (typeof nameOrFunction === 'function') {
-    name = nameOrFunction.name;
-  } else if (typeof nameOrFunction === 'string') {
-    name = nameOrFunction;
-  }
-
-  name ??= fn.name ?? 'anonymous';
-
-  const match = name.match(/^ *(?<prefix>.*?) *\((?<description>.*)( #(?<trait>.*))?\) *(?<suffix>.*) *$/);
-
-  let prefix, suffix, description, trait;
-  if (match) {
-    ({prefix, suffix, description, trait} = match.groups);
-  }
-
-  prefix ??= '';
-  suffix ??= name;
-  description ??= '';
-  trait ??= '';
-
-  if (newDescription) {
-    if (description) {
-      description += '; ' + newDescription;
-    } else {
-      description = newDescription;
-    }
-  }
-
-  if (newTrait) {
-    if (trait) {
-      trait += ' #' + newTrait;
-    } else {
-      trait = '#' + newTrait;
-    }
-  }
-
-  let parenthesesPart;
-
-  if (description && trait) {
-    parenthesesPart = `${description} ${trait}`;
-  } else if (description || trait) {
-    parenthesesPart = description || trait;
-  } else {
-    parenthesesPart = '';
-  }
-
-  let finalName;
-
-  if (prefix && parenthesesPart) {
-    finalName = `${prefix} (${parenthesesPart}) ${suffix}`;
-  } else if (parenthesesPart) {
-    finalName = `(${parenthesesPart}) ${suffix}`;
-  } else {
-    finalName = suffix;
-  }
-
-  Object.defineProperty(fn, 'name', {value: finalName});
-}