Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

group_by multiple times in ruby

Tags:

ruby

group-by

I have an array of hashes called events:

events = [
  {:name => "Event 1", :date => "2019-02-21 08:00:00", :area => "South", :micro_area => "A"},
  {:name => "Event 2", :date => "2019-02-21 08:00:00", :area => "South", :micro_area => "A"},
  {:name => "Event 3", :date => "2019-02-21 08:00:00", :area => "South", :micro_area => "B"},
  {:name => "Event 4", :date => "2019-02-21 08:00:00", :area => "South", :micro_area => "B"},
  {:name => "Event 5", :date => "2019-02-21 08:00:00", :area => "North", :micro_area => "A"},
  {:name => "Event 6", :date => "2019-02-21 08:00:00", :area => "North", :micro_area => "A"},
  {:name => "Event 7", :date => "2019-02-21 08:00:00", :area => "North", :micro_area => "B"},
  {:name => "Event 8", :date => "2019-02-21 08:00:00", :area => "North", :micro_area => "B"}
]

I want to know how to group_by first date, then area then micro_area to end up with a single array of hashes for example:

[
  {
    "2019-02-21 08:00:00": {
      "South": {
        "A": [
          {:name=>"Event 1", :date=>"2019-02-21 08:00:00", :area=>"South", :micro_area=>"A" },
          {:name=>"Event 2", :date=>"2019-02-21 08:00:00", :area=>"South", :micro_area=>"A" }
        ],
        "B": [
          {:name=>"Event 3", :date=>"2019-02-21 08:00:00", :area=>"South", :micro_area=>"B" },
          {:name=>"Event 4", :date=>"2019-02-21 08:00:00", :area=>"South", :micro_area=>"B" }
        ]  
      },
      "North": {
        "A": [
          {:name=>"Event 5", :date=>"2019-02-21 08:00:00", :area=>"North", :micro_area=>"A" },
          {:name=>"Event 6", :date=>"2019-02-21 08:00:00", :area=>"North", :micro_area=>"A" }
        ],
        "B": [
          {:name=>"Event 7", :date=>"2019-02-21 08:00:00", :area=>"North", :micro_area=>"B" },
          {:name=>"Event 8", :date=>"2019-02-21 08:00:00", :area=>"North", :micro_area=>"B" }
        ]  
      }
    }
  }
] 

Trying events.group_by { |r| [r[:date], r[:area], r[:micro_area]] } doesn't seem too work the way I want it to.

like image 631
MikeHolford Avatar asked Feb 21 '19 10:02

MikeHolford


People also ask

What does Group_by do in Ruby?

The group_by() of enumerable is an inbuilt method in Ruby returns an hash where the groups are collectively kept as the result of the block after grouping them. In case no block is given, then an enumerator is returned.

What is group_ by in Rails?

group_by takes a block as an argument and returns a hash. Our block will return the number passed to it divided by 5. Each key in the returned hash will be a value that was returned by the block, and each value in the hash will be an array of the values passed to the block that returned that result. So, for example.


4 Answers

I think following will work for you,

events = [
  { name: 'Event 1', date: '2019-02-21 08:00:00', area: 'South', micro_area: 'A' }
]

events.group_by { |x| x[:date] }.transform_values do |v1|
  v1.group_by { |y| y[:area] }.transform_values do |v2|
    v2.group_by { |z| z[:micro_area] }
  end
end
# {
#   "2019-02-21 08:00:00"=>{
#     "South"=>{
#       "A"=>[
#         {:name=>"Event 1", :date=>"2019-02-21 08:00:00", :area=>"South", :micro_area=>"A"}
#       ]
#     }
#   }
# }   
like image 88
ray Avatar answered Nov 15 '22 07:11

ray


Another option is to build the nested structure as you traverse your hash:

events.each_with_object({}) do |event, result|
  d, a, m = event.values_at(:date, :area, :micro_area)
  result[d] ||= {}
  result[d][a] ||= {}
  result[d][a][m] ||= []
  result[d][a][m] << event
end
like image 39
Stefan Avatar answered Nov 15 '22 05:11

Stefan


Another option is grouping them like you did in the question. Then build the nested structure from the array used as key.

# build an endless nested structure
nested = Hash.new { |hash, key| hash[key] = Hash.new(&hash.default_proc) }

# group by the different criteria and place them in the above nested structure
events.group_by { |event| event.values_at(:date, :area, :micro_area) }
      .each { |(*path, last), events| nested.dig(*path)[last] = events }

# optional - reset all default procs
reset_default_proc = ->(hash) { hash.each_value(&reset_default_proc).default = nil if hash.is_a?(Hash) }
reset_default_proc.call(nested)

The above leaves the answer in the nested variable.

References:

  • Hash::new to create the nested hash.
  • Hash#default_proc to get the default proc of a hash.
  • Hash#default= to reset the hash default back to nil.
  • Hash#dig to traverse the nested structure until the last node.
  • Hash#[]= to set the last node equal to the grouped events.
  • Array decomposition and array to argument conversion to capture all but the last node into path and call #dig with the contents of path as arguments.
like image 25
3limin4t0r Avatar answered Nov 15 '22 06:11

3limin4t0r


Here is a recursive solution that will handle arbitrary levels of nesting and arbitrary grouping objects.

def hashify(events, grouping_keys)
  return events if grouping_keys.empty?
  first_key, *remaining_keys = grouping_keys
  events.group_by { |h| h[first_key] }.
         transform_values { |a|
           hashify(a.map { |h|
             h.reject { |k,_| k == first_key } },
             remaining_keys) }
end

Before executing this with the sample data from the questions let's add a hash with a different date to events.

events <<
  { :name=>"Event 8", :date=>"2018-12-31 08:00:00",
    :area=>"North",   :micro_area=>"B" }

grouping_keys = [:date, :area, :micro_area]

hashify(events, grouping_keys)
  #=> {"2019-02-21 08:00:00"=>{
  #      "South"=>{
  #        "A"=>[{:name=>"Event 1"}, {:name=>"Event 2"}],
  #        "B"=>[{:name=>"Event 3"}, {:name=>"Event 4"}]
  #      },
  #      "North"=>{
  #        "A"=>[{:name=>"Event 5"}, {:name=>"Event 6"}],
  #        "B"=>[{:name=>"Event 7"}, {:name=>"Event 8"}]
  #      }
  #    },
  #    "2018-12-31 08:00:00"=>{
  #      "North"=>{
  #        "B"=>[{:name=>"Event 8"}]
  #      }
  #    }
  #  } 

hashify(events, [:date, :area])
  #=> {"2019-02-21 08:00:00"=>{
  #      "South"=>[
  #        {:name=>"Event 1", :micro_area=>"A"},
  #        {:name=>"Event 2", :micro_area=>"A"},
  #        {:name=>"Event 3", :micro_area=>"B"},
  #        {:name=>"Event 4", :micro_area=>"B"}
  #      ],
  #      "North"=>[
  #        {:name=>"Event 5", :micro_area=>"A"},
  #        {:name=>"Event 6", :micro_area=>"A"},
  #        {:name=>"Event 7", :micro_area=>"B"},
  #        {:name=>"Event 8", :micro_area=>"B"}
  #      ]
  #    },
  #    "2018-12-31 08:00:00"=>{
  #      "North"=>[
  #       {:name=>"Event 8", :micro_area=>"B"}
  #      ]
  #    }
  #  } 

See Enumerable#group_by, Hash#transform_values and Hash#reject.

like image 38
Cary Swoveland Avatar answered Nov 15 '22 05:11

Cary Swoveland