Ruby Hash Group by two keys

Viewed 158

I have a Ruby hash which looks like this-

report = {
    "count_by_state"=>[
        {"day"=>"2021-02-23", "policy_state"=>2, "count"=>48},
        {"day"=>"2021-02-23", "policy_state"=>3, "count"=>5},
        {"day"=>"2021-02-23", "policy_state"=>4, "count"=>89},
        {"day"=>"2021-02-25", "policy_state"=>2, "count"=>1},
        {"day"=>"2021-02-25", "policy_state"=>3, "count"=>4},
        {"day"=>"2021-02-25", "policy_state"=>4, "count"=>90}
    ]
}

I am trying to get a hash which looks like this

{2: {"2021-02-23" => 48,"2021-02-25" => 1},
3: {"2021-02-23" => 5,"2021-02-25" => 4},
4: {"2021-02-23" => 89,"2021-02-25" => 90}}

I have tried a bunch of things and have got so far with this code-

grouping = report['count_by_state'].group_by {|x| x['policy_state']}
puts grouping
puts grouping.map { |k,v| [ k => v.group_by {|x| x['day']} ] }

{2=>{"2021-02-23"=>[{"day"=>"2021-02-23", "policy_state"=>2, "count"=>48}], "2021-02-25"=>[{"day"=>"2021-02-25", "policy_state"=>2, "count"=>1}]}}
{3=>{"2021-02-23"=>[{"day"=>"2021-02-23", "policy_state"=>3, "count"=>5}], "2021-02-25"=>[{"day"=>"2021-02-25", "policy_state"=>3, "count"=>4}]}}
{4=>{"2021-02-23"=>[{"day"=>"2021-02-23", "policy_state"=>4, "count"=>89}], "2021-02-25"=>[{"day"=>"2021-02-25", "policy_state"=>4, "count"=>90}]}}

How can I get the values from day and count and create a value pair? I am trying to create a chart using squid and it uses data in a certain format.

5 Answers

You're grouping the result after the first group, but the hash content remains the same, you can use map to create a new hash only with the values you need:

report['count_by_state'].group_by { |count| count['policy_state'] }
                        .transform_values do |val|
                          val.to_h { |hash| hash.values_at('day', 'count') }
                        end
# {
# 2=>{"2021-02-23"=>48, "2021-02-25"=>1},
# 3=>{"2021-02-23"=>5,  "2021-02-25"=>4},
# 4=>{"2021-02-23"=>89, "2021-02-25"=>90}
# }

to_h is the short version of map { |...| [x, y] }.to_h, if your Ruby version doesn't support it yet, you can use that.

You could go straight to the result without going through group_by by using each_with_object and an auto-vivifying Hash:

report['count_by_state'].each_with_object(Hash.new { |h, k| h[k] = { } }) { |c, h| h[c['policy_state']][c['day']] = c['count'] }

If you really want Symbols as keys then:

report['count_by_state'].each_with_object(Hash.new { |h, k| h[k] = { } }) { |c, h| h[c['policy_state'].to_s.to_sym][c['day']] = c['count'] }
# or
report['count_by_state'].each_with_object(Hash.new { |h, k| h[k] = { } }) { |c, h| h[:"#{c['policy_state']}"][c['day']] = c['count'] }

If that sort of one-liner is too long for your taste then you can clarify it by naming the parts with methods and lambdas:

def auto_hash
  Hash.new { |h, k| h[k] = { } }
end

def extract_policy_states_from(report)
  set_day_counts = ->(c, h) { h[c['policy_state']][c['day']] = c['count'] }
  report['count_by_state'].each_with_object(auto_hash, &set_day_counts)
end

and just say extract_policy_states_from(report) to get the Hash you're after.

def parse_report(report)
    recursive_default_proc = ->(h, k) {
        h[k] = Hash.new(&recursive_default_proc)
    }
    result = Hash.new(&recursive_default_proc)
    report["count_by_state"].each do |data|
        result[data["policy_state"]][data["day"]] = data["count"]
    end
    result
end

report = {
    "count_by_state"=>[
        {"day"=>"2021-02-23", "policy_state"=>2, "count"=>48},
        {"day"=>"2021-02-23", "policy_state"=>3, "count"=>5},
        {"day"=>"2021-02-23", "policy_state"=>4, "count"=>89},
        {"day"=>"2021-02-25", "policy_state"=>2, "count"=>1},
        {"day"=>"2021-02-25", "policy_state"=>3, "count"=>4},
        {"day"=>"2021-02-25", "policy_state"=>4, "count"=>90}
    ]
}

p parse_report(report)
# {
#  2=>{"2021-02-23"=>48, "2021-02-25"=>1}, 
#  3=>{"2021-02-23"=>5, "2021-02-25"=>4}, 
#  4=>{"2021-02-23"=>89, "2021-02-25"=>90}
# }

reference: https://www.youtube.com/watch?v=jAn3c6O2OZo

I would use the form of Hash#update (aka merge!) that employs a block to determine the values of keys that are present in both hashes being merged:

report["count_by_state"].each_with_object({}) do |g,h|
  h.update(g["policy_state"]=>{g["day"]=>g["count"]}) {|_k,o,n| o.merge(n)}
end
  #=> {2=>{"2021-02-23"=>48, "2021-02-25"=>1},
  #    3=>{"2021-02-23"=>5, "2021-02-25"=>4},
  #    4=>{"2021-02-23"=>89, "2021-02-25"=>90}}

The block variables equal the following:

  • _k: the common key. The underscore signals to the reader that it is not used in the block calculation.
  • o: the value h[_k] of the ("old") hash that is being constructed by merging other hashes into it; and
  • n: the value { g["day"]=>g["count"] } of the ("new") hash { g["policy_state"]=>{ g["day"]=>g["count"] } } that is being merged into the hash being constructed, h, when _k == g["policy_state"].

Another way, maybe less obscure, is to map your data to very simple "one-case" versions of your result, and then merge them. This will work provided you don't have duplicate rows.

report['count_by_state']
  .map { |data| { data['policy_state'] => { data['day'] => data['count'] } } }
  .reduce { |this, that| this.merge(that) { |_, v1, v2| v1.merge(v2) } }

The first mapping would lead to

[{2=>{"2021-02-23"=>48}},
 {3=>{"2021-02-23"=>5}},
 {4=>{"2021-02-23"=>89}},
 {2=>{"2021-02-25"=>1}},
 {3=>{"2021-02-25"=>4}},
 {4=>{"2021-02-25"=>90}}]

which you can reduce on.

Related