module WorkingHours::Computation

Public Instance Methods

add_days(origin, days, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 7
def add_days origin, days, config: nil
  return origin if days.zero?

  config ||= wh_config
  time = in_config_zone(origin, config: config)
  time += (days <=> 0).day until working_day?(time, config: config)

  while days > 0
    time += 1.day
    days -= 1 if working_day?(time, config: config)
  end
  while days < 0
    time -= 1.day
    days += 1 if working_day?(time, config: config)
  end
  convert_to_original_format time, origin
end
add_hours(origin, hours, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 25
def add_hours origin, hours, config: nil
  config ||= wh_config
  add_minutes origin, hours * 60, config: config
end
add_minutes(origin, minutes, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 30
def add_minutes origin, minutes, config: nil
  config ||= wh_config
  add_seconds origin, minutes * 60, config: config
end
add_seconds(origin, seconds, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 35
def add_seconds origin, seconds, config: nil
  config ||= wh_config
  time = in_config_zone(origin, config: config)
  while seconds > 0
    # roll to next business period
    time = advance_to_working_time(time, config: config)
    # look at working ranges
    time_in_day = time.seconds_since_midnight
    working_hours_for(time, config: config).each do |from, to|
      if time_in_day >= from and time_in_day < to
        # take all we can
        take = [to - time_in_day, seconds].min
        # advance time
        time += take
        # decrease seconds
        seconds -= take
      end
    end
  end
  while seconds < 0
    # roll to previous business period
    time = return_to_exact_working_time(time, config: config)
    # look at working ranges
    time_in_day = time.seconds_since_midnight
    
    working_hours_for(time, config: config).reverse_each do |from, to|
      if time_in_day > from and time_in_day <= to
        # take all we can
        take = [time_in_day - from, -seconds].min
        # advance time
        time -= take
        # decrease seconds
        seconds += take
      end
    end
  end
  convert_to_original_format(time.round, origin)
end
advance_to_closing_time(time, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 94
def advance_to_closing_time time, config: nil
  config ||= wh_config
  time = in_config_zone(time, config: config)
  loop do
    # skip holidays and weekends
    while not working_day?(time, config: config)
      time = (time + 1.day).beginning_of_day
    end
    # find next working range after time
    time_in_day = time.seconds_since_midnight
    working_hours_for(time, config: config).each do |from, to|
      return move_time_of_day(time, to) if time_in_day < to
    end
    # if none is found, go to next day and loop
    time = (time + 1.day).beginning_of_day
  end
end
advance_to_working_time(time, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 74
def advance_to_working_time time, config: nil
  config ||= wh_config
  time = in_config_zone(time, config: config)
  loop do
    # skip holidays and weekends
    while not working_day?(time, config: config)
      time = (time + 1.day).beginning_of_day
    end
    # find first working range after time
    time_in_day = time.seconds_since_midnight
    
    working_hours_for(time, config: config).each do |from, to|
      return time if time_in_day >= from and time_in_day < to
      return move_time_of_day(time, from) if from >= time_in_day
    end
    # if none is found, go to next day and loop
    time = (time + 1.day).beginning_of_day
  end
end
in_working_hours?(time, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 151
def in_working_hours? time, config: nil
  config ||= wh_config
  time = in_config_zone(time, config: config)
  return false if not working_day?(time, config: config)
  time_in_day = time.seconds_since_midnight
  working_hours_for(time, config: config).each do |from, to|
    return true if time_in_day >= from and time_in_day < to
  end
  false
end
next_working_time(time, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 112
def next_working_time(time, config: nil)
  time = advance_to_closing_time(time, config: config) if in_working_hours?(time, config: config)
  advance_to_working_time(time, config: config)
end
return_to_exact_working_time(time, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 124
def return_to_exact_working_time time, config: nil
  config ||= wh_config
  time = in_config_zone(time, config: config)
  loop do
    # skip holidays and weekends
    while not working_day?(time, config: config)
      time = (time - 1.day).end_of_day
    end
    # find last working range before time
    time_in_day = time.seconds_since_midnight
    working_hours_for(time, config: config).reverse_each do |from, to|
      return time if time_in_day > from and time_in_day <= to
      return move_time_of_day(time, to) if to <= time_in_day
    end
    # if none is found, go to previous day and loop
    time = (time - 1.day).end_of_day
  end
end
return_to_working_time(time, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 117
def return_to_working_time(time, config: nil)
  # return_to_exact_working_time may return values with a high number of milliseconds,
  # this is necessary for the end of day hack, here we return a rounded value for the
  # public API
  return_to_exact_working_time(time, config: config).round
end
working_day?(time, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 143
def working_day? time, config: nil
  config ||= wh_config
  time = in_config_zone(time, config: config)

  (config[:working_hours][time.wday].present? && !config[:holidays].include?(time.to_date)) ||
    config[:holiday_hours].include?(time.to_date)
end
working_days_between(from, to, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 162
def working_days_between from, to, config: nil
  config ||= wh_config
  if to < from
    -working_days_between(to, from, config: config)
  else
    from = in_config_zone(from, config: config)
    to = in_config_zone(to, config: config)
    days = 0
    while from.to_date < to.to_date
      from += 1.day
      days += 1 if working_day?(from, config: config)
    end
    days
  end
end
working_time_between(from, to, config: nil) click to toggle source
# File lib/working_hours/computation.rb, line 178
def working_time_between from, to, config: nil
  config ||= wh_config
  if to < from
    -working_time_between(to, from, config: config)
  else
    from = advance_to_working_time(in_config_zone(from, config: config))
    to = in_config_zone(to, config: config)
    distance = 0
    while from < to
      from_was = from
      # look at working ranges
      time_in_day = from.seconds_since_midnight
      working_hours_for(from, config: config).each do |begins, ends|
        if time_in_day >= begins and time_in_day < ends
          if (to - from) > (ends - time_in_day)
            # take all the range and continue
            distance += (ends - time_in_day)
            from = move_time_of_day(from, ends)
          else
            # take only what's needed and stop
            distance += (to - from)
            from = to
          end
        end
      end
      # roll to next business period
      from = advance_to_working_time(from, config: config)
      raise "Invalid loop detected in working_time_between (from=#{from.iso8601(12)}, to=#{to.iso8601(12)}, distance=#{distance}, config=#{config}), please open an issue ;)" unless from > from_was
    end
    distance.round # round up to supress miliseconds introduced by 24:00 hack
  end
end

Private Instance Methods

convert_to_original_format(time, original) click to toggle source
# File lib/working_hours/computation.rb, line 245
def convert_to_original_format time, original
  case original
  when Date then time.to_date
  when DateTime then time.to_datetime
  else time
  end
end
in_config_zone(time, config: nil) click to toggle source

fix for ActiveRecord < 4, doesn't implement in_time_zone for Date

# File lib/working_hours/computation.rb, line 235
def in_config_zone time, config: nil
  if time.respond_to? :in_time_zone
    time.in_time_zone(config[:time_zone])
  elsif time.is_a? Date
    config[:time_zone].local(time.year, time.month, time.day)
  else
    raise TypeError.new("Can't convert #{time.class} to a Time")
  end
end
move_time_of_day(time, seconds) click to toggle source

Changes the time of the day to match given time (in seconds since midnight) preserving nanosecond prevision (rational number) and honoring time shifts

This replaces the previous implementation which was:

time.beginning_of_day + seconds

(because this one would shift hours during time shifts days)

# File lib/working_hours/computation.rb, line 219
def move_time_of_day time, seconds
  # return time.beginning_of_day + seconds
  hour = (seconds / 3600).to_i
  seconds %= 3600
  minutes = (seconds / 60).to_i
  seconds %= 60
  # sec/usec separation is required for ActiveSupport <= 5.1
  usec = ((seconds % 1) * 10**6)
  time.change(hour: hour, min: minutes, sec: seconds.to_i, usec: usec)
end
wh_config() click to toggle source
# File lib/working_hours/computation.rb, line 230
def wh_config
  WorkingHours::Config.precompiled
end
working_hours_for(time, config:) click to toggle source
# File lib/working_hours/computation.rb, line 253
def working_hours_for(time, config:)
  config[:holiday_hours][time.to_date] || config[:working_hours][time.wday]
end