aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/core_ext/date/calculations.rb
blob: f0f67765c66a0b92efa9d57a252c7e4576bd6cce (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
require 'date'
require 'active_support/duration'
require 'active_support/core_ext/object/acts_like'
require 'active_support/core_ext/date/zones'
require 'active_support/core_ext/time/zones'

class Date
  DAYS_INTO_WEEK = { :monday => 0, :tuesday => 1, :wednesday => 2, :thursday => 3, :friday => 4, :saturday => 5, :sunday => 6 }

  if RUBY_VERSION < '1.9'
    undef :>>

    # Backported from 1.9. The one in 1.8 leads to incorrect next_month and
    # friends for dates where the calendar reform is involved. It additionally
    # prevents an infinite loop fixed in r27013.
    def >>(n)
      y, m = (year * 12 + (mon - 1) + n).divmod(12)
      m,   = (m + 1)                    .divmod(1)
      d = mday
      until jd2 = self.class.valid_civil?(y, m, d, start)
        d -= 1
        raise ArgumentError, 'invalid date' unless d > 0
      end
      self + (jd2 - jd)
    end
  end

  class << self
    # Returns a new Date representing the date 1 day ago (i.e. yesterday's date).
    def yesterday
      ::Date.current.yesterday
    end

    # Returns a new Date representing the date 1 day after today (i.e. tomorrow's date).
    def tomorrow
      ::Date.current.tomorrow
    end

    # Returns Time.zone.today when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns Date.today.
    def current
      ::Time.zone ? ::Time.zone.today : ::Date.today
    end
  end

  # Returns true if the Date object's date lies in the past. Otherwise returns false.
  def past?
    self < ::Date.current
  end

  # Returns true if the Date object's date is today.
  def today?
    self.to_date == ::Date.current # we need the to_date because of DateTime
  end

  # Returns true if the Date object's date lies in the future.
  def future?
    self > ::Date.current
  end

  # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
  # and then subtracts the specified number of seconds.
  def ago(seconds)
    to_time_in_current_zone.since(-seconds)
  end

  # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
  # and then adds the specified number of seconds
  def since(seconds)
    to_time_in_current_zone.since(seconds)
  end
  alias :in :since

  # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
  def beginning_of_day
    to_time_in_current_zone
  end
  alias :midnight :beginning_of_day
  alias :at_midnight :beginning_of_day
  alias :at_beginning_of_day :beginning_of_day

  # Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59)
  def end_of_day
    to_time_in_current_zone.end_of_day
  end

  def plus_with_duration(other) #:nodoc:
    if ActiveSupport::Duration === other
      other.since(self)
    else
      plus_without_duration(other)
    end
  end
  alias_method :plus_without_duration, :+
  alias_method :+, :plus_with_duration

  def minus_with_duration(other) #:nodoc:
    if ActiveSupport::Duration === other
      plus_with_duration(-other)
    else
      minus_without_duration(other)
    end
  end
  alias_method :minus_without_duration, :-
  alias_method :-, :minus_with_duration

  # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
  # any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
  def advance(options)
    options = options.dup
    d = self
    d = d >> options.delete(:years) * 12 if options[:years]
    d = d >> options.delete(:months)     if options[:months]
    d = d +  options.delete(:weeks) * 7  if options[:weeks]
    d = d +  options.delete(:days)       if options[:days]
    d
  end

  # Returns a new Date where one or more of the elements have been changed according to the +options+ parameter.
  #
  # Examples:
  #
  #   Date.new(2007, 5, 12).change(:day => 1)                  # => Date.new(2007, 5, 1)
  #   Date.new(2007, 5, 12).change(:year => 2005, :month => 1) # => Date.new(2005, 1, 12)
  def change(options)
    ::Date.new(
      options[:year]  || self.year,
      options[:month] || self.month,
      options[:day]   || self.day
    )
  end

  # Returns a new Date/DateTime representing the time a number of specified weeks ago.
  def weeks_ago(weeks)
    advance(:weeks => -weeks)
  end

  # Returns a new Date/DateTime representing the time a number of specified months ago.
  def months_ago(months)
    advance(:months => -months)
  end

  # Returns a new Date/DateTime representing the time a number of specified months in the future.
  def months_since(months)
    advance(:months => months)
  end

  # Returns a new Date/DateTime representing the time a number of specified years ago.
  def years_ago(years)
    advance(:years => -years)
  end

  # Returns a new Date/DateTime representing the time a number of specified years in the future.
  def years_since(years)
    advance(:years => years)
  end

  # Shorthand for years_ago(1)
  def prev_year
    years_ago(1)
  end unless method_defined?(:prev_year)

  # Shorthand for years_since(1)
  def next_year
    years_since(1)
  end unless method_defined?(:next_year)

  # Shorthand for months_ago(1)
  def prev_month
    months_ago(1)
  end unless method_defined?(:prev_month)

  # Shorthand for months_since(1)
  def next_month
    months_since(1)
  end unless method_defined?(:next_month)

  # Returns number of days to start of this week. Week is assumed to start on
  # +start_day+, default is +:monday+.
  def days_to_week_start(start_day = :monday)
    start_day_number = DAYS_INTO_WEEK[start_day]
    current_day_number = wday != 0 ? wday - 1 : 6
    (current_day_number - start_day_number) % 7
  end

  # Returns a new +Date+/+DateTime+ representing the start of this week. Week is
  # assumed to start on +start_day+, default is +:monday+. +DateTime+ objects
  # have their time set to 0:00.
  def beginning_of_week(start_day = :monday)
    days_to_start = days_to_week_start(start_day)
    result = self - days_to_start
    acts_like?(:time) ? result.midnight : result
  end
  alias :at_beginning_of_week :beginning_of_week

  # Returns a new +Date+/+DateTime+ representing the start of this week. Week is
  # assumed to start on a Monday. +DateTime+ objects have their time set to 0:00.
  def monday
    beginning_of_week
  end

  # Returns a new +Date+/+DateTime+ representing the end of this week. Week is
  # assumed to start on +start_day+, default is +:monday+. +DateTime+ objects
  # have their time set to 23:59:59.
  def end_of_week(start_day = :monday)
    days_to_end = 6 - days_to_week_start(start_day)
    result = self + days_to_end.days
    self.acts_like?(:time) ? result.end_of_day : result
  end
  alias :at_end_of_week :end_of_week

  # Returns a new +Date+/+DateTime+ representing the end of this week. Week is
  # assumed to start on a Monday. +DateTime+ objects have their time set to 23:59:59.
  def sunday
    end_of_week
  end

  # Returns a new +Date+/+DateTime+ representing the given +day+ in the previous
  # week. Default is +:monday+. +DateTime+ objects have their time set to 0:00.
  def prev_week(day = :monday)
    result = (self - 7).beginning_of_week + DAYS_INTO_WEEK[day]
    self.acts_like?(:time) ? result.change(:hour => 0) : result
  end

  # Returns a new Date/DateTime representing the start of the given day in next week (default is :monday).
  def next_week(day = :monday)
    result = (self + 7).beginning_of_week + DAYS_INTO_WEEK[day]
    self.acts_like?(:time) ? result.change(:hour => 0) : result
  end

  # Returns a new ; DateTime objects will have time set to 0:00DateTime representing the start of the month (1st of the month; DateTime objects will have time set to 0:00)
  def beginning_of_month
    self.acts_like?(:time) ? change(:day => 1, :hour => 0) : change(:day => 1)
  end
  alias :at_beginning_of_month :beginning_of_month

  # Returns a new Date/DateTime representing the end of the month (last day of the month; DateTime objects will have time set to 0:00)
  def end_of_month
    last_day = ::Time.days_in_month( self.month, self.year )
    self.acts_like?(:time) ? change(:day => last_day, :hour => 23, :min => 59, :sec => 59) : change(:day => last_day)
  end
  alias :at_end_of_month :end_of_month

  # Returns a new Date/DateTime representing the start of the quarter (1st of january, april, july, october; DateTime objects will have time set to 0:00)
  def beginning_of_quarter
    beginning_of_month.change(:month => [10, 7, 4, 1].detect { |m| m <= self.month })
  end
  alias :at_beginning_of_quarter :beginning_of_quarter

  # Returns a new Date/DateTime representing the end of the quarter (last day of march, june, september, december; DateTime objects will have time set to 23:59:59)
  def end_of_quarter
    beginning_of_month.change(:month => [3, 6, 9, 12].detect { |m| m >= self.month }).end_of_month
  end
  alias :at_end_of_quarter :end_of_quarter

  # Returns a new Date/DateTime representing the start of the year (1st of january; DateTime objects will have time set to 0:00)
  def beginning_of_year
    self.acts_like?(:time) ? change(:month => 1, :day => 1, :hour => 0) : change(:month => 1, :day => 1)
  end
  alias :at_beginning_of_year :beginning_of_year

  # Returns a new Time representing the end of the year (31st of december; DateTime objects will have time set to 23:59:59)
  def end_of_year
    self.acts_like?(:time) ? change(:month => 12, :day => 31, :hour => 23, :min => 59, :sec => 59) : change(:month => 12, :day => 31)
  end
  alias :at_end_of_year :end_of_year

  # Convenience method which returns a new Date/DateTime representing the time 1 day ago
  def yesterday
    self - 1
  end

  # Convenience method which returns a new Date/DateTime representing the time 1 day since the instance time
  def tomorrow
    self + 1
  end
end