Convert recursive function to while loop - javascript

I have a potential stack overflow issue with a recursive function. Usually I can solve this with a while loop and a condition, but I cannot figure out the condition to based this while loop on.
Here is the current recursive function which counts the number of handlers in an object of unknown # of nested objects.
countHandlers(obj){
let count = 0;
for(let k in obj){
if(k === "_handlers"){
count += obj[k].length;
}
else if(typeof obj[k] === 'object') {
count += this.countHandlers(obj[k])
}
}
return count;
}
Can this be converted to a non-recursive function?

The way I usually get around recursive functions is to use a stack or a queue to maintain the data that needs to be processed.
Stacks are easier in JavaScript, so we'll go with that. :)
function countHandlers(obj) {
let stack = [];
stack.push(obj);
let count = 0;
while (stack.length > 0) {
let currentObj = stack.pop();
for (let k in currentObj) {
if (k === "_handlers") {
count += currentObj[k].length;
}
else if (typeof currentObj[k] === 'object') {
stack.push(currentObj[k]);
}
}
}
return count;
}

Problem arises in such recursive function when you have circular reference. You have to keep track of what objects you already parsed.
Let's say we have this object :
var test = {
_handlers: {
length: 1
},
child1: {
member1: {
_handlers: [7, 9, 12],
child: {
morehandlers: {
_handlers: {
length: 7
}
},
_handlers: [1]
}
},
member2: {
_handlers: {
length: 1
}
}
},
child2: {
value: 2
},
child3: {
last: {
_handlers: {
length: 7
}
}
}
}
Total handlers count should be 20.
And then we add a circular reference :
test.child1.member3 = test;
Here is how I would handle it without thinking of performances :
let parsedHandlers = null;
let handlersCountLaunched = false;
function countHandlers(obj) { // Cannot be async
let countObj = obj;
let count = 0;
for (let i = 0; i < parsedHandlers.length; i++) {
if (countObj === parsedHandlers[i]) {
countObj = null;
break;
}
}
if (countObj !== null) {
parsedHandlers.push(countObj);
for (let k in obj) {
if (k === "_handlers") {
count += obj[k].length;
} else if (typeof obj[k] === 'object') {
count += this.countHandlers(obj[k]);
}
}
}
return count;
}
function getHandlersCount(mainObj) {
if (!handlersCountLaunched) {
parsedHandlers = [];
handlersCountLaunched = true;
let count = countHandlers(mainObj);
handlersCountLaunched = false;
parsedHandlers = null;
return count;
} else {
console.error('TimingError : getHandlersCount() has been called and has not yet finished counting');
return -1;
}
}
console.log(getHandlersCount(test));
In javascript, unless you have setup a mapping logic, you can't retrive the parent object of a member. With a circular reference in the object, you will probably end up with the total amount of handlers in the object tree, unless you select a branch with no circular reference.

Related

adding two objects without overriding keys within the objects Javascript

i have two objects, a master and a temp
the master looks like
{
"gnome":{
"child":{
name:"child",
race:"gnome"
},
"youngling":{
name:"youngling",
race:"gnome"
}
},
"human":{...},
...
}
and the temp looks like
{
"gnome":{
"man":{
name:"man",
race:"gnome"
}
}
what i am trying to do is have the temp be added to the master like
{
"gnome":{
"child":{...},
"youngling":{...},
"man":{...}
},
"human":{...},
...
}
what i currently have
let obj = {}
function generateJson() {
let race = getinput("race").value
let name = getinput("name").value
let temp = {}
temp[`${race}`] = {}
temp[`${race}`][`${name}`] = {
name: name,
race: race
}
obj = Object.assign(obj, temp)
}
all it does is empties and override the first duplicate key with the temp value
a.e. {gnome:{man:{..}}}
earlier this question was closed because it should have been awnsered with How can I merge properties of two JavaScript objects dynamically?
which sadly it didn't, all of the solutions override objects within objects, i want to add to similar keys
Based on your example, this should work
<script>
function merge_without_override(master, temp) {
for(var key in temp) {
if( !temp.hasOwnProperty(key) )
continue;
if(master[key] !== undefined) // key already exists in master.
continue;
master[key] = Object.assign(temp[key]); // key doesnt exist, assign it.
}
}
var master = {
"gnome":{
"child":{
name:"child",
race:"gnome"
},
"youngling":{
name:"youngling",
race:"gnome"
}
},
"human":{}
};
var temp = {
"gnome":{
"man":{
name:"man",
race:"gnome"
}
}
};
console.log("master before merge");
console.log(master);
merge_without_override(master["gnome"], temp["gnome"]);
console.log("master after merge");
console.log(master);
</script>
Output (in jsfiddle):
{
gnome: {
child: { ... },
man: { ... },
youngling: { ... }
},
human: { ... }
}
JsFiddle: https://jsfiddle.net/h10fpcx2/
Chris Ferdinandi wrote a helper for this here;
I've added an updated version below, but wanted to add a fix for my biggest frustration with Object.assign.
It mutates the source material. To fix this, you can add an empty object as the first argument of the deepAssign function, or use function copyDeepAssign below.
// mutates the source material
function deepAssign(...args) {
// Make sure there are objects to merge
const len = args.length;
if (len < 1) return;
const main = args[0];
if (len < 2) return main
// Merge all objects into first
let i = 0,
curr;
while (i < len) {
curr = args[i];
for (var key in curr) {
// If it's an object, recursively merge
// Otherwise, push to key
if (Object.prototype.toString.call(curr[key]) === '[object Object]') {
main[key] = deepAssign(main[key] || {}, curr[key]);
} else {
main[key] = curr[key];
}
}
i++;
}
return main;
}
// Doesn't mutate the source material
function copyDeepAssign(...args) {
const base = {};
return deepAssign(base, ...args);
}

how to convert 2D nested array into 2D single array in javascript

I need to convert a nested array into 2D in javascript, somewhat similar to the question answered for python at link
How to convert 2d nested array into 2d array single?
For example, the array
[[[[[[[[
[16,12],[16,13],[16,14]]
],
[[[[[[
[46,42],[46,43]
]]]]],[
[62,58],[62,59],[62,60]
]]]]]],
[103,102]],[[118,114],[118,115],[118,116]]
]
needs to be converted to
[[16,12],[16,13],[16,14],[46,42],[46,43],[62,58],[62,59],[62,60],[103,102],[118,114],[118,115],[118,116]]
Please help, thanks in advance
This is what I tried, finally works after many trials :
function removeNestArray2D(object) {
var result = [];
if (Array.isArray(object)) { // check if object is valid array
for(var i=0; i<object.length; i++) {
if(!Array.isArray(object[i])) { // check is each of array element is a valid array
return object;
}
else {
var tmp = removeNestArray2D(object[i]);
if(tmp.length == 1) {
result = tmp[0];
}
else if (tmp.length == 2 && Number.isInteger(tmp[0]) && Number.isInteger(tmp[1])) {
result.push(tmp);
}
else {
for (var j=0; j<tmp.length; j++) {
result.push(tmp[j]);
}
}
}
}
}
return result;
}
Recursive approach will help here. Check each array item if there are size 2 and both are number values then push to result array otherwise continue iteration recursively.
const arr = [[[[[[[[
[16,12],[16,13],[16,14]]
],
[[[[[[
[46,42],[46,43]
]]]]],[
[62,58],[62,59],[62,60]
]]]]]],
[103,102]],[[118,114],[118,115],[118,116]]
];
const get2dArray = arr => {
const res = [];
const pushRecursive = arr => {
if (arr.length == 2 && arr.every(x => Number.isInteger(x))) {
res.push(arr);
} else {
arr.forEach(pushRecursive);
}
};
pushRecursive(arr);
return res;
};
console.log(get2dArray(arr));
function removeNestArray2D(object) {
var result = [];
if (Array.isArray(object)) { // check if object is valid array
for(var i=0; i<object.length; i++) {
if(!Array.isArray(object[i])) { // check is each of array element is a valid array
return object;
}
else {
var tmp = removeNestArray2D(object[i]);
if(tmp.length == 1) {
result = tmp[0];
}
else if (tmp.length == 2 && Number.isInteger(tmp[0]) && Number.isInteger(tmp[1])) {
result.push(tmp);
}
else {
for (var j=0; j<tmp.length; j++) {
result.push(tmp[j]);
}
}
}
}
}
return result;
}

is the value at this key an array?

I've written the following function to return the average of all the numbers of an array at a given key in an object:
var obj = {
key: [1, 2, 3]
};
function getAverageOfElementsAtProperty(obj, key) {
if (!(key in obj)) {
return 0;
} else if (obj[key].length === 0) {
return 0;
} else {
var total = 0;
for (i = 0; i < obj[key].length; i++) {
total += obj[key][i];
}
return total/(obj[key].length);
}
}
console.log(getAverageOfElementsAtProperty(obj, "key"));
Everything is fine, except I need to add one more else if statement before the else statement that checks if the value at that key is an array. If it isn't, it should return 0. I have looked up ways to do this but none of them have worked.
With instanceof:
var obj = {
k1: [1, 2, 3],
k2: null,
k3: "string",
k4: 5
};
function getAverageOfElementsAtProperty(obj, key) {
if (!(obj[key] instanceof Array) || obj[key].length == 0) {
return 0;
} else {
var total = 0;
for (i = 0; i < obj[key].length; i++) {
total += obj[key][i];
}
return total / (obj[key].length);
}
}
console.log(getAverageOfElementsAtProperty(obj, "k1"))
console.log(getAverageOfElementsAtProperty(obj, "k2"))
console.log(getAverageOfElementsAtProperty(obj, "k3"))
console.log(getAverageOfElementsAtProperty(obj, "k4"))
console.log(getAverageOfElementsAtProperty(obj, "k5"))
To check if a particular property is an array or not, use 'instanceof' operator.
In your case, add the below condition.
else if(! (obj.key instanceof Array)) {
return 0
}
When you say that you've looked up ways but they haven't worked, what have you tried and what problems did you find?
There is the Array.isArray function, which will return a boolean.
Array.isArray([]); // true
You can use Array.isArray(obj[key])
var obj = {
key: [1, 2, 3]
};
function getAverageOfElementsAtProperty(obj, key) {
if(!(key in obj)){
return 0;
}
if(!Array.isArray(obj[key])){
return 0;
}
if(obj[key].length === 0){
return 0;
}
return obj[key].reduce(function(op1, op2){
return op1 + op2;
}, 0)/obj[key].length
}

counting duplicate arrays within an array in javascript

I have an array of arrays as follows:
[[3, 4], [1, 2], [3, 4]]
I wish to create a new array of arrays that has no duplicates, and has a count of the number of occurrences of each element in the first array:
[[3,4,2], [1,2,1]]
here is what I have so far:
var alreadyAdded = 0;
dataset.forEach(function(data) {
From = data[0];
To = data[1];
index = 0;
newDataSet.forEach(function(newdata) {
newFrom = newData[0];
newTo = newData[1];
// check if the point we are looking for is already added to the new array
if ((From == newFrom) && (To == newTo)) {
// if it is, increment the count for that pair
var count = newData[2];
var newCount = count + 1;
newDataSet[index] = [newFrom, newTo, newCount];
test = "reached here";
alreadyAdded = 1;
}
index++;
});
// the pair was not already added to the new dataset, add it
if (alreadyAdded == 0) {
newDataSet.push([From, To, 1]);
}
// reset alreadyAdded variable
alreadyAdded = 0;
});
I am very new to Javascript, can someone help explain to me what I'm doing wrong? I'm sure there is a more concise way of doing this, however I wasn't able to find an example in javascript that dealt with duplicate array of arrays.
Depending on how large the dataset is that you're iterating over I'd be cautious of looping over it so many times. You can avoid having to do that by creating an 'index' for each element in the original dataset and then using it to reference the elements in your grouping. This is the approach that I took when I solved the problem. You can see it here on jsfiddle. I used Array.prototype.reduce to create an object literal which contained the grouping of elements from the original dataset. Then I iterated over it's keys to create the final grouping.
var dataSet = [[3,4], [1,2], [3,4]],
grouping = [],
counts,
keys,
current;
counts = dataSet.reduce(function(acc, elem) {
var key = elem[0] + ':' + elem[1];
if (!acc.hasOwnProperty(key)) {
acc[key] = {elem: elem, count: 0}
}
acc[key].count += 1;
return acc;
}, {});
keys = Object.keys(counts);
for (var i = 0, l = keys.length; i < l; i++) {
current = counts[keys[i]];
current.elem.push(current.count);
grouping.push(current.elem);
}
console.log(grouping);
Assuming order of sub array items matters, assuming that your sub arrays could be of variable length and could contain items other than numbers, here is a fairly generic way to approach the problem. Requires ECMA5 compatibility as it stands, but would not be hard to make it work on ECMA3.
Javascript
// Create shortcuts for prototype methods
var toClass = Object.prototype.toString.call.bind(Object.prototype.toString),
aSlice = Array.prototype.slice.call.bind(Array.prototype.slice);
// A generic deepEqual defined by commonjs
// http://wiki.commonjs.org/wiki/Unit_Testing/1.0
function deepEqual(a, b) {
if (a === b) {
return true;
}
if (toClass(a) === '[object Date]' && toClass(b) === '[object Date]') {
return a.getTime() === b.getTime();
}
if (toClass(a) === '[object RegExp]' && toClass(b) === '[object RegExp]') {
return a.toString() === b.toString();
}
if (a && typeof a !== 'object' && b && typeof b !== 'object') {
return a == b;
}
if (a.prototype !== b.prototype) {
return false;
}
if (toClass(a) === '[object Arguments]') {
if (toClass(b) !== '[object Arguments]') {
return false;
}
return deepEqual(aSlice(a), aSlice(b));
}
var ka,
kb,
length,
index,
it;
try {
ka = Object.keys(a);
kb = Object.keys(b);
} catch (eDE) {
return false;
}
length = ka.length;
if (length !== kb.length) {
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) {
return false;
}
} else {
return false;
}
} else {
ka.sort();
kb.sort();
for (index = 0; index < length; index += 1) {
if (ka[index] !== kb[index]) {
return false;
}
}
}
for (index = 0; index < length; index += 1) {
it = ka[index];
if (!deepEqual(a[it], b[it])) {
return false;
}
}
return true;
};
// Recursive function for counting arrays as specified
// a must be an array of arrays
// dupsArray is used to keep count when recursing
function countDups(a, dupsArray) {
dupsArray = Array.isArray(dupsArray) ? dupsArray : [];
var copy,
current,
count;
if (a.length) {
copy = a.slice();
current = copy.pop();
count = 1;
copy = copy.filter(function (item) {
var isEqual = deepEqual(current, item);
if (isEqual) {
count += 1;
}
return !isEqual;
});
current.push(count);
dupsArray.push(current);
if (copy.length) {
countDups(copy, dupsArray);
}
}
return dupsArray;
}
var x = [
[3, 4],
[1, 2],
[3, 4]
];
console.log(JSON.stringify(countDups(x)));
Output
[[3,4,2],[1,2,1]]
on jsFiddle
After fixing a typo I tried your solution in the debugger; it works!
Fixed the inner forEach-loop variable name to match case. Also some var-keywords added.
var alreadyAdded = 0;
dataset.forEach(function (data) {
var From = data[0];
var To = data[1];
var index = 0;
newDataSet.forEach(function (newData) {
var newFrom = newData[0];
var newTo = newData[1];
// check if the point we are looking for is already added to the new array
if ((From == newFrom) && (To == newTo)) {
// if it is, increment the count for that pair
var count = newData[2];
var newCount = count + 1;
newDataSet[index] = [newFrom, newTo, newCount];
test = "reached here";
alreadyAdded = 1;
}
index++;
});
// the pair was not already added to the new dataset, add it
if (alreadyAdded == 0) {
newDataSet.push([From, To, 1]);
}
// reset alreadyAdded variable
alreadyAdded = 0;
});
const x = [[3, 4], [1, 2], [3, 4]];
const with_duplicate_count = [
...x
.map(JSON.stringify)
.reduce( (acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map() )
.entries()
].map(([k, v]) => JSON.parse(k).concat(v));
console.log(with_duplicate_count);

Removing circular objects

I have a function which I call to cut of anything beyond a point, but is there any other way to find a reference to itself and remove that reference somehow smartly?
Here is what I am using now:
//Changing the max level will speed things up, but it will mean that some things might not be persisted
function cleanse(Obj, level) {
var r, i, prims = ["string", "number", "boolean"], maxLevel = 8;
level = level || 0;
if (prims.indexOf(typeof Obj) !== -1) {
r = Obj;
} else if (Obj instanceof Function) {
console.log("Please dont reference objects");
return undefined;
} else {
if (Obj instanceof Array) {
r = [];
} else {
r = {};
}
for (i in Obj) {
if (Obj.hasOwnProperty(i)) {
if (level <= maxLevel) {
r[i] = BackStack.cleanse(Obj[i], level + 1);
}
}
}
}
return r;
};

Categories