How to group array of objects by key when object can have differnt types

Viewed 161

I have an array of objects like this:

const input = [
    {
        "name" : "car",
        "sign" : "+",
        "options" : "benz"
    },
    {
        "name" : "bike",
        "sign" : "+",
        "options" : "pulsar"
    },
    {
        "name" : "bike",
        "sign" : "+",
        "options" : "enfield"
    },
    {
        "name" : "car",
        "sign" : "+",
        "options" : ["toyota","hyundai","benz"]
    },
    {
        "name" : "",
        "sign" : "",
        "options" : "" 
    },
    {
        "name" : "car",
        "sign" : "+",
        "options" : ["audi", "ford"]
    }
]

The input structure is like this: every object has 3 key-value pairs.

  1. All of them can be empty

  2. It can have valid values:

    a. The options can either be a string

    b. Or it can be an array

I dont want the objects with values of "" (empty string) to be added in the output, The output should be like this:

[
    {"car" : [ "benz", "toyota","hyundai", "audi", "ford"]},
    {"bike" : ["pulsar","enfield" ]}
]

The various other answers do not show how to handle when the object value can be either an array or a string, So I'm writing a new question.

I am not sure if I have to use reduce or map. Any help is greatly appreciated, Thanks in advance.

5 Answers

This is a fairly standard 'group by' operation, grouping by one property and accumulating another into an array.

In order to accommodate both strings and arrays you use concat() to add the options value into the accumulator. Concat accepts either individual values or arrays of values and flattens the latter out into the target array. To avoid duplicates you can use a Set.The snippet below only adds elements to the accumulator if both name and options are truthy.

const input = [
  { "name": "", "sign": "+", "options": "benz" }, // blank 'name' property
  { "name": "bike", "sign": "+", "options": "" }, // blank 'options' property
  { "name": "bike", "sign": "+", "options": "enfield" },
  { "name": "car", "sign": "+", "options": ["toyota", "hyundai"] },
  { "name": "car", "sign": "", "options": "ford" },
  { "name": "car", "sign": "+", "options": ["audi", "ford"] }]

const result = Object.values(
  input.reduce((acc, { name, options }) => {
    if (name && options) {
      const category = (acc[name] || (acc[name] = { [name]: [] }));
      category[name] = [...new Set(category[name].concat(options))];
    }
    return acc;
  }, {}));

console.log(result)
.as-console-wrapper { max-height: 100% !important; top: 0; }

Alternatively, to avoid creating a new Set on each iteration, you can accumulate into a Set and then map the result of the reduce() call and convert each set to an array.

const input = [
  { "name": "", "sign": "+", "options": "benz" }, // blank 'name' property
  { "name": "bike", "sign": "+", "options": "" }, // blank 'options' property
  { "name": "bike", "sign": "+", "options": "enfield" },
  { "name": "car", "sign": "+", "options": ["toyota", "hyundai"] },
  { "name": "car", "sign": "", "options": "ford" },
  { "name": "car", "sign": "+", "options": ["audi", "ford"] }]

const result = Object.entries(
  input.reduce((acc, { name, options }) => {
    if (name && options) {
      acc[name] || (acc[name] = new Set);
      [].concat(options).forEach(option => acc[name].add(option));
    }
    return acc;
  }, {}))
  .map(([name, options]) => ({ [name]: [...options] }));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Since it seems you can't use ES6 iterators here is an option using just an Array#includes() check to avoid duplicates.

const input = [
  { "name": "", "sign": "+", "options": "benz" }, // blank 'name' property
  { "name": "bike", "sign": "+", "options": "" }, // blank 'options' property
  { "name": "bike", "sign": "+", "options": "enfield" },
  { "name": "car", "sign": "+", "options": ["toyota", "hyundai"] },
  { "name": "car", "sign": "", "options": "ford" },
  { "name": "car", "sign": "+", "options": ["audi", "ford"] }]

const result = Object.values(
  input.reduce((acc, { name, options }) => {
    if (name && options) {
      const category = (acc[name] || (acc[name] = { [name]: [] }));
      [].concat(options).forEach(option => {
        if (!category[name].includes(option)) category[name].push(option)
      });
    }
    return acc;
  }, {}));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

ES3

var input = [
  { "name": "", "sign": "+", "options": "benz" },
  { "name": "bike", "sign": "+", "options": "" },
  { "name": "bike", "sign": "+", "options": "enfield" },
  { "name": "car", "sign": "+", "options": ["toyota", "hyundai"] },
  { "name": "car", "sign": "", "options": "ford" },
  { "name": "car", "sign": "+", "options": ["audi", "ford"] }
];

var temp = {};

for (var i = 0; i < input.length; i++) {
  var name_ = input[i].name;
  var options_ = input[i].options;

  if (name_ !== undefined && name_ !== "" && options_ !== undefined && options_ !== "") {

    if (!temp.hasOwnProperty(name_)) {
      temp[name_] = {};
      temp[name_][name_] = [];
    }

    var options_ = [].concat(options_);

    for (var j = 0; j < options_.length; j++) {
      if (!temp[name_][name_].includes(options_[j])) {
        temp[name_][name_].push(options_[j]);
      }
    }

  }
}

var result = [];

for (var key_ in temp) {
  if (temp.hasOwnProperty(key_)) {
    result.push(temp[key_]);
  }
}

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

You can use a reduce function adding the conditions tha you want.

const input = [
    {
        "name": "car",
        "sign": "+",
        "options": "benz"
    },
    {
        "name": "bike",
        "sign": "+",
        "options": "pulsar"
    },
    {
        "name": "bike",
        "sign": "+",
        "options": "enfield"
    },
    {
        "name": "bike",
        "sign": "+",
        "options": "enfield"
    },
    {
        "name": "car",
        "sign": "+",
        "options": ["toyota", "hyundai"]
    },
    {
        "name": "",
        "sign": "",
        "options": ""
    },
    {
        "name": "car",
        "sign": "+",
        "options": ["audi", "ford"]
    }
]

let grouped = input.reduce(function (r, a) {
    if (a.name !== "") {
        r[a.name] = r[a.name] || [];
        if (Array.isArray(a.options)) {
            r[a.name] = Array.from( new Set (r[a.name].concat(a.options)))
        } else {
            r[a.name].push(a.options)
            r[a.name] = Array.from( new Set (r[a.name]));
        }

    }
    return r;
}, {});
let mapped = Object.entries(grouped).map(([key, value])=>{return{[key]:value}})

console.log(mapped)

First convert the input to a map so that you can have everything in some key-value pair:

input.forEach((e)=>{
if(!(e["name"] in tempMap)){
    tempMap[e["name"]] = []
}
if(typeof e["options"] == typeof 'test'){
        tempMap[e["name"]].push(e["options"])
}else
    tempMap[e["name"]].push(...e["options"])
})

then convert it to whatever form you like

output = []
Object.keys(tempMap).forEach((e)=>{
    obj = {}
    obj[e] = tempMap[e]
    if(e != '')
        output.push(obj)
})

You can use reduce to achieve your requirements. Check this out-

const input=[{name:"car",sign:"+",options:"benz"},{name:"bike",sign:"+",options:"pulsar"},{name:"bike",sign:"+",options:"enfield"},{name:"car",sign:"+",options:["toyota","hyundai"]},{name:"",sign:"",options:""},{name:"car",sign:"+",options:["audi","ford","benz"]}];

const result = input.reduce((acc, {name, options}) => {
    // Check if the name is not empty
    if (name) {
    
      // Initialize the object key with empty array if not already initialized
      acc[name] ||= [];
      
      // Check if the options is an array then push the array using spread operation
      // Otherwise directly push the string.
      if (Array.isArray(options)) {
        acc[name].push(...options);
      } else {
        acc[name].push(options);
      }
    }
  return acc;
}, {});

// Convert the object into array of objects.
// This is redundant I believe. If you use the `result` object
// that would be more accessible for you.
const output = Object.entries(result).map(([key, value]) => {
  // Use Set to remove duplicate entries from the value array.
  return Object.fromEntries([[key, [...new Set(value)]]]);
});

console.log(output);

// If you don't need the array of object as output then the result object is enough for you.
// You can access the car and bike object easily in this case.
console.log(result);
.as-console-wrapper{min-height: 100%!important; top: 0}

I've done that...

const input = 
  [ { name: 'car',  sign: '+', options: 'benz'                 } 
  , { name: 'bike', sign: '+', options: 'pulsar'               } 
  , { name: 'bike', sign: '+', options: 'enfield'              } 
  , { name: 'bike', sign: '+', options: 'enfield'              } 
  , { name: 'car',  sign: '+', options: [ 'toyota', 'hyundai'] } 
  , { name: '',     sign: '',  options: ''                     } 
  , { name: 'car',  sign: '+', options: [ 'audi', 'ford']      } 
  ] 

const result = Object.entries( input.reduce((a,{name,options})=>
  {
  if (name)
    (a[name] = a[name] ?? []).push(...[options].flat() )
  return a
  }, {})
  ).map(([k,v])=>({[k]:[...new Set(v)]})) // remove duplicates

console.log( result )
.as-console-wrapper { max-height: 100% !important; top: 0 }
.as-console-row::after { display: none !important; }

Related