1

I’m working on a mapReduce script for MongoDB but I’m stuck with a rather beginnerish JavaScript problem: I can't construct the path to a nested object. The setting is this: in the reduce step I have a nested object containing all possible properties (and some example values).

var result = {
    computers: {
        total: 12,
        servers: {
            total: 2,
             os: {
                unix: 2,
                windows: 0
            }
        },
        clients: {
            total: 10,
            os: {
                unix: 3,
                windows: 7
            }
        }
    }
}

From the mapping step I get incoming documents like the following:

var incoming = {
    computers {
        total: 1,
        clients: {
            total: 1,
            os: {
                windows: 1
            }
        }
    }
}

The incoming documents are logically subsets of the result document: the ordering of elements and the possible elements are the same, they are just not complete: one may only contain servers data, the other may contain only client data, a third may contain both etc.

I would like to traverse the incoming document and for each property add it’s value to the corresponding property in the result document. Traversing the incoming document recursively is not the problem (I think) but constructing the path is. I came up with the following code:

var traverse = function(knots, path) {
    for (var k in knots) {
        if (knots[k] !== null && typeof(knots[k]) == "object") {
            path = path[knots[k]];
            traverse(knots[k], path);
        }
        else {
            // do something to get rid of the root-level incoming object
            var rest = incoming.computers;
            result[rest][knots[k]] += incoming[rest][knots[k]];
        }
    }
};

traverse(incoming.computers, incoming.computers);

This script isn’t working. I suspect that the ways I try to concatenate the path (line 4) and pass it to the addition operator (line 7) are both broken.
MongoDB responds with “16722 TypeError: Cannot read property '1' of undefined” but I can't make much sense of that.

Edit: Changed the code above: now calling traverse[path] with an object (following Felix' hint). Follow-up problem is that I don't know how to get rid of the 'incoming' root object when constructing the paths in the else clause. var rest = incoming.computers; doesn't seem to do the trick. At least MongoDB is still responding with the same error as above.

tom lurge
  • 105
  • 2
  • 10
  • 1
    What exactly should the value of `path` be in one of the intermediate steps? – Felix Kling Dec 15 '13 at 16:38
  • @FelixKling: you mean in line 4? I'm adding the current knot to 'path' before I recursively call the function again with the modified path. – tom lurge Dec 15 '13 at 16:44
  • 1
    But `path` is a string. What do you think accessing the property `knots[k]` of the string in `path` returns? – Felix Kling Dec 15 '13 at 17:02
  • You're probably right that this is wrong. I passed it in as a string because to pass it as a variable I'd have to pass the full value 'incoming.computers' - but I'd have to get rid of the 'incoming' part later when i want to add the same path to 'result' and 'incoming' in line 7. – tom lurge Dec 15 '13 at 17:31

2 Answers2

1

I believe what you want is

function clone(obj){
    if(obj == null || typeof(obj) != 'object')
        return obj;

    var temp = obj.constructor(); // changed

    for(var key in obj)
        temp[key] = clone(obj[key]);
    return temp;
}

function update( oldData, newData){
    for (property in newData){
        if (oldData[property] !== undefined){ // existing path - needs to be updated
            if (typeof(oldData[property]) === 'number'){ // element is number (total) - just add it
                oldData[property] += newData[property];
            } else { // element is object - drill down
                update( oldData[property], newData[property] );
            }
        } else { // new path - needs to be added
            oldData[property] = clone(newData[property]);
        }
    }
}

It will handle adding new objects as well..

(clone method is copied from Most efficient way to clone an object?)


Assuming the wanted result is

result = {
        computers: {
            total: 13,
            servers: {
                total: 2,
                os: {
                    unix: 2,
                    windows: 0
                }
            },
            clients: {
                total: 11,
                os: {
                    unix: 3,
                    windows: 8
                }
            }
        }
    }
Community
  • 1
  • 1
Gabriele Petrioli
  • 191,379
  • 34
  • 261
  • 317
  • That's exactly the result I want and I love that this solution also relieves me from constructing the initial result object. There's only one caveat: it makes mongod quit unexpectedly :( I get the same 'Illegal instruction: 4' that I always get when I bungle objects that don't exist already. I tried restoring my pre initialized result object and removed the clone command - but to no avail. – tom lurge Dec 15 '13 at 22:33
  • Found the problem: my incoming document also contains one property with a string value. Although I had that case covered in my code already your update() function sees that property and treats it's string value as an object, which is of course wrong. My bad that I didn't spot that earlier. I added another else clause and now everything is just great :) Thanks a lot! – tom lurge Dec 16 '13 at 18:52
0

This is an interesting problem to solve. Here is an interesting solution using object-scan.

Basically the context is used to keep track of the current state in the result object and the scanner itself is used to traverse the incoming object.

// const objectScan = require('object-scan');

const result = { computers: { total: 12, servers: { total: 2, os: { unix: 2, windows: 0 } }, clients: { total: 10, os: { unix: 3, windows: 7 } } } };

const incoming = { computers: { total: 1, clients: { total: 1, os: { windows: 1 } } } };

const update = (target, subset) => objectScan(['**'], {
  rtn: 'count',
  breakFn: ({ property, context }) => {
    if (property !== undefined) {
      context.push(context[context.length - 1][property]);
    }
  },
  filterFn: ({ value, property, context }) => {
    context.pop();
    if (Number.isInteger(value)) {
      context[context.length - 1][property] += value;
      return true;
    }
    return false;
  }
})(subset, [target]);

console.log(update(result, incoming));
// => 3

console.log(result);
// => { computers: { total: 13, servers: { total: 2, os: { unix: 2, windows: 0 } }, clients: { total: 11, os: { unix: 3, windows: 8 } } } }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan@13.8.0"></script>

Disclaimer: I'm the author of object-scan

vincent
  • 1,953
  • 3
  • 18
  • 24