I'm building a xml to json function which converts our XML structure to a specifically formatted JSON.
I've tried many libraries, and ultimately, I've settled on just turning the XML into a DOM tree that I can walk and convert to JSON myself because the libraries don't provide the format I need.
So here's an example xml document:
var xml = '<document>' +
'<divisions>' +
'<division id="123" division="foo">' +
'<departments>' +
'<department id="456" department="bar"/>'+
'<department id="678" department="bar"/>'+
'</departments>' +
'</division>' +
'</divisions>' +
'<roles>' +
'<role id="123" name="foo"/>' +
'<role id="123" name="foo"/>' +
'</roles>' +
'</document>';
Desired output:
{ divisions: [ { id: '123', division: 'foo', departments: [ { id: '456', department: 'bar' } ] } ], roles: [ { id: '123', name: 'foo'}, { id: '123', name: 'foo'} ] }
Here's my initial stab at it:
var DOMParser = require('xmldom').DOMParser;
function XMLtoJSON(xml) {
var json = {};
var dom = new DOMParser().parseFromString(xml).childNodes[0];
function process(nodes, parent) {
var node, name, hasChildren;
for(var i = 0, l = nodes.length; i < l; i++) {
node = nodes[i];
name = node.tagName;
hasChildren = node.hasChildNodes();
if(!parent) {
json[name] = [];
}
if(node.hasAttributes()) {
var attributes = node.attributes, obj = {};
for(var x = 0, al = attributes.length; x < al; x++) {
obj[attributes[x].name] = attributes[x].value;
if(!parent) {
json[name].push(obj);
}
}
}
if(hasChildren) {
process(nodes[i].childNodes);
}
}
}
process(dom.childNodes);
return json;
}
console.log( XMLtoJSON(xml) );
Currently, this will output:
{ divisions: [],
division:
[ { id: '123', division: 'foo' },
{ id: '123', division: 'foo' } ],
departments: [],
department:
[ { id: '678', department: 'bar' },
{ id: '678', department: 'bar' } ],
roles: [],
role: [ { id: '123', name: 'foo' }, { id: '123', name: 'foo' } ] }
Note: I don't want to the parent document in my json, thus the initial parseFromString(xml).childNodes[0];
I'm wondering if anyone can help get me a little closer. Specifically, I'm having trouble wrapping my head around how to handle embedded collections (departments is an array within divisions). I've been working on this for a few hours and the recursion keeps tripping me up.
Sorry it took so long to answer. This was kind of hard one. Following code works at least with you current example XML. Tricky part is to decide which elements are arrays and which elements are not. The logic here is that everything with attributes is object and everything else is array.
function DOMToObject (node, obj, isArray) {
var child = node.firstChild,
attributes,
newObj,
i,
j,
k = 0;
if (!obj) {
obj = {};
}
while (child) {
attributes = child.attributes;
if (attributes && attributes.length) {
newObj = {};
DOMToObject(child, newObj);
for (i = 0, j = attributes.length; i < j; i++) {
newObj[attributes[i].name] = attributes[i].value;
}
} else {
newObj = [];
DOMToObject(child, newObj, true);
}
if (isArray) {
obj[k] = newObj;
k += 1;
} else {
obj[child.nodeName] = newObj;
}
child = child.nextSibling;
}
return obj;
};
var JSON = JSON.stringify(DOMToObject(dom.firstChild));
Related
I'm trying to manipulate this sample array of objects.
[ { name: 'John Wilson',
id: 123,
classes: ['java', 'c++']},
{ name: 'John Wilson',
id: 123,
classes: 'uml'},
{ name: 'Jane Smith',
id: 321,
classes: 'c++'} ]
What I need to do is to merge objects with the same 'id', concatenating 'classes' and keeping one 'name'.
The result should be:
[ { name: 'John Wilson',
id: 123,
classes: ['java', 'c++', 'uml']},
{ name: 'Jane Smith',
id: 321,
classes: 'c++'} ]
I tried using .merge but it doesn't concatenate the values from 'classes', it just keeps the values from the last equal object.
What is the simplest way to do that, using lodash?
The function you're looking for is _.uniqWith, with a special twist which I will explain in a minute.
_.uniqWith is a lot like _.uniq in that it generates a unique array, but it allows you to pass your own custom comparator function that will be called to determine what counts as "equality."
Sane programmers would understand that this comparator should be side-effect free. The way this code works is by breaking that rule, and using a comparison function that does extra magic behind the scenes. However, this results in very concise code that will work no matter how many of these objects are in your array, so I feel like the transgression is well-justified.
I named the comparator function compareAndMerge so as not to hide its impure nature. It will merge both classes arrays and update the relevant property on both objects, but only if their id values are identical.
function merge(people) {
return _.uniqWith(people, compareAndMerge)
}
function compareAndMerge(first, second) {
if (first.id === second.id) {
first.classes = second.classes = [].concat(first.classes, second.classes)
return true
}
return false
}
var people = [{
name: 'John Wilson',
id: 123,
classes: ['java', 'c++']
}, {
name: 'John Wilson',
id: 123,
classes: 'uml'
}, {
name: 'Jane Smith',
id: 321,
classes: 'c++'
}]
console.log(merge(people))
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.min.js"></script>
An aside: You were missing square brackets around your original classes lists. I made sure that the code above doesn't care whether or not the classes property holds a single string or an array of strings, though, just in case.
Using ES6 you can do so with a Map to hold the unique values, Array#reduce to populate it, and the spread operator with Map#values to convert it back to array:
const arr = [{"name":"John Wilson","id":123,"classes":["java","c++"]},{"name":"John Wilson","id":123,"classes":"uml"},{"name":"Jane Smith","id":321,"classes":"c++"}];
const result = [...arr.reduce((hash, { id, name, classes }) => {
const current = hash.get(id) || { id, name, classes: [] };
classes && (current.classes = current.classes.concat(classes));
return hash.set(id, current);
}, new Map).values()];
console.log(result);
Not sure using lodash... here's a way to do it with normal JS:
var combined = arr.reduce(function(a, item, idx) {
var found = false;
for (var i = 0; i < a.length; i++) {
if (a[i].id == item.id) {
a[i].classes = a[i].classes.concat(item.classes);
found = true;
break;
}
}
if (!found) {
a.push(item);
}
return a;
}, []);
Fiddle: https://jsfiddle.net/6zwr47mt/
use _.mergeWith to set merging customizer
_.reduce(data, function(result, item) {
item = _.mergeWith(
item,
_.find(result, {id: item.id}),
function(val, addVal) {
return _.isArray(val) ? _.concat(val, addVal) : val;
});
result = _.reject(result, {id: item.id})
return _.concat(result, item);
}, []);
The following algorithm is not the best one but at least I know what it does :-)
console.log(clean(data));
function clean (data) {
var i, x, y;
var clean = [];
var m = clean.length;
var n = data.length;
data.sort((x, y) => x.id - y.id);
for (i = 0; i < n; i++) {
y = data[i];
if (i == 0 || x.id != y.id) {
clean.push(x = clone(y)), m++;
} else {
clean[m - 1] = merge(x, y);
}
}
return clean;
}
function clone (x) {
var z = {};
z.id = x.id;
z.name = x.name;
z.classes = x.classes.slice();
return z;
}
function merge (x, y) {
var z = {};
z.id = x.id;
z.name = x.name;
z.classes = unique(
x.classes.concat(y.classes)
);
return z;
}
function unique (xs) {
var i, j, n;
n = xs.length;
for (i = 1; i < n; i++) {
j = 0; while (j < i && xs[i] !== xs[j]) j++;
if (j < i) swap(xs, i, n - 1), i--, n--;
}
return xs.slice(0, n);
}
function swap (xs, i, j) {
var x = xs[i];
xs[i] = xs[j];
xs[j] = x;
}
<script>
var data = [{
id: 123,
name: 'John Wilson',
classes: ['java', 'c++']
}, {
id: 123,
name: 'John Wilson',
classes: ['uml', 'java']
}, {
id: 321,
name: 'Jane Smith',
classes: ['c++']
}];
</script>
I have an array of tag names:
var tags = ['tagOne', 'tagTwo']
Which I want to use, to query the array below and get all items which match a tag.
var items =
[
{
'name': 'itemOne',
'tags': [
{ name: 'tagOne' }
]
},
{
'name': 'itemTwo',
'tags': [
{ name: 'tagTwo' }
]
}
];
How can I do this with linq Js? I.E in this case both items would be returned
Try this; it may not be the most efficient way (I've never used linq.js before) but it will work:
// Enumerate through the items
var matches = Enumerable.From(items)
.Where(function(item) {
// Enumerate through the item's tags
return Enumerable.From(item.tags).Any(function(tag) {
// Find matching tags by name
return Enumerable.From(tags).Contains(tag.name);
})
})
.ToArray();
This should work for you:-
Items
var items =
[
{
'name': 'itemOne',
'tags': [
{ name: 'tagOne' }
]
},
{
'name': 'itemTwo',
'tags': [
{ name: 'tagTwo' }
]
},
{
'name': 'itemThree',
'tags': [
{ name: 'tagThree' }
]
}
];
Tags:-
var tags = ['tagOne', 'tagTwo'];
Search for Tags:-
var fillteredItems = items.filter(function(item){
var tagsInItem = item["tags"];
for (var i = 0; i < tags.length; i++) {
for (var j = 0; j < tagsInItem.length; j++) {
if(tags[i]==tagsInItem[j].name)
return item;
};
};
});
Print Results:-
fillteredItems.forEach(function(item){
console.log("items",item);
})
I want to store the "node indentation string" for each object, something like this:
foo
┣bar
┃┗baz
┃ ┗qux
┃ ┣quux
┃ ┗corge
┣fizz
┗buzz
Given data for each object:
objects = [
{'id':1,'parent_id':null, 'name':'foo'}
{'id':2,'parent_id':1, 'name':'bar'}
];
Note that I don't want to print anything, I just want to work out the indent as an array of characters for each object:
{'id':6,'parent_id':4, 'name':'corge', 'indent':['┃',' ',' ','┗']}
So far I can only indent them with spaces but no 'pipes' and I am stumped at coming up with a solution. Any help?
I am using JS with Angular if it helps.
EDIT: As requested the code I have so far. I didn't post this at first because I felt that it's a wrong foundation/approach to build on. How it works is pretty trivial: for each object, count it's ancestors and add " "'s accordingly.
// go through all our objects and set their indent strings
setIndents = function()
{
for (var x in objects) {
var o = objects[x];
o.nodes = [];
// push space character for amount of ancestors
numParents = countParents(o, 0);
for (var i = 0; i < numParents; i++)
o.nodes.push(" ");
}
};
// recursively counts how many ancestors until we hit the root
countParents = function(current, count)
{
if (current.parent_id !== null) {
for (var x in objects) {
if (objects[x].id == current.parent_id) {
current = objects[x]; //set as new current
count++;
break;
}
}
return countParents(current, count);
} else {
return count;
}
};
As #JBCP pointed out (see comments) there is a serious flaw in my original code that would break the whole thing if the initial order was anything but perfect.
So here's an updated version, the order of elements can now be random (it still plays a role in such that it indirectly defines the children order, but the tree-structure will be correct).
I also split the functions so that they can be better configured. For example treeIndent now expects a node branch produced by treeify. (Note: the shuffle function is just there to test the order independence)
'use strict';
/**
* #see https://bost.ocks.org/mike/shuffle/
*
* #param array
* #returns {*}
*/
function shuffle(array) {
var m = array.length, t, i;
// While there remain elements to shuffle…
while (m) {
// Pick a remaining element…
i = Math.floor(Math.random() * m--);
// And swap it with the current element.
t = array[m];
array[m] = array[i];
array[i] = t;
}
return array;
}
function treeify(flat) {
var map = { __root__: { children: [] }};
flat.forEach(function (node) {
var
parentId = node.parent_id || '__root__',
id = node.id;
// init parent
if (!map.hasOwnProperty(parentId)) {
map[parentId] = { element: null, children: [] };
}
// init self
if (!map.hasOwnProperty(id)) {
map[id] = { element: null, children: [] };
}
map[id].element = node;
map[parentId].children.push(map[id]);
});
return map.__root__.children;
}
function treeIndent(branch, cfg, decorator, indent)
{
indent = indent || [];
branch.forEach(function (node, i) {
decorator(node.element, indent.concat(
i === branch.length - 1 ? cfg.isLastChild : cfg.hasNextSibling
));
treeIndent(node.children, cfg, decorator, indent.concat(
i === branch.length - 1 ? cfg.ancestorIsLastChild : cfg.ancestorHasNextSibling
));
});
}
var input = [
{ id: 1, parent_id: null, name: 'root' },
{ id: 2, parent_id: 1, name: 'bar' },
{ id: 5, parent_id: 2, name: 'baz' },
{ id: 6, parent_id: 5, name: 'qux' },
{ id: 7, parent_id: 6, name: 'quux' },
{ id: 8, parent_id: 6, name: 'corge' },
{ id: 9, parent_id: 2, name: 'but' },
{ id: 3, parent_id: 1, name: 'fizz' },
{ id: 4, parent_id: 1, name: 'buzz' }
];
var log = document.getElementById('log');
treeIndent(treeify(shuffle(input)), {
hasNextSibling: '├',
isLastChild: '└',
ancestorHasNextSibling: '│',
ancestorIsLastChild: ' '
}, function (element, indent) {
log.innerHTML += indent.join(' ') + ' ' + element.name + "\n";
});
<pre id="log"></pre>
Old answer (broken!):
try the following:
function makeTree(flat) {
var map = { __root__: { children: [] }};
flat.forEach(function (node) {
var
parentId = node.parent_id || '__root__',
id = node.id;
// init parent
if (!map.hasOwnProperty(parentId)) {
map[parentId] = { children: [] };
}
// init self
if (!map.hasOwnProperty(id)) {
map[id] = { children: [] };
}
map[id].element = node;
map[parentId].children.push(map[id]);
});
return map.__root__.children;
}
function injectTreeIndent(input) {
var
levelMap = [],
indicators = {
hasNextSibling: '┣',
isLastChild: '┗',
ancestorHasNextSibling: '┃',
ancestorIsLastChild: ' '
}
;
// apply `indent`
(function traverse(branch, depth) {
branch.forEach(function (node, idx) {
node.element.indent = levelMap.map(function (ancestor) {
return ancestor === indicators.hasNextSibling ? indicators.ancestorHasNextSibling : indicators.ancestorIsLastChild;
});
// if (depth > 0) { // uncomment this, if root elements should have no indentation
node.element.indent.push(
levelMap[depth] = branch.length - 1 > idx ? indicators.hasNextSibling : indicators.isLastChild
);
// }
traverse(node.children, depth + 1);
levelMap.pop();
});
}(makeTree(input), 0));
}
var input = [
{ id: 1, parent_id: null, name: 'foo' },
{ id: 2, parent_id: 1, name: 'bar' },
{ id: 5, parent_id: 2, name: 'baz' },
{ id: 6, parent_id: 5, name: 'qux' },
{ id: 7, parent_id: 6, name: 'quux' },
{ id: 8, parent_id: 6, name: 'corge' },
{ id: 3, parent_id: 1, name: 'fizz' },
{ id: 4, parent_id: 1, name: 'buzz' }
];
injectTreeIndent(input);
makeTree is used to optain a nested structure derived from the given flat data.
injectTreeIndent then traverses that nested structure to inject the required indent informatoin.
demo: http://jsfiddle.net/6R7wf/1/
demo with root elements having no indenation: http://jsfiddle.net/zMY7v/
After for (var i = 0; i < numParents; i++) o.nodes.push(" ");, try
if (o.nodes.length === 1)
o.nodes[0] = "┣";
else if (o.nodes.length > 1) {
o.nodes[0] = "┃";
o.nodes[o.nodes.length - 1] = "┗";
}
I have a multidimensional array but the ID's are unique across parents and children, so I have a problem looping through using a for loop. The problem is that I cannot seem to grab the ID of the children. How do you think I should handle this?
var Options = [
{
id: 0,
children: []
},
{
id: 2,
children: []
},
{
id: 3,
children: [
{
id: 4,
children: []
},
{
id: 5,
children: []
},
{
id: 6,
children: []
}
]
},
{
id: 7,
children: [
{
id: 8,
children: []
},
{
id: 9,
children: []
}
]
}
];
I have kept the code concise for the sake of brevity. What I am trying to do is iterate through the array to compare ID's.
This does not look like a "multidimensional array", but rather like a tree. Looping one level can be done with a simple for-loop:
for (var i=0; i<Options.length; i++) // do something
To loop the tree in-order, you will need a recursive function:
function loop (children, callback) {
for (var i=0; i<children.length; i++) {
callback(children[i]);
loop(children[i].children, callback);
}
}
loop(Options, console.log);
To get all children by their id, so that you can loop through the ids (regardless of the tree structure), use a lookup table:
var nodesById = {};
loop(Options, function(node) {
nodesById[node.id] = node;
});
// access:
nodesById[4];
…and to loop them sorted by id, you now can do
Object.keys(nodesById).sort(function(a,b){return a-b;}).forEach(function(id) {
var node = nodesById[id];
// do something
});
How about recursion?
var findById = function (arr, id) {
var i, l, c;
for (i = 0, l = arr.length; i < l; i++) {
if (arr[i].id === id) {
return arr[i];
}
else {
c = findById(arr[i].children, id);
if (c !== null) {
return c;
}
}
}
return null;
}
findById(Options, 8);
Ah, use recursion :D
var Options = "defined above";//[]
var OptionArray = []; //just as an example (not sure what you want to do after looping)
(function looper(start){
for( var i = 0, len = start.length; i < len; i++ ){
var currentOption = start[i];
if( currentOption.id > 3 ){//could be more complex
OptionArray.push(currentOption);
}
if( currentOption.children.length > 0 ){
looper(currentOption.children);
}
}
})(Options);
So I have an object, that I'm using in nodejs. It looks as such:
for(var i = 0; i < x.length; i++) {
var sUser = x[i];
mUsers[sUser.userid] = CreateUser(sUser);
++mUsers.length;
}
So I'm pulling information from an external source, and it breaks down as an array full of instances of this:
[{ name: 'Michael Lovesllamas Lankford',
created: 1338420951.11,
laptop: 'pc',
laptop_version: null,
userid: '4fc6aed7eb35c14ad6000057',
acl: 0,
fans: 1,
points: 5,
avatarid: 34 }]
and so forth.
so that information is passed as x in the above function.
global.mUsers = {length:0}
global.UserBase = {
userid: -1,
name: "noidea",
isSuperUser: false,
isDJ: false,
laptop: "pc" };
process.on("registered", OnRegistered);
global.OnRegistered = function(a) {
//misc code here
RegisterUsers(a.users);
//misc code here
}
global.CreateUser = function(a) {
var b = UserBase;
b.userid = a.userid;
b.name = a.name;
b.laptop = a.laptop;
if (a.acl > 0) b.isSuperUser = true;
return b;
};
global.RegisterUsers = function(x) {
for(var i = 0; i < x.length; i++) {
var sUser = x[i];
mUsers[sUser.userid] = sUser;
++mUsers.length;
}
}
Now, I've logged it in the loop, and mUsers[sUser.userid] does indeed = sUser.
but when I console.log(mUsers) immediately after the loop, I get this:
{
userid1: { userid: userid3, name: name3, item: item3 },
userid2: { userid: userid3, name: name3, item: item3 },
userid3: { userid: userid3, name: name3, item: item3 }
}
And I don't know why it's overwriting. Any ideas?
The main problem is that you where continuously referencing the same object when you where calling CreateUser, as such it was simply updating and returning a reference which was being kept through out all the calls, this is why when you where printing it, it just printed the last update.
You need to create a copy of the object.
global.CreateUser = function(a) {
var b = Object.create(UserBase); // this will create a copy of it.
b.userid = a.userid;
b.name = a.name;
b.laptop = a.laptop;
if (a.acl > 0) b.isSuperUser = true;
return b;
};
now CreateUser is actually creating a copy, when you go through the properties the default ones may not appear right away, but theres still there, they've being simply moved to __proto__ you can still call them.
Try the below it is working for me
var obj = {
userid1: { userid: "userid1", name: "name3", item: "item3" },
userid2: { userid: "userid2", name: "name3", item: "item3" },
userid3: { userid: "userid3", name: "name3", item: "item3" }
};
var muser = {};
for (var key in obj) {
muser[key] = obj[key];
}