2

I'm new to Javascript and I have nested objects and arrays that I would like to flatten.

I have...

[{ a: 2, b: [{ c: 3, d: [{e: 4, f: 5}, {e: 5,f: 6}]}, 
             { c: 4, d: [{e: 7, f: 8}]}
            ]
}]

and would like...

[{a:2,c:3,e:4,f:5}, {a:2,c:3,e:5,f:6}, {a:2,c:4,e:7,f:8}]

I've tried to adapt the following function written for an object for my array but i only get the final object within the array [{a:2,c:4,e:7,f:8}] https://stackoverflow.com/a/33158929/14313188. I think my issue is knowing how to iterate through arrays and objects?

original script:

function flatten(obj) {
    var flattenedObj = {};
    Object.keys(obj).forEach(function(key){
        if (typeof obj[key] === 'object') {
            $.extend(flattenedObj, flatten(obj[key]));
        } else {
            flattenedObj[key] = obj[key];
        }
    });
    return flattenedObj;    
}

my scripts (same result for both):

flat_array=[];
function superflat(array){
   for (var i = 0; i < array.length; i++) {
       var obj = array[i]
       var flattenedObj = {};
       Object.keys(obj).forEach(function(key){
          if (typeof obj[key] === 'object') {
             $.extend(flattenedObj, flatten(obj[key]));
          } else {
             flattenedObj[key] = obj[key];
          }
       });
       flat_array.push(flattenedObj);
    }
};
mega_flat_array=[];
function megaflatten(obj) {
     Object.keys(obj).forEach(function(key){
     var flattenedObj = {};
        if (typeof obj[key] === 'object') {
            $.extend(flattenedObj, flatten(obj[key]));
        } else {
            flattenedObj[key] = obj[key];
        }
     mega_flat_array.push(flattenedObj);
     });
}

Thanks for your help

Matt Ellen
  • 11,268
  • 4
  • 68
  • 90

2 Answers2

1

I would suggest starting with simpler data objects to test your function with, then progressively add more complex objects until your function performs as expected. forEach

Here you can see how I started with a simple test1 object, then test2 and so on so that the logic gets broken down into smaller increments.

To remove the duplicates we previously had, I had to throw and catch an error to break out of the recursive forEach loops, which added the unnecessary duplicate "rows" - perhaps it is better to use a normal for loop out of which you can simply break; and then use error handling for real errors.

The basic idea of the recursive function is to check the type of object (either array or object) and then loop through them to add values, but another check is needed on those to see if they are not Arrays and not Objects, if they are, call the function again. When a duplicate key is found i.e { c: 3}, remove the current key and add the new one before continuing the loop.

You can add some more tests if you have some more sample data, but there are better libraries to help you with TDD (test-driven development).



const isArray = (arr) => {
  return Array.isArray(arr);
};

const isObject = (obj) => {
  return typeof obj === "object" && obj !== null;
};

const flatten = (tree, row, result) => {
  try {
    
    if (isArray(tree)) {
      tree.forEach((branch, index) => {
        flatten(branch, row, result);
      });
    } else if (isObject(tree)) {
      Object.keys(tree).forEach((key) => {
        //we don't want to add objects or arrays to the row - 
        if (!isArray(tree[key]) && !isObject(tree[key])) {
          
          if (key in row) {
            
            // new row detected, get existing keys to work with
            let keysArray = Object.keys(row);
            // we are going to loop backwards and delete duplicate keys
            let end = Object.keys(row).length;
            let stopAt = Object.keys(row).indexOf(key);
            //delete object keys from back of object to the newly found one
            for (let z = end; z > stopAt; z--) {
              delete row[keysArray[z - 1]];
            }

            row[key] = tree[key];
          } else {
            row[key] = tree[key];
          }
        } else {
          flatten(tree[key], row, result);
          throw "skip";
        }
      });

      //all other rows in results will be overridden if we don't stringify
      result.push(JSON.stringify(row));
      
    }
  } catch (e) {
    //console.log(e)
  } finally {
    return result.map((row) => JSON.parse(row));
  }
};


///tests
const test1 = [
    {
      a: 2,
      b: 3,
    },
  ];
  const expected1 = [{ a: 2, b: 3 }];
  
  const test2 = [
    {
      a: 2,
      b: [
        {
          c: 3,
        },
      ],
    },
  ];
  const expected2 = [{ a: 2, c: 3 }];
  
  const test3 = [
    {
      a: 2,
      b: [
        {
          c: 3,
        },
        { c: 4 },
        { c: 5 },
      ],
    },
  ];
  const expected3 = [
    { a: 2, c: 3 },
    { a: 2, c: 4 },
    { a: 2, c: 5 },
  ];
  
  let test4 = [
    {
      a: 2,
      b: [
        {
          c: 3,
          d: [
            { e: 4, f: 5 },
            { e: 5, f: 6 },
          ],
        },
        { c: 4, d: [{ e: 7, f: 8 }] },
      ],
    },
  ];
  const expected4 = [
    { a: 2, c: 3, e: 4, f: 5 },
    { a: 2, c: 3, e: 5, f: 6 },
    { a: 2, c: 4, e: 7, f: 8 },
  ];
const test = (name, res, expected) => {
  console.log(
    `${name} passed ${JSON.stringify(res) === JSON.stringify(expected)}`
  );
  //console.log(res, expected);
};

//test("test1", flatten(test1, {}, []), expected1);
//test("test2", flatten(test2, {}, []), expected2);
//test("test3", flatten(test3, {}, []), expected3);
test("test4", flatten(test4, {}, []), expected4);

Herald Smit
  • 2,342
  • 1
  • 22
  • 28
  • 1
    Thanks @Herald Smit!! The output is definitely in the format I wanted - just need to look at the duplicates part more carefully because i was expecting more "rows" than i got when I ran my array. – Rebeca Fiadeiro Sep 21 '20 at 17:02
  • 1
    Hi @RebecaFiadeiro, I've made some changes to get rid of the duplicates, and added tests, so you can easily test some more data samples. Good Luck! – Herald Smit Sep 22 '20 at 15:56
1

It's a bit of a behemoth, and it doesn't preserve the keys' order, but it does work with no duplicates.

It is recursive, so watch out for the call stack.

  • First, loop through the items in the array,
  • If an item is an array, make a recursive call.
    • On returning from that call, if the number of objects returned is more than there currently are in the final result, then update the returned objects with the properties from the objects in the final result, being careful to avoid overwriting pre-existing properties.
    • Otherwise update the final results with the properties in the returned result, again being careful not to overwrite existing properties.
  • If the item is not an array
    • If this is the first item put it into the final result
    • Otherwise add the item's properties to all the items in the final result, without overwriting any.

function makeFlat(arr) //assume you're always passing in an array
{
  let objects = [];
  arr.forEach(item =>
  {
    let currentObject = {};
    const keys = Object.keys(item);
    keys.forEach(key =>
    {
      const obj = item[key];
      if(Array.isArray(obj))
      {
        let parts = makeFlat(obj);
        if(objects.length > 0)
        {          
          if(parts.length > objects.length)
          {
            parts.forEach(part =>
            {
              objects.forEach(ob =>
              {
                Object.keys(ob).forEach(k =>
                {
                  if(Object.keys(part).indexOf(k) == -1)
                  {
                    part[k] = ob[k];
                  }
                });
              });
            });
            objects = parts;
          }
          else
          {
            objects.forEach(ob =>
            {
              parts.forEach(part =>
              {
                Object.keys(part).forEach(k =>
                {
                  if(Object.keys(ob).indexOf(k) == -1)
                  {
                    ob[k] = part[k];
                  }
                });
              });
            });
          }
        }
        else
        {
          objects = parts;
        }
      }
      else
      {
        if(Object.keys(currentObject).length == 0)
        {
          objects.push(currentObject);
        }
        currentObject[key] = item[key];
        
        objects.forEach(ob =>
        {
          if(Object.keys(ob).indexOf(key) == -1)
          {
            ob[key] = currentObject[key]
          }
        });
      }
    });
  });
  return objects;
}

const inp = [{ a: 2, b: [{ c: 3, d: [{e: 4, f: 5}, {e: 5,f: 6}]}, 
                         { c: 4, d: [{e: 7, f: 8}]}
            ], g:9
}];

let flattened = makeFlat(inp);

flattened.forEach(item => console.log(JSON.stringify(item)));
Matt Ellen
  • 11,268
  • 4
  • 68
  • 90