2

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.

Chaitra D
  • 177
  • 3
  • 14
  • Does this answer your question? [Group array items using object](https://stackoverflow.com/questions/31688459/group-array-items-using-object) most of these use `push()` to add to the accumulator, but look at [`concat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat) to fit your case. (it accepts both strings or arrays as parameters) – pilchard Aug 03 '21 at 17:56
  • No, in that answer, they only have a string in color. But in my case, the "options" key can either have a string or array of strings as value, as I have mentioned. – Chaitra D Aug 03 '21 at 17:58
  • Did you read my comment? `concat()` accepts either a string or an array. – pilchard Aug 03 '21 at 18:02
  • I tried that. When it encounters an object with "" as a value for any key, it is failing. – Chaitra D Aug 03 '21 at 18:10

5 Answers5

2

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; }
pilchard
  • 12,414
  • 5
  • 11
  • 23
  • I am getting this error at ?? Module parse failed: Unexpected token File was processed with these loaders: * ./node_modules/@pmmmwh/react-refresh-webpack-plugin/loader/index.js * ./node_modules/babel-loader/lib/index.js You may need an additional loader to handle the result of these loaders. | }) => { | if (name) { > (acc[name] ??= { | [name]: [] | })[name].push(...[].concat(options)); – Chaitra D Aug 03 '21 at 18:18
  • Edited to provide `||` short circuit instead of logical nullish assignment. – pilchard Aug 03 '21 at 18:22
  • But the array values will have dulpicate values. For ex, if there is "benz" in 2 objects, then the result will have "benz" twice {"car": ["benz","benz"]. How to get rid of these duplicated itensm – Chaitra D Aug 03 '21 at 18:25
  • Edited to avoid duplicates using Set – pilchard Aug 03 '21 at 18:32
  • Type 'Set' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators. TS2569 56 | if (name && options) { 57 | const category = (acc[name] || (acc[name] = { [name]: [] })); category[name] = [...new Set(category[name].concat(options))]; | ^ – Chaitra D Aug 03 '21 at 18:35
  • As it notes in the error, use `--downlevelIteration` to allow ES6 iterators in compilation – pilchard Aug 03 '21 at 18:42
1

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)
Rubén Vega
  • 722
  • 6
  • 11
  • Close, OP is looking for an array of objects. – pilchard Aug 03 '21 at 18:22
  • But the array values will have dulpicate values. For ex, if there is "benz" in 2 objects, then the result will have "benz" twice {"car": ["benz","benz"]. How to get rid of these duplicated items – Chaitra D Aug 03 '21 at 18:31
  • 1
    @ChaitraD edited with Set() to avoid duplicates. – Rubén Vega Aug 03 '21 at 18:41
  • @pilchard you totally right. How can I made an array of object rewritting the reduce function without using another map? – Rubén Vega Aug 03 '21 at 18:42
  • @ChaitraD I edited the answer again to add the map function, so theoutput is the same that you posted. – Rubén Vega Aug 03 '21 at 18:50
  • what is r and a in reduce? @RubénVega – Chaitra D Aug 05 '21 at 06:45
  • I use r and a as "reducer" and "actual". The reduce funtion iterates, and for each iteration the current value will be the "actual", and the "reducer" is the acumulator. You could name those values as you want. More info about reduce [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce). – Rubén Vega Aug 05 '21 at 15:16
1

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)
})
1

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}
Sajeeb Ahamed
  • 6,070
  • 2
  • 21
  • 30
  • How to handle duplicate values in output ? For ex, if there is "benz" in 2 objects, then the result will have "benz" twice {"car": ["benz","benz"]. How to get rid of these duplicated items – Chaitra D Aug 03 '21 at 18:40
  • Good point, the OP had changed his question and didn't mention the duplicate entries. BTW, I've updated my answer. It's `Set` for our rescue. – Sajeeb Ahamed Aug 03 '21 at 18:46
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; }
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40