lib/norm/attribute/interval.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Norm
  module Attribute
    class Interval
      extend Loading
      include Comparable

      YEAR, MONTH, DAY, HOUR, MINUTE, SECOND = 0..5

      attr_reader :years, :months, :days, :seconds

      def initialize(years, months, days, seconds)
        @years, @months, @days, @seconds =
          years.to_r, months.to_r, days.to_r, seconds.to_r
        normalize!
      end

      def to_s
        (word_parts + number_parts).join(' ')
      end

      def to_r
        # Interestingly, extracting epoch from an interval of a year yields a
        # number that accounts for an extra 1/4 days, but extracting it from a
        # month or day doesn't treat them as 1/12th of that number, or
        # 1/365.25th, respectively. I'm just following PostgreSQL's example,
        # here.
        [@years * 31557600, @months * 2592000, @days * 86400, @seconds].
          inject(&:+)
      end

      def to_i
        to_r.to_i
      end

      def to_f
        to_r.to_f
      end

      def <=>(other)
        Interval === other ? self.to_r <=> other.to_r : self.to_r <=> other
      end

      def eql?(other)
        self.class == other.class &&
          self.to_r == other.to_r
      end

      private

      def normalize!
        @years, remainder = extract_fraction @years
        @months += remainder / Rational(1, 12)
        @months, remainder = extract_fraction @months
        @days += remainder / Rational(1, 30)
        @days, remainder = extract_fraction @days
        @seconds += ((remainder / Rational(1, 24)) * 3600).round(6)
        @years, @months = @years + @months / 12, @months % 12
      end

      def extract_fraction(number)
        mod = number < 0 ? -1 : 1
        [(number / 1).to_i, Rational(number, 1) % mod]
      end

      def inflect(number, word)
        if number.abs == 1
          "#{number} #{word}"
        else
          "#{number} #{word}s"
        end
      end

      def word_parts
        @word_parts ||= [years_string, months_string, days_string].compact
      end

      def number_parts_sign
        @seconds < 0 ? '-' : ''
      end

      def number_parts
        @number_parts ||= if @seconds.zero? && word_parts.any?
                            []
                          else
                            [
                              sprintf(
                                "#{number_parts_sign}%02d:%02d:%02d.%06d",
                                abs_hours, abs_minutes, abs_seconds,
                                abs_seconds_remainder * 1000000
                              )
                            ]
                          end
      end

      def abs_hours
        (@seconds.abs / 3600).to_i
      end

      def abs_hours_remainder
        @seconds.abs % 3600
      end

      def abs_minutes
        (abs_hours_remainder / 60).to_i
      end

      def abs_minutes_remainder
        abs_hours_remainder % 60
      end

      def abs_seconds
        abs_minutes_remainder.to_i
      end

      def abs_seconds_remainder
        abs_minutes_remainder % 1
      end

      def years_string
        inflect(@years, 'year') if @years.nonzero?
      end

      def months_string
        inflect(@months, 'month') if @months.nonzero?
      end

      def days_string
        inflect(@days, 'day') if @days.nonzero?
      end


      class << self

        private

        def load_String(object, *args)
          parsed = Parser::Attribute::Interval.new(object)
          new(parsed.years, parsed.months, parsed.days, parsed.seconds)
        end

      end

    end

    def self.Interval(*)
      Interval
    end

  end
end