Лучший способ подсчета дней в периоде с разбивкой по месяцам

#ruby-on-rails #ruby

#ruby-on-rails #ruby

Вопрос:

Нужен пример, как рассчитать количество дней в периоде, разбитом по месяцам. Например:

 Wed, 25 Nov 2020 : Tue, 15 Dec 2020 => [6 (nov), 15(dec)]
  

Спасибо!

Комментарии:

1. Извините, но мне это непонятно

2. У нас есть две даты, и нам нужно найти количество дней между этими двумя днями для каждого месяца.

Ответ №1:

Это было бы задачей tally_by , но это не добавлено в Ruby (пока?). подсчет тоже работает:

 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}
  

Комментарии:

1. Похоже, я могу заменить .tally (я использую ruby 2.6.3) на .inject(Hash.new(0)) {|hash, item| hash[item] = 1; hash}

2. @prosto.vint поскольку ваш исходный объект становится результатом, вы могли бы использовать each_with_object(Hash.new(0)) { |item, hash| hash[item] = 1 }

3. range.group_by{|d| Date::ABBR_MONTHNAMES[d.month] }.transform_values(amp;:size) другой способ.

Ответ №2:

 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]] 
  

Как насчет того, что интервал настолько длинный, что у вас одинаковые месяцы, но разных лет? Я добавил годы из-за этого случая.

Это работает и в чистом ruby, вам просто нужно require 'date'

Ответ №3:

Код

 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
  

Смотрите Диапазон ::новый, Дата::strptime и Перечисляемый#срез_когда.

Примеры

 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} 
  

Объяснение

Для первого примера шаги следующие.

 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> 
  

Мы можем видеть элементы, сгенерированные этим счетчиком, преобразовав его в массив.

 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)>]] 
  

Продолжение,

 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}
  

Первый элемент, сгенерированный f и переданный в блок, и переменные блока присваивают значения по правилам декомпозиции массива:

 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 #=> {}
  

Пары ключ-значение будут добавляться в h течение вычислений. Смотрите Enumerator#next . Теперь выполняется вычисление блока.

 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 
  

в результате:

 h #=> {[2020, "Nov"]=>6}
  

Остальные шаги аналогичны.

Ответ №4:

Я бы начал с разбиения всего периода на периоды для каждого месяца. Поскольку в Ruby есть диапазоны, я бы написал вспомогательный метод, который принимает диапазон дат и выдает диапазоны месяцев:

 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 гарантирует, что границы диапазона учитываются при расчете диапазона каждого месяца. Для версии Ruby до 2.7 вы должны передавать границы отдельно:

 from = date.clamp(range.begin, range.end)
to   = (date.end_of_month).clamp(range.begin, range.end)
  

Пример использования:

 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
#   ]
  

Теперь все, что нам нужно, это способ подсчета дней в каждом месячном диапазоне: (например, через jd )

 def days(range)
  range.end.jd - range.begin.jd   1
end
  

и некоторое форматирование:

 each_month(from..to).map { |r| format('%d (%s)', days(r), r.begin.strftime('%b')) }
#=> ["6 (Nov)", "31 (Dec)", "13 (Jan)"]