pboling/month-serializer

View on GitHub
lib/month/serializer.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'month/serializer/version'

# Eternal Gems
require 'month'

class Month
  module Serializer
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def load(month_as_integer)
        raise_load_error(month_as_integer) unless month_as_integer.is_a?(Integer)
        year, number = month_as_integer.divmod(12)
        # A zero month actually points to December of the previous year,
        #   because when dividing by 12, possible remainders are 0 - 11
        #   and we need months 1 - 12
        #
        # NOTE ON ZERO:
        #
        # There is no Year Zero in the anno domini system.
        # See: https://en.wikipedia.org/wiki/Year_zero
        #
        # But ISO 8601:2004 does have a year zero, and it coincides with Gregorian year 1 BC.
        # See: https://en.wikipedia.org/wiki/ISO_8601
        #
        # And what about Ruby?
        #
        # >> Date.new(0, 1, 1)
        # => #<Date: 0000-01-01 ((1721058j,0s,0n),+0s,2299161j)>
        # >> Date.new(0, 1, 1) << 1
        # => #<Date: -0001-12-01 ((1721027j,0s,0n),+0s,2299161j)>
        # >> Date.new(0, 1, 1) >> 12
        # => #<Date: 0001-01-01 ((1721424j,0s,0n),+0s,2299161j)>
        # >> Date.new(2018, 1, 1) >> 12
        # => #<Date: 2019-01-01 ((2458485j,0s,0n),+0s,2299161j)>
        #
        # We want a line of integers that represent months that are unbroken
        #   steps of distance 1, so we have to have a Month 0
        #
        # Month -24 is December, Year -3  # -24.divmod(12) => [-2, 0]   # year -= 1, number = 12
        # Month -23 is January, Year -2   # -23.divmod(12) => [-2, 1]   # no mod
        # Month -12 is December, Year -2  # -12.divmod(12) => [-1, 0]   # year -= 1, number = 12
        # Month -11 is January, Year -1   # -11.divmod(12) => [-1, 1]   # no mod
        # Month -10 is February, Year -1  # -10.divmod(12) => [-1, 2]   # no mod
        # Month -2 is October, Year -1    # -2.divmod(12) => [-1, 10]   # no mod
        # Month -1 is November, Year -1   # -1.divmod(12) => [-1, 11]   # no mod
        # Month 0 is December, Year -1    # 0.divmod(12) => [0, 0]      # year -= 1, number = 12
        # Month 1 is January, Year 0      # 1.divmod(12) => [0, 1]      # no mod
        # Month 2 is February, Year 0     # 2.divmod(12) => [0, 2]      # no mod
        # Month 11 is November, Year 0    # 11.divmod(12) => [0, 11]    # no mod
        # Month 12 is December, Year 0    # 12.divmod(12) => [1, 0]     # year -= 1, number = 12
        # Month 13 is January, Year 1     # 13.divmod(12) => [1, 1]     # no mod
        # Month 23 is November, Year 1    # 23.divmod(12) => [1, 11]    # no mod
        # Month 24 is December, Year 1    # 24.divmod(12) => [2, 0]     # year -= 1, number = 12
        # Month 25 is January, Year 2     # 25.divmod(12) => [2, 1]     # no mod
        if number.zero?
          number = 12
          year -= 1
        end
        self.new(year, number)
      end

      def dump(month)
        raise_dump_error(month) unless month.is_a?(self)
        month.to_i
      end

      private

      def active_record?
        Object.const_defined?("::ActiveRecord::SerializationTypeMismatch")
      end

      def argument_error_class
        active_record? ? ::ActiveRecord::SerializationTypeMismatch : ArgumentError
      end

      def raise_load_error(obj)
        raise argument_error_class, "Argument was supposed to be an Integer, but was a #{obj.class}. -- #{obj.inspect}"
      end

      def raise_dump_error(obj)
        raise argument_error_class, "Argument was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
      end
    end
  end

  def to_i
    yr = year
    mon = number
    if number == 12
      mon = 0
      yr += 1
    end
    yr * 12 + mon
  end

  # Does the same thing as Month#start_date, but to_<class> is a common idiom
  #   for built-in class conversion methods.
  def to_date
    Date.new(year, number, 1)
  end
end

# Add this to the bootstrapping process of your app, somewhere after Month is loaded.
# In Rails, config/initializers/month-serializer.rb would be perfect!
# Month.send(:include, Month::Serializer)