apply function to all instances of an attribute in an object tree - javascript

Let's say I have an object of unknown structure and depth, where I want to modify an attribute. For example, let's imagine that the attribute I want to change is id and the structure might be something like this:
"{
"a": "foo",
"b": "bar",
"c": {
"id": {
"a": "foo"
},
"b": "whatever"
},
"id": {
"a": "foo"
}
}"
Is there any stablished pattern or way to take any instance of a wanted key in the whole tree and apply a generic function to it? Ideally, checking array elements too if they existed.

In terms of functional programming, we need our structure to be a Functor, that is, supply it with a function fmap so that fmap(fn, someTree) will yield the same structure with fn applied to all values inside it. There are quite a few ways to implement that, for example:
let isObject = x => x && typeof x === 'object';
let map = fn => obj =>
Array.isArray(obj)
? obj.map((v, k) => fn([null, v]).pop())
: Object.fromEntries(Object.entries(obj).map(fn));
let _fmap = fn => ([key, val]) =>
isObject(val)
? [
key,
map(_fmap(fn))(val)
]
: fn([key, val]);
let fmap = fn => map(_fmap(fn));
//
const renameKey = (o, n) => ([key, val]) => (key === o ? [n, val] : [key, val]);
test = {
a: 1,
b: 2,
c: [
{a: 1}, {a: 2}, {b: 3}
]
};
newTest = fmap(renameKey('a', 'blah'))(test);
console.log(newTest);
Here, we treat a tree "node" as a [key, value] pair. The callback receives a pair and is supposed to return it back, with or without a modification.

Related

filter object by keys then flatten it to an array

I want to filter an object by it's keys then flatten it to an array so I ended up with this code:
Object.keys(
Object.keys(x)
.filter((key) => allowed.includes(key))
.reduce((obj, key) => {
obj[key] = x[key];
return obj;
}, {})
).reduce(
(r, k) => r.concat(k, choices[k]), []
);
Is there any other way to do it without calling Object.keys twice?
Thanks in advance.
#Edit:
Sorry for not providing the data, and I forgot to mention that I want it to be sorted as well.
For example I want this :
{
"b": [
"b1",
"b2",
"b3"
],
"v": [
"v1",
"v2",
"v3"
],
"a": [
"a1",
"a2",
"a3"
]
}
To be like this:
["a", "a1", "a2", "a3", "b", "b1", "b2", "b3"]
allowed is : ["a", "b"]
Unless there is something very strange in your data, the following code is completely unnecessary:
Object.keys(x)
.filter((key) => allowed.includes(key))
.reduce((obj, key) => {
obj[key] = x[key];
return obj;
}, {})
It will take the object x, filter its keys and create a new object with filtered values from it:
const x = {a: 1, b: 2, c: 3};
const allowed = ["a", "c"];
const y = Object.keys(x)
.filter((key) => allowed.includes(key))
.reduce((obj, key) => {
obj[key] = x[key];
return obj;
}, {});
console.log(y); // {a: 1, c: 3};
However, after all that, you just take the keys of the new object again. This is a useless operation, it can all be shortened to
Object.keys(x)
.filter(key => allowed.includes(key))
.reduce(
(r, k) => r.concat(k, choices[k]), []
);
This however, leaves another useless call to .reduce. Unless you're constrained to pre-ES2019 versions, you can just .flatMap() for this:
Object.keys(x)
.filter(key => allowed.includes(key))
.flatMap(k => [k, ...choices[k]]);
is this what you mean?
Array.from(Object.entries(x).filter(x=>allowed.includes(x[0])).map(x=>x[1]));
// map "1" for values, "0" for keys or map {x[0]:x[1]}
// to return an array of objects representing the filtered key=>value pairs.
//where "x" is the object
here is a code pen test as well:
https://codepen.io/altruios/pen/eYzpNxj

Merge objects updating duplicate keys (case-insensitively)

I'm using a javascript module that has a configuration object. One is already set with defaults and the user can pass in values to overwrite these set values. I am using Object.assign to merge the two objects.
Here is an example:
const target = { a: 1, b: 2 }; // <-- default config
const source = { B: 4, c: 5 }; // <-- User input config
Object.assign(target, source);
console.log(target); //{a: 1, b: 2, B: 4, c: 5}
In this example if the user accidentally types in an uppercase 'B' instead of a lowercase one then the config object adds another value to itself when what I really want is the lowercase 'b' to be updated.
I know that's the intended behavior of Object.assign but trying to make this easier for the user and be case insensitive.
This version is a little different from the others. It normalizes only the keys found in the initial object, leaving the others intact. Something like this:
insensitiveAssign ({a: 1, b: 2}, {B: 4, c: 5, D: 6}) //=> {a: 1, b: 4, c: 5, D: 6}
// ^ ^ ^ ^
// unaltered --------------' | | |
// overwritten ------------------+ | |
// added ------------------------------+ |
// added (note: key not modified) -----------+
That may or may not be of use to you, but it's an interesting approach to the problem. It also does not modify either of your objects, creating an altered clone instead.
const insensitiveAssign = (target, source) => {
const keys = Object .keys (target) .reduce ((a, k) => ((a[k.toLowerCase()] = k), a), {})
return Object .entries (source) .reduce ((a, [k, v]) => {
const lowerK = k.toLowerCase()
const key = lowerK in keys ? keys[lowerK] : k
a[key] = v;
return a
}, Object.assign({}, target)) // start with a shallow copy
}
const target = {a: 1, b: 2};
const source = {B: 4, c: 5, D: 6};
console .log (
'result:',
insensitiveAssign (target, source),
)
console .log (
'target:',
target,
)
console .log (
'source:',
source
)
Update
A comment updated the question to ask how this might be applied to nested objects. In actuality, I would probably try to write that from scratch, but I don't have time now and a (only slightly tested) modification of this seems like it would work:
const insensitiveAssign = (target, source) => {
// if-block added
if (Object(target) !== target || (Object(source) !== source)) {
return source
}
const keys = Object .keys (target) .reduce ((a, k) => ((a[k.toLowerCase()] = k), a), {})
return Object .entries (source) .reduce ((a, [k, v]) => {
const lowerK = k.toLowerCase()
const key = lowerK in keys ? keys[lowerK] : k
a[key] = insensitiveAssign(target[key], v); // this line updated
return a
}, Object.assign({}, target))
}
const target = {a: 1, b: 2, x: {w: 'a', y: {z: 42}}};
const source = {B: 4, c: 5, D: 6, x: {V: 'c', Y: {z: 101}}};
console .log (
'result:',
insensitiveAssign (target, source),
)
console .log (
'target:',
target,
)
console .log (
'source:',
source
)
You'll have to lowercase the object keys first, like done here
const target = { a: 1, b: 2 }; // <-- default config
const source = { B: 4, c: 5 }; // <-- User input config
const lowerSource = Object.keys(source).reduce((c, k) => (c[k.toLowerCase()] = source[k], c), {});
Object.assign(target, lowerSource);
console.log(target);
You may simply remap source object lower-casing its keys with Object.keys() and Array.prototype.map(), then pass resulting key-value pairs as parameter to Object.assign():
const target = { a: 1, b: 2 },
source = { B: 4, c: 5 },
result = Object.assign(
target,
...Object
.keys(source)
.map(key =>
({[key.toLowerCase()]: source[key]}))
)
console.log(result)
You can try something like below code.
target = { a: 1, b: 2 }; // <-- default config
source = { B: 4, c: 5 }; // <-- User input config
source = JSON.parse(JSON.stringify(source).toLowerCase())
Object.assign(target, source);

In JavaScript there an elegant way to merge two Objects and sum any common properties?

I'm deduping a large array of Objects, many of which have some properties in common. All of the properties are integers.
It's trivially easy to loop through the keys and merge manually, but I can't help but feel like there's some combination of Object.assign and map and reduce that could do this is one line. As the language matures, it seems worth staying ahead of the curve.
EDIT: As an example:
let A = {
"a": 10,
"e": 2,
"g": 14
}
let B = {
"b": 3,
"e": 15,
"f": 1,
"g": 2,
"h": 11
}
let C = Object.magicMerge(A, B)
/*
{
"a": 10,
"e": 17,
"g": 16
"b": 3,
"f": 1,
"h": 11
}
*/
There isn't really any shortcut in vanilla JS, you have to explicitly iterate through each object and each property, and sum them up somehow. One option is to group with reduce:
const arr = [{
foo: 1,
bar: 2
}, {
bar: 5,
baz: 7
}, {
baz: 10,
buzz: 10
}];
const combined = arr.reduce((a, obj) =>
Object.entries(obj).reduce((a, [key, val]) => {
a[key] = (a[key] || 0) + val;
return a;
}, a)
, {});
console.log(combined);
I prefer reduce because the outer combined variable, once created, is not mutated within its scope, but you can use for..of instead if you wish:
const arr = [{
foo: 1,
bar: 2
}, {
bar: 5,
baz: 7
}, {
baz: 10,
buzz: 10
}];
const combined = {};
for (const obj of arr) {
for (const [key, val] of Object.entries(obj)) {
combined[key] = (combined[key] || 0) + val;
}
}
console.log(combined);
Here's something without loops/reduce:
let C = Object.fromEntries(
Object.keys(A)
.concat(Object.keys(B))
.map(k => [k,
(A[k] || 0) + (B[k] || 0)
])
)
And here's a generic function for any number of objects:
let mergeSum = (...objs) => Object.fromEntries(
Array.from(
new Set(objs.flatMap(Object.keys)),
k => [k,
objs
.map(o => o[k] || 0)
.reduce((a, b) => a + b)
]))
C = mergeSum(A, B)
or even more generic:
let mergeReduce = (objs, fn, init) => Object.fromEntries(
Array.from(
new Set(objs.flatMap(Object.keys)),
k => [k, objs.map(o => o[k]).reduce(fn, init)]
));
// for example:
sumOfProps = mergeReduce([A, B],
(a, b) => (a || 0) + (b || 0))
listOfProps = mergeReduce([A, B],
(a, b) => b ? a.concat(b) : a,
[])
I would do like:
function objTotals(array){
const r = {};
array.forEach(o => {
for(let i in o){
if(!(i in r))r[i] = 0;
}
});
for(let i in r){
array.forEach(o => {
if(i in o)r[i]+=o[i];
});
}
return r;
}
const a = [{a:0, b:2, c:5, d:1, e:2}, {a:1, b:3}, {a:5, b:7, c:4}, {a:2, b:1, c:9, d:11}];
console.log(objTotals(a));
I would do it in this way:
const a = { "a": 10, "e": 2, "g": 14 };
const b = { "b": 3, "e": 15, "f": 1, "g": 2, "h": 11 };
const sum = [...Object.entries(a), ...Object.entries(b)]
.reduce(
(acc, [key, val]) => ({ ...acc, [key]: (acc[key] || 0) + val }),
{}
);
console.log("sum:", sum);
One liner as you asked :)
In my opinion it is most elegant way to do that.
Yeah, seems to me you're going to need to merge the keys then merge the objects. Tried for a simpler solution, but this one (while longer than I like) does the job, and for any number of objects.
const object1 = {
a: 4,
c: 2.2,
d: 43,
g: -18
}, object2 = {
b: -22.4,
c: 14,
d: -42,
f: 13.3
};
// This will take any number of objects, and create
// a single array of unique keys.
const mergeKeys = (objects)=>{
let keysArr = objects.reduce((acc, object)=>{
acc.push(...Object.keys(object));
acc = [...new Set(acc.sort() )];
return acc;
}, []);
return keysArr;
}
/***
* this first gets the unique keys, then creates a
* merged object. Each object is checked for each
* property, and if it exists, we combine them.
* if not, we simply keep the current value.
*
***/
const combineAnyNumberOfObjects = (...objects)=>{
const keys = mergeKeys(objects),
returnObj = {};
keys.forEach(key=>{
returnObj[key]=0;
objects.forEach(object=>{
returnObj[key] = !!object[key] ? returnObj[key]+object[key] : returnObj[key]
})
})
return returnObj;
}
console.log(combineAnyNumberOfObjects(object1, object2));
As you said, trivially easy to merge and manually add each prop. The one line answer above? Beautiful. ;)

Recursively return array of object and nested object keys

I am learning about recursion at the moment and have moved on from numbers, string and arrays into using it on objects... I'm trying to work out the best method for taking an object as an argument and collecting the keys of the object and all nested objects into an array
I can return the object keys of a single object not using recursion. So i was trying to create a variable as an empty array then iterate over the object using a for loop and if "i" is an object then push object keys into the array variable and return it. This wouldnt work unfortunate.
I would like the following:
{lamp: 2, candle: 2, pillow: {big: 2, small: 4}, bathroom: {toilet: 1, shower: {shampoo: 1, conditioner: 2}}}
To return:
[lamp, candle, pillow, big, small, bathroom, toilet, shower, shampoo, conditioner]
Hope this explains enough, let me know if not :)
I tried the following:
function(obj) {
let keysArray = [];
for (let i = 0, i < obj.length, i++)
if (obj[i] === typeOf object) {
keysArray.push(obj[i].keys);
}
return keysArray
}
You can write a recursive function as follows
let obj = {lamp: 2, candle: 2, pillow: {big: 2, small: 4}, bathroom: {toilet: 1, shower: {shampoo: 1, conditioner: 2}}};
function getKeys(o) {
let result = [];
for (let key in o) {
result.push(key);
if(o[key] && typeof o[key] === "object") result.push(...getKeys(o[key]));
}
return result;
}
console.log(getKeys(obj));
You need to loop through the object using for...in. The for loop is for arrays
obj[i] === typeOf object is not correct. It should be typeof obj[key] === "object".
If the nested property is an object, you need to recursively call the function and push keys to keysArray
function getKeys(obj) {
let keysArray = [];
for (let key in obj) {
keysArray.push(key);
if (typeof obj[key] === "object")
keysArray.push(...getKeys(obj[key]))
}
return keysArray
}
const input={lamp:2,candle:2,pillow:{big:2,small:4},bathroom:{toilet:1,shower:{shampoo:1,conditioner:2}}}
console.log(getKeys(input))
FYI: typeof null is "object". So, the above code will throw an error if any of the properties are null. So, Object(obj[k]) === obj[k] can be used. This is true for all objects EXCEPT for null
Also, if flatMap is supported, you could do something like this
const input={lamp:2,candle:2,pillow:{big:2,small:4},bathroom:{toilet:1,shower:{shampoo:1,conditioner:2}}};
const getKeys = obj =>
Object.keys(obj).flatMap(key => Object(obj[key]) === obj[key]
? [key, ...getKeys(obj[key])]
: key)
console.log(getKeys(input))
How about:
const keys = obj => Object.keys(obj).reduce((acc, key) => {
acc.push(key);
return (obj[key] !== null && typeof obj[key] === 'object') // Avoid evaluating null as an object
? acc.concat(keys(obj[key]))
: acc;
}, []);
Usage:
keys({foo: 1, bar: {foobar: 2}}); // Outputs ['foo', 'bar', 'foobar']
A very good use-case for generators. Here's a demonstration -
const data =
{lamp: 2, candle: 2, pillow: {big: 2, small: 4}, bathroom: {toilet: 1, shower: {shampoo: 1, conditioner: 2}}}
const keys = function* (o = {}) {
if (Object(o) === o)
for (const [k, v] of Object.entries(o)) {
yield k
yield* keys(v)
}
}
console.log(Array.from(keys(data)))
// [lamp, candle, pillow, big, small, bathroom, toilet, shower, shampoo, conditioner]
Another solution is to use Array.prototype.flatMap -
const data =
{lamp: 2, candle: 2, pillow: {big: 2, small: 4}, bathroom: {toilet: 1, shower: {shampoo: 1, conditioner: 2}}}
const keys = (o = {}) =>
Object(o) === o
? Object.entries(o).flatMap(([ k, v ]) =>
[ k, ...keys(v) ]
)
: []
console.log(keys(data))
// [lamp, candle, pillow, big, small, bathroom, toilet, shower, shampoo, conditioner]

(_.merge in ES6/ES7)Object.assign without overriding undefined values

There is _.merge functionality in lodash. I want to achieve the same thing in ES6 or ES7.
Having this snippet:
Object.assign({}, {key: 2}, {key: undefined})
I want to receive {key: 2}. Currently I receive {key: undefined}
This is NOT a deep merge.
Is it possible? If yes then how to achieve that?
You can't achieve that with a straight usage of Object.assign, because each next object will rewrite the same keys for prev merge. The only way, to filter your incoming objects with some hand-crafted function.
function filterObject(obj) {
const ret = {};
Object.keys(obj)
.filter((key) => obj[key] !== undefined)
.forEach((key) => ret[key] = obj[key]);
return ret;
}
You can simply filter out the keys with undefined values before passing them to Object.assign():
const assign = (target, ...sources) =>
Object.assign(target, ...sources.map(x =>
Object.entries(x)
.filter(([key, value]) => value !== undefined)
.reduce((obj, [key, value]) => (obj[key] = value, obj), {})
))
console.log(assign({}, {key: 2}, {key: undefined}))
Write a little utility to remove undefined values:
function removeUndefined(obj) {
for (let k in obj) if (obj[k] === undefined) delete obj[k];
return obj;
}
Then
Object.assign({}, {key: 2}, removeUndefined({key: undefined}))
This seems preferable to writing your own assign with wired-in behavior to remove undefined values.
use lodash to omit nil values and then combine the two objects into one via spread
{ ...(omitBy({key: 2}, isNil)), ...(omitBy({key: undefined}, isNil))}
See more info on lodash here https://lodash.com/docs/4.17.15
With ES2019/ES10's new object method, Object.fromEntries(), MichaƂ's answer can be updated:
const assign = (target, ...sources) =>
Object.assign(target, ...sources.map(x =>
Object.fromEntries(
Object.entries(x)
.filter(([key, value]) => value !== undefined)
)
))
console.log(assign({}, {key: 2}, {key: undefined}))
If you just need the values and don't need an object, you could also use object destructuring:
const input = { a: 0, b: "", c: false, d: null, e: undefined };
const { a = 1, b = 2, c = 3, d = 4, e = 5, f = 6 } = input;
console.log(a, b, c, d, e, f);
// => 0, "", false, null, 5, 6
This will only override absent or undefined values.
I often use this for function argument default values like this:
function f(options = {}) {
const { foo = 42, bar } = options;
console.log(foo, bar);
}
f();
// => 42, undefined
f({})
// => 42, undefined
f({ foo: 123 })
// => 123, undefined
f({ bar: 567 })
// => 42, 567
f({ foo: 123, bar: 567 })
// => 123, 567

Categories