Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

reduce array of arrays into on array in collated order

Tags:

I'm trying to use reduce() combine a set of arrays in a "collated" order so items with similar indexes are together. For example:

input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["first","second","third"]]  output = [ 'first','1','uno','one','second','2','dos','two','third','3','three','4' ] 

It doesn't matter what order the items with similar index go as long as they are together, so a result of 'one','uno','1'... is a good as what's above. I would like to do it just using immutable variables if possible.

I have a way that works:

    const output = input.reduce((accumulator, currentArray, arrayIndex)=>{         currentArray.forEach((item,itemIndex)=>{             const newIndex = itemIndex*(arrayIndex+1);             accumulator.splice(newIndex<accumulator.length?newIndex:accumulator.length,0,item);         })         return accumulator;     }) 

But it's not very pretty and I don't like it, especially because of the way it mutates the accumulator in the forEach method. I feel there must be a more elegant method.

I can't believe no one has asked this before but I've tried a bunch of different queries and can't find it, so kindly tell me if it's there and I missed it. Is there a better way?

To clarify per question in comments, I would like to be able to do this without mutating any variables or arrays as I'm doing with the accumulator.splice and to only use functional methods such as .map, or .reduce not a mutating loop like a .forEach.

like image 287
jimboweb Avatar asked Feb 20 '19 22:02

jimboweb


2 Answers

Maybe just a simple for... i loop that checks each array for an item in position i

var input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["1st","2nd","3rd"]]    var output = []  var maxLen = Math.max(...input.map(arr => arr.length));    for (i=0; i < maxLen; i++) {    input.forEach(arr => { if (arr[i] !== undefined) output.push(arr[i]) })  }    console.log(output)

Simple, but predictable and readable


Avoiding For Each Loop

If you need to avoid forEach, here's a similar approach where you could: get the max child array length, build a range of integers that would've been created by the for loop ([1,2,3,4]), map each value to pivot the arrays, flatten the multi-dimensional array, and then filter out the empty cells.

First in discrete steps, and then as a one liner:

var input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["1st","2nd","3rd"]]; 

Multiple Steps:

var maxLen = Math.max(...input.map(arr => arr.length)); var indexes = Array(maxLen).fill().map((_,i) => i); var pivoted = indexes.map(i => input.map(arr => arr[i] )); var flattened = pivoted.flat().filter(el => el !== undefined); 

One Liner:

var output = Array(Math.max(...input.map(arr => arr.length))).fill().map((_,i) => i)                .map(i => input.map(arr => arr[i] ))                .flat().filter(el => el !== undefined) 
like image 191
KyleMit Avatar answered Oct 05 '22 02:10

KyleMit


Use Array.from() to create a new array with the length of the longest sub array. To get the length of the longest sub array, get an array of the lengths with Array.map() and take the max item.

In the callback of Array.from() use Array.reduceRight() or Array.reduce() (depending on the order you want) to collect items from each sub array. Take the item if the current index exists in the sub array. Flatten the sub arrays with Array.flat().

const input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["first","second","third"]]    const result = Array.from(      { length: Math.max(...input.map(o => o.length)) },      (_, i) => input.reduceRight((r, o) =>        i < o.length ? [...r, o[i]] : r      , [])    )    .flat();    console.log(result);
like image 24
Ori Drori Avatar answered Oct 05 '22 01:10

Ori Drori