Reputation: 158
I have a hash of Month->Value
{"Apr 2016"=>6, "Aug 2016"=>9, "Jan 2017"=>11, "Apr 2017"=>6, "May 2017"=>9, "Jun 2017"=>1, "Jul 2017"=>9}
I would know what is the best way to get all empty months between the first and the last value
I would like something like
{"Apr 2016"=>6, May 2016=>0, Jun 2016=>0 .... "Aug 2016"=>9, "Sep 2016" => 0 "Jan 2017"=>11, "Apr 2017"=>6, "May 2017"=>9, "Jun 2017"=>1, "Jul 2017"=>9}
Upvotes: 1
Views: 559
Reputation: 110685
Code
require 'date'
def fill_in_missing_months(dates)
date_fmt = '%b %Y'
fm, lm = dates.keys.
minmax_by { |date| Date.strptime(date, date_fmt) }.
map { |date| Date.strptime(date, date_fmt) }
(0..12*(lm.year-fm.year) + lm.month-fm.month).each_with_object({}) do |_,h|
str = fm.strftime(date_fmt)
h[str] = dates.fetch(str, 0)
fm >>= 1
end
end
Example
dates = {"Apr 2016"=>6, "Aug 2016"=>9, "Jan 2017"=>11, "Apr 2017"=>6,
"May 2017"=>9, "Jun 2017"=>1, "Jul 2017"=>9}
fill_in_missing_months(dates)
#=> {"Apr 2016"=>6, "May 2016"=>0, "Jun 2016"=>0, "Jul 2016"=>0, "Aug 2016"=>9,
# "Sep 2016"=>0, "Oct 2016"=>0, "Nov 2016"=>0, "Dec 2016"=>0, "Jan 2017"=>11,
# "Feb 2017"=>0, "Mar 2017"=>0, "Apr 2017"=>6, "May 2017"=>9, "Jun 2017"=>1,
# "Jul 2017"=>9}
Explanation
Experienced Rubiests: A gory-detail-follows advisory has been issued, so you may wish to skip the rest of my answer.
Ruby expands fm >>= 1
to fm = fm >> 1
when parsing the code. Date#>> advances the date by the number of months given by its argument, which here is 1
.
In addition to :>>
, see the docs for the methods Integer#times, Hash#fetch, Date::strptime, Date#strftime, Date#year, Date#month, Enumerator#with_object and Enumerable#minimax_by (not including the more common methods such as Enumerable#map
). Recall #
in Date#year
denotes an instance method whereas ::
in [Date::strptime]
indicates a class method.
For dates
given in the example, the steps are as follows.
date_fmt = '%b %Y'
b = dates.keys
#=> ["Apr 2016", "Aug 2016", "Jan 2017", "Apr 2017", "May 2017",
# "Jun 2017", "Jul 2017"]
c = b.minmax_by { |date| Date.strptime(date, date_fmt) }
#=> ["Apr 2016", "Jul 2017"]
fm, lm = c.map { |date| Date.strptime(date, date_fmt) }
#=> [#<Date: 2016-04-01 ((2457480j,0s,0n),+0s,2299161j)>,
# #<Date: 2017-07-01 ((2457936j,0s,0n),+0s,2299161j)>]
fm #=> #<Date: 2016-04-01 ((2457480j,0s,0n),+0s,2299161j)>
lm #=> #<Date: 2017-07-01 ((2457936j,0s,0n),+0s,2299161j)>]
d = 0..12*(lm.year-fm.year) + lm.month-fm.month
#=> 0..15
e = d.each_with_object({})
#=> #<Enumerator: 0..15:each_with_object({})>
We can see the values that will be generated by e
and passed to the block by converting it to an array, using Enumerable#entries (or Enumerable#to_a).
e.entries
#=> [[0, {}], [1, {}],..., [15, {}]]
The hashes in these tuples is initially empty, but will be filled in as block calculations are performed.
The first element is generated by e
, which is passed to the block and the block variables are set equal to its value, using a process called disambiguation or decomposition to stab out the part associated with each block variable.
_,h = e.next
#=> [0, {}]
h #=> {}
I've used _
as the first block variable to signify that it (the index) is not used in the block calculation. Continuing,
str = fm.strftime(date_fmt)
#=> "Apr 2016"
h[str] = dates.fetch(str, 0)
#=> 6
h #=> {"Apr 2016"=>6}
In this case dates
has a key "Apr 2016"
, so h["Apr 2016"]
is set equal to dates["Apr 2016"]
. In other cases dates
will not have key equal to str
("May 2016"
, for example), so the value will be set equal to fetch
's default value of 0
.
fm >>= 1
#=> #<Date: 2016-05-01 ((2457510j,0s,0n),+0s,2299161j)>
fm
is now May, 2016. The remaining calculations are similar.
Upvotes: 3
Reputation: 10507
Here's another way, using each_with_object
:
def add_months(dates)
min, max = dates.keys.map { |date| Date.parse(date) }.minmax
range = (min..max).map { |date| date.strftime("%b %Y") }.uniq
range.each_with_object({}) { |date, result| result[date] = dates[date] || 0 }
end
Output:
dates = {"Apr 2016"=>6, "Aug 2016"=>9, "Jan 2017"=>11, "Apr 2017"=>6, "May 2017"=>9, "Jun 2017"=>1, "Jul 2017"=>9}
add_months(dates)
#=> {
# "Apr 2016"=>6,
# "May 2016"=>0,
# "Jun 2016"=>0,
# "Jul 2016"=>0,
# "Aug 2016"=>9,
# "Sep 2016"=>0,
# "Oct 2016"=>0,
# "Nov 2016"=>0,
# "Dec 2016"=>0,
# "Jan 2017"=>11,
# "Feb 2017"=>0,
# "Mar 2017"=>0,
# "Apr 2017"=>6,
# "May 2017"=>9,
# "Jun 2017"=>1,
# "Jul 2017"=>9
# }
Upvotes: 4
Reputation: 3857
I would suggest a method like the following:
require 'date'
def add_missing_months(dates)
# get all months
months = dates.keys.map{|m| Date.parse(m)}
# get min and max
min = months.min
max = months.max
# collect all missing months
missing_months = {}
while min < max
min = min.next_month
missing_months[min.strftime('%b %Y')] = 0 unless months.include?(min)
end
# merge hashes
dates.merge(missing_months)
end
Upvotes: 0
Reputation: 1833
You can use the following method to do the above :
def normalize_months(month_values)
ordered_months = month_values.keys.map do |m| Date.parse(m) end.sort.map do |s| s.strftime('%b %Y') end
normalized = []
current = ordered_months.first
while current != ordered_months.last do
normalized << current
current = Date.parse(current).next_month.strftime('%b %Y')
end
result = {}
normalized.each do |g| result[g] = month_values[g].nil? ? 0 : month_values[g] end
result
end
Do a require 'date'
above this if you are doing this using pure ruby.
Upvotes: 1