I have a JSON tree that contains nodes and children - the format is:
jsonObject =
{
id:nodeid_1,
children: [
{
id:nodeid_2,
children:[]
},
{
id:nodeid_3,
children:[
{
id:nodeid_4,
children:[]
},
{
id:nodeid_5,
children:[]
}
}
}
I don't know the depth of this tree, a node is capable of having many children that also have many children and so on.
My problem is that I need to add nodes into this tree by using a nodeID. For example, a function that can take a nodeID and the node object (including its children), would be able to replace that node within the tree - which as a result would become a bigger tree.
I have only come across recursive functions that allow me to traverse all the nodes within a JSON tree and a modification I have made of one of these functions returns me the node object - but doesn't help me as I need to modify the original tree:
var findNode = {
node:{},
find:function(nodeID,jsonObj) {
if( typeof jsonObj == "object" ) {
$.each(jsonObj, function(k,v) {
if(v == nodeID) {
findNode.node = $(jsonObj).eq(0).toArray()[0];
} else {
findNode.find(nodeID,v);
}
});
} else {
//console.log("jsobObj is not an object");
}
}
}
which allows me to do the following test:
findNode.find("nodeid_3",json);
alert(findNode.node);
So to sum up - how can I modify a value of a JSON tree that has an unknown depth?
Thanks in advance
If you want to modify a node, like you said, you can just modify the properties of that node.
var node = findNode.find("nodeid_3",json);
node.id = "nodeid_3_modified";
node.children = [];
Also, why are you using jQuery for this?
Here's an alternative without using jQuery should work:
function findNode(object, nodeId) {
if (object.id === nodeId) return object;
var result;
for (var i = 0; i < object.children.length; i++) {
result = findNode(object.children[i], nodeId);
if (result !== undefined) return result;
}
}
That is not JSON, it is a Javascript object literal. JSON is a certain way to encode a simple Javascript object into a string; your example is not written that way. Also, your code does not manipulate JSON objects (which are actually strings, not objects); it manipulates Javascript objects (which is a way simpler task).
That said, I'm not sure what your actual question is, but if it's about adding new elements to the children array, you can use Array.push:
findNode.find("nodeid_3",json);
findNode.node.children.push(child);
(This assumes that findNode.find actually works, which I'm pretty sure it does not.)
Related
I have a typical tree data structure that looks like this:
[
{
data: object,
subs:
[ ...other objects... ]
},
...other objects...
]
It can have any shape and number of nodes.
I wrote a method that should recursively find and return the path (an array of intermediate objects) between the root r and a given object o. (whether or not including r and o I don't care)
public getPath(tree: Array<object>, o: object): Array<object> {
let path: Array<object> = [];
function f(subtree: Array<object>): void {
for (let node of subtree) {
path.push(node['data']);
if (node['data'] == o) return;
else if (node['subs'].length > 0) f(node['subs']);
else path = [];
}
}
f(tree);
return path;
}
Basically my idea was to
always add every object to the array that is visited during the traversal from the r to o,
empty the array if a path is traversed that wasn't the way from the r to o,
and return if o is reached where the traversal should end.
Results:
It works for r as o, returns [r].
It also works for the first child of the r as o, returns [r, first child of r].
But for choosing any other object as o, not only the objects of the correct path but also many other objects of the tree are returned.
The flaw with your code is that you are using a global (out of f's scope anyway) path array. The problem is that you are clearing the entire array if a node doesn't match, whereas you should only cut the current part out. There are two ways to achieve what you want: first is to make f accepts an array path that it copies and pass over recursively untill it find the object, and the other way which is the best approach is to make use of the call stack (created by the recursion):
public getPath(tree: Array<object>, o: object): Array<object> {
function f(subtree: Array<object>) { // I don't know typescript, so specify the return type as (Array<object> or null)
for (let node of subtree) {
if (node.data == o) { // if this is the node we're looking for
return [node]; // return an array (which will be our path), here you can either return [] to exclude the matched node (o) or [node] to include it
} else if(node.subs.length) { // not the node we are looking for, but it has children, so let check'em out
let result = f(node.subs); // result will either be an array (if we recursively found something), or null otherwise
if(result) { // if we found something, then result will be the path from the current node to the object o (the current node not included)
result.unshift(node); // we include the current node by pushing it into the result array (pushing it to the first position)
return result; // return result (an array) to signal successfulness
}
}
}
return null; // the object o not found in this subtree, return null to signal unsuccessfullness. Kind of redundant, because undefined is returned by default, so feel free to remove it
}
return f(tree);
}
I have a Class containing keys, values and children like this
class Container{
key: string,
value: string,
children: Container[]
}
function searchKey(container: Container, key:string){
if (container.key == key) {
return container;
}
else if (container.children.length>0){
for (let child of container.children) {
let found = searchKey(child, key);
if (found != null) {
return found;
}
}
}
return null;
}
Input which I will be supplying to searchKey() function would be an array with deep objects and I want to get value for the supplied key parameter. But the current searchKey() function would not accept an array. How to make it to work with an array as input?
Your searchKey() function currently accepts a single Container and checks it for a match against key. If it fails, it iterates through the children array of Containers and calls itself recursively on each one.
If you only intend to call the function with an array of Container and never need to pass in a single Container, then you can turn the function inside-out like this:
function searchKey(containers: Container[], key: string): Container | null {
if (containers.length > 0) {
for (let container of containers) {
if (container.key == key) {
return container;
} else {
let found = searchKey(container.children, key);
if (found != null) {
return found;
}
}
}
}
return null;
}
(I tried to keep the above function in the same style as your original.)
This searchKey() function starts off by iterating through an array of Containers. It checks each Container for a key match, and if it doesn't find it, it calls itself recursively on the children array.
There are other ways of doing this, of course. Examples:
two mutually recursive functions;
a single dual-purpose function that accepts Container | Container[] arguments;
a simple shim which calls your existing searchKey() with a dummy Container object whose children is the array you want.
Which one is best depends on your use case.
Hope that helps; good luck.
I have a data:
data: function() {
return {
conversations:
[
]
}
}
I'm getting my data from response object: response.data.conversation
Is there a way to check this.conversations already contains response.data.conversation?
To build on your answer, if you're already using underscore or lodash you can use its _.any()/_.some() function:
var exists = _.any(this.conversations, function(conversation) {
return _.isEqual(conversation, response.data.conversation);
})
You can also use Array.prototype.some to do the same kind of thing:
var exists = this.conversations.some(function(conversation) {
return _.isEqual(conversation, response.data.conversation);
})
The benefits of these over your solution is that they'll return as soon as they find a match (instead of iterating through the whole array), though you could easily update your code to break out of the loop early.
Also, while _.isEqual() is cool, you might be able to get away with some simple property comparisons (if your objects are flat enough or, even better, you have a key that uniquely identifies a conversation) to determine if two objects are equivalent:
var exists = this.conversations.some(function(conversation) {
return conversation.id === response.data.conversation.id;
})
I figured it out:
Used underscore.js.
Iterate trought all objects in array and compare them with _.isEqual(a,b)
function
var count=0;
for(var i=0; i<this.conversations.length; i++ ) {
if(_.isEqual(this.conversations[i], response.data.conversation)) {
count++;
}
}
Then check the value of count variable:
if (count == 0) {
//Add element to array
this.conversations.push(response.data.conversation);
} else {
console.warn('exists');
}
I have an array of objects that can contain children of the same object type, like this:
var exampleArray = [
{
alias: 'alias1',
children: [
{
alias: 'child1'
},
{
alias: 'child2',
children: [
{
alias: 'child4'
},
{
alias: 'child5'
}
]
},
{
alias: 'child3'
}
]
},
{
alias: 'alias2'
},
{
alias: 'alias3',
children: [
{
alias: 'child6'
},
{
alias: 'child7'
}
]
}
];
The base object has other properties but they are not important to the question(s) at hand. For now, lets just assume the objects can be:
{
alias: 'string',
children: []
}
The children are optional.
I am looking for the best methods / fastest methods for managing some things with an object like this. I have created some recursive methods to do some of the things I want, but I want to know if there are better ways to go about doing the following tasks:
hasAlias(arr, alias) - I need to determine if the entire object contains any object with the give alias.
Currently, I do this recursively but given that this array can grow finitely, the recursive method is eventually going to hit stack limits.
getParent(arr, alias) - I need to be able to obtain the parent that contains an element with the given alias. Given that alias' are unique to the entire array, there will never be two of the same alias. Again I do this recursively right now but I want to find better methods of doing this.
deleteObject(arr, alias) - I am unsure how to accomplish this one currently. I need to be able to pass an array and an alias and have that object (and all its children) removed from the given array. I started a recursive method of doing this but stopped and decided to post here instead.
I am using Node.js and have lodash available for faster methods of doing things. I'm still fairly new to JavaScript so I am unsure if there are better ways to go about doing things with larger scale arrays like this.
Back in the days of FORTRAN which didn't support recursion, one achieved similar effects by changing a data set to simulate a level of "recursion". Applying this principle to the example object structure, a function to lookup an object by its "alias" (a name or id by another word) could be written without recursion like this:
function findAlias( parent, alias) // parent object, alias value string
{ function frame( parent)
{ return {parent: parent, children: parent.children,
index: 0, length: parent.children.length};
}
var stack, tos, child, children, i;
stack = [];
if( parent.children)
stack.push( frame( parent));
search:
while( stack.length)
{ tos = stack.pop(); // top of generation stack
children = tos.children;
for( i = tos.index; i < tos.length; ++i)
{ child = children[i];
if( child.alias == alias)
{ return { parent: tos.parent, child: child, childIndex: i}
}
if( child.children)
{ tos.index = i + 1;
stack.push(tos); // put it back
stack.push( frame(child));
continue search;
}
}
}
return null;
}
In short one ends up creating a stack of smallish data objects which are pushed and popped in the same function instead of making recursive calls. The example above returns and object with parent and child object values. The child value is the one with the supplied alias property, and the parent object is the one with the child in its children array.
It returns null if the alias could not be found so can be used for hasAlias functionality. If it doesn't return null it performs the getParent functionality. You do have to create a root node however:
// create a rootnode
var rootNode = { alias: "root", children: exampleArray};
var found = findAlias(rootNode, "alias3");
if( found)
{ console.log("%s is a child of %s, childIndex = %s",
found.child.alias, found.parent.alias, found.childIndex);
}
else
console.log("not found");
[Edit: add childIndex to search return object, update test example code, add conclusion.]
Conclusion
Using recursive function calls when supported for a tree walking application makes sense in terms of self documenting code and maintainability. A non recursive variation may pay for itself it if it can be shown that it has significant benefits in reducing server load under volume pressure tests, but requires sound documentation.
Regardless of internal coding, a tree walking function which returns an object with details of parent, child, and child index values might contribute to overall program efficiency by reducing the total number of treewalks ever performed:
truthiness of the search return value substitutes for an hasAlias function
the return object from the search can be passed to update, remove or insert functions without requiring repeated tree searches in each function.
The guaranteed fastest way is obviously to have
- an index for the aliases (thats actually a unique id)
- have a parent backlink on each child item (if it has a parent)
You look up against the id index
var index = {}
(function build(parent) {
index[parent.alias] = parent;
(parent.children || []).forEach( item => {
item.parent = parent
build(item)
})
})(objectRoot)
function hasAlias(alias) { return alias in index }
function getAlias(alias) { return index[alias] }
function getParent(alias) { return index[alias] && index[alias].parent}
Deleting an alias would mean removing it and its children from the index and from the parent that still remains in the index
function deleteAlias(alias) {
function deleteFromIndex(item) {
delete index[item.alias]
(item.children || []).forEach(deleteFromIndex)
}
var item = index[alias]
item.parent.children.splice(item.parent.children.indexOf(item))
deleteFromIndex(item)
}
I might approach your main array slightly differently, and keep it as a flat array that references other items rather than incorporates them entirely.
var flat = [
{
alias : "str1",
children : [ flat[1], flat[2] ],
parent : null
},
{
alias : "str1",
children : [],
parent : flat[0]
},
{
alias : "str1",
children : [],
parent : flat[0]
}
]
This is kind of a "linked list" approach. There are pros and cons to linked lists, but you'll be able to iterate over all items quickly.
TL;DR version: I want to avoid adding duplicate Javascript objects to an array of similar objects, some of which might be really big. What's the best approach?
I have an application where I'm loading large amounts of JSON data into a Javascript data structure. While it's a bit more complex than this, assume that I'm loading JSON into an array of Javascript objects from a server through a series of AJAX requests, something like:
var myObjects = [];
function processObject(o) {
myObjects.push(o);
}
for (var x=0; x<1000; x++) {
$.getJSON('/new_object.json', processObject);
}
To complicate matters, the JSON:
is in an unknown schema
is of arbitrary length (probably not enormous, but could be in the 100-200 kb range)
might contain duplicates across different requests
My initial thought is to have an additional object to store a hash of each object (via JSON.stringify?) and check against it on each load, like this:
var myHashMap = {};
function processObject(o) {
var hash = JSON.stringify(o);
// is it in the hashmap?
if (!(myHashMap[hash])) {
myObjects.push(o);
// set the hashmap key for future checks
myHashMap[hash] = true;
}
// else ignore this object
}
but I'm worried about having property names in myHashMap that might be 200 kb in length. So my questions are:
Is there a better approach for this problem than the hashmap idea?
If not, is there a better way to make a hash function for a JSON object of arbitrary length and schema than JSON.stringify?
What are the possible issues with super-long property names in an object?
I'd suggest you create an MD5 hash of the JSON.stringify(o) and store that in your hashmap with a reference to your stored object as the data for the hash. And to make sure that there are no object key order differences in the JSON.stringify(), you have to create a copy of the object that orders the keys.
Then, when each new object comes in, you check it against the hash map. If you find a match in the hash map, then you compare the incoming object with the actual object that you've stored to see if they are truly duplicates (since there can be MD5 hash collisions). That way, you have a manageable hash table (with only MD5 hashes in it).
Here's code to create a canonical string representation of an object (including nested objects or objects within arrays) that handles object keys that might be in a different order if you just called JSON.stringify().
// Code to do a canonical JSON.stringify() that puts object properties
// in a consistent order
// Does not allow circular references (child containing reference to parent)
JSON.stringifyCanonical = function(obj) {
// compatible with either browser or node.js
var Set = typeof window === "object" ? window.Set : global.Set;
// poor man's Set polyfill
if (typeof Set !== "function") {
Set = function(s) {
if (s) {
this.data = s.data.slice();
} else {
this.data = [];
}
};
Set.prototype = {
add: function(item) {
this.data.push(item);
},
has: function(item) {
return this.data.indexOf(item) !== -1;
}
};
}
function orderKeys(obj, parents) {
if (typeof obj !== "object") {
throw new Error("orderKeys() expects object type");
}
var set = new Set(parents);
if (set.has(obj)) {
throw new Error("circular object in stringifyCanonical()");
}
set.add(obj);
var tempObj, item, i;
if (Array.isArray(obj)) {
// no need to re-order an array
// but need to check it for embedded objects that need to be ordered
tempObj = [];
for (i = 0; i < obj.length; i++) {
item = obj[i];
if (typeof item === "object") {
tempObj[i] = orderKeys(item, set);
} else {
tempObj[i] = item;
}
}
} else {
tempObj = {};
// get keys, sort them and build new object
Object.keys(obj).sort().forEach(function(item) {
if (typeof obj[item] === "object") {
tempObj[item] = orderKeys(obj[item], set);
} else {
tempObj[item] = obj[item];
}
});
}
return tempObj;
}
return JSON.stringify(orderKeys(obj));
}
And, the algorithm
var myHashMap = {};
function processObject(o) {
var stringifiedCandidate = JSON.stringifyCanonical(o);
var hash = CreateMD5(stringifiedCandidate);
var list = [], found = false;
// is it in the hashmap?
if (!myHashMap[hash] {
// not in the hash table, so it's a unique object
myObjects.push(o);
list.push(myObjects.length - 1); // put a reference to the object with this hash value in the list
myHashMap[hash] = list; // store the list in the hash table for future comparisons
} else {
// the hash does exist in the hash table, check for an exact object match to see if it's really a duplicate
list = myHashMap[hash]; // get the list of other object indexes with this hash value
// loop through the list
for (var i = 0; i < list.length; i++) {
if (stringifiedCandidate === JSON.stringifyCanonical(myObjects[list[i]])) {
found = true; // found an exact object match
break;
}
}
// if not found, it's not an exact duplicate, even though there was a hash match
if (!found) {
myObjects.push(o);
myHashMap[hash].push(myObjects.length - 1);
}
}
}
Test case for jsonStringifyCanonical() is here: https://jsfiddle.net/jfriend00/zfrtpqcL/
Maybe. For example if You know what kind object goes by You could write better indexing and searching system than JS objects' keys. But You could only do that with JavaScript and object keys are written in C...
Must Your hashing be lossless or not? If can than try to lose compression (MD5). I guessing You will lose some speed and gain some memory. By the way, do JSON.stringify(o) guarantees same key ordering. Because {foo: 1, bar: 2} and {bar: 2, foo: 1} is equal as objects, but not as strings.
Cost memory
One possible optimization:
Instead of using getJSON use $.get and pass "text" as dataType param. Than You can use result as Your hash and convert to object afterwards.
Actually by writing last sentence I though about another solution:
Collect all results with $.get into array
Sort it with buildin (c speed) Array.sort
Now You can easily spot and remove duplicates with one for
Again different JSON strings can make same JavaScript object.