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