js-utl
Version:
A collection of JS utility functions to be used across several applications or libraries.
589 lines (532 loc) • 28.5 kB
JavaScript
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