Setting an object's nested key / values by arbitrary number of parameters - javascript

Suppose I have some object like:
const someObj = {
x: null,
y: {
z: null
},
a: {
b: {
c: null
}
}
}
I would like to create a function to set values using something like:
const setKV = (obj, ...keyArray) => {
/* Not quite sure how to phrase this function */
const val = keyArray.pop()
}
Such that I can set the values:
x with setKV(someObj, 'x', true)
z with setKV(someObj, 'y', 'z', true)
c with setKV(someObj, 'a', 'b', 'c', true)
How would I define an object's nested key by this arbitrary number of parameters?

You can do this easily using a rest parameter and spread argument like ...rest below. setKV does not mutate its input object, o; a new object is always returned.
const setKV = (o = {}, key, value, ...rest) =>
rest.length === 0
? { ...o, [key]: value }
: { ...o, [key]: setKV (o[key], value, ...rest) }
console.log
( setKV ({ a: 0 }, 'b', 1)
// { a: 0, b: 1 }
, setKV ({ a: { b: { c: false } } }, 'a', 'b', 'c', true)
// { a: { b: { c: true } } }
, setKV ({}, 'a', 'b', 'c', 1)
// { a: { b: { c: 1 } } }
, setKV ({ a: { b: { c: 0 } } }, 'a', 'b', 'd', 0)
// { a: { b: { c: 0, d: 0 } } }
, setKV ({ a: { b: { c: 0 } } }, 'a', 'b', 1)
// { a: { b: 1 } }
, setKV ({ a: 1, b: { c: 2, d: { e: 3 } } }, 'b', 'd', 'e', { f: 4 })
// { a: 1, b: { c: 2, d: { e: { f: 4 } } } }
, setKV ({ a: 0 }, 'b')
// { a: 0, b: undefined }
)
"If I did want to mutate the input object ..."
While mutations should be avoided, the specific needs of your program may warrant their use. In such a case, review mutKV, if only to see how it differs from the implementation above
const mutKV = (o = {}, key, value, ...rest) =>
rest.length === 0
? (o[key] = value, o)
: (o[key] = mutKV (o[key], value, ...rest), o)
const data =
{ a: 0 }
mutKV (data, 'b', 1)
console.log (data)
// { a: 0, b: 1 }
mutKV (data, 'c', 'd', 2)
console.log (data)
// { a: 0, b: 1, c: { d: 2 } }
mutKV (data, 'c', 'd', 3)
console.log (data)
// { a: 0, b: 1, c: { d: 0 } }
mutKV (data, 'c', 4)
console.log (data)
// { a: 0, b: 1, c: 4 }
This opens the book for a short lesson about side effects, and encoding them using effect. Below we create an effect mut using effect, then mut is used in each branch of mutKV. The behavior of the program is identical to mutKV above.
const effect = f => x =>
(f (x), x)
const mut = (key, value) =>
effect (o => o[key] = value)
const mutKV = (o = {}, key, value, ...rest) =>
rest.length === 0
? mut (key, value) (o)
: mut (key, mutKV (o[key], value, ...rest)) (o)

lodash provides what you're looking for, no need to rewrite it
https://lodash.com/docs/4.17.10#set
_.set(someObj, ['a', 'b', 'c'], true);

We can create function where rest params are two element arrays where first item is key name and secound is desired value
function setKV(obj, ...kvs) {
return kvs.reduce((obj, [k, v]) => {
obj[k] = v;
return obj;
}, obj);
}
function setKV(obj, ...kvs) {
return kvs.reduce((obj, [k, v]) => {
obj[k] = v;
return obj;
}, obj);
}
console.log(setKV({}, [
"a",
1
], [
"b",
"b"
]));

Related

How to sum the values in this object of objects in javascript?

const ob = {
a: 1,
b: {
c: 3,
d: 6,
e: {
f: {
g: 3,
h: {
i: 5,
j: {
k: 7
}
}
}
}
}
};
Any methods to solve this code?
I have no idea how to solve this code.
For abovementioned input I would expect a result of 1 + 3 + 6 + 3 + 5 + 7 = 25. So what I want to return from a function sumObject(ob) is: 25
You can try reduce with recursion
The condition for the sum is
If the current value is a number, sum it with result
If the current value is not a number (in your case, it's an object), call the function sumObject recursively with current result
const ob = {
a: 1,
b: {
c: 3,
d: 6,
e: {
f: {
g: 3,
h: {
i: 5,
j: {
k: 7
}
}
}
}
}
};
function sumObject(data, result = 0) {
return Object.values(data).reduce((sum, value) => typeof value === 'number' ? sum + value : sumObject(value, sum), result)
}
console.log(sumObject(ob))
If you don't understand some of the other answers, this is an easier solution to understand:
function sumObject(obj){
let result = 0;
for(let i of Object.values(obj)){ //we iterate through all values in obj
if(typeof i == "number"){ //if the current value of i is a number
result+=i; //then add that to the result
} else { //otherwise, it will be an object
result+=sumObject(i); //so we call the function on itself to iterate over this new object
}
}
return result; //finally, we return the total
}
console.log(sumObject(ob));
You can do this to extract all values into an array of numbers and then sum that array:
const getObjectValues = (obj) => (obj && typeof obj === 'object')
? Object.values(obj).map(getObjectValues).flat()
: [obj]
let nums = getObjectValues(ob)
let sum = nums.reduce((a, b) => a + b, 0)
You can recursively sum the value of each object using array#reduce and Object.values()
const ob = { a: 1, b: { c: 3, d: 6, e: { f: { g: 3, h: { i: 5, j: { k: 7 } } } } } },
getSum = o =>
Object.values(o).reduce((s, v) => {
s += typeof v === 'object' ? getSum(v): v;
return s;
}, 0);
console.log(getSum(ob));
Try recursion. This will support any amount of nested object at any level of nesting.
const sumAllNumbers = (object, total = 0) => {
const nextValues = [];
for (const value of Object.values(object)) {
if (typeof value === 'number') total += value;
if (typeof value === 'object') nextValues.push(Object.values(value));
}
if (!nextValues.length) return total;
return sumAllNumbers(nextValues.flat(), total);
};
// adds up to 25
const testCase1 = {
a: 1,
b: {
c: 3,
d: 6,
e: {
f: {
g: 3,
h: {
i: 5,
j: {
k: 7,
},
},
},
},
},
};
// adds up to 30
const testCase2 = {
a: 1,
b: 2,
c: {
a: 1,
b: 2,
c: 3,
d: {
a: 5,
b: {
c: {
d: {
e: 1,
},
},
},
},
},
d: {
g: {
f: {
c: 10,
},
},
},
e: {
f: {
a: 1,
g: {
a: 4,
},
},
},
};
// Your original test case
console.log(sumAllNumbers(testCase1));
// My much more demanding test case
console.log(sumAllNumbers(testCase2));
You could get the value of the objects and check if the value is an object, then take the result of the nested objects or call the handed over sum function for accumulator and actual value.
This approach works with a function which takes
an object
an accumulator function
a start value
This function works as well for getting all values with different accumulator function and an array as startValue.
const
reduce = (object, fn, startValue) => Object
.values(object)
.reduce(
(r, v) => v && typeof v === 'object' ? reduce(v, fn, r) : fn(r, v),
startValue
),
data = { a: 1, b: { c: 3, d: 6, e: { f: { g: 3, h: { i: 5, j: { k: 7 } } } } } },
total = reduce(data, (a, b) => a + b, 0);
console.log(total);
const ob = {
a: 1,
b: {
c: 3,
d: 6,
e: {
f: {
g: 3,
h: {
i: 5,
j: {
k: 7
}
}
}
}
}
};
const sum = data =>
Object
.keys(data)
.reduce((a,b) => a + (typeof(s = data[b]) == "number" ? s : sum(s)), 0);
console.log(sum(ob))

Find duplicate or falsy value in a JS object - Javascript

I have an object where it can contain a duplicate and/or a falsy value. I want to compose an array of objects based on that and add a new boolean property based on the check for case-insensitive values.
This is what I have:
const obj = {
a: 'A',
b: 'B',
c: 'C',
d: 'c',
e: 'E',
f: ''
}
console.log(Object.keys(obj).map(i => {
return {
key: i,
isDuplicateOrFalsy: _.filter(
Object.values(obj),
j =>
_.trimEnd(_.toLower(j)) ===
_.trimEnd(
_.toLower(
obj[i]
)
)
).length > 1 ||
!_.every(
Object.values(obj),
Boolean
)
}
}))
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
Expected Output:
[{
isDuplicateOrFalsy: false,
key: "a"
}, {
isDuplicateOrFalsy: false,
key: "b"
}, {
isDuplicateOrFalsy: true,
key: "c"
}, {
isDuplicateOrFalsy: true,
key: "d"
}, {
isDuplicateOrFalsy: false,
key: "e"
}, {
isDuplicateOrFalsy: true,
key: "f"
}]
Please advice.
Convert the object to entries of [key, value] with _.toPairs(), and group them by the lower case version of the value. Flat map the groups, and map each entry in the group back to an object. Any item within a group with length greater than 1 is a duplicate. Merge the objects, and get the items in the correct order using _.at():
const fn = obj => _.at(
_.merge(..._.flatMap(
_.groupBy(_.toPairs(obj), ([, v]) => _.lowerCase(v)),
group => group.map(([key, v]) => ( { [key]:{
key,
isDuplicateOrFalsy: group.length > 1 || _.isEmpty(_.trim(v))
}}))
)),
_.keys(obj)
)
const obj = {"a":"A","b":"B","c":"C","d":"C","e":"E","f":"","g":"c"}
const result = fn(obj)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
You could do something similar to this:
const obj = { a: 'A', b: 'B', c: 'C', d: 'C', e: 'E', f: '' };
const res = Object.entries(obj)
.map(([key, val], i, arr) => ({
key,
isDuplicateOrFalsy: !val ||
arr.some(([k, v], j) =>
j !== i && v.toLowerCase().trim() === val.toLowerCase().trim()
)
}));
console.log(res);
Solution does not contain unnecessary cycles:
const obj = {
a: 'A',
b: 'B',
c: 'C',
d: 'C',
e: 'E',
f: ''
}
// make array [ [key, value], ... ] and sort by values
values = Object.entries(obj).sort((a,b) => a[1] > b[1])
result = values.map((e,i, arr) => {
const [key, value] = e;
const last = values.length -1; // last index
let isDuplicateOrFalsy = false;
// true conditions = dublicates are near
if (!value) isDuplicateOrFalsy = true; // falsy check
else if (i > 0 && i < last // for middle
&& (arr[i-1][1] === value || arr[i+1][1] === value)) isDuplicateOrFalsy = true;
else if (i === 0 && arr[1][1] === value) isDuplicateOrFalsy = true; // for first
else if (i === last && arr[last-1][1] === value) isDuplicateOrFalsy = true; // for last
return {
key,
isDuplicateOrFalsy
}
})
console.log(result)
const obj = {
a: 'A',
b: 'B',
c: 'C',
d: 'c',
e: 'E',
f: ''
};
const isDuplicateOrFalsyByValue = Object
.values(obj)
.reduce(
(result, value) => {
const caseInsensetiveValue = value.toLowerCase();
result[caseInsensetiveValue] = result[caseInsensetiveValue] === undefined
/*
* If `caseInsensetiveValue` is a falsy value,
then set `isDuplicateOrFalsy` to `true`
* Otherwise set it to `false`
*/
? !caseInsensetiveValue
/*
* If result[caseInsensetiveValue] is `true` (we had a falsy value),
then this `true` won't hurt
* Otherwise we have a duplicate at this point
and should set it to `true` as well.
*/
: true;
return result;
},
{},
);
const keysWithDuplicationOrFalsyInfo = Object
.entries(obj)
.reduce(
(result, [key, value]) => [
...result,
{
isDuplicateOrFalsy: isDuplicateOrFalsyByValue[value.toLowerCase()],
key,
},
],
[],
);
console.log('keysWithDuplicationOrFalsyInfo');
console.log(keysWithDuplicationOrFalsyInfo);
A short, and more human readable.
const obj = {
a: 'A',
b: 'B',
c: 'C',
d: 'c',
e: 'E',
f: ''
}
// Object map number of occurance of each value. { a: 1, b: 1, c: 2, d: 1 }
const valuesOccurance = _.mapValues(_.groupBy(obj, _.lowerCase), occurances => occurances.length);
// function to check duplicate
const isDuplicate = value => valuesOccurance[_.lowerCase(value)] > 1;
// function to check falsy value
const isFalsy = value => !value;
const result = _.map(obj, (value, key) => {
return {
isDuplicateOrFalsy: isFalsy(value) || isDuplicate(value),
key,
};
});
console.log({ result })
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>

Cleaner way to filter properties from object

I created a function to remove the properties I tell him to
function trimProperties(data, properties) {
return data.map(o => {
Object.keys(o).forEach(k => {
if (properties.includes(k)) {
delete o[k];
}
});
return o;
});
}
My use case is usually like this
let array = [
{
a: 'A',
b: 'B',
c: 'C'
},
{
a: 'A2',
b: 'B2',
c: 'C2'
}
]
// Remove every property 'b' or 'c' from the objects inside the array
trimProperties(array, ['b','c']);
My question is simple, how can I make this function faster, because my array sometimes can get pretty big since it's the result set from a database access
delete cause indexes recalculation all the time, creating new array would be faster
let array = [
{
a: 'A',
b: 'B',
c: 'C'
},
{
a: 'A2',
b: 'B2',
c: 'C2'
}
]
function trimProperties(data, properties) {
let i = 0;
const result = []
while (i < data.length) {
var o = {};
Object.keys(data[i]).forEach(k => {
if (!properties.includes(k)) {
o[k] = data[i][k];
}
})
i++;
if (Object.keys(o).length) {
result.push(o);
}
}
return result;
}
// Remove every property 'b' or 'c' from the objects inside the array
console.log(trimProperties(array, ['b','c']));
A one liner:
array.map(o => Object.fromEntries(Object.entries(o).filter(([k,v]) => !['b','c'].includes(k))))
Demo:
const array = [
{
a: 'A',
b: 'B',
c: 'C'
},
{
a: 'A2',
b: 'B2',
c: 'C2'
}
];
const excluded = ['b','c'];
const filtered = array.map(o => Object.fromEntries(Object.entries(o).filter(([k,v]) => !excluded.includes(k))));
console.log(filtered)

Find depth of object in iterative way

I am writing a function for calculating the depth of an object.
Here is my recursive version which seems to work as expected:
function findDepth(obj, firstCall = true) {
if (firstCall && typeof obj !== "object") {
return -1;
}
return Object.keys(obj).reduce((max, k) => {
if (typeof obj[k] === "object" && obj[k] !== null) {
const val = findDepth(obj[k], false) + 1;
if (val > max) {
max = val;
}
}
return max;
}, 1);
}
const input1 = {
a: {
b: "test",
c: {
d: {
e: {
f: [1, 2, 3],
g: {
a: null,
z: {
d: "casdsadasdsa",
q: {
z: {
i: undefined
}
}
}
}
}
},
c: {
a: "sad"
}
},
d: {
e: 5
}
},
b: {
c: {
d: "dsada"
}
}
};
const input2 = {
w: {
d: "hello",
f: {
g: "dsadas",
z: {
b: "dsajkdasjldk",
q: {
w: {
z: "dsajkdasjdla"
}
}
},
h: "dsiaodsiad"
}
},
a: "test",
b: "test2",
c: {
d: "hello",
f: {
g: "dsadas",
z: {
b: "dsajkdasjldk"
},
h: "dsiaodsiad"
}
},
e: "bye"
};
console.log(findDepth(input1));
console.log(findDepth(input2));
Now I am trying to write an iterative version, but I cannot find the best way to make the loop work.
function findDepthIterative(obj) {
if (typeof obj !== "object") {
return -1;
}
let max = 1;
let copy = Object.assign({}, obj);
let keys = Object.keys(copy);
while (keys.length) {
if (typeof copy[keys[0]] !== "object" && copy[keys[0]] !== null) {
delete copy[keys[0]];
keys = Object.keys(copy);
} else {
max++;
copy = Object.assign({}, copy[keys[0]]);
keys = Object.keys(copy);
}
}
return max;
}
const input1 = {
a: {
b: "test",
c: {
d: {
e: {
f: [1, 2, 3],
g: {
a: null,
z: {
d: "casdsadasdsa",
q: {
z: {
i: undefined
}
}
}
}
}
},
c: {
a: "sad"
}
},
d: {
e: 5
}
},
b: {
c: {
d: "dsada"
}
}
};
const input2 = {
w: {
d: "hello",
f: {
g: "dsadas",
z: {
b: "dsajkdasjldk",
q: {
w: {
z: "dsajkdasjdla"
}
}
},
h: "dsiaodsiad"
}
},
a: "test",
b: "test2",
c: {
d: "hello",
f: {
g: "dsadas",
z: {
b: "dsajkdasjldk"
},
h: "dsiaodsiad"
}
},
e: "bye"
};
console.log(findDepthIterative(input1));
console.log(findDepthIterative(input2));
As you can see from the output and the code, it just takes the first property inside the loop:
while (keys.length) {
if (typeof copy[keys[0]] !== "object" && copy[keys[0]] !== null) {
delete copy[keys[0]];
keys = Object.keys(copy);
} else {
max++;
copy = Object.assign({}, copy[keys[0]]);
keys = Object.keys(copy);
}
}
The idea was to delete the property each iteration, but I am not getting in the right way.
I tried to change it with copy[keys[keys.length - 1]] but in this way it takes only the last property instead.
Actually the issue is how to loop over all the keys, on all the depth levels, as in the recursive version.
Any suggestion about how to implement this algorithm in an iterative way?
Even any suggestion on how to improve the recursive one (or if I am missing something) is more than welcome.
p.s. NO LOADASH, UNDERSCORE, RAMDA, or whatever. Just Vanilla JS
You just need to keep a stack and push children into it while keeping track of the current depth. You can keep track of that by pushing an array of [depth, obj] into the stack and then when you pop() add one to the depth before pushing children.
const input1 = {w: {d: "hello",f: {g: "dsadas",z: {b: "dsajkdasjldk",q: {w: {z: "dsajkdasjdla"}}},h: "dsiaodsiad"}},a: "test",b: "test2",c: {d: "hello",f: {g: "dsadas",z: {b: "dsajkdasjldk"},h: "dsiaodsiad"}},e: "bye"};
function findDepthIterative(obj) {
if (typeof obj !== "object") {
return -1;
}
let max = 0;
// current depth, children
let stack = [[0, Object.values(obj)]];
while(stack.length){
let [depth, cur] = stack.pop()
if (depth > max) max = depth
if (typeof cur === "object" && cur !== null){
Object.values(cur).forEach(c => stack.push([depth+1, c]))
}
}
return max
}
console.log(findDepthIterative(input1))
// sanity check:
const depth0 = {}
const depth1 = {a:1}
const depth2 = {a:{b:2}}
console.log(findDepthIterative(depth0))
console.log(findDepthIterative(depth1))
console.log(findDepthIterative(depth2))
One way to iterate could be a depth first search using a stack.
function f(obj){
let stack = Object.keys(obj).map(k => [obj[k], 1]);
let max = 0;
while (stack.length){
let [maybeObj, d] = stack.pop();
max = Math.max(max, d);
if (typeof maybeObj == 'object' && maybeObj !== null)
Object.keys(maybeObj).map(k =>
stack.push([maybeObj[k], d + 1]));
}
return max;
}
We could also make the recursion slightly more succinct:
function f(obj){
if (typeof obj !== 'object' || obj === null)
return 0;
return Object.keys(obj).reduce((acc, k) =>
Math.max(acc, 1 + f(obj[k])), 0);
}
You should use an array instead of the object which won't work properly if there are duplicate keys. The array should contain all the objects that occur at a certain level. For each iteration, you map the array into a new one containing the previous objects' direct children:
function findDepthIterative(obj) {
if (typeof obj !== "object") {
return -1;
}
let arr = [obj]; // the array containing the objects that occur at a certain level, initially contains obj being the only object at the first level
let levels = 0; // levels counter
do { // keep doing this
levels++; // increment the levels counter
let newArr = []; // make a new array for the next level
arr.forEach(obj => { // populate it with the old level objects' children
for(let key in obj) {
if(obj[key] && typeof obj[key] === "object") {
newArr.push(obj[key]);
}
}
});
arr = newArr; // make arr the new array of object (next level)
} while (arr.length); // while there are still levels with objects in them
return levels;
}
Demo:
function findDepthIterative(obj) {
if (typeof obj !== "object") {
return -1;
}
let arr = [obj];
let levels = 0;
do {
levels++;
let newArr = [];
arr.forEach(obj => {
for(let key in obj) {
if(obj[key] && typeof obj[key] === "object") {
newArr.push(obj[key]);
}
}
});
arr = newArr;
} while (arr.length);
return levels;
}
const input1 = {
a: {
b: "test",
c: {
d: {
e: {
f: [1, 2, 3],
g: {
a: null,
z: {
d: "casdsadasdsa",
q: {
z: {
i: undefined
}
}
}
}
}
},
c: {
a: "sad"
}
},
d: {
e: 5
}
},
b: {
c: {
d: "dsada"
}
}
};
const input2 = {
w: {
d: "hello",
f: {
g: "dsadas",
z: {
b: "dsajkdasjldk",
q: {
w: {
z: "dsajkdasjdla"
}
}
},
h: "dsiaodsiad"
}
},
a: "test",
b: "test2",
c: {
d: "hello",
f: {
g: "dsadas",
z: {
b: "dsajkdasjldk"
},
h: "dsiaodsiad"
}
},
e: "bye"
};
console.log(findDepthIterative(input1));
console.log(findDepthIterative(input2));

How to change key name with map into nested object?

Here is the object
{
a: 1,
b: {
c: {
d: 2
},
e: 3
}
}
Here is the map
{
'a': 'aaa',
'b': 'bbb',
'b.c.d': 'bcd'
}
Here is the expected result.
{
aaa: 1,
bbb: {
c: {
bcd: 2
},
e: 3
}
}
I know there's a function in lodash _.get could get the value like b.c.d.
But how can I change the key name with the map?
You can do this recursively by keeping track of the current path and building a key into the map with that:
let o = {a: 1,b: {c: {d: 2},e: 3}}
let map = {
'a': 'aaa',
'b': 'bbb',
'b.c.d': 'bcd'
}
function makeObj(obj, map, p=[]) {
let ret = {}
Object.entries(obj).forEach(([k, v]) => {
let path = p.concat(k) // add onto current path
let mapKey = map[path.join('.')] || k
ret[mapKey] = (typeof v === 'object')
? makeObj(v, map, path) // if an object recurse and pass on the current path
: v // otherwise set the value
})
return ret
}
console.log(makeObj(o, map))

Categories