I am trying to overwrite an object given specific changes to that object. The problem is that there are other nested objects that get overwritten as well. How would I prevent this?
const deviceChanges = {
"a": 5,
"card": {
"back": [
{
"key": "iphoneText",
"label": "IPHONE",
"value": "UPDATED VALUE FOR IPHONE"
},
]
}
};
let newBody = {
"a": 3,
"card": {
"back": [
{
"key": "androidText",
"label": "ANDROID",
"value": "androidOS"
},
{
"key": "samsungText",
"label": "SAMSUNG",
"value": "samsungOS"
},
{
"key": "iphoneText",
"label": "IPHONE",
"value": "iphone"
},
{
"key": "macbookText",
"label": "MACBOOK",
"value": "macbookOS"
}
]
},
"c": 8
};
const expected = {
"object": {
"a": 5,
"card": {
"back": [
{
"key": "androidText",
"label": "ANDROID",
"value": "androidOS"
},
{
"key": "samsungText",
"label": "SAMSUNG",
"value": "samsungOS"
},
{
"key": "iphoneText",
"label": "IPHONE",
"value": "UPDATED VALUE FOR IPHONE"
},
{
"key": "macbookText",
"label": "MACBOOK",
"value": "macbookOS"
}
]
},
"c": 8
}
};
Here is a Unit Test example of what I am trying to do. I want to take the changes object, and basically replace b.x in newBody, but I also want to preserve the other fields like b.y, a, and C. I want to make it as dynamic as possible, so if there is another object for newBody.b.x or another value for A, I want the code to be able to notice that and adequately change that. Does anyone have an idea on what to do here?
for (let [key, value] of Object.entries(changes)) {
for(let [key1, value1] of Object.entries(value)) {
newBody[key][key1] = value1;
}
}
This is what I have so far in terms of Code. But it only takes into account the fact that it only needs to traverse through two objects to replace. If I had something like:
const changes = {
"b": {
"x": "new",
"y": {
"n": "iphone"
}
}
};
The code would not work. How do I make it as dynamic as possible to realize how many objects it needs to replace?
Here's a function that assigns values for matching keys from a source object into a target. It does so recursively.
Lodash _merge() does something like this, probably handling many more edge cases than I've anticipated here (which is approximately none).
const changes = {
"b": {
"x": "changed",
"Z": "new key/value pair"
}
};
let newBody = {
"a": 3,
"b": {
"x": "old",
"y": "fields"
},
"c": 8
};
// overwrite values in target with matching keys in source
// side-effects target, also returns it
function merge(target, source) {
for (const [key, value] of Object.entries(source)) {
if (key in target) {
if (typeof value === 'object' && typeof target[key] === 'object') {
merge(target[key], value);
} else {
target[key] = value;
}
} else {
// the key in source isn't in the target. add it
target[key] = value;
}
}
return target;
}
const r = merge(newBody, changes)
console.log(r)
If you don't know how deep an object/array is nested you often need to make use of recursive functions (functions that call themselves)
The function I wrote below is that.
I'll try to explain how it works:
For simplicities sake we're gonna assume there is only 1 property per "layer" (the function can actually handle multiple - since we loop, but to explain the recursive part we're just gonna simplify the whole thing)
So you start by checking if the property of the changes object is itself an object or not. If it not an object, this means we are at the deepest level of this particular nesting, so we can replace the property of our actual object with the property of the changes object. If the property is however an object we need to go deeper. So what we do in this case is call the function again but pass only the property object (in our case we would pass {x: "new"} and {x: "old", y: "fields"} - we do this until we don't find an object as our property in which case we'll replace.
This way we can go however deep we need and replace only properties that are primitives.
const changes = {
"b": {
"x": "new",
}
};
let newBody = {
"a": 3,
"b": {
"x": "old",
"y": "fields"
},
"c": 8
};
function doTheThing(theObject, changes) {
const keys = Object.keys(changes)
keys.forEach(key => {
if (typeof changes[key] === "object" && changes[key] !== null) {
//this will also be true if changes[key] is an array
doTheThing(theObject[key], changes[key])
} else {
theObject[key] = changes[key]
}
})
return theObject
}
console.log(doTheThing(newBody, changes))
So we solved that problem with recursion (there are tons of resources about that if you need more information)
Related
So I have an interesting problem which I have been able to solve, but my solution is not elegant in any way or form, so I was wondering what others could come up with :)
The issue is converting this response here
const response = {
"device": {
"name": "Foo",
"type": "Bar",
"telemetry": [
{
"timeStamp": "2022-06-01T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-02T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-03T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-04T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-05T00:00:00.000Z",
"temperature": 100,
"pressure": 50
}
]
}
};
Given this selection criteria
const fields = ['device/name', 'device/telemetry/timeStamp', 'device/telemetry/temperature']
and the goal is to return something like this
[
{"device/name": "Foo", "device/telemetry/timeStamp": "2022-06-01T00:00:00.000Z", "device/telemetry/temperature": 100},
{"device/name": "Foo", "device/telemetry/timeStamp": "2022-06-02T00:00:00.000Z", "device/telemetry/temperature": 100},
{"device/name": "Foo", "device/telemetry/timeStamp": "2022-06-03T00:00:00.000Z", "device/telemetry/temperature": 100},
...,
{"device/name": "Foo", "device/telemetry/timeStamp": "2022-06-05T00:00:00.000Z", "device/telemetry/temperature": 100},
]
If you are interested, here is my horrible brute force solution, not that familiar with typescript yet, so please forgive the horribleness :D
EDIT #1
So some clarifications might be needed. The response can be of completely different format, so we can't use our knowledge of how the response looks like now, the depth can also be much deeper.
What we can assume though is that even if there are multiple arrays in the reponse (like another telemetry array called superTelemetry) then the selection criteria will only choose from one of these arrays, never both :)
function createRecord(key: string, value: any){
return new Map<string, any>([[key, value]])
}
function getNestedData (data: any, fieldPath: string, records: Map<string, any[]>=new Map<string, any[]>()) {
let dataPoints: any = [];
const paths = fieldPath.split('/')
paths.forEach((key, idx, arr) => {
if(Array.isArray(data)){
data.forEach(
(row: any) => {
dataPoints.push(row[key])
}
)
} else {
data = data[key]
if(idx + 1== paths.length){
dataPoints.push(data);
}
}
})
records.set(fieldPath, dataPoints)
return records
}
function getNestedFields(data: any, fieldPaths: string[]){
let records: Map<string, any>[] = []
let dataset: Map<string, any[]> = new Map<string, any[]>()
let maxLength = 0;
// Fetch all the fields
fieldPaths.forEach((fieldPath) => {
dataset = getNestedData(data, fieldPath, dataset)
const thisLength = dataset.get(fieldPath)!.length;
maxLength = thisLength > maxLength ? thisLength : maxLength;
})
for(let i=0; i<maxLength; i++){
let record: Map<string, any> = new Map<string, any>()
for(let [key, value] of dataset){
const maxIdx = value.length - 1;
record.set(key, value[i > maxIdx ? maxIdx : i])
}
records.push(record)
}
// Normalize into records
return records
}
As per my understanding you are looking for a solution to construct the desired result as per the post. If Yes, you can achieve this by using Array.map() along with the Array.forEach() method.
Try this :
const response = {
"device": {
"name": "Foo",
"type": "Bar",
"telemetry": [
{
"timeStamp": "2022-06-01T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-02T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-03T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-04T00:00:00.000Z",
"temperature": 100,
"pressure": 50
},
{
"timeStamp": "2022-06-05T00:00:00.000Z",
"temperature": 100,
"pressure": 50
}
]
}
};
const fields = ['device/name', 'device/telemetry/timeStamp', 'device/telemetry/temperature'];
const res = response.device.telemetry.map(obj => {
const o = {};
fields.forEach(item => {
const splittedItem = item.split('/');
o[item] = (splittedItem.length === 2) ? response[splittedItem[0]][splittedItem[1]] : obj[splittedItem[2]];
});
return o;
})
console.log(res);
In what follows I will be concerned with just the implementation and runtime behavior, and not so much the types. I've given things very loose typings like any and string instead of the relevant generic object types. Here goes:
function getNestedFields(data: any, paths: string[]): any[] {
If data is an array, we want to perform getNestedFields() on each element of the array, and then concatenate the results together into one big array. So the first thing we do is check for that and make a recursive call:
if (Array.isArray(data)) return data.flatMap(v => getNestedFields(v, paths));
Now that we know data is not an array, we want to start gathering the pieces of the answer. If paths is, say, ['foo/bar', 'foo/baz/qux', 'x/y', 'x/z'], then we want to make recursive calls to getNestedFields(data.foo, ["bar", "baz/qux"]) and to getNestedFields(data.x, ["y", "z"]). In order to do this we have to split each path element at its first slash "/", and collect the results into a new object whose keys are the part to the left of the slash and whose values are arrays of parts to the right. In this example it would be {foo: ["bar", "baz/qux"], x: ["y", "z"]}.
Some important edge cases: for every element of paths with no slash, then we have a key with an empty value... that is, ["foo"] should result in a call like getNestedFields(data.foo, [""]). And if there is an element of paths that's just the empty string "", then we don't want to do a recursive call; the empty path is the base case and implies that we're asking about data itself. That is, instead of a recursive call, we can just return [{"": data}]. So we need to keep track of the empty path (hence the emptyPathInList variable below).
Here's how it looks:
const pathMappings: Record<string, string[]> = {};
let emptyPathInList = false;
paths.forEach(path => {
if (!path) {
emptyPathInList = true;
} else {
let slashIdx = path.indexOf("/");
if (slashIdx < 0) slashIdx = path.length;
const key = path.substring(0, slashIdx);
const restOfPath = path.substring(slashIdx + 1);
if (!(key in pathMappings)) pathMappings[key] = [];
pathMappings[key].push(restOfPath);
}
})
Now, for each key-value pair in pathMappings (with key key and with value restsOfPath) we need to call getNestedFields() recursively... the results will be an array of objects whose keys are relative to data[key], so we need to prepend key and a slash to their keys. Edge cases: if there's an empty path we shouldn't add a slash. And if data` is nullish then we will have a runtime error recursing down into it, so we might want to do something else there (although a runtime error might be fine since it's a weird input):
const subentries = Object.entries(pathMappings).map(([key, restsOfPath]) =>
(data == null) ? [{}] : // <-- don't recurse down into nullish data
getNestedFields(data[key], restsOfPath)
.map(nestedFields =>
Object.fromEntries(Object.entries(nestedFields)
.map(([path, value]) =>
[key + (path ? "/" : "") + path, value])))
)
Now subentries is an array of all the separate recursive call results, with the proper keys. We want to add one more entry correpsonding to data if emptyPathInList is true:
if (emptyPathInList) subentries.push([{ "": data }]);
And now we need to combine these sub-entries by taking their Cartesian product and spreading into a single object for each entry. By Cartesian product I mean that if subentries looks like [[a,b],[c,d,e],[f]] then I need to get [[a,c,f],[a,d,f],[a,e,f],[b,c,f],[b,d,f],[b,e,f]], and then for each of those we spread into single entries. Here's that:
return subentries.reduce((a, v) => v.flatMap(vi => a.map(ai => ({ ...ai, ...vi }))), [{}])
}
Okay, so let's test it out:
console.log(getNestedFields(response, fields));
/* [{
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-01T00:00:00.000Z",
"device/telemetry/temperature": 100
}, {
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-02T00:00:00.000Z",
"device/telemetry/temperature": 100
}, {
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-03T00:00:00.000Z",
"device/telemetry/temperature": 100
}, {
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-04T00:00:00.000Z",
"device/telemetry/temperature": 100
}, {
"device/name": "Foo",
"device/telemetry/timeStamp": "2022-06-05T00:00:00.000Z",
"device/telemetry/temperature": 100
}] */
That's what you wanted. Even though you said you will never walk into different arrays, this version should support that:
console.log(getNestedFields({
a: [{ b: 1 }, { b: 2 }],
c: [{ d: 3 }, { d: 4 }]
}, ["a/b", "c/d"]))
/* [
{ "a/b": 1, "c/d": 3 },
{ "a/b": 2, "c/d": 3 },
{ "a/b": 1, "c/d": 4 },
{ "a/b": 2, "c/d": 4 }
]*/
There are probably all kinds of crazy edge cases, so anyone using this should test thoroughly.
Playground link to code
I am trying to figure out an easy way to convert an array of objects to an object
I have an array of objects that looks like this:
[
{
"id": "-LP9_kAbqnsQwXq0oGDT",
"value": Object {
"date": 1541482236000,
"title": "First",
},
},
.... more objects here
]
And id like to convert it to an object with the timestamps as the keys, and arrays of objects corresponding to that date. If that key already exists, then add the object to the corresponding array associated with that key
{
1541482236000:
[{
"id": "-LP9_kAbqnsQwXq0oGDT",
"value": Object {
"date": 1541482236000,
"title": "First",
},
},
{
"id": "-LP9_kAbqnsQwXqZZZZ",
"value": Object {
"date": 1541482236000,
"title": "Some other title",
},
},
.... more objects here
],
1541482236001:
[{
"id": "-LP9_kAbqnsQ1234",
"value": Object {
"date": 1541482236001,
"title": "Another title",
},
},
.... more objects here
]
}
I was able to achieve something similar using reduce. However it does not handle adding objects to the array when their key already exists.
calendarReminders = action.value.reduce((obj, reminder) => {
dateKey = moment(reminder.value.date).format('YYYY-MM-DD')
obj[dateKey] = [reminder]
return obj;
}, {});
How can I do this?
You just need to check whether the object is already a key and if not add it with the value of an array. Then you can just push() into it:
let arr = [{"id": "-LP9_kAbqnsQwXq0oGDT","value": {"date": 1541482236000,"title": "First",},},{"id": "SomID","value": {"date": 1541482236000,"title": "Some other title",},},{"id": "A different ID","value": {"date": 1541482236001,"title": "A third title",},}]
let calendarReminders = arr.reduce((obj, reminder) => {
(obj[reminder.value.date] || (obj[reminder.value.date] = [])).push(reminder)
return obj;
}, {});
console.log(calendarReminders)
If you want to set the keys to a different format with moment, you should be able to do that without changing the basic idea.
Please test the below code!
First you iterate through your array of data,
if your result object/dictionary already has the key then you just add the current item
otherwise you make the key and set the value
const data = [];
let result = {};
for (const item of data) {
const key = item.value.date;
if (result.hasOwnProperty(key)) {
const prevData = result[key];
result[key] = [...prevData, item];
} else {
result[key] = [item];
}
}
This is the sample json:
{
"search": {
"facets": {
"author": [
],
"language": [
{
"value": "nep",
"count": 3
},
{
"value": "urd",
"count": 1
}
],
"source": [
{
"value": "West Bengal State Council of Vocational Education & Training",
"count": 175
}
],
"type": [
{
"value": "text",
"count": 175
}
],
}
}
There are several ways to delete key search.facets.source:
delete search.facets.source
delete jsobObj['search']['facets']['source']
var jsonKey = 'source';
JSON.parse(angular.toJson(jsonObj), function (key, value) {
if (key != jsonKey)
return value;
});
Above 1 & 2 are not dynamic, and 3 is one of the way but not a proper way. Because if source is present in another node then it will not work. Please anybody can tell me how to delete it dynamically in any kind of nested key. Because we can not generate sequence of array dynamically in above 2.
Assuming you're starting from this:
let path = 'search.facets.source';
Then the logic is simple: find the search.facets object, then delete obj['source'] on it.
Step one, divide the path into the initial path and trailing property name:
let keys = path.split('.');
let prop = keys.pop();
Find the facets object in your object:
let parent = keys.reduce((obj, key) => obj[key], jsonObj);
Delete the property:
delete parent[prop];
I have found out another solution, it is very easy.
var jsonKey = 'search.facets.source';
eval('delete jsonObj.' + jsonKey + ';');
I have a javascript dictionary:
{
"a": {
"b": {
"c": null,
"d": null
}
}
}
How can I turn it into a JSON object which I can specify the name and children property? Is there any elegant way to do it?
The JSON object could be:
{
name:"a"
children: [{
name:"b",
children: [{
name:"c",
children: null
},{
name:"d",
children: null}]
}]
}
You could create a recursive function for generating your output:
var x = {
"a": {
"b": {
"c": null,
"d": null
}
}
};
function generate(item, key) {
var result = {
name: key,
children: []
};
for (var _ in item)
result.children.push(generate(item[_], _))
if (result.children.length == 0)
result.children = null;
return (key == undefined) ? result.children : result;
}
console.log(JSON.stringify(generate(x), null, 1));
Output:
[
{
"name": "a",
"children": [
{
"name": "b",
"children": [
{
"name": "c",
"children": null
},
{
"name": "d",
"children": null
}
]
}
]
}
]
The above generate function returns a list instead of a dictionary, because it's possible to have more than one name at the root level. But if we are sure that we have only one name at the root name, we can generate the json like this:
console.log(JSON.stringify(generate(x)[0], null, 1));
Here's my solution. It's similar to JuniorCompressor's.
function generate(obj) {
// Return primitives as-is
if (!(obj instanceof Object)) return obj;
// Loop through object properties and generate array
var result = [];
for (var key in obj) {
result.push({
name: key,
children: generate(obj[key])
});
}
// Return resulting array
return result;
}
As mentioned, the resulting object will actually be an array (in case there is more than one root-level property in the original object). If you really need the resulting object to be an object with only properties name and value, then you should access the first element of the resulting array, like this:
generate(obj)[0]
Solution
You need a recursive function, which calls itself for children. Note that in your example, there is only one top-level child (a). I instead use the assumption that the top-level 'name' refers to the name of the actual object itself. If you want to get results exactly like you demonstrate, from an object called 'obj', run toJSON(obj).children[0]. For the overall function, try something like the following:
function toJSON(obj, name) {
var subTree = {
name: name,
children: []
};
if (obj !== null && Object.keys(obj).length >= 1) {
for (var child in obj) {
subTree.children.push(toJSON(obj[child], child));
}
} else {
subTree.children = null;
}
return subTree;
}
Results of toJSON(obj).children[0]:
{
"name": "a",
"children": [{
"name": "b",
"children": [{
"name": "c",
"children": null
},{
"name": "d",
"children": null
}]
}]
}
Results of toJSON(obj, 'obj'):
{
"name": "obj",
"children": [{
"name": "a",
"children": [{
"name": "b",
"children": [{
"name": "c",
"children":null
},
{
"name": "d",
"children": null
}]
}]
}]
}
Here's a line-by-line explanation:
Declares the function, which expects two arguments: the object, and it's name. If you're going to be using toJSON(obj).children[0], though, don't bother with the second argument. It won't affect the result.
Declares the result, an object containing information about the current level and all levels below in the object. If you consider the object a tree, this result contains information about the current branch, and all it's branches.
Declares the property 'name', containing the name/key of the object at the current level. When you call the function, you need to include the name as second argument because there is no way of dynamically finding the name of a variable. They're passed into functions by value. As described above, though, if you're looking for results EXACTLY like those in your example, you're going to use toJSON(obj).children[0], instead of toJSON(obj, 'obj'), and then don't need to bother with the second argument.
Declares the children array, to be filled below
Terminates the declaration begun on Line 2
Checks if the object ISN'T null, and that it has children, using a handy method of the Object built-in object, running Lines 7, 8 and 9 if so
Iterates over the children of the object, running Line 8 for each child
Recursively runs the toJSON() function for each child, to get it's subTree. Because the children can't dynamically figure out their own names, it passes those in as well.
Terminates the for loop begun at Line 7
If there are no children, run Line 11. This is only run if Lines 7, 8 and 9 are not.
Sets children to null (only run if there are no children, as checked by Line 6)
Terminates the else started at line 10
Returns the current subTree, either to the function if called recursively by the function, or to you if you called it yourself
Terminates the function
Information about the Previous Version, Pre-edit
The original function only used one argument, whereas that above has another argument for 'name'. This is because the original tried to figure out the name of each level within that same level, which I have since realized isn't possible in Javascript. Basically, the original didn't work, and an extra argument had to be added to make it work. For records' sake, though, here was the original function:
// THIS FUNCTION DOESN'T WORK. IT'S HERE ONLY FOR HISTORICAL ACCURACY:
function toJSON(obj) {
var subTree = {
name: obj.constructor.name, // This should get the object key
children: []
};
if (Object.keys(obj).length >= 1) { // If there is at least one child
for (var child in obj) {
subTree.children.push(toJSON(obj[child]));
}
} else {
subTree.children = null;
}
return subTree;
}
Here is my Javascript code:
var subRow = [];
var rowarr = [];
subRow.push({ v: "Jay" });
subRow.push({ v: "Ram" });
rowarr.push({ c: subRow });
subRow.length = 0;
subRow.push({ v: "Jay1" });
subRow.push({ v: "Ram1" });
rowarr.push({ c: subRow });
console.log(JSON.stringify(rowarr));
The output is:
[{
"c": [{
"v": "Jay1"
}, {
"v": "Ram1"
}]
}, {
"c": [{
"v": "Jay1"
}, {
"v": "Ram1"
}]
}]
The expected output is:
[{
"c": [{
"v": "Jay"
}, {
"v": "Ram"
}]
}, {
"c": [{
"v": "Jay1"
}, {
"v": "Ram1"
}]
}]
Can anyone explain why it so?
Arrays are handled by reference.
subRow.length = 0; erases the contents of the array.
rowarr then contains two pointers to the same array (which only has the content in it that you put there after emptying it)
Change subRow.length = 0; to subRow = [] to work on a new array instead of modifying the existing one.
subRow points to an object. When you push it onto rowArr you create a reference to that object. You push it twice, that's two references to one object. When you edit subRow both references to the object see the changes, so you've trampled all over the old contents of the object - they are not stored anywhere else, so they are completely lost. You need to create a brand new object instead of editing the old object.