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
@private
@private
Public Class Methods
@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
“Negates” the diff by swapping its operands. @return [Diff]
# File lib/time_calc/diff.rb, line 53 def -@ Diff.new(to, from) end
@return [-1, 0, 1]
# File lib/time_calc/diff.rb, line 192 def <=>(other) return unless other.is_a?(Diff) exact <=> other.exact end
@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
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
@private
# File lib/time_calc/diff.rb, line 177 def exact from.unwrap.to_time - to.unwrap.to_time end
“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
@private
# File lib/time_calc/diff.rb, line 47 def inspect '#<%s(%s − %s)>' % [self.class, from.unwrap, to.unwrap] end
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
@return [true, false]
# File lib/time_calc/diff.rb, line 182 def negative? exact.negative? end
@return [true, false]
# File lib/time_calc/diff.rb, line 187 def positive? exact.positive? end
Private Instance Methods
# 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
# 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
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
# 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
# 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
# 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
# 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
# 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
# File lib/time_calc/diff.rb, line 242 def try_unwrap(tm) tm.respond_to?(:unwrap) ? tm.unwrap : tm end
# 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
# 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