prosto.vint
prosto.vint

Reputation: 1505

Best way to count days in period spitted by month

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

Answers (4)

Stefan
Stefan

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

Cary Swoveland
Cary Swoveland

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

steenslag
steenslag

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

Ursus
Ursus

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

Related Questions