lib/aixm/schedule/day.rb

Summary

Maintainability
A
0 mins
Test Coverage
using AIXM::Refinements

module AIXM
  module Schedule

    # Days suitable for schedules
    #
    # @example
    #   from = AIXM.day(:monday)                   # => :monday
    #   to = AIXM.day(4)                           # => :thursday
    #   AIXM.day(:tuesday).covered_by?(from..to)   # => true
    class Day
      include AIXM::Concerns::HashEquality
      include Comparable

      WEEKDAYS = %i(sunday monday tuesday wednesday thursday friday saturday).freeze
      DAYS =  (WEEKDAYS + %i(workday day_preceding_workday day_following_workday holiday day_preceding_holiday day_following_holiday any)).freeze

      # Day of the week or special named day
      #
      # @return [Symbol] any from {DAYS}
      attr_reader :day

      # Set the given day of the week or special named day.
      #
      # @param day [Symbol, String, Integer] any from {DAYS} or 0=Monday to
      #   6=Sunday
      def initialize(day=:any)
        case day
        when Symbol, String
          self.day = day
        when Integer
          fail ArgumentError unless day.between?(0, 6)
          self.day = WEEKDAYS[day]
        else
          fail ArgumentError
        end
      end

      # Human readable representation such as "monday" or "day preceding workday"
      #
      # @return [String]
      def to_s
        day.to_s.gsub('_', ' ')
      end

      # Symbol used to initialize this day
      #
      # @return [Symbol]
      def to_sym
        day.to_s.to_sym
      end

      def inspect
        %Q(#<#{self.class} #{to_s}>)
      end

      # Create new day one day prior to this one.
      #
      # @return [AIXM::Schedule::Day]
      def pred
        return self if any?
        fail(TypeError, "can't iterate from #{day}") unless wday
        self.class.new(WEEKDAYS[wday.pred % 7])
      end
      alias_method :prev, :pred

      # Create new day one day after this one.
      #
      # @return [AIXM::Schedule::Day]
      def succ
        return self if any?
        fail(TypeError, "can't iterate from #{day}") unless wday
        self.class.new(WEEKDAYS[wday.succ % 7])
      end
      alias_method :next, :succ

      # Whether two days are equal.
      #
      # @return [Boolean]
      def ==(other)
        day == other.day
      end

      # @see Object#hash
      def hash
        [self.class, day].hash
      end

      # Whether the day is set to :any
      #
      # @return [Boolean]
      def any?
        day == :any
      end

      # Whether this schedule day sortable.
      #
      # @return [Boolean]
      def sortable?
        WEEKDAYS.include? day
      end

      # Whether this schedule day falls within the given range of schedule
      # days.
      #
      # @note Only weekdays and +:any+ can be computed!
      #
      # @param other [AIXM::Schedule::Day, Range<AIXM::Schedule::Day>] single
      #   schedule day or range of schedule days
      # @raise RuntimeError if anything but workdays or +:any+ are involved
      # @return [Boolean]
      def covered_by?(other)
        range = Range.from other
        case
        when any? || range.first.any? || range.last.any?
          true
        when !sortable? || !range.first.sortable? || !range.last.sortable?
          fail "includes unsortables"
        when range.min
          range.cover? self
        else
          range.first <= self || self <= range.last
        end
      end

      private

      def day=(value)
        @day = value.to_s.to_sym
        fail ArgumentError unless DAYS.include? @day
      end

      def wday
        WEEKDAYS.index(day.to_sym)
      end

      # @note Necessary to use this class in Range.
      def <=>(other)
        DAYS.index(day) <=> DAYS.index(other.day) || day.to_s <=> other.to_s
      end
    end

  end
end