date.rb   [plain text]


# date.rb: Written by Tadayoshi Funaba 1998-2002
# $Id: date.rb,v 1.1.1.2 2003/10/15 10:11:48 melville Exp $

require 'rational'
require 'date/format'

class Date

  include Comparable

  MONTHNAMES = [nil] + %w(January February March April May June July
			  August September October November December)

  DAYNAMES = %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)

  ABBR_MONTHNAMES = [nil] + %w(Jan Feb Mar Apr May Jun
			       Jul Aug Sep Oct Nov Dec)

  ABBR_DAYNAMES = %w(Sun Mon Tue Wed Thu Fri Sat)

  ITALY     = 2299161 # 1582-10-15
  ENGLAND   = 2361222 # 1752-09-14
  JULIAN    = false
  GREGORIAN = true

  def self.os? (jd, sg)
    case sg
    when Numeric; jd < sg
    else;         not sg
    end
  end

  def self.ns? (jd, sg) not os?(jd, sg) end

  def self.civil_to_jd(y, m, d, sg=GREGORIAN)
    if m <= 2
      y -= 1
      m += 12
    end
    a = (y / 100.0).floor
    b = 2 - a + (a / 4.0).floor
    jd = (365.25 * (y + 4716)).floor +
      (30.6001 * (m + 1)).floor +
      d + b - 1524
    if os?(jd, sg)
      jd -= b
    end
    jd
  end

  def self.jd_to_civil(jd, sg=GREGORIAN)
    if os?(jd, sg)
      a = jd
    else
      x = ((jd - 1867216.25) / 36524.25).floor
      a = jd + 1 + x - (x / 4.0).floor
    end
    b = a + 1524
    c = ((b - 122.1) / 365.25).floor
    d = (365.25 * c).floor
    e = ((b - d) / 30.6001).floor
    dom = b - d - (30.6001 * e).floor
    if e <= 13
      m = e - 1
      y = c - 4716
    else
      m = e - 13
      y = c - 4715
    end
    return y, m, dom
  end

  def self.ordinal_to_jd(y, d, sg=GREGORIAN)
    civil_to_jd(y, 1, d, sg)
  end

  def self.jd_to_ordinal(jd, sg=GREGORIAN)
    y = jd_to_civil(jd, sg)[0]
    doy = jd - civil_to_jd(y - 1, 12, 31, ns?(jd, sg))
    return y, doy
  end

  def self.jd_to_commercial(jd, sg=GREGORIAN)
    ns = ns?(jd, sg)
    a = jd_to_civil(jd - 3, ns)[0]
    y = if jd >= commercial_to_jd(a + 1, 1, 1, ns) then a + 1 else a end
    w = 1 + (jd - commercial_to_jd(y, 1, 1, ns)) / 7
    d = (jd + 1) % 7
    if d.zero? then d = 7 end
    return y, w, d
  end

  def self.commercial_to_jd(y, w, d, ns=GREGORIAN)
    jd = civil_to_jd(y, 1, 4, ns)
    (jd - (((jd - 1) + 1) % 7)) +
      7 * (w - 1) +
      (d - 1)
  end

  %w(self.clfloor clfloor).each do |name|
    module_eval <<-"end;"
      def #{name}(x, y=1)
	q, r = x.divmod(y)
	q = q.to_i
	return q, r
      end
    end;
  end

  private_class_method :clfloor
  private              :clfloor

  def self.ajd_to_jd(ajd, of=0) clfloor(ajd + of + 1.to_r/2) end
  def self.jd_to_ajd(jd, fr, of=0) jd + fr - of - 1.to_r/2 end

  def self.day_fraction_to_time(fr)
    h,   fr = clfloor(fr, 1.to_r/24)
    min, fr = clfloor(fr, 1.to_r/1440)
    s,   fr = clfloor(fr, 1.to_r/86400)
    return h, min, s, fr
  end

  def self.time_to_day_fraction(h, min, s)
    h.to_r/24 + min.to_r/1440 + s.to_r/86400
  end

  def self.amjd_to_ajd(amjd) amjd + 4800001.to_r/2 end
  def self.ajd_to_amjd(ajd) ajd - 4800001.to_r/2 end
  def self.mjd_to_jd(mjd) mjd + 2400001 end
  def self.jd_to_mjd(jd) jd - 2400001 end
  def self.ld_to_jd(ld) ld + 2299160 end
  def self.jd_to_ld(jd) jd - 2299160 end

  def self.jd_to_wday(jd) (jd + 1) % 7 end

  def self.julian_leap? (y) y % 4 == 0 end
  def self.gregorian_leap? (y) y % 4 == 0 and y % 100 != 0 or y % 400 == 0 end

  class << self; alias_method :leap?, :gregorian_leap? end
  class << self; alias_method :new0, :new end

  def self.valid_jd? (jd, sg=ITALY) jd end

  def self.jd(jd=0, sg=ITALY)
    jd = valid_jd?(jd, sg)
    new0(jd_to_ajd(jd, 0, 0), 0, sg)
  end

  def self.valid_ordinal? (y, d, sg=ITALY)
    if d < 0
      ny, = clfloor(y + 1, 1)
      jd = ordinal_to_jd(ny, d + 1, sg)
      ns = ns?(jd, sg)
      return unless [y] == jd_to_ordinal(jd, sg)[0..0]
      return unless [ny, 1] == jd_to_ordinal(jd - d, ns)
    else
      jd = ordinal_to_jd(y, d, sg)
      return unless [y, d] == jd_to_ordinal(jd, sg)
    end
    jd
  end

  def self.ordinal(y=-4712, d=1, sg=ITALY)
    unless jd = valid_ordinal?(y, d, sg)
      raise ArgumentError, 'invalid date'
    end
    new0(jd_to_ajd(jd, 0, 0), 0, sg)
  end

  def self.valid_civil? (y, m, d, sg=ITALY)
    if m < 0
      m += 13
    end
    if d < 0
      ny, nm = clfloor(y * 12 + m, 12)
      nm,    = clfloor(nm + 1, 1)
      jd = civil_to_jd(ny, nm, d + 1, sg)
      ns = ns?(jd, sg)
      return unless [y, m] == jd_to_civil(jd, sg)[0..1]
      return unless [ny, nm, 1] == jd_to_civil(jd - d, ns)
    else
      jd = civil_to_jd(y, m, d, sg)
      return unless [y, m, d] == jd_to_civil(jd, sg)
    end
    jd
  end

  class << self; alias_method :valid_date?, :valid_civil? end

  def self.civil(y=-4712, m=1, d=1, sg=ITALY)
    unless jd = valid_civil?(y, m, d, sg)
      raise ArgumentError, 'invalid date'
    end
    new0(jd_to_ajd(jd, 0, 0), 0, sg)
  end

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

  def self.valid_commercial? (y, w, d, sg=ITALY)
    if d < 0
      d += 8
    end
    if w < 0
      w = jd_to_commercial(commercial_to_jd(y + 1, 1, 1) + w * 7)[1]
    end
    jd = commercial_to_jd(y, w, d)
    return unless ns?(jd, sg)
    return unless [y, w, d] == jd_to_commercial(jd)
    jd
  end

  def self.commercial(y=1582, w=41, d=5, sg=ITALY)
    unless jd = valid_commercial?(y, w, d, sg)
      raise ArgumentError, 'invalid date'
    end
    new0(jd_to_ajd(jd, 0, 0), 0, sg)
  end

  def self.new_with_hash(elem, sg)
    elem ||= {}
    y, m, d = elem.values_at(:year, :mon, :mday)
    if [y, m, d].include? nil
      raise ArgumentError, 'invalid date'
    else
      civil(y, m, d, sg)
    end
  end

  private_class_method :new_with_hash

  def self.strptime(str='-4712-01-01', fmt='%F', sg=ITALY)
    elem = _strptime(str, fmt)
    new_with_hash(elem, sg)
  end

  def self.parse(str='-4712-01-01', comp=false, sg=ITALY)
    elem = _parse(str, comp)
    new_with_hash(elem, sg)
  end

  def self.today(sg=ITALY)
    jd = civil_to_jd(*(Time.now.to_a[3..5].reverse << sg))
    new0(jd_to_ajd(jd, 0, 0), 0, sg)
  end

  class << self

    def once(*ids)
      for id in ids
	module_eval <<-"end;"
	  alias_method :__#{id.to_i}__, :#{id.to_s}
	  private :__#{id.to_i}__
	  def #{id.to_s}(*args, &block)
	    (@__#{id.to_i}__ ||= [__#{id.to_i}__(*args, &block)])[0]
	  end
	end;
      end
    end

    private :once

  end

  def initialize(ajd=0, of=0, sg=ITALY) @ajd, @of, @sg = ajd, of, sg end

  def ajd() @ajd end
  def amjd() self.class.ajd_to_amjd(@ajd) end

  once :amjd

  def jd() self.class.ajd_to_jd(@ajd, @of)[0] end
  def day_fraction() self.class.ajd_to_jd(@ajd, @of)[1] end
  def mjd() self.class.jd_to_mjd(jd) end
  def ld() self.class.jd_to_ld(jd) end

  once :jd, :day_fraction, :mjd, :ld

  def civil() self.class.jd_to_civil(jd, @sg) end
  def ordinal() self.class.jd_to_ordinal(jd, @sg) end
  def commercial() self.class.jd_to_commercial(jd, @sg) end

  once :civil, :ordinal, :commercial
  private :civil, :ordinal, :commercial

  def year() civil[0] end
  def yday() ordinal[1] end
  def mon() civil[1] end
  def mday() civil[2] end

  alias_method :month, :mon
  alias_method :day, :mday

  def time() self.class.day_fraction_to_time(day_fraction) end

  once :time
  private :time

  def hour() time[0] end
  def min() time[1] end
  def sec() time[2] end
  def sec_fraction() time[3] end

  private :hour, :min, :sec, :sec_fraction

  def zone
    ['Z',
      format('%+.2d%02d',
	     (@of     / (1.to_r/24)).to_i,
	     (@of.abs % (1.to_r/24) / (1.to_r/1440)).to_i)
    ][@of<=>0]
  end

  private :zone

  def cwyear() commercial[0] end
  def cweek() commercial[1] end
  def cwday() commercial[2] end

  def wday() self.class.jd_to_wday(jd) end

  once :wday

  def os? () self.class.os?(jd, @sg) end
  def ns? () self.class.ns?(jd, @sg) end

  once :os?, :ns?

  def leap?
    self.class.jd_to_civil(self.class.civil_to_jd(year, 3, 1, ns?) - 1,
		     ns?)[-1] == 29
  end

  once :leap?

  def start() @sg end
  def new_start(sg=self.class::ITALY) self.class.new0(@ajd, @of, sg) end

  def italy() new_start(self.class::ITALY) end
  def england() new_start(self.class::ENGLAND) end
  def julian() new_start(self.class::JULIAN) end
  def gregorian() new_start(self.class::GREGORIAN) end

  def offset() @of end
  def new_offset(of=0) self.class.new0(@ajd, of, @sg) end

  private :offset, :new_offset

  def + (n)
    case n
    when Numeric; return self.class.new0(@ajd + n, @of, @sg)
    end
    raise TypeError, 'expected numeric'
  end

  def - (x)
    case x
    when Numeric; return self.class.new0(@ajd - x, @of, @sg)
    when Date;    return @ajd - x.ajd
    end
    raise TypeError, 'expected numeric or date'
  end

  def <=> (other)
    case other
    when Numeric; return @ajd <=> other
    when Date;    return @ajd <=> other.ajd
    end
    nil
  end

  def === (other)
    case other
    when Numeric; return jd == other
    when Date;    return jd == other.jd
    end
    false
  end

  def >> (n)
    y, m = clfloor(year * 12 + (mon - 1) + n, 12)
    m,   = clfloor(m + 1, 1)
    d = mday
    d -= 1 until jd2 = self.class.valid_civil?(y, m, d, ns?)
    self + (jd2 - jd)
  end

  def << (n) self >> -n end

  def step(limit, step)
    da = self
    op = [:-,:<=,:>=][step<=>0]
    while da.__send__(op, limit)
      yield da
      da += step
    end
    self
  end

  def upto(max, &block) step(max, +1, &block) end
  def downto(min, &block) step(min, -1, &block) end

  def succ() self + 1 end

  alias_method :next, :succ

  def eql? (other) Date === other and self == other end
  def hash() @ajd.hash end

  def inspect() format('#<%s: %s,%s,%s>', self.class, @ajd, @of, @sg) end
  def to_s() strftime end

  def _dump(limit) Marshal.dump([@ajd, @of, @sg], -1) end

# def self._load(str) new0(*Marshal.load(str)) end

  def self._load(str)
    a = Marshal.load(str)
    if a.size == 2
      ajd,     sg = a
           of = 0
      ajd -= 1.to_r/2
    else
      ajd, of, sg = a
    end
    new0(ajd, of, sg)
  end

end

class DateTime < Date

  def self.valid_time? (h, min, s)
    h   += 24 if h   < 0
    min += 60 if min < 0
    s   += 60 if s   < 0
    return unless (0..24) === h and
		  (0..59) === min and
		  (0..59) === s
    time_to_day_fraction(h, min, s)
  end

  def self.jd(jd=0, h=0, min=0, s=0, of=0, sg=ITALY)
    unless (jd = valid_jd?(jd, sg)) and
	   (fr = valid_time?(h, min, s))
      raise ArgumentError, 'invalid date'
    end
    new0(jd_to_ajd(jd, fr, of), of, sg)
  end

  def self.ordinal(y=-4712, d=1, h=0, min=0, s=0, of=0, sg=ITALY)
    unless (jd = valid_ordinal?(y, d, sg)) and
	   (fr = valid_time?(h, min, s))
      raise ArgumentError, 'invalid date'
    end
    new0(jd_to_ajd(jd, fr, of), of, sg)
  end

  def self.civil(y=-4712, m=1, d=1, h=0, min=0, s=0, of=0, sg=ITALY)
    unless (jd = valid_civil?(y, m, d, sg)) and
	   (fr = valid_time?(h, min, s))
      raise ArgumentError, 'invalid date'
    end
    new0(jd_to_ajd(jd, fr, of), of, sg)
  end

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

  def self.commercial(y=1582, w=41, d=5, h=0, min=0, s=0, of=0, sg=ITALY)
    unless (jd = valid_commercial?(y, w, d, sg)) and
	   (fr = valid_time?(h, min, s))
      raise ArgumentError, 'invalid date'
    end
    new0(jd_to_ajd(jd, fr, of), of, sg)
  end

  def self.new_with_hash(elem, sg)
    elem ||= {}
    y, m, d, h, min, s, of =
      elem.values_at(:year, :mon, :mday, :hour, :min, :sec, :offset)
    h   ||= 0
    min ||= 0
    s   ||= 0
    of  ||= 0
    if [y, m, d].include? nil
      raise ArgumentError, 'invalid date'
    else
      civil(y, m, d, h, min, s, of.to_r/86400, sg)
    end
  end

  private_class_method :new_with_hash

  def self.strptime(str='-4712-01-01T00:00:00Z', fmt='%FT%T%Z', sg=ITALY)
    elem = _strptime(str, fmt)
    new_with_hash(elem, sg)
  end

  def self.parse(str='-4712-01-01T00:00:00Z', comp=false, sg=ITALY)
    elem = _parse(str, comp)
    new_with_hash(elem, sg)
  end

  class << self; undef_method :today end

  def self.now(sg=ITALY)
    i = Time.now
    a = i.to_a[0..5].reverse
    jd = civil_to_jd(*(a[0,3] << sg))
    fr = time_to_day_fraction(*(a[3,3])) + i.usec.to_r/86400000000
    d = Time.gm(*i.to_a).to_i - i.to_i
    d += d / d.abs if d.nonzero?
    of = (d / 60).to_r/1440
    new0(jd_to_ajd(jd, fr, of), of, sg)
  end

  public :hour, :min, :sec, :sec_fraction, :zone, :offset, :new_offset

end

class Date

  [ %w(exist1?	valid_jd?),
    %w(exist2?	valid_ordinal?),
    %w(exist3?	valid_date?),
    %w(exist?	valid_date?),
    %w(existw?	valid_commercial?),
    %w(new1	jd),
    %w(new2	ordinal),
    %w(new3	new),
    %w(neww	commercial)
  ].each do |old, new|
    module_eval <<-"end;"
      def self.#{old}(*args, &block)
	if $VERBOSE
	  warn("\#{caller.shift.sub(/:in .*/, '')}: " \
	       "warning: \#{self}::#{old} is deprecated; " \
	       "use \#{self}::#{new}")
	end
	#{new}(*args, &block)
      end
    end;
  end

  [ %w(sg	start),
    %w(newsg	new_start),
    %w(of	offset),
    %w(newof	new_offset)
  ].each do |old, new|
    module_eval <<-"end;"
      def #{old}(*args, &block)
	if $VERBOSE
	  warn("\#{caller.shift.sub(/:in .*/, '')}: " \
	       "warning: \#{self.class}\##{old} is deprecated; " \
	       "use \#{self.class}\##{new}")
	end
	#{new}(*args, &block)
      end
    end;
  end

  private :of, :newof

end

class DateTime < Date

  public :of, :newof

end