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 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
|
require 'date'
require 'time'
module IceCube
module TimeUtil
extend Deprecated
DAYS = {
:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3,
:thursday => 4, :friday => 5, :saturday => 6
}
ICAL_DAYS = {
'SU' => :sunday, 'MO' => :monday, 'TU' => :tuesday, 'WE' => :wednesday,
'TH' => :thursday, 'FR' => :friday, 'SA' => :saturday
}
MONTHS = {
:january => 1, :february => 2, :march => 3, :april => 4, :may => 5,
:june => 6, :july => 7, :august => 8, :september => 9, :october => 10,
:november => 11, :december => 12
}
CLOCK_VALUES = [:year, :month, :day, :hour, :min, :sec]
# Provides a Time.now without the usec, in the reference zone or utc offset
def self.now(reference=Time.now)
match_zone(Time.at(Time.now.to_i), reference)
end
def self.build_in_zone(args, reference)
if reference.respond_to?(:time_zone)
reference.time_zone.local(*args)
elsif reference.utc?
Time.utc(*args)
elsif reference.zone
Time.local(*args)
else
Time.new(*args << reference.utc_offset)
end
end
def self.match_zone(input_time, reference)
return unless time = ensure_time(input_time, reference)
time = if reference.respond_to? :time_zone
time.in_time_zone(reference.time_zone)
else
if reference.utc?
time.getgm
elsif reference.zone
time.getlocal
else
time.getlocal(reference.utc_offset)
end
end
(Date === input_time) ? beginning_of_date(time, reference) : time
end
# Ensure that this is either nil, or a time
def self.ensure_time(time, reference = nil, date_eod = false)
case time
when DateTime
warn "IceCube: DateTime support is deprecated (please use Time) at: #{ caller[2] }"
Time.local(time.year, time.month, time.day, time.hour, time.min, time.sec)
when Date
if date_eod
end_of_date(time, reference)
else
if reference
build_in_zone([time.year, time.month, time.day], reference)
else
time.to_time
end
end
else
time
end
end
# Ensure that this is either nil, or a date
def self.ensure_date(date)
case date
when Date then date
else
return Date.new(date.year, date.month, date.day)
end
end
# Serialize a time appropriate for storing
def self.serialize_time(time)
case time
when Time, Date
if time.respond_to?(:time_zone)
{:time => time.utc, :zone => time.time_zone.name}
else
time
end
when DateTime
Time.local(time.year, time.month, time.day, time.hour, time.min, time.sec)
else
raise ArgumentError, "cannot serialize #{time.inspect}, expected a Time"
end
end
# Deserialize a time serialized with serialize_time or in ISO8601 string format
def self.deserialize_time(time_or_hash)
case time_or_hash
when Time, Date
time_or_hash
when DateTime
Time.local(time.year, time.month, time.day, time.hour, time.min, time.sec)
when Hash
hash = FlexibleHash.new(time_or_hash)
hash[:time].in_time_zone(hash[:zone])
when String
Time.parse(time_or_hash)
end
end
# Get a more precise equality for time objects
# Ruby provides a Time#hash method, but it fails to account for UTC
# offset (so the current date may be different) or DST rules (so the
# hour may be wrong for different schedule occurrences)
def self.hash(time)
[time, time.utc_offset, time.zone].hash
end
# Check the deserialized time offset string against actual local time
# offset to try and preserve the original offset for plain Ruby Time. If
# the offset is the same as local we can assume the same original zone and
# keep it. If it was serialized with a different offset than local TZ it
# will lose the zone and not support DST.
def self.restore_deserialized_offset(time, orig_offset_str)
return time if time.respond_to?(:time_zone) ||
time.getlocal(orig_offset_str).utc_offset == time.utc_offset
warn "IceCube: parsed Time from nonlocal TZ. Use ActiveSupport to fix DST at: #{ caller[0] }"
time.localtime(orig_offset_str)
end
# Get the beginning of a date
def self.beginning_of_date(date, reference=Time.now)
build_in_zone([date.year, date.month, date.day, 0, 0, 0], reference)
end
# Get the end of a date
def self.end_of_date(date, reference=Time.now)
build_in_zone([date.year, date.month, date.day, 23, 59, 59], reference)
end
# Convert a symbol to a numeric month
def self.sym_to_month(sym)
MONTHS.fetch(sym) do |k|
MONTHS.values.detect { |i| i.to_s == k.to_s } or
raise ArgumentError, "Expecting Integer or Symbol value for month. " \
"No such month: #{k.inspect}"
end
end
deprecated_alias :symbol_to_month, :sym_to_month
# Convert a symbol to a wday number
def self.sym_to_wday(sym)
DAYS.fetch(sym) do |k|
DAYS.values.detect { |i| i.to_s == k.to_s } or
raise ArgumentError, "Expecting Integer or Symbol value for weekday. " \
"No such weekday: #{k.inspect}"
end
end
deprecated_alias :symbol_to_day, :sym_to_wday
# Convert wday number to day symbol
def self.wday_to_sym(wday)
return wday if DAYS.keys.include? wday
DAYS.invert.fetch(wday) do |i|
raise ArgumentError, "Expecting Integer value for weekday. " \
"No such wday number: #{i.inspect}"
end
end
# Convert weekday from base sunday to the schedule's week start.
def self.normalize_wday(wday, week_start)
(wday - sym_to_wday(week_start)) % 7
end
deprecated_alias :normalize_weekday, :normalize_wday
def self.ical_day_to_symbol(str)
day = ICAL_DAYS[str]
raise ArgumentError, "Invalid day: #{str}" if day.nil?
day
end
# Return the count of the number of times wday appears in the month,
# and which of those time falls on
def self.which_occurrence_in_month(time, wday)
first_occurrence = ((7 - Time.utc(time.year, time.month, 1).wday) + time.wday) % 7 + 1
this_weekday_in_month_count = ((days_in_month(time) - first_occurrence + 1) / 7.0).ceil
nth_occurrence_of_weekday = (time.mday - first_occurrence) / 7 + 1
[nth_occurrence_of_weekday, this_weekday_in_month_count]
end
# Get the days in the month for +time
def self.days_in_month(time)
date = Date.new(time.year, time.month, 1)
((date >> 1) - date).to_i
end
# Get the days in the following month for +time
def self.days_in_next_month(time)
date = Date.new(time.year, time.month, 1) >> 1
((date >> 1) - date).to_i
end
# Count the number of days to the same day of the next month without
# overflowing shorter months
def self.days_to_next_month(time)
date = Date.new(time.year, time.month, time.day)
((date >> 1) - date).to_i
end
# Get a day of the month in the month of a given time without overflowing
# into the next month. Accepts days from positive (start of month forward) or
# negative (from end of month)
def self.day_of_month(value, date)
if value.to_i > 0
[value, days_in_month(date)].min
else
[1 + days_in_month(date) + value, 1].max
end
end
# Number of days in a year
def self.days_in_year(time)
date = Date.new(time.year, 1, 1)
((date >> 12) - date).to_i
end
# Number of days to n years
def self.days_in_n_years(time, year_distance)
date = Date.new(time.year, time.month, time.day)
((date >> year_distance * 12) - date).to_i
end
# The number of days in n months
def self.days_in_n_months(time, month_distance)
date = Date.new(time.year, time.month, time.day)
((date >> month_distance) - date).to_i
end
def self.dst_change(time)
one_hour_ago = time - ONE_HOUR
if time.dst? ^ one_hour_ago.dst?
(time.utc_offset - one_hour_ago.utc_offset) / ONE_HOUR
end
end
# Handle discrepancies between various time types
# - Time has subsec
# - DateTime does not
# - ActiveSupport::TimeWithZone can wrap either type, depending on version
# or if `parse` or `now`/`local` was used to build it.
def self.subsec(time)
if time.respond_to?(:subsec)
time.subsec
elsif time.respond_to?(:sec_fraction)
time.sec_fraction
else
0.0
end
end
# A utility class for safely moving time around
class TimeWrapper
def initialize(time, dst_adjust = true)
@dst_adjust = dst_adjust
@base = time
if dst_adjust
@time = Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec + TimeUtil.subsec(time))
else
@time = time
end
end
# Get the wrapped time back in its original zone & format
def to_time
return @time unless @dst_adjust
parts = @time.year, @time.month, @time.day, @time.hour, @time.min, @time.sec + @time.subsec
TimeUtil.build_in_zone(parts, @base)
end
# DST-safely add an interval of time to the wrapped time
def add(type, val)
type = :day if type == :wday
@time += case type
when :year then TimeUtil.days_in_n_years(@time, val) * ONE_DAY
when :month then TimeUtil.days_in_n_months(@time, val) * ONE_DAY
when :day then val * ONE_DAY
when :hour then val * ONE_HOUR
when :min then val * ONE_MINUTE
when :sec then val
end
end
# Clear everything below a certain type
CLEAR_ORDER = [:sec, :min, :hour, :day, :month, :year]
def clear_below(type)
type = :day if type == :wday
CLEAR_ORDER.each do |ptype|
break if ptype == type
send :"clear_#{ptype}"
end
end
def hour=(value)
@time += (value * ONE_HOUR) - (@time.hour * ONE_HOUR)
end
def min=(value)
@time += (value * ONE_MINUTE) - (@time.min * ONE_MINUTE)
end
def sec=(value)
@time += (value) - (@time.sec)
end
def clear_sec
@time.sec > 0 ? @time -= @time.sec : @time
end
def clear_min
@time.min > 0 ? @time -= (@time.min * ONE_MINUTE) : @time
end
def clear_hour
@time.hour > 0 ? @time -= (@time.hour * ONE_HOUR) : @time
end
# Move to the first of the month, 0 hours
def clear_day
@time.day > 1 ? @time -= (@time.day - 1) * ONE_DAY : @time
end
# Clear to january 1st
def clear_month
@time -= ONE_DAY
until @time.month == 12
@time -= TimeUtil.days_in_month(@time) * ONE_DAY
end
@time += ONE_DAY
end
end
end
end
|