Reputation: 687
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
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.
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:
rowN
property once and never has to check whether it already exists.Array.prototype.slice()
method to select our subsets rather than pushing individual items one at a time and repeatedly resizing the chunk array.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
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
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
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
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