lib/time_math/units/base.rb
module TimeMath
module Units
# It is a main class representing most of TimeMath functionality.
# It (or rather its descendants) represents "unit of time" and
# connected calculations logic. Typical usage:
#
# ```ruby
# TimeMath.day.advance(tm, 5) # advances tm by 5 days
# ```
#
# See also {TimeMath::Op} for performing multiple operations in
# concise & DRY manner, like this:
#
# ```ruby
# TimeMath().advance(:day, 5).floor(:hour).advance(:min, 20).call(tm)
# ```
#
class Base
# Creates unit of time. Typically you don't need it, as it is
# easier to do `TimeMath.day` or `TimeMath[:day]` to obtain it.
#
# @param name [Symbol] one of {TimeMath.units}.
def initialize(name)
@name = name
end
attr_reader :name
# Rounds `tm` down to nearest unit (this means, `TimeMath.day.floor(tm)`
# will return beginning of `tm`-s day, and so on).
#
# An optional second argument allows you to floor to arbitrary
# number of units, like to "each 3-hour" mark:
#
# ```ruby
# TimeMath.hour.floor(Time.parse('14:00'), 3)
# # => 2016-06-23 12:00:00 +0300
#
# # works well with float/rational spans
# TimeMath.hour.floor(Time.parse('14:15'), 1/2r)
# # => 2016-06-23 14:00:00 +0300
# TimeMath.hour.floor(Time.parse('14:45'), 1/2r)
# # => 2016-06-23 14:30:00 +0300
# ```
#
# @param tm [Time,Date,DateTime] time value to floor.
# @param span [Numeric] how many units to floor to. For units
# less than week supports float/rational values.
# @return [Time,Date,DateTime] floored time value; class and timezone offset of origin
# would be preserved.
def floor(tm, span = 1)
int_floor = advance(floor_1(tm), (tm.send(name) / span.to_f).floor * span - tm.send(name))
float_fix(tm, int_floor, span % 1)
end
# Rounds `tm` up to nearest unit (this means, `TimeMath.day.ceil(tm)`
# will return beginning of day next after `tm`, and so on).
# An optional second argument allows to ceil to arbitrary
# amount of units (see {#floor} for more detailed explanation).
#
# @param tm [Time,Date,DateTime] time value to ceil.
# @param span [Numeric] how many units to ceil to. For units
# less than week supports float/rational values.
# @return [Time,Date,DateTime] ceiled time value; class and timezone offset
# of origin would be preserved.
def ceil(tm, span = 1)
f = floor(tm, span)
f == tm ? f : advance(f, span)
end
# Rounds `tm` up or down to nearest unit (this means, `TimeMath.day.round(tm)`
# will return beginning of `tm` day if `tm` is before noon, and
# day next after `tm` if it is after, and so on).
# An optional second argument allows to round to arbitrary
# amount of units (see {#floor} for more detailed explanation).
#
# @param tm [Time,Date,DateTime] time value to round.
# @param span [Numeric] how many units to round to. For units
# less than week supports float/rational values.
# @return [Time,Date,DateTime] rounded time value; class and timezone offset
# of origin would be preserved.
def round(tm, span = 1)
f, c = floor(tm, span), ceil(tm, span)
(tm - f).abs < (tm - c).abs ? f : c
end
# Like {#floor}, but always return value lower than `tm` (e.g. if
# `tm` is exactly midnight, then `TimeMath.day.prev(tm)` will return
# _previous midnight_).
# An optional second argument allows to floor to arbitrary
# amount of units (see {#floor} for more detailed explanation).
#
# @param tm [Time,Date,DateTime] time value to calculate prev on.
# @param span [Numeric] how many units to floor to. For units
# less than week supports float/rational values.
# @return [Time,Date,DateTime] prev time value; class and timezone offset
# of origin would be preserved.
def prev(tm, span = 1)
f = floor(tm, span)
f == tm ? decrease(f, span) : f
end
# Like {#ceil}, but always return value greater than `tm` (e.g. if
# `tm` is exactly midnight, then `TimeMath.day.next(tm)` will return
# _next midnight_).
# An optional second argument allows to ceil to arbitrary
# amount of units (see {#floor} for more detailed explanation).
#
# @param tm [Time,Date,DateTime] time value to calculate next on.
# @param span [Numeric] how many units to ceil to. For units
# less than week supports float/rational values.
# @return [Time,Date,DateTime] next time value; class and timezone offset
# of origin would be preserved.
def next(tm, span = 1)
c = ceil(tm, span)
c == tm ? advance(c, span) : c
end
# Checks if `tm` is exactly rounded to unit.
#
# @param tm [Time,Date,DateTime] time value to check.
# @param span [Numeric] how many units to check round at. For units
# less than week supports float/rational values.
# @return [Boolean] whether `tm` is exactly round to unit.
def round?(tm, span = 1)
floor(tm, span) == tm
end
# Advances `tm` by given amount of unit.
#
# @param tm [Time,Date,DateTime] time value to advance;
# @param amount [Numeric] how many units forward to go. For units
# less than week supports float/rational values.
#
# @return [Time,Date,DateTime] advanced time value; class and timezone offset
# of origin would be preserved.
def advance(tm, amount = 1)
return decrease(tm, -amount) if amount < 0
_advance(tm, amount)
end
# Decreases `tm` by given amount of unit.
#
# @param tm [Time,Date,DateTime] time value to decrease;
# @param amount [Integer] how many units forward to go. For units
# less than week supports float/rational values.
#
# @return [Time,Date,DateTime] decrease time value; class and timezone offset
# of origin would be preserved.
def decrease(tm, amount = 1)
return advance(tm, -amount) if amount < 0
_decrease(tm, amount)
end
# Creates range from `tm` to `tm` increased by amount of units.
#
# ```ruby
# tm = Time.parse('2016-05-28 16:30')
# TimeMath.day.range(tm, 5)
# # => 2016-05-28 16:30:00 +0300...2016-06-02 16:30:00 +0300
# ```
#
# @param tm [Time,Date,DateTime] time value to create range from;
# @param amount [Integer] how many units should be between range
# start and end.
#
# @return [Range]
def range(tm, amount = 1)
(tm...advance(tm, amount))
end
# Creates range from `tm` decreased by amount of units to `tm`.
#
# ```ruby
# tm = Time.parse('2016-05-28 16:30')
# TimeMath.day.range_back(tm, 5)
# # => 2016-05-23 16:30:00 +0300...2016-05-28 16:30:00 +0300
# ```
#
# @param tm [Time,Date,DateTime] time value to create range from;
# @param amount [Integer] how many units should be between range
# start and end.
#
# @return [Range]
def range_back(tm, amount = 1)
(decrease(tm, amount)...tm)
end
# Measures distance between `from` and `to` in units of this class.
#
# @param from [Time,Date,DateTime] start of period;
# @param to [Time,Date,DateTime] end of period.
#
# @return [Integer] how many full units are inside the period.
# :nocov:
def measure(from, to)
from, to = from.to_time, to.to_time unless from.class == to.class
from <= to ? _measure(from, to) : -_measure(to, from)
end
# :nocov:
# Like {#measure} but also returns "remainder": the time where
# it would be **exactly** returned amount of units between `from`
# and `to`:
#
# ```ruby
# TimeMath.day.measure(Time.parse('2016-05-01 16:20'), Time.parse('2016-05-28 15:00'))
# # => 26
# TimeMath.day.measure_rem(Time.parse('2016-05-01 16:20'), Time.parse('2016-05-28 15:00'))
# # => [26, 2016-05-27 16:20:00 +0300]
# ```
#
# @param from [Time,Date,DateTime] start of period;
# @param to [Time,Date,DateTime] end of period.
#
# @return [Array<Integer, Time or DateTime>] how many full units
# are inside the period; exact value of `from` + full units.
def measure_rem(from, to)
m = measure(from, to)
[m, advance(from, m)]
end
# Creates {Sequence} instance for producing all time units between
# from and too. See {Sequence} class documentation for detailed functionality description.
#
# @param range [Range<Time,Date,DateTime>] start and end of sequence.
#
# @return [Sequence]
def sequence(range)
TimeMath::Sequence.new(name, range)
end
# Converts input timestamps list to regular list of timestamps
# over current unit.
#
# Like this:
#
# ```ruby
# times = [Time.parse('2016-05-01'), Time.parse('2016-05-03'), Time.parse('2016-05-08')]
# TimeMath.day.resample(times)
# # => => [2016-05-01 00:00:00 +0300, 2016-05-02 00:00:00 +0300, 2016-05-03 00:00:00 +0300, 2016-05-04 00:00:00 +0300, 2016-05-05 00:00:00 +0300, 2016-05-06 00:00:00 +0300, 2016-05-07 00:00:00 +0300, 2016-05-08 00:00:00 +0300]
# ```
#
# The best way about resampling it also works for hashes with time
# keys. Like this:
#
# ```ruby
# h = {Date.parse('Wed, 01 Jun 2016')=>1, Date.parse('Tue, 07 Jun 2016')=>3, Date.parse('Thu, 09 Jun 2016')=>1}
# # => {#<Date: 2016-06-01>=>1, #<Date: 2016-06-07>=>3, #<Date: 2016-06-09>=>1}
#
# pp TimeMath.day.resample(h)
# # {#<Date: 2016-06-01>=>[1],
# # #<Date: 2016-06-02>=>[],
# # #<Date: 2016-06-03>=>[],
# # #<Date: 2016-06-04>=>[],
# # #<Date: 2016-06-05>=>[],
# # #<Date: 2016-06-06>=>[],
# # #<Date: 2016-06-07>=>[3],
# # #<Date: 2016-06-08>=>[],
# # #<Date: 2016-06-09>=>[1]}
#
# # The default resample just groups all related values in arrays
# # You can pass block or symbol, to have the values you need:
# pp TimeMath.day.resample(h,&:first)
# # {#<Date: 2016-06-01>=>1,
# # #<Date: 2016-06-02>=>nil,
# # #<Date: 2016-06-03>=>nil,
# # #<Date: 2016-06-04>=>nil,
# # #<Date: 2016-06-05>=>nil,
# # #<Date: 2016-06-06>=>nil,
# # #<Date: 2016-06-07>=>3,
# # #<Date: 2016-06-08>=>nil,
# # #<Date: 2016-06-09>=>1}
# ```
#
# @param array_or_hash array of time-y values (Time/Date/DateTime)
# or hash with time-y keys.
# @param symbol in case of first param being a hash -- method to
# call on key arrays while grouping.
# @param block in case of first param being a hash -- block to
# call on key arrays while grouping.
#
# @return array or hash spread regular by unit; if first param was
# hash, keys corresponding to each period are grouped into arrays;
# this array could be further processed with block/symbol provided.
def resample(array_or_hash, symbol = nil, &block)
Resampler.call(name, array_or_hash, symbol, &block)
end
def inspect
"#<#{self.class}>"
end
private
def index
Util::NATURAL_UNITS.index(name) or
raise NotImplementedError, "Can not be used for #{name}"
end
def floor_1(tm)
components = Util.tm_to_array(tm).first(index + 1)
Util.array_to_tm(tm, *components)
end
def float_fix(tm, floored, float_span_part)
if float_span_part.zero?
floored
else
float_floored = advance(floored, float_span_part)
float_floored > tm ? floored : float_floored
end
end
end
end
end