Last active
December 11, 2021 17:34
-
-
Save jhunterkohler/037cf9c010d2fd1b46226fe6623f1b44 to your computer and use it in GitHub Desktop.
Some lodash-esc functional utilities; mostly manipulating path strings.
This file contains hidden or 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
/* | |
* Copyright (C) 2021 John Hunter Kohler <[email protected]> | |
*/ | |
/** | |
* Returns true if `value` is non-primitive (ECMA script standard, i.e., null is | |
* a primitive, function is an object). | |
* | |
* @param {any} value | |
* @returns {bool} | |
*/ | |
export function isObject(value) { | |
return ( | |
(typeof value == "object" && value !== null) || | |
typeof value == "function" | |
); | |
} | |
/** | |
* Checks if value is an array. Optionally checks that condition is true for | |
* each argument. | |
* | |
* @template T | |
* @param {any} value | |
* @param {(val: any) => val is T} [condition] Condition to run on all | |
* arguments. Does not attempt if no function. | |
* @returns {value is T[]} | |
*/ | |
export function isArray(value, condition) { | |
return ( | |
Array.isArray(value) && | |
(typeof condition != "function" || value.every(condition)) | |
); | |
} | |
/** | |
* Coerces any value into a *path* array. Arrays are returned with all elements | |
* cast to string. Non-strings are casted to string, then parsed as strings. | |
* | |
* @param {any} value | |
* @returns {string[]} Path array. | |
*/ | |
export function toPath(value) { | |
if (Array.isArray(value)) { | |
return value.map(String); | |
} else if (typeof value != "string") { | |
return [String(value)]; | |
} | |
return value | |
.replace(/(?<=\[.*?\])([^.])/, ".$1") // fixes a string like 'a.b[c]d' | |
.replace(/\[(.*?)\](?=$|\.)/g, ".$1") // leave quotes in subscript (: | |
.split("."); | |
} | |
/** | |
* Gets value at `path` on `source` object. If the path does not exist (or when | |
* `source` is not an object), returns undefined. | |
* | |
* @param {any} source Source of data. | |
* @param {any} path Item to cast to path array. | |
* @returns {any} Value at `path` on `source`. | |
*/ | |
export function get(source, path) { | |
for (const field of toPath(path)) { | |
source = source?.[field]; | |
} | |
return source; | |
} | |
/** | |
* Sets `value` in `target` at `path`. If intermediate values are not objects, | |
* it creates them as empty object before proceeding. *Ignores* non-objects. | |
* `target` is returned. | |
* | |
* @param {any} target Object on which to set `value` at `path`. | |
* @param {any} path Item to cast to *path array*. | |
* @param {any} value Value to set at path `path` on `target`. | |
* @returns {any} `target` | |
*/ | |
export function set(target, path, value) { | |
if (!isObject(target)) { | |
return target; | |
} | |
path = toPath(path); | |
let temp = target; | |
let i = 0; | |
for (; i < path.length - 1; i++) { | |
if (!isObject(temp[path[i]])) { | |
temp = temp[path[i]] = {}; | |
} | |
} | |
temp[path[i]] = value; | |
return target; | |
} | |
/** | |
* Checks if `object` has property at `path`. Checks own property descriptor's, | |
* not values. Return's false on primitives. | |
* | |
* @param {any} object | |
* @param {string | string[]} path Item to cast to *path array*. | |
* @returns {boolean} | |
*/ | |
export function has(object, path) { | |
path = toPath(path); | |
if (!path.length) { | |
return false; | |
} | |
let i = 0; | |
for (; i < path.length - 1; i++) { | |
if (!isObject(object[path[i]])) { | |
return false; | |
} | |
object = object[path[i]]; | |
} | |
return ( | |
isObject(object) && | |
Reflect.ownKeys(object).includes(path[path.length - 1]) | |
); | |
} | |
/** | |
* Creates new object which values at each path from `paths` with values at said | |
* paths in `object`. Ignores paths that are not propeties on `object`. | |
* | |
* @param {any} object Source object. | |
* @param {(string | string[])[]} paths Items to cast to *path arrays*. | |
* @returns {any} | |
*/ | |
export function pick(object, paths) { | |
if (!isArray(paths)) { | |
return {}; | |
} | |
const target = {}; | |
for (let path of paths) { | |
path = toPath(path); | |
if (has(object, path)) { | |
set(target, path, get(object, path)); | |
} | |
} | |
return target; | |
} | |
/** | |
* Flattens `array` for `depth`. Ignores non-arrays and input other than | |
* integers greater than or equal to one for `depth` by returning a shallow copy | |
* of `array`. Does not edit in place, creates new array. | |
* | |
* @param {any[]} array | |
* @param {number} [depth=1] Levels to flatten `array` by. Default is `1` | |
* @returns {any[]} `array` | |
*/ | |
export function flatten(array, depth = 1) { | |
if (!isArray(array) || depth < 1 || !Number.isInteger(depth)) { | |
return [...array]; // must adhere to returning non-original | |
} | |
const result = []; | |
for (const element of array) { | |
if (isArray(element) && depth >= 1) { | |
result.push(...flatten(element, depth - 1)); | |
} else { | |
result.push(element); | |
} | |
} | |
return result; | |
} | |
/** | |
* Deletes all `paths` on `object` if configurable. | |
* | |
* @param {any} object | |
* @param {(string | string[])[]} paths Items to be cast to *path arrays*. | |
* @returns {any} `object` | |
* @alias delete | |
*/ | |
function delete_(object, paths) { | |
if (!isArray(paths)) { | |
return object; | |
} | |
for (let path of paths) { | |
path = toPath(path); | |
let i = 0; | |
let temp = object; | |
for (; i < path.length - 1; i++) { | |
if (!isObject(temp[path[i]])) { | |
break; | |
} | |
temp = temp[path[i]]; | |
} | |
if (isObject(temp)) { | |
Reflect.deleteProperty(temp, path[i]); | |
} | |
} | |
return object; | |
} | |
export { delete_ as delete }; | |
export default { | |
get, | |
set, | |
has, | |
pick, | |
toPath, | |
isArray, | |
flatten, | |
isObject, | |
delete: delete_, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment