Kori John Roys
Kori John Roys

Reputation: 2661

How to combine two Ruby collections?

A user is given two inputs, A and B (collections), and a number, X, and must create an output. The output starts with the first value from A, then takes X units from B, then the second value of A, and continuing like that until both A and B have been exhausted. You can assume 1 < X < sizeof(B). However, if either A or B runs out, you should loop back to the beginning of the short collection, and continue until the other runs out. The looping back should continue until both collections have hit their end.

What is an elegant way to do this in Ruby? I have some half-broken code and can't seem to find a good way to do this. No, it's not a homework problem. I have strange hobbies.

Here's some sample tests of the behavior I want:

# SomeModule.copy(A, B, X)
SomeModule.copy(%w(a1 a2 a3),    %w(b1 b2 b3),       1) == %w(a1 b1 a2 b2 a3 b3))
SomeModule.copy(%w(a1),          %w(b1),             1) == %w(a1 b1))
SomeModule.copy(%w(a1),          %w(b1 b2 b3),       1) == %w(a1 b1 a1 b2 a1 b3))
SomeModule.copy(%w(a1 a2),       %w(b1 b2 b3 b4 b5), 2) == %w(a1 b1 b2 a2 b3 b4 a1 b5 b1))
SomeModule.copy(%w(a1 a2),       %w(b1 b2 b3 b4 b5), 2) == %w(a1 b1 b2 a2 b3 b4 a1 b5 b1))
SomeModule.copy(%w(a1 a2 a3 a4), %w(b1 b2 b3 b4 b5), 3) == %w(a1 b1 b2 b3 a2 b4 b5 b1 a3 b2 b3 b4 a4 b5 b1 b2))

Upvotes: 0

Views: 103

Answers (4)

Eric Mathison
Eric Mathison

Reputation: 1256

def copy(array1, array2, slice_length)
  iterations = [array1.length,
                (array2.length.to_f / slice_length.to_f).round
               ].max

  result = []
  enum1 = array1.cycle
  enum2 = array2.cycle

  iterations.times do
    result << enum1.next
    slice_length.times { result << enum2.next }
  end

  result
end

require 'minitest/autorun'

class TestCopy < Minitest::Test
  def test_copy
    assert_equal(%w(a1 b1 a2 b2 a3 b3),                               copy(%w(a1 a2 a3),    %w(b1 b2 b3),       1))
    assert_equal(%w(a1 b1),                                           copy(%w(a1),          %w(b1),             1))
    assert_equal(%w(a1 b1 a1 b2 a1 b3),                               copy(%w(a1),          %w(b1 b2 b3),       1))
    assert_equal(%w(a1 b1 b2 a2 b3 b4 a1 b5 b1),                      copy(%w(a1 a2),       %w(b1 b2 b3 b4 b5), 2))
    assert_equal(%w(a1 b1 b2 a2 b3 b4 a1 b5 b1),                      copy(%w(a1 a2),       %w(b1 b2 b3 b4 b5), 2))
    assert_equal(%w(a1 b1 b2 b3 a2 b4 b5 b1 a3 b2 b3 b4 a4 b5 b1 b2), copy(%w(a1 a2 a3 a4), %w(b1 b2 b3 b4 b5), 3))
  end
end

Upvotes: 0

Kori John Roys
Kori John Roys

Reputation: 2661

Here's what finally jumped out of my brain at 4.30am:

class InfiniteArray
  private
  attr_reader :collection
  attr_accessor :index, :wrapped_count
  public

  def initialize(collection)
    @collection = collection
    reset
  end

  def next
    value = collection[index]
    if index < collection.length - 1
      self.index = index + 1
    else
      self.index = 0
      self.wrapped_count = wrapped_count + 1
    end
    value
  end

  def wrapped?
    wrapped_count > 0
  end

  def reset
    @index = 0
    @wrapped_count = 0
  end
end

class IntervalCopier
  private
  attr_reader :source_collection, :destination_collection, :interval
  public

  def initialize(source_collection, destination_collection, interval)
    @source_collection = InfiniteArray.new(source_collection)
    @destination_collection = InfiniteArray.new(destination_collection)
    @interval = interval
  end

  def copy
    final = []
    until source_collection.wrapped? && destination_collection.wrapped? do
      final << source_collection.next
      interval.times { final << destination_collection.next }
    end
    [source_collection, destination_collection].each(&:reset)
    final
  end
end

Upvotes: 0

coreyward
coreyward

Reputation: 80041

Enumerable has an each_slice method that'll let you iterate over an enumerable object N items at a time. Using it, you can do something like this:

a1 = [1, 2, 3, 4, 5, 6] 
a2 = [10, 11, 12, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52, 60] 

a1.each_slice(1).zip(a2.each_slice(3)).flatten

# => [1, 10, 11, 12, 2, 20, 21, 22, 3, 30, 31, 32, 4, 40, 41, 42, 5, 50, 51, 52, 6, 60] 

Upvotes: 1

JCorcuera
JCorcuera

Reputation: 6834

Here is something, the idea is to iterate every array until both of them reach their max index:

def custom_combine(a,b,x)
  max_index_a = a.count - 1
  max_index_b = b.count - 1
  a_run_out = false
  b_run_out = false

  output = []

  a.cycle.each_with_index do |elem_a, index_a|
    output << elem_a
    # base on a index get x elements from b
    x.times do |i|
      index_b = ((index_a * x) + i) % (max_index_b + 1)
      output << b[index_b]
      b_run_out = true if index_b == max_index_b
    end
    a_run_out = true if index_a == max_index_a
    break if a_run_out && b_run_out
  end

  output
end

a = %w(a1 a2 a3 a4)
b = %w(b1 b2 b3 b4 b5)
x = 3

custom_combine(a,b,x)
# => ["a1", "b1", "b2", "b3", "a2", "b4", "b5", "b1", "a3", "b2", "b3", "b4", "a4", "b5", "b1", "b2"] 

Upvotes: 1

Related Questions