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:

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

comparison_tolerance[W]
comparison_tolerance[W]
other_time[R]
time[RW]

Public Class Methods

add(time, duration) click to toggle source

@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
between?(time1, t_arg, t_max=nil, tolerance: nil) click to toggle source

@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
compare(time1, time2, tolerance: nil) click to toggle source

@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
comparison_tolerance() click to toggle source

@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
convert(arg, coerce = true) click to toggle source

@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
is_a_time?(value) click to toggle source

@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
method_missing(symbol, *args, &block) click to toggle source
Calls superclass method
# 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
new(*time, tolerance: nil) click to toggle source
# 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
newer?(time1, time2, tolerance: nil) click to toggle source

@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
older?(time1, time2, tolerance: nil) click to toggle source

@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
parse(time_string) click to toggle source

@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
respond_to_missing?(symbol, include_all=false) click to toggle source
# File lib/easy_time.rb, line 224
def respond_to_missing?(symbol, include_all=false)
  Time.respond_to?(symbol, include_all)
end
same?(time1, time2, tolerance: nil) click to toggle source

@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
subtract(time, time_or_duration) click to toggle source

@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
time_format_style(str) click to toggle source

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

parse_string(time_str) click to toggle source
# 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

+(duration) click to toggle source

@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
-(other) click to toggle source

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
<=>(other) click to toggle source

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
acts_like_time?() click to toggle source
# File lib/easy_time.rb, line 373
def acts_like_time?
  true
end
between?(t_arg, t_max = nil) click to toggle source

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
compare(time2, tolerance: nil) click to toggle source

@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
comparison_tolerance() click to toggle source

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
different?(time2) click to toggle source

@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
eql?(time2)
Alias for: same?
newer?(time2) click to toggle source

@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
older?(time2) click to toggle source

@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
same?(time2) click to toggle source

@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
Also aliased as: eql?
with_tolerance(value) click to toggle source

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

convert(datetime, coerce = true) click to toggle source
# File lib/easy_time.rb, line 379
def convert(datetime, coerce = true)
  self.class.convert(datetime, coerce)
end
is_a_time?(value) click to toggle source
# File lib/easy_time.rb, line 397
def is_a_time?(value)
  self.class.is_a_time?(value)
end
method_missing(symbol, *args, &block) click to toggle source

intercept any time methods so they can wrap the time-like result in a new EasyTime object.

Calls superclass method
# 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
respond_to_missing?(symbol, include_all=false) click to toggle source
# File lib/easy_time.rb, line 393
def respond_to_missing?(symbol, include_all=false)
  time.respond_to?(symbol, include_all)
end