#
#               Date.rb - 
#                       $Release Version: $
#                       $Revision: 1.1.1.1.4.5 $
#                       $Date: 1998/03/03 02:39:34 $
#                       by Yasuo OHBA(SHL Japan Inc. Technology Dept.)
#
# --
#
#    September 1752
#  S  M Tu  W Th  F  S
#        1  2 14 15 16
# 17 18 19 20 21 22 23
# 24 25 26 27 28 29 30
#       

class Date
  include Comparable
  
  Weektag = [
    "Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"
  ]

  Monthtag = [
    "January","February","March","April", "May", "June","July",
    "August", "September", "October", "November", "December"
  ]

  Monthtab = {
    "jan"=>1, "feb"=>2, "mar"=>3, "apr"=>4, "may"=>5, "jun"=>6,
    "jul"=>7, "aug"=>8, "sep"=>9, "oct"=>10, "nov"=>11, "dec"=>12
  }

  def initialize(y = 1, m = 1, d = 1)
    if y.kind_of?(String)
      case y
      when /(\d\d\d\d)-?(?:(\d\d)-?(\d\d)?)?/
	@year = $1.to_i
	@month = if $2 then $2.to_i else 1 end
	@day = if $3 then $3.to_i else 1 end
      else
	require 'parsedate'
	@year, @month, @day = ParseDate.parsedate(y)
      end
    else
      if m.kind_of?(String)
        m = Monthtab[m.downcase]
        if m.nil?
          raise ArgumentError, "Wrong argument. (month)"
        end
      end
      @year = y.to_i
      @month = m.to_i
      @day = d.to_i
    end
    _check_date
    return self
  end
  
  def year
    return @year
  end
  
  def month
    return @month
  end
  
  def day
    return @day
  end
  
  def period
    return Date.period!(@year, @month, @day)
  end

  def jd
    return period + 1721423
  end

  def mjd
    return jd - 2400000.5
  end

  def to_s
    format("%.3s, %.3s %2d %4d", name_of_week, name_of_month, @day, @year)
  end

  def inspect
    to_s
  end

  def day_of_week
    return (period + 5) % 7
  end
  
  def name_of_week
    return Weektag[self.day_of_week]
  end
  
  def name_of_month
    return Monthtag[@month-1]
  end
  
  def +(o)
    if o.kind_of?(Numeric)
      d = Integer(self.period + o)
    elsif o.kind_of?(Date)
      d = self.period + o.period
    else
      raise TypeError, "Illegal type. (Integer or Date)"
    end
    if d <= 0
      raise ArgumentError, "argument out of range. (self > other)"
    end
    return Date.at(d)
  end
  
  def -(o)
    if o.kind_of?(Numeric)
      d = Integer(self.period - o)
    elsif o.kind_of?(Date)
      return Integer(self.period - o.period)
    else
      raise TypeError, "Illegal type. (Integer or Date)"
    end
    if d <= 0
      raise ArgumentError, "argument out of range. (self > other)"
    end
    return Date.at(d)
  end
  
  def <=>(o)
    if o.kind_of?(Integer)
      d = o
    elsif o.kind_of?(Date)
      d = o.period
    else
      raise TypeError, "Illegal type. (Integer or Date)"
    end
    return self.period <=> d
  end

  def eql?(o)
    self == o
  end
  
  def hash
    return @year ^ @month ^ @day
  end
  
  def leapyear?
    Date.leapyear(@year) != 1
  end

  def _check_date
    if @year == nil or @month == nil or @day == nil
      raise ArgumentError, "argument contains nil"
    end
    m = Date.daylist(@year)
    if @month < 1 || @month > 12
      raise ArgumentError, "argument(month) out of range."
      return nil
    end
    if @year == 1752 && @month == 9
      if @day >= 3 && @day <= 13
        raise ArgumentError, "argument(1752/09/3-13) out of range."
        return nil
      end
      d = 30
    else
      d = m[@month]
    end
    if @day < 1 || @day > d
      raise ArgumentError, "argument(day) out of range."
      return nil
    end
    return self
  end
  
  private :_check_date
end

def Date.at(d)
  if d.kind_of? Time
    return Date.new(d.year, d.mon, d.mday)
  end
  if d.kind_of? Date
    return Date.at(d.period)
  end
  mm = 1
  yy = (d / 366.0).to_i
  if yy != 0
    dd = d - (Date.period!(yy, 1, 1) - 1)
  else
    dd = d
    yy = 1
  end
  dl = Date.daylist(yy)
  while dd > dl[mm]
    if dd > dl[0]
      dd -= dl[0]
      yy += 1
      dl = Date.daylist(yy)
    else
      dd -= dl[mm]
      mm += 1
    end
  end
  if yy == 1752 && mm == 9 && dd >= 3 && dd <= 19
    dd += (14 - 3)              # 1752/09/03-19 -> 1752/09/14-30
  end
  
  return Date.new(yy, mm, dd)
end

def Date.period!(y, m, d)
  p = d
  dl = Date.daylist(y)
  for mm in 1..(m - 1)
    p += dl[mm]
  end
  p += (y - 1) * 365 + ((y - 1) / 4.0).to_i
  if y > 1752
    p -= ((y - 1) / 100.0).to_i
    p += ((y - 1) / 400.0).to_i
    p += 2
  elsif y == 1752 && m == 9 && d >= 14 && d <= 30
    p -= (14 - 3)
  end
  return p
end

def Date.leapyear(yy)
  return ((Date.jan1!(yy + 1) + 7 - Date.jan1!(yy)) % 7)
end

def Date.daylist(yy)
  case (Date.leapyear(yy))
  when 1 # non-leapyear
    return [365, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  when 2 # leapyear
    return [366, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  else   # 1752
    return [355, 31, 29, 31, 30, 31, 30, 31, 31, 19, 31, 30, 31]
  end
end

def Date.jan1!(y)
  d = 4 + y + (y + 3) / 4
  if y > 1800
    d -= (y - 1701) / 100
    d += (y - 1601) / 400
  end
  if y > 1752
    d += 3
  end
  return (d % 7)
end
