# delta.rb: Written by Tadayoshi Funaba 2004-2009

require 'date/delta/parser'

class Date

  class Delta

    include Comparable

    UNIT_PREFIXES = {
      'yotta'      => Rational(10**24),
      'zetta'      => Rational(10**21),
      'exa'        => Rational(10**18),
      'peta'       => Rational(10**15),
      'tera'       => Rational(10**12),
      'giga'       => Rational(10**9),
      'mega'       => Rational(10**6),
      'kilo'       => Rational(10**3),
      'hecto'      => Rational(10**2),
      'deca'       => Rational(10**1),
      'deka'       => Rational(10**1),
      'deci'       => Rational(1, 10**1),
      'centi'      => Rational(1, 10**2),
      'milli'      => Rational(1, 10**3),
      'decimilli'  => Rational(1, 10**4),
      'centimilli' => Rational(1, 10**5),
      'micro'      => Rational(1, 10**6),
      'nano'       => Rational(1, 10**9),
      'millimicro' => Rational(1, 10**9),
      'pico'       => Rational(1, 10**12),
      'micromicro' => Rational(1, 10**12),
      'femto'      => Rational(1, 10**15),
      'atto'       => Rational(1, 10**18),
      'zepto'      => Rational(1, 10**21),
      'yocto'      => Rational(1, 10**24)
    }

    IUNITS = {
      'year'       => Complex(0, 12),
      'month'      => Complex(0, 1)
    }

    RUNITS = {
      'day'        => Rational(1),
      'week'       => Rational(7),
      'sennight'   => Rational(7),
      'fortnight'  => Rational(14),
      'hour'       => Rational(1, 24),
      'minute'     => Rational(1, 1440),
      'second'     => Rational(1, 86400)
    }

    UNIT_PREFIXES.each do |k, v|
      RUNITS[k + 'second'] = v * RUNITS['second']
    end

    remove_const :UNIT_PREFIXES

    UNITS = {}

    IUNITS.each do |k, v|
      UNITS[k] = v
    end

    RUNITS.each do |k, v|
      UNITS[k] = v
    end

    UNITS4KEY = {}

    UNITS.each do |k, v|
      UNITS4KEY[k] = UNITS4KEY[k + 's'] = v
    end

    UNITS4KEY['y'] = UNITS4KEY['years']
    UNITS4KEY['yr'] = UNITS4KEY['years']
    UNITS4KEY['yrs'] = UNITS4KEY['years']
    UNITS4KEY['m'] = UNITS4KEY['months']
    UNITS4KEY['mo'] = UNITS4KEY['months']
    UNITS4KEY['mon'] = UNITS4KEY['months']
    UNITS4KEY['mnth'] = UNITS4KEY['months']
    UNITS4KEY['mnths'] = UNITS4KEY['months']
    UNITS4KEY['w'] = UNITS4KEY['weeks']
    UNITS4KEY['wk'] = UNITS4KEY['weeks']
    UNITS4KEY['d'] = UNITS4KEY['days']
    UNITS4KEY['dy'] = UNITS4KEY['days']
    UNITS4KEY['dys'] = UNITS4KEY['days']
    UNITS4KEY['h'] = UNITS4KEY['hours']
    UNITS4KEY['hr'] = UNITS4KEY['hours']
    UNITS4KEY['hrs'] = UNITS4KEY['hours']
    UNITS4KEY['min'] = UNITS4KEY['minutes']
    UNITS4KEY['mins'] = UNITS4KEY['minutes']
    UNITS4KEY['s'] = UNITS4KEY['seconds']
    UNITS4KEY['sec'] = UNITS4KEY['seconds']
    UNITS4KEY['secs'] = UNITS4KEY['seconds']
    UNITS4KEY['ms'] = UNITS4KEY['milliseconds']
    UNITS4KEY['msec'] = UNITS4KEY['milliseconds']
    UNITS4KEY['msecs'] = UNITS4KEY['milliseconds']
    UNITS4KEY['milli'] = UNITS4KEY['milliseconds']
    UNITS4KEY['us'] = UNITS4KEY['microseconds']
    UNITS4KEY['usec'] = UNITS4KEY['microseconds']
    UNITS4KEY['usecs'] = UNITS4KEY['microseconds']
    UNITS4KEY['micro'] = UNITS4KEY['microseconds']
    UNITS4KEY['ns'] = UNITS4KEY['nanoseconds']
    UNITS4KEY['nsec'] = UNITS4KEY['nanoseconds']
    UNITS4KEY['nsecs'] = UNITS4KEY['nanoseconds']
    UNITS4KEY['nano'] = UNITS4KEY['nanoseconds']

    def self.delta_to_dhms(delta)
      fr = delta.imag.abs
      y,   fr = fr.divmod(12)
      m,   fr = fr.divmod(1)

      if delta.imag < 0
	y = -y
	m = -m
      end

      fr = delta.real.abs
      ss,  fr = fr.divmod(SECONDS_IN_DAY) # 4p
      d,   ss = ss.divmod(86400)
      h,   ss = ss.divmod(3600)
      min, s  = ss.divmod(60)

      if delta.real < 0
	d = -d
	h = -h
	min = -min
	s = -s
      end

      return y, m, d, h, min, s, fr
    end

    def self.dhms_to_delta(y, m, d, h, min, s, fr)
      fr = 0 if fr == 0
      Complex(0, y.to_i * 12 + m.to_i) +
	Rational(d * 86400 + h * 3600 + min * 60 + (s + fr), 86400) # 4p
    end

    def initialize(delta)
      @delta = delta
      @__ca__ = {}
    end

    class << self; alias_method :new!, :new end

    def self.new(arg=0, h=0, min=0, s=0)
      if Hash === arg
	d = Complex(0)
	arg.each do |k, v|
	  k = k.to_s.downcase
	  unless UNITS4KEY[k]
	    raise ArgumentError, "unknown keyword #{k}"
	  end
	  d += v * UNITS4KEY[k]
	end
      else
	d = dhms_to_delta(0, 0, arg, h, min, s, 0)
      end
      new!(d)
    end

    UNITS.each_key do |k|
      module_eval <<-"end;"
	def self.#{k}s(n=1)
	  new(:d=>n * UNITS['#{k}'])
	end
      end;
    end

    class << self; alias_method :mins, :minutes end
    class << self; alias_method :secs, :seconds end

    def self.parse(str)
      d = begin (@@pa ||= Parser.new).parse(str)
	  rescue Racc::ParseError
	    raise ArgumentError, 'syntax error'
	  end
      new!(d)
    end

    def self.diff(d1, d2) new(d1.ajd - d2.ajd) end

    class << self

      def once(*ids) # :nodoc: -- restricted
	for id in ids
	  module_eval <<-"end;"
	    alias_method :__#{id.object_id}__, :#{id.to_s}
	    private :__#{id.object_id}__
	    def #{id.to_s}(*args)
	      @__ca__[#{id.object_id}] ||= __#{id.object_id}__(*args)
	    end
	  end;
        end
      end

      private :once

    end

    def dhms() self.class.delta_to_dhms(@delta) end

    once :dhms

    def delta() @delta end

    protected :delta

    def years() dhms[0] end
    def months() dhms[1] end
    def days() dhms[2] end
    def hours() dhms[3] end
    def minutes() dhms[4] end
    def seconds() dhms[5] end
    def second_fractions() dhms[6] end

    alias_method :mins, :minutes
    alias_method :secs, :seconds
    alias_method :sec_fractions, :second_fractions

    RUNITS.each_key do |k|
      module_eval <<-"end;"
	def in_#{k}s(u=1)
	  if @delta.imag != 0
	    raise ArgumentError, "#{k}: #{self} has month"
	  end
	  @delta.real / (u * RUNITS['#{k}'])
	end
      end;
    end

    alias_method :in_mins, :in_minutes
    alias_method :in_secs, :in_seconds

    def zero?() @delta.zero? end
    def nonzero?() unless zero? then self end end

    def integer? () @delta.imag == 0 && @delta.real.integer? end

    def -@ () self.class.new!(-@delta) end
    def +@ () self.class.new!(+@delta) end

    def dx_addsub(m, n)
      case n
      when Numeric; return self.class.new!(@delta.__send__(m, n))
      when Delta; return self.class.new!(@delta.__send__(m, n.delta))
      else
	l, r = n.coerce(self)
	return l.__send__(m, r)
      end
    end

    private :dx_addsub

    def + (n) dx_addsub(:+, n) end
    def - (n) dx_addsub(:-, n) end

    def dx_muldiv(m, n)
      case n
      when Numeric
	return self.class.new!(@delta.__send__(m, n))
      else
	l, r = n.coerce(self)
	return l.__send__(m, r)
      end
    end

    private :dx_muldiv

    def * (n) dx_muldiv(:*, n) end
    def / (n) dx_muldiv(:/, n) end

    def dx_conv1(m, n)
      if @delta.imag != 0
	raise ArgumentError, "#{m}: #{self} has month"
      end
      case n
      when Numeric
	return self.class.new!(Complex(@delta.real.__send__(m, n), 0))
      else
	l, r = n.coerce(self)
	return l.__send__(m, r)
      end
    end

    private :dx_conv1

    def % (n) dx_conv1(:%, n) end

    def div(n) dx_conv1(:div, n) end
    def modulo(n) dx_conv1(:modulo, n) end
    def divmod(n) [div(n), modulo(n)] end

    def quotient(n)
      if @delta.imag != 0
	raise ArgumentError, "quotient: #{self} has month"
      end
      case n
      when Numeric
	return self.class.new!(Complex((@delta.real / n).truncate))
      else
	l, r = n.coerce(self)
	return l.__send__(m, r)
      end
    end

    def remainder(n) dx_conv1(:remainder, n) end
    def quotrem(n) [quotient(n), remainder(n)] end

    def ** (n) dx_conv1(:**, n) end
    def quo(n) dx_muldiv(:quo, n) end

    def <=> (other)
      if @delta.imag != 0
	raise ArgumentError, "<=>: #{self} has month"
      end
      case other
      when Numeric; return @delta.real <=> other
      when Delta;   return @delta.real <=> other.delta.real
      else
	begin
	  l, r = other.coerce(self)
	  return l <=> r
	rescue NoMethodError
	end
      end
      nil
    end

    def == (other)
      case other
      when Numeric; return @delta == other
      when Delta;   return @delta == other
      else
	begin
	  l, r = other.coerce(self)
	  return l == r
	rescue NoMethodError
	end
      end
      nil
    end

    def coerce(other)
      case other
      when Numeric; return other, @delta
      else
	super
      end
    end

    def eql? (other) Delta === other && self == other end
    def hash() @delta.hash end

    def dx_conv0(m)
      if @delta.imag != 0
	raise ArgumentError, "#{m}: #{self} has month"
      end
      @delta.real.__send__(m)
    end

    private :dx_conv0

    def abs() dx_conv0(:abs) end

    def ceil() dx_conv0(:ceil) end
    def floor() dx_conv0(:floor) end
    def round() dx_conv0(:round) end
    def truncate() dx_conv0(:truncate) end

    def to_i() dx_conv0(:to_i) end
    def to_f() dx_conv0(:to_f) end
    def to_r() dx_conv0(:to_r) end
    def to_c() @delta end

    alias_method :to_int, :to_i

    def inspect() format('#<%s: %s (%s)>', self.class, to_s, @delta) end

    def to_s
      format(%(%s(%dd %.02d:%02d'%02d"%03d)%s(%dy %dm)), # '
	     if @delta.real < 0 then '-' else '+' end,
	     days.abs, hours.abs, mins.abs, secs.abs, sec_fractions.abs * 1000,
	     if @delta.imag < 0 then '-' else '+' end,
	     years.abs, months.abs)
    end

    def marshal_dump() @delta end

    def marshal_load(a)
      @delta = a
      @__ca__ = {}
    end

  end

end
