# format.rb: Written by Tadayoshi Funaba 1999-2009 # $Id: format.rb,v 2.43 2008-01-17 20:16:31+09 tadf Exp $ class Date module Format # :nodoc: MONTHS = { 'january' => 1, 'february' => 2, 'march' => 3, 'april' => 4, 'may' => 5, 'june' => 6, 'july' => 7, 'august' => 8, 'september'=> 9, 'october' =>10, 'november' =>11, 'december' =>12 }.freeze DAYS = { 'sunday' => 0, 'monday' => 1, 'tuesday' => 2, 'wednesday'=> 3, 'thursday' => 4, 'friday' => 5, 'saturday' => 6 }.freeze ABBR_MONTHS = { 'jan' => 1, 'feb' => 2, 'mar' => 3, 'apr' => 4, 'may' => 5, 'jun' => 6, 'jul' => 7, 'aug' => 8, 'sep' => 9, 'oct' =>10, 'nov' =>11, 'dec' =>12 }.freeze ABBR_DAYS = { 'sun' => 0, 'mon' => 1, 'tue' => 2, 'wed' => 3, 'thu' => 4, 'fri' => 5, 'sat' => 6 }.freeze ZONES = { 'ut' => 0*3600, 'gmt' => 0*3600, 'est' => -5*3600, 'edt' => -4*3600, 'cst' => -6*3600, 'cdt' => -5*3600, 'mst' => -7*3600, 'mdt' => -6*3600, 'pst' => -8*3600, 'pdt' => -7*3600, 'a' => 1*3600, 'b' => 2*3600, 'c' => 3*3600, 'd' => 4*3600, 'e' => 5*3600, 'f' => 6*3600, 'g' => 7*3600, 'h' => 8*3600, 'i' => 9*3600, 'k' => 10*3600, 'l' => 11*3600, 'm' => 12*3600, 'n' => -1*3600, 'o' => -2*3600, 'p' => -3*3600, 'q' => -4*3600, 'r' => -5*3600, 's' => -6*3600, 't' => -7*3600, 'u' => -8*3600, 'v' => -9*3600, 'w' =>-10*3600, 'x' =>-11*3600, 'y' =>-12*3600, 'z' => 0*3600, 'utc' => 0*3600, 'wet' => 0*3600, 'at' => -2*3600, 'brst'=> -2*3600, 'ndt' => -(2*3600+1800), 'art' => -3*3600, 'adt' => -3*3600, 'brt' => -3*3600, 'clst'=> -3*3600, 'nst' => -(3*3600+1800), 'ast' => -4*3600, 'clt' => -4*3600, 'akdt'=> -8*3600, 'ydt' => -8*3600, 'akst'=> -9*3600, 'hadt'=> -9*3600, 'hdt' => -9*3600, 'yst' => -9*3600, 'ahst'=>-10*3600, 'cat' =>-10*3600, 'hast'=>-10*3600, 'hst' =>-10*3600, 'nt' =>-11*3600, 'idlw'=>-12*3600, 'bst' => 1*3600, 'cet' => 1*3600, 'fwt' => 1*3600, 'met' => 1*3600, 'mewt'=> 1*3600, 'mez' => 1*3600, 'swt' => 1*3600, 'wat' => 1*3600, 'west'=> 1*3600, 'cest'=> 2*3600, 'eet' => 2*3600, 'fst' => 2*3600, 'mest'=> 2*3600, 'mesz'=> 2*3600, 'sast'=> 2*3600, 'sst' => 2*3600, 'bt' => 3*3600, 'eat' => 3*3600, 'eest'=> 3*3600, 'msk' => 3*3600, 'msd' => 4*3600, 'zp4' => 4*3600, 'zp5' => 5*3600, 'ist' => (5*3600+1800), 'zp6' => 6*3600, 'wast'=> 7*3600, 'cct' => 8*3600, 'sgt' => 8*3600, 'wadt'=> 8*3600, 'jst' => 9*3600, 'kst' => 9*3600, 'east'=> 10*3600, 'gst' => 10*3600, 'eadt'=> 11*3600, 'idle'=> 12*3600, 'nzst'=> 12*3600, 'nzt' => 12*3600, 'nzdt'=> 13*3600, 'afghanistan' => 16200, 'alaskan' => -32400, 'arab' => 10800, 'arabian' => 14400, 'arabic' => 10800, 'atlantic' => -14400, 'aus central' => 34200, 'aus eastern' => 36000, 'azores' => -3600, 'canada central' => -21600, 'cape verde' => -3600, 'caucasus' => 14400, 'cen. australia' => 34200, 'central america' => -21600, 'central asia' => 21600, 'central europe' => 3600, 'central european' => 3600, 'central pacific' => 39600, 'central' => -21600, 'china' => 28800, 'dateline' => -43200, 'e. africa' => 10800, 'e. australia' => 36000, 'e. europe' => 7200, 'e. south america' => -10800, 'eastern' => -18000, 'egypt' => 7200, 'ekaterinburg' => 18000, 'fiji' => 43200, 'fle' => 7200, 'greenland' => -10800, 'greenwich' => 0, 'gtb' => 7200, 'hawaiian' => -36000, 'india' => 19800, 'iran' => 12600, 'jerusalem' => 7200, 'korea' => 32400, 'mexico' => -21600, 'mid-atlantic' => -7200, 'mountain' => -25200, 'myanmar' => 23400, 'n. central asia' => 21600, 'nepal' => 20700, 'new zealand' => 43200, 'newfoundland' => -12600, 'north asia east' => 28800, 'north asia' => 25200, 'pacific sa' => -14400, 'pacific' => -28800, 'romance' => 3600, 'russian' => 10800, 'sa eastern' => -10800, 'sa pacific' => -18000, 'sa western' => -14400, 'samoa' => -39600, 'se asia' => 25200, 'malay peninsula' => 28800, 'south africa' => 7200, 'sri lanka' => 21600, 'taipei' => 28800, 'tasmania' => 36000, 'tokyo' => 32400, 'tonga' => 46800, 'us eastern' => -18000, 'us mountain' => -25200, 'vladivostok' => 36000, 'w. australia' => 28800, 'w. central africa' => 3600, 'w. europe' => 3600, 'west asia' => 18000, 'west pacific' => 36000, 'yakutsk' => 32400 }.freeze end def asctime() strftime('%c') end alias_method :ctime, :asctime def iso8601() strftime('%F') end def rfc3339() strftime('%FT%T%:z') end def xmlschema() iso8601 end # :nodoc: def rfc2822() strftime('%a, %-d %b %Y %T %z') end alias_method :rfc822, :rfc2822 def httpdate() new_offset(0).strftime('%a, %d %b %Y %T GMT') end # :nodoc: def jisx0301 if jd < 2405160 strftime('%F') else case jd when 2405160...2419614 g = 'M%02d' % (year - 1867) when 2419614...2424875 g = 'T%02d' % (year - 1911) when 2424875...2447535 g = 'S%02d' % (year - 1925) when 2447535...2458605 g = 'H%02d' % (year - 1988) else g = 'R%02d' % (year - 2018) end g + strftime('.%m.%d') end end def self._parse_iso2(str, hash) # :nodoc: if m = subs(str, /\b(\d{2}|\d{4})?-?w(\d{2})(?:-?(\d))?\b/i) hash[:cwyear] = m[1]&.to_i hash[:cweek] = m[2].to_i hash[:cwday] = m[3]&.to_i true elsif m = subs(str, /-w-(\d)\b/i) hash[:cwday] = m[1].to_i true elsif m = subs(str, /--(\d{2})?-(\d{2})\b/) hash[:mon] = m[1]&.to_i hash[:mday] = m[2].to_i true elsif m = subs(str, /--(\d{2})(\d{2})?\b/) hash[:mon] = m[1].to_i hash[:mday] = m[2]&.to_i true elsif /[,.](\d{2}|\d{4})-\d{3}\b/ !~ str && m = subs(str, /\b(\d{2}|\d{4})-(\d{3})\b/) hash[:year] = m[1].to_i hash[:yday] = m[2].to_i true elsif /\d-\d{3}\b/ !~ str && m = subs(str, /\b-(\d{3})\b/) hash[:yday] = m[1].to_i true end end def self._parse_jis(str, hash) # :nodoc: if m = subs(str, /\b([mtshr])(\d+)\.(\d+)\.(\d+)/i) era = { 'm'=>1867, 't'=>1911, 's'=>1925, 'h'=>1988, 'r'=>2018 }[m[1].downcase] hash[:year] = m[2].to_i + era hash[:mon] = m[3].to_i hash[:mday] = m[4].to_i true end end ABBR_MONTHS_KEYS = Format::ABBR_MONTHS.keys.join('|').freeze private_constant :ABBR_MONTHS_KEYS def self._parse_vms(str, hash) # :nodoc: if m = subs(str, /('?-?\d+)-(#{ABBR_MONTHS_KEYS})[^-\/.]*-('?-?\d+)/iox) s3e(hash, m[3], Format::ABBR_MONTHS[m[2].downcase].to_s, m[1]) true elsif m = subs(str, /\b(#{ABBR_MONTHS_KEYS})[^-\/.]*-('?-?\d+)(?:-('?-?\d+))?/iox) s3e(hash, m[3], Format::ABBR_MONTHS[m[1].downcase].to_s, m[2]) true end end def self._parse_ddd(str, hash) # :nodoc: if str.sub!( /([-+]?)(\d{2,14}) (?: \s* t? \s* (\d{2,6})?(?:[,.](\d*))? )? (?: \s* ( z\b | [-+]\d{1,4}\b | \[[-+]?\d[^\]]*\] ) )? /ix, ' ') case $2.size when 2 if $3.nil? && $4 hash[:sec] = $2[-2, 2].to_i else hash[:mday] = $2[ 0, 2].to_i end when 4 if $3.nil? && $4 hash[:sec] = $2[-2, 2].to_i hash[:min] = $2[-4, 2].to_i else hash[:mon] = $2[ 0, 2].to_i hash[:mday] = $2[ 2, 2].to_i end when 6 if $3.nil? && $4 hash[:sec] = $2[-2, 2].to_i hash[:min] = $2[-4, 2].to_i hash[:hour] = $2[-6, 2].to_i else hash[:year] = ($1 + $2[ 0, 2]).to_i hash[:mon] = $2[ 2, 2].to_i hash[:mday] = $2[ 4, 2].to_i end when 8, 10, 12, 14 if $3.nil? && $4 hash[:sec] = $2[-2, 2].to_i hash[:min] = $2[-4, 2].to_i hash[:hour] = $2[-6, 2].to_i hash[:mday] = $2[-8, 2].to_i if $2.size >= 10 hash[:mon] = $2[-10, 2].to_i end if $2.size == 12 hash[:year] = ($1 + $2[-12, 2]).to_i end if $2.size == 14 hash[:year] = ($1 + $2[-14, 4]).to_i hash[:_comp] = false end else hash[:year] = ($1 + $2[ 0, 4]).to_i hash[:mon] = $2[ 4, 2].to_i hash[:mday] = $2[ 6, 2].to_i hash[:hour] = $2[ 8, 2].to_i if $2.size >= 10 hash[:min] = $2[10, 2].to_i if $2.size >= 12 hash[:sec] = $2[12, 2].to_i if $2.size >= 14 hash[:_comp] = false end when 3 if $3.nil? && $4 hash[:sec] = $2[-2, 2].to_i hash[:min] = $2[-3, 1].to_i else hash[:yday] = $2[ 0, 3].to_i end when 5 if $3.nil? && $4 hash[:sec] = $2[-2, 2].to_i hash[:min] = $2[-4, 2].to_i hash[:hour] = $2[-5, 1].to_i else hash[:year] = ($1 + $2[ 0, 2]).to_i hash[:yday] = $2[ 2, 3].to_i end when 7 if $3.nil? && $4 hash[:sec] = $2[-2, 2].to_i hash[:min] = $2[-4, 2].to_i hash[:hour] = $2[-6, 2].to_i hash[:mday] = $2[-7, 1].to_i else hash[:year] = ($1 + $2[ 0, 4]).to_i hash[:yday] = $2[ 4, 3].to_i end end if $3 if $4 case $3.size when 2, 4, 6 hash[:sec] = $3[-2, 2].to_i hash[:min] = $3[-4, 2].to_i if $3.size >= 4 hash[:hour] = $3[-6, 2].to_i if $3.size >= 6 end else case $3.size when 2, 4, 6 hash[:hour] = $3[ 0, 2].to_i hash[:min] = $3[ 2, 2].to_i if $3.size >= 4 hash[:sec] = $3[ 4, 2].to_i if $3.size >= 6 end end end if $4 hash[:sec_fraction] = Rational($4.to_i, 10**$4.size) end if $5 hash[:zone] = zone = $5 if zone[0] == '[' o, n, = zone[1..-2].split(':') hash[:zone] = n || o if /\A\d/.match? o o = format('+%s', o) end hash[:offset] = zone_to_diff(o) end end true end end private_class_method :_parse_iso2, :_parse_jis, :_parse_vms, :_parse_ddd def self._parse(str, comp=true, limit: 128) check_limit(str, limit) if str.kind_of?(::String) # no-op elsif str.respond_to?(:to_str) str = str.to_str else raise TypeError, "no implicit conversion of #{str.class.name} into String" end # we do not str = str.dup since we do a gsub (instead of gsub!) return _parse_impl(str, :_comp => comp) end def self._iso8601(str, limit: 128) # :nodoc: check_limit(str, limit) h = {} if m = match(/\A\s* (?: (?[-+]?\d{2,} | -) - (?\d{2})? - (?\d{2}) | (?[-+]?\d{2,})? - (?\d{3}) | (?\d{4}|\d{2})? - w(?\d{2}) - (?\d) | -w- (?\d) ) (?: t (?\d{2}) : (?\d{2}) (?: :(?\d{2})(?:[,.](?\d+))?)? (?z | [-+]\d{2}(?::?\d{2})?)? )? \s*\z/ix, str) if m[:mday] h[:mday] = i m[:mday] h[:year] = comp_year69(m[:year]) if m[:year] != "-" if m[:mon] h[:mon] = i m[:mon] else return {} if m[:year] != "-" end elsif m[:yday] h[:yday] = i m[:yday] h[:year] = comp_year69(m[:year]) if m[:year] elsif m[:cwday] h[:cweek] = i m[:cweek] h[:cwday] = i m[:cwday] h[:cwyear] = comp_year69(m[:cwyear]) if m[:cwyear] elsif m[:cwday2] h[:cwday] = i m[:cwday2] end if m[:hour] h[:hour] = i m[:hour] h[:min] = i m[:min] h[:sec] = i m[:sec] if m[:sec] end if sec_fraction = m[:sec_fraction] # JRuby bug fix! h[:sec_fraction] = Rational(sec_fraction.to_i, 10**sec_fraction.size) end set_zone(h, m[:zone]) if m[:zone] elsif m = match(/\A\s* (?: (?[-+]?(?:\d{4}|\d{2})|--) (?\d{2}|-) (?\d{2}) | (?[-+]?(?:\d{4}|\d{2})) (?\d{3}) | -(?\d{3}) | (?\d{4}|\d{2}|-) w(?\d{2}|-) (?\d) ) (?: t? (?\d{2}) (?\d{2}) (?:(?\d{2})(?:[,.](?\d+))?)? (?z | [-+]\d{2}(?:\d{2})?)? )? \s*\z/ix, str) if m[:mday] h[:mday] = i m[:mday] h[:year] = comp_year69(m[:year]) if m[:year] != "--" if m[:mon] != "-" h[:mon] = i m[:mon] else return {} if m[:year] != "--" end elsif m[:yday] h[:yday] = i m[:yday] h[:year] = comp_year69(m[:year]) elsif m[:yday2] h[:yday] = i m[:yday2] elsif m[:cwday] h[:cweek] = i m[:cweek] if m[:cweek] != "-" h[:cwday] = i m[:cwday] h[:cwyear] = comp_year69(m[:cwyear]) if m[:cwyear] != "-" end if m[:hour] h[:hour] = i m[:hour] h[:min] = i m[:min] h[:sec] = i m[:sec] if m[:sec] end if sec_fraction = m[:sec_fraction] # JRuby bug fix! h[:sec_fraction] = Rational(sec_fraction.to_i, 10**sec_fraction.size) end set_zone(h, m[:zone]) if m[:zone] elsif m = match(/\A\s* (?\d{2}) (?: : (?\d{2}) (?: :(?\d{2})(?:[,.](?\d+))? (?z | [-+]\d{2}(?: :?\d{2})?)? )? | (?\d{2}) (?: (?\d{2})(?:[,.](?\d+))? (?z | [-+]\d{2}(?:\d{2})?)? )? ) \s*\z/ix, str) h[:hour] = i m[:hour] h[:min] = i m[:min] h[:sec] = i m[:sec] if m[:sec] if sec_fraction = m[:sec_fraction] # JRuby bug fix! h[:sec_fraction] = Rational(sec_fraction.to_i, 10**sec_fraction.size) end set_zone(h, m[:zone]) if m[:zone] end h end def self._rfc3339(str, limit: 128) # :nodoc: check_limit(str, limit) str = str.to_s if Symbol === str if /\A\s*-?\d{4}-\d{2}-\d{2} # allow minus, anyway (t|\s) \d{2}:\d{2}:\d{2}(\.\d+)? (z|[-+]\d{2}:\d{2})\s*\z/ix.match?(str) _parse(str) else {} end end def self._xmlschema(str, limit: 128) # :nodoc: check_limit(str, limit) if m = match(/\A\s*(-?\d{4,})(?:-(\d{2})(?:-(\d{2}))?)? (?:t (\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?)? (z|[-+]\d{2}:\d{2})?\s*\z/ix, str) hash = Hash.new hash[:year] = m[1].to_i hash[:mon] = m[2].to_i if m[2] hash[:mday] = m[3].to_i if m[3] hash[:hour] = m[4].to_i if m[4] hash[:min] = m[5].to_i if m[5] hash[:sec] = m[6].to_i if m[6] if sf = m[7] hash[:sec_fraction] = Rational(sf.to_i, 10**sf.size) end if zone = m[8] hash[:zone] = zone hash[:offset] = zone_to_diff(zone) end hash elsif m = match(/\A\s*(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))? (z|[-+]\d{2}:\d{2})?\s*\z/ix, str) hash = Hash.new hash[:hour] = m[1].to_i if m[1] hash[:min] = m[2].to_i if m[2] hash[:sec] = m[3].to_i if m[3] if sf = m[4] hash[:sec_fraction] = Rational(sf.to_i, 10**sf.size) end if zone = m[5] hash[:zone] = zone hash[:offset] = zone_to_diff(zone) end hash elsif m = match(/\A\s*(?:--(\d{2})(?:-(\d{2}))?|---(\d{2})) (z|[-+]\d{2}:\d{2})?\s*\z/ix, str) hash = Hash.new hash[:mon] = m[1].to_i if m[1] hash[:mday] = m[2].to_i if m[2] hash[:mday] = m[3].to_i if m[3] if zone = m[4] hash[:zone] = zone hash[:offset] = zone_to_diff(zone) end hash else {} end end def self._rfc2822(str, limit: 128) # :nodoc: check_limit(str, limit) str = str.to_s if Symbol === str if m = match(/\A\s*(?:(?:#{Format::ABBR_DAYS.keys.join('|')})\s*,\s+)? \d{1,2}\s+ (?:#{Format::ABBR_MONTHS.keys.join('|')})\s+ -?(\d{2,})\s+ # allow minus, anyway \d{2}:\d{2}(:\d{2})?\s* (?:[-+]\d{4}|ut|gmt|e[sd]t|c[sd]t|m[sd]t|p[sd]t|[a-ik-z])\s*\z/iox, str) hash = _parse(str, false) if m[1].size < 4 if hash[:year] < 50 hash[:year] += 2000 elsif hash[:year] < 1000 hash[:year] += 1900 end end hash else {} end end class << self; alias_method :_rfc822, :_rfc2822 end def self._httpdate(str) # :nodoc: if /\A\s*(#{Format::ABBR_DAYS.keys.join('|')})\s*,\s+ \d{2}\s+ (#{Format::ABBR_MONTHS.keys.join('|')})\s+ -?\d{4}\s+ # allow minus, anyway \d{2}:\d{2}:\d{2}\s+ gmt\s*\z/iox.match?(str) _rfc2822(str) elsif /\A\s*(#{Format::DAYS.keys.join('|')})\s*,\s+ \d{2}\s*-\s* (#{Format::ABBR_MONTHS.keys.join('|')})\s*-\s* \d{2}\s+ \d{2}:\d{2}:\d{2}\s+ gmt\s*\z/iox.match?(str) _parse(str) elsif /\A\s*(#{Format::ABBR_DAYS.keys.join('|')})\s+ (#{Format::ABBR_MONTHS.keys.join('|')})\s+ \d{1,2}\s+ \d{2}:\d{2}:\d{2}\s+ \d{4}\s*\z/iox.match?(str) _parse(str) else {} end end def self._jisx0301(str, limit: 128) # :nodoc: check_limit(str, limit) str = str.to_s if Symbol === str if /\A\s*[mtshr]?\d{2}\.\d{2}\.\d{2} (t (\d{2}:\d{2}(:\d{2}([,.]\d*)?)? (z|[-+]\d{2}(:?\d{2})?)?)?)?\s*\z/ix.match?(str) if /\A\s*\d/.match?(str) _parse(str.sub(/\A\s*(\d)/, 'h\1')) else _parse(str) end else _iso8601(str) end end def self.set_zone(hash, zone) # :nodoc: hash[:zone] = zone hash[:offset] = zone_to_diff(zone) end private_class_method :set_zone def self.check_limit(str, limit) return if str.nil? if String === str # ok elsif Symbol === str str = str.to_s elsif str.respond_to?(:to_str) str = str.to_str else raise TypeError, "no implicit conversion of #{str.class.name} into String" end limit = 2 ** 64 if limit.nil? slen = str.length if slen > limit raise ArgumentError.new("string length #{slen} exceeds the limit #{limit}") end end private_class_method :check_limit end class DateTime < Date def iso8601_timediv(n) # :nodoc: n = n.to_i strftime('T%T' + if n < 1 '' else '.%0*d' % [n, (sec_fraction * 10**n).round] end + '%:z') end private :iso8601_timediv def iso8601(n=0) super() + iso8601_timediv(n) end def rfc3339(n=0) iso8601(n) end def xmlschema(n=0) iso8601(n) end # :nodoc: def jisx0301(n=0) super() + iso8601_timediv(n) end end