# date.rb - date and time library
# Author: Tadayoshi Funaba 1998-2010
# Documentation: William Webber <william@williamwebber.com>
# $Id: date.rb,v 2.37 2008-01-17 20:16:31+09 tadf Exp $
# This file provides two classes for working with
# The first class, Date, represents dates.
# It works with years, months, weeks, and days.
# See the Date class documentation for more details.
# The second, DateTime, extends Date to include hours,
# minutes, seconds, and fractions of a second. It
# provides basic support for time zones. See the
# DateTime class documentation for more details.
# === Ways of calculating the date.
# In common usage, the date is reckoned in years since or
# before the Common Era (CE/BCE, also known as AD/BC), then
# as a month and day-of-the-month within the current year.
# This is known as the *Civil* *Date*, and abbreviated
# as +civil+ in the Date class.
# Instead of year, month-of-the-year, and day-of-the-month,
# the date can also be reckoned in terms of year and
# day-of-the-year. This is known as the *Ordinal* *Date*,
# and is abbreviated as +ordinal+ in the Date class. (Note
# that referring to this as the Julian date is incorrect.)
# The date can also be reckoned in terms of year, week-of-the-year,
# and day-of-the-week. This is known as the *Commercial*
# *Date*, and is abbreviated as +commercial+ in the
# Date class. The commercial week runs Monday (day-of-the-week
# 1) to Sunday (day-of-the-week 7), in contrast to the civil
# week which runs Sunday (day-of-the-week 0) to Saturday
# (day-of-the-week 6). The first week of the commercial year
# starts on the Monday on or before January 1, and the commercial
# year itself starts on this Monday, not January 1.
# For scientific purposes, it is convenient to refer to a date
# simply as a day count, counting from an arbitrary initial
# day. The date first chosen for this was January 1, 4713 BCE.
# A count of days from this date is the *Julian* *Day* *Number*
# or *Julian* *Date*, which is abbreviated as +jd+ in the
# Date class. This is in local time, and counts from midnight
# on the initial day. The stricter usage is in UTC, and counts
# from midday on the initial day. This is referred to in the
# Date class as the *Astronomical* *Julian* *Day* *Number*, and
# abbreviated as +ajd+. In the Date class, the Astronomical
# Julian Day Number includes fractional days.
# Another absolute day count is the *Modified* *Julian* *Day*
# *Number*, which takes November 17, 1858 as its initial day.
# This is abbreviated as +mjd+ in the Date class. There
# is also an *Astronomical* *Modified* *Julian* *Day* *Number*,
# which is in UTC and includes fractional days. This is
# abbreviated as +amjd+ in the Date class. Like the Modified
# Julian Day Number (and unlike the Astronomical Julian
# Day Number), it counts from midnight.
# Alternative calendars such as the Chinese Lunar Calendar,
# the Islamic Calendar, or the French Revolutionary Calendar
# are not supported by the Date class; nor are calendars that
# are based on an Era different from the Common Era, such as
# the Japanese Imperial Calendar or the Republic of China
# The standard civil year is 365 days long. However, the
# solar year is fractionally longer than this. To account
# for this, a *leap* *year* is occasionally inserted. This
# is a year with 366 days, the extra day falling on February 29.
# In the early days of the civil calendar, every fourth
# year without exception was a leap year. This way of
# reckoning leap years is the *Julian* *Calendar*.
# However, the solar year is marginally shorter than 365 1/4
# days, and so the *Julian* *Calendar* gradually ran slow
# over the centuries. To correct this, every 100th year
# (but not every 400th year) was excluded as a leap year.
# This way of reckoning leap years, which we use today, is
# the *Gregorian* *Calendar*.
# The Gregorian Calendar was introduced at different times
# in different regions. The day on which it was introduced
# for a particular region is the *Day* *of* *Calendar*
# *Reform* for that region. This is abbreviated as +sg+
# (for Start of Gregorian calendar) in the Date class.
# Two such days are of particular
# significance. The first is October 15, 1582, which was
# the Day of Calendar Reform for Italy and most Catholic
# countries. The second is September 14, 1752, which was
# the Day of Calendar Reform for England and its colonies
# (including what is now the United States). These two
# dates are available as the constants Date::ITALY and
# Date::ENGLAND, respectively. (By comparison, Germany and
# Holland, less Catholic than Italy but less stubborn than
# England, changed over in 1698; Sweden in 1753; Russia not
# till 1918, after the Revolution; and Greece in 1923. Many
# Orthodox churches still use the Julian Calendar. A complete
# list of Days of Calendar Reform can be found at
# http://www.polysyllabic.com/GregConv.html.)
# Switching from the Julian to the Gregorian calendar
# involved skipping a number of days to make up for the
# accumulated lag, and the later the switch was (or is)
# done, the more days need to be skipped. So in 1582 in Italy,
# 4th October was followed by 15th October, skipping 10 days; in 1752
# in England, 2nd September was followed by 14th September, skipping
# 11 days; and if I decided to switch from Julian to Gregorian
# Calendar this midnight, I would go from 27th July 2003 (Julian)
# today to 10th August 2003 (Gregorian) tomorrow, skipping
# 13 days. The Date class is aware of this gap, and a supposed
# date that would fall in the middle of it is regarded as invalid.
# The Day of Calendar Reform is relevant to all date representations
# involving years. It is not relevant to the Julian Day Numbers,
# except for converting between them and year-based representations.
# In the Date and DateTime classes, the Day of Calendar Reform or
# +sg+ can be specified a number of ways. First, it can be as
# the Julian Day Number of the Day of Calendar Reform. Second,
# it can be using the constants Date::ITALY or Date::ENGLAND; these
# are in fact the Julian Day Numbers of the Day of Calendar Reform
# of the respective regions. Third, it can be as the constant
# Date::JULIAN, which means to always use the Julian Calendar.
# Finally, it can be as the constant Date::GREGORIAN, which means
# to always use the Gregorian Calendar.
# Note: in the Julian Calendar, New Years Day was March 25. The
# Date class does not follow this convention.
# DateTime objects support a simple representation
# of time zones. Time zones are represented as an offset
# from UTC, as a fraction of a day. This offset is the
# how much local time is later (or earlier) than UTC.
# UTC offset 0 is centred on England (also known as GMT).
# As you travel east, the offset increases until you
# reach the dateline in the middle of the Pacific Ocean;
# as you travel west, the offset decreases. This offset
# is abbreviated as +of+ in the Date class.
# This simple representation of time zones does not take
# into account the common practice of Daylight Savings
# Most DateTime methods return the date and the
# time in local time. The two exceptions are
# #ajd() and #amjd(), which return the date and time
# in UTC time, including fractional days.
# The Date class does not support time zone offsets, in that
# there is no way to create a Date object with a time zone.
# However, methods of the Date class when used by a
# DateTime instance will use the time zone offset of this
# === Print out the date of every Sunday between two dates.
# def print_sundays(d1, d2)
# d1 +=1 while (d1.wday != 0)
# d1.step(d2, 7) do |date|
# puts "#{Date::MONTHNAMES[date.mon]} #{date.day}"
# print_sundays(Date::civil(2003, 4, 8), Date::civil(2003, 5, 23))
# === Calculate how many seconds to go till midnight on New Year's Day.
# def secs_to_new_year(now = DateTime::now())
# new_year = DateTime.new(now.year + 1, 1, 1)
# hours, mins, secs, ignore_fractions = Date::day_fraction_to_time(dif)
# return hours * 60 * 60 + mins * 60 + secs
# puts secs_to_new_year()
# Class representing a date.
# See the documentation to the file date.rb for an overview.
# Internally, the date is represented as an Astronomical
# Julian Day Number, +ajd+. The Day of Calendar Reform, +sg+, is
# also stored, for conversions to other date formats. (There
# is also an +of+ field for a time zone offset, but this
# is only for the use of the DateTime subclass.)
# A new Date object is created using one of the object creation
# class methods named after the corresponding date format, and the
# arguments appropriate to that date format; for instance,
# Date::civil() (aliased to Date::new()) with year, month,
# and day-of-month, or Date::ordinal() with year and day-of-year.
# All of these object creation class methods also take the
# Day of Calendar Reform as an optional argument.
# Date objects are immutable once created.
# Once a Date has been created, date values
# can be retrieved for the different date formats supported
# using instance methods. For instance, #mon() gives the
# Civil month, #cwday() gives the Commercial day of the week,
# and #yday() gives the Ordinal day of the year. Date values
# can be retrieved in any format, regardless of what format
# was used to create the Date instance.
# The Date class includes the Comparable module, allowing
# date objects to be compared and sorted, ranges of dates
# to be created, and so forth.
# Full month names, in English. Months count from 1 to 12; a
# month's numerical representation indexed into this array
# gives the name of that month (hence the first element is nil).
MONTHNAMES = [nil] + %w(January February March April May June July
August September October November December)
# Full names of days of the week, in English. Days of the week
# count from 0 to 6 (except in the commercial week); a day's numerical
# representation indexed into this array gives the name of that day.
DAYNAMES = %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
# Abbreviated month names, in English.
ABBR_MONTHNAMES = [nil] + %w(Jan Feb Mar Apr May Jun
# Abbreviated day names, in English.
ABBR_DAYNAMES = %w(Sun Mon Tue Wed Thu Fri Sat)
[MONTHNAMES, DAYNAMES, ABBR_MONTHNAMES, ABBR_DAYNAMES].each do |xs|
xs.each{|x| x.freeze unless x.nil?}.freeze
class Infinity < Numeric # :nodoc:
def initialize(d=1) @d = d <=> 0 end
def infinite? () d.nonzero? end
def abs() self.class.new end
def -@ () self.class.new(-d) end
def +@ () self.class.new(+d) end
when Infinity; return d <=> other.d
l, r = other.coerce(self)
when Numeric; return -d, d
# The Julian Day Number of the Day of Calendar Reform for Italy
# and the Catholic countries.
ITALY = 2299161 # 1582-10-15
# The Julian Day Number of the Day of Calendar Reform for England
ENGLAND = 2361222 # 1752-09-14
# A constant used to indicate that a Date should always use the
# A constant used to indicate that a Date should always use the
GREGORIAN = -Infinity.new
HALF_DAYS_IN_DAY = Rational(1, 2) # :nodoc:
HOURS_IN_DAY = Rational(1, 24) # :nodoc:
MINUTES_IN_DAY = Rational(1, 1440) # :nodoc:
SECONDS_IN_DAY = Rational(1, 86400) # :nodoc:
MILLISECONDS_IN_DAY = Rational(1, 86400*10**3) # :nodoc:
NANOSECONDS_IN_DAY = Rational(1, 86400*10**9) # :nodoc:
MILLISECONDS_IN_SECOND = Rational(1, 10**3) # :nodoc:
NANOSECONDS_IN_SECOND = Rational(1, 10**9) # :nodoc:
MJD_EPOCH_IN_AJD = Rational(4800001, 2) # 1858-11-17 # :nodoc:
UNIX_EPOCH_IN_AJD = Rational(4881175, 2) # 1970-01-01 # :nodoc:
MJD_EPOCH_IN_CJD = 2400001 # :nodoc:
UNIX_EPOCH_IN_CJD = 2440588 # :nodoc:
LD_EPOCH_IN_CJD = 2299160 # :nodoc:
# Does a given Julian Day Number fall inside the old-style (Julian)
# +jd+ is the Julian Day Number in question. +sg+ may be Date::GREGORIAN,
# in which case the answer is false; it may be Date::JULIAN, in which case
# the answer is true; or it may a number representing the Day of
# Calendar Reform. Date::ENGLAND and Date::ITALY are two possible such
def self.julian? (jd, sg)
warn("#{caller.shift.sub(/:in .*/, '')}: " \
"warning: do not use non-numerical object as julian day number anymore")
# Does a given Julian Day Number fall inside the new-style (Gregorian)
# The reverse of self.os? See the documentation for that method for
def self.gregorian? (jd, sg) !julian?(jd, sg) end
def self.fix_style(jd, sg) # :nodoc:
private_class_method :fix_style
# Convert an Ordinal Date to a Julian Day Number.
# +y+ and +d+ are the year and day-of-year to convert.
# +sg+ specifies the Day of Calendar Reform.
# Returns the corresponding Julian Day Number.
def self.ordinal_to_jd(y, d, sg=GREGORIAN)
# Convert a Julian Day Number to an Ordinal Date.
# +jd+ is the Julian Day Number to convert.
# +sg+ specifies the Day of Calendar Reform.
# Returns the corresponding Ordinal Date as
def self.jd_to_ordinal(jd, sg=GREGORIAN)
y = jd_to_civil(jd, sg)[0]
doy = jd - civil_to_jd(y - 1, 12, 31, fix_style(jd, sg))
# Convert a Civil Date to a Julian Day Number.
# +y+, +m+, and +d+ are the year, month, and day of the
# month. +sg+ specifies the Day of Calendar Reform.
# Returns the corresponding Julian Day Number.
def self.civil_to_jd(y, m, d, sg=GREGORIAN)
b = 2 - a + (a / 4.0).floor
jd = (365.25 * (y + 4716)).floor +
(30.6001 * (m + 1)).floor +
# Convert a Julian Day Number to a Civil Date. +jd+ is
# the Julian Day Number. +sg+ specifies the Day of
# Returns the corresponding [year, month, day_of_month]
# as a three-element array.
def self.jd_to_civil(jd, sg=GREGORIAN)
x = ((jd - 1867216.25) / 36524.25).floor
a = jd + 1 + x - (x / 4.0).floor
c = ((b - 122.1) / 365.25).floor
e = ((b - d) / 30.6001).floor
dom = b - d - (30.6001 * e).floor
# Convert a Commercial Date to a Julian Day Number.
# +y+, +w+, and +d+ are the (commercial) year, week of the year,
# and day of the week of the Commercial Date to convert.
# +sg+ specifies the Day of Calendar Reform.
def self.commercial_to_jd(y, w, d, ns=GREGORIAN)
jd = civil_to_jd(y, 1, 4, ns)
(jd - (((jd - 1) + 1) % 7)) +
# Convert a Julian Day Number to a Commercial Date
# +jd+ is the Julian Day Number to convert.
# +sg+ specifies the Day of Calendar Reform.
# Returns the corresponding Commercial Date as
# [commercial_year, week_of_year, day_of_week]
def self.jd_to_commercial(jd, sg=GREGORIAN)
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).floor
def self.weeknum_to_jd(y, w, d, f=0, ns=GREGORIAN) # :nodoc:
a = civil_to_jd(y, 1, 1, ns) + 6
(a - ((a - f) + 1) % 7 - 7) + 7 * w + d
def self.jd_to_weeknum(jd, f=0, sg=GREGORIAN) # :nodoc:
y, m, d = jd_to_civil(jd, ns)
a = civil_to_jd(y, 1, 1, ns) + 6
w, d = (jd - (a - ((a - f) + 1) % 7) + 7).divmod(7)
private_class_method :weeknum_to_jd, :jd_to_weeknum
# Convert an Astronomical Julian Day Number to a (civil) Julian
# +ajd+ is the Astronomical Julian Day Number to convert.
# +of+ is the offset from UTC as a fraction of a day (defaults to 0).
# Returns the (civil) Julian Day Number as [day_number,
# fraction] where +fraction+ is always 1/2.
def self.ajd_to_jd(ajd, of=0) (ajd + of + HALF_DAYS_IN_DAY).divmod(1) end
# Convert a (civil) Julian Day Number to an Astronomical Julian
# +jd+ is the Julian Day Number to convert, and +fr+ is a
# +of+ is the offset from UTC as a fraction of a day (defaults to 0).