I wanted to contribute a fix for this to the lodash repo but thought I should check that I'm not doing something wrong first...
When I use standard lodash in this example...
const _ = require('lodash');
run = () => {
const mapping = {
a: 'hello',
b: 'world'
};
const object = {
a: { value: true },
b: { value: false }
};
// this is the original transform function that takes three arguments...
// 1. the object to be transformed
// 2. the transform function
// 3. the accumulator object
const transformed = _.transform(
object,
(a, v, k, o) => { a[mapping[k]] = _.get(v, 'value'); },
{}
);
console.log(transformed);
};
run();
The output is { hello: true, world: false } like I expected.
When logging a, v, k, and o on the above code the output is...
1 a: {}
1 v: { value: true }
1 k: a
1 o: { a: { value: true }, b: { value: false } }
2 a: { hello: true }
2 v: { value: false }
2 k: b
2 o: { a: { value: true }, b: { value: false } }
However, when I run (what I think is) the equivalent code using lodash/fp...
const _ = require('lodash/fp');
run = () => {
const mapping = {
a: 'hello',
b: 'world'
};
const object = {
a: { value: true },
b: { value: false }
};
// this is the fp transform function that takes two arguments...
// 1. the transform function
// 2. the accumulator object
// it then returns a function that takes one argument...
// 1. the object to be transformed
const transformed = _.transform(
(a, v, k, o) => { a[mapping[k]] = _.get('value')(v); },
{}
)(object);
console.log(transformed);
};
run();
The output is { undefined: false }. This is because the parameters to the iterator do not seem to be correct. When I log a, v, k, and o I get the following output...
1 a: {}
1 v: { value: true }
1 k: undefined
1 o: undefined
2 a: { undefined: true }
2 v: { value: false }
2 k: undefined
2 o: undefined
Am I doing something wrong or is this not working as expected?
I have also added this as an issue on the repo but thought I'd add here as maybe I would get a faster response :D
https://github.com/lodash/lodash/issues/4381
Try this:
const transform = _.transform.convert({ cap: false });
const transformed = transform(
(a, v, k, o) => { a[mapping[k]] = _.get('value')(v); },
{}
)(object);
pen
OK, it's very odd but I can get it working.
When using lodash/fp the autocompletion of the iteratee shows the function signature like...
function(accumulator, currentValue) {}
So it will only accept two arguments (which is what is said in the documentation linked to by #charlietfl).
However, when I create the function like this...
function(a, v) {
console.log(arguments.length);
}
It outputs 4.
So I kept exploring...
function(a, v) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
console.log(arguments[3]);
}
This outputs...
1 a: {}
1 v: { value: true }
1 k: a
1 o: { a: { value: true }, b: { value: false } }
2 a: {}
2 v: { value: false }
2 k: b
2 o: { a: { value: true }, b: { value: false } }
Just like I wanted it to. So, even though the function is only declared with 2 arguments and only accepts 2 parameters, 4 arguments are actually passed into it.
So, when I update the code like this...
function(a, v) {
const k = arguments[2];
a[mapping[k]] = v.value;
}
This returns { hello: true, world: false } which is what I was hoping to get in the first place.
I can get around it this way but don't understand why this is happening. So, because of that I'm going to stick with my current method of mapKeys followed by mapValues and TBH... I'll probably be looking at dropping lodash in the near future and writing my own functions to replace this functionality.
Related
I want to remove all empty objects from another object by comparing it with another. Example of this would be:
We have default object like:
defaultObj = {
a: {},
b: {},
c: {
d: {}
}
};
And target object like this:
targetObj = {
a: { x: {} },
b: {},
c: {
d: {},
e: {}
},
f: {}
};
Now, I need to perform operation on targetObj by comparing it with defaultObj, and remove all objects that remain empty, but leave every object in targetObj that weren't originally in default.
Result of operation should look like this:
result = {
a: { x: {} },
c: {
e: {}
},
f: {}
}
Here is a solution that you can use which recursively iterates an object and removes the empty properties as defined in your use case. Make sure to create a deep copy of the object first (as shown in the example) so that the original does not get manipulated:
const defaultObj = {
a: {},
b: {},
c: {
d: {}
}
};
const targetObj = {
a: { x: {} },
b: {},
c: {
d: {},
e: {}
},
f: {}
};
// traverse the object
function removeEmptyObjectProperties(targetObject, defaultObject) {
Object.keys(targetObject).forEach((key) => {
if (defaultObject.hasOwnProperty(key)) {
// checks if it is a json object and has no keys (is empty)
if (targetObject.constructor === ({}).constructor && Object.keys(targetObject[key]).length === 0) {
delete targetObject[key];
} else {
// iterate deeper
removeEmptyObjectProperties(targetObject[key], defaultObject[key]);
}
}
})
}
// deep copy to remove the reference
const targetObjDeepCopy = JSON.parse(JSON.stringify(targetObj));
// execute
removeEmptyObjectProperties(targetObjDeepCopy, defaultObj);
// result
console.log(targetObjDeepCopy)
I ran into this problem, I was able to write solution which can handle array of object (not posted here) or one level deep nested object but i couldn't solve when the given object has nested structure like below. I am curious to know how we can solve this.
const source = {
a: 1,
b: {
c: true,
d: {
e: 'foo'
}
},
f: false,
g: ['red', 'green', 'blue'],
h: [{
i: 2,
j: 3
}]
};
solution should be
const solution = {
'a': 1,
'b.c': true,
'b.d.e': 'foo',
'f': false,
'g.0': 'red',
'g.1': 'green',
'g.2': 'blue',
'h.0.i': 2,
'h.0.j': 3
};
attempt for one deep nested object
let returnObj = {}
let nestetdObject = (source, parentKey) => {
let keys = keyFunction(source);
keys.forEach((key) => {
let values = source[key];
if( typeof values === 'object' && values !== null ) {
parentKey = keys[0]+'.'+keyFunction(values)[0]
nestetdObject(values, parentKey);
}else{
let key = parentKey.length > 0 ? parentKey : keys[0];
returnObj[key] = values;
}
})
return returnObj
};
// Key Extractor
let keyFunction = (obj) =>{
return Object.keys(obj);
}
calling the function
nestetdObject(source, '')
But my attempt will fail if the object is like { foo: { boo : { doo : 1 } } }.
You should be able to do it fairly simply with recursion. The way it works, is you just recursively call a parser on object children that prepend the correct key along the way down. For example (not tested very hard though):
const source = {
a: 1,
b: {
c: true,
d: {
e: 'foo'
}
},
f: false,
g: ['red', 'green', 'blue'],
h: [{
i: 2,
j: 3
}]
}
const flatten = (obj, prefix = '', res = {}) =>
Object.entries(obj).reduce((r, [key, val]) => {
const k = `${prefix}${key}`
if(typeof val === 'object'){
flatten(val, `${k}.`, r)
} else {
res[k] = val
}
return r
}, res)
console.log(flatten(source))
I am very late to the party but it can be easily achieved with a module like Flatify-obj.
Usage:
const flattenObject = require('flatify-obj');
flattenObject({foo: {bar: {unicorn: '🦄'}}})
//=> { 'foo.bar.unicorn': '🦄' }
flattenObject({foo: {unicorn: '🦄'}, bar: 'unicorn'}, {onlyLeaves: true});
//=> {unicorn: '🦄', bar: 'unicorn'}
// Licensed under CC0
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
const source = {
a: 1,
b: { c: true, d: { e: "foo" } },
f: false,
g: ["red", "green", "blue"],
h: [{ i: 2, j: 3 }],
};
function flatten(source, parentKey, result = {}) {
if (source?.constructor == Object || source?.constructor == Array) {
for (const [key, value] of Object.entries(source)) {
flatten(
value,
parentKey != undefined ? parentKey + "." + key : key,
result
);
}
} else {
result[parentKey] = source;
}
return result;
}
console.log(flatten(source));
one more simple example with Object.keys
const apple = { foo: { boo : { doo : 1 } } }
let newObj = {}
const format = (obj,str) => {
Object.keys(obj).forEach((item)=>{
if(typeof obj[item] ==='object'){
const s = !!str? str+'.'+item:item
format(obj[item],s)
} else {
const m = !!str?`${str}.`:''
newObj[m+item]= obj[item]
}
})
}
format(apple,'')
console.log(newObj)
If there is a Javascript object with multiple levels, as in:
myObject = {
a: 12,
obj11: {
obj111: 'John',
b:13,
obj1111: { a:15, b: 35 }
},
obj21: {
a:15,
b:16 }
}
I want to write a function to which is passed the object and an array of keys. The function should return a value based upon these keys. For example, passing [obj11,b] should return 13. Passing [obj11, obj1111,a] should return 15. Passing obj21 should return the object {a:15, b:16}
function (myObj,myArr) {
return keyVal;
}
Assuming that the keys are always correct, can anyone help me solve this problem?
You could reduce the array with the keys and take an object as default value.
function getValue(object, path) {
return path.reduce(function (r, k) {
return (r || {})[k];
}, object);
}
var object = { a: 12, obj11: { obj111: 'John', b: 13, obj1111: { a: 15, b: 35 }, obj21: { a: 15, b: 16 } } };
console.log(getValue(object, ['obj11', 'b']));
console.log(getValue(object, ['obj11', 'obj1111', 'a']));
console.log(getValue(object, ['obj11', 'obj21']));
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can do this with a reduce() one-liner. Of course if you want to wrap it in a function, you can do that too.
var myObject = {
a: 12,
obj11: {
obj111: 'John',
b:13,
obj1111: { a:15,
b: 35 }
},
obj21: {
a:15,
b:16 }
}
var arr = ['obj11','b']
var val = arr.reduce((acc,curr) => acc[curr], myObject)
console.log(val)
var arr = ['obj11','obj1111', 'b']
var val = arr.reduce((acc,curr) => acc[curr], myObject)
console.log(val)
To increment the discussion, if you're using lodash you can also use the function _.get to get the value like this:
_.get(myObject, ['obj111', 'obj1111'])
//returns 'John'
If you object has properties with array values you can also get by indexes
var obj = {
a: 1,
b:[{
d:4,
c:8
}]
_.get(obj, ['b','0','d'])
//returns 4
Source: https://lodash.com/docs/4.17.4#get
update: I described the problem in a wrong way and have rewritten the description completely, along with the code that works but is ugly as hell as well as limited.
Let's pretend there's an object
const input = {
a: 1,
b: '2',
c: {
d: true,
e: '4'
},
f: [{
g: 5,
h: {
i: '6'
}
}, {
g: 7,
h: {
i: '8'
}
}]
}
what I'm looking for is a collection of all possible arrangements of nested arrays, with object's keys flattened and joined with ".", like
[{
a: 1,
b: '2',
'c.d': true,
'c.e': '4',
'f.g': 5,
'f.h.i': '6'
}, {
a: 1,
b: '2',
'c.d': true,
'c.e': '4',
'f.g': 7,
'f.h.i': '8'
}]
Note that there are no keys that would have non-primitive values, for example, 'f.h' that would point at an object.
So, what I do first, is collect all the keys, and artificially add # sign to every key that points at an array item, so # kind of means "every index in that array":
function columns(data, prefix = '') {
if (_.isArray(data)) {
return columns(_.first(data), `${prefix}.#`);
} else if (_.isObject(data)) {
return _.filter(_.flatMap(_.keys(data), key => {
return _.concat(
!_.isObject(_.result(data, key)) ? `${prefix}.${key}` : null,
columns(data[key], `${prefix}.${key}`)
);
}));
} else {
return null;
}
}
console.log(columns(input)); // -> [".a", ".b", ".c.d", ".c.e", ".f.#.g", ".f.#.h.i"]
Now, I wield lodash. The leading "." in keys isn't a problem for lodash, so I just leave it as is. With lodash, I squash the object into a one-level object with weird keys:
function flattenKeys(original, keys) {
return _.mapValues(_.groupBy(_.map(keys, key => ({
key,
value: _.result(original, key)
})), 'key'), e => _.result(e, '0.value'));
}
console.log(flattenKeys(input, columns(input))) // -> {".a":1,".b":"2",".c.d":true,".c.e":"4"}
And now I run (in a very wrong way) through every array-like property of original object and produce an array of objects, setting keys like .f.#.h.i with the values of .f.0.h.i for first element, etc.:
function unfold(original, keys, iterables) {
if (!_.isArray(iterables)) {
return unfold(original, keys, _.uniq(_.map(_.filter(keys, key => /#/i.test(key)), key => _.replace(key, /\.\#.*/, ''))));
} else if (_.isEmpty(iterables)) {
return [];
} else {
const first = _.first(iterables);
const rest = _.tail(iterables);
const values = _.result(original, first);
const flatKeys = _.mapKeys(_.filter(keys, key => _.includes(key, first)));
const updated = _.map(values, (v, i) => ({
...flattenKeys(original, keys),
..._.mapValues(flatKeys, k => _.result(original, _.replace(k, /\#/, i)))
}));
return _.concat(updated, unfold(original, keys, rest));
}
}
console.log(unfold(input, columns(input))) // -> [{".a":1,".b":"2",".c.d":true,".c.e":"4",".f.#.g":5,".f.#.h.i":"6"},{".a":1,".b":"2",".c.d":true,".c.e":"4",".f.#.g":7,".f.#.h.i":"8"}]
So in the end, I only need to clean keys, which, in fact, isn't necessary in my case.
The question is, aside of ugliness of the code, how can I make it work with possible multiple array-like properties in original objects?
Now, I understand, that this question is more suitable for CodeReview StackExchange, so if somebody transfers it there, I'm okay with that.
Based on your updated structure, the following recursive function does the trick:
function unfold(input) {
function flatten(obj) {
var result = {},
f,
key,
keyf;
for(key in obj) {
if(obj[key] instanceof Array) {
obj[key].forEach(function(k) {
f = flatten(k);
for(keyf in f) {
result[key+'.'+keyf] = f[keyf];
}
output.push(JSON.parse(JSON.stringify(result))); //poor man's clone object
});
} else if(obj[key] instanceof Object) {
f = flatten(obj[key]);
for(keyf in f) {
result[key+'.'+keyf] = f[keyf];
}
} else {
result[key] = obj[key];
}
}
return result;
} //flatten
var output = [];
flatten(input);
return output;
} //unfold
Snippet:
function unfold(input) {
function flatten(obj) {
var result = {},
f,
key,
keyf;
for(key in obj) {
if(obj[key] instanceof Array) {
obj[key].forEach(function(k) {
f = flatten(k);
for(keyf in f) {
result[key+'.'+keyf] = f[keyf];
}
output.push(JSON.parse(JSON.stringify(result))); //poor man's clone object
});
} else if(obj[key] instanceof Object) {
f = flatten(obj[key]);
for(keyf in f) {
result[key+'.'+keyf] = f[keyf];
}
} else {
result[key] = obj[key];
}
}
return result;
} //flatten
var output = [];
flatten(input);
return output;
} //unfold
const input = {
a: 1,
b: '2',
c: {
d: true,
e: '4'
},
f: [{
g: 5,
h: {
i: '6'
}
}, {
g: 7,
h: {
i: '8'
}
}]
};
document.body.innerHTML+= '<pre>' + JSON.stringify(unfold(input), null, 2) + '</pre>';
I'll leave my original answer, which worked with your original structure:
var o = {a: [{b: 1, c: 2}], d: [{e: 4, f: 5}]},
keys = Object.keys(o),
result = [];
keys.forEach(function(i, idx1) {
keys.forEach(function(j, idx2) {
if(idx2 > idx1) { //avoid duplicates
for(var k in o[i][0]) {
for(var l in o[j][0]) {
result.push({
[i + '.' + k]: o[i][0][k],
[j + '.' + l]: o[j][0][l]
});
}
}
}
});
});
console.log(JSON.stringify(result));
I have two objects A
var A = {
a: 1,
b: 2,
c: 3,
d: {
0: {
e: 4,
f: 5
},
1: {
g: 6
}
}
};
var B = {
a: 1,
b: 9,
c: 3,
d: {
0: {
e: 4,
f: 8
}
}
};
I want to compare all attributes in B with corresponding attributes in A, if present. Return if their values doesn't match.
so I want to return
b: 9,
d:{
0: {
f: 8
}
}
Is there a simple soluton for this in lodash way?
EDIT : I tried Object.keys for A and B, did a intersection and then compare each key value. Its failing when I have nested attributes.
Here is a basic function that uses lodash to detect changes in simple nested objects and arrays. It is by no means comprehensive, but it works well for simple changesets.
function changes (a, b) {
if (_.isEqual(a, b)) {
return;
} else {
if (_.isArray(a) && _.isArray(b)) {
return _.reduce(b, function(array, value, index) {
value = changes(a[index], value);
if (!_.isUndefined(value)) {
array[index] = value;
}
return array;
}, []);
} else if (_.isObject(a) && _.isObject(b)) {
return _.reduce(b, function(object, value, key) {
value = changes(a[key], value);
if (!_.isUndefined(value)) {
object[key] = value;
}
return object;
}, {});
} else {
return b;
}
}
}
Here is an example of it in action: http://plnkr.co/edit/drMyGDAh2XP0925pBi8X?p=preview