tandusrl/acts_as_bookable

View on GitHub
lib/acts_as_bookable/time_utils.rb

Summary

Maintainability
B
5 hrs
Test Coverage
module ActsAsBookable
  ##
  # Provide helper functions to manage operations and queries related to times
  # and schedules
  #
  module TimeUtils
    class << self
      ##
      # Check if time is included in a time interval. The ending time is excluded
      #
      # @param time The time to check
      # @param interval_start The beginning time of the interval to match against
      # @param interval_end The ending time of the interval to match against
      #
      def time_in_interval? (time, interval_start, interval_end)
        time >= interval_start && time < interval_end
      end

      ##
      # Check if there is an occurrence of a schedule that contains a time interval
      #
      # @param schedule The schedule
      # @param interval_start The beginning Time of the interval
      # @param interval_end The ending Time of the interval
      # @return true if the interval falls within an occurrence of the schedule, otherwise false
      #
      def interval_in_schedule?(schedule, interval_start, interval_end)
        # Check if interval_start and interval_end falls within any occurrence
        return false if(!time_in_schedule?(schedule,interval_start) || !time_in_schedule?(schedule,interval_end))

        # Check if both interval_start and interval_end falls within the SAME occurrence
        between = schedule.occurrences_between(interval_start, interval_end, true)
        contains = false
        between.each do |oc|
          oc_end = oc + schedule.duration
          contains = true if (time_in_interval?(interval_start,oc,oc_end) && time_in_interval?(interval_end,oc,oc_end))
          break if contains
        end

        contains
      end

      ##
      # Check if there is an occurrence of a schedule that contains a time
      # @param schedule The schedule
      # @param time The time
      # @return true if the time falls within an occurrence of the schedule, otherwise false
      #
      def time_in_schedule?(schedule, time)
        return schedule.occurring_at? time
      end

      ##
      # Returns an array of sub-intervals given another array of intervals, which are the overlapping insersections of each-others.
      #
      # @param intervals an array of intervals
      # @return an array of subintervals, sorted by time_start
      #
      # An interval is defined as a hash with at least the following fields: `time_from` and `time_end`. An interval may contain more
      # fields. In that case, it's suggested to give a block with the instructions to correctly merge two intervals when needed.
      #
      # e.g: given these 7 intervals
      #   |------|    |---|       |----------|
      #      |---|          |--|
      #      |------|       |--|      |-------------|
      #   the output is an array containing these 8 intervals:
      #   |--|   |--| |---| |--|  |---|      |------|
      #      |---|                    |------|
      #   the number of subintervals may increase or decrease because some intervals may be split, while
      #   some others may be merged.
      #
      # If a block is given, it's called before merging two intervals. The block should provide instructions to merge intervals, and should return the merged fields in a hash
      def subintervals(intervals, &block)
        raise ArgumentError.new('intervals must be an array') unless intervals.is_a? Array

        steps = [] # Steps will be extracted from intervals
        subintervals = [] # The output
        last_time = nil
        last_attrs = nil
        started_count = 0 # The number of intervals opened inside the cycle

        # Extract start times and end times from intervals, and create steps
        intervals.each do |el|
          begin
            ts = el[:time_start].to_time
            te = el[:time_end].to_time
          rescue NoMethodError
            raise ArgumentError.new('intervals must define :time_start and :time_end as Time or Date')
          end
          attrs = el.clone
          attrs.delete(:time_start)
          attrs.delete(:time_end)
          steps << { opening: 1, time: el[:time_start], attrs: attrs } # Start step
          steps << { opening: -1, time: el[:time_end], attrs: attrs.clone } # End step
        end

        # Sort steps by time (and opening if time is the same)
        steps.sort! do |a,b|
          diff = a[:time] <=> b[:time]
          diff = a[:opening] <=> b[:opening] if (diff == 0)
          diff
        end

        # Iterate over steps
        steps.each do |step|
          if (started_count == 0)
            last_time = step[:time]
            last_attrs = step[:attrs]
          else
            if(step[:time] > last_time)
              subintervals << ({
                time_start: last_time,
                time_end: step[:time]
              }.merge(last_attrs))

              last_time = step[:time]
            end

            if block_given?
              last_attrs = block.call(last_attrs.clone, step[:attrs],(step[:opening] == 1 ? :open : :close))
            else
              last_attrs = step[:attrs]
            end
          end

          # Update started_count
          started_count += step[:opening]
        end

        subintervals
      end

    end
  end
end