# encoding: US-ASCII # frozen_string_literal: false # date.rb: Written by Tadayoshi Funaba 1998-2011 # == Overview # # This file provides two classes for working with # dates and times. # # 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 # Calendar. # # === Calendar Reform # # 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. # # === Time Zones # # 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 # Time or Summer Time. # # 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 # instance. # Load built-in date library JRuby::Util.load_ext("org.jruby.ext.date.DateLibrary") require 'date/format' # 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. class Date # 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 Jul Aug Sep Oct Nov Dec) # 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 end def infinite? false end class Infinity < Numeric # :nodoc: def initialize(d=1) @d = d <=> 0 end def d() @d end protected :d def zero?() false end def finite?() false end def infinite?() d.nonzero? end def nan?() d.zero? end def abs() self.class.new end def -@() self.class.new(-d) end def +@() self.class.new(+d) end def <=>(other) case other when Infinity; return d <=> other.d when Float::INFINITY; return d <=> 1 when -Float::INFINITY; return d <=> -1 when Numeric; return d else begin l, r = other.coerce(self) return l <=> r rescue NoMethodError end end nil end def coerce(other) case other when Numeric; return -d, d else super end end def to_f return 0 if @d == 0 if @d > 0 Float::INFINITY else -Float::INFINITY end end end # 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 # and her Colonies. #ENGLAND = 2361222 # 1752-09-14 # A constant used to indicate that a Date should always use the # Julian calendar. JULIAN = Infinity.new # A constant used to indicate that a Date should always use the # Gregorian calendar. GREGORIAN = -Infinity.new def self.rewrite_frags(elem) # :nodoc: if seconds = elem[:seconds] d, fr = seconds.divmod(86400) h, fr = fr.divmod(3600) min, fr = fr.divmod(60) s, fr = fr.divmod(1) #offset = elem[:offset] #seconds += offset unless offset.nil? elem[:jd] = 2440588 + d # UNIX_EPOCH_IN_CJD elem[:hour] = h elem[:min] = min elem[:sec] = s elem[:sec_fraction] = fr elem.delete(:seconds) end elem end private_class_method :rewrite_frags COMPLETE_FRAGS = [ [:time, []], [nil, [:jd]], [:ordinal, [:year, :yday]], [:civil, [:year, :mon, :mday]], [:commercial, [:cwyear, :cweek, :cwday]], [:wday, [:wday, :__need_jd_filling]], [:wnum0, [:year, :wnum0, :wday]], [:wnum1, [:year, :wnum1, :wday]], [nil, [:cwyear, :cweek, :wday]], [nil, [:year, :wnum0, :cwday]], [nil, [:year, :wnum1, :cwday]] ].freeze private_constant :COMPLETE_FRAGS def self.complete_frags(elem) # :nodoc: g = COMPLETE_FRAGS.max_by { |_, fields| fields.count { |field| elem.key? field } } c = g[1].count { |field| elem.key? field } if c == 0 && [:hour, :min, :sec].none? { |field| elem.key? field } g = nil end if g && g[0] && g[1].size != c d ||= Date.today case g[0] when :ordinal elem[:year] ||= d.year elem[:yday] ||= 1 when :civil g[1].each do |e| break if elem[e] elem[e] = d.__send__(e) end elem[:mon] ||= 1 elem[:mday] ||= 1 when :commercial g[1].each do |e| break if elem[e] elem[e] = d.__send__(e) end elem[:cweek] ||= 1 elem[:cwday] ||= 1 when :wday elem[:jd] ||= (d - d.wday + elem[:wday]).jd when :wnum0 g[1].each do |e| break if elem[e] elem[e] = d.__send__(e) end elem[:wnum0] ||= 0 elem[:wday] ||= 0 when :wnum1 g[1].each do |e| break if elem[e] elem[e] = d.__send__(e) end elem[:wnum1] ||= 0 elem[:wday] ||= 1 end end if self <= DateTime if g && g[0] == :time d ||= Date.today elem[:jd] ||= d.jd end elem[:hour] ||= 0 elem[:min] ||= 0 elem[:sec] ||= 0 # see [ruby-core:47226] and the "fix" elem[:sec] = 59 if elem[:sec] == 60 end elem end private_class_method :complete_frags def self.valid_date_frags?(elem, sg) # :nodoc: if jd = elem[:jd] and jd = _valid_jd?(jd, sg) return jd end year = elem[:year] if year and yday = elem[:yday] and jd = _valid_ordinal?(year, yday, sg) return jd end if year and mon = elem[:mon] and mday = elem[:mday] and jd = _valid_civil?(year, mon, mday, sg) return jd end if cwyear = elem[:cwyear] and cweek = elem[:cweek] and cwday = (elem[:cwday] || elem[:wday].nonzero? || 7) and jd = _valid_commercial?(cwyear, cweek, cwday, sg) return jd end if year and wnum0 = elem[:wnum0] and wday = (elem[:wday] || (elem[:cwday] && elem[:cwday] % 7)) and jd = _valid_weeknum?(year, wnum0, wday, 0, sg) return jd end if year and wnum1 = elem[:wnum1] and wday = ( (elem[:wday] && (elem[:wday] - 1) % 7) || (elem[:cwday] && (elem[:cwday] - 1) % 7) ) and jd = _valid_weeknum?(year, wnum1, wday, 1, sg) return jd end end private_class_method :valid_date_frags? def self.valid_time_frags?(elem) # :nodoc: h, min, s = elem.values_at(:hour, :min, :sec) _valid_time?(h, min, s) end private_class_method :valid_time_frags? def self.new_by_frags(elem, sg) # :nodoc: raise Date::Error, 'invalid date' unless elem # fast path if !elem.key?(:jd) && !elem.key?(:yday) && (year = elem[:year]) && (mon = elem[:mon]) && (mday = elem[:mday]) return Date.civil(year, mon, mday, sg) end elem = rewrite_frags(elem) elem = complete_frags(elem) unless jd = valid_date_frags?(elem, sg) raise Date::Error, 'invalid date' end new!(jd_to_ajd(jd, 0, 0), 0, sg) end private_class_method :new_by_frags # Create a new Date object by parsing from a String # according to a specified format. # # +str+ is a String holding a date representation. # +fmt+ is the format that the date is in. See # date/format.rb for details on supported formats. # # The default +str+ is '-4712-01-01', and the default # +fmt+ is '%F', which means Year-Month-Day_of_Month. # This gives Julian Day Number day 0. # # +sg+ specifies the Day of Calendar Reform. # # An ArgumentError will be raised if +str+ cannot be # parsed. def self.strptime(str='-4712-01-01', fmt='%F', sg=ITALY) new_by_frags(_strptime(str, fmt), sg) end # Create a new Date object by parsing from a String, # without specifying the format. # # +str+ is a String holding a date representation. # +comp+ specifies whether to interpret 2-digit years # as 19XX (>= 69) or 20XX (< 69); the default is not to. # The method will attempt to parse a date from the String # using various heuristics; see #_parse in date/format.rb # for more details. If parsing fails, an ArgumentError # will be raised. # # The default +str+ is '-4712-01-01'; this is Julian # Day Number day 0. # # +sg+ specifies the Day of Calendar Reform. def self.parse(str='-4712-01-01', comp=true, sg=ITALY, limit: 128) elem = _parse(str, comp, limit: limit) new_by_frags(elem, sg) end def self.iso8601(str='-4712-01-01', sg=ITALY, limit: 128) # :nodoc: elem = _iso8601(str, limit: limit) new_by_frags(elem, sg) end def self.rfc3339(str='-4712-01-01T00:00:00+00:00', sg=ITALY, limit: 128) # :nodoc: elem = _rfc3339(str, limit: limit) new_by_frags(elem, sg) end def self.xmlschema(str='-4712-01-01', sg=ITALY, limit: 128) # :nodoc: elem = _xmlschema(str, limit: limit) new_by_frags(elem, sg) end def self.rfc2822(str='Mon, 1 Jan -4712 00:00:00 +0000', sg=ITALY, limit: 128) # :nodoc: elem = _rfc2822(str, limit: limit) new_by_frags(elem, sg) end class << self; alias_method :rfc822, :rfc2822 end def self.httpdate(str='Mon, 01 Jan -4712 00:00:00 GMT', sg=ITALY) # :nodoc: elem = _httpdate(str) new_by_frags(elem, sg) end def self.jisx0301(str='-4712-01-01', sg=ITALY, limit: 128) # :nodoc: elem = _jisx0301(str, limit: limit) new_by_frags(elem, sg) end DAYNAMES.each_with_index do |n, i| define_method(n.downcase + '?') { wday == i } end # Step the current date forward +step+ days at a # time (or backward, if +step+ is negative) until # we reach +limit+ (inclusive), yielding the resultant # date at each step. def step(limit, step=1) # :yield: date =begin if step.zero? raise ArgumentError, "step can't be 0" end =end unless block_given? return to_enum(:step, limit, step) end da = self step_cmp = step <=> 0 if step_cmp.nil? raise ArgumentError.new("comparison of #{step.class} with 0 failed") end step_cmp = step_cmp > 0 ? 1 : step_cmp < 0 ? -1 : 0 op = %w(- <= >=)[step_cmp] while da.__send__(op, limit) yield da da += step end self end # Step forward one day at a time until we reach +max+ # (inclusive), yielding each date as we go. def upto(max, &block) # :yield: date step(max, +1, &block) end # Step backward one day at a time until we reach +min+ # (inclusive), yielding each date as we go. def downto(min, &block) # :yield: date step(min, -1, &block) end end # Class representing a date and time. # # See the documentation to the file date.rb for an overview. # # DateTime objects are immutable once created. # # == Other methods. # # The following methods are defined in Date, but declared private # there. They are made public in DateTime. They are documented # here. # # === hour() # # Get the hour-of-the-day of the time. This is given # using the 24-hour clock, counting from midnight. The first # hour after midnight is hour 0; the last hour of the day is # hour 23. # # === min() # # Get the minute-of-the-hour of the time. # # === sec() # # Get the second-of-the-minute of the time. # # === sec_fraction() # # Get the fraction of a second of the time. This is returned as # a +Rational+. # # === zone() # # Get the time zone as a String. This is representation of the # time offset such as "+1000", not the true time-zone name. # # === offset() # # Get the time zone offset as a fraction of a day. This is returned # as a +Rational+. # # === new_offset(of=0) # # Create a new DateTime object, identical to the current one, except # with a new time zone offset of +of+. +of+ is the new offset from # UTC as a fraction of a day. # class DateTime < Date # Create a new DateTime object corresponding to the specified # Ordinal Date and hour +h+, minute +min+, second +s+. # # The 24-hour clock is used. Negative values of +h+, +min+, and # +sec+ are treating as counting backwards from the end of the # next larger unit (e.g. a +min+ of -2 is treated as 58). No # wraparound is performed. If an invalid time portion is specified, # an ArgumentError is raised. # # +of+ is the offset from UTC as a fraction of a day (defaults to 0). # +sg+ specifies the Day of Calendar Reform. # # +y+ defaults to -4712, and +d+ to 1; this is Julian Day Number # day 0. The time values default to 0. def self.ordinal(y=-4712, d=1, h=0, min=0, s=0, of=0, sg=ITALY) unless (jd = _valid_ordinal?(y, d, sg)) && (fr = _valid_time?(h, min, s)) raise Date::Error, 'invalid date' end if String === of of = Rational(zone_to_diff(of) || 0, 86400) end new!(jd_to_ajd(jd, fr, of), of, sg) end # Create a new DateTime object corresponding to the specified # Commercial Date and hour +h+, minute +min+, second +s+. # # The 24-hour clock is used. Negative values of +h+, +min+, and # +sec+ are treating as counting backwards from the end of the # next larger unit (e.g. a +min+ of -2 is treated as 58). No # wraparound is performed. If an invalid time portion is specified, # an ArgumentError is raised. # # +of+ is the offset from UTC as a fraction of a day (defaults to 0). # +sg+ specifies the Day of Calendar Reform. # # +y+ defaults to -4712, +w+ to 1, and +d+ to 1; this is # Julian Day Number day 0. # The time values default to 0. def self.commercial(y=-4712, w=1, d=1, h=0, min=0, s=0, of=0, sg=ITALY) unless (jd = _valid_commercial?(y, w, d, sg)) && (fr = _valid_time?(h, min, s)) raise Date::Error, 'invalid date' end if String === of of = Rational(zone_to_diff(of) || 0, 86400) end new!(jd_to_ajd(jd, fr, of), of, sg) end def self.new_by_frags(elem, sg) # :nodoc: raise Date::Error, 'invalid date' unless elem # More work to do if not :civil if !elem.key?(:jd) && !elem.key?(:yday) && (year = elem[:year]) && (mon = elem[:mon]) && (mday = elem[:mday]) jd = Date._valid_civil?(year, mon, mday, sg) elem[:hour] ||= 0 elem[:min] ||= 0 elem[:sec] ||= 0 elem[:sec] = 59 if elem[:sec] == 60 else elem = rewrite_frags(elem) elem = complete_frags(elem) jd = valid_date_frags?(elem, sg) end unless jd && (fr = valid_time_frags?(elem)) raise Date::Error, 'invalid date' end fr += (elem[:sec_fraction] || 0) / 86400 of = Rational(elem[:offset] || 0, 86400) if of < -1 || of > 1 of = 0 warn "invalid offset is ignored" if $VERBOSE end new!(jd_to_ajd(jd, fr, of), of, sg) end private_class_method :new_by_frags # Create a new DateTime object by parsing from a String # according to a specified format. # # +str+ is a String holding a date-time representation. # +fmt+ is the format that the date-time is in. See # date/format.rb for details on supported formats. # # The default +str+ is '-4712-01-01T00:00:00+00:00', and the default # +fmt+ is '%FT%T%z'. This gives midnight on Julian Day Number day 0. # # +sg+ specifies the Day of Calendar Reform. # # An ArgumentError will be raised if +str+ cannot be # parsed. def self.strptime(str='-4712-01-01T00:00:00+00:00', fmt='%FT%T%z', sg=ITALY) elem = _strptime(str, fmt) new_by_frags(elem, sg) end # Create a new DateTime object by parsing from a String, # without specifying the format. # # +str+ is a String holding a date-time representation. # +comp+ specifies whether to interpret 2-digit years # as 19XX (>= 69) or 20XX (< 69); the default is not to. # The method will attempt to parse a date-time from the String # using various heuristics; see #_parse in date/format.rb # for more details. If parsing fails, an ArgumentError # will be raised. # # The default +str+ is '-4712-01-01T00:00:00+00:00'; this is Julian # Day Number day 0. # # +sg+ specifies the Day of Calendar Reform. def self.parse(str='-4712-01-01T00:00:00+00:00', comp=true, sg=ITALY, limit: 128) elem = _parse(str, comp, limit: limit) new_by_frags(elem, sg) end def self.iso8601(str='-4712-01-01T00:00:00+00:00', sg=ITALY, limit: 128) # :nodoc: elem = _iso8601(str, limit: limit) new_by_frags(elem, sg) end def self.rfc3339(str='-4712-01-01T00:00:00+00:00', sg=ITALY, limit: 128) # :nodoc: elem = _rfc3339(str, limit: limit) new_by_frags(elem, sg) end def self.xmlschema(str='-4712-01-01T00:00:00+00:00', sg=ITALY, limit: 128) # :nodoc: elem = _xmlschema(str, limit: limit) new_by_frags(elem, sg) end def self.rfc2822(str='Mon, 1 Jan -4712 00:00:00 +0000', sg=ITALY, limit: 128) # :nodoc: elem = _rfc2822(str, limit: limit) new_by_frags(elem, sg) end class << self; alias_method :rfc822, :rfc2822 end def self.httpdate(str='Mon, 01 Jan -4712 00:00:00 GMT', sg=ITALY) # :nodoc: elem = _httpdate(str) new_by_frags(elem, sg) end def self.jisx0301(str='-4712-01-01T00:00:00+00:00', sg=ITALY, limit: 128) # :nodoc: elem = _jisx0301(str, limit: limit) new_by_frags(elem, sg) end # adjust superclass (Date) methods : public :hour, :min, :sec, :sec_fraction, :zone, :offset, :new_offset, :minute, :second, :second_fraction class << self undef_method :today end end