class TimeCalc::Value

Wrapper (one can say “monad”) around date/time value, allowing to perform several TimeCalc operations in a chain.

@example

TimeCalc.wrap(Time.parse('2019-06-01 14:50')).+(1, :year).-(1, :month).round(:week).unwrap
# => 2020-05-04 00:00:00 +0300

Constants

CLASS_NAME

@private Because AS::TimeWithZone so frigging smart that it returns “Time” from redefined class name.

TIMEY

@private

Attributes

internal[R]

@private

Public Class Methods

new(time_or_date) click to toggle source

@note

Prefer {TimeCalc.wrap} to create a Value.

@param time_or_date [Time, Date, DateTime]

# File lib/time_calc/value.rb, line 50
def initialize(time_or_date)
  @internal = time_or_date
end
wrap(value) click to toggle source

@private

# File lib/time_calc/value.rb, line 28
def self.wrap(value)
  case value
  when Time, Date, DateTime
    # NB: ActiveSupport::TimeWithZone will also pass to this branch if
    # active_support/core_ext/time is required. But it is doubtfully it is not -- TWZ will be
    # mostly unusable :)
    new(value)
  when Value
    value
  when TIMEY
    wrap(value.to_time)
  else
    fail ArgumentError, "Unsupported value: #{value}"
  end
end

Public Instance Methods

+(span, unit) click to toggle source

Add `<span units>` to wrapped value.

@param span [Integer] @param unit [Symbol] @return [Value]

# File lib/time_calc/value.rb, line 147
def +(span, unit)
  unit = Units.(unit)
  case unit
  when :sec, :min, :hour, :day
    plus_seconds(span, unit)
  when :week
    self.+(span * 7, :day)
  when :month
    plus_months(span)
  when :year
    merge(year: year + span)
  end
end
-(span_or_other, unit = nil) click to toggle source

@overload -(span, unit)

Subtracts `span units` from wrapped value.
@param span [Integer]
@param unit [Symbol]
@return [Value]

@overload -(date_or_time)

Produces {Diff}, allowing to calculate structured difference between two points in time.
@param date_or_time [Date, Time, DateTime]
@return [Diff]

Subtracts `span units` from wrapped value.

# File lib/time_calc/value.rb, line 171
def -(span_or_other, unit = nil)
  unit.nil? ? Diff.new(self, span_or_other) : self.+(-span_or_other, unit)
end
<=>(other) click to toggle source

@return [1, 0, -1]

# File lib/time_calc/value.rb, line 65
def <=>(other)
  return unless other.is_a?(self.class)

  Types.compare(internal, other.internal)
end
ceil(unit) click to toggle source

Ceils (rounds up) underlying date/time to nearest `unit`.

@example

TimeCalc.from(Time.parse('2018-06-23 12:30')).ceil(:month)
# => #<TimeCalc::Value(2018-07-01 00:00:00 +0300)>

@param unit [Symbol] @return [Value]

# File lib/time_calc/value.rb, line 124
def ceil(unit)
  floor(unit).then { |res| res == self ? res : res.+(1, unit) }
end
convert(klass) click to toggle source

@private

# File lib/time_calc/value.rb, line 229
def convert(klass)
  return dup if internal.class == klass

  Value.new(Types.convert(internal, klass))
end
dst?() click to toggle source
# File lib/time_calc/value.rb, line 75
def dst?
  return unless internal.respond_to?(:dst?)

  internal.dst?
end
floor(unit)
Alias for: truncate
for(span, unit) click to toggle source

Produces {Sequence} from this value to `this + <span units>`

@param span [Integer] @param unit [Symbol] @return [Sequence]

# File lib/time_calc/value.rb, line 224
def for(span, unit)
  to(self.+(span, unit))
end
inspect() click to toggle source

@private

# File lib/time_calc/value.rb, line 60
def inspect
  '#<%s(%s)>' % [self.class, internal]
end
iterate(span, unit) { |internal| ... } click to toggle source

Like {#+}, but allows conditional skipping of some periods. Increases value by `unit` at least `span` times, on each iteration checking with block provided if this point matches desired period; if it is not, it is skipped without increasing iterations counter. Useful for “business date/time” algorithms.

See {TimeCalc#iterate} for examples.

@param span [Integer] @param unit [Symbol] @return [Value] @yield [Time/Date/DateTime] Object of wrapped class @yieldreturn [true, false] If this point in time is “suitable”. If the falsey value is returned,

iteration is skipped without increasing the counter.
# File lib/time_calc/value.rb, line 188
def iterate(span, unit)
  block_given? or fail ArgumentError, 'No block given'
  Integer === span or fail ArgumentError, 'Only integer spans are supported' # rubocop:disable Style/CaseEquality

  Enumerator.produce(self) { |v| v.+((span <=> 0).nonzero? || 1, unit) }
            .lazy.select { |v| yield(v.internal) }
            .drop(span.abs).first
end
merge(**attrs) click to toggle source

Produces new value with some components of underlying time/date replaced.

@example

TimeCalc.from(Date.parse('2018-06-01')).merge(year: 1983)
# => #<TimeCalc::Value(1983-06-01)>

@param attrs [Hash<Symbol => Integer>] @return [Value]

# File lib/time_calc/value.rb, line 89
def merge(**attrs)
  class_name = CLASS_NAME.bind(internal.class).call.tr(':', '_')
  Value.new(Types.public_send("merge_#{class_name.downcase}", internal, **attrs))
end
round(unit) click to toggle source

Rounds up or down underlying date/time to nearest `unit`.

@example

TimeCalc.from(Time.parse('2018-06-23 12:30')).round(:month)
# => #<TimeCalc::Value(2018-07-01 00:00:00 +0300)>

@param unit [Symbol] @return Value

# File lib/time_calc/value.rb, line 136
def round(unit)
  f, c = floor(unit), ceil(unit)

  (internal - f.internal).abs < (internal - c.internal).abs ? f : c
end
step(span, unit = nil) click to toggle source

Produces endless {Sequence} from this value, with step specified.

@overload step(unit)

Shortcut for `step(1, unit)`
@param unit [Symbol]

@overload step(span, unit)

@param span [Integer]
@param unit [Symbol]

@return [Sequence]

# File lib/time_calc/value.rb, line 214
def step(span, unit = nil)
  span, unit = 1, span if unit.nil?
  Sequence.new(from: self).step(span, unit)
end
to(date_or_time) click to toggle source

Produces {Sequence} from this value to `date_or_time`

@param date_or_time [Date, Time, DateTime] @return [Sequence]

# File lib/time_calc/value.rb, line 201
def to(date_or_time)
  Sequence.new(from: self).to(date_or_time)
end
truncate(unit) click to toggle source

Truncates all time components lower than `unit`. In other words, “floors” (rounds down) underlying date/time to nearest `unit`.

@example

TimeCalc.from(Time.parse('2018-06-23 12:30')).floor(:month)
# => #<TimeCalc::Value(2018-06-01 00:00:00 +0300)>

@param unit [Symbol] @return Value

# File lib/time_calc/value.rb, line 103
def truncate(unit)
  unit = Units.(unit)
  return floor_week if unit == :week

  Units::STRUCTURAL
    .drop_while { |u| u != unit }
    .drop(1)
    .then { |keys| Units::DEFAULTS.slice(*keys) }
    .then { |attrs| merge(**attrs) } # can't simplify to &method(:merge) due to 2.7 keyword param problem
end
Also aliased as: floor
unwrap() click to toggle source

@return [Time, Date, DateTime] The value of the original type that was wrapped and processed

# File lib/time_calc/value.rb, line 55
def unwrap
  @internal
end

Private Instance Methods

floor_week() click to toggle source
# File lib/time_calc/value.rb, line 237
def floor_week
  extra_days = (internal.wday.nonzero? || 7) - 1
  floor(:day).-(extra_days, :days)
end
plus_months(span) click to toggle source
# File lib/time_calc/value.rb, line 242
def plus_months(span)
  target = month + span.to_i
  m = (target - 1) % 12 + 1
  dy = (target - 1) / 12
  merge(year: year + dy, month: m)
end
plus_seconds(span, unit) click to toggle source
# File lib/time_calc/value.rb, line 249
def plus_seconds(span, unit)
  Value.new(internal + span * Units.multiplier_for(internal.class, unit))
       .then { |res| unit == :day ? DST.fix_value(res, self) : res }
end