tom lurge
tom lurge

Reputation: 127

Construct path to nested object

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.

Upvotes: 1

Views: 727

Answers (2)

vincent
vincent

Reputation: 2181

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/[email protected]"></script>

Disclaimer: I'm the author of object-scan

Upvotes: 0

Gabriele Petrioli
Gabriele Petrioli

Reputation: 196236

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
                }
            }
        }
    }

Upvotes: 1

Related Questions