js-utl
Version:
A collection of JS utility functions to be used across several applications or libraries.
1,316 lines (1,243 loc) • 40.5 kB
JavaScript
/*
* 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.
*/
import { shallowEqual, objectPropEqual, is } from "./compare";
export { shallowEqual, objectPropEqual, is };
/**
* Core utility functions.
*/
/**
* Optional configuration with useful properties.
*
* @type {Object}
*/
export const config = {
uniqueIdPrefix: "",
elementUniqueIdPrefix: "",
checkNetworkURI: null,
};
/**
* Tests if an object is empty.
*
* @param {Object} obj The object to test.
* @return {boolean} "true" if the given object is empty (does not have own properties), "false" otherwise.
*/
export function isObjectEmpty(obj) {
for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
return false;
}
}
return true;
}
/**
* @type {string}
*/
const objPrototypeToString = Object.prototype.toString.call({});
/**
* Tests if a variable is an object.
*
* @param {*} obj The variable to test.
* @return {boolean} "true" if "obj" is indeed an object, "false" otherwise.
*/
export const isObject = function (obj) {
return objPrototypeToString === Object.prototype.toString.call(obj);
};
/**
* Tests if a variable is a plain object (i.e. "{}", an object literal).
*
* @param {*} obj The variable to test.
* @return {boolean} "true" if "obj" is a plain object, "false" otherwise.
*/
export const isPlainObject = obj => {
return (
obj !== null &&
typeof obj === "object" &&
obj.constructor === Object &&
isObject(obj)
);
};
/**
* Tests to see whether something is an array or not.
*
* @param {*} something A variable to check whether it is an array or not.
* @return {boolean} True if the parameter passed in is an array, false otherwise.
*/
export function isArray(something) {
return (
Object.prototype.toString.call(something) ===
Object.prototype.toString.call([])
);
}
/**
* Tests if the given value is callable.
*
* @param {*} v The value.
* @return {boolean} True if callable, false otherwise.
*/
export function isCallable(v) {
return typeof v === "function";
}
/**
* Tests if a variable is empty returning true for empty strings and empty arrays.
*
* @param {*} data The variable to test.
* @return {boolean} True if the variable is empty, false otherwise.
*/
export function isEmpty(data) {
return !data || data.length === 0;
}
/**
* Tests if a variable is empty or 0 ("0" string) returning true for empty strings,
* empty arrays, the "0" string and empty values.
*
* @param {*} data The variable to test.
* @return {boolean} True if the variable is empty or "0", false otherwise.
*/
export function isEmptyOr0(data) {
return !data || data === "0" || data.length === 0;
}
/**
* Returns a reference to the global object.
*
* @return {Window|global} The global object (this function is cross-platform aware).
*/
export function getGlobalObject() {
return typeof global !== "undefined" ? global : window;
}
/**
* @type {string}
*/
const JSUtlUniqueIdCounterProp = "JSUtlUniqueIdCounterLEzKKl87QCDxwVH";
/**
* Generates a unique ID which can be used as an "id" attribute.
*
* @param {string|undefined} [uniqueIdPrefix] Local unique ID prefix which overrides the prefix
* set on the "config" configuration object.
* @return {string} The unique ID.
*/
export function uniqueId(uniqueIdPrefix = void 0) {
const globalObject = getGlobalObject();
globalObject[JSUtlUniqueIdCounterProp] =
globalObject[JSUtlUniqueIdCounterProp] || 0;
globalObject[JSUtlUniqueIdCounterProp]++;
const uniqueIdCounter = globalObject[JSUtlUniqueIdCounterProp];
const uniqueId = (uniqueIdPrefix || config.uniqueIdPrefix) + uniqueIdCounter;
return uniqueId;
}
/**
* Gets a nested value of an object given an array of nested property names (keys).
*
* @param {Object} data JS POJO object.
* @param {Array} props Array of object nested keys.
* @return {*} The leaf value.
*/
export function nestedPropertyValue(data, props) {
let root = data;
for (let i = 0; i < props.length; i++) {
const prop = props[i];
root = root[prop];
}
return root;
}
/**
* Alias for "nestedPropertyValue".
*
* @alias
*/
export const getNestedPropertyValue = nestedPropertyValue;
/**
* Checks if a nested value of an object given an array of nested property names (keys) exists.
*
* @param {Object} data JS POJO object.
* @param {Array} props Array of object nested keys.
* @return {boolean} True if the nested key exists, false otherwise.
*/
export function hasNestedPropertyValue(data, props) {
if (!props.length) {
return false;
}
let root = data;
for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (!root[prop]) {
return false;
}
root = root[prop];
}
return true;
}
/**
* Sets a nested value of an object given an array of nested property names (keys).
*
* @param {Object} data JS POJO object.
* @param {Array} props Array of object nested keys.
* @param {*} value Leaf value.
* @return {undefined}
*/
export function setNestedPropertyValue(data, props, value) {
if (!props.length) {
return;
}
let root = data;
let prev = null;
for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (typeof root[prop] !== "object") {
root[prop] = {};
}
prev = root;
root = root[prop];
}
if (prev) {
prev[props[props.length - 1]] = value;
}
}
/**
* Sets a nested value on a nested map.
*
* @param {Map|WeakMap} map A map or weak map.
* @param {Array} keys Array of keys to traverse. Each key will lead to a nested map.
* @param {*} value The value to set at the inner key.
* @return {undefined}
*/
export const nestedMapSet = (map, keys, value) => {
let i = 0;
let current = map;
while (i < keys.length - 1) {
const key = keys[i];
const nested = current.get(key);
if (nested instanceof Map || nested instanceof WeakMap) {
current = nested;
} else {
const newMap = new Map();
current.set(key, newMap);
current = newMap;
}
i++;
}
current.set(keys[i], value);
};
/**
* Tests if a map has the given nested keys.
*
* @param {Map|WeakMap} map A map or weak map.
* @param {Array} keys Array of keys to check. Each key represents a nested map.
* @return {boolean} "true" if all the nested keys exist, false otherwise.
*/
export const nestedMapHas = (map, keys) => {
let current = map;
let i = 0;
const l = keys.length;
while (
(current instanceof Map || current instanceof WeakMap) &&
current.has(keys[i]) &&
i < l
) {
current = current.get(keys[i]);
i++;
}
return i == l;
};
/**
* Gets a value from a nested map.
*
* @param {Map|WeakMap} map A map or weak map.
* @param {Array} keys Array of keys. Each key represents a nested map.
* @return {*} The value of the map or "undefined" if there is no value for the given nested keys.
*/
export const nestedMapGet = (map, keys) => {
let current = map;
let i = 0;
const l = keys.length;
while (
(current instanceof Map || current instanceof WeakMap) &&
current.has(keys[i]) &&
i < l
) {
current = current.get(keys[i]);
i++;
}
return i == l ? current : void 0;
};
/**
* @type {Symbol}
*/
const treeMapSubtree = Symbol("treeMapSubtree");
/**
* Sets a nested value on a nested tree map.
*
* @param {Map|WeakMap} rootMap A map or weak map to use as the root.
* @param {Array} keys Array of keys to traverse. Each key will lead to a nested node of the tree map.
* @param {*} value The value to set at the inner nested key.
* @return {undefined}
*/
export const nestedTreeMapSet = (rootMap, keys, value) => {
let i = 0;
let current = rootMap;
const MapConstructor = rootMap instanceof WeakMap ? WeakMap : Map;
while (i < keys.length - 1) {
const key = keys[i];
const nested = current.get(key);
if (nested) {
current =
nested[treeMapSubtree] ||
(nested[treeMapSubtree] = new MapConstructor());
} else {
const newMap = new MapConstructor();
const node = {
[treeMapSubtree]: newMap,
value: void 0,
};
current.set(key, node);
current = newMap;
}
i++;
}
const key = keys[i];
!current.has(key)
? current.set(key, {
value,
})
: (current.get(key).value = value);
};
/**
* Tests if a tree map has the given nested keys.
*
* @param {Map|WeakMap} rootMap The root of the map or weak map.
* @param {Array} keys Array of keys to check. Each key represents a nested node of the tree map.
* @return {boolean} "true" if all the nested keys exist, false otherwise.
*/
export const nestedTreeMapHas = (rootMap, keys) => {
let current = rootMap;
let i = 0;
const l = keys.length;
while (
(current instanceof Map || current instanceof WeakMap) &&
current.has(keys[i]) &&
i < l
) {
current = current.get(keys[i])[treeMapSubtree];
i++;
}
return i == l;
};
/**
* Gets a value from a nested tree map.
*
* @param {Map|WeakMap} rootMap The root of the map or weak map.
* @param {Array} keys Array of keys. Each key represents a nested node of the tree map.
* @return {*} The value of the tree map or "undefined" if there is no value for the given nested keys.
*/
export const nestedTreeMapGet = (rootMap, keys) => {
let current = rootMap;
let i = 0;
const lastIndex = keys.length - 1;
while (
(current instanceof Map || current instanceof WeakMap) &&
current.has(keys[i]) &&
i < lastIndex
) {
current = current.get(keys[i])[treeMapSubtree];
i++;
}
if (i === lastIndex && current) {
const lastKey = keys[i];
if (current.has(lastKey)) {
const nested = current.get(lastKey);
return nested.value;
}
}
return void 0;
};
/**
* Yields values of an array mapping the yielded value.
*
* @generator
* @param {Array} items An array of items.
* @param {*} fn The function to call.
* The function will receive, in order the nth item,
* the index of the item in the array of items and the whole items array
* as parameters.
* @param {*} thisArg Optional this arg of the called function (defaults to undefined).
* @yields {*} The next yielded mapped item.
*/
export function* mapYield(items, fn, thisArg = void 0) {
items.map();
const boundFn = fn.bind(thisArg);
for (let i = 0; i < items.length; i++) {
yield boundFn(items[i], i, items);
}
}
/**
* Compares two arrays deeply.
*
* @param {Array} arr1 First array.
* @param {Array} arr2 Second array.
* @return {boolean} True if they are equal (same indexes and same values), false otherwise.
*/
export function deepArrayCompare(arr1, arr2) {
if (arr1.length != arr2.length) {
return false;
}
const toString = Object.prototype.toString;
const arrayToStringStr = toString.call([]);
for (let i = 0; i < arr1.length; i++) {
if (!(i in arr2)) {
return false;
} else if (isPlainObject(arr1[i])) {
if (
!isPlainObject(arr2[i]) ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
!deepObjectCompare(arr1[i], arr2[i])
) {
return false;
}
} else if (arrayToStringStr === toString.call(arr1[i])) {
if (
arrayToStringStr !== toString.call(arr2[i]) ||
!deepArrayCompare(arr1[i], arr2[i])
) {
return false;
}
} else {
if (arr1[i] !== arr2[i]) {
return false;
}
}
}
return true;
}
/**
* Compare two objects deeply.
*
* @param {Object} obj1 First object.
* @param {Object} obj2 Second object.
* @return {boolean} True if they are equal (same properties and same values), false otherwise.
*/
export function deepObjectCompare(obj1, obj2) {
const toString = Object.prototype.toString;
const arrayToStringStr = toString.call([]);
for (const property in obj1) {
if (!(property in obj2)) {
// `obj2[property]` does not contain the property of `obj1`.
return false;
} else if (isPlainObject(obj1[property])) {
// `obj1[property]` is an object.
if (
!isPlainObject(obj2[property]) ||
!deepObjectCompare(obj1[property], obj2[property])
) {
// `obj2[property]` is not an object or the branches are different.
return false;
}
} else if (arrayToStringStr === toString.call(obj1[property])) {
// `obj1[property]` is an array.
if (
arrayToStringStr !== toString.call(obj2[property]) ||
!deepArrayCompare(obj1[property], obj2[property])
) {
// `obj2[property]` is not an array or the two objects are different.
return false;
}
} else {
// `obj1[property]` is not an object and is not an array.
if (obj1[property] !== obj2[property]) {
return false;
}
}
}
// `obj1` equals `obj2` (has the same properties and the same values).
return true;
}
/**
* Nests the properties of an object using an array of props definitions and defaults.
* Returns the leaf.
*
* Example:
*
* ```
* const obj = {};
* let nest = [{a: {}}, {b: {}}, {c: {}}, {d: {}}, {e: {}}, {f: {}}, {g: {}}, {h: {}} ];
* const leaf = nestedObjectConstructValue(obj, nest);
* leaf.i = 'i';
* leaf.l = { m: "m" };
* JSON.stringify(obj); // {"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":"i","l":{"m":"m"}}}}}}}}}}
* nest = nest.concat([ {n: {}}, {o: {}}, {p: {}} ]);
* var pLeaf = nestedObjectConstructValue(obj, nest);
* pLeaf.q = 'q';
* nest = nest.concat([ {q: {}} ]);
* nestedObjectConstructValue(obj, nest); // 'q'
* ```
*
* @param {Object} root The root object.
* @param {Array} nestedPropsDef The nexted props definitions.
* @param {boolean} [isRootArrayIfRootFalsy] True if the root should be an array if the first argument "root" is falsy.
* @return {*} The leaf.
*/
export function nestedObjectConstructValue(
root,
nestedPropsDef,
isRootArrayIfRootFalsy
) {
root = root || (isRootArrayIfRootFalsy ? [] : {});
let leaf = root;
for (let i = 0; i < nestedPropsDef.length; i++) {
const propDef = nestedPropsDef[i];
const propKey = Object.keys(propDef)[0];
const propDefault = propDef[propKey];
leaf[propKey] = leaf[propKey] || propDefault;
leaf = leaf[propKey];
}
return leaf;
}
/**
* Clones an object deeply using the JSON API.
*
* @param {Object} obj The object to clone.
* @return {Object} The cloned object.
*/
export function cloneDeeplyJSON(obj) {
return JSON.parse(JSON.stringify(obj));
}
/**
* Tests whether the given value is a reference type or not.
*
* @param {*} value Any value which can be an object or a primitive type.
* @return {boolean} True if the given value is a reference type, false otherwise.
*/
export function isReferenceType(value) {
return new Object(value) === value;
}
/**
* Tests whether the given value is a primitive type or not.
*
* @param {*} value Any value which can be an object or a primitive type.
* @return {boolean} True if the given value is a primitive type, false otherwise.
*/
export function isPrimitiveType(value) {
return new Object(value) !== value;
}
/**
* Checks whether an object has a cyclic reference or not.
*
* @param {Object} obj The object to check for a cyclic reference.
* @return {boolean} True if the object has a cyclic reference, false otherwise.
*/
export function hasCyclicReference(obj) {
const stackSet = [];
let detected = false;
function detect(obj) {
if (detected) {
return;
}
if (typeof obj !== "object") {
return;
}
const indexOfObj = stackSet.indexOf(obj);
if (indexOfObj !== -1) {
detected = true;
return;
}
stackSet.push(obj);
for (const k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
detect(obj[k]);
}
}
stackSet.splice(indexOfObj, 1);
return;
}
detect(obj);
return detected;
}
/**
* Converts a type to its string representation.
*
* @param {*} type A variable.
* @return {string} The string representation of "type".
*/
export function typeToStr(type) {
return Object.prototype.toString.call(type);
}
/**
* Clones an object deeply and returns the clone.
*
* @param {Object} object The object to clone.
* @return {Object} The clone.
* @throws {Error} If a circular reference if even only one property is of an unkown type
* (this should never happen) or a circular reference is detected.
*/
export function cloneObjDeeply(object) {
const newObject = new object.constructor();
for (const prop in object) {
// If the property is defined on the prototype, ignore it. We don't want to assign it for each clone instance.
if (!Object.prototype.hasOwnProperty.call(object, prop)) {
continue;
}
const property = object[prop];
if (isPrimitiveType(property)) {
newObject[prop] = property;
} else if (isReferenceType(property)) {
if (!hasCyclicReference(property)) {
const clone = cloneObjDeeply(property);
newObject[prop] = clone;
} else {
throw new Error(
"Circular reference detected inside of property '" +
prop +
"' (" +
typeToStr(property) +
") in object (" +
typeToStr(object) +
")"
);
}
} else {
throw new Error(
"Oops! Unknown type for property '" +
prop +
"' (" +
typeToStr(property) +
") in object (" +
typeToStr(object) +
")"
);
}
}
return newObject;
}
/**
* Deep object extension implementation.
* Nothing is returned, but the destination object will be modified and merged with the source object
* so that properties of the source object which are objects will recursively merge with the corresponding
* destination property while the other properties with all the other types will replace the properties of the
* destination object.
* Note that this method should not be used for inheritance via the Prototypal Combination Inheritance pattern.
* Also, this method doesn't perform a deep object cloning, it just extends the destinationObject by adding properties
* it doesn't have in a deep way.
*
* @param {Object} destinationObject The destination object which will be modified and merged with the source object.
* @param {Object} sourceObject The source object which will be used to extend the destination object.
* @param {Object} [options] An object containing the options for the extension.
* The currently available options are:
*
* - extendsArrays (boolean: false): Whether or not to extend nested arrays too (defaults to false);
*
* @return {undefined}
*/
export function deepObjectExtend(
destinationObject,
sourceObject,
options = {}
) {
for (const property in sourceObject) {
if (sourceObject[property] && isPlainObject(sourceObject[property])) {
destinationObject[property] = destinationObject[property] || {};
deepObjectExtend(
destinationObject[property],
sourceObject[property],
options
);
} else if (
options.extendArrays &&
sourceObject[property] &&
isArray(sourceObject[property])
) {
destinationObject[property] = destinationObject[property] || [];
deepObjectExtend(
destinationObject[property],
sourceObject[property],
options
);
} else {
destinationObject[property] = sourceObject[property];
}
}
}
/**
* Deep object cloning extension implementation. If the source objects contain a property with a reference type, a clone object
* of the same type of that property will be created and then merged with the property object of the destination object.
*
* @param {Object} destinationObject The destination object which will be modified and merged with the source object.
* @param {...Object} sourceObject One or more objects which will be used to extend the destination object.
* @return {undefined}
*/
export function deepObjectCloningExtend(...args) {
const destinationObject = args[0];
let sourceObject;
for (let i = 1; args[i]; i++) {
sourceObject = args[i];
for (const property in sourceObject) {
if (sourceObject[property] && isPlainObject(sourceObject[property])) {
destinationObject[property] = destinationObject[property] || {};
deepObjectExtend(
destinationObject[property],
cloneObjDeeply(sourceObject[property])
);
} else {
destinationObject[property] = sourceObject[property];
}
}
}
}
/**
* Extends a destination object with the provided source objects.
*
* @param {Object} destinationObj The destination object.
* @param {...Object|Array} sourceObjects The source objects. If the last argument is an array containing one single truthy element,
* it will be treated as an options parameter and its single first truthy element will be treated as object
* containing the options for the extension.
* The currently available options are:
*
* - extendsArrays (boolean: false): Whether or not to extend nested arrays too (defaults to false);
*
* @return {Object} The destination object "destinationObj" given as parameter after extension.
*/
export function extend(destinationObj, ...sourceObjects) {
let options = {};
if (sourceObjects.length) {
const last = sourceObjects.pop();
if (isArray(last) && last.length === 1 && isPlainObject(last[0])) {
options = last[0];
} else {
sourceObjects.push(last);
}
}
for (const sourceObject of sourceObjects) {
deepObjectExtend(destinationObj, sourceObject, options);
}
return destinationObj;
}
/**
* Extends a destination object with the provided source objects.
*
* @param {Object} destinationObject The destination object.
* @param {...*} rest The source objects with the last parameter being an array of rules,
* each rule being a tuple array where the first element is an array of "ORed" property names (strings or numbers)
* or regexes matching property names for which the corresponding values should be decorated
* (or a single property name or regex matching a property name if the decoration should only happen on a single property),
* and where the second element is a callback to execute for each value which is a value of a property
* of a source object.
* The callback has the following signature:
*
* (value: *, prop: string|number, parent: Object) => *|undefined
*
* The callback will receive the final value after extension, its associated property and the parent object
* where that property is set with that value.
* Its returned value will be used as the final value of the property for that object in "destinationObject".
*
* If the last parameter is not an array of rules, it will be treated as the last source object to use for the extension
* (the "extend" function will be simply called under the hood without any decoration logic).
* @return {Object} The destination object "destinationObject" given as parameter after extension and, if the callback is given
* as the last parameter, after applying the given callback.
*/
export function extendDecorate(destinationObject, ...rest) {
const rules = rest[rest.length - 1];
if (isArray(rules)) {
rest.pop();
const sourceObjects = rest;
const initialRetValue = {};
const matchedRulesMap = new Map();
const callbacksMap = new Map();
const paths = [];
const mapKeys = (
destinationObject,
sourceObject,
currentStack,
currentPath
) => {
for (const key in sourceObject) {
currentStack.push({
destinationObject,
sourceObject,
property: key,
path: [...currentPath, key],
});
}
};
const matchRule = (rule, property) => {
if (rule instanceof RegExp && typeof property === "string") {
return property.match(rule);
} else {
return rule === property;
}
};
const matchArrayRule = (arrayRule, property) => {
for (const rule of arrayRule) {
if (matchRule(rule, property)) {
return true;
}
}
return false;
};
const ruleMatches = (rule, property) => {
if (isArray(rule)) {
return matchArrayRule(rule, property);
} else {
return matchRule(rule, property);
}
};
const matchRules = property => {
if (!matchedRulesMap.has(property)) {
const callbacks = [];
for (const [rule, callback] of rules) {
if (ruleMatches(rule, property)) {
callbacks.push(callback);
}
}
matchedRulesMap.set(property, callbacks);
}
return matchedRulesMap.get(property);
};
for (const sourceObject of sourceObjects) {
const currentStack = [];
const currentPath = [];
mapKeys(destinationObject, sourceObject, currentStack, currentPath);
while (currentStack.length) {
const {
destinationObject,
sourceObject,
property,
path: currentPath,
} = currentStack.pop();
if (sourceObject[property] && isPlainObject(sourceObject[property])) {
// "sourceObject[property]" is an object of class "Object".
destinationObject[property] = isPlainObject(
destinationObject[property]
)
? destinationObject[property]
: {};
mapKeys(
destinationObject[property],
sourceObject[property],
currentStack,
currentPath
);
} else {
// "sourceObject[property]" is not an object.
destinationObject[property] = sourceObject[property];
}
const callbacks = matchRules(property);
if (callbacks && callbacks.length) {
if (!nestedTreeMapHas(callbacksMap, currentPath)) {
paths.push([...currentPath]);
}
nestedTreeMapSet(
callbacksMap,
currentPath,
callbacks.map(callback => retValue => path => {
const value =
retValue === initialRetValue
? destinationObject[property]
: retValue;
return callback(value, property, destinationObject, path);
})
);
}
}
}
// Decorating the final nested values.
for (let i = paths.length - 1; i >= 0; i--) {
const path = paths[i];
const callbacks = nestedTreeMapGet(callbacksMap, path) || [];
let retValue = initialRetValue;
for (const callback of callbacks) {
retValue = callback(retValue)(path);
}
if (retValue !== initialRetValue) {
setNestedPropertyValue(destinationObject, path, retValue);
}
}
return destinationObject;
} else {
return extend(destinationObject, ...rest);
}
}
/**
* Shallowly extends a destination object with the provided source objects (first dimension).
*
* @param {Object} destinationObject The destination object.
* @param {...Object} sourceObjects The source objects.
* @return {Object} The destination object "destinationObject" given as parameter after shallow extension.
*/
export function shallowExtend(destinationObject, ...sourceObjects) {
sourceObjects.map(obj =>
Object.keys(obj).map(key => (destinationObject[key] = obj[key]))
);
return destinationObject;
}
/**
* Like "Array.prototype.includes", but with type coercion.
*
* @param {Array} array The array.
* @param {Anything} value The value.
* @return {boolean} True if the value is included within the array (checking with type coercion`==`).
*/
export function includesTypeCoercion(array, value) {
for (const valueOfArray of array) {
if (valueOfArray == value) {
return true;
}
}
return false;
}
/**
* Tests whether a value is undefined or not.
*
* @param {*} value A value
* @return {boolean} True value is undefined, false otherwise.
*/
export function isUndefined(value) {
return typeof value === "undefined";
}
/**
* Tests if the given value is an int.
*
* @param {*} value The value.
* @return {boolean} True if value is an int, false otherwise.
*/
export function isInt(value) {
return Number.isInteger(value);
}
/**
* Tests if the given string is an integer string.
*
* @param {*} a The string.
* @return {boolean} True if the given string is an integer string, false otherwise.
*/
export function ctypeDigit(a) {
return Boolean(a.match(/^[0-9]+$/));
}
/**
* Tests if the given value is an int or an integer string.
*
* @param {*} a The value.
* @return {boolean} True if the value is an int or an integer string, false otherwise.
*/
export function isIntegerOrIntegerStr(a) {
return Number.isInteger(a) || ctypeDigit(a);
}
/**
* Finds the index of a value in an array without type juggling
* (i.e. like "Array.prototype.indexOf", but using "==" for equality comparison).
*
* @param {Array} array An array.
* @param {*} value A value.
* @return {number} -1 if the value is not in array, otherwise the index of value in array.
*/
export function findIndex(array, value) {
return array.findIndex(el => el == value);
}
/**
* Returns the first value of the first property of an object.
*
* @param {Object} obj The object.
* @return {*} The value of the first property of the given object.
*/
export function firstPropValue(obj) {
let prop;
for (prop in obj) {
break;
}
return obj[prop];
}
/**
* Tests if a value is strictly a boolean "true".
*
* @param {*} value A value.
* @return {boolean} "true" if the value is a boolean "true", "false" otherwise.
*/
export function isStrictlyTrue(value) {
return value === true;
}
/**
* Tests if a value is truthy or not.
*
* @param {*} value The value.
* @return {boolean} "true" if the value is truthy (evaluates to boolean "true"), "false" otherwise.
*/
export function isTruthy(value) {
return Boolean(value);
}
/**
* Tests if all the given values are truthy.
*
* @param {...*} values The values.
* @return {boolean} "true" if and only if all the values are truthy.
*/
export function allTruthy(...values) {
return values.every(isTruthy);
}
/**
* Tests if all the given values are not undefined.
*
* @param {...*} values The values.
* @return {boolean} "true" if and only if all the values are not undefined.
*/
export function allNotUndefined(...values) {
return values.every(value => typeof value !== "undefined");
}
/**
* Tests if a string is a valid JSON string.
*
* @param {string} str A string.
* @return {boolean} "true" if the string represents a valid JSON string, "false" otherwise.
*/
export function isJSONString(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
/**
* No-op function.
*
* @return {undefined}
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
export function noOpFn() {}
/**
* Tests if a partial object is a subset of another object.
*
* @param {Object} obj An object.
* @param {Object} partialObj A partial object which may not have all the keys of "obj"
* or may even have different keys, or keys with different values.
* @return {boolean} False if "partialObj" has a key which is not in "obj",
* or has at least one key which is also in "obj" but with a different value
* (shallow comparison), true otherwise.
*/
export function partialShallowEqual(obj, partialObj) {
return shallowEqual(
Object.keys(partialObj).reduce((carry, key) => {
carry[key] = obj[key];
return carry;
}, {}),
partialObj
);
}
/**
* Returns a shallow object diff, returning an object with two keys "objA" and "objB",
* each containing an object with all the properties of one of the two objects which are not within
* the other object, respectively.
* If a property is on both objects but each object has a different value for that same property
* (using shallow equality comparison), the returned property will be set on both objects with their
* respective values.
*
* @param {Object} objA First object.
* @param {Object} objB Second object.
* @return {Object} An object containing the shallow diff, with two keys "objA" and "objB".
*/
export function shallowObjectDiff(objA, objB) {
const diff = {
objA: {},
objB: {},
};
if (shallowEqual(objA, objB)) {
return diff;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
for (let i = 0; i < keysA.length; i++) {
const prop = keysA[i];
if (!objectPropEqual(objA, objB, prop)) {
diff.objA[prop] = objA[prop];
if (hasOwnProperty.call(objB, prop)) {
diff.objB[prop] = objB[prop];
}
}
}
for (let i = 0; i < keysB.length; i++) {
const prop = keysB[i];
if (!objectPropEqual(objB, objA, prop)) {
diff.objB[prop] = objB[prop];
if (hasOwnProperty.call(objA, prop)) {
diff.objA[prop] = objA[prop];
}
}
}
return diff;
}
/**
* Casts a value to a string.
*
* @param {*} v A value.
* @return {string} The string representation of the value.
*/
export function str(v) {
return "" + v;
}
/**
* Maps an object, executing a function on each of its properties
* returning a new mapped object.
*
* @param {Object} obj The object to map.
* @param {Function} fn The function to use for the mapping.
* @return {Object} The new mapped object.
*/
export const mapObject = (obj, fn) =>
Object.fromEntries(
Object.entries(obj).map(([key, value], index) => [
key,
fn(value, key, index),
])
);
/**
* Maps an array to an object.
*
* @param {Array} arr An array.
* @param {Function} fn A function receiving the current value, its index as well as the whole array "arr"
* as parameters returning a tuple with the key at index 0 and the value at index 1 to set on the object.
* @returns {Object} The array mapped to an object.
*/
export const mapToObject = (arr, fn) => {
let res;
return arr.reduce(
(carry, ...rest) =>
((res = fn(...rest)) && false) ||
((carry[res[0]] = res[1]) && false) ||
carry,
{}
);
};
/**
* Selects the truthy properties of an object or the properties of an object
* passing a test specified with a callback function.
*
* @param {Object} obj The input object.
* @param {Function} fn A function which will receive two parameters:
*
* - value: The current value of the input object for a given property;
* - prop: The name of the current property of the input object.
*
* The function will have to return a boolean "true" indicating that this value
* and property has to be included in the returned object, or "false" otherwise.
*
* @return {Object} An object having only the properties which passed the test
* implemented by the "fn" callback function.
*/
export const propSelection = (obj, fn = isTruthy) =>
Object.keys(obj).reduce((carry, key) => {
if (fn(obj[key], key)) {
carry[key] = obj[key];
}
return carry;
}, {});
/**
* Returns all the properties of the given object traversing its prototype chain.
*
* @param {Object} obj The object.
* @param {Object} [options] An object with options.
* @param {?Object} [options.stopAtPrototype] The prototype in the chain at which to stop the traversing.
* Defaults to null, in which case the whole prototype chain will be traversed.
* @param {boolean} [options.stopAtPrototypeInclude] Whether or not to include the properties of the given prototype at which to stop.
* Works only if "stopAtPrototype" is set to a valid prototype object in the prototype chain
* of "obj".
* Defaults to true, in which case the properties of the prototype at which to stop will be included
* in the returned array.
* @return {string[]} An array of containing the names of the properties.
*/
export function prototypeChainProperties(
obj,
{ stopAtPrototype = null, stopAtPrototypeInclude = true } = {}
) {
let current = obj;
const map = {};
let isStopPrototype = false;
let stop = false;
while (!stop && current && (!isStopPrototype || stopAtPrototypeInclude)) {
if (isStopPrototype) {
stop = true;
}
const properties = Object.getOwnPropertyNames(current);
properties.map(property => (map[property] = true));
current = Object.getPrototypeOf(current);
if (stopAtPrototype) {
isStopPrototype = current === stopAtPrototype;
}
}
const properties = Object.keys(map);
return properties;
}
/**
* Defines an object's property with a getter and an optional setter.
*
* @param {Object} obj An object (may be a prototype).
* @param {string} propname The property name.
* @param {Function} getfn Getter function.
* @param {Function|undefined} [setfn] Setter function.
* @return {undefined}
*/
export function prop(obj, propname, getfn, setfn = void 0) {
const propObj = {};
propObj[propname] = {
get: getfn,
set: setfn,
};
Object.defineProperties(obj, propObj);
}
/**
* Defines a property on an object.
*
* @param {Object} o An object.
* @param {string} p The property to define.
* @param {Object} descriptor Optional object containing the descriptor's properties to use to override the default properties.
* @return {Object} The object that was passed to the function.
*/
export function defineProperty(o, p, descriptor = {}) {
return Object.defineProperty(o, p, {
configurable: false,
enumerable: false,
writable: true,
...descriptor,
});
}
/**
* Assigns the properties of the given source objects to the target object.
*
* @source https://stackoverflow.com/questions/39515321/spread-operator-issues-with-property-accessors-getters#answer-39521418
*
* @param {Object} target The target object.
* @param {...Object} sources The source objects.
* @return {Object} The target object.
*/
export function completeObjectAssign(target, ...sources) {
sources.forEach(source => {
const descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});
// By default, Object.assign copies enumerable Symbols too.
Object.getOwnPropertySymbols(source).forEach(sym => {
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
if (descriptor.enumerable) {
descriptors[sym] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
}