Split an array into slices, with groupings

93 views Asked by At

I've got some Ruby code here, that works, but I'm certain I'm not doing it as efficiently as I can.

I have an Array of Objects, along this line:

[
    { name: "foo1", location: "new york" },
    { name: "foo2", location: "new york" },
    { name: "foo3", location: "new york" },
    { name: "bar1", location: "new york" },
    { name: "bar2", location: "new york" },
    { name: "bar3", location: "new york" },
    { name: "baz1", location: "chicago" },
    { name: "baz2", location: "chicago" },
    { name: "baz3", location: "chicago" },
    { name: "quux1", location: "chicago" },
    { name: "quux2", location: "chicago" },
    { name: "quux3", location: "chicago" }
]

I want to create some number of groups - say 3 - where each group contains a semi-equal amount of items, but interspersed by location.

I tried something like this:

group_size = 3
groups = []

group_size.times do
    groups.push([])
end

i = 0
objects.each do |object|
    groups[i].push(object)
    if i < (group_size - 1)
        i += 1
    else
        i = 0
    end
end

This returns a groups object, that looks like:

[
    [{:name=>"foo1", :location=>"new york"},
     {:name=>"bar1", :location=>"new york"},
     {:name=>"baz1", :location=>"chicago"},
     {:name=>"quux1", :location=>"chicago"}],
    [{:name=>"foo2", :location=>"new york"},
     {:name=>"bar2", :location=>"new york"},
     {:name=>"baz2", :location=>"chicago"},
     {:name=>"quux2", :location=>"chicago"}],
    [{:name=>"foo3", :location=>"new york"},
     {:name=>"bar3", :location=>"new york"},
     {:name=>"baz3", :location=>"chicago"},
     {:name=>"quux3", :location=>"chicago"}]
]

So you can see there's a couple of objects from each location in each grouping.

I played around with each_slice() and group_by(), even tried to use inject([]) - but I couldn't figure out a more elegant method to do this.

I'm hoping it's something that I'm overlooking - and I need to account for more locations and a non-even number of Objects.

3

There are 3 answers

2
steenslag On BEST ANSWER

Yes, this bookkeeping with i is usually a sign there should be something better. I came up with:

ar =[
    { name: "foo1", location: "new york" },
    { name: "foo2", location: "new york" },
    { name: "foo3", location: "new york" },
    { name: "bar1", location: "new york" },
    { name: "bar2", location: "new york" },
    { name: "bar3", location: "new york" },
    { name: "baz1", location: "chicago" },
    { name: "baz2", location: "chicago" },
    { name: "baz3", location: "chicago" },
    { name: "quux1", location: "chicago" },
    { name: "quux2", location: "chicago" },
    { name: "quux3", location: "chicago" }
]

# next line handles unsorted arrays, irrelevant with this data 
ar = ar.sort_by{|h| h[:location]}

num_groups = 3
groups     = Array.new(num_groups){[]}
wheel      = groups.cycle
ar.each{|h| wheel.next << h}

# done.
p groups
# => [[{:name=>"baz1", :location=>"chicago"}, {:name=>"quux1", :location=>"chicago"}, {:name=>"foo1", :location=>"new york"}, ...]

because I like the cycle method.

4
engineersmnky On

a.each_slice(group_size).to_a.transpose

Will work given that your data is accurately portrayed in the example. If it is not please supply accurate data so that we can answer the question more appropriately.

e.g.

a= [
    { name: "foo1", location: "new york" },
    { name: "foo2", location: "new york" },
    { name: "foo3", location: "new york" },
    { name: "bar1", location: "new york" },
    { name: "bar2", location: "new york" },
    { name: "bar3", location: "new york" },
    { name: "baz1", location: "chicago" },
    { name: "baz2", location: "chicago" },
    { name: "baz3", location: "chicago" },
    { name: "quux1", location: "chicago" },
    { name: "quux2", location: "chicago" },
    { name: "quux3", location: "chicago" }
]
group_size = 3
a.each_slice(group_size).to_a.transpose
#=> [
     [
      {:name=>"foo1", :location=>"new york"},
      {:name=>"bar1", :location=>"new york"},
      {:name=>"baz1", :location=>"chicago"},
      {:name=>"quux1", :location=>"chicago"}
    ],
    [
      {:name=>"foo2", :location=>"new york"},
      {:name=>"bar2", :location=>"new york"},
      {:name=>"baz2", :location=>"chicago"},
      {:name=>"quux2", :location=>"chicago"}
    ],
    [
      {:name=>"foo3", :location=>"new york"},
      {:name=>"bar3", :location=>"new york"},
      {:name=>"baz3", :location=>"chicago"},
      {:name=>"quux3", :location=>"chicago"}
    ]
  ]

each_slice 3 will turn this into 4 equal groups (numbered 1,2,3) in your example. transpose will then turn these 4 groups into 3 groups of 4.

If the locations are not necessarily in order you can add sorting to the front of the method chain

a.sort_by { |h| h[:location]  }.each_slice(group_size).to_a.transpose

Update

It was pointed out that an uneven number of arguments for transpose will raise. My first though was to go with @CarySwoveland's approach but since he already posted it I came up with something a little different

class Array
  def indifferent_transpose
    arr = self.map(&:dup)
    max = arr.map(&:size).max
    arr.each {|a| a.push(*([nil] * (max - a.size)))}
    arr.transpose.map(&:compact)
  end
end

then you can still use the same methodology

a << {name: "foobar1", location: "taiwan" }
a.each_slice(group_size).to_a.indifferent_transpose
#=> [[{:name=>"foo1", :location=>"new york"},
      {:name=>"bar1", :location=>"new york"},
      {:name=>"baz1", :location=>"chicago"},
      {:name=>"quux1", :location=>"chicago"},
      #note the extras values will be placed in the group arrays in order 
      {:name=>"foobar4", :location=>"taiwan"}], 
    [{:name=>"foo2", :location=>"new york"},
      {:name=>"bar2", :location=>"new york"},
      {:name=>"baz2", :location=>"chicago"},
      {:name=>"quux2", :location=>"chicago"}],
    [{:name=>"foo3", :location=>"new york"},
      {:name=>"bar3", :location=>"new york"},
      {:name=>"baz3", :location=>"chicago"},
      {:name=>"quux3", :location=>"chicago"}]]
3
Cary Swoveland On

Here's another way to do it.

Code

def group_em(a, ngroups)
  a.each_with_index.with_object(Array.new(ngroups) {[]}) {|(e,i),arr|
    arr[i%ngroups] << e}
end

Example

a = [
    { name: "foo1",  location: "new york" },
    { name: "foo2",  location: "new york" },
    { name: "foo3",  location: "new york" },
    { name: "bar1",  location: "new york" },
    { name: "bar2",  location: "new york" },
    { name: "bar3",  location: "new york" },
    { name: "baz1",  location: "chicago"  },
    { name: "baz2",  location: "chicago"  },
    { name: "baz3",  location: "chicago"  },
    { name: "quux1", location: "chicago"  },
    { name: "quux2", location: "chicago"  }
]

Note that I've omitted the last element of a from the question in order for a to have an odd number of elements.

group_em(a,3)
  #=> [[{:name=>"foo1",  :location=>"new york"},
  #     {:name=>"bar1",  :location=>"new york"},
  #     {:name=>"baz1",  :location=>"chicago" },
  #     {:name=>"quux1", :location=>"chicago" }],
  #    [{:name=>"foo2",  :location=>"new york"},
  #     {:name=>"bar2",  :location=>"new york"},
  #     {:name=>"baz2",  :location=>"chicago" },
  #     {:name=>"quux2", :location=>"chicago" }],
  #    [{:name=>"foo3",  :location=>"new york"},
  #     {:name=>"bar3",  :location=>"new york"},
  #     {:name=>"baz3",  :location=>"chicago" }]]