Skip to content

Instantly share code, notes, and snippets.

@webbower
Last active November 11, 2024 22:39
Show Gist options
  • Save webbower/0de319bd09564d2d8c8c420ef85e007d to your computer and use it in GitHub Desktop.
Save webbower/0de319bd09564d2d8c8c420ef85e007d to your computer and use it in GitHub Desktop.
JS Utilities
/**
* Combinators
*
* @see https://en.wikipedia.org/wiki/SKI_combinator_calculus
* @see https://en.wikipedia.org/wiki/B,_C,_K,_W_system
* @see https://www.angelfire.com/tx4/cus/combinator/birds.html
* @see https://leanpub.com/combinators/read
* @see https://www.amazon.com/gp/product/B00A1P096Y
*/
/**
* I/Identity combinator
*
* Returns the value it was passed
*/
export const I = ident = x => x;
/**
* K/Constant combinator
*
* Takes 2 values, one at a time, and always returns the first one
*/
export const K = constant = x => y => x;
/**
* S/Substitution combinator
*/
export const S = subs = (x, y, z) => x(z)(y(z));
/**
* C/Flip combinator
*
* Takes a function and 2 arguments and applies those arguments to the function in reverse order
*/
export const C = flip = f => x => y => f(y, x);
/**
* W/Duplicate combinator
*/
export const W = duplicate = (x, y) => x(y)(y);
// Loop function inspired by Lisp lop macro
// @see https://lispcookbook.github.io/cl-cookbook/iteration.html
const isStepLoop = config => false;
const isIterableLoop = config => false;
const isCountedLoop = config => false;
const loop = (config, onIter = x => x) => {
const {
// Loop from `from` to `to` by `step`
from,
to,
step = 1,
// Loop through an iterable
through,
// Loop x number of times
times,
} = config;
if (isStepLoop(config)) {
} else if (isIterableLoop)) {
} else if (isCountedLoop(config)) {
} else {
throw new Error('No valid config detected');
}
}
export const using = (...values) => {
const fn = values.pop();
return typeof fn !== 'function' ? (fn) => fn(...values) : fn(...values);
}
const not = (fn) => x => !fn(x);
const isValidNumber = x => typeof x === 'number' && !Number.isNaN(x);
// Loop function inspired by Lisp loop macro
const isRangeLoop = ({ from, to, step = 1 }) => [from, to, step].map(Number).every(not(Number.isNaN));
const isIterableLoop = ({ over, times }) => times == null && over != null && typeof over[Symbol.iterator] === 'function';
const isCountedLoop = ({ times }) => using(Number(times), (t) => !Number.isNaN(t) && t > 0);
// const updateCountState = (_, { count, ...rest }) => ({ ...rest, count: count + 1 });
const doIterableLoop = ({ over, forEach, collect }) => {
const loopArgs = [];
const len = over.length;
const collected = new Array(over.length);
for (let i = 0; i < len; i++) {
loopArgs.length = 0;
loopArgs[0] = i;
loopArgs.unshift(over[i]);
// loopArgs.unshift(...over.slice(i * size, size));
// forEach(...loopArgs);
const result = forEach(...loopArgs);
if (collect) {
collected[i] = result;
}
}
if (collect) {
return collected;
}
};
const doCountedLoop = ({ times, over, forEach, collect }) => {
// TODO handle Number(iterSize) === NaN
// const size = Math.max(1, Number(iterSize));
const hasIterable = over && Array.isArray(over);
const loopArgs = [];
const collected = new Array(times);
for (let i = 0; i < times; i++) {
loopArgs.length = 0;
loopArgs[0] = i;
if (hasIterable) {
loopArgs.unshift(over[i]);
// loopArgs.unshift(...over.slice(i * size, size));
}
const result = forEach(...loopArgs);
if (collect) {
collected[i] = result;
}
}
if (collect) {
return collected;
}
};
export const range = (from, to) => Array.from({length: to}, (_, i) => i + from);
/**
*
* @param {number} from
* @param {number} to
* @returns
*/
const createNumberRange = (from, to) => {
const length = Math.abs(from >= to ? from - to : to - from);
return Array.from({ length }, (_, i) => i + from);
};
/**
*
* @param {string} from
* @param {string} to
* @returns
*/
const createLetterRange = (from, to) => {
const fromVal = from.charCodeAt(0),
toVal = to.charCodeAt(0);
const length = Math.abs(fromVal >= toVal ? fromVal - toVal : toVal - fromVal);
return Array.from({ length }, (_, i) => String.fromCharCode(i + fromVal));
}
const doRangeLoop = ({ from, to, step, forEach, collect }) => {
const over = new Array(to - from)
};
/**
* Loop config object
*
* @template {Object} [LoopState={ count: number }] The loop state for the loop. Will at least include the iteration count.
* @template LoopData=number The loop data. It's a number by default but can be overridden by the type of {@link LoopConfig.over}
* @typedef {Object} LoopConfig
* //// Loop type properties
* // Range loop
* @prop {number | string} from The starting number or string character for a range loop
* @prop {number | string} to The ending number or string character for a range loop
* @prop {number} step The step size for a range loop
* // Counter loop
* @prop {number} times Define the number of times to do the counted loop
* // Iterable loop
* @prop {Iterable<LoopData>} over An iterable to loop over the contents
* //// Loop behavior properties
* @prop {(loopItem: unknown, loopState: LoopState) => void | unknown} forEach The loop iteration handler
* @prop {boolean} collect If `true`, will return the result of forEach handler applied to each list entry in a final array
* @prop {(loopItem: unknown, loopState: LoopState) => boolean} skipStep Called on each loop. Will skip the handler when this returns true
* @prop {number} iterSize The number of items to process per iteration, e.g. `iterSize = 2` means the loop will iterate 2 over items at a time
* @prop {(loopItem: unknown, loopState: LoopState) => boolean} until A function that will be passed the loop state and, when it returns `true`, will terminate the loop
* @prop {LoopState} initialState The initial loop state. Akin to the first section in a `for` loop
* @prop {(loopItem: unknown, loopState: LoopState) => LoopState} updateState Called on each loop. Will skip the handler when this returns true
*/
/**
*
* @param {LoopConfig} config The configuration object for the loop
* @returns {void | unknown[]} By default, `loop()` will not return a value. When {@link LoopConfig.collect} is `true`,
* `loop()` will return an array of a size equal to the number of times the loop ran (taking
* {@link LoopConfig.skipStep} into account) with the result of each loop item passed through the
* {@link LoopConfig.forEach} handler
*/
export const loop = config => {
const {
// Loop from `from` to `to` by `step`
from,
to,
step = from < to ? 1 : -1,
// Loop over an iterable
over,
// Loop x number of times
times,
// handlers
forEach,
// behavior settings
/** If `true`, will return the result of forEach handler applied to each list entry */
collect = false,
/** Called on each loop. Will skip the handler when this returns true */
skipStep = () => false,
/** Specify number of items to process per iteration */
iterSize = 1,
// loop state
initialState,
updateState = (loopItem, { count }) => ({ count: count + 1 }),
} = config;
if (typeof forEach !== 'function') {
throw new TypeError('a `forEach` function must be provided');
}
if (isIterableLoop(config)) {
return doIterableLoop(config);
}
if (isCountedLoop(config)) {
return doCountedLoop(config);
}
if (isRangeLoop(config)) {
return doRangeLoop(config);
}
throw new Error('No valid config detected');
};
loop2
import { assertEquals } from 'https://deno.land/[email protected]/assert/assert_equals.ts';
import { describe, it } from 'https://deno.land/[email protected]/testing/bdd.ts';
import { assertSpyCall, assertSpyCalls, spy } from 'https://deno.land/[email protected]/testing/mock.ts';
import { loop } from './loop.js';
describe('loop(): iterable variant', () => {
// TODO add test to verify throw if `forEach` is not provided
it('should call the `forEach` function expected times with expected args when given an iterable loop with iterable `over` with no return value', () => {
const forEachSpy = spy((item, _i) => item);
const over = [1,2,3,4,5];
const result = loop({ over, forEach: forEachSpy });
assertEquals(result, undefined);
assertSpyCalls(forEachSpy, over.length);
for (let index = 0; index < over.length; index++) {
assertSpyCall(forEachSpy, index, {
args: [over[index], index],
returned: over[index],
});
}
});
// it('should call the `forEach` function expected times with expected args when given an iterable loop with iterable `over` with no return value', () => {
it('should call the `forEach` function expected times with expected args when given an iterable loop with iterable `over` and returning the return values of `forEach` when `collect` is true', () => {
const forEachSpy = spy((item, i) => [String(i), item]);
const over = [1,2,3,4,5];
const expected = Object.entries(over);
const result = loop({ over, forEach: forEachSpy, collect: true });
assertEquals(result, expected);
assertSpyCalls(forEachSpy, over.length);
for (let index = 0; index < over.length; index++) {
assertSpyCall(forEachSpy, index, {
args: [over[index], index],
returned: [String(index), over[index]],
});
}
});
});
describe('loop(): counted variant', () => {
// TODO add test to verify throw if `forEach` is not provided
it('should call the `forEach` function expected times with expected args when given a counted loop with no return value', () => {
const forEachSpy = spy(i => i);
const times = 5;
const result = loop({ times, forEach: forEachSpy });
assertEquals(result, undefined);
assertSpyCalls(forEachSpy, times);
for (let index = 0; index < times; index++) {
assertSpyCall(forEachSpy, index, {
args: [index],
returned: index,
});
}
});
it('should call the `forEach` function equal to the `times` config, receiving items from `over` until they run out and undefined for the rest when given an iterable `over` with fewer items than `times` config', () => {
const forEachSpy = spy((item, _) => item);
const times = 5;
const over = ['a', 'b', 'c'];
const result = loop({ times, over, forEach: forEachSpy });
assertEquals(result, undefined);
assertSpyCalls(forEachSpy, times);
for (let index = 0; index < times; index++) {
assertSpyCall(forEachSpy, index, {
args: [over[index], index],
returned: over[index],
});
}
});
it('should call the `forEach` function equal to the `times` config, receiving items from `over` when given an iterable `over` with more items than `times` config', () => {
const forEachSpy = spy((item, _) => item);
const times = 5;
const over = ['a', 'b', 'c', 'd', 'e', 'f'];
const result = loop({ times, over, forEach: forEachSpy });
assertEquals(result, undefined);
assertSpyCalls(forEachSpy, times);
for (let index = 0; index < times; index++) {
assertSpyCall(forEachSpy, index, {
args: [over[index], index],
returned: over[index],
});
}
});
it('should call the `forEach` function equal to the `times` config, receiving items from `over` when given an iterable `over` and returning the return values of `forEach` when `collect` is true', () => {
const forEachSpy = spy((item, _) => item.toUpperCase());
const times = 5;
const over = ['a', 'b', 'c', 'd', 'e'];
const result = loop({ times, over, forEach: forEachSpy, collect: true });
assertEquals(result, over.map(c => c.toUpperCase()));
assertSpyCalls(forEachSpy, times);
for (let index = 0; index < times; index++) {
assertSpyCall(forEachSpy, index, {
args: [over[index], index],
returned: over[index].toUpperCase(),
});
}
});
// it('should call the `forEach` function expected times with expected args ignoring `iterSize` override in this case when given a counted loop and an `iterSize` override with no return value', () => {
// const forEachSpy = spy(i => i);
// const times = 5;
// const result = loop({ times, iterSize: 2, forEach: forEachSpy });
// assertEquals(result, undefined);
// assertSpyCalls(forEachSpy, times);
// for (let index = 0; index < times; index++) {
// assertSpyCall(forEachSpy, index, {
// args: [index],
// returned: index,
// });
// }
// });
// it('should call the `forEach` function equal to the `times` config, receiving items from `over` until they run out and undefined for the rest when given an iterable `over` with fewer items than `times` config', () => {
// const forEachSpy = spy((item1, _item2, _i) => item1);
// const times = 5;
// const over = ['a', 'b', 'c'];
// const result = loop({ times, over, iterSize: 2, forEach: forEachSpy });
// assertEquals(result, undefined);
// assertSpyCalls(forEachSpy, times);
// for (let index = 0; index < times; index++) {
// assertSpyCall(forEachSpy, index, {
// args: [over[index], over[index + 1], index],
// returned: over[index],
// });
// }
// });
});
describe('loop(): range variant', () => {
// TODO add test to verify throw if `forEach` is not provided
it('should call the `forEach` function expected times with expected args when given a range loop (default `step = 1`) with no return value', () => {
const forEachSpy = spy(([item, i]) => [String(i), item]);
const from = 0, to = 5;
const result = loop({ from, to, forEach: forEachSpy });
assertEquals(result, undefined);
assertSpyCalls(forEachSpy, to);
for (let index = 0; index < to; index++) {
assertSpyCall(forEachSpy, index, {
args: [index, index],
returned: [String(index), index],
});
}
});
});
import { describe, sinon } from 'src/utils/testing/unit';
import { returning } from './returning';
describe('returning()', async assert => {
const data = { foo: 'bar' };
const spy = sinon.spy();
const result = returning(data, spy);
assert({
given: 'a value and a effectful function',
should: 'return the value and call the effectful function with the value arg',
actual: [result, spy.callCount, spy.firstCall.args],
expected: [data, 1, [data]],
});
});
/**
* Perform a side effect with a provided {@link value} and return that value
*
* Useful to produce a value, perform some kind of side effect with it (e.g. log it, apply it to
* something), and return it in one expression.
*
* @param value The value to provide to {@link effect} and return
* @param effect A side effect to perform that uses {@link value}
* @returns The {@link value} arg
*/
export const returning = <T>(value: T, effect: (value: T) => void): T => {
effect(value);
return value;
};
// Shorter
// export const returning = (x, fn) => (fn(x), x);
const until = (predicate, iterator, initialValue) => {
let value = initialValue;
while(!predicate(value)) {
value = iterator(value);
}
return value;
}
/**
* Perform logic referencing dynamically fetched data only once each
*
* In some cases, your code needs to use a value stored in a data structure or is expensive to fetch/generate that requires
* verbose code and you may need to reference the value multiple times (e.g. checking if the value is set). Normally, you
* would either need to perform the lookup multiple times or store the value in a short-lived variable to only get it once.
*
* <code>
* const value = sessionStorage.getItem('foo') ? sessionStorage.getItem('foo') : 'fallback';
* const tmp = expensiveGetter();
* const value2 = tmp ? tmp : 'fallback'
* </code>
*
* `using()` allows you to shorten the code by only needing reference the generated value(s) once and limits the lifecycle
* of temporary variable(s) to only the scope of the operation:
*
* <code>
* const value = using(sessionStorage.getItem('foo'), foo => foo ? foo : 'fallback');
* const value2 = using(expensiveGetter(), val => val ? val : 'fallback';)
* </code>
*
* `using()` takes any number of arguments. The last argument must be a function that will receive all the preceding arguments
* in `using()` as its own function arguments.
*
* <code>
* const total = using(1, 2, 3, 4, (a, b, c, d) => a + b + c + d); // 10
*/
const using = (...values) => {
if (values.length === 0) {
return null;
}
const fn = values.pop();
if (typeof fn !== 'function') {
throw new TypeError('The last argument of `using` must be a function.');
}
return fn(...values);
}
// TODO Combine versions. This one allow for a curried impl
// export const using = (...values) => {
// const fn = values.pop();
// return typeof fn !== 'function' ? (fn) => fn(...values) : fn(...values);
// }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment