简体   繁体   中英

reduce array of arrays into on array in collated order

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 .

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)

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

I made it with recursion approach to avoid mutation.

 let input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["first","second","third"]] function recursion(input, idx = 0) { let tmp = input.map(elm => elm[idx]) .filter(e => e !== undefined) return tmp[0] ? tmp.concat(recursion(input, idx + 1)) : [] } console.log(recursion(input))

Here's a recursive solution that meets the standards of elegance that you specified:

 const head = xs => xs[0]; const tail = xs => xs.slice(1); const notNull = xs => xs.length > 0; console.log(collate([ ["one", "two", "three"] , ["uno", "dos"] , ["1", "2", "3", "4"] , ["first", "second", "third"] ])); function collate(xss) { if (xss.length === 0) return []; const yss = xss.filter(notNull); return yss.map(head).concat(collate(yss.map(tail))); }

It can be directly translated into Haskell code:

collate :: [[a]] -> [a]
collate []  = []
collate xss = let yss = filter (not . null) xss
              in map head yss ++ collate (map tail yss)

The previous solution took big steps to compute the answer. Here's a recursive solution that takes small steps to compute the answer:

 console.log(collate([ ["one", "two", "three"] , ["uno", "dos"] , ["1", "2", "3", "4"] , ["first", "second", "third"] ])); function collate(xss_) { if (xss_.length === 0) return []; const [xs_, ...xss] = xss_; if (xs_.length === 0) return collate(xss); const [x, ...xs] = xs_; return [x, ...collate(xss.concat([xs]))]; }

Here's the equivalent Haskell code:

collate :: [[a]] -> [a]
collate []           = []
collate ([]:xss)     = collate xss
collate ((x:xs):xss) = x : collate (xss ++ [xs])

Hope that helps.

Funny solution

  1. add index as prefix on inner array
  2. Flatten the array
  3. sort the array
  4. Remove the prefix

 let input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["first","second","third"]] let ranked=input.map(i=>i.map((j,k)=>k+'---'+j)).slice() console.log(ranked.flat().sort().map(i=>i.split('---')[1]));

I've seen the problem called round-robin , but maybe interleave is a better name. Sure, map , reduce , and filter are functional procedures, but not all functional programs need rely on them. When these are the only functions we know how to use, the resulting program is sometimes awkward because there is often a better fit.

  • map produces a one-to-one result. If we have 4 subarrays, our result will have 4 elements. interleave should produce a result equal to the length of the combined subarrays, so map could only possibly get us part way there. Additional steps would be required to get the final result.

  • reduce iterates through the input elements one-at-a-time to produce a final result. In the first reduce, we will be given the first subarray, but there's no straightforward way to process the entire subarray before moving onto the next one. We can force our program to use reduce , but in doing so, it makes us think about our collation procedure as a recuding procedure instead of what it actually is.

The reality is you're not limited to the use of these primitive functional procedures. You can write interleave in a way that directly encodes its intention. I think a interleave has a beautiful recursive definition. I think the use of deep destructuring assignment is nice here because the function's signature shows the shape of the data that interleave is expecting; an array of arrays. Mathematical induction allows us to naturally handle the branches of our program -

 const None = Symbol ('None') const interleave = ( [ [ v = None, ...vs ] = [] // first subarray , ...rest // rest of subarrays ] ) => v === None ? rest.length === 0 ? vs // base: no `v`, no `rest` : interleave (rest) // inductive: some `rest` : [ v, ...interleave ([ ...rest, vs ]) ] // inductive: some `v`, some `rest` const input = [ [ "one", "two", "three" ] , [ "uno", "dos" ] , [ "1", "2", "3", "4" ] , [ "first", "second", "third" ] ] console.log (interleave (input)) // [ "one", "uno", "1", "first", "two", "dos", "2", "second", "three", "3", "third", "4" ]

interleave has released us from the shackles of close-minded thinking. I no longer need to think about my problem in terms of misshapen pieces that awkwardly fit together – I'm not thinking about array indexes, or sort , or forEach , or mutating state with push , or making comparisons using > or Math.max . Nor am I having to think about perverse things like array-like – wow, we really do take for granted just how much we've come to know about JavaScript!

Above, it should feel refreshing that there are no dependencies. Imagine a beginner approaching this program: he/she would only need to learn 1) how to define a function, 2) destructuring syntax, 3) ternary expressions. Programs cobbled together with countless small dependencies will require the learner to familiarize him/herself with each before an intuition for the program can be acquired.

That said, JavaScript syntaxes for destructuring values are not the most pretty and sometimes trades for convenience are made for increased readability -

const interleave = ([ v, ...vs ], acc = []) =>
  v === undefined
    ? acc
: isEmpty (v)
    ? interleave (vs, acc)
: interleave
    ( [ ...vs, tail (v) ]
    , [ ...acc, head (v) ]
    )

The dependencies that evolved here are isEmpty , tail , and head -

const isEmpty = xs =>
  xs.length === 0

const head = ([ x, ...xs ]) =>
  x

const tail = ([ x, ...xs ]) =>
  xs

Functionality is the same -

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

console.log (interleave (input))
// [ "one", "uno", "1", "first", "two", "dos", "2", "second", "three", "3", "third", "4" ]

Verify the results in your own browser below -

 const isEmpty = xs => xs.length === 0 const head = ([ x , ...xs ]) => x const tail = ([ x , ...xs ]) => xs const interleave = ([ v, ...vs ], acc = []) => v === undefined ? acc : isEmpty (v) ? interleave (vs, acc) : interleave ( [ ...vs, tail (v) ] , [ ...acc, head (v) ] ) const input = [ [ "one", "two", "three" ] , [ "uno", "dos" ] , [ "1", "2", "3", "4" ] , [ "first", "second", "third" ] ] console.log (interleave (input)) // [ "one", "uno", "1", "first", "two", "dos", "2", "second", "three", "3", "third", "4" ]

If you start thinking about interleave by using map , filter , and reduce , then it's likely they will be a part of the final solution. If this is your approach, it should surprise you that map , filter , and reduce are nowhere to be seen in the two programs in this answer. The lesson here is you become a prisoner to what you know. You sometimes need to forget map and reduce in order to observe that other problems have a unique nature and thus a common approach, although potentially valid, is not necessarily the best fit.

Here I have provided a generator function that will yield the values in the desired order. You could easily turn this into a regular function returning an array if you replace the yield with a push to a results array to be returned.

The algorithm takes in all the arrays as arguments, then gets the iterators for each of them. Then it enters the main loop where it treats the iters array like a queue, taking the iterator in front, yielding the next generated value, then placing it back at the end of the queue unless it is empty. The efficiency would improve if you transformed the array into a linked list where adding to the front and back take constant time, whereas a shift on an array is linear time to shift everything down one spot.

function* collate(...arrays) {
  const iters = arrays.map(a => a.values());
  while(iters.length > 0) {
    const iter = iters.shift();
    const {done, value} = iter.next();
    if(done) continue;
    yield value;
    iters.push(iter);
  }
}

What about this?

  • Sort arrays to put the longest one first.
  • flatMap them, so the index of each item in the longest array gets each index of any other array in xs .
  • Filter out undefined items in the flat array (those produced by getting indexes out of range of each possible array)

 const input = [ ["one", "two", "three"], ["uno", "dos"], ["1", "2", "3", "4"], ["first", "second", "third"] ] const arrayLengthComparer = (a, b) => b.length - a.length const collate = xs => { const [xs_, ...ys] = xs.sort (arrayLengthComparer) return xs_.flatMap ((x, i) => [x, ...ys.map (y => y[i])]) .filter (x => x) } const output = collate (input) console.log (output)

This is what I came up with... although now after seeing the other answers, this solution seems to be bulkier... and it still uses a forEach. I'm interested to hear the benefit of not using a forEach.

 var input = [["1a", "2a", "3a"], ["1b"], ["1c", "2c", "3c", "4c", "5c", "6c", "7c"],["one","two","three","four","five","six","seven"],["uno","dos","tres"],["1","2","3","4","5","6","7","8","9","10","11"],["first","second","third","fourth"]]; // sort input array by length input = input.sort((a, b) => { return b.length - a.length; }); let output = input[0]; let multiplier = 2; document.writeln(output + "<br>"); input.forEach((arr, i1) => { if (i1 > 0) { let index = -1; arr.forEach((item) => { index = index + multiplier; output.splice(index, 0, item); }); document.writeln(output + "<br>"); multiplier++; } });

Array.prototype.coallate = function (size) {
  return [...Array(Math.ceil(this.length / size)).keys()].map(i => this.slice(i * size, size * (i + 1)));
};
const result = [0,1,2,3,4,5,6,7,8,9].coallate(3)


console.log(JSON.stringify(result));

Result: [[0,1,2],[3,4,5],[6,7,8],[9]]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM