2
\$\begingroup\$

I have a JSON data which can be an object/array of recursively nested object or array. The value of an array can not be null, but an value in a object can be null. I would like to return all combination of these keys and values through depth first search.

For example:

var data = {
  "title": {
     "original": "Hitchhiker",
     "more": ["HGTTG"],
     "link": null
  },
  "date": ["20150101", "20160101"]
}

The data's length and depth is arbitrary, and the combination result I want is something like this:

["title-original-Hitchhiker", "title-more-HTTG", "title-link", "date-20150101", "date-20160101"]

What I came up with is recursive:

function nestedObjectToArray(obj) {
  if (typeof(obj) != "object"){
    return [obj];
  }
  var result = [];
  if (obj.constructor == Array){
    for (var i = 0; i <obj.length; i++){
      if (obj[i]){
        var temp = nestedObjectToArray(obj[i]);
        for (var j = 0; j < temp.length; j++){
          result.push(temp[j]);
        }
      }
    }
  } else {
    for (var i in obj){
      if (obj.hasOwnProperty(i)) {
        if (obj[i] == null){
          result.push(i);
        } else {
          var temp = nestedObjectToArray(obj[i]);
          for (var j = 0; j < temp.length; j++){
            result.push(i+"-"+temp[j]);
          }
        }
      }
    }
  }
  return result;
}

Would you have a better/more elegant solution then this chunk of conditions, loops and recursion? I can use any lib if it's more convenient.

\$\endgroup\$
1
\$\begingroup\$

Your code is clean and works fine.
The only little point I've noticed is that you didn't follow this rule:

  • instead of (for instance) for (var i = 0; i <obj.length; i++)
  • you should write for (var i = 0, n = obj.length; i < n; i++), so the obj.length is evaluated only once rather than for each iteration

That's said, it may be possible that it doesn't matter any longer in modern browsers, due to optimizers.

Beyond that, I was interested by your challenge to find "a better/more elegant solution".

Then I found one that uses reduced code, and probably works faster:

var data = {
  "title": {
     "original": "Hitchhiker",
     "more": ["HGTTG"],
     "link": null
  },
  "date": ["20150101", "20160101"]
}

function nestedObjectToArray(obj, prefix) {
  prefix = prefix ? prefix : '';
  var result = [], isArray = Array.isArray(obj), value, nextPrefix;
  obj = typeof obj == 'object' ? obj : JSON.parse('{"' + obj + '": null}');
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      value = obj[key];
      nextPrefix = prefix + (isArray ? '' : (key + '-'));
      result = result.concat(
        value === null ? (prefix + key) : nestedObjectToArray(value, nextPrefix)
      );
    }
  }
  return result;
}

console.log(nestedObjectToArray(data));

The main points of interest are:

  • The function gets a second argument prefix which is recursively transmitted, growing as depth increases: this allows to always add the complete prefix at each level, instead of having to re-process the complete result returned to prepend each item with the key of the ancestor level.
  • There is a unique loop to process arrays and objects: since the only difference is the absence/presence of a key, the test (is array ?) is made at this precise point, inside the generation for the current item:
    nextPrefix = prefix + (isArray ? '' : (key + '-'));.
  • More over, also a final "leaf" is not processed separately, like:
    if (typeof(obj) != "object") {return [obj];} in your version;
    instead, the leaf is transformed into an object:
    obj = typeof obj == 'object' ? obj : JSON.parse('{"' + obj + '": null}');
    so it can be processed in the loop, where it falls into the (already planned by design, following your initial needs) case of null value: the trick is that the value became a key.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.