I have been playing around with some Nested Set Models (NSM). One thing I wanted to do is to be able to generate a NSM from a given JavaScript object.
For example, given the following object:
var data = {
Clothes: {
Jackets: {
Waterproof: true,
Insulated: true
},
Hats: true,
Socks: true
},
}
I'd like to generate an array of objects like so.
[
{
"name": "Clothes",
"lft": 1,
"rgt": 12
},
{
"name": "Jackets",
"lft": 2,
"rgt": 7
},
{
"name": "Waterproof",
"lft": 3,
"rgt": 4
},
{
"name": "Insulated",
"lft": 5,
"rgt": 6
},
{
"name": "Hats",
"lft": 8,
"rgt": 9
},
{
"name": "Socks",
"lft": 10,
"rgt": 11
}
]
That is - a depth first walk through the object, assigning an ID and counting the left and right edge for each object in the hierarchy. So that each node has a unique ID and the correct lft and rgt values for a NSM.
I've tried various approaches but just can't seem to get the result I am after...I had some success by altering the model to use properties for the node name and child nodes - i.e.
var data2 = {
name: "Clothes",
children: [{
name: "Jackets",
children: [{
name: "Waterproof",
}, {
name: "Insulated"
}]
}, {
name: "Hats"
},
{
name: "Socks"
}
]
};
function nestedSet(o, c, l = 0) {
let n = {
name: o.name,
lft: l + 1
};
c.push(n);
let r = n.lft;
for (var x in o.children) {
r = nestedSet(o.children[x], c, r);
}
n.rgt = r + 1;
return n.rgt;
}
let out = [];
nestedSet(data2, out);
console.log(out)
This gives the correct result but requires altering the input data...is there a way to generate the same Nested Set Model using the original data object?
I actually managed to solve this in the end...I just forgot about it for a long while! Basically all that is required is to reclusively pass the Object.entries as kindly suggested in #CherryDT's comment. This way one can resolve the name/children to build the nested set model as required.
var data = {
Clothes: {
Jackets: {
Waterproof: {},
Insulated: {},
},
Hats: {},
Socks: {},
},
};
function ns(node, stack = [], lft = 0) {
var rgt = ++lft;
var item = {
name: node[0],
lft: lft,
};
stack.push(item);
Object.entries(node[1]).forEach(function (c) {
rgt = ns(c, stack, rgt);
});
item.rgt = ++rgt;
return rgt;
}
var result = [];
ns(Object.entries(data)[0], result);
console.log(result);
Related
I've coded and I've got the results I want, but I think this code looks unclean.
const verses = [{
index: 1,
verses: [{
level: 1
},
{
level: 1
}
]
},
{
index: 2,
verses: [{
level: 1
},
{
level: 1
}
]
},
{
index: 3,
verses: [{
level: 2
}]
},
{
index: 4,
verses: [{
level: 2
}]
},
{
index: 5,
verses: [{
level: 2
},
{
level: 2
}
]
},
{
index: 6,
verses: [{
level: 3
},
{
level: 3
}
]
},
{
index: 7,
verses: [{
level: 3
},
{
level: 3
},
{
level: 4
},
{
level: 4
}
]
}
]
function getVerseIndexByLevel(level) {
const result = verses.map(v => {
const mappingByLevel = Math.max.apply(Math, [...new Set(v.verses.map(w => w.level))])
const mappingByVerse = v.index
return {
level: mappingByLevel,
verse: mappingByVerse
}
}).filter(v => v.level === level).map(v => v.verse)
return result
}
for (let i = 1; i <= 4; i++) {
console.log({
level: i,
verse: getVerseIndexByLevel(i)
})
}
Is my code above consuming too much performance?
Could you make it cleaner and simpler without changing the result at all?
-I don't know what details I have to add more, StackOverflow forced me to write this because the post is mostly code. but I hope you understand just by looking at the results of the code.-
On the performance end, the one thing I can see that could improve it would be to call getVerseIndexByLevel only once, and use the calculated result mapping, instead of calling it multiple times (requiring re-parsing the input every time). Something like
const verseIndicies = getVerseIndicies();
for (let i = 1; i <= 4; i++) {
console.log({
level: i,
verse: verseIndicies[i]
})
}
Another slight improvement - since you're using ES6 syntax, you don't need to .apply to Math.max - just spread the set into it.
You also don't need to turn the level numbers into a Set first - since it's being passed into Math.max anyway, repetitions aren't an issue.
const verses=[{index:1,verses:[{level:1},{level:1}]},{index:2,verses:[{level:1},{level:1}]},{index:3,verses:[{level:2}]},{index:4,verses:[{level:2}]},{index:5,verses:[{level:2},{level:2}]},{index:6,verses:[{level:3},{level:3}]},{index:7,verses:[{level:3},{level:3},{level:4},{level:4}]}];
function getVerseIndicies() {
const verseIndicies = {};
for (const v of verses) {
const level = Math.max(...v.verses.map(w => w.level));
verseIndicies[level] ??= [];
verseIndicies[level].push(v.index);
}
return verseIndicies;
}
const verseIndicies = getVerseIndicies();
for (let i = 1; i <= 4; i++) {
console.log({
level: i,
verse: verseIndicies[i]
})
}
I'm trying to set variables from a JSON object that I retrieve with a POST query. But the results aren't returned in the same order every time, so it doesn't always work. I'm not sure how to correctly set my variables with the array positions not remaining constant:
var idle = Example1.results[0].data[1].stats.count;
var waiting = Example1.results[1].data[0].stats.count;
(i.e. This works on example 1, but not example 2)
Example1 = {"results":[{"group":{"queueId":"someID"},"data":[{"metric":"oOnQueueUsers","qualifier":"INTERACTING","stats":{"count":2}},{"metric":"oOnQueueUsers","qualifier":"IDLE","stats":{"count":5}}]},{"group":{"queueId":"someID","mediaType":"voice"},"data":[{"metric":"oWaiting","stats":{"count":0}}]}]}
Example2 = {"results":[{"group":{"queueId":"someID","mediaType":"voice"},"data":[{"metric":"oWaiting","stats":{"count":1}}]},{"group":{"queueId":"someID"},"data":[{"metric":"oOnQueueUsers","qualifier":"INTERACTING","stats":{"count":4}},{"metric":"oOnQueueUsers","qualifier":"IDLE","stats":{"count":6}}]}]}
You can use find() and some() to get the result you want.
const example1 = { results: [ { group: { queueId: "someID" }, data: [ { metric: "oOnQueueUsers", qualifier: "INTERACTING", stats: { count: 2 }, }, { metric: "oOnQueueUsers", qualifier: "IDLE", stats: { count: 5 } }, ], }, { group: { queueId: "someID", mediaType: "voice" }, data: [{ metric: "oWaiting", stats: { count: 0 } }], }, ], };
const example2 = { results: [ { group: { queueId: "someID", mediaType: "voice" }, data: [{ metric: "oWaiting", stats: { count: 1 } }], }, { group: { queueId: "someID" }, data: [ { metric: "oOnQueueUsers", qualifier: "INTERACTING", stats: { count: 4 }, }, { metric: "oOnQueueUsers", qualifier: "IDLE", stats: { count: 6 } }, ], }, ], };
const idle1 = example1.results
.find(a => a.data.some(d => d.qualifier === "IDLE"))
.data.find(b => b.qualifier === "IDLE").stats.count;
const waiting1 = example1.results
.find(a => a.data.some(d => d.metric === "oWaiting"))
.data.find(b => b.metric === "oWaiting").stats.count;
const idle2 = example2.results
.find(a => a.data.some(d => d.qualifier === "IDLE"))
.data.find(b => b.qualifier === "IDLE").stats.count;
const waiting2 = example2.results
.find(a => a.data.some(d => d.metric === "oWaiting"))
.data.find(b => b.metric === "oWaiting").stats.count;
console.log({ idle1 }, { waiting1 }, { idle2 }, { waiting2 });
If you cannot depend on the order being the same every time... or given your example data that each object doesn't even have the same properties... then you shouldn't search the resultant json for your data.
Rather then, you need to create your own data structure against which you will be able to search/index and then loop through your json, parsing each level within to decide how to map that particular element to your new data structure.
Here's an example of what you might could do...
var exampleResults1 = {
"results": [{
"group": {
"queueId": "someID"
},
"data": [{
"metric": "oOnQueueUsers",
"qualifier": "INTERACTING",
"stats": {
"count": 2
}
},
{
"metric": "oOnQueueUsers",
"qualifier": "IDLE",
"stats": {
"count": 5
}
}
]
},
{
"group": {
"queueId": "someID",
"mediaType": "voice"
},
"data": [{
"metric": "oWaiting",
"stats": {
"count": 0
}
}]
}
]
}
var newDataSource = {
parseResults: function(resultObj) {
resultObj.results.forEach(function(result) {
var queueID = result.group.queueId;
if (!newDataSource.hasOwnProperty(queueID)) {
newDataSource[queueID] = {
data: {}
};
}
var newDataSourceQueue = newDataSource[queueID];
result.data.forEach(function(dataObj) {
var metric = dataObj.metric;
if (!newDataSourceQueue.data.hasOwnProperty(metric)) {
newDataSourceQueue.data[metric] = {};
}
var queueMetric = newDataSourceQueue.data[metric];
var qualifier = "noQualifier";
if (dataObj.hasOwnProperty("qualifier")) {
qualifier = dataObj.qualifier;
}
queueMetric[qualifier] = {};
var metricQualifier = queueMetric[qualifier];
var statKeys = Object.keys(dataObj.stats);
statKeys.forEach(function(stat) {
if (!metricQualifier.hasOwnProperty(stat)) {
metricQualifier[stat] = dataObj.stats[stat];
}
});
});
});
}
};
newDataSource.parseResults(exampleResults1);
console.log(JSON.stringify(newDataSource));
console.log("IDLE Count = " + newDataSource["someID"]["data"]["oOnQueueUsers"]["IDLE"]["count"]);
Running this code, you should be able to see what the new data structure looks like after its been populated with values from your original json object. Notice how the keys of the objects are values from the original json.
My example code here doesn't take into account all data points in your example result sets... but should be enough to illustrate that you need to understand the data you are getting back and be able to come up with a consolidated data structure to encapsulate it using property keys that come from the actual returned results.
The following example demonstrates what I would like to achieve:
const data_template = {
key: 1,
data_object: {
data1: 1,
data2: 2,
data3: 3
},
data_array: [
{ key: 1, data1: 1 },
{ key: 2, data1: 2 },
{ key: 3, data1: 3 }
]
}
let data = [];
data.push({ ...data_template, key: 1 });
data.push({ ...data_template, key: 2 });
console.log(data);
By using destructuring I can easily modify my "template object" (data_template) while adding instances of it to my "data array". The result of the code above is:
[
{
key: 1,
data_object: { data1: 1, data2: 2, data3: 3 },
data_array: [ [Object], [Object], [Object] ]
},
{
key: 2,
data_object: { data1: 1, data2: 2, data3: 3 },
data_array: [ [Object], [Object], [Object] ]
}
]
As you can see, there are 2 objects in my array (data), each with a unique key (1 and 2, respectively).
Now my question: How can I modify the keys within data_array, which is an array within my "data template"? Is there a smart way to do this by destructuring as well?
Depends on what you're trying to do.
If you simply want to clone the array with extra elements:
data.push({
...data_template,
key: 1,
data_array: [
...data_template.data_array,
{ key: 4, data1: 4},
],
});
If you want to e.g. add a new field to all the objects inside data_array:
const data_template = {
key: 1,
data_object: { data1: 1, data2: 2, data3: 3 },
data_array: [ { key: 1, data1: 1 }, { key: 2, data1: 2 }, { key: 3, data1: 3 }]
}
console.log({
...data_template,
key: 1,
data_array: data_template.data_array.map(d => ({ ...d, newField: 123 })),
});
which you could also use to overwrite the key field
Otherwise I can't immediately think of any other destructuring "tricks" you could use.
The short answer
No, you cant.
JavaScript destructuring only allows you to override root level keys of an object, which replaces their value.
(You also usually wouldn't do this, to keep cognitive complexity low.)
The long answer
There are actually better ways to do this.
Usually when creating a new object by spreading becomes more complex, you'd simply create a function for each level of nesting, and give it a better name than I did, that correlates to your domain.
This is exactly what functions are made for; to split pieces of code to reduce complexity.
Simple example
function createDataEntry(key, data) {
return { key, data }
}
function createRecord(key, data) {
return {
key,
dataObject: {
data,
},
dataArray: [
createDataEntry(key, data),
// or simply:
// { key, data },
]
}
}
let data = [];
data.push(createRecord(1, 'foo'));
data.push(createRecord(2, 'bar'));
console.log(JSON.stringify(data, null, 2));
produces
[
{
"key": 1,
"dataObject": {
"data": "foo"
},
"dataArray": [
{
"key": 1,
"data": "foo"
}
]
},
{
"key": 2,
"dataObject": {
"data": "bar"
},
"dataArray": [
{
"key": 2,
"data": "bar"
}
]
}
]
Add + Edit example
If you want to add or edit an entry from that data array, you'll have to do a check to see if an entry already exists.
If it already exists, update it using map.
If it does not exist, use array.push().
function addOrUpdateEntry(myObject, key, data) {
let updated = false;
const updatedObject = {
...myObject,
items: myObject.items.map(member => {
if (member.key === key) {
updated = true;
return { ...member, data };
}
return member;
})
};
if (!updated) {
updatedObject.items.push({ key, data });
}
return updatedObject;
}
let data = {
items: []
};
data = addOrUpdateEntry(data, 1, "foo");
data = addOrUpdateEntry(data, 2, "bar");
data = addOrUpdateEntry(data, 2, "baz");
console.log(JSON.stringify(data, null, 2));
Which will output
{
"items": [
{
"key": 1,
"data": "foo"
},
{
"key": 2,
"data": "baz"
}
]
}
Note that mutating an existing object can easily make for undesired behaviour in JavaScript, hence the creation of a new object and returning that from the function.
This concept is called immutability.
I have this JSON
{
"people": [{
"games": [
"destiny",
"horizon",
"fifa",
"cuphead"
],
"name": "Cartman"
},
{
"games": [
"fifa",
"pes"
],
"name": "Kyle"
},
{
"games": [
"cuphead",
"destiny"
],
"name": "Stan"
},
{
"games": [
"pokemon",
"metroid"
],
"name": "Clyde"
},
{
"games": [
"fifa",
"metroid",
"pes"
],
"name": "Butters"
}
]
}
and I need to get the users that have at least two games in common so for this JSON the result will be:
Cartman and Stan plays destiny and cuphead
Kyle and Butters plays fifa and pes
Note: Clyde will be not in the results because theres only one game match with other player
I thought about two workarounds
1- have a dictionary using two tags (covering all possibilities) as a key and filled it if it doesnt exist and after other key match that way I will know that theres already two matches
2- get the first user compare it against the others and it if matches at least two printed it then continue with the next one and compare with the rest and so on
Any help will be appreciated thanks
An approach with reducing the games/peoples by checking if only one exists and delet then the respondend part of the other object.
It returns more items than one, but more than two.
For getting a result, you could eliminate the one with two parts and adjust the rest.
var object = { people: [{ games: ["destiny", "horizon", "fifa", "cuphead"], name: "Cartman" }, { games: ["fifa", "pes"], name: "Kyle" }, { games: ["cuphead", "destiny"], name: "Stan" }, { games: ["pokemon", "metroid"], name: "Clyde" }, { games: ["fifa", "metroid", "pes"], name: "Butters" }] },
peoples = {},
games = {},
change = false;
object.people.forEach(function (person) {
person.games.forEach(function (game) {
games[game] = games[game] || {};
games[game][person.name] = true;
peoples[person.name] = peoples[person.name] || {};
peoples[person.name][game] = true;
});
});
do {
change = false;
Object.keys(games).forEach(function (k) {
var keys = Object.keys(games[k]);
if (keys.length === 1) {
delete peoples[keys[0]][k];
delete games[k];
change = true;
}
});
Object.keys(peoples).forEach(function (k) {
var keys = Object.keys(peoples[k]);
if (keys.length === 1) {
delete games[keys[0]][k];
delete peoples[k];
change = true;
return;
}
});
} while (change);
console.log(peoples);
console.log(games);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have a JSON file
{
"data": [
{
"name": "Jake",
"id": "123"
},
{
"name": "Bob",
"id": "234"
}]
}
with all id's unique, and say I have an array of banned ids ["123","423"] and I would like to delete all entries that have an id number in the array (so as an output I'd like the following).
{
"data": [
{
"name": "Bob",
"id": "234"
}]
}
What would be a moderately efficient way (runs in a few seconds on an ordinary computer) to achieve this if there's a few thousand entries in the JSON and array?
You can use the Array.prototype.filter() method in conjunction with .indexOf():
var bannedIds = ["123", "423"];
var input = {
"data": [
{
"name": "Jake",
"id": "123"
},
{
"name": "Bob",
"id": "234"
}]
};
input.data = input.data.filter(function(v) {
return bannedIds.indexOf(v.id) === -1;
});
console.log(input);
If you don't want to overwrite the original array then just assign the result of the .filter() call to a new variable.
If the above turns out to be too slow with your large amount of data, you can try replacing .filter() with a conventional for loop, and/or replacing .indexOf() with a lookup object created from the array of banned ids.
If you can use ES6, you can do this:
const source = {
"data": [
{
"name": "Jake",
"id": "123"
},
{
"name": "Bob",
"id": "234"
}
]
};
const banned = ["123", "423"];
// O(n) startup cost for constant access time later
const bannedSet = new Set(banned);
// O(n)
const result = source.data.filter(x => !bannedSet.has(x.id));
console.log(result);
As mentioned in the comments, there's a startup cost for creating the Set. However, this lets you then call Set.prototype.has, which is constant.
Then, it's just a matter of iterating over every element and filtering out the ones that are in the banned set.
If you can't use ES6, you could replace Set with a plain JS object. If you have to support IE<9, use a polyfill for Array.prototype.filter (thanks #nnnnnn).
UPDATE
#SpencerWieczorek points out that the ES6 spec seems to indicate that Set.prototype.has iterates. I spoke too soon about the lookup being constant (I was carrying over my experience from other languages). Typically, sets will do better than O(n), e.g. constant or O(log n) depending on the underlying implementation. Your mileage may vary, so nnnnnn's answer may be faster in some cases.
Try a few of the solutions here with large amounts of data to confirm.
EDIT
I shied away from using filter or the like because that involves creating a new array. That's actually probably fine for the data sizes we're talking about, but the approach I have below is more efficient.
On my laptop, this whole program runs in about 0.2 seconds. (It uses 10,000 entries and 100 banned IDs.)
var o = {
data: []
};
for (var i = 0; i < 10000; i++) {
o.data.push({
name: i % 2 === 0 ? 'Jake' : 'Bob', // couldn't think of more names :-)
id: ''+i // convert to string
});
}
var banned = {};
for (var i = 0; i < 100; i++) {
banned[''+(i * 3)] = true; // ban 0, 3, 6, 9, 12, ...
}
for (var i = o.data.length - 1; i >= 0; i--) {
if (banned[o.data[i].id]) {
o.data.splice(i, 1);
}
}
console.log(o);
// { data:
// [ { name: 'Bob', id: '1' },
// { name: 'Jake', id: '2' },
// { name: 'Jake', id: '4' },
// { name: 'Bob', id: '5' },
// { name: 'Bob', id: '7' },
// { name: 'Jake', id: '8' },
// { name: 'Jake', id: '10' },
// ...
I am assuming that you have already parsed the JSON data and you have a variable pointing to the array you want to filter. Also, you have an array with the "banned" IDs.
var data = [{
"name": "Jake",
"id": "123"
}, {
"name": "Bob",
"id": "234"
}, {
"name": "Joe",
"id": "345"
}];
var banned = ["123", "345"];
The following function wil probably do the best job that can be done in terms of performance:
// Modifies the data array "in place", removing all elements
// whose IDs are found in the "banned" array
function removeBanned(data, banned) {
// Index the "banned" IDs by writing them as the properties
// of a JS object for really quick read access later on
var bannedObj = {};
banned.forEach(function(b) { bannedObj[b] = true; });
var index = data.length - 1;
while (index >= 0) {
if (bannedObj[data[index].id]) {
data.splice(index, 1);
}
--index;
}
}
This one seems fast enough, but I'd suggest you make a free clean copy instead of modifying the existing array, - it may be faster.
function filterout(o,p,f) {
var i = 0; f = f.join();
while( o[i] ) {
if( f.match( o[i][p] ) ){ o.splice(i,1) }
i++
};
}
var filter = ["123","423"];
var object =
{
"data": [
{
"name": "John",
"id": "723"
},
{
"name": "Jake",
"id": "123"
},
{
"name": "Bob",
"id": "234"
}]
};
filterout( object.data, "id", filter );
console.log(JSON.stringify( object ));