calculations.rb 7.1 KB
Newer Older
1
# frozen_string_literal: true
2

3
require "date"
B
brainopia 已提交
4

5 6
class DateTime
  class << self
7 8 9
    # Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or
    # <tt>config.time_zone</tt> are set, otherwise returns
    # <tt>Time.now.to_datetime</tt>.
10
    def current
11
      ::Time.zone ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
12 13
    end
  end
14

15 16 17 18 19
  # Returns the number of seconds since 00:00:00.
  #
  #   DateTime.new(2012, 8, 29,  0,  0,  0).seconds_since_midnight # => 0
  #   DateTime.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296
  #   DateTime.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399
20 21 22
  def seconds_since_midnight
    sec + (min * 60) + (hour * 3600)
  end
23

24 25 26 27 28 29 30 31 32
  # Returns the number of seconds until 23:59:59.
  #
  #   DateTime.new(2012, 8, 29,  0,  0,  0).seconds_until_end_of_day # => 86399
  #   DateTime.new(2012, 8, 29, 12, 34, 56).seconds_until_end_of_day # => 41103
  #   DateTime.new(2012, 8, 29, 23, 59, 59).seconds_until_end_of_day # => 0
  def seconds_until_end_of_day
    end_of_day.to_i - to_i
  end

A
Andrew White 已提交
33 34 35 36 37 38 39
  # Returns the fraction of a second as a +Rational+
  #
  #   DateTime.new(2012, 8, 29, 0, 0, 0.5).subsec # => (1/2)
  def subsec
    sec_fraction
  end

40 41
  # Returns a new DateTime where one or more of the elements have been changed
  # according to the +options+ parameter. The time options (<tt>:hour</tt>,
42
  # <tt>:min</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is
43 44 45 46
  # passed, then minute and sec is set to 0. If the hour and minute is passed,
  # then sec is set to 0. The +options+ parameter takes a hash with any of these
  # keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>,
  # <tt>:min</tt>, <tt>:sec</tt>, <tt>:offset</tt>, <tt>:start</tt>.
47
  #
48 49 50
  #   DateTime.new(2012, 8, 29, 22, 35, 0).change(day: 1)              # => DateTime.new(2012, 8, 1, 22, 35, 0)
  #   DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1)  # => DateTime.new(1981, 8, 1, 22, 35, 0)
  #   DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => DateTime.new(1981, 8, 29, 0, 0, 0)
51
  def change(options)
52 53 54 55 56 57 58 59 60 61
    if new_nsec = options[:nsec]
      raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
      new_fraction = Rational(new_nsec, 1000000000)
    else
      new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
      new_fraction = Rational(new_usec, 1000000)
    end

    raise ArgumentError, "argument out of range" if new_fraction >= 1

62
    ::DateTime.civil(
A
Alexey Gaziev 已提交
63 64 65 66 67
      options.fetch(:year, year),
      options.fetch(:month, month),
      options.fetch(:day, day),
      options.fetch(:hour, hour),
      options.fetch(:min, options[:hour] ? 0 : min),
68
      options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_fraction,
A
Alexey Gaziev 已提交
69 70
      options.fetch(:offset, offset),
      options.fetch(:start, start)
71 72
    )
  end
73

74 75 76 77 78
  # Uses Date to provide precise Time 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>, <tt>:hours</tt>,
  # <tt>:minutes</tt>, <tt>:seconds</tt>.
  def advance(options)
79 80 81 82 83 84 85 86 87 88
    unless options[:weeks].nil?
      options[:weeks], partial_weeks = options[:weeks].divmod(1)
      options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
    end

    unless options[:days].nil?
      options[:days], partial_days = options[:days].divmod(1)
      options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
    end

89
    d = to_date.advance(options)
90
    datetime_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
A
Alexey Gaziev 已提交
91 92 93 94 95 96 97 98
    seconds_to_advance = \
      options.fetch(:seconds, 0) +
      options.fetch(:minutes, 0) * 60 +
      options.fetch(:hours, 0) * 3600

    if seconds_to_advance.zero?
      datetime_advanced_by_date
    else
99
      datetime_advanced_by_date.since(seconds_to_advance)
A
Alexey Gaziev 已提交
100
    end
101
  end
102

103
  # Returns a new DateTime representing the time a number of seconds ago.
104 105 106 107
  # Do not use this method in combination with x.months, use months_ago instead!
  def ago(seconds)
    since(-seconds)
  end
108

109 110 111
  # Returns a new DateTime representing the time a number of seconds since the
  # instance time. Do not use this method in combination with x.months, use
  # months_since instead!
112 113 114 115
  def since(seconds)
    self + Rational(seconds.round, 86400)
  end
  alias :in :since
116

117
  # Returns a new DateTime representing the start of the day (0:00).
118
  def beginning_of_day
119
    change(hour: 0)
120 121 122 123
  end
  alias :midnight :beginning_of_day
  alias :at_midnight :beginning_of_day
  alias :at_beginning_of_day :beginning_of_day
124

A
Anatoli Makarevich 已提交
125 126
  # Returns a new DateTime representing the middle of the day (12:00)
  def middle_of_day
127
    change(hour: 12)
A
Anatoli Makarevich 已提交
128 129 130 131 132 133 134
  end
  alias :midday :middle_of_day
  alias :noon :middle_of_day
  alias :at_midday :middle_of_day
  alias :at_noon :middle_of_day
  alias :at_middle_of_day :middle_of_day

135
  # Returns a new DateTime representing the end of the day (23:59:59).
136
  def end_of_day
137
    change(hour: 23, min: 59, sec: 59, usec: Rational(999999999, 1000))
138
  end
A
Andrew White 已提交
139
  alias :at_end_of_day :end_of_day
140

141
  # Returns a new DateTime representing the start of the hour (hh:00:00).
142
  def beginning_of_hour
143
    change(min: 0)
144 145 146
  end
  alias :at_beginning_of_hour :beginning_of_hour

147
  # Returns a new DateTime representing the end of the hour (hh:59:59).
148
  def end_of_hour
149
    change(min: 59, sec: 59, usec: Rational(999999999, 1000))
150
  end
A
Andrew White 已提交
151
  alias :at_end_of_hour :end_of_hour
152 153

  # Returns a new DateTime representing the start of the minute (hh:mm:00).
154
  def beginning_of_minute
155
    change(sec: 0)
156 157 158 159 160
  end
  alias :at_beginning_of_minute :beginning_of_minute

  # Returns a new DateTime representing the end of the minute (hh:mm:59).
  def end_of_minute
161
    change(sec: 59, usec: Rational(999999999, 1000))
162 163
  end
  alias :at_end_of_minute :end_of_minute
164

165
  # Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
166
  def localtime(utc_offset = nil)
167
    utc = new_offset(0)
168 169 170 171 172 173 174 175

    Time.utc(
      utc.year, utc.month, utc.day,
      utc.hour, utc.min, utc.sec + utc.sec_fraction
    ).getlocal(utc_offset)
  end
  alias_method :getlocal, :localtime

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
  # Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone.
  #
  #   DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24))     # => Mon, 21 Feb 2005 10:11:12 -0600
  #   DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 UTC
  def utc
    utc = new_offset(0)

    Time.utc(
      utc.year, utc.month, utc.day,
      utc.hour, utc.min, utc.sec + utc.sec_fraction
    )
  end
  alias_method :getgm, :utc
  alias_method :getutc, :utc
  alias_method :gmtime, :utc

192
  # Returns +true+ if <tt>offset == 0</tt>.
193 194 195
  def utc?
    offset == 0
  end
196

197
  # Returns the offset value in seconds.
198 199 200
  def utc_offset
    (offset * 86400).to_i
  end
201 202 203 204

  # Layers additional behavior on DateTime#<=> so that Time and
  # ActiveSupport::TimeWithZone instances can be compared with a DateTime.
  def <=>(other)
205
    if other.respond_to? :to_datetime
206
      super other.to_datetime rescue nil
207
    else
208
      super
209
    end
210
  end
211 212
  alias_method :before?, :<
  alias_method :after?, :>
213
end