class EasyTime
Are you tired of having to deal with many kinds of date and time objects?
Are you frustrated that comparing timestamps from different systems yields incorrect results? _(Were you surprised to learn that, despite really good time sync sources, many systems aren't actually synced all that closely in time?)_
Well, then, give EasyTime
a try!
`EasyTime` accepts most of the well-known date and time objects, including `RFC2822`, `HTTPDate`, `XMLSchema`, and `ISO8601` strings and provides comparisons that have an adjustable tolerance. With `EasyTime` methods, you can reliably compare two timestamps and determine which one is “newer”, “older” or the “same” withing a configurable tolerance.
The default comparison tolerance is 1 second. This means that if a local object is created at time t1 and then transferred across the network and used to create a corresponding object in a 3rd-party system, eg: AWS S3, at time t2, then if `(t1 - t2).abs` is < 1.second the two times would be logicall the same.
In other words, if you have a time-stamp from an `ActiveRecord` object that is a few milliseconds different from a related object obtained from a 3rd-party system, (eg: AWS S3), then logically, from an application perspective, these two objects could be considered having the “same” time-stamp.
This is quite useful when one is trying to keep state synchronized between different systems. How does one know if an object is “newer” or “older” than that from another system? If the system time from the connected systems varies by a few or more seconds, then comparisons needs to have some tolerance.
Having a tolerant comparison makes “newness” and “oldness” checks easier to manage across systems with possibly varying time sources.
However, it is also important to keep the tolerance as small as possible in order to avoid causing more problems, such as false equivalences when objects really were created at different moments in time.
`EasyTime` objects are just like Time objects, except:
-
they auto-convert most time objects, including strings, to Time objects
-
they provide configurable tolerant comparisons between two time objects
Even if you decide to set the configurable comparison tolerance to zero _(which disables it)_, the auto-type conversion of most date and time objects makes time and date comparisons and arithmetic very easy.
Finally, this module adds an new instance method to the familiar date and time classes, to easily convert from the object to the corresponding `EasyTime` object:
time.easy_time
The conversion to an `EasyTime` can also be provided with a tolerance value:
time.easy_time(tolerance: 2.seconds)
These are the currently known date and time classes the values of which will be automatically converted to an `EasyTime` value with tolerant comparisons:
Date Time EasyTime DateTime ActiveSupport::Duration ActiveSupport::TimeWithZone String
The String values are examined and parsed into a `Time` value. If a string cannot be parsed, the `new` and `convert` methods return a nil.
These class methods are for converting most date or time formats to a Time
Constants
- DEFAULT_TIME_COMPARISON_TOLERANCE
we define a default tolerance below. This causes time value differences less than this to be considered “equal”. This allows for time comparisons between values from different systems where the clock sync might not be very accurate.
If this default tolerance is not desired, it can be overridden with an explicit tolerance setting in the singleton class instance:
EasyTime.comparison_tolerance = 0
- HTTPDATE_RE
A regexp pattern to match an HTTPDate time string _(used in web server transactions and logs)_
- ISO8601_RE
A regexp pattern to match an ISO8601 time string. @see en.wikipedia.org/wiki/ISO_8601
- ISO_DATE_RE
A regexp pattern to match the date part of an ISO8601 time string
- ISO_TIME_RE
A regexp pattern to match the time part of an ISO8601 time string
- ISO_ZONE_RE
A regexp pattern to match the timezone part of an ISO8601 time string
- RFC2822_RE
A regexp pattern to match an RFC2822 time string _(used in Email messages and systems)_
- VERSION
- XMLSCHEMA_RE
A regexp pattern to match an XMLSchema time string _(used in XML documents)_
Attributes
Public Class Methods
@param time [Time,DateTime,ActiveSupport::TimeWithZone,Date,String,Integer,Array<Integer>] a time value @param duration [ActiveSupport::Duration,Integer,Float] a duration value @return [EasyTime] the `time` with the `duration` added
# File lib/easy_time.rb, line 182 def add(time, duration) EasyTime.new(time) + duration end
@overload between?(time1, t_min, t_max, tolerance: nil)
@param time1 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] a time value @param t_min [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] the minimum time @param t_max [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] the maximum time @return [Boolean] true if `t_min <= time1 <= t_max`, using tolerant comparisons
@overload between?(time1, time_range, tolerance: nil)
@param time1 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] a time value @param time_range [Range] a range `(t_min..t_max)` of time values @return [Boolean] true if `time_range.min <= time1 <= time_range.max`, using tolerant comparisons
# File lib/easy_time.rb, line 153 def between?(time1, t_arg, t_max=nil, tolerance: nil) if t_arg.is_a?(Range) t_min = t_arg.min t_max = t_arg.max else t_min = t_arg end compare(time1, t_min, tolerance: tolerance) >= 0 && compare(time1, t_max, tolerance: tolerance) <= 0 end
@param time1 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] a time value @param time2 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] another time value @param tolerance [Integer] seconds of tolerance _(optional)_ @return [Integer] one of [-1, 0, 1] if `time1` <, ==, or > than `time2`,
or nil if `time2` cannot be converted to a `Time` value.
# File lib/easy_time.rb, line 170 def compare(time1, time2, tolerance: nil) new(time1, tolerance: tolerance) <=> time2 end
@return [Integer] the number of seconds of tolerance to use for “equality” tests
# File lib/easy_time.rb, line 205 def comparison_tolerance @tolerance || DEFAULT_TIME_COMPARISON_TOLERANCE end
@param arg [String, EasyTime
, Time, Date, DateTime, Array<Integer>, Duration]
various kinds of date and time values
@param coerce [Boolean] if true, coerce the `arg` into a Time object _(default: true)_ @return [Time]
# File lib/easy_time/convert.rb, line 44 def convert(arg, coerce = true) case arg when String parse_string(arg) # parse the string value into an EasyTime object when Array ::Time.new(*arg) # convert Time arguments: [yyyy, mm, dd, hh, MM, SS] when ::EasyTime arg.time # extract the EasyTime value when ActiveSupport::TimeWithZone arg.to_time # convert the TimeWithZone value to a Time object when ActiveSupport::Duration coerce ? Time.now + arg : arg # coerced duration objects are relative to "now" when ::Time arg # accept Time objects as-as when ::Date, ::DateTime ::Time.iso8601(arg.iso8601) # convert Date and DateTime objects via ISO8601 formatting when NilClass ::Time.now # a nil object means "now" when Numeric coerce ? Time.at(arg) : arg # if coerced, treat as seconds-since-Epoch else raise ArgumentError, "EasyTime: unknown value: '#{arg.inspect}'" end end
@param value [Anything] value to test as a time-like object @return [Boolean] true if value is one the known Time classes, or responds to :acts_like_time?
# File lib/easy_time.rb, line 230 def is_a_time?(value) case value when Integer, ActiveSupport::Duration false when Date, Time, DateTime, ActiveSupport::TimeWithZone, EasyTime true else value.respond_to?(:acts_like_time?) && value.acts_like_time? end end
# File lib/easy_time.rb, line 215 def method_missing(symbol, *args, &block) if Time.respond_to?(symbol) value = Time.send(symbol, *args, &block) is_a_time?(value) ? new(value) : value else super(symbol, *args, &block) end end
# File lib/easy_time.rb, line 248 def initialize(*time, tolerance: nil) @time = time.presence && convert(time.size == 1 ? time.first : time) @comparison_tolerance = tolerance end
@param time1 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] a time value @param time2 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] another time value @param tolerance [Integer] seconds of tolerance _(optional)_ @return [Boolean] true if `time1` > `time2`, using a tolerant comparison
# File lib/easy_time.rb, line 119 def newer?(time1, time2, tolerance: nil) compare(time1, time2, tolerance: tolerance).positive? end
@param time1 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] a time value @param time2 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] another time value @param tolerance [Integer] seconds of tolerance _(optional)_ @return [Boolean] true if `time1` > `time2`, using a tolerant comparison
# File lib/easy_time.rb, line 128 def older?(time1, time2, tolerance: nil) compare(time1, time2, tolerance: tolerance).negative? end
@param time_string [String] a time string in one of the many known Time string formats @return [EasyTime]
# File lib/easy_time.rb, line 211 def parse(time_string) new(parse_string(time_string)) end
# File lib/easy_time.rb, line 224 def respond_to_missing?(symbol, include_all=false) Time.respond_to?(symbol, include_all) end
@param time1 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] a time value @param time2 [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] another time value @param tolerance [Integer] seconds of tolerance _(optional)_ @return [Boolean] true if `time1` > `time2`, using a tolerant comparison
# File lib/easy_time.rb, line 137 def same?(time1, time2, tolerance: nil) compare(time1, time2, tolerance: tolerance).zero? end
@param time [Time,DateTime,ActiveSupport::TimeWithZone,Date,String,Integer,Array<Integer>] a time value @param time_or_duration [Time,DateTime,ActiveSupport::Duration,Integer,Float] a time or duration value @return [EasyTime,Duration] an `EasyTime` value, when a duration is subtracted from a time, or
a duration _(Integer)_ value, when one time is subtracted from another time.
# File lib/easy_time.rb, line 191 def subtract(time, time_or_duration) EasyTime.new(time) - time_or_duration end
this method returns parser class methods in the Time class for corresponding time format patterns
# File lib/easy_time/convert.rb, line 139 def time_format_style(str) case str when RFC2822_RE then :rfc2822 when HTTPDATE_RE then :httpdate when XMLSCHEMA_RE then :xmlschema when ISO8601_RE then :iso8601 end end
Private Class Methods
# File lib/easy_time/convert.rb, line 71 def parse_string(time_str) parser = time_format_style(time_str) # invoke the found parser format, otherwise use the fall-back general-purpose parser (parser && ::Time.send(parser, time_str) rescue nil) || ::Time.parse(time_str) end
Public Instance Methods
@param duration [Integer] seconds to add to the EasyTime
value @return [EasyTime] updated date and time value
# File lib/easy_time.rb, line 353 def +(duration) dup.tap { |eztime| eztime.time += duration } end
Subtract a value from an EasyTime
. If the value is an integer, it is treated as seconds. If the value is any of the Date, DateTime, Time, EasyTime
, or a String- formatted date/time, it is subtracted from the EasyTime
value resulting in an integer duration. @param other [Date,Time,DateTime,EasyTime,Duration,String,Integer]
a date/time value, a duration, or an Integer
@return [EasyTime,Integer] updated time _(time - duration)_ or duration _(time - time)_
# File lib/easy_time.rb, line 364 def -(other) @other_time = convert(other, false) if is_a_time?(other_time) time - other_time elsif other_time dup.tap { |eztime| eztime.time -= other_time } end end
compare with automatic type-conversion and tolerance @return [Integer] one of [-1, 0, 1] or nil
# File lib/easy_time.rb, line 320 def <=>(other) diff = self - other # note: this has a side-effect of setting @other_time if diff && diff.to_i.abs <= comparison_tolerance.to_i 0 elsif diff time <=> other_time end end
# File lib/easy_time.rb, line 373 def acts_like_time? true end
compare a time against a min and max date pair, or against a time Range value. @overload between?(t_min, t_max, tolerance: nil)
@param t_min [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] the minimum time @param t_max [Date,Time,DateTime,EasyTime,Duration,String,Array<Integer>] the maximum time @param tolerance [Integer] the optional amount of seconds of tolerance to use in the comparison @return [Boolean] true if `t_min <= self.time <= t_max`, using tolerant comparisons
@overload between?(time_range, tolerance: nil)
@param time_range [Range] a range `(t_min..t_max)` of time values @param tolerance [Integer] the optional amount of seconds of tolerance to use in the comparison @return [Boolean] true if `time_range.min <= self.time <= time_range.max`, using tolerant comparisons
# File lib/easy_time.rb, line 341 def between?(t_arg, t_max = nil) if t_arg.is_a?(Range) t_min = t_arg.min t_max = t_arg.max else t_min = t_arg end compare(t_min) >= 0 && compare(t_max) <= 0 end
@param time2 [String,Date,Time,DateTime,Duration,Array<Integer>] another time value @return [Integer] one of the values: [-1, 0, 1] if `self` [<, ==, >] `time2`,
or nil if `time2` cannot be converted to a `Time` value
# File lib/easy_time.rb, line 312 def compare(time2, tolerance: nil) self.comparison_tolerance = tolerance if tolerance self <=> time2 end
if there is no instance value, default to the class value
# File lib/easy_time.rb, line 254 def comparison_tolerance @comparison_tolerance || self.class.comparison_tolerance end
@param time2 [String,Date,Time,DateTime,Duration,Array<Integer>] another time value @return [Boolean] true if `self` != `time2`
# File lib/easy_time.rb, line 304 def different?(time2) self != time2 end
@param time2 [String,Date,Time,DateTime,Duration,Array<Integer>] another time value @return [Boolean] true if `self` > `time2`
# File lib/easy_time.rb, line 282 def newer?(time2) self > time2 end
@param time2 [String,Date,Time,DateTime,Duration,Array<Integer>] another time value @return [Boolean] true if `self` < `time2`
# File lib/easy_time.rb, line 289 def older?(time2) self < time2 end
@param time2 [String,Date,Time,DateTime,Duration,Array<Integer>] another time value @return [Boolean] true if `self` == `time2`
# File lib/easy_time.rb, line 296 def same?(time2) self == time2 end
returns a new EasyTime
value with the tolerance set to value
@example Example:
t1 = EasyTime.new(some_time) t1.with_tolerance(2.seconds) <= some_other_time
@param value [Integer] a number of seconds to use as the comparison tolerance @return [EasyTime] a new EasyTime
value with the given tolerance
# File lib/easy_time.rb, line 268 def with_tolerance(value) dup.tap { |time| time.comparison_tolerance = value } end
Private Instance Methods
# File lib/easy_time.rb, line 379 def convert(datetime, coerce = true) self.class.convert(datetime, coerce) end
# File lib/easy_time.rb, line 397 def is_a_time?(value) self.class.is_a_time?(value) end
intercept any time methods so they can wrap the time-like result in a new EasyTime
object.
# File lib/easy_time.rb, line 384 def method_missing(symbol, *args, &block) if time.respond_to?(symbol) value = time.send(symbol, *args, &block) is_a_time?(value) ? dup.tap { |eztime| eztime.time = value } : value else super(symbol, *args, &block) end end
# File lib/easy_time.rb, line 393 def respond_to_missing?(symbol, include_all=false) time.respond_to?(symbol, include_all) end