@conjecture-dev/g-std
Version:
A collection of TypeScript utility functions for common programming tasks
374 lines • 11.9 kB
JavaScript
;
import { Failure, Success } from "./outcome";
export * from "./outcome";
// @g-check: all functions should have comments
/**
* Maps over an object's entries, transforming each value while preserving keys
* @param obj The source object to transform
* @param fn Mapping function that receives key, value, and index
* @returns New object with transformed values and same keys
*/
export function objectMap(obj, fn) {
return Object.fromEntries(Object.entries(obj).map(([key, value], index) => [
key,
fn(key, value, index),
]));
}
/**
* Filters and maps an array in one pass, using an Option-like pattern
* @param arr Source array to transform
* @param fn Function that returns ['some', value] to keep an item or ['none'] to filter it out
* @returns Array of transformed values that weren't filtered out
*/
export const arrayFilterMap = (arr, fn) => {
return arr.map(fn).filter((x) => x[0] === 'some').map((x) => x[1]);
};
/**
* Return true if the array is non-empty, and make typescript inference happy about you using myArray[0]
* @param arr the array
* @returns a type guard/boolean
*/
export const isNonEmpty = (arr) => {
return arr.length > 0;
};
/**
* Recursively filters an object or array's keys at any depth
* @param obj Source object or array to filter
* @param fn Predicate function that returns true to keep a key, false to remove it
* @returns New object with filtered keys
*/
export const deepObjectFilterK = (obj, fn) => {
if (Array.isArray(obj)) {
return obj.map(x => deepObjectFilterK(x, fn));
}
if (typeof obj !== 'object' || obj === null) {
return obj;
}
return Object.fromEntries(arrayFilterMap(Object.entries(obj), ([key, value]) => {
const fnd = fn(key);
if (!fnd)
return ['none'];
return ['some', [key, deepObjectFilterK(value, fn)]];
}));
};
/**
* Helper function for exhaustive type checking
* @param x Value that should be impossible to pass (type never)
* @throws Error if called, indicating a type checking failure
*/
export const assertNever = (x) => {
throw new Error(`Assert never ${JSON.stringify(x)}`);
};
/**
* Checks if all items in an iterable are truthy
* @param iterable Collection to check
* @returns true if all items are truthy, false otherwise
*/
export const all = (iterable) => {
for (const item of iterable) {
if (!item) {
return false;
}
}
return true;
};
/**
* Creates an array of tuples containing index and value pairs
* @param arr Source array
* @returns Array of [index, value] tuples
*/
export const enumerate = (arr) => {
return arr.map((x, ix) => [ix, x]);
};
/**
* Creates a canonical string representation of a value
* Sorts object keys and handles nested structures
* @param value Any value to canonicalize
* @returns Consistent string representation
*/
export const canonicalize = (value) => {
const aux = (value) => {
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
return value.map(aux);
}
else {
const sortedObj = {};
Object.keys(value).sort().forEach(key => {
sortedObj[key] = aux(value[key]);
});
return sortedObj;
}
}
return value;
};
return JSON.stringify(aux(value), null, 2);
};
/**
* Finds duplicate values in an array using canonical string representation
* @param arr Array to check for duplicates
* @returns Array of duplicate values
*/
export const findDuplicates = (arr) => {
return arr.filter((x, i) => arr.filter((y, j) => i < j && canonicalize(x) === canonicalize(y)).length > 0);
};
/**
* Template literal tag for type-safe string interpolation
* @param strs Template string array
* @param params Values to interpolate (must be string, number, or boolean)
* @returns Interpolated string
* @throws Error if any param is not a string, number, or boolean
*/
export const f = (strs, ...params) => {
// When calling f`Hello ${name}!`, strs is ["Hello ", "!"] and params is [name]
// Typescript ensures that strs is always one element longer than params
// We iterate over params, and for each param, we check if it is a string, number, or boolean
for (let param of params) {
if (typeof param === 'string' || typeof param === 'boolean' || typeof param === 'number')
continue;
else
throw new Error('Was not a string');
}
let str = "";
for (let i = 0; i < params.length; i++) {
str += strs[i] + params[i];
}
str += strs[params.length];
return str;
};
/**
* Creates an immutable (frozen) copy of an object or array
* @param x Object or array to freeze
* @returns Frozen copy of the input
*/
export const ice = (x) => {
return Object.freeze(x);
};
/**
* Type-safe version of Object.fromEntries
* @param entries Array of key-value pairs
* @returns Object constructed from entries with preserved types
*/
export const objectFromEntries = (entries) => {
return Object.fromEntries(entries);
};
/**
* Type-safe version of Object.entries
* @param obj Source object
* @returns Array of key-value pairs with preserved types
*/
export const objectEntries = (obj) => {
return Object.entries(obj);
};
/**
* Check if any item in the iterable is truthy
*
* @param iterable Collection to check
*/
export const any = (iterable) => {
for (const item of iterable) {
if (item) {
return true;
}
}
return false;
};
/**
* Transposes two arrays into a single array of tuples (like python's zip)
* @param a First array
* @param b Second array
* @returns Array of tuples
* @throws Error if arrays are of different lengths
*/
export const zip = (a, b) => {
if (a.length !== b.length) {
throw Error(`Called zip on arrays of different lenghts: ${a.length} and ${b.length}`);
}
return a.map((_, index) => [a[index], b[index]]);
};
/**
* Transposes two arrays into a single array of tuples (like python's zip) in a safe way
* @param a First array
* @param b Second array
* @returns Array of tuples if a and b have the same size. Otherwise, return the size of a and b
*/
export const zipOutcome = (a, b) => {
if (a.length !== b.length) {
return Failure({ a: a.length, b: b.length });
}
return Success(a.map((_, index) => [a[index], b[index]]));
};
/**
* Sorts an array (not in place)
* @param arr Array to sort
* @param compareFn Comparison function (returns -1 if a < b, 0 if a == b, 1 if a > b)
* @returns Sorted array
*/
export const sorted = (arr, compareFn) => {
return [...arr].sort(compareFn);
};
/**
* Same as Promise.withResolvers, but in 2024
* @returns Object containing a promise and its resolve/reject functions
*/
export const promiseWithResolvers = () => {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
if (resolve === undefined || reject === undefined) {
throw Error("This really shouldn't happen");
}
return { promise, resolve, reject };
};
/**
* Prevents a function from being called more than once within a specified time interval
*
* Rejected calls return Failure("throttled")
*
* @param options How many milliseconds to wait before allowing another call
* @param inputFn Function to throttle
* @returns Throttled function
*/
export const throuncedAsync = (inputFn, options) => {
let lastCallTimestamp = 0;
let lastPendingCall = null;
let waiting = [];
const doFlush = async (now, ...is) => {
lastCallTimestamp = now;
lastPendingCall = null;
const result = await inputFn(...is);
for (const resolve of waiting) {
resolve();
}
return Success(result);
};
const augmentedFn = async (...is) => {
const { promise, resolve, reject } = promiseWithResolvers();
const now = Date.now();
if (now - lastCallTimestamp < options.throunceMs) {
lastPendingCall = is;
setTimeout(async () => {
if (lastPendingCall === is) {
try {
resolve(await doFlush(now, ...is));
}
catch (error) {
reject(error);
}
}
else {
resolve(Failure("throttled"));
}
}, options.throunceMs);
return promise;
}
lastCallTimestamp = now;
lastPendingCall = null;
return doFlush(now, ...is);
};
const flush = () => {
if (lastPendingCall === null) {
return;
}
const { promise, resolve, reject: _ } = promiseWithResolvers();
waiting = [
...waiting,
resolve,
];
return promise;
};
return { fn: augmentedFn, flush };
};
/**
* Class that remembers a last value and can tell if a new value is different
*/
export class ChangeDetector {
lastValueCanonicalized = null;
hasChanged(newValue) {
const canonicalized = canonicalize(newValue);
if (this.lastValueCanonicalized === canonicalized) {
return false;
}
this.lastValueCanonicalized = canonicalized;
return true;
}
}
/**
* Flattens an array of arrays
* @param arr Array of arrays
* @returns Flattened array
*/
export const flatten = (arr) => {
return [...arr].flat();
};
/**
* Same as Array.prototype.findIndex, but returns null if the item is not found
* @param arr Array to search
* @param fn Function to test each element
* @returns Index of the first element that satisfies the function, or null if not found
*/
export const safeFindIndex = (arr, fn) => {
const index = arr.findIndex(fn);
return index === -1 ? null : index;
};
/**
* Checks if all items in an array are non-null
* @param arr Array to check
* @returns true if all items are non-null, false otherwise
*/
export const allNonNull = (arr) => {
return arr.every((item) => item !== null);
};
/**
* Returns all consecutive subarrays of length num
* @param arr Array to check
* @param num Length of the subarrays
* @returns All consecutive subarrays of length num
*/
export const consecutive = (arr, num, options) => {
const padding = options?.padding ?? false;
let result = [];
if (!padding) {
for (let i = 0; i < arr.length; i++) {
if (i + num <= arr.length) {
result.push(arr.slice(i, i + num));
}
}
}
else {
if (options?.default === undefined) {
throw Error("Default is required when padding is true");
}
const numPadding = num - Math.min(1, arr.length);
const before = Array(numPadding).fill(options.default);
const after = Array(numPadding).fill(options.default);
return consecutive([...before, ...arr, ...after], num, { ...options, padding: false });
}
return result;
};
/**
* Parses a string as an integer, returning null if the string is not a valid integer
* @param x String to parse
* @returns Parsed integer, or null if the string is not a valid integer
*/
export const safeParseInt = (x) => {
const parsed = parseInt(x);
if (x.replace(/^\+/, "") !== parsed.toString()) {
return null;
}
return parsed;
};
/**
* Clamps a number between a minimum and maximum value
* @param x The number to clamp
* @param min The minimum value
* @param max The maximum value
* @returns The clamped number
*/
export const clamp = (x, min, max) => {
return Math.min(Math.max(x, min), max);
};
//# sourceMappingURL=index.js.map