Home Distribute items into containers in twos - Rails
Reply: 3

Distribute items into containers in twos - Rails

clueless
1#
clueless Published in 2018-02-14 08:22:00Z

I have a list of 10 items -- it is an array of hashes.

[{ id: 1, name: 'one'}, { id: 2, name: 'two' } .. { id: 10, name: 'ten' }]

I also have a random number of containers -- let's say 3, in this case. These containers are hashes with array values.

{ one: [], two: [], three: [] }

What I want to do, is iterate over the containers and drop 2 items at a time resulting in:

{ 
   one: [{id:1}, {id:2}, {id:7}, {id:8}], 
   two: [{id:3}, {id:4}, {id:9}, {id:10}], 
   three: [{id:5}, {id:6}] 
}

Also, if the item list is an odd number (11), the last item is still dropped into the next container.

{ 
   one: [{id:1}, {id:2}, {id:7}, {id:8}], 
   two: [{id:3}, {id:4}, {id:9}, {id:10}], 
   three: [{id:5}, {id:6}, {id:11}] 
}

note: the hashes are snipped here so it's easier to read.

My solution is something like this: (simplified)

x = 10
containers = { one: [], two: [], three: [] }

until x < 1 do
    containers.each do |c|
        c << 'x'
        c << 'x'
    end
    x -= 2
end

puts containers

I'm trying to wrap my head around how I can achieve this but I can't seem to get it to work.

Amadan
2#
Amadan Reply to 2018-02-14 09:29:36Z

Round-robin pair distribution into three bins:

bins = 3
array = 10.times.map { |i| i + 1 }
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

array.
  each_slice(2).                  # divide into pairs
  group_by.                       # group into bins
  with_index { |p, i| i % bins }. # round-robin style
  values.                         # get rid of bin indices
  each(&:flatten!)                # join pairs in each bin

Completely different approach, stuffing bins in order:

base_size, bins_with_extra = (array.size / 2).divmod(bins)
pos = 0
bins.times.map { |i|
  length = 2 * (base_size + (i < bins_with_extra ? 1 : 0)) # how much in this bin?
  array[pos, length].tap { pos += length }                 # extract and advance
}
# => [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]]

If you absolutely need to have this in a hash,

Hash[%i(one two three).zip(binned_array)]
# => {:one=>[1, 2, 7, 8], :two=>[3, 4, 9, 10], :three=>[5, 6]}

The lovely (but likely not as performant) solution hinted at by Stefan Pochmann:

bins.times.with_object(array.to_enum).map { |i, e|
  Array.new(2 * (base_size + (i < bins_with_extra ? 1 : 0))) { e.next }
}
Austin Richardson
3#
Austin Richardson Reply to 2018-02-14 08:29:57Z

You can use Enumerable#each_slice to iterate over a range from 0 to 10 in 3s and then append to an array of arrays:

containers = [
  [],
  [],
  []
]

(1...10).each_slice(3) do |slice|
  containers[0] << slice[0]
  containers[1] << slice[1]
  containers[2] << slice[2]
end

p containers
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
Stefan
4#
Stefan Reply to 2018-02-14 09:31:12Z

This is just to show a different approach (and I would probably not use this one myself).

Given an array of items and the containers hash:

items = (1..10).to_a
containers = { one: [], two: [], three: [] }

You could dup the array (in order not to modify the original one) and build an enumerator that cycles each_value in the hash:

array = items.dup
enum = containers.each_value.cycle

Using the above, you can shift 2 items off the array and push them to the next container until the array is emtpy?:

enum.next.push(*array.shift(2)) until array.empty?

Result:

containers
#=> {:one=>[1, 2, 7, 8], :two=>[3, 4, 9, 10], :three=>[5, 6]}
You need to login account before you can post.

About| Privacy statement| Terms of Service| Advertising| Contact us| Help| Sitemap|
Processed in 0.244179 second(s) , Gzip On .

© 2016 Powered by mzan.com design MATCHINFO