VerdigrisTech/green-button-data

View on GitHub
lib/green-button-data/dst.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module GreenButtonData
  module Dst
    include Utilities

    # From ESPI XML schema:
    # [extension] Bit map encoded rule from which is calculated the start or
    # end time, within the current year, to which daylight savings time offset
    # must be applied.
    #
    # The rule encoding:
    # Bits  0 - 11: seconds 0 - 3599
    # Bits 12 - 16: hours 0 - 23
    # Bits 17 - 19: day of the week 0 = not applicable, 1 - 7 (Monday = 1)
    # Bits:20 - 24: day of the month 0 = not applicable, 1 - 31
    # Bits: 25 - 27: operator  (detailed below)
    # Bits: 28 - 31: month 1 - 12
    #
    # Rule value of 0xFFFFFFFF means rule processing/DST correction is disabled.
    #
    # The operators:
    #
    # 0: DST starts/ends on the Day of the Month
    # 1: DST starts/ends on the Day of the Week that is on or after the Day of the Month
    # 2: DST starts/ends on the first occurrence of the Day of the Week in a month
    # 3: DST starts/ends on the second occurrence of the Day of the Week in a month
    # 4: DST starts/ends on the third occurrence of the Day of the Week in a month
    # 5: DST starts/ends on the forth occurrence of the Day of the Week in a month
    # 6: DST starts/ends on the fifth occurrence of the Day of the Week in a month
    # 7: DST starts/ends on the last occurrence of the Day of the Week in a month
    #
    # An example: DST starts on third Friday in March at 1:45 AM.  The rule...
    # Seconds: 2700
    # Hours: 1
    # Day of Week: 5
    # Day of Month: 0
    # Operator: 4
    # Month: 3
    BITMASK_SECOND = 0x00000fff
    BITMASK_HOUR = 0x0001f000
    BITMASK_DAY_OF_WEEK = 0x000e0000
    BITMASK_DAY_OF_MONTH = 0x01f00000
    BITMASK_DST_RULE = 0x0e000000
    BITMASK_MONTH = 0xf0000000

    BITSHIFT_HOUR = 12
    BITSHIFT_DAY_OF_WEEK = 17
    BITSHIFT_DAY_OF_MONTH = 20
    BITSHIFT_DST_RULE = 25
    BITSHIFT_MONTH = 28

    def byte_to_dst_datetime(byte, year = Time.now.year)
      # Bits 0 - 11: seconds 0 - 3599
      seconds = byte & BITMASK_SECOND

      # Bits 12 - 16: hours 0 - 23
      hour = (byte & BITMASK_HOUR) >> BITSHIFT_HOUR

      # Bits 17 - 19: day of the week; 0 = NA, 1 - 7 (Monday = 1)
      weekday = (byte & BITMASK_DAY_OF_WEEK) >> BITSHIFT_DAY_OF_WEEK

      # Bits 20 - 24: day of the month; 0 = NA, 1 - 31
      day = (byte & BITMASK_DAY_OF_MONTH) >> BITSHIFT_DAY_OF_MONTH

      # Bits 25 - 27: DST rule 0 - 7
      dst_rule = (byte & BITMASK_DST_RULE) >> BITSHIFT_DST_RULE

      # Bits 28 - 31: month 1 - 12
      month = (byte & BITMASK_MONTH) >> BITSHIFT_MONTH

      # Raise an error unless all the values are in valid range
      validate_dst_rules dst_rule, month, weekday, day, hour, seconds

      # In Ruby, Sunday = 0 not 7
      weekday = weekday == 7 ? 0 : weekday

      # Add the hour and seconds component to the day
      dst_datetime dst_rule, year, month, weekday, day, hour, seconds
    end

    def dst_datetime(dst_rule, year, month, weekday, day, hour, seconds)
      if dst_rule == 1
        # Rule 1: DST starts/ends on Day of Week on or after the Day of Month
        day_of_month = DateTime.new year, month, day
        day_offset = if weekday >= day_of_month.wday
          weekday - day_of_month.wday
        else
          7 + weekday - day_of_month.wday
        end

        day_of_month + day_offset
      elsif dst_rule.between?(2, 6)
        # Rule 2 - 6: DST starts/ends on Nth Day of Week in given month
        # Nth Day of Week (e.g. third Friday of July)
        nth_weekday_of year, month, weekday, dst_rule - 1
      elsif dst_rule == 7
        # Rule 7: DST starts/ends on last Day of Week in given month
        last_weekday_of year, month, weekday
      else
        # Rule 0: DST starts/ends on the Day of Month
        DateTime.new year, month, day
      end + Rational(hour * 3600 + seconds, 86400)
    end

    def validate_dst_rules(dst_rule, month, weekday, day, hour, seconds)
      seconds.between?(0, 3599) and hour.between?(0, 23) and
      weekday.between?(1, 7) and day.between?(0, 31) and
      dst_rule.between?(0, 7) and month.between?(1, 12) or
      raise RangeError, 'Invalid value range'
    end
  end
end