UNPKG

js-utl

Version:

A collection of JS utility functions to be used across several applications or libraries.

589 lines (532 loc) 28.5 kB
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* * Copyright (c) 2022 Anton Bagdatyev (Tonix) * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ /** * Utility functions for functional programming. */ import { isObjectEmpty, isArray } from "./core"; import { arrayOrArrayLike } from "./array"; /** * @type {string} */ const JSUtlCurryPlaceholderProp = "JSUtlCurryPlaceholderPropRPecoyYmCYqZ2lE"; /** * Curry function placeholder. * * @type {Object} */ export const _ = { [JSUtlCurryPlaceholderProp]: true }; /** * @type {string} */ const JSUtlProceedCallingFnProp = "JSUtlProceedCallingFnPropRBmGaAgOCgftF9t"; /** * Return value of "onFnCall" to call the curried function and return its value. * * @type {Object} */ export const proceedCallingFn = { [JSUtlProceedCallingFnProp]: true }; /** * Curries a function. * * @see https://medium.com/@kj_huang/implementation-of-lodash-curry-function-8b1024d71e3b * * @param {Function} fn A function to curry in order to return the curried version of the function. * @param {Object} [obj] An optional object with further properties to tweak the currying behaviour * and execute code while collecting the arguments of the curried function. * @param {number|undefined} [obj.arity] The arity of the function, i.e. its number of arguments. * If omitted, "fn.length" will be used. * @param {Function|undefined} [obj.onEffectiveArgAdded] An optional callback to execute whenever a new effective argument * (not a placeholder) is added to the curried function. * The callback will receive an object with the following properties as argument: * * - addedArg: The effective argument added; * - args: An array with all the previously arguments collected so far * without considering "addedArg"; * - fn: The function "fn"; * - curriedFn: The current curried function; * * The return value of the function is ignored. * @param {Function|undefined} [obj.onPlaceholder] An optional callback to execute whenever a new placeholder is added to the curried function. * The callback will receive an object with the following properties as argument: * * - args: An array with all the previously arguments collected so far * without considering "addedArg"; * - fn: The function "fn"; * - curriedFn: The current curried function; * * The return value of the function is ignored. * @param {Function|undefined} [obj.onFnCall] An optional callback to execute just before calling the "fn" function * (i.e. when the "fn" function is ready to be called and all its arguments have been collected). * * The callback will receive an object with the following properties as argument: * * - args: The array of the effective arguments of the "fn" function; * - fn: The function "fn" which was initially passed to "curry"; * - curriedFn: The current curried function; * * This way, the code of the callback may decide what to do and may even call the function on its own * and prevent the call from the caller side (i.e. within the "curry" function). * * The callback must explicitly tell "curry" to call the function by returing "proceedCallingFn". * If the callback returns any other value, then "fn" will not be called in "curry" and the return value * of this callback will be returned. * @param {Function|undefined} [obj.onCurriedFnFirstCall] An optional callback to execute only the first time when the first curried function returned by "curry" * is invoked for the very first time with the very first argument or arguments. * * The callback will receive an object with the following properties as argument: * * - addedArgs: The arguments provided by the caller; * - fn: The "fn" function; * - curriedFn: The current curried function; * * @param {Function|undefined} [obj.onNewCurriedFn] An optional callback to execute initially and each time a new curried function is going to be returned. * * The callback will an object with the following properties as argument: * * - curriedFn: The current curried function (same as "newCurriedFn" when this callback is invoked * for the very first time); * - newCurriedFn: The new curried function (will be the same as "curriedFn" when this callback is invoked * for the very first time); * * * @return {Function} The curried version of the function. */ export const curry = (fn, { arity = void 0, onEffectiveArgAdded = void 0, onPlaceholder = void 0, onFnCall = void 0, onCurriedFnFirstCall = void 0, onNewCurriedFn = void 0 } = {}) => { let curriedFnFirstCall = true; const expectedNumberOfArgs = typeof arity !== "undefined" ? arity : fn.length; const nextParameterIndex = 0; const placeholdersIndices = []; const curriedInner = (expectedNumberOfArgs, nextParameterIndex, placeholdersIndices, ...args) => function curriedFn(...addedArgs) { let newExpectedNumberOfArgs = expectedNumberOfArgs; let newNextParameterIndex = nextParameterIndex; let newPlaceholdersIndices = null; let argsRequiredChange = false; curriedFnFirstCall && (onCurriedFnFirstCall && onCurriedFnFirstCall({ addedArgs, fn, curriedFn }) || true) && (curriedFnFirstCall = false); let numberOfConsumablePlaceholders = placeholdersIndices.length; for (const addedArg of addedArgs) { !argsRequiredChange && (args = [...args]); argsRequiredChange = true; // "addedArg" may be either a placeholder or an effective argument. const isPlaceholder = addedArg ? addedArg[JSUtlCurryPlaceholderProp] === _[JSUtlCurryPlaceholderProp] : false; if (numberOfConsumablePlaceholders > 0 && !isPlaceholder) { // Argument is an effective argument consuming a previously set placeholder. onEffectiveArgAdded && onEffectiveArgAdded({ addedArg, args, fn, curriedFn }); let argIndex; if (newPlaceholdersIndices === null) { const [firstIndex, ...rest] = placeholdersIndices; argIndex = firstIndex; newPlaceholdersIndices = rest; } else { argIndex = newPlaceholdersIndices.shift(); } args[argIndex] = addedArg; numberOfConsumablePlaceholders--; newExpectedNumberOfArgs--; } else { if (isPlaceholder) { // Argument is a new placeholder. if (newPlaceholdersIndices === null) { newPlaceholdersIndices = placeholdersIndices.concat(newNextParameterIndex); } else { newPlaceholdersIndices.push(newNextParameterIndex); } onPlaceholder && onPlaceholder({ args, fn, curriedFn }); } else { // Argument is an effective argument. newExpectedNumberOfArgs--; onEffectiveArgAdded && onEffectiveArgAdded({ addedArg, args, fn, curriedFn }); } args[newNextParameterIndex] = addedArg; newNextParameterIndex++; } } // eslint-disable-next-line @typescript-eslint/no-use-before-define return curried.call(null, curriedFn, newExpectedNumberOfArgs, newNextParameterIndex, newPlaceholdersIndices === null ? [...placeholdersIndices] : newPlaceholdersIndices, ...args); }; const curried = (curriedFn, expectedNumberOfArgs, nextParameterIndex, placeholdersIndices, ...args) => { if (expectedNumberOfArgs <= 0) { if (onFnCall) { const shouldCallCurriedFn = onFnCall({ args, fn, curriedFn }); if (shouldCallCurriedFn && shouldCallCurriedFn[JSUtlProceedCallingFnProp] === proceedCallingFn[JSUtlProceedCallingFnProp]) { return fn(...args); } else { return shouldCallCurriedFn; } } else { return fn(...args); } } else { const newCurried = curriedInner(expectedNumberOfArgs, nextParameterIndex, placeholdersIndices, ...args); onNewCurriedFn && onNewCurriedFn({ curriedFn, newCurriedFn: newCurried }); return newCurried; } }; const newCurried = curriedInner(expectedNumberOfArgs, nextParameterIndex, placeholdersIndices); onNewCurriedFn && onNewCurriedFn({ curriedFn: newCurried, newCurriedFn: newCurried }); return newCurried; }; /** * Curries a POJO function, i.e. a function having only a single POJO (Plain Old JavaScript Object) * as parameter (a function with named arguments). * * Each object passed to the returned curried function will be merged with the final object * to pass to the function "fn" when finally calling it. * * @param {Function} fn A function to curry in order to return the curried version of the function. * @param {Object} [obj] An optional object with further properties to tweak the currying behaviour * and execute code while collecting the POJO objects which will construct * the final POJO object to pass to the curried function after plugging the curried POJO function. * @param {string} [obj.plugPropertyName] The name of the property which instructs that it's time to call the function. * @param {boolean} [obj.plugPropertyMustBeTruthy] By default, it is enough that the property with "obj.plugPropertyName" name * is set on the object passed to the curried function to instruct it to call * "fn". * If this property is set to "true", the property with "plugPropertyName" name * will also have to be truthy. * @param {Function} [obj.onPOJOArgMerged] An optional callback to execute right after merging the next POJO argument with the so far accumulated POJO object. * The callback will an object with the following properties as argument: * * - POJOArg: The given POJO argument; * - POJO: the so far merged POJO object merged with the given "POJOArg"; * - fn: The "fn" function; * - curriedFn: The current curried function; * * @param {Function} [obj.onFnCall] An optional callback to execute just before calling the "fn" function * (i.e. when the "fn" function is ready to be called and has been plugged and all * of its POJO object argument properties have been collected). * * The callback will receive an object with the following properties as argument: * * - POJO: The callback will receive the function "fn" POJO argument (an object) as its first argument; * - fn: The "fn" function; * - curriedFn: The current curried function; * * This way, the code of the callback may decide what to do and may even call the function on its own * and prevent the call from the caller side (i.e. within the "POJOCurry" function). * * The callback must explicitly tell "POJOCurry" to call the function by returing "proceedCallingFn" * If the callback returns any other value, then "fn" will not be called in "POJOCurry" and the return value * of this callback will be returned. * @param {Function|undefined} [obj.onCurriedFnFirstCall] An optional callback to execute only the first time when the first curried function returned by "curry" * is invoked for the very first time with the very first POJO argument. * * The callback will receive an object with the following properties as argument: * * - POJOArg: The given POJO argument yet to merge with the internal final POJO; * - fn: The "fn" function; * - curriedFn: The current curried function; * * @param {Function|undefined} [obj.onNewCurriedFn] An optional callback to execute initially and each time a new curried function is going to be returned. * * The callback will receive an object with the following properties as argument: * * - curriedFn: The current curried function (same as "newCurriedFn" when this callback is invoked * for the very first time); * - newCurriedFn: The new curried function (will be the same as "curriedFn" when this callback is invoked * for the very first time); * * @return {Function} The curried version of the function. */ export const POJOCurry = (fn, { plugPropertyName = "plugCurried", plugPropertyMustBeTruthy = false, onPOJOArgMerged = void 0, onFnCall = void 0, onCurriedFnFirstCall = void 0, onNewCurriedFn = void 0 } = {}) => { const POJO = {}; let curriedFnFirstCall = true; const curriedInner = accPOJO => function curriedFn(POJOArg) { curriedFnFirstCall && (onCurriedFnFirstCall && onCurriedFnFirstCall({ POJOArg, fn, curriedFn }) || true) && (curriedFnFirstCall = false); let POJO = Object.assign({}, accPOJO, POJOArg || {}); if (Object.prototype.hasOwnProperty.call(POJO, plugPropertyName) && (!plugPropertyMustBeTruthy || POJO[plugPropertyName])) { const { [plugPropertyName]: POJOArgIgnoredProperty } = POJOArg, rest = _objectWithoutProperties(POJOArg, [plugPropertyName].map(_toPropertyKey)); const { [plugPropertyName]: POJOIgnoredProperty } = POJO, POJORest = _objectWithoutProperties(POJO, [plugPropertyName].map(_toPropertyKey)); POJO = POJORest; if (!isObjectEmpty(rest)) { onPOJOArgMerged && onPOJOArgMerged({ POJOArg: rest, POJO, fn, curriedFn }); } let shouldCallCurriedFn = true; let ret = void 0; if (onFnCall) { ret = onFnCall({ POJO, fn, curriedFn }); shouldCallCurriedFn = ret ? ret[JSUtlProceedCallingFnProp] === proceedCallingFn[JSUtlProceedCallingFnProp] : false; } if (shouldCallCurriedFn) { return fn(POJO); } else { return ret; } } else { onPOJOArgMerged && onPOJOArgMerged({ POJOArg, POJO, fn, curriedFn }); const newCurried = curriedInner(POJO); onNewCurriedFn && onNewCurriedFn({ curriedFn, newCurriedFn: newCurried }); return newCurried; } }; const newCurried = curriedInner(POJO); onNewCurriedFn && onNewCurriedFn({ curriedFn: newCurried, newCurriedFn: newCurried }); return newCurried; }; /** * A utility function which composes functions or higher-order functions. * * @param {...Function|...Function[]} fns A list of functions or higher-order functions or arrays of functions * (arrays will be flattened) to compose. * @return {Function} A function composed of all the functions or higher-order functions * used for composition. */ export const compose = (...fns) => (...args) => { let outerArgs = args; let hoFn = void 0; fns = fns.flat(1); for (let i = fns.length - 1; i >= 0; i--) { const fn = fns[i]; hoFn = fn(...outerArgs); outerArgs = [hoFn]; } return hoFn; }; /** * A utility function which pipes functions. * * @param {...Function|...Function[]} fns A list of functions or arrays of functions (arrays will be flattened) * to pipe. * @return {Function} A function representing the pipe. */ export const pipe = (...fns) => (...args) => { fns = fns.flat(1); return fns.length ? fns.reduce((arg, fn) => [fn(...arg)], args)[0] : void 0; }; /** * Returns a function which lets picking the properties of an object. * * @param {...string|...number} props The properties to pick. * @return {Function} A function which if called picks the "props" properties from its argument object * and returns a new object with the picked properties. */ export const pick = (...props) => o => props.reduce((a, e) => _objectSpread(_objectSpread({}, a), {}, { [e]: o[e] }), {}); /** * Lifts two functions using a binary function which takes their results as arguments. * * @param {Function} binaryFn A binary function (i.e. a function which takes two arguments). * @return {Function} A higher-order function which has to be called with the first function as argument ("firstFn") * and returns another higher-order function which has to be called with the second function as argument ("secondFn"). * Then, the returned function will take the parameters to pass to the two functions ("firstFn" and "secondFn") * and return the result of calling "binaryFn" with the result of those functions given as parameters. */ export const liftBinaryFn = binaryFn => firstFn => secondFn => (...params) => binaryFn(firstFn(...params), secondFn(...params)); /** * Applies an array of functions to a list of values. * * @param {Function[]} fns An array of functions. * @return {Function} A function which if called with a list of values, will pass the list to each function of "fns" * and return an array with the values after applying each of the original "fns" to its parameters. */ export const juxt = fns => (...values) => fns.map(fn => fn(...values)); /** * Converges a multi-arg function. * * @param {Function} multiArgFn A multi-arg function. * @param {Function[]} fns An array of functions to converge. * Each function will receive the parameter passed to the function returned * by this higher-order function (i.e. "params"). * @return {Function} A function which, if called, will pass its arguments to each of the functions in "fns" * and pass each result of those functions to the multi-arg function "multiArgFn", * returning its result. */ export const converge = (multiArgFn, fns) => (...params) => multiArgFn(...fns.map(fn => fn(...params))); /** * Executes a callback if a POJO object has a property. * * @param {Object} POJO A POJO object. * @return {(prop: string|number) => (fn: Function) => *} A function which receives the property * and returns and returns a function receiving the callback * function ("fn") to execute. */ export const execIfPOJOHas = POJO => prop => fn => Object.prototype.hasOwnProperty.call(POJO, prop) && fn(POJO[prop], POJO); /** * Executes a callback with the existent properties of an object. * * @param {...string|...number} props The properties to check for existence. * @return {(o: Object) => (fn: (existentProps: string[]|number[]) => *) => *} A function which receives the POJO object for which to check for the given properties * and returns a function receiving a callback which will receive the existent properties * of the object as parameter. */ export const execWithExistentProps = (...props) => o => fn => fn(props.filter(prop => Object.prototype.hasOwnProperty.call(o, prop))); /** * Loops through the values of a generator and returns an array with its mapped * values mapped with the given callback. * * @param {GeneratorFunction} gen A generator function. * @return {(args: ...*) => (fn: (val: *) => *) => Array} A function which takes the arguments for the generator and returns * another function which takes the callback to use to map each * value of the generator returning an array with all the mapped values of the generator. */ export const forGen = gen => (...args) => fn => { const generator = gen(...args); const arr = []; for (const value of generator) { const res = fn(value); arr.push(res); } return arr; }; /** * Flattens an array (its first dimension, at most). * * @param {Array} arr An array. * @return {Array} A new flattened array. */ export const flatten = arr => Array.prototype.concat.apply([], arr); /** * Flattens an array with any dimension. * * @param {Array} arr An array. * @param {number} dimension The flattening dimension (defaults to 1, but can be greater, even "Infinity"). * @return {Array} The flattened array. */ export const flattenDeep = (arr, dimension = 1) => { return dimension > 0 ? arr.reduce((acc, val) => acc.concat(isArray(val) ? flattenDeep(val, dimension - 1) : val), []) : arrayOrArrayLike(arr); }; /** * Computes the cartesian product of the given sets. * * @param {...Array} sets The sets to use to compute the cartesian product. * @return {Array} The cartesian product of the given sets. */ export const cartesianProduct = (...sets) => sets.reduce((acc, set) => flatten(acc.map(x => set.map(y => [...x, y]))), [[]]); /** * Identity function. * * @see https://en.wikipedia.org/wiki/Identity_function * * @param {*} value Any value. * @return {*} The same passed value. */ export const identityFn = value => value; /** * Identity function for multiple args returning an array of those args. * * @param {...*} args The args. * @return {Array} The same args returned in an array. */ export const identityArgsFn = (...args) => args; /** * @type {Function} */ const chainLink = (fn, next) => args => fn(args, next && ((...args) => next(args))); /** * A higher-order function to create a chain of functions following the Chain of Responsibility design pattern. * * @param {...Function|...Function[]} fns A list of functions or higher-order functions or arrays of functions * (arrays will be flattened) to chain. * @return {Function} A function representing the chain of the given functions which, if called, will return the result of the chain. * Each function will receive the next function as its last parameter. */ export const chain = (...fns) => (...args) => { fns = fns.flat(1); const chainFn = fns.reduceRight((nextChainLink, fn) => { return chainLink(fn, nextChainLink); }, void 0); return chainFn(args); }; //# sourceMappingURL=fn.js.map