Last active
November 11, 2024 22:39
-
-
Save webbower/0de319bd09564d2d8c8c420ef85e007d to your computer and use it in GitHub Desktop.
JS Utilities
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | |
}); | |
} | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]], | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const until = (predicate, iterator, initialValue) => { | |
let value = initialValue; | |
while(!predicate(value)) { | |
value = iterator(value); | |
} | |
return value; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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