I have an array of objects which I need to parse in some way. The objects have a set of common fields and some optional fields that aren't present in all objects. For the sake of the example, a,b,c are common fields and d,e,f are optional. I want to perform some action on some of the fields, e.g double the value of a and capitalize d, while leaving the rest as they were. If an object was missing one or more of the optional fields, it should remain so in the result.
The catch is that I want to do it in a purely functional way, without declaring an empty array and pushing into it.
Example input:
const input = [
{
a: 3,
b: 'test',
c: 34,
d: 'example'
},
{
a: 6,
b: 'another',
c: 0,
e: true,
f: () => {}
}
];
Expected result:
[
{
a: 6,
b: 'test',
c: 34,
d: 'EXAMPLE'
},
{
a: 12,
b: 'another',
c: 0,
e: true,
f: () => {}
}
]
What I tried so far was using map like so:
const result = input.map(x => ({
a: 2 * x.a,
d: x.d.toUppercase()
});
Or like so:
const result = input.map(x => ({
a: 2 * x.a,
b,
c,
d: x.d.toUppercase(),
e,
f
});
But this results in objects that either contain only the fields which were manipulated, or all of them, regardless if they existed in the original object.
Any suggestions on how to accomplish that?
If you want your objects to be immutable, you will need to make a copy of the objects before making the needed changes. Object.assign allows you to do this in one expression:
const result = input.map(x => Object.assign({}, x,
{ a: 2 * x.a },
('d' in x) && { d: x.d.toUpperCase() }
));
const input = [{
a: 3,
b: 'test',
c: 34,
d: 'example'
}, {
a: 6,
b: 'another',
c: 0,
e: true,
f: () => {}
}
];
const result = input.map(x => Object.assign({},
x,
{ a: 2 * x.a },
('d' in x) && { d: x.d.toUpperCase() }
));
console.log(result);
.as-console-wrapper { min-height: 100%; }
This solution requires Babel's Object rest spread transform plugin because Object Rest/Spread Properties for ECMAScript is still in the proposals stage.
Create a new object. Spread the original object into the new one. If the properties you wish to change exist in the original object, return an object with the the new property value to the spread. If not, return null (spread ignores undefined and null). The new values will override the old.
const input = [{
a: 3,
b: 'test',
c: 34,
d: 'example'
},
{
a: 6,
b: 'another',
c: 0,
e: true,
f: () => {}
}
];
const result = input.map(x => ({
...x, // spread all props of x
...('a' in x ? { a: 2 * x.a } : null), // override a if exists
...('d' in x ? { d: x.d.toUpperCase() } : null) // override d if exists
}));
console.log(result);
You could use the map method and make a check in each object if the key, whose value you want to change, exists. If so, then yuo can alter correspondingly the value of the key.
const input = [
{
a: 3,
b: 'test',
c: 34,
d: 'example'
},
{
a: 6,
b: 'another',
c: 0,
e: true,
f: () => {}
}
];
const result = input.map(function(obj){
if(obj.a){
obj.a = 2 * obj.a;
}
if(obj.d){
obj.d = obj.d.toUpperCase()
}
return obj;
});
console.log(result);
map function be still used.In this case you can check if a key a exist in the object then do the necessary operation only on it's value.
Hope this snippet will be useful
const input = [{
a: 3,
b: 'test',
c: 34,
d: 'example'
},
{
a: 6,
b: 'another',
c: 0,
e: true,
f: () => {}
}
];
input.map(function(elem, index) {
if (elem.hasOwnProperty('a')) {
elem.a = elem['a'] * 2;
}
})
console.log(input)
Inside the map function, you can first add the conditions to manipulate the input and then you need to return the modified object.
const input = [
{
a: 3,
b: 'test',
c: 34,
d: 'example'
},
{
a: 6,
b: 'another',
c: 0,
e: true,
f: () => {}
}
];
let output = input.map(o => {
if(o.a) { o.a = o.a * 2; }
if(o.d) { o.d = o.d.toUpperCase(); }
return o;
});
console.log(output)
Output::
[
{
"a": 6,
"b": "test",
"c": 34,
"d": "EXAMPLE"
},
{
"a": 12,
"b": "another",
"c": 0,
"e": true,
"f": () => {}
}
]
It might be worth googling for lenses. Many "functional" javascript libraries implement them to easily chain the kinds of data modifications you describe.
Under the hood, they might use the same Object.assign approach you've already found, but they allow for readable, reusable and easily chainable code.
A quick & dirty implementation to show the change in syntax (es6):
const propLens = prop => ({
get: obj => obj[prop],
// Return a new object with the property set to
// a value
set: (val, obj) => Object.assign({}, obj, {
[prop]: val
}),
// Return a new object with the property mapped
// by a function
over: (fn, obj) => Object.assign({}, obj, {
[prop]: fn(obj[prop])
})
});
// Helpers to create specific set/over functions
const set = (lens, val) => obj => lens.set(val, obj);
const over = (lens, fn) => obj => lens.over(fn, obj);
// Some utils for the example
const { pipe, double, toUpper } = utils();
// Now, you can define your modification by a set
// of lens + set/over combinations
const convertData = pipe(
set( propLens("d"), "added" ),
over( propLens("a"), double ),
over( propLens("b"), toUpper )
);
// Example data
const item = {a: 5, b: "hello", c: "untouched"};
// Converted copy
const convertedItem = convertData(item);
console.log("Converted:", convertedItem);
console.log("item !== convertedItem ->", item !== convertedItem);
// Utils
function utils() {
return {
pipe: (...fns) => x =>
fns.reduce((acc, f) => f(acc), x),
double: x => x * 2,
toUpper: x => x.toUpperCase()
};
}
(note that this example will run a bit slower than doing everything in one function, but you'll probably only notice for large amounts of data)
If you like the lenses-approach, I'd advice to use a well maintained and tested library (like Ramda). You'll even get more awesome features like index lenses and lens paths, and things are curried by default.
Related
I need to find an object in an array by a key and value
I'm trying to do a search on an array of objects with for in. but I can not.
My input:
findObject[{ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3 } }, { c: 3 }] //the last argument ({ c: 3 }) is the target
What I'm trying to return:
{ a: 1, b: { c: 3 } }
Note: The array of objects can have any object and the target too
You can use Array.find to find the item in the array whose values contain an object whose c property is 3:
const arr = [{ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3 } }, { c: 3 }]
const result = arr.find(e => Object.values(e).some(k => k.c == 3))
console.log(result)
What the OP is looking for is a find using deep equality (keys and values, including nested keys and values are equal). Even shallow equality is a deep topic in JS and applying it recursively is even deeper.
A fast but flawed idea is comparing JSON encodings for the two objects being compared. I wave a hand at that in the snippet, but prefer to use a utility that has thought through edge cases and probably executes fast.
function isEqual(a, b) {
// not good, but quick to code:
// return JSON.stringify(a) === JSON.stringify(b)
// lodash has put some thought into it
return _.isEqual(a, b)
}
// find the first object in array with a value deeply equal to object
function findObject(array, object) {
return array.find(el => {
return Object.values(el).some(v => isEqual(v, object))
})
}
let array = [{ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3, d: { e: "hi" }} }];
let object = { c: 3, d: { e: "hi" }};
let result = findObject(array, object);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
The OP asks to find the last object of an array as the target in the array up to that point. Adopt as follows...
// array is the array to search plus the last item is the thing to search for
let firstObjects = array.slice(0, -1)
let lastObject = array.at(-1)
let result = findObject(firstObjects, lastObject)
I have a javascript plain object like this one: {a: {b: 1} }
and I want to convert it to a dot-notation string like this a.b = 1
use case:
sending the object to a plain-text environment such as cli or as a url parameter.
It's rather hard to tell whether this is what you want, but something like this would flatten a tree of objects into a list of dotted paths...
var data = {
a: {
b: 1,
c: {
d: 8
}
},
e: {
f: {
g: 9
},
h: 10,
i: [1, 2, 3]
}
};
function toDotList(obj) {
function walk(into, obj, prefix = []) {
Object.entries(obj).forEach(([key, val]) => {
if (typeof val === "object" && !Array.isArray(val)) walk(into, val, [...prefix, key]);
else into[[...prefix, key].join(".")] = val;
});
}
const out = {};
walk(out, obj);
return out;
}
console.log(toDotList(data));
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);
Suppose I have this object:
const obj = {
a: {
b: {
c: {
d: 'hi'
}
},
g: 23
},
f: {
e: [1,2]
}
}
To change the variable "d" it without mutating it, will this:
const newObj = {...obj};
newObj.a.b.c.d = 'Bye';
return newObj;
work?
This is in regards to redux...
No – only the first level of properties of obj will be copied when you spread it with the spread operator ....
This is called shallow copy, as opposed to deep copy.
const obj = {
a: {
b: {
c: {
d: "hi"
}
},
g: 23
},
f: {
e: [1, 2]
}
};
const newObj = { ...obj };
newObj.a.b.c.d = "Bye";
console.log(obj.a.b.c.d); // Bye
As #ASDFGerge mentioned, you should play around with this to really internalize it.
But the short answer is no, your copy newObj is only a copy 1 level deep. Changing newObj.a will not modify the original, but changing newObj.a.b (or anything further down) will.
const original = {
one: {
two: {
three: 3
},
otherTwo: 2
},
otherOne: 1
}
const copy1LevelDeep = {...original};
copy1LevelDeep.one.two.three = 30;
copy1LevelDeep.one.two = {three: 300, four: 4};
copy1LevelDeep.one.otherTwo = 20;
copy1LevelDeep.one = 100;
copy1LevelDeep.otherOne = 10;
console.log(original);
console.log(copy1LevelDeep);
I'd like to merge two similar but not identical objects and override null values in one of them, if such exist. For example I'd have these two objects:
const obj1 = {
a: 1,
b: '',
c: [],
d: null
}
const obj2 = {
a: 2,
b: null,
d: 1
}
And the effect of merge should be:
const objMerged = {
a: 2,
b: '',
c: [],
d: 1
}
In other words, the most important source of data in the merged object is obj2 but it lacks some properties from obj1, so they need to be copied and also some of the obj2 values are null so they should be taken from obj1 as well.
EDIT
I tried:
_.extend({}, obj1, obj2)
and
Object.assign({}, obj1, obj2)
You could also mix and match with ES6 destructuring and lodash _.omitBy:
const obj1 = { a: 1, b: '', c: [], d: null }
const obj2 = { a: 2, b: null, d: 1 }
const result = {..._.omitBy(obj1, _.isNull), ..._.omitBy(obj2, _.isNull)}
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
You could also do it with ES6 only like this:
const obj1 = { a: 1, b: '', c: [], d: null }
const obj2 = { a: 2, b: null, d: 1 }
let omitNull = obj => {
Object.keys(obj).filter(k => obj[k] === null).forEach(k => delete(obj[k]))
return obj
}
const result = { ...omitNull(obj1), ...omitNull(obj2) }
console.log(result)
To add to this list of good answers, here's a recursive solution that will work with nested structures.
This example will merge the common properties of the dst object to the src object in all levels of nesting, leaving any properties that are not common intact.
const merge = (dst, src) => {
Object.keys(src).forEach((key) => {
if (!dst[key]) {
dst[key] = src[key];
} else if (typeof src[key] === 'object' && src[key] !== null && typeof dst[key] === 'object' && dst[key] !== null) {
merge(dst[key], src[key]);
}
});
},
/* Usage: */
src = {
prop1: '1',
prop2: {
val: 2,
}
},
dst = {
prop1: null,
prop2: {
val: null,
},
prop3: null,
};
merge(dst, src);
console.log(dst);
You can use _.mergeWith(), and in the merge callback only take the 2nd value if it's not null:
const obj1 = { a: 1, b: '', c: [], d: null }
const obj2 = { a: 2, b: null, d: 1 }
const result = _.mergeWith({}, obj1, obj2, (o, s) => _.isNull(s) ? o : s)
console.log(result)
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.11/lodash.min.js"></script>
Here is a pure JS based solution:
Iterate through the first object to replace values from second object, then add the additional values from the second object.
const obj1 = {
a: 1,
b: '',
c: [],
d: null
}
const obj2 = {
a: 2,
b: null,
d: 1
}
function mergeObjs(obj1, obj2){
const merged = {}
keys1 = Object.keys(obj1);
keys1.forEach(k1 => {
merged[k1] = obj2[k1] || obj1[k1]; // replace values from 2nd object, if any
})
Object.keys(obj2).forEach(k2 => {
if (!keys1.includes(k2)) merged[k2] = obj[k2]; // add additional properties from second object, if any
})
return merged
}
console.log(mergeObjs(obj1, obj2))
Using Lodash by create() and omitBy()
const obj1 = {"a":1,"b":"","c":[],"d":null}
const obj2 = {"a":2,"b":null,"d":1}
const objMerged = _.create(
_.omitBy(obj1, _.isNull),
_.omitBy(obj2, _.isNull)
)
console.log(objMerged)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
If you're interested in only the first level of the two objects you could do something like this:
const obj1 = {
a: 1,
b: '',
c: [],
d: null
}
const obj2 = {
a: 2,
b: null,
d: 1
}
const merged = Object.keys(obj1).concat(Object.keys(obj2)) // create an array that contains the keys of the two objects.
.filter((k, i, arr) => arr.indexOf(k) === i) // remove duplicate keys
.reduce((a, c) => {
a[c] = obj1[c] !== null ? obj1[c] : obj2[c];
return a;
}, {});
console.log(merged);
This example only check for null values, you should probably extend it to check for others like undefined, empty strings, etc.
You did it the good way using Object.assign, just remove what you don't want right before
Object.keys(obj1).forEach( k => {
if ( obj1[k] //write the condition you want
delete obj1[k]
});
var objMerged = {};
for (var kobj1 in obj1) {
for (var kobj2 in obj2) {
if (obj1[kobj1] == null && obj2[kobj1] != null)
objMerged[kobj1] = obj2[kobj1];
else if (obj2[kobj2] == null && obj1[kobj2] != null)
objMerged[kobj2] = obj1[kobj2];
}
}
//Print objMerged to display