5

I have this:

    products = [
       {
         'id': 1
         'name: 'test'
       },
       {
         'id': 2
         'name: 'test'
       },
       {
         'id': 3
         'name: 'test'
       }
       ... etc, etc
    ]

I need to restructure it to this:

    products = [
       row1: [
        {
         'id': 1
         'name: 'test'
        },
        {
         'id': 2
         'name: 'test'
        },
        {
         'id': 3
         'name: 'test'
        },
        {
         'id': 4
         'name: 'test'
        }
       ]
    row2: [
       {
         'id': 5
         'name: 'test'
        },
        {
         'id': 6
         'name: 'test'
        },
        {
         'id': 7
         'name: 'test'
        },
        {
         'id': 8
         'name: 'test'
        }
       ]
       row3: [.. etc, etc
    ]

the hard part is that the number of objects in each group is set using a variable (in this example the variable would be 4).

How can I achieve this using Typescript/Javascript? Its driving me mad!

Thanks

DBoi
  • 627
  • 2
  • 17
  • 35

4 Answers4

8

Option A: Custom Chunking/Translation Method

Given that you'd like to break the array into chunks without regard for item content, I'd suggest a simple loop to step through the array by chunk-size, slicing subsets as we go:

function chunk(array, chunkSize) { 
  // Create a plain object for housing our named properties: row1, row2, ...rowN
  const output = {}, 
  // Cache array.length
  arrayLength = array.length;
  // Loop variables
  let arrayIndex = 0, chunkOrdinal = 1;
  // Loop over chunks
  while (arrayIndex < arrayLength) {
    // Use slice() to get a chunk. Note the incrementing/assignment operations.
    output[`row${chunkOrdinal++}`] = array.slice(arrayIndex, arrayIndex += chunkSize);
  }
  return output;
}
// Testing with a simplified demo array
console.table(chunk([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 4));
<script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script><style>.as-console-wrapper{display:block}</style><script>console.config({timeStamps:false,maximize:true})</script>

This has a few advantages over some of the reduce() suggestions. This one:

  • Steps by chunk-size rather than 1; so, it performs fewer iterations.
  • Has no need for repeated, comparative logic or calculations for every index.
  • Defines each rowN property once and never has to check whether it already exists.
  • Uses the native Array.prototype.slice() method to select our subsets rather than pushing individual items one at a time.

Option B: Pre-chunk and Reduce

Alternatively, you could pre-chunk the array using a more generic (reusable?) method and then use Array.prototype.reduce() on the resultant, shorter, two-dimensional array. This mitigates many of the weaknesses of using reduce on its own and actually becomes faster than Option A at certain thresholds of input array length and chunk size:

function generateChunks(array, size) {
  // Cache array.length
  const length = array.length;
  // Pre-size output array so we don't have to push/resize
  const output = new Array(Math.ceil(length / size));
  // Loop variables
  let seekIndex = 0, outputIndex = 0;
  // Loop over chunks
  while (seekIndex < length) {
    // Use slice() to get a chunk. Note the incrementing/assignment operations.
    output[outputIndex++] = array.slice(seekIndex, seekIndex += size);
  }
  // Return our chunks
  return output;
}

console.table(
  // Pre-chunk
  generateChunks([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 4)
    // Reduce to translate into the desired object
    .reduce((output, chunk, index) => {
      output[`row${index + 1}`] = chunk;
      return output;
    },{})
);
<script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script><style>.as-console-wrapper{display:block}</style><script>console.config({timeStamps:false,maximize:true})</script>

Either of these methods, A or B, should be considerably faster (~10x) than the accepted reduce answer, particularly as the input array length and/or chunk size increases. Here's a jsbench comparing these options.

JavaScript engines are getting smarter all the time and it's likely the developers will continue to find new ways to optimize your loops, regardless of your preferred construct. So, your mileage may vary. Feel free to fork the test to try different input data or chunk sizes.

canon
  • 40,609
  • 10
  • 73
  • 97
2

Use Array.reduce()

You can run a .reduce() method on your products array, like so:

var products = [
   { 'id': 1, name: 'test' },
   { 'id': 2, name: 'test' },
   { 'id': 3, name: 'test' },
   { 'id': 4, name: 'test' },
   { 'id': 5, name: 'test'  },
   { 'id': 6, name: 'test' }
]

var numberOfObjects = 4 // <-- decides number of objects in each group

var groupedProducts = products.reduce((acc, elem, index) => {
  var rowNum = Math.floor(index/numberOfObjects) + 1
  acc[`row${rowNum}`] = acc[`row${rowNum}`] || []
  acc[`row${rowNum}`].push(elem)
  return acc
}, {})

console.log(groupedProducts)
UtkarshPramodGupta
  • 7,486
  • 7
  • 30
  • 54
  • `reduce()` would be more useful if the chunking was based on the value of the individual items. Since it's not, there's no need to iterate over each item, repeatedly calculating `rowNum`, testing for the row's existence (or creating it), or pushing individual items one at a time. This solution functions but it suffers from some of the same inefficiencies as the other array-iteration-method solutions. – canon Feb 04 '20 at 16:12
-1

Lodash groupBy is handy. It takes an array, and an iterator function and groups the entries in the array accordingly. The grouping logic can easily be done by counting the index for each iteration, and increment the group count on when the modulo (remainder) operator returns zero.

Using your example:


const { groupBy } = require("lodash");

const products = [
  { id: "1", name: "test" },
  { id: "2", name: "test" },
  { id: "3", name: "test" },
  { id: "4", name: "test" },
  { id: "5", name: "test" },
  { id: "6", name: "test" },
  { id: "7", name: "test" },
  { id: "8", name: "test" },
];
const myGroupingFunction = (val) => {
  ++index;
  const groupLabel = "row" + groupCount;
  if (index % groupSize === 0) {
    ++groupCount;
  }
  return groupLabel;
};

const groupSize = 2;
let groupCount = 1;
let index = 0;
const groupedEntries = groupBy(products, myGroupingFunction);

console.log("GroupedEntries: ", groupedEntries);


// GroupedEntries:  {
//  row1: [ { id: '1', name: 'test' }, { id: '2', name: 'test' } ],
//  row2: [ { id: '3', name: 'test' }, { id: '4', name: 'test' } ],
//  row3: [ { id: '5', name: 'test' }, { id: '6', name: 'test' } ],
//  row4: [ { id: '7', name: 'test' }, { id: '8', name: 'test' } ]
//}

This will iterate through a list, group the entries in equally sized groups according to the groupSize variable, in the order they appear in the list.

If you want, you can also calculate the group number based on object values in the list. I'm incrementing an index instead.

https://lodash.com/docs/4.17.15#groupBy

Emanuel Lindström
  • 1,607
  • 16
  • 25
-2

This should return exactly what you are looking for:

const filteredProducts = {}
products.map((product, i) => {
  const row = `row${Math.floor(i / 4) + 1}`
  if (!filteredProducts[row]) filteredProducts[row] = []
  return filteredProducts[row].push(product)
})
Dane Stevens
  • 465
  • 3
  • 7
  • 3
    Likely because it doesn't come with any explanation, or in-line comments to describe the code. Usually people want to understand how you solved the problem, or the limitations of your solution. Also- you used the `map` method needlessly if you're not going to use the returned value. You might as well just have used `forEach`, or the reduce function described in a different answer. – dsapalo Jan 31 '20 at 18:41