3

I'm trying to build some Heatmap specifics charts for ApexChart. So far, I got this Array of Hash, for 3 activities.

[{
  :name => "Activity 1",
  :data => {
    "May 2020" => 37, "June 2020" => 17, "July 2020" => 9, "August 2020" => 18
  }
}, {
  :name => "Activity 2",
  :data => {
    "May 2020" => 3
  }
}, {
  :name => "Activity 3",
  :data => {
    "July 2020" => 5, "November 2020" => 11
  }
}]

On Activity 3, we got only 2 months which are, July and November.

My needs would be to fill for each Hash, all missing date, and filling them with 0 as value. My awaited results would be

[{
  :name => "Activity 1",
  :data => {
    "May 2020" => 37, "June 2020" => 17, "July 2020" => 9, "August 2020" => 18, "November 2020" => 0
  }
}, {
  :name => "Activity 2",
  :data => {
    "May 2020" => 3, "June 2020" => 0, "July 2020" => 0, "August 2020" => 0, "November 2020" => 0
  }
}, {
  :name => "Activity 3",
  :data => {
    "May 2020" => , "June 2020" => 0, "July 2020" => 5, "August 2020" => 0, "November 2020" => 11
  }
}]

Yes, September is missing on purpose. I suppose the best way to achieve this would be to retrieve every months, one by one; Then to fill each arrays with the missing months; But I don't know how to achieve this.

KARASZI István
  • 30,900
  • 8
  • 101
  • 128
Toucouleur
  • 1,194
  • 1
  • 10
  • 30

3 Answers3

1

You can achieve this by something similar by first fetching the months and then modifying the array items which are Hashes in your case:

months = data.flat_map { |d| d[:data].keys }.to_set
data.each do |d|
  months.each do |month|
    d[:data][month] = 0 unless d[:data].key?(month)
  end
end

What you could also do is to create a new Array where the Hash values are initialized with a default value:

data_with_default = data.map do |d|
  {
    name: d[:name],
    data: Hash.new(0).update(d[:data])
  }
end
KARASZI István
  • 30,900
  • 8
  • 101
  • 128
  • It's close to be perfect, and thanks for your time. It gave me a better understanding on what I should have used. However, It does not follow the order of the month. Unfortunately, for Apex Charts, it's mandatory. Which means with your method I got for `Activity 3`: `{"July 2020" => 5, "November 2020" => 11, "May 2020" => , "June 2020" => 0, "August 2020" => 0}` But I will handle this by my own – Toucouleur Nov 25 '20 at 11:54
  • 1
    The Ruby `Hash` implementation does not keep the order and you should not depend on that. If that is required you'll need to find an alternative implementation like `ActiveSupport::OrderedHash`. – KARASZI István Nov 25 '20 at 11:58
  • Seems `ActiveSupport::OrderedHash` has been deprecated – Toucouleur Nov 25 '20 at 12:02
  • I might have misled you: https://stackoverflow.com/a/31418688/221213 – KARASZI István Nov 25 '20 at 12:19
  • You should note that your solution #1 mutates the original hash. In place of `d[:data][month] = 0 unless d[:data].key?(month)` you could write `d[:data][month] ||= 0`. One of the hashes produced in solution #2 is `h = {:name=>"Activity 2", :data=>{"May 2020"=>3}}`. While it is true that `h[:data]["September"] #=> 0` that's not the same as a hash having all the months. For example, `h[:data].keys #=> ["May 2020"]` produces an erroneous result. – Cary Swoveland Nov 25 '20 at 20:16
1

If arr is your array of hashes, you could construct the desired array in two steps.

require 'date'
date_fmt = "%B %Y"
first_month, last_month = arr.flat_map do |g|
  g[:data].keys
end.map { |s| Date.strptime(s, date_fmt) }.minmax
  #=> [#<Date: 2020-05-01 ((2458971j,0s,0n),+0s,2299161j)>,
  #    #<Date: 2020-11-01 ((2459155j,0s,0n),+0s,2299161j)>] 

h = (first_month..last_month).map do |d|
  d.strftime(date_fmt)
end.product([0]).to_h
  #=> {"May 2020"=>0, "June 2020"=>0, "July 2020"=>0, "August 2020"=>0,
  #    "September 2020"=>0, "October 2020"=>0, "November 2020"=>0} 
arr.map { |g| g.merge(:data => h.merge(g[:data])) }
  #=> [
  #     {
  #       :name=>"Activity 1",
  #       :data=>{
  #         "May 2020"=>37, "June 2020"=>17, "July 2020"=>9,
  #         "August 2020"=>18, "September 2020"=>0,
  #         "October 2020"=>0, "November 2020"=>0
  #       }
  #     },
  #     {
  #       :name=>"Activity 2",
  #       :data=>{
  #         "May 2020"=>3, "June 2020"=>0, "July 2020"=>0,
  #         "August 2020"=>0, "September 2020"=>0,
  #         "October 2020"=>0, "November 2020"=>0
  #       }
  #     },
  #     {
  #       :name=>"Activity 3",
  #       :data=>{
  #         "May 2020"=>0, "June 2020"=>0, "July 2020"=>5,
  #         "August 2020"=>0, "September 2020"=>0,
  #         "October 2020"=>0, "November 2020"=>11
  #       }
  #     }
  #   ] 

See Enumerable#flat_map, Date::strptime, Array#minmax, Date#strftime, Array#product and Hash#merge. See also DateTime#strptime for date formatting directives.

Note that in the calculation of first_month and last_month,

[#<Date: 2020-05-01 ((2458971j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-11-01 ((2459155j,0s,0n),+0s,2299161j)>].
  map { |d| d.strftime(date_fmt) }
  #=> ["May 2020", "November 2020"]
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

Try the following:

a = [{
  :name => "Activity 1",
  :data => {
    "May 2020" => 37, "June 2020" => 17, "July 2020" => 9, "August 2020" => 18
  }
}, {
  :name => "Activity 2",
  :data => {
    "May 2020" => 3
  }
}, {
  :name => "Activity 3",
  :data => {
    "July 2020" => 5, "November 2020" => 11
  }
}]

months = ['May 2020', 'June 2020', 'July 2020', 'August 2020', 'November 2020']

puts a.map { |h| { name: h[:name], data: Hash[months.map { |m| [m, h[:data][m] ? h[:data][m] : 0] }] } }
Ben Trewern
  • 1,583
  • 1
  • 10
  • 13