6

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

4
  • 1
    Does this answer your question? Split array into chunks Commented Jan 31, 2020 at 16:29
  • What have you tried so far? Commented Jan 31, 2020 at 16:30
  • I have tried this but i need the nested arrays to have names (row1, row2, etc). How can I add the array names? Commented Jan 31, 2020 at 16:32
  • You would need an object for that, I added an answer on how you can implement it. Commented Jan 31, 2020 at 16:54

5 Answers 5

9

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.

Sign up to request clarification or add additional context in comments.

Comments

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)

1 Comment

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.
0

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)

7 Comments

Object.groupBy() is a great tool for when you need to group based on item content! Unfortunately, it's still reliant on generating a key per item, which is wasteful given the OP's item-agnostic requirements. It also performs poorly for this case, i.e.: about on par with reduce(): jsbench.me/xvkv1dmqul/1.
It performs fantastically - depending on how you measure performance ;). For me, this approach is the quickest and easiest to interpret as a developer. It's essentially a special syntax for a common use case of reduce. Sure, it's going to be slower in execution - but that's rarely at issue for most use cases. If you really want fast execution - using splice is going to get a 100x speedup over slice in your option A.
Okay, I see what happened. Most of your iterations were processing a 0-length array since you were modifying the shared setup input array. Just slap a console.log(input.length) in there to see. I've modified the bench (jsbench.me/5jlmpciv91/1) to prevent this from happening (and to remove the now-redundant shallow-copy test) and results are now in line with what I'd expect: splice() performs much better than reduce() and Object.groupBy() but worse than slice(), presumably because slice() doesn't modify the input array.
You are correct, thanks for the update! I was thinking splice would just change start/end pointers of the modified array and the returned array would just adopt start/end pointers of the spliced elements. Something tells me there's more overhead than that (creating a new array anyway and moving data around in the old array). This would make slice more efficient than splice as you have pointed out.
Based on the initial performance claim, I suspected something similar; but the spec remaps individual indices. Additionally, given that splice() can remove/inject items anywhere in the array, an optimization for lead segment removal seems unlikely -particularly since shift() has no such an optimization and all that does is remove the first item and shift down. Who knows what the vendors do in native space, though.
|
-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

Comments

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

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.