Reputation: 1505
Need to example how to calculate the count of days in a period splited by month. For example:
Wed, 25 Nov 2020 : Tue, 15 Dec 2020 => [6 (nov), 15(dec)]
Thank you!
Upvotes: 1
Views: 180
Reputation: 114188
I would start by breaking the whole period into periods for each month. Since Ruby has ranges, I'd write a helper method that takes a date range and yields month-ranges:
def each_month(range)
return enum_for(__method__, range) unless block_given?
date = range.begin.beginning_of_month
loop do
from = date.clamp(range)
to = (date.end_of_month).clamp(range)
yield from..to
date = date.next_month
break unless range.cover?(date)
end
end
clamp
ensures that the range's bounds are taken into account when calculating each month's range. For Ruby version prior to 2.7 you have to pass the bounds separately:
from = date.clamp(range.begin, range.end)
to = (date.end_of_month).clamp(range.begin, range.end)
Example usage:
from = '25 Nov 2020'.to_date
to = '13 Jan 2021'.to_date
each_month(from..to).to_a
#=> [
# Wed, 25 Nov 2020..Mon, 30 Nov 2020
# Tue, 01 Dec 2020..Thu, 31 Dec 2020
# Fri, 01 Jan 2021..Wed, 13 Jan 2021
# ]
Now all we need is a way to count the days in each month-range: (e.g. via jd
)
def days(range)
range.end.jd - range.begin.jd + 1
end
and some formatting:
each_month(from..to).map { |r| format('%d (%s)', days(r), r.begin.strftime('%b')) }
#=> ["6 (Nov)", "31 (Dec)", "13 (Jan)"]
Upvotes: 0
Reputation: 110685
Code
require 'date'
def count_days_by_month(str)
Range.new(*str.split(/ +: +/).
map { |s| Date.strptime(s, '%a, %d %b %Y') }).
slice_when { |d1,d2| d1.month != d2.month }.
with_object({}) do |a,h|
day1 = a.first
h[[day1.year, Date::ABBR_MONTHNAMES[day1.month]]] = a.size
end
end
See Range::new, Date::strptime and Enumerable#slice_when.
Examples
count_days_by_month "Wed, 25 Nov 2020 : Tue, 15 Dec 2020"
#=> {[2020, "Nov"]=>6, [2020, "Dec"]=>15}
count_days_by_month "Wed, 25 Nov 2020 : Tue, 15 Dec 2021"
#=> {[2020, "Nov"]=>6, [2020, "Dec"]=>31, [2021, "Jan"]=>31,
# ...
# [2021, "Nov"]=>30, [2021, "Dec"]=>15}
Explanation
For the first example the steps are as follows.
str = "Wed, 25 Nov 2020 : Tue, 15 Dec 2020"
b = str.split(/ +: +/)
#=> ["Wed, 25 Nov 2020", "Tue, 15 Dec 2020"]
c = b.map { |s| Date.strptime(s, '%a, %d %b %Y') }
#=> [#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-12-15 ((2459199j,0s,0n),+0s,2299161j)>]
d = Range.new(*c)
#=> #<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>..
# #<Date: 2020-12-15 ((2459199j,0s,0n),+0s,2299161j)>
e = d.slice_when { |d1,d2| d1.month != d2.month }
#=> #<Enumerator: #<Enumerator::Generator:0x00007fb1058abb10>:each>
We can see the elements generated by this enumerator by converting it to an array.
e.to_a
#=> [[#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-11-26 ((2459180j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-11-30 ((2459184j,0s,0n),+0s,2299161j)>],
# [#<Date: 2020-12-01 ((2459185j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-12-02 ((2459186j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-12-15 ((2459199j,0s,0n),+0s,2299161j)>]]
Continuing,
f = e.with_object({})
#=> #<Enumerator: #<Enumerator: #<Enumerator::Generator:0x00007fb1058abb10>
# :each>:with_object({})>
f.each do |a,h|
day1 = a.first
h[[day1.year, Date::ABBR_MONTHNAMES[day1.month]]] = a.size
end
#=> {[2020, "Nov"]=>6, [2020, "Dec"]=>15}
The first element generated by f
and passed to the block, and the block variables are assign values by the rules of array decomposition:
a,h = f.next
#=> [[#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-11-26 ((2459180j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-11-30 ((2459184j,0s,0n),+0s,2299161j)>],
# {}]
a #=> [#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-11-26 ((2459180j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-11-30 ((2459184j,0s,0n),+0s,2299161j)>],
h #=> {}
Key-value pairs will be added to h
over the course of the calculations. See Enumerator#next. The block calculation is now performed.
day1 = a.first
#=> #<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>
g = day1.year
#=> 2020
i = day1.month
#=> 11
j = Date::ABBR_MONTHNAMES[day1.month]
#=> "Nov"
k = a.size
#=> 6
h[[g,j]] = k
#=> 6
resulting in:
h #=> {[2020, "Nov"]=>6}
The remaining steps are similar.
Upvotes: 0
Reputation: 80065
This would be a job for tally_by
, but that is not added to Ruby (yet?).
tally works too:
require 'date'
range = Date.parse("Wed, 25 Nov 2020") .. Date.parse("Tue, 15 Dec 2020")
p month_counts = range.map{|d| Date::ABBR_MONTHNAMES[d.month] }.tally
# => {"Nov"=>6, "Dec"=>15}
Upvotes: 4
Reputation: 30056
date1 = Date.new(2020, 11, 25)
date2 = Date.new(2020, 12, 15)
(date1..date2).group_by { |date| [date.year, date.month] }
.map { |(year, month), dates| ["#{year}/#{month}", dates.length] }
=> [["2020/11", 6], ["2020/12", 15]]
What about the interval is so long that you have same months but of different years? I've added years because of this case.
This works in pure ruby too, you just need require 'date'
Upvotes: 2