Skip to content

Instantly share code, notes, and snippets.

@webbower
Last active March 14, 2024 17:47
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
// 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');
}
}
const isValidNumber = x => typeof x === 'number' && !Number.isNaN(x);
// Loop function inspired by Lisp loop macro
const isRangeLoop = ({ from, to, step }) => [from, to, step].every(isValidNumber);
const isIterableLoop = ({ over }) => over != null && typeof over[Symbol.iterator] === 'function';
const isCountedLoop = ({ times }) => isValidNumber(times) && times > 0;
const updateCountState = (_, { count, ...rest }) => ({ ...rest, count: count + 1 });
const doRangeLoop = ({ from, to, step, forEach, collect, skipStep, initialState, updateState }) => {};
const doIterableLoop = ({ over, forEach, collect, skipStep, initialState, updateState }) => {};
const doCountedLoop = ({ times, forEach, collect, skipStep, initialState, updateState }) => {};
/**
* 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
* @prop {number} from The starting number for a range loop
* @prop {number} to The ending number for a range loop
* @prop {number} step The step size for a range loop
* @prop {Iterable<LoopData>} over An iterable to loop over the contents of
* @prop {number} times Define the number of times to do the counted loop
* @prop {(loopItem: unknown, loopState: LoopState) => void | unknown} forEach The loop 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 {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
*/
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,
// loop state
initialState,
updateState = (loopItem, { count }) => ({ count: count + 1 }),
} = config;
if (isRangeLoop(config)) {
} else if (isIterableLoop(config)) {
} else if (isCountedLoop(config)) {
} else {
throw new Error('No valid config detected');
}
};
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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment