简体   繁体   中英

Ruby group array of hashes by numeric key

I got an array of hashes like this one:

[{1=>6}, {1=>5}, {4=>1}]

I try to group by the keys.

So the solution with named keys was like: group_by { |h| h['keyName'] } group_by { |h| h['keyName'] } .

How can I get the following array with short Lambda expressions or with group_by:

[{1=>[5, 6], 4=>[1]}]

EDIT - To explain what I am trying to achieve: I got a database to allocate pupils to courses. Each pupil is able to vote each year for a course.

The votes look like this:

Vote(id: integer, first: integer, second: integer, active: boolean,
     student_id: integer, course_id: integer, year_id: integer,
     created_at: datetime, updated_at: datetime)

Now I would like to allocate the pupils automatically to a course, if the course is not overstaffed . To find out how many pupils voted for each course I first tried this:

Year.get_active_year.votes.order(:first).map(&:first).group_by(&:itself)

the result looks like this:

{1=>[1, 1], 4=>[4]}

Now I am able to use the .each function:

Year.get_active_year.votes.order(:first).map(&:first).group_by(&:itself).each do |_key, value|
  if Year.get_active_year.courses.where(number: _key).first.max_visitor >= value.count

  end
end

each course got an explicit number and the pupils just use the course number to vote.

But if I do all this, I lose the information which pupil voted for that course, so I tried to keep the information like this:

Year.get_active_year.votes.order(:first).map{|c| {c.first=> c.student_id}}

Injecting into a default hash:

arr = [{1=>6}, {1=>5}, {4=>1}]
arr.inject(Hash.new{|h,k| h[k]=[]}){|h, e| h[e.first[0]] << e.first[1]; h}

# => {1=>[6, 5], 4=>[1]}

Or, as suggested in the comments:

arr.each.with_object(Hash.new{|h, k| h[k] = []}){|e, h| h[e.first[0]] << e.first[1]}

# => {1=>[6, 5], 4=>[1]}
def group_values(arr)
  arr.reduce(Hash.new {|h,k| h[k]=[]}) do |memo, h|
    h.each { |k, v| memo[k] << v }
    memo
  end
end

xs = [{1=>6}, {1=>5}, {4=>1}]
group_values(xs) # => {1=>[6, 5], 4=>[1]}

Note that this solution also works when the hashes contain multiple entries:

ys = [{1=>6, 4=>2}, {1=>5}, {4=>1}]
group_values(ys) # => {1=>[6, 5], 4=>[2, 1]}
arr = [{1=>6}, {1=>5}, {4=>1}]

arr.flat_map(&:to_a).
    group_by(&:first).
    transform_values { |arr| arr.transpose.last }
  #=> {1=>[6, 5], 4=>[1]}

The steps are as follows.

a = arr.flat_map(&:to_a)
  #=> [[1, 6], [1, 5], [4, 1]]
b = a.group_by(&:first)
  #=> {1=>[[1, 6], [1, 5]], 4=>[[4, 1]]}
b.transform_values { |arr| arr.transpose.last }
  #=> {1=>[6, 5], 4=>[1]}

Note that

b.transform_values { |arr| arr.transpose }
  #=> {1=>[[1, 1], [6, 5]], 4=>[[4], [1]]}

and arr.flat_map(&:to_a) can be replaced with arr.map(&:flatten) .

Another way:

arr.each_with_object({}) do |g,h|
  k,v = g.flatten
  h.update(k=>[v]) { |_,o,n| o+n }  
end
  #=> {1=>[6, 5], 4=>[1]}

This uses the form of Hash#update (aka merge! ) that employs the block { |_,o,n| o+n } { |_,o,n| o+n } to determine the values of keys that are present in both hashes being merged. The block variable _ is the common key (represented by an underscore to signal that it is not used in the block calculations). The variables o and n are respectively the values of the common key in the two hashes being merged.

One way to achieve this using #group_by could be to group by the first key of each hash, then #map over the result to return the corresponding values:

arr = [{1=>6}, {1=>5}, {4=>1}]
arr.group_by {|h| h.keys.first}.map {|k, v| {k => v.map {|h| h.values.first}}}

# => [{1=>[6, 5], 4=>[1]}]

Hope this helps!

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