Set JavaScript object property by traversing array - javascript

I'm looking for a way to walk an object based on an array and set the property for the last key on the object, for example:
var myArr = [ 'foo', 'bar', 'quz' ];
var myVal = 'somethingElse';
var myObj = {
foo: {
bar: {
quz: 'something'
}
}
};
I'd like to be able to change the value of the quz property to somethingElse. I've tried recursing but I feel like there's an easier way to do this.
I've been looking to lodash but can't find a method that seems to allow me to accomplish this.

You could walk the object like this:
var myArr = [ 'foo', 'bar', 'quz' ],
myVal = 'somethingElse',
myObj = {
foo: {
bar: {
quz: 'something'
}
}
};
var obj= myObj;
do {
obj= obj[myArr.shift()];
} while(myArr.length>1);
obj[myArr[0]]= 'somethingElse';
document.body.innerHTML= JSON.stringify(myObj);
Update
To address #Tomalak's concerns, and because you didn't specifically forbid a recursive solution, here's a reusable function with no side effects (other than changing the appropriate value of the object):
function setObj(obj, arr, val) {
!(arr.length-1) && (obj[arr[0]]=val) ||
setObj(obj[arr[0]], arr.slice(1), val);
}
Short-circuit evaluation prevents this from being an infinite loop.
Snippet:
var myArr = [ 'foo', 'bar', 'quz' ],
myVal = 'somethingElse',
myObj = {
foo: {
lorem: 'ignore me',
bar: {
quz: 'something'
},
other: {
quz: 'leave me be'
}
}
};
function setObj(obj, arr, val) {
!(arr.length-1) && (obj[arr[0]]=val) ||
setObj(obj[arr[0]], arr.slice(1), val);
}
setObj(myObj, myArr, 'somethingElse');
document.body.innerHTML= JSON.stringify(myObj);

var myArr = [ 'foo', 'bar', 'quz' ];
var myVal = 'somethingElse';
var myObj = {
foo: {
bar: {
quz: 'something'
}
}
};
function setHierarchcally(obj, keys, value) {
if ( !(obj && keys && keys.length) ) return;
if ( !obj.hasOwnProperty(keys[0]) ) return;
if (keys.length === 1) {
obj[keys[0]] = value;
} else {
setHierarchcally(obj[keys[0]], keys.slice(1, keys.length), value);
}
}
setHierarchcally(myObj, myArr, myVal);

getPath digs down into an object to get a property several levels deep based on an array of "paths", in your case MyArr. Use that to get the object containing the final property, and then just set it.
function getPath(obj, paths) {
return paths.reduce(function(obj, path) { return obj[path]; }, obj);
}
function setLastProperty(obj, paths, val) {
var final = paths.pop();
getPath(obj, paths) [ final ] = val;
}
setLastProperty(MyObj, MyArray, MyVal);

If you want more general object traverse, you can tweak js-travserse a little bit, as demoed in this jsfiddle I just created:
`https://jsfiddle.net/yxpx9wvL/10/
var leaves = new Traverse(myObj).reduce(function (acc, x) {
if (this.isLeaf) acc.push(x);
return acc;
}, []);
alert(leaves[0]);

Related

Aggregate same key values into an array and avoid undefined

I am trying to aggregate the same key values into an array by value.
so for example I have an array of objects, like so
const data = [{foo: true},{foo: false},{bar: true},{buzz: false}]
when they get aggregated the array transforms into
[
foo: {true: [{foo: true}], false: [{foo: false}]},
bar: {true: [{bar: true}]},
buzz: {false: [{buzz: false}]}
]
the array entries is the original object.
Now I know the keys that I want to group by..
they are foo, bar, buzz and fizz.
But fizz is not part of the original array, so the return is undefined, like so
[
foo: {true:[{foo: true}], false: [{foo: false}]},
bar: {true: [{bar: true}]},
buzz: {false: A[{buzz: false}]}
fizz: {undefined: [{foo: true},{foo: false},{bar: true},{buzz: false}]}
],
how do I reduce the original array without including the fizz value that is undefined?
code here:
let v = [];
let types = ['foo', 'bar', 'buzz', 'fizz' ]
for (let x = 0; x < types.length; x++) {
let data = data.reduce((acc, i) => {
if (!acc[i[types[x]]]) {
acc[i[types[x]]] = [i]
}
else if (Array.isArray(acc[i[types[x]]])) {
acc[i[types[x]]].push(i);
}
else if (typeof acc[i[types[x]]] === 'object') {
acc[i[types[x]]] = [acc[i[types[x]]]]
acc[i[types[x]]].push(i)
}
return acc;
}, {})
v.push({ [types[x]]: data });
}
return v;
You were close, you just need to check if the property you were adding was undefined before adding. You can also check if the reduced object has any properties before adding to the result object.
Note that this may not be the most efficient way of doing it, but sometimes it's better to understand the code than it is to have highly efficient code.
const data = [{
foo: true
}, {
foo: false
}, {
bar: true
}, {
buzz: false
}];
let v = [];
let types = ['foo', 'bar', 'buzz', 'fizz']
for (let x = 0; x < types.length; x++) {
let reduced = data.reduce((acc, i) => {
// /* Added this type check */
if (!acc[i[types[x]]] && typeof i[types[x]] !== 'undefined') {
acc[i[types[x]]] = [i]
} else if (Array.isArray(acc[i[types[x]]])) {
acc[i[types[x]]].push(i);
} else if (typeof acc[i[types[x]]] === 'object') {
acc[i[types[x]]] = [acc[i[types[x]]]]
acc[i[types[x]]].push(i)
}
return acc;
}, {});
// Doesn't add a property for the type if there are no data
if (Object.keys(reduced).length) {
v.push({
[types[x]]: reduced
});
}
}
console.log(v);
Have a look at how Array.prototype.reduce works. It might be the right method to build your approach upon.
A generic way of solving the OP's problem was to iterate the provided data array. For each item one would extract its key and value. In case the item's key is listed (included) in another provided types array, one would continue creating a new data structure and collecting the currently processed item within the latter.
One does not want to iterate the types array for it will cause a unnecessarily complex lookup for the data items, each time a type item is going to be processed.
Thus a generically working (better code reuse) reduce method might be the best solution to the OP's problem ...
const sampleDataList = [
{ foo: true },
{ foo: false },
{ bar: true },
{ baz: false },
{ buzz: false },
{ baz: false },
{ bar: true }
];
// foo: {true: [{foo: true}], false: [{foo: false}]},
// bar: {true: [{bar: true}]},
// buzz: {false: [{buzz: false}]}
function collectItemIntoInclusiveKeyValueGroup(collector, item) {
const { inclusiveKeyList, index } = collector;
const firstItemEntry = Object.entries(item)[0];
const key = firstItemEntry[0];
const isProceedCollecting = ( // proceed with collecting ...
//
!Array.isArray(inclusiveKeyList) // - either for no given list
|| inclusiveKeyList.includes(key) // - or if item key is listed.
);
if (isProceedCollecting) {
let keyGroup = index[key]; // access the group identified
if (!keyGroup) { // by an item's key, ... or ...
// ...create it in case ...
keyGroup = index[key] = {}; // ...it did not yet exist.
}
const valueLabel = String(firstItemEntry[1]); // item value as key.
let valueGroupList = keyGroup[valueLabel]; // acces the group list
if (!valueGroupList) { // identified by an item's
// value, ...or create it in
valueGroupList = keyGroup[valueLabel] = []; // case it did not yet exist.
}
// push original reference into a grouped
// key value list, as required by the OP.
valueGroupList.push(item);
}
return collector;
}
console.log(
"'foo', 'bar', 'buzz' and 'fizz' only :",
sampleDataList.reduce(collectItemIntoInclusiveKeyValueGroup, {
inclusiveKeyList: ['foo', 'bar', 'buzz', 'fizz'],
index: {}
}).index
);
console.log(
"'foo', 'bar' and 'baz' only :",
sampleDataList.reduce(collectItemIntoInclusiveKeyValueGroup, {
inclusiveKeyList: ['foo', 'bar', 'baz'],
index: {}
}).index
);
console.log(
"all available keys :",
sampleDataList.reduce(collectItemIntoInclusiveKeyValueGroup, {
index: {}
}).index
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Try something like:
const data = [{foo: true},{foo: false},{bar: true},{buzz: false}];
let v = [];
let types = ['foo', 'bar', 'buzz', 'fizz' ];
for (let x = 0; x < types.length; x++) {
let filteredlist = data.filter(function (d) {
return Object.keys(d)[0] == types[x];
});
let isTrue = 0;
let isFalse = 0;
if (filteredlist.length > 0) {
for (let i = 0; i < filteredlist.length; i++) {
let trueOrfalse = eval("filteredlist[i]." + types[x]);
if (trueOrfalse) {
isTrue++;
} else {
isFalse++;
}
}
v.push(types[x], {true: isTrue, false: isFalse});
}
}
console.log(v);
Assuming you only want to count the number of each key (e.g. true or false) you can use the following code.
I've written this as a function named 'aggregate' so that it can be called multiple times with different arguments.
const initialData = [{foo: true},{foo: true},{foo: false},{bar: true},{buzz: false}];
const types = ['foo', 'bar', 'buzz', 'fizz'];
const aggregate = (data, types) => {
const result = {};
data.forEach(item => {
// Extract key & value from object
// Note: use index 0 because each object in your example only has a single key
const [key, value] = Object.entries(item)[0];
// Check if result already contains this key
if (result[key]) {
if (result[key][value]) {
// If value already exists, append one
result[key][value]++;
} else {
// Create new key and instantiate with value 1
result[key][value] = 1;
}
} else {
// If result doesn't contain key, instantiate with value 1
result[key] = { [value]: 1 };
}
});
return result;
};
console.log(aggregate(initialData, types));
This will output the following (note I've added another {foo: true} to your initialData array for testing).
The output should also be an object (not array) so that each key directly relates to its corresponding value, as opposed to an Array which will simply place the value as the next item in the Array (without explicitly linking the two).
{
foo: { true: 2, false: 1 },
bar: { true: 1 },
buzz: { false: 1 }
}

How to get nested value from within an object using a string without using multiple brackets?

Put differently can I do this somehow: obj['data.users.admins.dashboard[3]']
I could create a simple function and parse string then call the object. But I want to know what's possible first as there would lot of edge case, nontrivial.
above would be same as
obj['data']['users']['amdins']['dashboard'][3]
You can use lodash get https://lodash.com/docs/4.17.15#get
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.get(object, 'a[0].b.c');
You can do this very easily with some clever usage of split() and reduce():
const obj = { data : { users: { admins: { dashboard: [1, 2, 3, 4] } } } };
const path = 'data.users.admins.dashboard[3]';
const result = path.replace(/\[/g,'.').replace(/\]/g,'').split('.')
.reduce((obj,key) => obj && obj[key], obj);
console.log(result);
How?
Replace array accessors like [3] with just .3 -> data.users.admins.dashboard.3
Split string on periods -> ['data', 'users', 'admins', 'dashboard', '3']
reduce over the array accessing each key if the previous one was valid
Here's the snippet in a helper function:
function getByPath(obj, path) {
return path.replace(/\[/g, '.').replace(/\]/g, '').split('.').reduce((obj, key) => obj && obj[key], obj);
}
//sample usage
console.log(getByPath({thing: [1, 2]}, "thing[0]"))
It is possible by using eval, but eval is EVIL. Use it at your own risk!
const data = { users: { admins: { dashboard: [1, 2, 3, 4] } } };
eval('data.users.admins.dashboard[3]');
You can use Proxy:
const obj = new Proxy({
data: {
users: {
admins: {
dashboard: [10, 21, 31, 41],
}
}
},
}, {
get: function (map, key, receiver) {
try {
return eval(`map.${key}`)
} catch (error) {
return undefined;
};
},
});
obj['data.users.admins.dashboard[3]'] // 41
obj['erter.sdfdsfds.admins.sdfsdf[3]'] // undefined
In normal objects it would throw an error can't read property of undefined == better than regular objects in js :)
Updated:
https://repl.it/repls/HarmlessMessyCommas
const obj = new Proxy({
data: {
users: {
admins: {
dashboard: [10, 21, 31, 41, {
hi: 'I am hi'
}],
}
}
},
}, {
get: function (map, key, receiver) {
try {
let splitedKey = key.split(".");
let current = map;
while (splitedKey.length > 0) {
let newValue = splitedKey.shift();
let openingBracketIndex = newValue.lastIndexOf("[");
let arrIndex = Number(newValue.slice(openingBracketIndex + 1, newValue.length - 1));
let isArr = !isNaN(arrIndex)
if (openingBracketIndex > -1 && newValue[newValue.length - 1] === "]" && isArr) {
let arrName = newValue.slice(0, openingBracketIndex);
current = current[arrName][arrIndex];
} else {
current = current[newValue];
}
}
return current;
} catch (error) {
return undefined;
};
},
});
console.log(obj['data.users.admins.dashboard[3]'] == 41);
console.log(obj['data.users.admins.dashboard[0]'] == 10);
console.log(obj["data.users.admins.dashboard[4].hi"] === "I am hi");
console.log(obj['erter.sdfdsfds.admins.sdfsdf[3]'] === undefined)
It works but not perfectly, especially with nested arrays are not going to work b/c even a human wouldn't know if someone is trying to access a property name or array, ex:
const map = {
"dashboard[1]": 'i am string',
"dashboard": [1, 2],
}
So if someone wrote map.dashboard[1] so it is going to return "2" instead of "i am string" b/c if condition for array is above the else condition (else condition = if not array)
I hope it helps :)

How to split a doted string and retrive the data from object by notation?

At present, I do this approach:
var obj = {
sender: {
name: "tech"
}
}
var str = "sender.name".split('.');
console.log( obj[str[0]][str[1]] ); //getting update as 'Tech'
In the above I use obj[str[0]][str[1]] for just 2 step, this is works fine. In case if I received a long node parent and child this approach not going to work.
Instead is there any correct dynamic way to do this?
You can use array#reduce to navigate through each key.
var obj = { sender: { name: "tech" } };
var str = "sender.name".split('.').reduce((r,k) => r[k],obj);
console.log(str);
You can use reduce:
var obj = {
foo: {
bar: {
baz: {
sender: {
name: "tech"
}
}
}
}
}
const props = "foo.bar.baz.sender.name".split('.');
const val = props.reduce((currObj, prop) => currObj[prop], obj);
console.log(val);
You could split the string and reduce the path for the result. The function uses a default object for missing or not given properties.
function getValue(object, path) {
return path
.split('.')
.reduce(function (o, k) { return (o || {})[k]; }, object);
}
var obj = { sender: { name: "tech" } },
str = "sender.name";
console.log(getValue(obj, str));
You should be looking into libraries such as "https://lodash.com/"
https://lodash.com/docs/4.17.10
Use _.get : https://lodash.com/docs/4.17.10#get
You can simply write _.get(obj, 'sender.name', 'default') and you will get the value as you expect

Convert javascript object camelCase keys to underscore_case

I want to be able to pass any javascript object containing camelCase keys through a method and return an object with underscore_case keys, mapped to the same values.
So, I have this:
var camelCased = {firstName: 'Jon', lastName: 'Smith'}
And I want a method to output this:
{first_name: 'Jon', last_name: 'Jon'}
What's the fastest way to write a method that takes any object with any number of key/value pairs and outputs the underscore_cased version of that object?
Here's your function to convert camelCase to underscored text (see the jsfiddle):
function camelToUnderscore(key) {
return key.replace( /([A-Z])/g, "_$1").toLowerCase();
}
console.log(camelToUnderscore('helloWorldWhatsUp'));
Then you can just loop (see the other jsfiddle):
var original = {
whatsUp: 'you',
myName: 'is Bob'
},
newObject = {};
function camelToUnderscore(key) {
return key.replace( /([A-Z])/g, "_$1" ).toLowerCase();
}
for(var camel in original) {
newObject[camelToUnderscore(camel)] = original[camel];
}
console.log(newObject);
If you have an object with children objects, you can use recursion and change all properties:
function camelCaseKeysToUnderscore(obj){
if (typeof(obj) != "object") return obj;
for(var oldName in obj){
// Camel to underscore
newName = oldName.replace(/([A-Z])/g, function($1){return "_"+$1.toLowerCase();});
// Only process if names are different
if (newName != oldName) {
// Check for the old property name to avoid a ReferenceError in strict mode.
if (obj.hasOwnProperty(oldName)) {
obj[newName] = obj[oldName];
delete obj[oldName];
}
}
// Recursion
if (typeof(obj[newName]) == "object") {
obj[newName] = camelCaseKeysToUnderscore(obj[newName]);
}
}
return obj;
}
So, with an object like this:
var obj = {
userId: 20,
userName: "John",
subItem: {
paramOne: "test",
paramTwo: false
}
}
newobj = camelCaseKeysToUnderscore(obj);
You'll get:
{
user_id: 20,
user_name: "John",
sub_item: {
param_one: "test",
param_two: false
}
}
es6 node solution below. to use, require this file, then pass object you want converted into the function and it will return the camelcased / snakecased copy of the object.
const snakecase = require('lodash.snakecase');
const traverseObj = (obj) => {
const traverseArr = (arr) => {
arr.forEach((v) => {
if (v) {
if (v.constructor === Object) {
traverseObj(v);
} else if (v.constructor === Array) {
traverseArr(v);
}
}
});
};
Object.keys(obj).forEach((k) => {
if (obj[k]) {
if (obj[k].constructor === Object) {
traverseObj(obj[k]);
} else if (obj[k].constructor === Array) {
traverseArr(obj[k]);
}
}
const sck = snakecase(k);
if (sck !== k) {
obj[sck] = obj[k];
delete obj[k];
}
});
};
module.exports = (o) => {
if (!o || o.constructor !== Object) return o;
const obj = Object.assign({}, o);
traverseObj(obj);
return obj;
};
Came across this exact problem when working between JS & python/ruby objects. I noticed the accepted solution is using for in which will throw eslint error messages at you ref: https://github.com/airbnb/javascript/issues/851 which alludes to rule 11.1 re: use of pure functions rather than side effects ref:https://github.com/airbnb/javascript#iterators--nope
To that end, figured i'd share the below which passed the said rules.
import { snakeCase } from 'lodash'; // or use the regex in the accepted answer
camelCase = obj => {
const camelCaseObj = {};
for (const key of Object.keys(obj)){
if (Object.prototype.hasOwnProperty.call(obj, key)) {
camelCaseObj[snakeCase(key)] = obj[key];
}
}
return camelCaseObj;
};
Marcos Dimitrio posted above with his conversion function, which works but is not a pure function as it changes the original object passed in, which may be an undesireable side effect. Below returns a new object that doesn't modify the original.
export function camelCaseKeysToSnake(obj){
if (typeof(obj) != "object") return obj;
let newObj = {...obj}
for(var oldName in newObj){
// Camel to underscore
let newName = oldName.replace(/([A-Z])/g, function($1){return "_"+$1.toLowerCase();});
// Only process if names are different
if (newName != oldName) {
// Check for the old property name to avoid a ReferenceError in strict mode.
if (newObj.hasOwnProperty(oldName)) {
newObj[newName] = newObj[oldName];
delete newObj[oldName];
}
}
// Recursion
if (typeof(newObj[newName]) == "object") {
newObj[newName] = camelCaseKeysToSnake(newObj[newName]);
}
}
return newObj;
}
this library does exactly that: case-converter
It converts snake_case to camelCase and vice versa
const caseConverter = require('case-converter')
const snakeCase = {
an_object: {
nested_string: 'nested content',
nested_array: [{ an_object: 'something' }]
},
an_array: [
{ zero_index: 0 },
{ one_index: 1 }
]
}
const camelCase = caseConverter.toCamelCase(snakeCase);
console.log(camelCase)
/*
{
anObject: {
nestedString: 'nested content',
nestedArray: [{ anObject: 'something' }]
},
anArray: [
{ zeroIndex: 0 },
{ oneIndex: 1 }
]
}
*/
following what's suggested above, case-converter library is deprectaed, use snakecase-keys instead -
https://github.com/bendrucker/snakecase-keys
supports also nested objects & exclusions.
Any of the above snakeCase functions can be used in a reduce function as well:
const snakeCase = [lodash / case-converter / homebrew]
const snakeCasedObject = Object.keys(obj).reduce((result, key) => ({
...result,
[snakeCase(key)]: obj[key],
}), {})
jsfiddle
//This function will rename one property to another in place
Object.prototype.renameProperty = function (oldName, newName) {
// Do nothing if the names are the same
if (oldName == newName) {
return this;
}
// Check for the old property name to avoid a ReferenceError in strict mode.
if (this.hasOwnProperty(oldName)) {
this[newName] = this[oldName];
delete this[oldName];
}
return this;
};
//rename this to something like camelCase to snakeCase
function doStuff(object) {
for (var property in object) {
if (object.hasOwnProperty(property)) {
var r = property.replace(/([A-Z])/, function(v) { return '_' + v.toLowerCase(); });
console.log(object);
object.renameProperty(property, r);
console.log(object);
}
}
}
//example object
var camelCased = {firstName: 'Jon', lastName: 'Smith'};
doStuff(camelCased);
Note: remember to remove any and all console.logs as they aren't needed for production code

Convert array to object inclusive

How can I convert a simple array like this: ['foo', 'bar', 'baz'] to an object like this:
{ 'foo': {
'bar': {
'baz' : {}
}
}
}
It seems so simple, but I can't figure it out.
I think this is what you want:
function arrayToNestedObject(arr) {
var obj = {},
current = obj;
for(var i = 0; i < arr.length; i++) {
var key = arr[i];
current = current[key] = {};
}
return obj;
}
console.log(arrayToNestedObject(['foo', 'bar', 'baz']));
You should use Array#reduceRight:
function arrayToNestedObject(arr) {
// Proceeding from the end of the array back towards the beginning...
return arr.reduceRight(function(prev, cur) {
// Create a new object with a property named by the array element,
// whose value is what we have got so far
return Object.defineProperty({}, cur, {value: prev});
}, {});
}
Test:
arrayToNestedObject(['foo', 'bar', 'baz']
> {foo: {bar: {baz: {} } } }
Note that Object.defineProperty({}, prop, {value: val}) is a convenient one-line shorthand for
var x = {};
x[prop] = val;
return x;
In ES6, using "computed properties", the above would simply be
arrayToNestedObject = (arr) => arr.reduceRight((prev, cur) => ({[cur]: prev}));
If one prefers a recursive solution, it is also better to proceed from the right, using pop:
function arrayToNestedObject(arr) {
return (function _(arr, obj) {
var val = arr.pop();
return val ? _(arr, Object.defineProperty({}, val, {value: obj})) : obj;
}(arr, {}));
}
arrayToNestedObject(['foo', 'bar'])
> { foo: { bar: { } } }

Categories