sealink/timely

View on GitHub
lib/timely/week_days.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

module Timely
  class WeekDays
    WEEKDAY_KEYS = %i[sun mon tue wed thu fri sat].freeze

    def self.from_range(date_range)
      dates = Array(date_range)
      return ALL_WEEKDAYS if dates.count >= WEEKDAY_KEYS.count

      new(dates.each_with_object({}) do |date, result|
        # e.g. {3: true, 5: true}
        result[date.to_date.wday] = true
      end)
    end

    # Create a new Weekdays object
    # weekdays can be in three formats
    # integer representing binary string
    #   e.g. 1 = Sun, 2 = Mon, 3 = Sun + Mon, etc.
    # hash with symbol keys for :sun, :mon, ... with true/false values
    #   e.g. {:sun => true, :tue => true} is Sunday and Tuesday
    #   Not passing in values is the same as setting them explicitly to true
    # array with true/false values from sun to sat
    #   e.g. [1, 0, 1, 0, 0, 0, 0] is Sunday and Tuesday
    def initialize(weekdays)
      @weekdays = {
        sun: false,
        mon: false,
        tue: false,
        wed: false,
        thu: false,
        fri: false,
        sat: false
      }

      case weekdays
      when Integer
        # 4 -> 0000100 (binary) -> "0010000" (reversed string) -> {:tue => true}
        weekdays.to_s(2).reverse.each_char.with_index do |char, index|
          set_day(index, char == '1')
        end
      when Hash
        weekdays.each_pair do |day, value|
          set_day(day, value)
        end
      when Array
        weekdays.each.with_index do |value, index|
          set_day(index, value)
        end
      when NilClass
        @weekdays = {
          sun: true,
          mon: true,
          tue: true,
          wed: true,
          thu: true,
          fri: true,
          sat: true
        }
      else
        raise ArgumentError,
              'You must initialize with an Integer, Hash or Array'
      end
    end

    # Set the weekday on or off
    # weekdays[:mon] = true
    # weekdays[:tue] = false
    # set ideally will be true, but 'true', 1 and '1' are accepted
    # All other values will be treated as false
    def []=(day, set)
      set_day(day, set)
    end

    def set_day(day, set)
      key = day_to_index(day)
      raise ArgumentError, "Invalid week day index #{key}" unless WEEKDAY_KEYS.include?(key)

      @weekdays[key] = [true, 'true', 1, '1'].include?(set)
    end

    def applies_for_date?(date)
      has_day?(date.wday)
    end

    # Determine if weekdays has day selected
    # Accepts either symbol or integer
    # e.g. :sun or 0 = Sunday, :sat or 6 = Saturday
    def has_day?(weekday)
      weekday = WEEKDAY_KEYS[weekday] if weekday.is_a?(Integer)
      @weekdays[weekday]
    end

    def number_of_occurances_in(range)
      range.inject(0) do |count, date|
        applies_for_date?(date) ? count + 1 : count
      end
    end

    # Returns true if all days are selected
    def all_days?
      @weekdays.all? { |_day, day_selected| day_selected }
    end

    # Returns array of weekday selected
    # e.g. [:sun, :sat]
    def weekdays
      @weekdays.select { |_day, day_selected| day_selected }.keys
    end

    # Returns comma separated and capitalized in Sun-Sat order
    # e.g. 'Mon, Tue, Wed' or 'Sat' or 'Sun, Sat'
    def to_s
      days = weekdays.map { |day| day.to_s.capitalize }
      last_day = days.pop

      days.empty? ? last_day : days.join(', ') + ', and ' + last_day
    end

    # 7 bits encoded in decimal number
    # 0th bit = Sunday, 6th bit = Saturday
    # Value of 127 => all days are on
    def weekdays_int
      int = 0
      WEEKDAY_KEYS.each.with_index do |day, index|
        int += 2**index if @weekdays[day]
      end
      int
    end

    private

    def day_to_index(day)
      if day.is_a?(Integer)
        WEEKDAY_KEYS[day]
      elsif day.is_a?(String)
        day.to_sym
      else
        day
      end
    end

    ALL_WEEKDAYS = WeekDays.new(%w[1 1 1 1 1 1 1]).freeze
  end
end