Kieran E
Kieran E

Reputation: 3666

Unflatten JS object and convert arrays

I have a function used to flatten objects like so:

let object = {
  a: 1,
  b: [
   { c: 2 },
   { c: 3 }
  ]
};

flatten(object)
// returns {
  'a': 1,
  'b.0.c': 2,
  'b.1.c': 3
}

I need to unflatten objects, but also revert arrays to how they were. I have the following code:

  unflatten(obj) {
    let final = {};

    for (let prop in obj) {
      this.assign(final, prop.split('.'), obj[prop]);
    }

    return final;
  }

  assign(final, path, value) {
     let lastKeyIndex = path.length-1;
     for (var i = 0; i < lastKeyIndex; ++ i) {
       let key = path[i];
       if (!(key in final)) {
         final[key] = {};
       }
       final = final[key];
     }
     final[path[lastKeyIndex]] = value;
  }

which works for the most part, but it treats arrays like so:

{
  a: 1,
  b: { // Notice how b's value is now an object
   "0": { c: 2 }, // Notice how these now have a key corresponding to their index
   "1": { c: 3 }
  }
}

Whereas I need b to be an array like before:

{
  a: 1,
  b: [
   { c: 2 },
   { c: 3 }
  ]
}

I'm at a loss for where to go from here. It needs to be able to deal with an arbitrary number of arrays like:

'a.b.0.c.0.d',
'a.b.0.c.1.d',
'a.b.1.c.0.d',
'a.b.1.c.1.d',
'a.b.1.c.2.d',
// etc

It needs to be vanilla JS, but es2015 is fine. It it assumed that any key that's a number is actually part of an array.

If anyone has any advice, it's appreciated!

Upvotes: 3

Views: 1690

Answers (2)

gyre
gyre

Reputation: 16777

When you find that key is not in final, you should check to see if the next key in the path is only digits (using a regular expression) and, if so, assign to an array instead of an object:

if (!(key in final)) {
  final[key] = /^\d+$/.test(path[i + 1]) ? [] : {};
}

let object = {
  a: 1,
  b: [{
      c: 2
    },
    {
      c: 3
    }
  ]
};

let flattened = {
  'a': 1,
  'b.0.c': 2,
  'b.1.c': 3
}

function unflatten(obj) {
  let final = {};

  for (let prop in obj) {
    assign(final, prop.split('.'), obj[prop]);
  }

  return final;
}

function assign (final, path, value) {
  let lastKeyIndex = path.length - 1;
  for (var i = 0; i < lastKeyIndex; ++i) {
    let key = path[i];
    if (!(key in final)) {
      final[key] = /^\d+$/.test(path[i + 1]) ? [] : {};
    }
    final = final[key];
  }
  final[path[lastKeyIndex]] = value;
}

console.log(unflatten(flattened))
.as-console-wrapper { min-height: 100vh; }

Upvotes: 1

Nina Scholz
Nina Scholz

Reputation: 386756

You could iterate the keys and then split the string for single properties. For building a new object, you could check for number and take an array for these properties.

function setValue(object, path, value) {
    var way = path.split('.'),
        last = way.pop();

    way.reduce(function (o, k, i, kk) {
        return o[k] = o[k] || (isFinite(i + 1 in kk ? kk[i + 1] : last) ? [] : {});
    }, object)[last] = value;
}

function unFlatten(object) {
    var keys = Object.keys(object),
        result = isFinite(keys[0][0]) ? [] : {};
    keys.forEach(function (k) {
        setValue(result, k, object[k]);
    });
    return result;
}

console.log(unFlatten({
    'a': 1,
    'b.0.c': 2,
    'b.1.c': 3
}));
console.log(unFlatten({
    '0': 1,
    '1.0.c': 2,
    '1.1.c': 3
}));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Upvotes: 0

Related Questions