class TimeCalc::Diff

Represents difference between two time-or-date values.

Typically created with just

“`ruby TimeCalc.(t1) - t2 “`

Allows to easily and correctly calculate number of years/monthes/days/etc between two points in time.

@example

t1 = Time.parse('2019-06-01 14:50')
t2 = Time.parse('2019-06-15 12:10')
(TimeCalc.(t2) - t1).div(:day)
# => 13
# the same:
(TimeCalc.(t2) - t1).days
# => 13
(TimeCalc.(t2) - t1).div(3, :hours)
# => 111

(TimeCalc.(t2) - t1).factorize
# => {:year=>0, :month=>0, :week=>1, :day=>6, :hour=>21, :min=>20, :sec=>0}
(TimeCalc.(t2) - t1).factorize(weeks: false)
# => {:year=>0, :month=>0, :day=>13, :hour=>21, :min=>20, :sec=>0}
(TimeCalc.(t2) - t1).factorize(weeks: false, zeroes: false)
# => {:day=>13, :hour=>21, :min=>20, :sec=>0}

Attributes

from[R]

@private

to[R]

@private

Public Class Methods

new(from, to) click to toggle source

@note

Typically you should prefer {TimeCalc#-} to create Diff.

@param from [Time,Date,DateTime] @param to [Time,Date,DateTime]

# File lib/time_calc/diff.rb, line 42
def initialize(from, to)
  @from, @to = coerce(try_unwrap(from), try_unwrap(to)).map(&Value.method(:wrap))
end

Public Instance Methods

%(span, unit = nil)
Alias for: modulo
-@() click to toggle source

“Negates” the diff by swapping its operands. @return [Diff]

# File lib/time_calc/diff.rb, line 53
def -@
  Diff.new(to, from)
end
/(span, unit = nil)
Alias for: div
<=>(other) click to toggle source

@return [-1, 0, 1]

# File lib/time_calc/diff.rb, line 192
def <=>(other)
  return unless other.is_a?(Diff)

  exact <=> other.exact
end
div(span, unit = nil) click to toggle source

@example

t1 = Time.parse('2019-06-01 14:50')
t2 = Time.parse('2019-06-15 12:10')
(TimeCalc.(t2) - t1).div(:day)
# => 13
(TimeCalc.(t2) - t1).div(3, :hours)
# => 111

@overload div(span, unit)

@param span [Integer]
@param unit [Symbol] Any of supported units (see {TimeCalc})

@overload div(unit)

Shortcut for `div(1, unit)`. Also can called as just `.<units>` methods (like {#years})
@param unit [Symbol] Any of supported units (see {TimeCalc})

@return [Integer] Number of whole `<unit>`s between `Diff`'s operands.

# File lib/time_calc/diff.rb, line 90
def div(span, unit = nil)
  return -(-self).div(span, unit) if negative?

  span, unit = 1, span if unit.nil?
  unit = Units.(unit)
  singular_div(unit).div(span)
end
Also aliased as: /
divmod(span, unit = nil) click to toggle source

Combination of {#div} and {#modulo} in one operation.

@overload divmod(span, unit)

@param span [Integer]
@param unit [Symbol] Any of supported units (see {TimeCalc})

@overload divmod(unit)

Shortcut for `divmod(1, unit)`
@param unit [Symbol] Any of supported units (see {TimeCalc})

@return [(Integer, Time or Date or DateTime)]

# File lib/time_calc/diff.rb, line 68
def divmod(span, unit = nil)
  span, unit = 1, span if unit.nil?
  div(span, unit).then { |res| [res, to.+(res * span, unit).unwrap] }
end
exact() click to toggle source

@private

# File lib/time_calc/diff.rb, line 177
def exact
  from.unwrap.to_time - to.unwrap.to_time
end
factorize(zeroes: true, max: :year, min: :sec, weeks: true) click to toggle source

“Factorizes” the distance between two points in time into units: years, months, weeks, days.

@example

t1 = Time.parse('2019-06-01 14:50')
t2 = Time.parse('2019-06-15 12:10')
(TimeCalc.(t2) - t1).factorize
# => {:year=>0, :month=>0, :week=>1, :day=>6, :hour=>21, :min=>20, :sec=>0}
(TimeCalc.(t2) - t1).factorize(weeks: false)
# => {:year=>0, :month=>0, :day=>13, :hour=>21, :min=>20, :sec=>0}
(TimeCalc.(t2) - t1).factorize(weeks: false, zeroes: false)
# => {:day=>13, :hour=>21, :min=>20, :sec=>0}
(TimeCalc.(t2) - t1).factorize(max: :hour)
# => {:hour=>333, :min=>20, :sec=>0}
(TimeCalc.(t2) - t1).factorize(max: :hour, min: :min)
# => {:hour=>333, :min=>20}

@param zeroes [true, false] Include big units (for ex., year), if they are zero @param weeks [true, false] Include weeks @param max [Symbol] Max unit to factorize into, from all supported units list @param min [Symbol] Min unit to factorize into, from all supported units list @return [Hash<Symbol => Integer>]

# File lib/time_calc/diff.rb, line 160
def factorize(zeroes: true, max: :year, min: :sec, weeks: true)
  t = to
  f = from
  select_units(max: Units.(max), min: Units.(min), weeks: weeks)
    .inject({}) { |res, unit|
      span, t = Diff.new(f, t).divmod(unit)
      res.merge(unit => span)
    }.then { |res|
      next res if zeroes

      res.drop_while { |_, v| v.zero? }.to_h
    }
end
inspect() click to toggle source

@private

# File lib/time_calc/diff.rb, line 47
def inspect
  '#<%s(%s − %s)>' % [self.class, from.unwrap, to.unwrap]
end
modulo(span, unit = nil) click to toggle source

Same as integer modulo: the “rest” of whole division of the distance between two time points by `<span> <units>`. This rest will be also time point, equal to `first diff operand - span units`

@overload modulo(span, unit)

@param span [Integer]
@param unit [Symbol] Any of supported units (see {TimeCalc})

@overload modulo(unit)

Shortcut for `modulo(1, unit)`.
@param unit [Symbol] Any of supported units (see {TimeCalc})

@return [Time, Date or DateTime] Value is always the same type as first diff operand

# File lib/time_calc/diff.rb, line 132
def modulo(span, unit = nil)
  divmod(span, unit).last
end
Also aliased as: %
negative?() click to toggle source

@return [true, false]

# File lib/time_calc/diff.rb, line 182
def negative?
  exact.negative?
end
positive?() click to toggle source

@return [true, false]

# File lib/time_calc/diff.rb, line 187
def positive?
  exact.positive?
end

Private Instance Methods

coerce(from, to) click to toggle source
# File lib/time_calc/diff.rb, line 246
def coerce(from, to)
  case
  when from.class != to.class
    coerce_classes(from, to)
  when zone(from) != zone(to)
    coerce_zones(from, to)
  else
    [from, to]
  end
end
coerce_classes(from, to) click to toggle source
# File lib/time_calc/diff.rb, line 269
def coerce_classes(from, to)
  case
  when from.class == Date # not is_a?(Date), it will catch DateTime
    [coerce_date(from, to), to]
  when to.class == Date
    [from, coerce_date(to, from)]
  else
    [from, to.public_send("to_#{from.class.downcase}")].then(&method(:coerce_zones))
  end
end
coerce_date(date, other) click to toggle source

Will coerce Date to Time or DateTime, with the _zone of the latter_

# File lib/time_calc/diff.rb, line 286
def coerce_date(date, other)
  TimeCalc.(other)
    .merge(**Units::DEFAULTS.merge(year: date.year, month: date.month, day: date.day))
end
coerce_zones(from, to) click to toggle source
# File lib/time_calc/diff.rb, line 280
def coerce_zones(from, to)
  # TODO: to should be in from zone, even if different classes!
  [from, to]
end
month_div() click to toggle source
# File lib/time_calc/diff.rb, line 222
def month_div # rubocop:disable Metrics/AbcSize -- well... at least it is short
  ((from.year - to.year) * 12 + (from.month - to.month))
    .then { |res| from.day >= to.day ? res : res - 1 }
end
select_units(max:, min:, weeks:) click to toggle source
# File lib/time_calc/diff.rb, line 231
def select_units(max:, min:, weeks:)
  Units::ALL
    .drop_while { |u| u != max }
    .reverse.drop_while { |u| u != min }.reverse
    .then { |list|
      next list if weeks

      list - %i[week]
    }
end
simple_div(t1, t2, unit) click to toggle source
# File lib/time_calc/diff.rb, line 215
def simple_div(t1, t2, unit)
  return simple_div(t1.to_time, t2.to_time, unit) unless Types.compatible?(t1, t2)

  t1.-(t2).div(Units.multiplier_for(t1.class, unit, precise: true))
    .then { |res| unit == :day ? DST.fix_day_diff(t1, t2, res) : res }
end
singular_div(unit) click to toggle source
# File lib/time_calc/diff.rb, line 202
def singular_div(unit)
  case unit
  when :sec, :min, :hour, :day
    simple_div(from.unwrap, to.unwrap, unit)
  when :week
    div(7, :day)
  when :month
    month_div
  when :year
    year_div
  end
end
try_unwrap(tm) click to toggle source
# File lib/time_calc/diff.rb, line 242
def try_unwrap(tm)
  tm.respond_to?(:unwrap) ? tm.unwrap : tm
end
year_div() click to toggle source
# File lib/time_calc/diff.rb, line 227
def year_div
  from.year.-(to.year).then { |res| to.merge(year: from.year) <= from ? res : res - 1 }
end
zone(tm) click to toggle source
# File lib/time_calc/diff.rb, line 257
def zone(tm)
  case tm
  when Time
    # "" is JRuby's way to say "I don't know zone"
    tm.zone&.then { |z| z == '' ? nil : z } || tm.utc_offset
  when Date
    nil
  when DateTime
    tm.zone
  end
end