DBoi
DBoi

Reputation: 687

Splitting array into groups using typescript/javascript

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

Upvotes: 6

Views: 18400

Answers (5)

canon
canon

Reputation: 41675

What you're really asking for is chunking, as distinct from grouping. Some of the other answers are trying to use Object.groupBy() or Array.prototype.reduce() to process each item and group them, which is inefficient. Chunking, as distinct from grouping, has no regard for item content; so, we don't need to loop over (and process) each individual element of the array.

Option A: Combined Chunking/Translation Method

The fastest option is a simple loop which steps through the array by chunk-size, slicing subsets as we go:

function chunkAndTranslate(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(chunkAndTranslate([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 the reduce() and Object.groupBy() suggestions. This one:

  • Steps by chunk-size rather than 1; so, it performs far fewer iterations.
  • Has no need for repeated, comparative logic or calculations (no division or flooring) for every index. The item content is irrelevant; so, we don't bother processing each individual item.
  • 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 and repeatedly resizing the chunk array.

Option B: Pre-chunk then Reduce

Alternatively, we can separate the chunking and translation steps. We'll create a more reusable Array.prototype.chunk() method, as is common in other languages and libraries*. We'll 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:

if (!Array.prototype.hasOwnProperty("chunk")) {
  Object.defineProperty(Array.prototype, "chunk", {
    enumerable: false,
    value: function(size) {
      // Cache array.length
      const length = this.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++] = this.slice(seekIndex, seekIndex += size);
      }
      // Return our chunks
      return output;
    }
  });
}

console.table(
  // Testing with a simplified demo array
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  // pre-chunk
  .chunk(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>

* You'd normally implement a chunking method like this as a generator, since chunking is generally used to process large sets and, as such, you may not want to materialize an entirely new array containing all chunks. For simplicity, and because the generator incurs additional overhead, we've created a simple output array of chunks. That said, here's a generator implementation if you'd like to take a look. Once Iterator.prototype.reduce() and similar methods get broad support, generators will be even more attractive.

if (!Array.prototype.hasOwnProperty("chunk")) {
  Object.defineProperty(Array.prototype, "chunk", {
    enumerable: false,
    value: function*(size) {
      // Cache array.length
      const length = this.length;
      // Loop variable
      let seekIndex = 0;
      // Loop over chunks
      while (seekIndex < length) {
        // Use slice() to yield a chunk. Note the incrementing/assignment operations.
        yield this.slice(seekIndex, seekIndex += size);
      }
    }
  });
}

console.table(
  // Testing with a simplified demo array
  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  // pre-chunk with our generator
  .chunk(4)
  // Iterator.prototype.reduce() to translate into the desired object
  // check browser support for this function
  .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>

Here's a jsbench, using a chunk size of 25 and an input length of 99, comparing options A and B along with solutions provided by other answers. The accepted reduce answer actually performs the worst. There are clear performance penalties for processing individual elements when the grouping has no regard for item content:

Solution Ops/s (higher is better) Relative to Fastest
Option A: while + slice 1,300,000 fastest
Option B: chunk → reduce 1,100,000 21% slower
Object.groupBy 71,000 95% slower
reduce 57,000 96% slower

Feel free to fork the test to try different input data or chunk sizes.

Upvotes: 9

BobtheMagicMoose
BobtheMagicMoose

Reputation: 2429

There's an experimental implementation for Object.groupBy that's supported in pre-release versions of most browsers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy

Hopefully this will be the go-to way to solve this in the future.

Example usage (untested and shouldn't work unless using a prelease version...):

  const products = [
       {
         'id': 1,
         'name': 'test1'
       },
       {
         'id': 2,
         'name': 'test2'
       },
       {
         'id': 3,
         'name': 'test3'
       },
       {
         'id': 4,
         'name': 'test4'
       },
       {
         'id': 5,
         'name': 'test5'
       },
    ]
    
const columnsPerRow = 3;    

const table = Object.groupBy(products,(p,index)=>{
  return `row${Math.floor(index/columnsPerRow)}`
  })
  
console.log(table)

Upvotes: 0

Emanuel Lindstr&#246;m
Emanuel Lindstr&#246;m

Reputation: 2029

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

Upvotes: -1

UtkarshPramodGupta
UtkarshPramodGupta

Reputation: 8152

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)

Upvotes: 2

Dane Stevens
Dane Stevens

Reputation: 535

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

Upvotes: -2

Related Questions