How can I get some value of a deep-nested object? - javascript

There are two types of arrays which I have to build dynamically.
data['fields']['title']
and
data['fields']['description']['html']
it returns the content of this structure:
{
"fields": {
"title": "Headline",
"description": {
"html": "<p>description text</p>"
}
},
"meta": {
"id": "995915463198380032"
}
}
The problem is "dynamically".
I call a function and give the path through it like "description>html".
I split the string into "description" and "html".
But how do I build now the array: data['fields']['description']['html']
Sometimes there is a level more or less like "title".
If I want to call title, the array is like data['fields']['title']
So the content and the number of parts in the array are dynamic.
I tried by myself this:
function comfort_x(item_fields) {
var splitter = item_fields.split(">");
var content = new Array();
for (var i = 1; i < splitter.length; ++i) {
content['splitter['+i+']'] = splitter[i];
}
data['fields'][splitter[0]][splitter[1]];
}
Thank you for your help.

You can create a function that will look up to the level you are passing. You can just split your path by > and reduce that array with the source input.
(data, path) => path.split(">").reduce((r, e) => r[e], data);
Here is a example.
var obj = {
"fields": {
"title": "Headline",
"description": {
"html": "<p>description text</p>"
}
},
"meta": {
"id": "995915463198380032"
}
}
var lookUp = (o, path) => path.split(">").reduce((r, e) => r[e], o);
console.log('fields: ', lookUp(obj, 'fields'))
console.log('fields>title: ', lookUp(obj, 'fields>title'))
console.log('fields>description>html: ', lookUp(obj, 'fields>description>html'))

It works with this addinitional work-around:
switch (splitter.length) {
case 0:
item.innerHTML = data['fields'];
break;
case 1:
item.innerHTML = data['fields'][splitter[0]];
break;
case 2:
item.innerHTML = data['fields'][splitter[0]][splitter[1]];
break;
case 3:
item.innerHTML = data['fields'][splitter[0]][splitter[1]][splitter[2]];
}
maybe you have a smarter solution.

/*
Be careful! The function won’t return a new object,
but take a reference to the input object aka. alter
it and don’t deep-clone it.
*/
const myObject = {};
function setValueByKeypath(object, path, value, separator = ">") {
path = path.split(separator);
let currentPart = object;
for (let i = 0, len = path.length; i < len; i++) {
const key = path[i];
const keyValue = (i === len - 1) ? value : {};
if (typeof currentPart[key] === "undefined") {
currentPart[key] = keyValue;
}
currentPart = currentPart[key];
}
}
setValueByKeypath(myObject, "fields>description>html", "<p>description text</p>");
console.log(myObject);
setValueByKeypath(myObject, "fields>title", "Headline");
console.log(myObject);
setValueByKeypath(myObject, "meta>id", "995915463198380032");
console.log(myObject);

Related

Convert plain text to deeply json object using JavaScript

I have one plain string including some conditions like this.
const optionString = '{2109} AND ({2370} OR {1701} OR {2702}) AND {1234} AND ({2245} OR {2339})';
I need to get object like the following structure from above.
const output = {
and: [
2109,
{ or: [2370, 1071, 2702] },
1234,
{ or: [2245, 2339] },
];
Currently, I have tried to do like following
function parseFormat(strArg) {
var
category,
output = [], // Output
str = strArg.trim(); // Remove unwanted space before processing
str.split('AND').forEach(function(line) {
var removedString = line.replace(/[\])}[{(]/g, '');
var item = removedString.split('OR');
item = item.map(it => {
return Number(it.replace(/ /g, ''))
})
if(item.length > 0) {
output.push(item)
} else {
output.push(item[0])
}
});
return output;
}
And its output is like here.
[
[
1069
],
[
1070,
1071,
1072
],
[
1244
],
[
1245,
1339
]
]
I have one question first
How to add key AND and OR in the current result?
If you know a good solution on the performance side, please update me.
Thanks for taking the time.
const optionString = '{2109} AND ({2370} OR {1701} OR {2702}) AND {1234} AND ({2245} OR {2339})';
const parseExpr = s => {
let op, m, a = [];
while(s?.length) {
if(m = /^{(?<num>[0-9]+)}( (?<rest>.*))?/.exec(s)) {
a.push(+m.groups.num);
s = m.groups.rest;
}
else if(m = /^(?<op>[A-Z]+)( (?<rest>.*))?/.exec(s)) {
let t = m.groups.op.toLowerCase();
if(op && op!==t) throw new Error('Multiple operators cannot exist at same level in syntax tree')
else op = t;
s = m.groups.rest;
}
else if(s.startsWith('(')) {
for(let i=0, level=0; i<s.length; i++) {
if(s.charAt(i)==='(') level++;
if(s.charAt(i)===')') level--;
if(!level) {
a.push(parseExpr(s.substring(1, i)));
s = s.substring(i+2);
break;
}
if(i===s.length-1) throw new Error('Mismatched brackets')
}
}
else throw new Error(`Unparseable expression: ${s}`);
}
return { [op]: a };
}
const result = parseExpr(optionString)
console.log(result)

Traverse down a JSON depending on length of array

Say I have a json
jsonData = {
"id":"dfd",
"properties":{
"Pri":"2",
"Brief Description":"asdf",
"Description":"",
"tree":{
"var": "2",
"rav": "3"
}
}
}
and a list
var variableArray = ['properties', 'tree', 'var'];
If I want to access the value of var and edit it. How would I do that while maintaining the value of jsonData?
I've tried
for (var i = 0; i < variableArray.length; i++) {
jsonData = jsonData[variableArray[i]];
}
jsonData = 'new value';
But I can no longer access the whole jsonData.
What are some way to implement this?
JavaScript doesn't have references to properties, which is effectively what you're trying to use there.
Instead, you can give yourself a method that will traverse an object and either retrieve or assign a property in it. Here's a quick and dirty in ES5 and earlier:
function accessPath(obj, path, value) {
var o = obj;
var i = 0;
var last = path.length - 1;
while (i < last) {
o = o[path[i]];
++i;
}
if (arguments.length < 3) {
// Getting
return o[path[last]];
} else {
// Setting
return o[path[last]] = value;
}
}
Live example:
function accessPath(obj, path, value) {
var o = obj;
var i = 0;
var last = path.length - 1;
while (i < last) {
o = o[path[i]];
++i;
}
if (arguments.length < 3) {
// Getting
return o[path[last]];
} else {
// Setting
return o[path[last]] = value;
}
}
var data = {
"id":"dfd",
"properties":{
"Pri":"2",
"Brief Description":"asdf",
"Description":"",
"tree":{
"var": "2",
"rav": "3"
}
}
}
var path = ['properties', 'tree', 'var'];
console.log("Existing: " + accessPath(data, path));
accessPath(data, path, "new value");
console.log("Updated: " + accessPath(data, path));
console.log("Confirm: " + data.properties.tree.var);
Looks fairly similar in ES2015+, other than perhaps how you check if value is supplied.
Not pretty, but fairly efficient.
Actually, we can go further if we return an object with a getter and setter, which would look a bit like a property reference even though it isn't actually:
function makeAccessor(obj, path) {
var o = obj;
var i = 0;
var last = path.length - 1;
var lastName = path[last];
while (i < last) {
o = o[path[i]];
++i;
}
return {
get value() {
return o[lastName];
},
set value(value) {
o[lastName] = value;
}
};
}
Then, getting the accessor:
var accessor = makeAccessor(data, path);
And using it:
console.log(accessor.value);
accessor.value = "new value";
function makeAccessor(obj, path) {
var o = obj;
var i = 0;
var last = path.length - 1;
var lastName = path[last];
while (i < last) {
o = o[path[i]];
++i;
}
return {
get value() {
return o[lastName];
},
set value(value) {
o[lastName] = value;
}
};
}
var data = {
"id":"dfd",
"properties":{
"Pri":"2",
"Brief Description":"asdf",
"Description":"",
"tree":{
"var": "2",
"rav": "3"
}
}
}
var path = ['properties', 'tree', 'var'];
var accessor = makeAccessor(data, path);
console.log("Existing: " + accessor.value);
accessor.value = "new value";
console.log("Updated: " + accessor.value);
console.log("Confirm: " + data.properties.tree.var);
You could write little helpers read and write that do what you need – note data is not altered by use of read
const read = (o, [k,...ks]) =>
o ? k === undefined ? o
: read (o[k], ks)
: undefined
const write = (o, [k,...ks], v) =>
o ? ks.length === 0 ? (o[k] = v, null)
: write (o[k], ks, v)
: undefined
const data = {
"id":"dfd",
"properties":{
"Pri":"2",
"Brief Description":"asdf",
"Description":"",
"tree":{
"var": "2",
"rav": "3"
}
}
}
// read
console.log (read (data, ['properties', 'tree', 'var']))
// => 2
console.log (read (data, ['foo', 'bar', 'barf']))
// => undefined (signals failure)
// write
console.log (write (data, ['properties', 'tree', 'var'], 'new value'))
// => null (signals success)
console.log (write (data, ['foo', 'bar', 'barf'], 'new value'))
// => undefined (signals failure)
// read updated value
console.log (read (data, ['properties', 'tree', 'var']))
// => "new value"
By the way, to add to others' comments, JavaScript Object Notation is what JSON stands for. It's the Notation part that makes JSON different from "JSO" (JavaScript Object). More simply put, JSON is a string representation of a JavaScript Object. You'll know it's not JSON if it's not a string.

Building a nested tree of data from plain array of objects

I have an array of objects, like those ones:
{
"short_id": "2p45q",
"path": "/",
"name": {
"en-US": "IndustrialDesign"
}
}
...
{
"short_id": "2q56r",
"path": "/2p45q/",
"name": {
"en-US": "Automotive"
}
}
I must iterate over each element of the array and check the path, then find the parent of the element and push it in a new array property of that parent called sub. Each child can have a sub property on it's own, thus being a parent of more children. The final result (for this example) would look like:
{
"short_id": "2p45q",
"path": "/",
"name": {
"en-US": "Test A"
},
"sub": [
{
"short_id": "2q56r",
"path": "/2p45q/",
"name": {
"en-US": "Test A.1"
}
}
]
}
I have a working code (using this jsonpath lib):
function(categories) {
var _categories = [];
angular.forEach(angular.copy(categories), function(_category) {
if (_category.path === "/") {
_categories.push(_category);
} else {
var _path = _category.path.split("/");
_path.pop();
var _parent = _path.pop();
jsonpath.apply(_categories, "$..[?(#.short_id=='" + _parent + "')]", function(obj) {
if(!obj.hasOwnProperty("sub")) {
obj.sub = [];
}
obj.sub.push(_category);
return obj;
});
}
});
return _categories;
}
but the performance is really bad, mainly because I'm querying the entire array for each iteration.
My question is how can I optimize my code?
Notes:
Each short_id is exactly 5 characters long.
Each character in short_id can be [0-9a-z]
path is guaranteed to start and end with a /
Create another tmp object as Hashmap, so you can just use path and id to create a new key to store.
Logic :
If path is '/', its root, put to the _categories array.
If not, check if the target parent is exist in the hashStore or not, if not, create a fake one, and put it self to target is sub attr.
For all element, create a key by _category.path + _category.short_id + '/', and check if its exist in the hashStore, if exist, the one in store should be a fake, get sub from fake. Then assign itself to the hashStore by created key.
Use a key to decide whether the object exist in the map or not should be O(1).
So the performance of the this function should be O(n) while n is the number of element in origin list.
function buildTree(categories) {
var _categories = [];
var store = {};
angular.forEach(angular.copy(categories), function(_category) {
if (_category.path === '/') {
_categories.push(_category);
} else {
var target;
// If parent exist
if (typeof store[_category.path] !== 'undefined') {
// Check parent have sub or not, if not, create one.
target = store[_category.path];
if (typeof store[_category.path].sub === 'undefined') {
target.sub = [];
}
} else {
// Create fake parent.
target = {sub: []};
store[_category.path] = target;
}
// Push to parent's sub
target.sub.push(_category);
}
// Create key map
var key = _category.path + _category.short_id + '/';
// If fake object exist, get its sub;
if (typeof store[key] !== 'undefined') {
_category.sub = store[key].sub;
}
store[key] = _category;
});
return _categories;
}
This solution is more flexible in that it doesn't require knowledge of path length or correlation with short_id
var source = [{
"short_id": "2p45q",
"path": "/",
"name": {
"en-US": "IndustrialDesign"
}
}, {
"short_id": "2q56r",
"path": "/2p45q/",
"name": {
"en-US": "Automotive"
}
}];
function buildTree(arr) {
var source = arr.slice();
source.sort(function(a, b) {
return a.path.length <= b.path.length;
});
var tree = source.splice(0, 1)[0];
tree.subo = {};
source.forEach(function(i) {
var re = /[^\/]*\//g;
var context = tree;
while ((m = re.exec(i.path.substr(1))) !== null) {
if (context.subo[m[0]] === undefined) {
context.subo[m[0]] = i;
i.subo = {};
return;
}
context = context.subo[m[0]];
}
});
(function subOsub(i) {
var keys = Object.keys(i.subo);
if (keys.length > 0) {
i.sub = [];
for (var j = 0; j < keys.length; j++) {
i.sub.push(i.subo[keys[j]]);
subOsub(i.subo[keys[j]]);
}
}
delete i.subo;
})(tree);
return tree;
}
alert(JSON.stringify(buildTree(source), null, ' '));
Well, just examine the path of each object to see where to put it.
You just need a mapping of paths to objects. E.g.
var objs = [
{
"short_id": "2p45q",
"path": "/",
"name": {
"en-US": "IndustrialDesign"
}
},
{
"short_id": "blah",
"path": "/2p45q/foo/",
"name": {
"blah": "blah"
}
},
{
"short_id": "2q56r",
"path": "/2p45q/",
"name": {
"en-US": "Automotive"
}
}
];
// map paths to objects (one iteration)
var path_to_obj = {};
objs.forEach(function(obj){
path_to_obj[obj.path] = obj;
});
// add objects to the sub array of their parent (one iteration)
objs.forEach(function(obj){
var parentpath = obj.path.replace(/[^\/]*\/$/, '');
var parent = path_to_obj[parentpath];
if(parent){
parent.sub = parent.sub || [];
parent.sub.push(obj);
}
});
var pre = document.createElement('pre');
pre.innerHTML = 'Result:\n' + JSON.stringify(path_to_obj['/'], null, 4);
document.body.appendChild(pre);

Getting nested obj value

Given the following obj:
var inputMapping = {
nonNestedItem: "someItem here",
sections: {
general: "Some general section information"
}
};
I'm writing a function to get that data by passing in a string "nonNestedItem" or in the nested case "sections.general". I'm having to use an eval and I was wondering if there was maybe a better way to do this.
Here is what I have so far and it works okay. But improve!
function getNode(name) {
var n = name.split(".");
if (n.length === 1) {
n = name[0];
} else {
var isValid = true,
evalStr = 'inputMapping';
for (var i=0;i<n.length;i++) {
evalStr += '["'+ n[i] +'"]';
if (eval(evalStr) === undefined) {
isValid = false;
break;
}
}
if (isValid) {
// Do something like return the value
}
}
}
Linky to Jsbin
You can use Array.prototype.reduce function like this
var accessString = "sections.general";
console.log(accessString.split(".").reduce(function(previous, current) {
return previous[current];
}, inputMapping));
Output
Some general section information
If your environment doesn't support reduce, you can use this recursive version
function getNestedItem(currentObject, listOfKeys) {
if (listOfKeys.length === 0 || !currentObject) {
return currentObject;
}
return getNestedItem(currentObject[listOfKeys[0]], listOfKeys.slice(1));
}
console.log(getNestedItem(inputMapping, "sections.general".split(".")));
You don't need to use eval() here. You can just use [] to get values from an object. Use a temp object to hold the current value, then update it each time you need the next key.
function getNode(mapping, name) {
var n = name.split(".");
if (n.length === 1) {
return mapping[name];
} else {
var tmp = mapping;
for (var i = 0; i < n.length; i++) {
tmp = tmp[n[i]];
}
return tmp;
}
}

Updating a JSON object using Javascript

How can i update the following JSON object dynamically using javascript or Jquery?
var jsonObj = [{'Id':'1','Username':'Ray','FatherName':'Thompson'},
{'Id':'2','Username':'Steve','FatherName':'Johnson'},
{'Id':'3','Username':'Albert','FatherName':'Einstein'}]
I would like to dynamically update the Username to 'Thomas' where the 'Id' is '3'.
How can I achieve this?
A plain JavaScript solution, assuming jsonObj already contains JSON:
Loop over it looking for the matching Id, set the corresponding Username, and break from the loop after the matched item has been modified:
for (var i = 0; i < jsonObj.length; i++) {
if (jsonObj[i].Id === 3) {
jsonObj[i].Username = "Thomas";
break;
}
}
Here it is on jsFiddle.
Here's the same thing wrapped in a function:
function setUsername(id, newUsername) {
for (var i = 0; i < jsonObj.length; i++) {
if (jsonObj[i].Id === id) {
jsonObj[i].Username = newUsername;
return;
}
}
}
// Call as
setUsername(3, "Thomas");
simply iterate over the list then check the properties of each object.
for (var i = 0; i < jsonObj.length; ++i) {
if (jsonObj[i]['Id'] === '3') {
jsonObj[i]['Username'] = 'Thomas';
}
}
$(document).ready(function(){
var jsonObj = [{'Id':'1','Username':'Ray','FatherName':'Thompson'},
{'Id':'2','Username':'Steve','FatherName':'Johnson'},
{'Id':'3','Username':'Albert','FatherName':'Einstein'}];
$.each(jsonObj,function(i,v){
if (v.Id == 3) {
v.Username = "Thomas";
return false;
}
});
alert("New Username: " + jsonObj[2].Username);
});
use:
var parsedobj = jQuery.parseJSON( jsonObj);
This will only be useful if you don't need the format to stay in string.
otherwise you'd have to convert this back to JSON using the JSON library.
var i = jsonObj.length;
while ( i --> 0 ) {
if ( jsonObj[i].Id === 3 ) {
jsonObj[ i ].Username = 'Thomas';
break;
}
}
Or, if the array is always ordered by the IDs:
jsonObj[ 2 ].Username = 'Thomas';
JSON is the JavaScript Object Notation. There is no such thing as a JSON object. JSON is just a way of representing a JavaScript object in text.
So what you're after is a way of updating a in in-memory JavaScript object. qiao's answer shows how to do that simply enough.
I took Michael Berkowski's answer a step (or two) farther and created a more flexible function allowing any lookup field and any target field. For fun I threw splat (*) capability in there incase someone might want to do a replace all. jQuery is NOT needed. checkAllRows allows the option to break from the search on found for performance or the previously mentioned replace all.
function setVal(update) {
/* Included to show an option if you care to use jQuery
var defaults = { jsonRS: null, lookupField: null, lookupKey: null,
targetField: null, targetData: null, checkAllRows: false };
//update = $.extend({}, defaults, update); */
for (var i = 0; i < update.jsonRS.length; i++) {
if (update.jsonRS[i][update.lookupField] === update.lookupKey || update.lookupKey === '*') {
update.jsonRS[i][update.targetField] = update.targetData;
if (!update.checkAllRows) { return; }
}
}
}
var jsonObj = [{'Id':'1','Username':'Ray','FatherName':'Thompson'},
{'Id':'2','Username':'Steve','FatherName':'Johnson'},
{'Id':'3','Username':'Albert','FatherName':'Einstein'}]
With your data you would use like:
var update = {
jsonRS: jsonObj,
lookupField: "Id",
lookupKey: 2,
targetField: "Username",
targetData: "Thomas",
checkAllRows: false
};
setVal(update);
And Bob's your Uncle. :) [Works great]
For example I am using this technique in Basket functionality.
Let us add new Item to Basket.
var productArray=[];
$(document).on('click','[cartBtn]',function(e){
e.preventDefault();
$(this).html('<i class="fa fa-check"></i>Added to cart');
console.log('Item added ');
var productJSON={"id":$(this).attr('pr_id'), "nameEn":$(this).attr('pr_name_en'), "price":$(this).attr('pr_price'), "image":$(this).attr('pr_image'), "quantity":1, "discount":0, "total":$(this).attr('pr_price')};
if(localStorage.getObj('product')!==null){
productArray=localStorage.getObj('product');
productArray.push(productJSON);
localStorage.setObj('product', productArray);
}
else{
productArray.push(productJSON);
localStorage.setObj('product', productArray);
}
itemCountInCart(productArray.length);
});
After adding some item to basket - generates json array like this
[
{
"id": "95",
"nameEn": "New Braslet",
"price": "8776",
"image": "1462012394815.jpeg",
"quantity": 1,
"discount": 0,
"total": "8776"
},
{
"id": "96",
"nameEn": "new braslet",
"price": "76",
"image": "1462012431497.jpeg",
"quantity": 1,
"discount": 0,
"total": "76"
},
{
"id": "97",
"nameEn": "khjk",
"price": "87",
"image": "1462012483421.jpeg",
"quantity": 1,
"discount": 0,
"total": "87"
}
]
For Removing some item from Basket.
$(document).on('click','[itemRemoveBtn]',function(){
var arrayFromLocal=localStorage.getObj('product');
findAndRemove(arrayFromLocal,"id",$(this).attr('basketproductid'));
localStorage.setObj('product', arrayFromLocal);
loadBasketFromLocalStorageAndRender();
});
//This function will remove element by specified property. In my case this is ID.
function findAndRemove(array, property, value) {
array.forEach(function(result, index) {
if(result[property] === value) {
//Remove from array
console.log('Removed from index is '+index+' result is '+JSON.stringify(result));
array.splice(index, 1);
}
});
}
And Finally the real answer of the question "Updating a JSON object using JS". In my example updating product quantity and total price on changing the "number" element value.
$(document).on('keyup mouseup','input[type=number]',function(){
var arrayFromLocal=localStorage.getObj('product');
setQuantityAndTotalPrice(arrayFromLocal,$(this).attr('updateItemid'),$(this).val());
localStorage.setObj('product', arrayFromLocal);
loadBasketFromLocalStorageAndRender();
});
function setQuantityAndTotalPrice(array,id,quantity) {
array.forEach(function(result, index) {
if(result.id === id) {
result.quantity=quantity;
result.total=(quantity*result.price);
}
});
}
I think this the more efficent way than for looping.
1-First find index of item.
2-Second edit exact element. (if not exist add)
Example :
let index= jsonObj.findIndex(x => x["Id"] === 3);
if (index == -1) {
// add
jsonObj.push({ id:3, .... });
} else {
// update
jsonObj[index]["Username"]="xoxo_gossip_girl"
}
var jsonObj = [{'Id':'1','Quantity':'2','Done':'0','state':'todo',
'product_id':[315,"[LBI-W-SL-3-AG-TA004-C650-36] LAURA BONELLI-WOMEN'S-SANDAL"],
'Username':'Ray','FatherName':'Thompson'},
{'Id':'2','Quantity':'2','Done':'0','state':'todo',
'product_id':[314,"[LBI-W-SL-3-AG-TA004-C650-36] LAURA BONELLI-WOMEN'S-SANDAL"],
'Username':'Steve','FatherName':'Johnson'},
{'Id':'3','Quantity':'2','Done':'0','state':'todo',
'product_id':[316,"[LBI-W-SL-3-AG-TA004-C650-36] LAURA BONELLI-WOMEN'S-SANDAL"],
'Username':'Albert','FatherName':'Einstein'}];
for (var i = 0; i < jsonObj.length; ++i) {
if (jsonObj[i]['product_id'][0] === 314) {
this.onemorecartonsamenumber();
jsonObj[i]['Done'] = ""+this.quantity_done+"";
if(jsonObj[i]['Quantity'] === jsonObj[i]['Done']){
console.log('both are equal');
jsonObj[i]['state'] = 'packed';
}else{
console.log('not equal');
jsonObj[i]['state'] = 'todo';
}
console.log('quantiy',jsonObj[i]['Quantity']);
console.log('done',jsonObj[i]['Done']);
}
}
console.log('final',jsonObj);
}
quantity_done: any = 0;
onemorecartonsamenumber() {
this.quantity_done += 1;
console.log(this.quantity_done + 1);
}
//update & push json value into json object
var sales1 = [];
if (jsonObj && Object.keys(jsonObj ).length != 0) {
jsonObj .map((value) => {
const check = sales1.filter(x => x.order_date === String(value.order_date).substring(0, 10))
if (check.length) {
sales1.filter(x => x.sale_price = Number(x.sale_price) + Number(value.sale_price))
} else {
let data = { "sale_price": Number(value.sale_price), "order_date": String(value.order_date).substring(0, 10) }
sales1.push(data)
}
})
}
You can easily update the username dynamically to 'Thomas' where the 'Id' is '3' using the following.
jsonObj.find(i=>i.Id===3).Username='Thomas'

Categories