ChrisInEdmonton
ChrisInEdmonton

Reputation: 4578

How to interleave arrays of different length in Ruby

If I want to interleave a set of arrays in Ruby, and each array was the same length, we could do so as:

a.zip(b).zip(c).flatten

However, how do we solve this problem if the arrays can be different sizes?

We could do something like:

def interleave(*args)
  raise 'No arrays to interleave' if args.empty?
  max_length = args.inject(0) { |length, elem| length = [length, elem.length].max }
  output = Array.new
  for i in 0...max_length
    args.each { |elem|
      output << elem[i] if i < elem.length
    }
  end
  return output
end

But is there a better 'Ruby' way, perhaps using zip or transpose or some such?

Upvotes: 8

Views: 5534

Answers (3)

David J.
David J.

Reputation: 32705

Here is a simpler approach. It takes advantage of the order that you pass the arrays to zip:

def interleave(a, b)
  if a.length >= b.length
    a.zip(b)
  else
    b.zip(a).map(&:reverse)
  end.flatten.compact
end

interleave([21, 22], [31, 32, 33])
# => [21, 31, 22, 32, 33]

interleave([31, 32, 33], [21, 22])
# => [31, 21, 32, 22, 33]

interleave([], [21, 22])
# => [21, 22]

interleave([], [])
# => []

Be warned: this removes all nil's:

interleave([11], [41, 42, 43, 44, nil])
# => [11, 41, 42, 43, 44]

Upvotes: 8

John La Rooy
John La Rooy

Reputation: 304147

If the source arrays don't have nil in them, you only need to extend the first array with nils, zip will automatically pad the others with nil. This also means you get to use compact to clean the extra entries out which is hopefully more efficient than explicit loops

def interleave(a,*args)
    max_length = args.map(&:size).max
    padding = [nil]*[max_length-a.size, 0].max
    (a+padding).zip(*args).flatten.compact
end

Here is a slightly more complicated version that works if the arrays do contain nil

def interleave(*args)
    max_length = args.map(&:size).max
    pad = Object.new()
    args = args.map{|a| a.dup.fill(pad,(a.size...max_length))}
    ([pad]*max_length).zip(*args).flatten-[pad]
end

Upvotes: 7

thorncp
thorncp

Reputation: 3627

Your implementation looks good to me. You could achieve this using #zip by filling the arrays with some garbage value, zip them, then flatten and remove the garbage. But that's too convoluted IMO. What you have here is clean and self explanatory, it just needs to be rubyfied.

Edit: Fixed the booboo.

def interleave(*args)
  raise 'No arrays to interleave' if args.empty?
  max_length = args.map(&:size).max
  output = []
  max_length.times do |i|
    args.each do |elem|
      output << elem[i] if i < elem.length
    end
  end
  output
end

a = [*1..5]
# => [1, 2, 3, 4, 5]
b = [*6..15]
# => [6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
c = [*16..18]
# => [16, 17, 18]

interleave(a,b,c)
# => [1, 6, 16, 2, 7, 17, 3, 8, 18, 4, 9, 5, 10, 11, 12, 13, 14, 15]

Edit: For fun

def interleave(*args)
  raise 'No arrays to interleave' if args.empty?
  max_length = args.map(&:size).max
  # assumes no values coming in will contain nil. using dup because fill mutates
  args.map{|e| e.dup.fill(nil, e.size...max_length)}.inject(:zip).flatten.compact
end

interleave(a,b,c)
# => [1, 6, 16, 2, 7, 17, 3, 8, 18, 4, 9, 5, 10, 11, 12, 13, 14, 15]

Upvotes: 5

Related Questions