0

I am trying to figure out the most performant Javascript way to convert an array of objects, into an object with unique keys and an array full of objects as the value.

For Example:

const array = [
  { "name": "greg", "year": "2000" },
  { "name": "john", "year": "2002" },
  { "name": "bob",  "year": "2005" },
  { "name": "ned",  "year": "2000" },
  { "name": "pam",  "year": "2000" },
];

I would like this converted to:

{
  "2000": [ 
    { "name": "greg", "year": "2000" }, 
    { "name": "ned",  "year": "2000" },
    { "name": "pam",  "year": "2000" }
  ],
  "2002": [ { "name": "john", "year": "2002" } ],
  "2005": [ { "name": "bob",  "year": "2005" } ],
}

As of now, this is what I've done so far:

let yearsObj = {};

for (let i=0; i<array.length; i++) {
  if (!yearsObj[array[i].year]) {
    yearsObj[array[i].year] = [];
  }

  yearsObj[array[i].year].push(array[i]);
}

Tom
  • 75
  • 5
  • 3
    The way you are using is best I guess – Maheer Ali May 09 '19 at 23:35
  • 1
    Are you sure that this particular problem is your performance bottleneck? It's `O(N)`, it shouldn't be something to worry about (and like the above comment says, your current code looks like the best it can be, I think). Code readability usually matters more than performance – CertainPerformance May 09 '19 at 23:45
  • Are you looking for micro-performance ? because yours is already linear time. couple of micro optimizations is caching the length. – alejandro May 09 '19 at 23:59
  • 1
    I believe the for loop is slightly faster than other methods, such as .forEach() or .map() – gavgrif May 10 '19 at 00:04
  • 1
    You can use .map function and ES6 arrow syntax to write above code concisely. ```array.map(a => { if (!yearsObj[a.year]) { yearsObj[a.year] = []; } yearsObj[a.year].push(a); })``` – Faizan May 10 '19 at 00:12

3 Answers3

-1

you can use a more elegant way to do it by using array's reduce function

// # impl


const group = key => array =>
  array.reduce(
    (objectsByKeyValue, obj) => ({
      ...objectsByKeyValue,
      [obj[key]]: (objectsByKeyValue[obj[key]] || []).concat(obj)
    }),
    {}
  );

// # usage 

console.log(
  JSON.stringify({
    byYear: group(array),
  }, null, 1)
);

// output

VM278:1 { "carsByBrand": { "2000": [ { "name": "greg", "year": "2000" }, { "name": "ned", "year": "2000" }, { "name": "pam", "year": "2000" } ], "2002": [ { "name": "john", "year": "2002" } ], "2005": [ { "name": "bob", "year": "2005" } ] } }

CodeIsLife
  • 1,205
  • 8
  • 14
-1

It could be as simple as that Object.fromEntries(array.map(obj => [obj.year,obj])) even it is not exactly what you need, but talking about performance it is way slower than all proposed, so i'm giving it as an bad example of showing how the short statement is not always the fastest. Your way seems to be the fastest taking about performance. Run the snippet below to see the actual timing.

// common
let array = [
  { "name": "greg", "year": "2000" },
  { "name": "john", "year": "2002" },
  { "name": "bob", "year": "2005" },
  { "name": "ned", "year": "2000" },
  { "name": "pam", "year": "2000" },
];


// simple as a statement way
console.time();
console.log(Object.fromEntries(array.map(obj => [obj.year,obj])));
console.timeEnd();

// using .reduce way
console.time();
const result = array.reduce((prev, curr) => {
  const { year } = curr;
  if (prev[year]) {
    prev[year].push(curr);
  } else {
    prev[year] = [curr];
  }
  return prev;
}, {});
console.log(result);
console.timeEnd();

// your way
console.time();
let yearsObj = {};
for (let i=0; i<array.length; i++) {
  if (!yearsObj[array[i].year]) {
    yearsObj[array[i].year] = [];
  }

  yearsObj[array[i].year].push(array[i]);
}
console.log(yearsObj);
console.timeEnd();
Reflective
  • 3,854
  • 1
  • 13
  • 25
-2

A for loop (imperative style) like you have is likely to be the fastest in most situations. However, in this case you are not likely to see much of a difference. One thing you could do to improve the code in your example is to get the array length before the for loop and assign it to the variable, so that it's not calculated every iteration of the loop.

const yearsObj = {};
const arrayLength = array.length; // Only calculate array length once

for (let i=0; i<arrayLength; i++) {
  if (!yearsObj[array[i].year]) {
    yearsObj[array[i].year] = [];
  }

  yearsObj[array[i].year].push(array[i]);
}

In this situation, my preference would be to use Array.reduce(). It is more readable and the performance difference will be negligible.

const arr = [
  { name: 'greg', year: '2000' },
  { name: 'john', year: '2002' },
  { name: 'bob', year: '2005' },
  { name: 'ned', year: '2000' },
  { name: 'pam', year: '2000' },
];

const result = arr.reduce((prev, curr) => {
  const { year } = curr;
  if (prev[year]) {
    prev[year].push(curr);
  } else {
    prev[year] = [curr];
  }
  return prev;
}, {});

/* Result:
{ '2000': 
   [ { name: 'greg', year: '2000' },
     { name: 'ned', year: '2000' },
     { name: 'pam', year: '2000' } ],
  '2002': [ { name: 'john', year: '2002' } ],
  '2005': [ { name: 'bob', year: '2005' } ] }
*/
djheru
  • 3,525
  • 2
  • 20
  • 20
  • 1
    He said the most performant way. And `reduce()` will not perform better than simple for loops and also you are using destructuring which makes code slow. – Maheer Ali May 09 '19 at 23:38
  • prev is a terrible variable name for an accumulator, for me it implies it is the previous element in the array rather than the result. – Adrian Brand May 10 '19 at 03:40
  • From MDN Docs: "The accumulator accumulates the callback's return values. It is the accumulated value PREVIOUSLY returned in the last invocation of the callback, or initialValue, if supplied" (emphasis added) Fortunately you are free to use whatever variable names you like in your code – djheru May 10 '19 at 18:21