Skip to content

Instantly share code, notes, and snippets.

@jhunterkohler
Last active December 11, 2021 17:34
Show Gist options
  • Save jhunterkohler/037cf9c010d2fd1b46226fe6623f1b44 to your computer and use it in GitHub Desktop.
Save jhunterkohler/037cf9c010d2fd1b46226fe6623f1b44 to your computer and use it in GitHub Desktop.
Some lodash-esc functional utilities; mostly manipulating path strings.
/*
* 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