UNPKG

@almela/obx

Version:

Fast & Lightweight Object Manipulation Library for Javascript

684 lines (644 loc) 16.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.add = add; exports.cp = cp; exports.eq = eq; exports.get = get; exports.isEmptyArr = isEmptyArr; exports.isEmptyObj = isEmptyObj; exports.len = len; exports.map = map; exports.reduce = reduce; exports.set = set; exports.sub = sub; exports.zip = zip; /* prevent prototype pollution */ Object.freeze(Object.prototype); /* type checker */ const t = d => d instanceof Function ? "f" // function : d instanceof Array ? "a" // array : d instanceof Object ? "o" // object : "p"; // primitive // Object inorder traversal iterator function* inorder(o, depth = -1, path = []) { if (depth === 0) return; for (const kv of Object.entries(o)) { const [k, v] = kv; // yield is of the form { kv: [k, v], path: "path.to.current.key.0" } // note that we yield no matter the value type: object, array, or other yield { kv, path: [...path, k].join(".") }; if ("oa".includes(t(v))) yield* inorder(v, depth - 1, [...path, k]); } } // create iterator from object/array // this ensures we aren't missing any keys const iterObj = o => { const type = t(o); if (!"ao".includes(type)) throw new Error("Object must be passed to iterObj"); if (type === "o") return Object.keys(o); // for arrays, iterate keys, return generator return function* arrayIter() { let c = 0; for (let i = 0; i < o.length; i++) { c++; yield i; } return c; }(); }; /** * Assert that two objects are equal. * Objects are equal if they have the same keys and values. * * @param {Object} a Object 1 * @param {Object} b Object 2 * @param {Object} params Parameters object * @param {number=} params.depth Depth of equality check. Defaults to infinity * * @example <caption>Object order doesn't matter</caption> * obx.eq( * { foo: "bar", bar: "baz" }, * { bar: "baz", foo: "bar" } * ) * // -> true * * @example <caption>With Arrays</caption> * obx.eq([1, 2, 3], [1, 2, 3]) * // -> true * * @example <caption>Array order does matter!</caption> * obx.eq([1, 2, 3], [3, 2, 1]) * // -> false * * @example <caption>Custom depth</caption> * obx.eq({ foo: "bar" }, { foo: "baz" }, { depth: 0 }) * // -> true * * @example * obx.eq({ foo: { bar: "baz" } }, { foo: {} }, { depth: 1 }) * // -> true * * @example * obx.eq({ foo: { bar: "baz" } }, { foo: {} }, { depth: 2 }) * // -> false * * @example <caption>Functions</caption> * // Unfortunately, functions are basically impossible to * // diff. `eq` assumes that all functions are the same. * obx.eq({ foo: (x) => x + 1 }, { foo: (x) => x + 2 }) * // -> true */ function eq(a, b, params = {}) { const { depth = -1 } = params; if (depth === 0) return true; // check for same root object type if (t(a) !== t(b)) return false; if (t(a) === "o") { // assert that b is in a // and that a is in b for (const k of Object.keys(b)) if (!(k in a)) return false; for (const k of Object.keys(a)) if (!(k in b)) return false; } // objects and arrays are iterated differently const keys = iterObj(a); for (const k of keys) { // type check if (t(a[k]) !== t(b[k])) return false; // asserting function equality is basically impossible if (t(a[k]) === "f") continue; // objects or arrays if ("oa".includes(t(a[k]))) return eq(a[k], b[k], { depth: depth - 1 }); // primitive types if (t(a[k]) === "p" && a[k] !== b[k]) return false; } return true; } /** * Deep copy an object. * * @param {Object} o Object to copy * @param {Object} params Parameters object * @param {number=} params.depth Depth of copy. Defaults to infinity * * @example <caption>Copy by value, not by reference</caption> * const a = { * foo: { * bar: 'baz' * } * } * const b = obx.cp(a) * * a.foo.bar = 'bar' * console.log(b) * // object remains the same * // -> { * // foo: { * // bar: 'baz' * // } * // } */ function cp(o, params = {}) { const { depth = -1 } = params; // this type check could be removed with TS const type = t(o); if (!"ao".includes(type)) throw new Error("Object must be passed to cp"); const fn = (a, [_, v], p) => { // init object if (t(v) === "o") set(a, p, {}); // init array if (t(v) === "a") set(a, p, []); // copy functions if (t(v) === "f") set(a, p, v.bind); // copy primitives if (t(v) === "p") set(a, p, v); return a; }; return reduce(o, fn, type === "a" ? [] : {}, { depth }); } /** * Get value from object. * * @param {Object} o The object * @param {String} p Value path * * @example <caption>Get a deep key</caption> * obx.get("foo.bar", { * foo: { * bar: "baz" * } * }); * // -> "baz" * * @example <caption>Also works with arrays</caption> * obx.get("foo.2.baz", { * foo: [ * { * foo: 'foo' * }, * { * bar: 'bar' * }, * { * baz: 'baz' * } * ] * }); * // -> "baz" * * @example <caption>No key? No problem.</caption> * obx.get("foo.2.baz", { * foo: 'bar' * }) * // -> null * */ function get(o, p) { const path = p.split("."); for (let i = 0; i < path.length; i++) { const d = path[i]; if (!(d in o)) return null; o = o[d]; } return o; } /** * Set value in object. * * @param {Object} o The object to be mutated * @param {String} p Value path * @param {Object} v Value to set * * @example * const o = {} * obx.set(o, "foo.bar.baz.haz", "hello") * // -> { * // foo: { * // bar: { * // baz: { * // haz: "hello" * // } * // } * // } * // } * * @example * const o = {} * obx.set(o, "foo.2.foo", 'bar') * // o -> { * // foo: [<2 empty slots>, { * // foo: 'bar' * // }] * // } */ function set(o, p, v) { const path = p.split("."); for (let i = 0; i < path.length; i++) { const d = path[i]; if (i + 1 === path.length) return o[d] = v; const n = path[i + 1]; // deepen object if (!(d in o)) o[d] = isNaN(n) ? {} : []; o = o[d]; } } /** * Recursively find the number of keys of an object. * Note that this includes object-valued keys. * * @param {Object} o Object to find length of * @param {Object} params Parameters object * @param {number=} params.depth Depth of len check. Defaults to infinity * * @example <caption>Simple object</caption> * obx.len({ foo: 'bar', bar: 'baz' }) // -> 2 * * @example <caption>Recursive object, depth 1</caption> * // Here depth is only computed at the top level * obx.len({ foo: 'bar', bar: { bar: 'baz', baz: [1, 2, 3] } }, { depth: 1 }) // -> 2 * * @example <caption>Recursive object, infinite depth</caption> * // Note: array keys are counted * obx.len({ foo: 'bar', bar: { bar: 'baz', baz: [1, 2, 3] } }) // -> 7 */ function len(o, params = {}) { return reduce(o, a => a + 1, 0, params); } /** * Recursively map though all entries of an object. * Note that map will also iterate through object-valued keys. * * @param {Object} o Object to map through * @param {function} fn Callback function. Contains [k, v] pair, path, object * @param {Object} params Parameters object * @param {number=} params.depth Depth of map. Defaults to infinity * * @example <caption>Basic Mapping</caption> * const o = { * foo: "bar", * bar: "baz", * baz: "foo", * }; * * // Note that map will callback on every value of the object, including sub objects! * const emphasis = ([_, v]) => (v instanceof Object ? v : v + "!"); * * // Note that depth could be anything here, since this is just a flat object. * obx.map(o, emphasis, { depth: 1 }); * // -> { * // foo: "bar!", * // bar: "baz!", * // baz: "foo!", * // } * * @example <caption>Recursive Mapping, low depth</caption> * const o = { * foo: "bar", * bar: { * baz: "foo", * }, * }; * * // Note that map will callback on every value of the object, including sub objects! * const emphasis = ([_, v]) => (v instanceof Object ? v : v + "!"); * obx.map(o, emphasis, { depth: 1 }); * // -> { * // foo: "bar!", * // bar: { * // baz: "foo", * // } * // } * // * // Note that the inner key is unchanged. * * @example <caption>Recursive Mapping, high depth</caption> * const o = { * foo: "bar", * bar: [ * { foo: "bar", bar: "baz" }, * { foo: "bar", bar: "baz" }, * { foo: "bar", bar: "baz" }, * ], * raz: "faz", * }; * * // Note that map will callback on every value of the object, including sub objects! * const emphasis = ([_, v]) => (v instanceof Object ? v : v + "!"); * obx.map(o, emphasis); * // -> { * // foo: "bar!", * // bar: [ * // { foo: "bar!", bar: "baz!" }, * // { foo: "bar!", bar: "baz!" }, * // { foo: "bar!", bar: "baz!" }, * // ], * // raz: "faz!", * // } */ function map(o, fn, params = {}) { const { depth = -1 } = params; // this type check could be removed with TS const type = t(o); if (!"ao".includes(type)) throw new Error("Object must be passed to map"); // n: new object // p: current object path // r: root object const aux = (o, fn, n, d, p, r) => { for (const [k, v] of Object.entries(o)) { // path string const _p = [...p, k].join("."); // callback is ran on all value types // n is mutated here set(n, _p, d !== 0 ? fn([k, v], _p, r) : v); // objects or arrays: recurse if (d !== 0 && "oa".includes(t(v))) aux(v, fn, n, d - 1, [...p, k], r); } return n; }; return aux(o, fn, type === "a" ? [] : {}, depth, [], o); } /** * Recursively reduce through all entries of an object. * Note that reduce will also iterate through object-valued keys. * * @param {Object} o Object to map through * @param {function} fn Callback function * @param {Object} a Accumulator * @param {Object} params Parameters object * @param {number=} params.depth Depth of reduce. Defaults to infinity * @param {function=} params.iter Iterator used by reduce. Defaults to inorder traversal. * * @example <caption>Flat object</caption> * const o = { foo: "bar", bar: "baz" }; * * const combineVals = (a, [k, v]) => [...a, v]; * obx.reduce(o, combineVals, []).join(", "); * // -> "bar, baz" * * @example <caption>Deeper object</caption> * const o = { * foo: "bar", * bar: { * baz: "haz", * }, * }; * * const combineVals = (a, [k, v]) => (v instanceof Object ? a : [...a, v]); * obx.reduce(o, combineVals, []).join(", "); * // -> "bar, haz" * * @example <caption>Custom depth</caption> * const o = { * foo: "bar", * bar: { * baz: { * haz: "wow", * }, * foo: "bar", * }, * raz: { * faz: "maz", * gaz: 'haz', * haz: [ * { maz: 'waz' }, * { foo: 'moo' } * ] * }, * } * * const combineVals = (a, [k, v]) => (v instanceof Object ? a : [...a, v]); * obx.reduce(o, combineVals, [], { depth: 2 }).join(", "); * // -> "bar, bar, maz, haz" * // Only gets keys down to depth 2 */ function reduce(o, fn, acc, params = {}) { const { depth = -1, iter = inorder } = params; for (const { kv, path } of iter(o, depth)) acc = fn(acc, kv, path, o); return acc; } /** * Group multiple objects into a single iterator. * Note that zip will also iterate through object-valued keys. * * @param {Array} objs Array of objects to be zipped together. * @param {Object} params Parameters object * @param {number=} params.depth Depth of zip. Defaults to infinity * @param {Boolean=} params.key Whether zip should return object keys. Defaults to `false` * @param {Boolean=} params.val Whether zip should return object values. Defaults to `true` * @param {Boolean=} params.last Whether zip should stop iterating when the last object is done, as opposed to the first. Defaults to `false` * @param {function=} params.iter Iterator used by zip. Defaults to inorder traversal. * * @example <caption>Stops at the first null value</caption> * const a = ["a", "b", "c"]; * const b = [1]; * * // loop runs only once * for (const z of obx.zip([a, b])) * console.log(z) * // -> ["a", 1] * * @example <caption>Recursive</caption> * const a = { * foo: "bar", * bar: { * baz: "haz", * }, * }; * * const b = [4, 5]; * * for (const z of obx.zip([a, b])) * console.log(z) * // -> ["bar", 4] * // -> ["haz", 5] * * @example <caption>More than 2 Objects</caption> * const a = ["a", "b", "c"]; * const b = [1, 2, 3]; * const c = ["x", "y", "z"]; * const d = [3, 2, 1]; * * for (const z of obx.zip([a, b, c, d])) * console.log(z) * // -> ["a", 1, "x", 3] * // -> ["b", 2, "y", 2] * // -> ["c", 3, "z", 1] */ function* zip(objs, params = {}) { const { depth = -1, key = false, val = true, last = false, iter = inorder } = params; const gens = []; // for each object, create an iterator for (const o of objs) gens.push(iter(o, depth)); let allDone; do { const vals = []; allDone = true; for (const g of gens) { const n = g.next(); allDone = allDone && n.done; // return as soon as one object is fully iterated if (!last && n.done) return; // return [k, v], or v, or k depending on params if (n.done) vals.push(null);else if (key && val) vals.push(n.value.kv);else if (key) vals.push(n.value.kv[0]);else if (val) vals.push(n.value.kv[1]); } // check if all generators are done if (last && allDone) return; yield vals; } while (!allDone); } /** * Recursive, in-place object subtraction. * * @param {Object} o The object to be subtracted from. This object is mutated. * @param {Object} s The object to subtract with * @param {Object} params Parameters object * @param {number=} params.depth Depth of subtraction. Defaults to infinity * * @example <caption>Simple subtraction</caption> * const a = { * foo: "bar", * bar: "baz", * list: [1, 2, 3], * }; * * const b = { * foo: "bar", * list: [1, 2, 3], * }; * * obx.sub(a, b); * console.log(a) * // -> { bar: "baz" } * * @example <caption>With arrays</caption> * const a = [1, 2, 3]; * const b = [1, 2, 3]; * * obx.sub(a, b); * console.log(a) * // -> [] */ function sub(o, s, params = {}) { const { depth = -1 } = params; if (depth === 0) return; for (const k of Object.keys(o)) { if (t(o[k]) !== t(s[k])) continue; // we don't diff functions if (t(o[k]) === "f") continue; // objects or arrays if ("oa".includes(t(o[k]))) { sub(o[k], s[k], { depth: depth - 1 }); if (len(o[k]) < 1) delete o[k]; } // primitive types if (t(o[k]) === "p" && k in s && s[k] === o[k]) delete o[k]; } } /** * Recursive, in-place object addition. * If both objects contain the same key, defaults to o * * @param {Object} o The object to be added to. This object is mutated. * @param {Object} a The object to add with * @param {Object} params Parameters object * @param {number=} params.depth Depth of addition. Defaults to infinity * * @example <caption>Simple addition</caption> * const a = { * foo: "bar", * bar: "baz", * list: [1, 2, 3], * }; * * const b = { * foo: "bar", * haz: 5, * }; * * obx.add(a, b); * console.log(a) * // -> { foo: "bar", bar: "baz", list: [1, 2, 3], haz: 5 } */ function add(o, a, params = {}) { const { depth = -1 } = params; if (depth === 0) return; for (const k of Object.keys(a)) { const type = t(a[k]); // objects or arrays if ("ao".includes(type)) { // init empty obj/arr if (!(k in o)) o[k] = type === "o" ? {} : []; add(o[k], a[k], { depth: depth - 1 }); } if (type === "p" && !(k in o)) o[k] = a[k]; } } /** * Assert that an object type is empty. * * @param {Object} o Object to assert is empty * * @example * obx.isEmptyObj({}) // -> true * * @example * obx.isEmptyObj({ foo: 'bar' }) // -> false * * @example <caption>Only works for objects</caption> * obx.isEmptyObj([]) // -> false */ function isEmptyObj(o) { return eq(o, {}); } /** * Assert that an array type is empty. * * @param {Array} a The array to assert is empty * * @example * obx.isEmptyArr([]) // -> true * * @example * obx.isEmptyArr([1, 2, 3]) // -> false * * @example <caption>Only works for arrays</caption> * obx.isEmptyArr({}) // -> false */ function isEmptyArr(o) { return eq(o, []); } //# sourceMappingURL=index.js.map