Class: ActiveDateRange::DateRange
- Inherits:
-
Range
- Object
- Range
- ActiveDateRange::DateRange
- Defined in:
- lib/active_date_range/date_range.rb
Overview
Provides a DateRange with parsing, calculations and query methods
Constant Summary collapse
- SHORTHANDS =
{ this_month: -> { DateRange.new(Time.zone.today.all_month) }, prev_month: -> { DateRange.new(1.month.ago.to_date.all_month) }, next_month: -> { DateRange.new(1.month.from_now.to_date.all_month) }, this_quarter: -> { DateRange.new(Time.zone.today.all_quarter) }, prev_quarter: -> { DateRange.new(3.months.ago.to_date.all_quarter) }, next_quarter: -> { DateRange.new(3.months.from_now.to_date.all_quarter) }, this_year: -> { DateRange.new(Time.zone.today.all_year) }, prev_year: -> { DateRange.new(12.months.ago.to_date.all_year) }, next_year: -> { DateRange.new(12.months.from_now.to_date.all_year) }, this_week: -> { DateRange.new(Time.zone.today.all_week) }, prev_week: -> { DateRange.new(1.week.ago.to_date.all_week) }, next_week: -> { DateRange.new(1.week.from_now.to_date.all_week) } }.freeze
- RANGE_PART_REGEXP =
%r{\A(?<year>((1\d|2\d)\d\d))-?(?<month>0[1-9]|1[012])-?(?<day>[0-2]\d|3[01])?\z}
Class Method Summary collapse
- .from_date_and_duration(date, duration) ⇒ Object
-
.month(year, month) ⇒ Object
Creates a DateRange for a specific month.
-
.parse(input) ⇒ Object
Parses a date range string to a
DateRangeinstance. -
.quarter(year, quarter) ⇒ Object
Creates a DateRange for a specific quarter (1-4).
-
.week(year, week) ⇒ Object
Creates a DateRange for a specific ISO week.
-
.year(year) ⇒ Object
Creates a DateRange for a full year.
Instance Method Summary collapse
-
#+(other) ⇒ Object
Adds two date ranges together.
-
#<=>(other) ⇒ Object
Sorts two date ranges by the begin date.
-
#after?(date) ⇒ Boolean
Returns true when the date range is after the given date.
-
#before?(date) ⇒ Boolean
Returns true when the date range is before the given date.
-
#begin_at_beginning_of_month? ⇒ Boolean
Returns true when begin of the range is at the beginning of the month.
-
#begin_at_beginning_of_quarter? ⇒ Boolean
Returns true when begin of the range is at the beginning of the quarter.
-
#begin_at_beginning_of_week? ⇒ Boolean
Returns true when begin of the range is at the beginning of the week.
-
#begin_at_beginning_of_year? ⇒ Boolean
Returns true when begin of the range is at the beginning of the year.
- #boundless? ⇒ Boolean
-
#cap_begin(duration) ⇒ Object
Returns a new DateRange with the begin date capped to the given duration from the end date.
-
#cap_end(duration) ⇒ Object
Returns a new DateRange with the end date capped to the given duration from the begin date.
- #current? ⇒ Boolean (also: #this_month?, #this_quarter?, #this_year?)
-
#days ⇒ Object
Returns the number of days in the range.
- #exceeds?(limit) ⇒ Boolean
-
#full_month? ⇒ Boolean
(also: #full_months?)
Returns true when the range is exactly one or more months long.
-
#full_quarter? ⇒ Boolean
(also: #full_quarters?)
Returns true when the range is exactly one or more quarters long.
-
#full_week? ⇒ Boolean
(also: #full_weeks?)
Returns true when the range is exactly one or more weeks long.
-
#full_year? ⇒ Boolean
(also: #full_years?)
Returns true when the range is exactly one or more years long.
-
#granularity ⇒ Object
Returns the granularity of the range.
-
#humanize(format: :short) ⇒ Object
Returns a human readable format for the date range.
-
#in_groups_of(granularity, amount: 1) ⇒ Object
Returns an array with date ranges containing full months/quarters/years in the current range.
- #include?(other) ⇒ Boolean
-
#initialize(begin_date, end_date = nil) ⇒ DateRange
constructor
Initializes a new DateRange.
-
#intersection(other) ⇒ Object
Returns the intersection of the current and the other date range.
-
#months ⇒ Object
Returns the number of months in the range or nil when range is no full month.
-
#next(periods = 1) ⇒ Object
Returns the period next to the current period.
-
#one_month? ⇒ Boolean
Returns true when the range is exactly one month long.
-
#one_quarter? ⇒ Boolean
Returns true when the range is exactly one quarter long.
- #one_week? ⇒ Boolean
-
#one_year? ⇒ Boolean
Returns true when the range is exactly one year long.
-
#previous(periods = 1) ⇒ Object
Returns the period previous to the current period.
-
#quarters ⇒ Object
Returns the number of quarters in the range or nil when range is no full quarter.
-
#relative_param ⇒ Object
Returns a string representation of the date range relative to today.
-
#same_year? ⇒ Boolean
Returns true when begin and end are in the same year.
-
#size ⇒ Object
(also: #length)
Returns the duration of the range as an ActiveSupport::Duration, compatible with validates_length_of.
- #stretch_to_end_of_month ⇒ Object
-
#to_datetime_range ⇒ Object
Returns a Range with begin and end as DateTime instances.
-
#to_param(relative: true) ⇒ Object
Returns a param representation of the date range.
- #to_s ⇒ Object
-
#weeks ⇒ Object
Returns the number of weeks on the range or nil when range is no full week.
-
#years ⇒ Object
Returns the number of years on the range or nil when range is no full year.
Constructor Details
#initialize(begin_date, end_date = nil) ⇒ DateRange
Initializes a new DateRange. Accepts both a begin and end date or a range of dates. Make sures the begin date is before the end date.
101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/active_date_range/date_range.rb', line 101 def initialize(begin_date, end_date = nil) begin_date, end_date = begin_date.begin, begin_date.end if begin_date.kind_of?(Range) begin_date, end_date = begin_date.first, begin_date.last if begin_date.kind_of?(Array) begin_date = begin_date.to_date if begin_date.kind_of?(Time) end_date = end_date.to_date if end_date.kind_of?(Time) raise InvalidDateRange, "Date range invalid, begin should be a date" if begin_date && !begin_date.kind_of?(Date) raise InvalidDateRange, "Date range invalid, end should be a date" if end_date && !end_date.kind_of?(Date) raise InvalidDateRange, "Date range invalid, begin #{begin_date} is after end #{end_date}" if begin_date && end_date && begin_date > end_date super(begin_date, end_date) end |
Class Method Details
.from_date_and_duration(date, duration) ⇒ Object
62 63 64 65 |
# File 'lib/active_date_range/date_range.rb', line 62 def self.from_date_and_duration(date, duration) duration = 1.send(duration) if duration.kind_of?(Symbol) new(date, date + duration - 1.day) end |
.month(year, month) ⇒ Object
Creates a DateRange for a specific month.
DateRange.month(2026, 1) # => DateRange(2026-01-01..2026-01-31)
77 78 79 |
# File 'lib/active_date_range/date_range.rb', line 77 def self.month(year, month) new(Date.new(year, month, 1).all_month) end |
.parse(input) ⇒ Object
Parses a date range string to a DateRange instance. Valid formats are:
-
A relative shorthand:
this_month,prev_month,next_month, etc. -
A begin and end date:
YYYYMMDD..YYYYMMDD -
A begin and end month:
YYYYMM..YYYYMM
33 34 35 36 37 38 39 40 41 42 |
# File 'lib/active_date_range/date_range.rb', line 33 def self.parse(input) return nil if input.nil? return DateRange.new(input) if input.kind_of?(Range) return SHORTHANDS[input.to_sym].call if SHORTHANDS.key?(input.to_sym) begin_date, end_date = input.split("..") raise InvalidDateRangeFormat, "#{input} doesn't have a begin..end format" if begin_date.blank? && end_date.blank? DateRange.new(parse_date(begin_date), parse_date(end_date, last: true)) end |
.quarter(year, quarter) ⇒ Object
Creates a DateRange for a specific quarter (1-4).
DateRange.quarter(2026, 4) # => DateRange(2026-10-01..2026-12-31)
84 85 86 87 |
# File 'lib/active_date_range/date_range.rb', line 84 def self.quarter(year, quarter) month = ((quarter - 1) * 3) + 1 new(Date.new(year, month, 1).all_quarter) end |
Instance Method Details
#+(other) ⇒ Object
Adds two date ranges together. Fails when the ranges are not subsequent.
115 116 117 118 119 |
# File 'lib/active_date_range/date_range.rb', line 115 def +(other) raise InvalidAddition if self.end != (other.begin - 1.day) DateRange.new(self.begin, other.end) end |
#<=>(other) ⇒ Object
Sorts two date ranges by the begin date.
122 123 124 |
# File 'lib/active_date_range/date_range.rb', line 122 def <=>(other) self.begin <=> other.begin end |
#after?(date) ⇒ Boolean
Returns true when the date range is after the given date. Accepts both a Date and DateRange as input.
304 305 306 307 |
# File 'lib/active_date_range/date_range.rb', line 304 def after?(date) date = date.end if date.kind_of?(DateRange) self.begin.present? && self.begin.after?(date) end |
#before?(date) ⇒ Boolean
Returns true when the date range is before the given date. Accepts both a Date and DateRange as input.
297 298 299 300 |
# File 'lib/active_date_range/date_range.rb', line 297 def before?(date) date = date.begin if date.kind_of?(DateRange) self.end.present? && self.end.before?(date) end |
#begin_at_beginning_of_month? ⇒ Boolean
Returns true when begin of the range is at the beginning of the month
180 181 182 183 184 |
# File 'lib/active_date_range/date_range.rb', line 180 def begin_at_beginning_of_month? memoize(:@begin_at_beginning_of_month) do self.begin.present? && self.begin.day == 1 end end |
#begin_at_beginning_of_quarter? ⇒ Boolean
Returns true when begin of the range is at the beginning of the quarter
187 188 189 190 191 |
# File 'lib/active_date_range/date_range.rb', line 187 def begin_at_beginning_of_quarter? memoize(:@begin_at_beginning_of_quarter) do self.begin.present? && begin_at_beginning_of_month? && [1, 4, 7, 10].include?(self.begin.month) end end |
#begin_at_beginning_of_week? ⇒ Boolean
Returns true when begin of the range is at the beginning of the week
201 202 203 204 205 |
# File 'lib/active_date_range/date_range.rb', line 201 def begin_at_beginning_of_week? memoize(:@begin_at_beginning_of_week) do self.begin.present? && self.begin == self.begin.at_beginning_of_week end end |
#begin_at_beginning_of_year? ⇒ Boolean
Returns true when begin of the range is at the beginning of the year
194 195 196 197 198 |
# File 'lib/active_date_range/date_range.rb', line 194 def begin_at_beginning_of_year? memoize(:@begin_at_beginning_of_year) do self.begin.present? && begin_at_beginning_of_month? && self.begin.month == 1 end end |
#boundless? ⇒ Boolean
126 127 128 |
# File 'lib/active_date_range/date_range.rb', line 126 def boundless? self.begin.nil? || self.end.nil? end |
#cap_begin(duration) ⇒ Object
470 471 472 473 474 |
# File 'lib/active_date_range/date_range.rb', line 470 def cap_begin(duration) return self if boundless? || !exceeds?(duration) DateRange.new(self.end - duration + 1.day, self.end) end |
#cap_end(duration) ⇒ Object
461 462 463 464 465 |
# File 'lib/active_date_range/date_range.rb', line 461 def cap_end(duration) return self if boundless? || !exceeds?(duration) DateRange.new(self.begin, self.begin + duration - 1.day) end |
#current? ⇒ Boolean Also known as: this_month?, this_quarter?, this_year?
285 286 287 288 289 |
# File 'lib/active_date_range/date_range.rb', line 285 def current? memoize(:@current) do cover?(Time.zone.today) end end |
#days ⇒ Object
Returns the number of days in the range
131 132 133 134 135 |
# File 'lib/active_date_range/date_range.rb', line 131 def days return if boundless? @days ||= (self.end - self.begin).to_i + 1 end |
#exceeds?(limit) ⇒ Boolean
454 455 456 |
# File 'lib/active_date_range/date_range.rb', line 454 def exceeds?(limit) self.days > limit.in_days.ceil end |
#full_month? ⇒ Boolean Also known as: full_months?
Returns true when the range is exactly one or more months long
243 244 245 246 247 |
# File 'lib/active_date_range/date_range.rb', line 243 def full_month? memoize(:@full_month) do begin_at_beginning_of_month? && self.end.present? && self.end == self.end.at_end_of_month end end |
#full_quarter? ⇒ Boolean Also known as: full_quarters?
Returns true when the range is exactly one or more quarters long
252 253 254 255 256 |
# File 'lib/active_date_range/date_range.rb', line 252 def full_quarter? memoize(:@full_quarter) do begin_at_beginning_of_quarter? && self.end.present? && self.end == self.end.at_end_of_quarter end end |
#full_week? ⇒ Boolean Also known as: full_weeks?
Returns true when the range is exactly one or more weeks long
270 271 272 273 274 |
# File 'lib/active_date_range/date_range.rb', line 270 def full_week? memoize(:@full_week) do begin_at_beginning_of_week? && self.end.present? && self.end == self.end.at_end_of_week end end |
#full_year? ⇒ Boolean Also known as: full_years?
Returns true when the range is exactly one or more years long
261 262 263 264 265 |
# File 'lib/active_date_range/date_range.rb', line 261 def full_year? memoize(:@full_year) do begin_at_beginning_of_year? && self.end.present? && self.end == self.end.at_end_of_year end end |
#granularity ⇒ Object
315 316 317 318 319 320 321 322 323 324 325 326 327 |
# File 'lib/active_date_range/date_range.rb', line 315 def granularity memoize(:@granularity) do if one_year? :year elsif one_quarter? :quarter elsif one_month? :month elsif one_week? :week end end end |
#humanize(format: :short) ⇒ Object
Returns a human readable format for the date range. See DateRange::Humanizer for options.
432 433 434 |
# File 'lib/active_date_range/date_range.rb', line 432 def humanize(format: :short) Humanizer.new(self, format: format).humanize end |
#in_groups_of(granularity, amount: 1) ⇒ Object
Returns an array with date ranges containing full months/quarters/years in the current range. Comes in handy when you need to have columns by month for a given range: ‘DateRange.this_year.in_groups_of(:months)`
Always returns full months/quarters/years, from the first to the last day of the period. The first and last item in the array can have a partial month/quarter/year, depending on the date range.
DateRange.parse("202101..202103").in_groups_of(:month) # => [DateRange.parse("202001..202001"), DateRange.parse("202002..202002"), DateRange.parse("202003..202003")]
DateRange.parse("202101..202106").in_groups_of(:month, amount: 2) # => [DateRange.parse("202001..202002"), DateRange.parse("202003..202004"), DateRange.parse("202005..202006")]
421 422 423 424 425 426 427 428 429 |
# File 'lib/active_date_range/date_range.rb', line 421 def in_groups_of(granularity, amount: 1) raise BoundlessRangeError, "Can't group date range without a begin." if self.begin.nil? if boundless? grouped_collection(granularity, amount: amount) else grouped_collection(granularity, amount: amount).to_a end end |
#include?(other) ⇒ Boolean
442 443 444 |
# File 'lib/active_date_range/date_range.rb', line 442 def include?(other) cover?(other) end |
#intersection(other) ⇒ Object
Returns the intersection of the current and the other date range
437 438 439 440 |
# File 'lib/active_date_range/date_range.rb', line 437 def intersection(other) intersection = self.to_a.intersection(other.to_a).sort DateRange.new(intersection) if intersection.any? end |
#months ⇒ Object
Returns the number of months in the range or nil when range is no full month
152 153 154 155 156 |
# File 'lib/active_date_range/date_range.rb', line 152 def months return nil unless full_month? ((self.end.year - self.begin.year) * 12) + (self.end.month - self.begin.month + 1) end |
#next(periods = 1) ⇒ Object
396 397 398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/active_date_range/date_range.rb', line 396 def next(periods = 1) raise BoundlessRangeError, "Can't calculate next for boundless range" if boundless? end_date = if granularity self.end + periods.send(granularity) elsif full_month? in_groups_of(:month).last.next(periods * months).end else self.end + (periods * days).days end end_date = end_date.at_end_of_month if full_month? DateRange.new(self.end + 1.day, end_date) end |
#one_month? ⇒ Boolean
Returns true when the range is exactly one month long
208 209 210 211 212 213 214 |
# File 'lib/active_date_range/date_range.rb', line 208 def one_month? memoize(:@one_month) do (28..31).cover?(days) && begin_at_beginning_of_month? && self.end == self.begin.at_end_of_month end end |
#one_quarter? ⇒ Boolean
Returns true when the range is exactly one quarter long
217 218 219 220 221 222 223 |
# File 'lib/active_date_range/date_range.rb', line 217 def one_quarter? memoize(:@one_quarter) do (90..92).cover?(days) && begin_at_beginning_of_quarter? && self.end == self.begin.at_end_of_quarter end end |
#one_week? ⇒ Boolean
234 235 236 237 238 239 240 |
# File 'lib/active_date_range/date_range.rb', line 234 def one_week? memoize(:@one_week) do days == 7 && begin_at_beginning_of_week? && self.end == self.begin.at_end_of_week end end |
#one_year? ⇒ Boolean
Returns true when the range is exactly one year long
226 227 228 229 230 231 232 |
# File 'lib/active_date_range/date_range.rb', line 226 def one_year? memoize(:@one_year) do (365..366).cover?(days) && begin_at_beginning_of_year? && self.end == self.begin.at_end_of_year end end |
#previous(periods = 1) ⇒ Object
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 |
# File 'lib/active_date_range/date_range.rb', line 375 def previous(periods = 1) raise BoundlessRangeError, "Can't calculate previous for boundless range" if boundless? begin_date = if granularity self.begin - periods.send(granularity) elsif full_month? in_groups_of(:month).first.previous(periods * months).begin else (self.begin - (periods * days).days) end begin_date = begin_date.at_beginning_of_month if full_month? DateRange.new(begin_date, self.begin - 1.day) end |
#quarters ⇒ Object
Returns the number of quarters in the range or nil when range is no full quarter
159 160 161 162 163 |
# File 'lib/active_date_range/date_range.rb', line 159 def quarters return nil unless full_quarter? months / 3 end |
#relative_param ⇒ Object
Returns a string representation of the date range relative to today. For example a range of 2021-01-01..2021-12-31 will return this_year when the current date is somewhere in 2021.
332 333 334 335 336 337 338 339 340 |
# File 'lib/active_date_range/date_range.rb', line 332 def relative_param memoize(:@relative_param) do SHORTHANDS .select { |key, _| key.end_with?(granularity.to_s) } .find { |key, range| self == range.call } &.first &.to_s end end |
#same_year? ⇒ Boolean
Returns true when begin and end are in the same year
279 280 281 282 283 |
# File 'lib/active_date_range/date_range.rb', line 279 def same_year? memoize(:@same_year) do !boundless? && self.begin.year == self.end.year end end |
#size ⇒ Object Also known as: length
Returns the duration of the range as an ActiveSupport::Duration, compatible with validates_length_of. Use Duration values for the constraint:
validates_length_of :period, maximum: 10.years
validates_length_of :period, maximum: 6.months
validates_length_of :period, maximum: 30.days
143 144 145 146 147 |
# File 'lib/active_date_range/date_range.rb', line 143 def size return Float::INFINITY if boundless? days.days end |
#stretch_to_end_of_month ⇒ Object
446 447 448 449 450 451 452 |
# File 'lib/active_date_range/date_range.rb', line 446 def stretch_to_end_of_month return self if self.end.present? && self.end == self.end.at_end_of_month side_to_stretch = boundless? ? self.begin : self.end DateRange.new(self.begin, side_to_stretch.at_end_of_month) end |
#to_datetime_range ⇒ Object
Returns a Range with begin and end as DateTime instances.
362 363 364 |
# File 'lib/active_date_range/date_range.rb', line 362 def to_datetime_range Range.new(self.begin.to_datetime.at_beginning_of_day, self.end.to_datetime.at_end_of_day) end |
#to_param(relative: true) ⇒ Object
Returns a param representation of the date range. When relative is true, the relative_param is returned when available. This allows for easy bookmarking of URL’s that always return the current month/quarter/year for the end user.
When relative is false, a YYYYMMDD..YYYYMMDD or YYYYMM..YYYYMM format is returned. The output of to_param is compatible with the parse method.
DateRange.parse("202001..202001").to_param # => "202001..202001"
DateRange.parse("20200101..20200115").to_param # => "20200101..20200115"
DateRange.parse("202001..202001").to_param(relative: true) # => "this_month"
352 353 354 355 356 357 358 359 |
# File 'lib/active_date_range/date_range.rb', line 352 def to_param(relative: true) if relative && relative_param relative_param else format = full_month? ? "%Y%m" : "%Y%m%d" "#{self.begin&.strftime(format)}..#{self.end&.strftime(format)}" end end |
#to_s ⇒ Object
366 367 368 |
# File 'lib/active_date_range/date_range.rb', line 366 def to_s "#{self.begin.strftime('%Y%m%d')}..#{self.end.strftime('%Y%m%d')}" end |
#weeks ⇒ Object
Returns the number of weeks on the range or nil when range is no full week
173 174 175 176 177 |
# File 'lib/active_date_range/date_range.rb', line 173 def weeks return nil unless full_week? days / 7 end |
#years ⇒ Object
Returns the number of years on the range or nil when range is no full year
166 167 168 169 170 |
# File 'lib/active_date_range/date_range.rb', line 166 def years return nil unless full_year? months / 12 end |