2

I have an array of objects like below,

cars = [
{id: 1, make: 'audi', year: '2010', someProperty: true},
{id: 2, make: 'bmw', year: '2011', someProperty: false},
{id: 3, make: 'bmw', year: '2011', someProperty: true},
{id: 4, make: 'vw', year: '2010', someProperty: true},
{id: 5, make: 'vw', year: '2011', someProperty: true},
{id: 6, make: 'audi', year: '2011', someProperty: true},
{id: 7, make: 'bmw', year: '2010', someProperty: false},
{id: 8, make: 'bmw', year: '2011', someProperty: false},
{id: 9, make: 'bmw', year: '2010', someProperty: true}
]

I want to format it as below,

requiredFormat = [{ 
somePropertyTrue: [
{id: 1, make: 'audi', year: '2010', someProperty: true},
{id: 4, make: 'vw', year: '2010', someProperty: true},
{id: 9, make: 'bmw', year: '2010', someProperty: true} 
],
somePropertyFalse: [
{id: 7, make: 'bmw', year: '2010', someProperty: false}
],
year: '2010'
}, {
somePropertyTrue: [
{id: 3, make: 'bmw', year: '2011', someProperty: true},
{id: 5, make: 'vw', year: '2011', someProperty: true},
{id: 6, make: 'audi', year: '2011', someProperty: true}
],
somePropertyFalse: [
{id: 2, make: 'bmw', year: '2011', someProperty: false},
{id: 8, make: 'bmw', year: '2011', someProperty: false}
],
year: '2011'
}]

Basically it is to group by year and someProperty = true/false, but want it to be formatted this way. I cannot use any plugin to do this, can use ES6.

I tried using reduce by writing a function that groups by a single property, but I am not sure how do we do it for multiple properties and project it this way. Please find my code to group and what I was able to achieve below,

let groupBy = function(arr, key) {
return arr.reduce(function(r, x) {
  (r[x[key]] = r[x[key]] || []).push(x);
  return r;
 }, {});
};

I was able to achieve below,

ableToDo = [{
'2010' : [
{id: 1, make: 'audi', year: '2010', someProperty: true},
{id: 4, make: 'vw', year: '2010', someProperty: true},
{id: 7, make: 'bmw', year: '2010', someProperty: false},
{id: 9, make: 'bmw', year: '2010', someProperty: true}
],
'2011' : [
{id: 2, make: 'bmw', year: '2011', someProperty: false},
{id: 3, make: 'bmw', year: '2011', someProperty: true},
{id: 5, make: 'vw', year: '2011', someProperty: true},
{id: 6, make: 'audi', year: '2011', someProperty: true},
{id: 8, make: 'bmw', year: '2011', someProperty: false}
]}]
Sriram
  • 179
  • 9

6 Answers6

1

You could do this with an ES6 Map that you accumulate with reduce. Here is how that would look:

function transform(cars) {
    return [...cars.reduce ( (acc, car) => {
        const yearGrp = acc.get(car.year) || { 
            somePropertyTrue: [], 
            somePropertyFalse: [],
            year: car.year
        };
        yearGrp['someProperty' + (car.someProperty ? 'True' : 'False')].push(car);
        return acc.set(car.year, yearGrp);
    }, new Map).values()];
}

var cars = [
    {id: 1, make: 'audi', year: '2010', someProperty: true},
    {id: 2, make: 'bmw', year: '2011', someProperty: false},
    {id: 3, make: 'bmw', year: '2011', someProperty: true},
    {id: 4, make: 'vw', year: '2010', someProperty: true},
    {id: 5, make: 'vw', year: '2011', someProperty: true},
    {id: 6, make: 'audi', year: '2011', someProperty: true},
    {id: 7, make: 'bmw', year: '2010', someProperty: false},
    {id: 8, make: 'bmw', year: '2011', someProperty: false},
    {id: 9, make: 'bmw', year: '2010', someProperty: true}
];

console.log(transform(cars));
.as-console-wrapper { max-height: 100% !important; top: 0; }
trincot
  • 317,000
  • 35
  • 244
  • 286
  • This works best for the current situation since this uses ES6 Map and accumulated with reduce. Is it also possible to merge two such outputs together? For example, the same method is invoked in a loop and want to accumulate the results to one merged array. – Sriram Sep 03 '17 at 12:50
  • To merge such outputs, something similar would be possible. Try it, and if you have trouble implementing it, ask a new question about it. – trincot Sep 03 '17 at 19:21
0

You could just simply loop again your result, then group the contents.

var groupBy = function(array, key)
{
  let result = {};

  for (let i = 0; i < array.length; i++)
  {
    if (array[i][key] in result) {
      result[array[i][key]].push(array[i]);
    }
    else
    {
      result[array[i][key]] = [array[i]];
    }
  }

  return result;
};
var byYear = groupBy(cars, "year");
var years  = Object.keys(byYear);

// loop those byYear Items
for (var i = 0; i < years.length; i++)
{
   var year = years[i];
   var yearData = byYear[year];
   // Then group those values in the inside
   byYear[year] = groupBy(yearData, "someProperty");
}

console.log(byYear);

// then byYear will be structured the way you wanted it to be.

hope that helps

masterpreenz
  • 2,280
  • 1
  • 13
  • 21
0

Please use the following code:

var cars = [
{id: 1, make: 'audi', year: '2010', someProperty: true},
{id: 2, make: 'bmw', year: '2011', someProperty: false},
{id: 3, make: 'bmw', year: '2011', someProperty: true},
{id: 4, make: 'vw', year: '2010', someProperty: true},
{id: 5, make: 'vw', year: '2011', someProperty: true},
{id: 6, make: 'audi', year: '2011', someProperty: true},
{id: 7, make: 'bmw', year: '2010', someProperty: false},
{id: 8, make: 'bmw', year: '2011', someProperty: false},
{id: 9, make: 'bmw', year: '2010', someProperty: true}
];
var requiredFormat = [];

function group()
  {
  var spt = [], spf = [], y = "0", nexty;
  temp.sort
    (
    function(x, y)
      {
      return (x.year === y.year ? (x.someProperty > y.someProperty ? -1 : x.someProperty < y.someProperty ? 1 : 0) : x.year < y.year ? -1 : 1);
      }
    ).forEach
      (
      function(e, i, temp)
        {
        nexty = (i < temp.length - 1 ? temp[i+1].year : undefined);
        (e.someProperty === true ? spt : spf).push(e);
        if (e.year !== nexty)
          {
          y = e.year;
          requiredFormat.push({somePropertyTrue: spt, somePropertyFalse: spf, year: y});
          spt = [];
          spf = [];
          y = "0";
          }
        }
      );
  }

var temp = cars.slice(0);
group();
console.log("cars: ", cars);
console.log("temp: ", temp);
console.log("requiredFormat: ", requiredFormat);

You also have the fiddle here, to test it. What this code does is:

  • firstly, create a temporary copy of cars array, with the help of slice() method (maybe you still need the original array?)

  • secondly, it sorts the temp array, first by year, and then by someProperty, giving you the exact order you'd want in the final result

  • finally, it "breaks up" the sorted temp array into the format you require by using 3 variables (spt, spf and y) to store the somePropertyTrue and somePropertyFalse arrays, plus the year, all of which will get "commited" (aka applied to the final result) when the year changes (aka e.year !== nexty, where nexty is the year of the next array element, or undefined if it's the last array element).

You can check each step of the execution by using the console viewer in every browser (you can find it in Developer Tools menu in Chrome, for example). If you want to develop this further, maybe you can use reduce() to shorten the code - me, I didn't yet master the reduce() method enough to use it frequently, so I went for the classical method.

This code often uses the ternary operator to shorten the code (quick explanation here, if you're not familiar with it).

Pang
  • 9,564
  • 146
  • 81
  • 122
Yin Cognyto
  • 986
  • 1
  • 10
  • 22
  • Some suggestions: it would be better if you avoided functions with side-effects. Also `y = "0"` is quite useless. `e.someProperty === true` is overkill when that property is already a boolean. Finally, using sorting instead of using a key map is reducing the time complexity from *O(n)* to *O(nlogn)*. – trincot Sep 02 '17 at 06:46
  • I agree with you. I also like your solution the most, since it works. I know it was pointIess to set y and someProperty the way I did, but I did it for clarity, not for a specific reason (for example, you could have a non-boolean key there, so I let it like that in case its type changes in a different scenario). Regarding the time complexity, I am aware that my solution isn't the most efficient, but as I said, I went for the classical method, at a time when there was no complete answer. Thank you for the suggestions - it's a pity I couldn't comment on your answer, as I need 50 rep to do that.. – Yin Cognyto Sep 02 '17 at 07:25
0

You can do a single pass by making your group criteria a function instead of a single field name. Code for that could be:

function groupBy(x, keyfunc) {
    let res = {};
    x.forEach(obj=>{
        let k = keyfunc(obj);
        (res[k]||(res[k]=[])).push(obj);
    });
    return res;
}

Called with let res = groupBy(input, obj=>obj.year+"/"+obj.sameProperty);. This will however return a single list per group with keys like "2005/true".

To get the structured object you are listing instead you need to do the group operation multiple times and for that a possible solution is recursion:

function groupBy(x, ...keys) {
    if (keys.length === 0) {
        return x;
    } else {
        let res = Object.create(null), key = keys[0];
        x.forEach(obj=>{
            let k = obj[key];
            (res[k]||(res[k]=[])).push(obj);
        });
        let other_keys = keys.slice(1);
        Object.keys(res).forEach(k => {
            res[k] = groupBy(res[k], ...other_keys);
        });
        return res;
    }
}

calling with x.groupBy("year", "sameProperty").

Note also that using an object as a dictionary you're grouping by the conversion to a string of the attribute value; in other words keys values like 2010, "2010" and even [2010] will all end up in the same group.

6502
  • 112,025
  • 15
  • 165
  • 265
0

You can achieve the desired format using reduce and a object literal to accumulate the groups:

const result = cars.reduce((state, car) => {
  const { year, someProperty } = car;
  const group = state[year] || { year, somePropertyTrue: [], somePropertyFalse: [] };
  const property = someProperty ? 'somePropertyTrue' : 'somePropertyFalse';
  group[property] = [...group[property], car];
  return { ...state, [year]: group };
}, {});

Because you are using an object to keep the groups, then the variable result would be an object. If you want an Array instead, you can use Object.values on result object.

pablogq
  • 446
  • 4
  • 8
0

const cars = [
  { id: 1, make: 'audi', year: '2010', someProperty: true },
  { id: 2, make: 'bmw', year: '2011', someProperty: false },
  { id: 3, make: 'bmw', year: '2011', someProperty: true },
  { id: 4, make: 'vw', year: '2010', someProperty: true },
  { id: 5, make: 'vw', year: '2011', someProperty: true },
  { id: 6, make: 'audi', year: '2011', someProperty: true },
  { id: 7, make: 'bmw', year: '2010', someProperty: false },
  { id: 8, make: 'bmw', year: '2011', someProperty: false },
  { id: 9, make: 'bmw', year: '2010', someProperty: true }
]

const groupedCars =
  cars.reduce((memo, car) => {
    memo[car.year] = memo[car.year] || []
    memo[car.year].push(car)
    return memo
  }, [])

  .reduce((memo, cars, i) => {
    memo[i] = {
      somePropertyTrue: cars.filter(car => car.someProperty),
      somePropertyFalse: cars.filter(car => !car.someProperty),
      year: i
    }
    return memo
  }, [])

  .filter(v => v)

document.querySelector('#out').innerHTML = JSON.stringify(groupedCars, null, 4)
<pre id="out" style />
skylize
  • 1,401
  • 1
  • 9
  • 21