aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/core_ext/date/calculations.rb
blob: de354540e91451641c060318b3a8bf0790291ba7 (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
module ActiveSupport #:nodoc:
  module CoreExtensions #:nodoc:
    module Date #:nodoc:
      # Enables the use of time calculations within Time itself
      module Calculations
        def self.included(base) #:nodoc:
          base.send(:include, ClassMethods)
          
          base.send(:alias_method, :plus_without_duration, :+)
          base.send(:alias_method, :+, :plus_with_duration)
          
          base.send(:alias_method, :minus_without_duration, :-)
          base.send(:alias_method, :-, :minus_with_duration)
        end

        module ClassMethods
          def plus_with_duration(other) #:nodoc:
            if ActiveSupport::Duration === other
              other.since(self)
            else
              plus_without_duration(other)
            end
          end
          
          def minus_with_duration(other) #:nodoc:
            if ActiveSupport::Duration === other
              plus_with_duration(-other)
            else
              minus_without_duration(other)
            end
          end
          
          # Provides precise Date calculations for years, months, and days.  The +options+ parameter takes a hash with 
          # any of these keys: :months, :days, :years.
          def advance(options)
            d = ::Date.new(year + (options.delete(:years) || 0), month, day)
            d = d >> options.delete(:months) if options[:months]
            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, 12)
          #   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]   || options[:mday] || self.day # mday is deprecated
            )
          end
          
          # Returns a new Date/DateTime representing the time a number of specified months ago
          def months_ago(months)
            months_since(-months)
          end

          def months_since(months)
            year, month, day = self.year, self.month, self.day

            month += months

            # in case months is negative
            while month < 1
             month += 12
             year -= 1
            end

            # in case months is positive
            while month > 12
             month -= 12
             year += 1
            end

            max = ::Time.days_in_month(month, year)
            day = max if day > max

            change(:year => year, :month => month, :day => day)
          end

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

          def years_since(years)
            change(:year => self.year + years)
          end

          # Short-hand for years_ago(1)
          def last_year
            years_ago(1)
          end

          # Short-hand for years_since(1)
          def next_year
            years_since(1)
          end

          # Short-hand for months_ago(1)
          def last_month
            months_ago(1)
          end

          # Short-hand for months_since(1)
          def next_month
            months_since(1)
          end

          # Returns a new Date/DateTime representing the "start" of this week (i.e, Monday; DateTime objects will have time set to 0:00)
          def beginning_of_week
            days_to_monday = self.wday!=0 ? self.wday-1 : 6
            result = self - days_to_monday
            self.acts_like?(:time) ? result.midnight : result
          end
          alias :monday :beginning_of_week
          alias :at_beginning_of_week :beginning_of_week

          # Returns a new Date/DateTime representing the start of the given day in next week (default is Monday).
          def next_week(day = :monday)
            days_into_week = { :monday => 0, :tuesday => 1, :wednesday => 2, :thursday => 3, :friday => 4, :saturday => 5, :sunday => 6}
            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, :min => 0, :sec => 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 => 0, :min => 0, :sec => 0) : 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 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, :min => 0, :sec => 0) : change(:month => 1, :day => 1)
          end
          alias :at_beginning_of_year :beginning_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
      end
    end
  end
end