5

An API I'm talking to returns it's registry in a very odd nested array structure. I want to convert this monstrosity into an object so my application has easy access to whole objects stored within this output.

The output the API gives me looks like this:

[ 
    [ "settings", "autoLogout", "false" ], 
    [ "settings", "autoLogoutMinutes", "60" ], 
    [ "settings", "presets", "true" ], 
    [ "controller", "rs232", "ip", "192.168.1.11" ], 
    [ "controller", "rs232", "name", "NX-22" ], 
    [ "source", "M23836", "slot1", "ip", "192.168.1.30" ]
]

The last value in each array represents the value of an entry, everything before that last one adds up to the key used to save the value. Because of size limitations I can't just drop big json-encoded objects in there, so thats not a viable workaround.

I've now made a pretty dirty and slow solution involving 2 eval()'s. (I know... that's a no-no so I'm looking for a better solution) I'm guessing this can be done loads faster, but I can't figure out how...

The snippet below uses angular because my application is Angular based, but I'm open to any fast/clean solution. A vanilla js approach or some clever use of something like lodash or underscore would be very welcome.

My dirty and slow solution now

function DemoCtrl($scope){ 
 $scope.data = [ 
        [ "settings", "autoLogout", "false" ], 
        [ "settings", "autoLogoutMinutes", "60" ], 
        [ "settings", "presets", "true" ], 
        [ "controller", "rs232", "ip", "192.168.1.11" ], 
        [ "controller", "rs232", "name", "NX-22" ], 
        [ "source", "M23836", "slot1", "ip", "192.168.1.30" ]
    ]
    
    $scope.init = function(){
        var registry = {};
        
        angular.forEach($scope.data, function(entry){
            var keys = '';
            entry.forEach(function(value, key, entry){
            
                if( key != entry.length - 1 ){
                    //not last of array, so must be a key
                    keys += '[\'' + value + '\']';
                    // check if the object already exists
                    if( !angular.isDefined( eval('registry' + keys) ) ){
                        eval('registry' + keys + ' = {}'); 
                    }
                }else{ 
                 //last one in this entry, must be the value
                   eval('registry' + keys + ' = \'' + value + '\''); 
                }
                
            });        
        });
        
        console.log('registry final');
        console.log(registry);
        $scope.registry = registry;
    }
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app>
  
  <div ng-controller="DemoCtrl" ng-init="init()">
    <pre>{{ registry | json }}</pre>
  </div>
    
</div>
JasperZelf
  • 2,731
  • 1
  • 22
  • 34

7 Answers7

4

Here's a solution that fits your need. Also, please, never use eval. There's always a better way in JavaScript.

You can adapt the code below to your use case.

var data = [ 
    [ "settings", "autoLogout", "false" ], 
    [ "settings", "autoLogoutMinutes", "60" ], 
    [ "settings", "presets", "true" ], 
    [ "controller", "rs232", "ip", "192.168.1.11" ], 
    [ "controller", "rs232", "name", "NX-22" ], 
    [ "source", "M23836", "slot1", "ip", "192.168.1.30" ]
];

var o = {};

data.forEach(function(a) {
  var keys = a.slice(0, a.length-2);
  var cur = o;

  keys.forEach(function(k) {
    if (cur[k] == null) {
      cur[k] = {};
    }
    cur = cur[k];
  });

  cur[a[a.length-2]] = a[a.length-1]
});

output.innerHTML = JSON.stringify(o, null, 2);
<pre id='output'></pre>
Brendan Molloy
  • 1,784
  • 14
  • 22
2

A compact solution which avoids the calculation of the value position in the array.

var array = [
        ["settings", "autoLogout", "false"],
        ["settings", "autoLogoutMinutes", "60"],
        ["settings", "presets", "true"],
        ["controller", "rs232", "ip", "192.168.1.11"],
        ["controller", "rs232", "name", "NX-22"],
        ["source", "M23836", "slot1", "ip", "192.168.1.30"]
    ],
    obj = {};

array.forEach(function (a) {
    var p = obj,
        v = a.pop(),
        k = a.reduce(function (r, b) {
            p[r] = p[r] || {};
            p = p[r];
            return b;
        });
    p[k] = v;
});

document.write('<pre>' + JSON.stringify(obj, 0, 4) + '</pre>');
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • passing as initialValue a[0] instead of undefined, would not avoid the if(r) condition check? – AndreaBogazzi Dec 02 '15 at 09:04
  • @AndreaBogazzi, it would, because of the if statement. `r` is then not undefined and `a[0]` gets processed twice, one as start value for `r` and one as value for `b`. – Nina Scholz Dec 02 '15 at 09:54
  • @AndreaBogazzi, thank you for the question. i changed the callback. there is now no start value, because without the function do not need the comparisson and it is even shorter. it works as well as with one item in the array, because the item is then returned. – Nina Scholz Dec 02 '15 at 10:17
1

Basically you just have loop over them and create nested objects. You don't need to use eval for this. There are a lot of reasons why you shouldn't use it. Performance, security, debuggability (https://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/)

var asObject = {}
//loop over them
data.forEach(function(val) {
    //create the top level object that matches the key if it doesn't exist
   if (!asObject.hasOwnProperty(val[0])) {
    asObject[val[0]] = {};
   }
   //store it 
   var theHolder = asObject[val[0]];
   //loop over all the middle elements creating nested object 
   for (var index = 1; index < val.length - 2; index++) {
       var element = val[index];
       if (!theHolder.hasOwnProperty[element]) {
           theHolder[element] = {};
       } 
       theHolder = theHolder[element]
   }
    //the last one is the value, so just set it
    var lastKey = val[val.length - 2];
    theHolder[lastKey] = val[val.length - 1];
});

console.log(asObject);
mcgraphix
  • 2,723
  • 1
  • 11
  • 15
0
var someObj = $scope.data.reduce(function(accum, array) {
    var value = array.pop(); //pulls last item off of array

    //takes the remaining items and condenses them into 1 string
    var key = array.reduce(function(acc, str) {
        return acc + str;
    }, '');

    accum[key] = value;
    return accum;
}, {}); //the empty object in this line is the seed value

Every sub-array gets the treatment and passed into the empty object seed which is then assigned to someObj.

Jared Smith
  • 19,721
  • 5
  • 45
  • 83
0

function DemoCtrl($scope){ 
 $scope.data = [ 
        [ "settings", "autoLogout", "false" ], 
        [ "settings", "autoLogoutMinutes", "60" ], 
        [ "settings", "presets", "true" ], 
        [ "controller", "rs232", "ip", "192.168.1.11" ], 
        [ "controller", "rs232", "name", "NX-22" ], 
        [ "source", "M23836", "slot1", "ip", "192.168.1.30" ]
    ]
    
    $scope.init = function(){
        var registry = {};
        
        angular.forEach($scope.data, function(entry) {
            var len = entry.length, tmp = registry;
            for (var i = 0; i < len - 1; i++) {
                key = entry[i];
                if (i < len - 2) {
                    if (!tmp[key]) {
                      tmp[key] = { };
                    }
                    tmp = tmp[key];
                } else {
                    tmp[key] = entry[i + 1];
                }
            }
        });
        console.log('registry final');
        $scope.registry = registry;
    }
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app>
  
  <div ng-controller="DemoCtrl" ng-init="init()">
    {{ registry }}
  </div>
    
</div>
AndreaBogazzi
  • 14,323
  • 3
  • 38
  • 63
0

Here it is done using recursion:

$scope.registry = $scope.data.reduce(function register(registry, entry) {
    var key = entry[0];
    if (entry.length === 2) {
        registry[key] = entry[1];
    } else {
        registry[key] = register(registry[key] || {}, entry.slice(1));
    }
    return registry;
}, {});
0

Here is another option based on @Jared Smith's solution above. In his solution the keys were concatenated into string keys in a shallow map. This creates the nested object structure of my other solution.

If you're new to array.reduce(), see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

var someObj = array.reduce(function(previousVal, currentVal) {
    //strip off the value to use at the end
    var value = currentVal.pop();

    //create all the nested objects
    currentVal.reduce(function(acc, str, idx, arr) {

        if (idx !== arr.length - 1 ) {
            if (!acc.hasOwnProperty(str)) {
                acc[str] = {};
            }
            return acc[str];    
        } else {
            //the last one in the array is the key for the value
            acc[str] = value;
            return;
        }

    }, previousVal);
    return previousVal;
}, {}); 

console.log(someObj);
mcgraphix
  • 2,723
  • 1
  • 11
  • 15