From f3168b6be999dc10e7b63fa2281e7d9265087609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Thu, 18 Jan 2018 14:09:46 +0100 Subject: [PATCH] Add time zones support (#5324) * Add cache for last zone to Time::Location#lookup * Implement Time::Location including timezone data loader Remove representation of floating time from `Time` (formerly expressed as `Time::Kind::Unspecified`). Floating time should not be represented as an instance of `Time` to avoid undefined operations through type safety (see #5332). Breaking changes: * Calls to `Time.new` and `Time.now` are now in the local time zone by default. * `Time.parse`, `Time::Format.new` and `Time::Format.parse` don't specify a default location. If none is included in the time format and no default argument is provided, the parse method wil raise an exception because there is no way to know how such a value should be represented as an instance of `Time`. Applications expecting time values without time zone should provide default location to apply in such a case. * Implement custom zip file reader to remove depenencies * Add location cache for `Location.load` * Rename `Location.local` to `.load_local` and make `local` a class property * Fix env ZONEINFO * Fix example code string representation of local Time instance * Time zone implementation for win32 This adds basic support for using the new time zone model on windows. * `Crystal::System::Time.zone_sources` returns an empty array because Windows does not include a copy of the tz database. * `Crystal::System::Time.load_localtime` creates a local time zone `Time::Location` based on data provided by `GetTimeZoneInformation`. * A mapping from Windows time zone names to identifiers used by the IANA timezone database is included as well as an automated generator for that file. * Add stubs for methods with file acces Trying to load a location from a file will fail because `File` is not yet ported to windows. --- scripts/generate_windows_zone_names.cr | 76 +++ spec/std/data/zoneinfo.zip | Bin 0 -> 366776 bytes spec/std/data/zoneinfo/Foo/Bar | Bin 0 -> 118 bytes spec/std/file_spec.cr | 10 +- spec/std/http/http_spec.cr | 14 +- spec/std/json/mapping_spec.cr | 2 +- spec/std/time/location_spec.cr | 327 ++++++++++ spec/std/time/spec_helper.cr | 22 + spec/std/time/time_spec.cr | 663 +++++++++++++-------- spec/std/yaml/mapping_spec.cr | 4 +- spec/std/yaml/serialization_spec.cr | 2 +- src/crystal/system/time.cr | 10 +- src/crystal/system/unix/time.cr | 43 +- src/crystal/system/win32/time.cr | 124 +++- src/crystal/system/win32/zone_names.cr | 145 +++++ src/file/stat.cr | 2 +- src/http/common.cr | 2 +- src/json/from_json.cr | 2 +- src/lib_c/x86_64-windows-msvc/c/winbase.cr | 4 + src/time.cr | 273 ++++----- src/time/format.cr | 8 +- src/time/format/formatter.cr | 58 +- src/time/format/parser.cr | 48 +- src/time/location.cr | 295 +++++++++ src/time/location/loader.cr | 236 ++++++++ src/yaml/from_yaml.cr | 2 +- src/yaml/to_yaml.cr | 5 +- 27 files changed, 1874 insertions(+), 503 deletions(-) create mode 100644 scripts/generate_windows_zone_names.cr create mode 100644 spec/std/data/zoneinfo.zip create mode 100644 spec/std/data/zoneinfo/Foo/Bar create mode 100644 spec/std/time/location_spec.cr create mode 100644 spec/std/time/spec_helper.cr create mode 100644 src/crystal/system/win32/zone_names.cr create mode 100644 src/time/location.cr create mode 100644 src/time/location/loader.cr diff --git a/scripts/generate_windows_zone_names.cr b/scripts/generate_windows_zone_names.cr new file mode 100644 index 000000000000..4c0a40c7c369 --- /dev/null +++ b/scripts/generate_windows_zone_names.cr @@ -0,0 +1,76 @@ +# This script generates the file src/crystal/system/win32/zone_names.cr +# that contains mappings for windows time zone names based on the values +# found in http://unicode.org/cldr/data/common/supplemental/windowsZones.xml + +require "http/client" +require "xml" +require "../src/compiler/crystal/formatter" + +WINDOWS_ZONE_NAMES_SOURCE = "http://unicode.org/cldr/data/common/supplemental/windowsZones.xml" +TARGET_FILE = File.join(__DIR__, "..", "src", "crystal", "system", "win32", "zone_names.cr") + +response = HTTP::Client.get(WINDOWS_ZONE_NAMES_SOURCE) + +# Simple redirection resolver +# TODO: Needs to be replaced by proper redirect handling that should be provided by `HTTP::Client` +if (300..399).includes?(response.status_code) && (location = response.headers["Location"]?) + response = HTTP::Client.get(location) +end + +xml = XML.parse(response.body) + +nodes = xml.xpath_nodes("/supplementalData/windowsZones/mapTimezones/mapZone[@territory=001]") + +entries = [] of {key: String, zones: {String, String}, tzdata_name: String} + +nodes.each do |node| + location = Time::Location.load(node["type"]) + next unless location + time = Time.now(location).at_beginning_of_year + zone1 = time.zone + zone2 = (time + 6.months).zone + + if zone1.offset > zone2.offset + # southern hemisphere + zones = {zone2.name, zone1.name} + else + # northern hemisphere + zones = {zone1.name, zone2.name} + end + + entries << {key: node["other"], zones: zones, tzdata_name: location.name} +rescue err : Time::Location::InvalidLocationNameError + pp err +end + +# sort by IANA database identifier +entries.sort_by! &.[:tzdata_name] + +hash_items = String.build do |io| + entries.each do |entry| + entry[:key].inspect(io) + io << " => " + entry[:zones].inspect(io) + io << ", # " << entry[:tzdata_name] << "\n" + end +end + +source = <<-CRYSTAL + # This file was automatically generated by running: + # + # scripts/generate_windows_zone_names.cr + # + # DO NOT EDIT + + module Crystal::System::Time + # These mappings for windows time zone names are based on + # #{WINDOWS_ZONE_NAMES_SOURCE} + WINDOWS_ZONE_NAMES = { + #{hash_items} + } + end + CRYSTAL + +source = Crystal.format(source) + +File.write(TARGET_FILE, source) diff --git a/spec/std/data/zoneinfo.zip b/spec/std/data/zoneinfo.zip new file mode 100644 index 0000000000000000000000000000000000000000..b60ae7f98e7562e1243d6171a52deb1ea68e7dea GIT binary patch literal 366776 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 z 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 time).should eq(0) end - it %(changes timezone with ENV["TZ"]) do - old_tz = ENV["TZ"]? - - begin - ENV["TZ"] = "America/New_York" - offset1 = Time.local_offset_in_minutes - - ENV["TZ"] = "Europe/Berlin" - offset2 = Time.local_offset_in_minutes - - offset1.should_not eq(offset2) - ensure - ENV["TZ"] = old_tz - end - end - it "does diff of utc vs local time" do local = Time.now utc = local.to_utc @@ -719,6 +856,36 @@ describe Time do (local - utc).should eq(0.seconds) end + describe "#in" do + it "changes location" do + location = Time::Location.fixed(3600) + location2 = Time::Location.fixed(12345) + time1 = Time.now(location) + time1.location.should eq(location) + + time2 = time1.in(location2) + time2.should eq(time1) + time2.location.should eq(location2) + end + end + + it "#to_s" do + with_zoneinfo do + time = Time.new(2017, 11, 25, 22, 6, 17, location: Time::Location::UTC) + time.to_s.should eq "2017-11-25 22:06:17 UTC" + + time = Time.new(2017, 11, 25, 22, 6, 17, location: Time::Location.fixed(-7200)) + time.to_s.should eq "2017-11-25 22:06:17 -02:00" + + time = Time.new(2017, 11, 25, 22, 6, 17, location: Time::Location.fixed(-7259)) + time.to_s.should eq "2017-11-25 22:06:17 -02:00:59" + + location = Time::Location.load("Europe/Berlin") + time = Time.new(2017, 11, 25, 22, 6, 17, location: location) + time.to_s.should eq "2017-11-25 22:06:17 +01:00 Europe/Berlin" + end + end + describe "days in month" do it "returns days for valid month and year" do Time.days_in_month(2016, 2).should eq(29) diff --git a/spec/std/yaml/mapping_spec.cr b/spec/std/yaml/mapping_spec.cr index 809b3c949ff1..f649fb7ff2e1 100644 --- a/spec/std/yaml/mapping_spec.cr +++ b/spec/std/yaml/mapping_spec.cr @@ -269,8 +269,8 @@ describe "YAML mapping" do it "parses yaml with Time::Format converter" do yaml = YAMLWithTime.from_yaml("---\nvalue: 2014-10-31 23:37:16\n") yaml.value.should be_a(Time) - yaml.value.to_s.should eq("2014-10-31 23:37:16") - yaml.value.should eq(Time.new(2014, 10, 31, 23, 37, 16)) + yaml.value.to_s.should eq("2014-10-31 23:37:16 UTC") + yaml.value.should eq(Time.utc(2014, 10, 31, 23, 37, 16)) yaml.to_yaml.should eq("---\nvalue: 2014-10-31 23:37:16\n") end diff --git a/spec/std/yaml/serialization_spec.cr b/spec/std/yaml/serialization_spec.cr index fa5026a046f9..801726d383f3 100644 --- a/spec/std/yaml/serialization_spec.cr +++ b/spec/std/yaml/serialization_spec.cr @@ -153,7 +153,7 @@ describe "YAML serialization" do ctx = YAML::ParseContext.new nodes = YAML::Nodes.parse("--- 2014-01-02\n...\n").nodes.first value = Time::Format.new("%F").from_yaml(ctx, nodes) - value.should eq(Time.new(2014, 1, 2)) + value.should eq(Time.utc(2014, 1, 2)) end it "deserializes union" do diff --git a/src/crystal/system/time.cr b/src/crystal/system/time.cr index e310995ac317..340a9e06f19e 100644 --- a/src/crystal/system/time.cr +++ b/src/crystal/system/time.cr @@ -1,13 +1,15 @@ module Crystal::System::Time - # Returns the number of seconds that you must add to UTC to get local time. - # *seconds* are measured from `0001-01-01 00:00:00`. - # def self.compute_utc_offset(seconds : Int64) : Int32 - # Returns the current UTC time measured in `{seconds, nanoseconds}` # since `0001-01-01 00:00:00`. # def self.compute_utc_seconds_and_nanoseconds : {Int64, Int32} # def self.monotonic : {Int64, Int32} + + # Returns a list of paths where time zone data should be looked up. + # def self.zone_sources : Enumerable(String) + + # Returns the system's current local time zone + # def self.load_localtime : ::Time::Location? end {% if flag?(:win32) %} diff --git a/src/crystal/system/unix/time.cr b/src/crystal/system/unix/time.cr index be8ea2dd7f07..df0075d8d39c 100644 --- a/src/crystal/system/unix/time.cr +++ b/src/crystal/system/unix/time.cr @@ -13,28 +13,6 @@ require "c/time" module Crystal::System::Time UnixEpochInSeconds = 62135596800_i64 - def self.compute_utc_offset(seconds : Int64) : Int32 - LibC.tzset - offset = nil - - {% if LibC.methods.includes?("daylight".id) %} - if LibC.daylight == 0 - # current TZ doesn't have any DST, neither in past, present or future - offset = -LibC.timezone.to_i - end - {% end %} - - unless offset - seconds_from_epoch = LibC::TimeT.new(seconds - UnixEpochInSeconds) - # current TZ may have DST, either in past, present or future - ret = LibC.localtime_r(pointerof(seconds_from_epoch), out tm) - raise Errno.new("localtime_r") if ret.null? - offset = tm.tm_gmtoff.to_i - end - - offset - end - def self.compute_utc_seconds_and_nanoseconds : {Int64, Int32} {% if LibC.methods.includes?("clock_gettime".id) %} ret = LibC.clock_gettime(LibC::CLOCK_REALTIME, out timespec) @@ -62,6 +40,27 @@ module Crystal::System::Time {% end %} end + # Many systems use /usr/share/zoneinfo, Solaris 2 has + # /usr/share/lib/zoneinfo, IRIX 6 has /usr/lib/locale/TZ. + ZONE_SOURCES = { + "/usr/share/zoneinfo/", + "/usr/share/lib/zoneinfo/", + "/usr/lib/locale/TZ/", + } + LOCALTIME = "/etc/localtime" + + def self.zone_sources : Enumerable(String) + ZONE_SOURCES + end + + def self.load_localtime : ::Time::Location? + if ::File.exists?(LOCALTIME) + ::File.open(LOCALTIME) do |file| + ::Time::Location.read_zoneinfo("Local", file) + end + end + end + {% if flag?(:darwin) %} @@mach_timebase_info : LibC::MachTimebaseInfo? diff --git a/src/crystal/system/win32/time.cr b/src/crystal/system/win32/time.cr index 55c6d147f39d..36d5aeed8780 100644 --- a/src/crystal/system/win32/time.cr +++ b/src/crystal/system/win32/time.cr @@ -1,5 +1,6 @@ require "c/winbase" require "winerror" +require "./zone_names" module Crystal::System::Time # Win32 epoch is 1601-01-01 00:00:00 UTC @@ -11,13 +12,7 @@ module Crystal::System::Time NANOSECONDS_PER_SECOND = 1_000_000_000 FILETIME_TICKS_PER_SECOND = NANOSECONDS_PER_SECOND / NANOSECONDS_PER_FILETIME_TICK - # TODO: For now, this method returns the UTC offset currently in place, ignoring *seconds*. - def self.compute_utc_offset(seconds : Int64) : Int32 - ret = LibC.GetTimeZoneInformation(out zone_information) - raise WinError.new("GetTimeZoneInformation") if ret == -1 - - zone_information.bias.to_i32 * -60 - end + BIAS_TO_OFFSET_FACTOR = -60 def self.compute_utc_seconds_and_nanoseconds : {Int64, Int32} # TODO: Needs a check if `GetSystemTimePreciseAsFileTime` is actually available (only >= Windows 8) @@ -47,4 +42,119 @@ module Crystal::System::Time {ticks / @@performance_frequency, (ticks.remainder(NANOSECONDS_PER_SECOND) * NANOSECONDS_PER_SECOND / @@performance_frequency).to_i32} end + + def self.load_localtime : ::Time::Location? + if LibC.GetTimeZoneInformation(out info) != LibC::TIME_ZONE_ID_UNKNOWN + initialize_location_from_TZI(info) + end + end + + def self.zone_sources : Enumerable(String) + [] of String + end + + private def self.initialize_location_from_TZI(info) + stdname, dstname = normalize_zone_names(info) + + if info.standardDate.wMonth == 0_u16 + # No DST + zone = ::Time::Location::Zone.new(stdname, info.bias * BIAS_TO_OFFSET_FACTOR, false) + return ::Time::Location.new("Local", [zone]) + end + + zones = [ + ::Time::Location::Zone.new(stdname, (info.bias + info.standardBias) * BIAS_TO_OFFSET_FACTOR, false), + ::Time::Location::Zone.new(dstname, (info.bias + info.daylightBias) * BIAS_TO_OFFSET_FACTOR, true), + ] + + first_date = info.standardDate + second_date = info.daylightDate + first_index = 0_u8 + second_index = 1_u8 + + if info.standardDate.wMonth > info.daylightDate.wMonth + first_date, second_date = second_date, first_date + first_index, second_index = second_index, first_index + end + + transitions = [] of ::Time::Location::ZoneTransition + + current_year = ::Time.utc_now.year + + (current_year - 100).upto(current_year + 100) do |year| + tstamp = calculate_switchdate_in_year(year, first_date) - (zones[second_index].offset) + transitions << ::Time::Location::ZoneTransition.new(tstamp, first_index, first_index == 0, false) + + tstamp = calculate_switchdate_in_year(year, second_date) - (zones[first_index].offset) + transitions << ::Time::Location::ZoneTransition.new(tstamp, second_index, second_index == 0, false) + end + + ::Time::Location.new("Local", zones, transitions) + end + + # Calculates the day of a DST switch in year *year* by extrapolating the date given in + # *systemtime* (for the current year). + # + # Returns the number of seconds since UNIX epoch (Jan 1 1970) in the local time zone. + private def self.calculate_switchdate_in_year(year, systemtime) + # Windows specifies daylight savings information in "day in month" format: + # wMonth is month number (1-12) + # wDayOfWeek is appropriate weekday (Sunday=0 to Saturday=6) + # wDay is week within the month (1 to 5, where 5 is last week of the month) + # wHour, wMinute and wSecond are absolute time + day = 1 + + time = ::Time.utc(year, systemtime.wMonth, day, systemtime.wHour, systemtime.wMinute, systemtime.wSecond) + i = systemtime.wDayOfWeek.to_i32 - time.day_of_week.to_i32 + + if i < 0 + i += 7 + end + + day += i + + week = systemtime.wDay - 1 + + if week < 4 + day += week * 7 + else + # "Last" instance of the day. + day += 4 * 7 + if day > ::Time.days_in_month(year, systemtime.wMonth) + day -= 7 + end + end + + time += (day - 1).days + + time.epoch + end + + # Normalizes the names of the standard and dst zones. + private def self.normalize_zone_names(info : LibC::TIME_ZONE_INFORMATION) : Tuple(String, String) + stdname = String.from_utf16(info.standardName.to_unsafe) + + if normalized_names = WINDOWS_ZONE_NAMES[stdname]? + return normalized_names + end + + dstname = String.from_utf16(info.daylightName.to_unsafe) + + if english_name = translate_zone_name(stdname, dstname) + if normalized_names = WINDOWS_ZONE_NAMES[english_name]? + return normalized_names + end + end + + # As a last resort, return the raw names as provided by TIME_ZONE_INFORMATION. + # They are most probably localized and we couldn't find a translation. + return stdname, dstname + end + + # Searches the registry for an English name of a time zone named *stdname* or *dstname* + # and returns the English name. + private def self.translate_zone_name(stdname, dstname) + # TODO: Needs implementation once there is access to the registry. + nil + end end diff --git a/src/crystal/system/win32/zone_names.cr b/src/crystal/system/win32/zone_names.cr new file mode 100644 index 000000000000..b29df3597772 --- /dev/null +++ b/src/crystal/system/win32/zone_names.cr @@ -0,0 +1,145 @@ +# This file was automatically generated by running: +# +# scripts/generate_windows_zone_names.cr +# +# DO NOT EDIT + +module Crystal::System::Time + # These mappings for windows time zone names are based on + # http://unicode.org/cldr/data/common/supplemental/windowsZones.xml + WINDOWS_ZONE_NAMES = { + "Egypt Standard Time" => {"EET", "EET"}, # Africa/Cairo + "Morocco Standard Time" => {"WET", "WEST"}, # Africa/Casablanca + "South Africa Standard Time" => {"SAST", "SAST"}, # Africa/Johannesburg + "W. Central Africa Standard Time" => {"WAT", "WAT"}, # Africa/Lagos + "E. Africa Standard Time" => {"EAT", "EAT"}, # Africa/Nairobi + "Libya Standard Time" => {"EET", "EET"}, # Africa/Tripoli + "Namibia Standard Time" => {"WAT", "WAST"}, # Africa/Windhoek + "Aleutian Standard Time" => {"HST", "HDT"}, # America/Adak + "Alaskan Standard Time" => {"AKST", "AKDT"}, # America/Anchorage + "Tocantins Standard Time" => {"BRT", "BRT"}, # America/Araguaina + "Paraguay Standard Time" => {"PYT", "PYST"}, # America/Asuncion + "Bahia Standard Time" => {"BRT", "BRT"}, # America/Bahia + "SA Pacific Standard Time" => {"COT", "COT"}, # America/Bogota + "Argentina Standard Time" => {"ART", "ART"}, # America/Buenos_Aires + "Eastern Standard Time (Mexico)" => {"EST", "EST"}, # America/Cancun + "Venezuela Standard Time" => {"VET", "VET"}, # America/Caracas + "SA Eastern Standard Time" => {"GFT", "GFT"}, # America/Cayenne + "Central Standard Time" => {"CST", "CDT"}, # America/Chicago + "Mountain Standard Time (Mexico)" => {"MST", "MDT"}, # America/Chihuahua + "Central Brazilian Standard Time" => {"AMT", "AMST"}, # America/Cuiaba + "Mountain Standard Time" => {"MST", "MDT"}, # America/Denver + "Greenland Standard Time" => {"WGT", "WGST"}, # America/Godthab + "Turks And Caicos Standard Time" => {"AST", "AST"}, # America/Grand_Turk + "Central America Standard Time" => {"CST", "CST"}, # America/Guatemala + "Atlantic Standard Time" => {"AST", "ADT"}, # America/Halifax + "Cuba Standard Time" => {"CST", "CDT"}, # America/Havana + "US Eastern Standard Time" => {"EST", "EDT"}, # America/Indianapolis + "SA Western Standard Time" => {"BOT", "BOT"}, # America/La_Paz + "Pacific Standard Time" => {"PST", "PDT"}, # America/Los_Angeles + "Central Standard Time (Mexico)" => {"CST", "CDT"}, # America/Mexico_City + "Saint Pierre Standard Time" => {"PMST", "PMDT"}, # America/Miquelon + "Montevideo Standard Time" => {"UYT", "UYT"}, # America/Montevideo + "Eastern Standard Time" => {"EST", "EDT"}, # America/New_York + "US Mountain Standard Time" => {"MST", "MST"}, # America/Phoenix + "Haiti Standard Time" => {"EST", "EST"}, # America/Port-au-Prince + "Canada Central Standard Time" => {"CST", "CST"}, # America/Regina + "Pacific SA Standard Time" => {"CLT", "CLST"}, # America/Santiago + "E. South America Standard Time" => {"BRT", "BRST"}, # America/Sao_Paulo + "Newfoundland Standard Time" => {"NST", "NDT"}, # America/St_Johns + "Pacific Standard Time (Mexico)" => {"PST", "PDT"}, # America/Tijuana + "Central Asia Standard Time" => {"+06", "+06"}, # Asia/Almaty + "Jordan Standard Time" => {"EET", "EEST"}, # Asia/Amman + "Arabic Standard Time" => {"AST", "AST"}, # Asia/Baghdad + "Azerbaijan Standard Time" => {"+04", "+04"}, # Asia/Baku + "SE Asia Standard Time" => {"ICT", "ICT"}, # Asia/Bangkok + "Altai Standard Time" => {"+07", "+07"}, # Asia/Barnaul + "Middle East Standard Time" => {"EET", "EEST"}, # Asia/Beirut + "India Standard Time" => {"IST", "IST"}, # Asia/Calcutta + "Transbaikal Standard Time" => {"+09", "+09"}, # Asia/Chita + "Sri Lanka Standard Time" => {"+0530", "+0530"}, # Asia/Colombo + "Syria Standard Time" => {"EET", "EEST"}, # Asia/Damascus + "Bangladesh Standard Time" => {"BDT", "BDT"}, # Asia/Dhaka + "Arabian Standard Time" => {"GST", "GST"}, # Asia/Dubai + "West Bank Standard Time" => {"EET", "EEST"}, # Asia/Hebron + "W. Mongolia Standard Time" => {"HOVT", "HOVST"}, # Asia/Hovd + "North Asia East Standard Time" => {"+08", "+08"}, # Asia/Irkutsk + "Israel Standard Time" => {"IST", "IDT"}, # Asia/Jerusalem + "Afghanistan Standard Time" => {"AFT", "AFT"}, # Asia/Kabul + "Russia Time Zone 11" => {"+12", "+12"}, # Asia/Kamchatka + "Pakistan Standard Time" => {"PKT", "PKT"}, # Asia/Karachi + "Nepal Standard Time" => {"NPT", "NPT"}, # Asia/Katmandu + "North Asia Standard Time" => {"+07", "+07"}, # Asia/Krasnoyarsk + "Magadan Standard Time" => {"+11", "+11"}, # Asia/Magadan + "N. Central Asia Standard Time" => {"+07", "+07"}, # Asia/Novosibirsk + "Omsk Standard Time" => {"+06", "+06"}, # Asia/Omsk + "North Korea Standard Time" => {"KST", "KST"}, # Asia/Pyongyang + "Myanmar Standard Time" => {"MMT", "MMT"}, # Asia/Rangoon + "Arab Standard Time" => {"AST", "AST"}, # Asia/Riyadh + "Sakhalin Standard Time" => {"+11", "+11"}, # Asia/Sakhalin + "Korea Standard Time" => {"KST", "KST"}, # Asia/Seoul + "China Standard Time" => {"CST", "CST"}, # Asia/Shanghai + "Singapore Standard Time" => {"SGT", "SGT"}, # Asia/Singapore + "Russia Time Zone 10" => {"+11", "+11"}, # Asia/Srednekolymsk + "Taipei Standard Time" => {"CST", "CST"}, # Asia/Taipei + "West Asia Standard Time" => {"+05", "+05"}, # Asia/Tashkent + "Georgian Standard Time" => {"+04", "+04"}, # Asia/Tbilisi + "Iran Standard Time" => {"IRST", "IRDT"}, # Asia/Tehran + "Tokyo Standard Time" => {"JST", "JST"}, # Asia/Tokyo + "Tomsk Standard Time" => {"+07", "+07"}, # Asia/Tomsk + "Ulaanbaatar Standard Time" => {"ULAT", "ULAST"}, # Asia/Ulaanbaatar + "Vladivostok Standard Time" => {"+10", "+10"}, # Asia/Vladivostok + "Yakutsk Standard Time" => {"+09", "+09"}, # Asia/Yakutsk + "Ekaterinburg Standard Time" => {"+05", "+05"}, # Asia/Yekaterinburg + "Caucasus Standard Time" => {"+04", "+04"}, # Asia/Yerevan + "Azores Standard Time" => {"AZOT", "AZOST"}, # Atlantic/Azores + "Cape Verde Standard Time" => {"CVT", "CVT"}, # Atlantic/Cape_Verde + "Greenwich Standard Time" => {"GMT", "GMT"}, # Atlantic/Reykjavik + "Cen. Australia Standard Time" => {"ACST", "ACDT"}, # Australia/Adelaide + "E. Australia Standard Time" => {"AEST", "AEST"}, # Australia/Brisbane + "AUS Central Standard Time" => {"ACST", "ACST"}, # Australia/Darwin + "Aus Central W. Standard Time" => {"ACWST", "ACWST"}, # Australia/Eucla + "Tasmania Standard Time" => {"AEST", "AEDT"}, # Australia/Hobart + "Lord Howe Standard Time" => {"LHST", "LHDT"}, # Australia/Lord_Howe + "W. Australia Standard Time" => {"AWST", "AWST"}, # Australia/Perth + "AUS Eastern Standard Time" => {"AEST", "AEDT"}, # Australia/Sydney + "UTC" => {"GMT", "GMT"}, # Etc/GMT + "UTC-11" => {"-11", "-11"}, # Etc/GMT+11 + "Dateline Standard Time" => {"-12", "-12"}, # Etc/GMT+12 + "UTC-02" => {"-02", "-02"}, # Etc/GMT+2 + "UTC-08" => {"-08", "-08"}, # Etc/GMT+8 + "UTC-09" => {"-09", "-09"}, # Etc/GMT+9 + "UTC+12" => {"+12", "+12"}, # Etc/GMT-12 + "UTC+13" => {"+13", "+13"}, # Etc/GMT-13 + "Astrakhan Standard Time" => {"+04", "+04"}, # Europe/Astrakhan + "W. Europe Standard Time" => {"CET", "CEST"}, # Europe/Berlin + "GTB Standard Time" => {"EET", "EEST"}, # Europe/Bucharest + "Central Europe Standard Time" => {"CET", "CEST"}, # Europe/Budapest + "E. Europe Standard Time" => {"EET", "EEST"}, # Europe/Chisinau + "Turkey Standard Time" => {"+03", "+03"}, # Europe/Istanbul + "Kaliningrad Standard Time" => {"EET", "EET"}, # Europe/Kaliningrad + "FLE Standard Time" => {"EET", "EEST"}, # Europe/Kiev + "GMT Standard Time" => {"GMT", "BST"}, # Europe/London + "Belarus Standard Time" => {"+03", "+03"}, # Europe/Minsk + "Russian Standard Time" => {"MSK", "MSK"}, # Europe/Moscow + "Romance Standard Time" => {"CET", "CEST"}, # Europe/Paris + "Russia Time Zone 3" => {"+04", "+04"}, # Europe/Samara + "Saratov Standard Time" => {"+04", "+04"}, # Europe/Saratov + "Central European Standard Time" => {"CET", "CEST"}, # Europe/Warsaw + "Mauritius Standard Time" => {"MUT", "MUT"}, # Indian/Mauritius + "Samoa Standard Time" => {"WSST", "WSDT"}, # Pacific/Apia + "New Zealand Standard Time" => {"NZST", "NZDT"}, # Pacific/Auckland + "Bougainville Standard Time" => {"BST", "BST"}, # Pacific/Bougainville + "Chatham Islands Standard Time" => {"CHAST", "CHADT"}, # Pacific/Chatham + "Easter Island Standard Time" => {"EAST", "EASST"}, # Pacific/Easter + "Fiji Standard Time" => {"FJT", "FJST"}, # Pacific/Fiji + "Central Pacific Standard Time" => {"SBT", "SBT"}, # Pacific/Guadalcanal + "Hawaiian Standard Time" => {"HST", "HST"}, # Pacific/Honolulu + "Line Islands Standard Time" => {"LINT", "LINT"}, # Pacific/Kiritimati + "Marquesas Standard Time" => {"MART", "MART"}, # Pacific/Marquesas + "Norfolk Standard Time" => {"NFT", "NFT"}, # Pacific/Norfolk + "West Pacific Standard Time" => {"PGT", "PGT"}, # Pacific/Port_Moresby + "Tonga Standard Time" => {"+13", "+14"}, # Pacific/Tongatapu + + } +end diff --git a/src/file/stat.cr b/src/file/stat.cr index cbce74db9b95..9bce15b19d69 100644 --- a/src/file/stat.cr +++ b/src/file/stat.cr @@ -202,7 +202,7 @@ class File end {% else %} private def time(value) - Time.new value, Time::Kind::Utc + Time.new value, Time::Location::UTC end {% end %} end diff --git a/src/http/common.cr b/src/http/common.cr index abd751f879e4..0c2d0b371b01 100644 --- a/src/http/common.cr +++ b/src/http/common.cr @@ -225,7 +225,7 @@ module HTTP def self.parse_time(time_str : String) : Time? DATE_PATTERNS.each do |pattern| begin - return Time.parse(time_str, pattern, kind: Time::Kind::Utc) + return Time.parse(time_str, pattern, location: Time::Location::UTC) rescue Time::Format::Error end end diff --git a/src/json/from_json.cr b/src/json/from_json.cr index 56b396a5e80b..362bc55120b9 100644 --- a/src/json/from_json.cr +++ b/src/json/from_json.cr @@ -237,7 +237,7 @@ end struct Time::Format def from_json(pull : JSON::PullParser) string = pull.read_string - parse(string) + parse(string, Time::Location::UTC) end end diff --git a/src/lib_c/x86_64-windows-msvc/c/winbase.cr b/src/lib_c/x86_64-windows-msvc/c/winbase.cr index 642a8ca8f1f0..fdcc39cde2b2 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winbase.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winbase.cr @@ -41,6 +41,10 @@ lib LibC daylightBias : LONG end + TIME_ZONE_ID_UNKNOWN = 0_u32 + TIME_ZONE_ID_STANDARD = 1_u32 + TIME_ZONE_ID_DAYLIGHT = 2_u32 + fun GetTimeZoneInformation(tz_info : TIME_ZONE_INFORMATION*) : DWORD fun GetSystemTimeAsFileTime(time : FILETIME*) fun GetSystemTimePreciseAsFileTime(time : FILETIME*) diff --git a/src/time.cr b/src/time.cr index 79ee7ba971b6..8d5a902cb54d 100644 --- a/src/time.cr +++ b/src/time.cr @@ -1,6 +1,6 @@ require "crystal/system/time" -# `Time` represents an instance in time. Here are some examples: +# `Time` represents an instance in incremental time. Here are some examples: # # ### Basic Usage # @@ -15,11 +15,12 @@ require "crystal/system/time" # time.second # => 30 # time.monday? # => true # -# # Creating a time instance with a date only -# Time.new(2016, 2, 15) # => 2016-02-15 00:00:00 +# # Creating a time instance with a date only in local timezone `Time::Location.local`. +# # The examples show an offset of `+01:00` but that can vary depending on +# Time.new(2016, 2, 15) # => 2016-02-15 00:00:00 +01:00 # # # Specifying a time -# Time.new(2016, 2, 15, 10, 20, 30) # => 2016-02-15 10:20:30 +# Time.new(2016, 2, 15, 10, 20, 30) # => 2016-02-15 10:20:30 +01:00 # # # Creating a time instance in UTC # Time.utc(2016, 2, 15, 10, 20, 30) # => 2016-02-15 10:20:30 UTC @@ -39,7 +40,7 @@ require "crystal/system/time" # ### Calculation # # ``` -# Time.new(2015, 10, 10) - 5.days # => 2015-10-05 00:00:00 +# Time.new(2015, 10, 10) - 5.days # => 2015-10-05 00:00:00 +01:00 # # # Time calculation returns a Time::Span instance # span = Time.new(2015, 10, 10) - Time.new(2015, 9, 10) @@ -56,6 +57,9 @@ require "crystal/system/time" # span.hours # => 1 # ``` struct Time + class FloatingTimeConversionError < Exception + end + include Comparable(self) # :nodoc: @@ -129,37 +133,9 @@ struct Time Saturday end - # `Kind` represents a specified time zone. - # - # Initializing a `Time` instance with specified `Kind`: - # - # ``` - # time = Time.new(2016, 2, 15, 21, 1, 10, 0, Time::Kind::Local) - # ``` - # - # Alternatively, you can switch the `Kind` for any instance: - # - # ``` - # time.to_utc # => 2016-02-15 21:00:00 UTC - # time.to_local # => 2016-02-16 05:01:10 +0800 - # ``` - # - # Inspection: - # - # ``` - # time.local? # => true - # time.utc? # => false - # ``` - # - enum Kind - Unspecified = 0 - Utc = 1 - Local = 2 - end - @seconds : Int64 @nanoseconds : Int32 - @kind : Kind + @location : Location # Returns a clock from an unspecified starting point, but strictly linearly # increasing. This clock should be independent from discontinuous jumps in the @@ -181,12 +157,12 @@ struct Time monotonic - start end - def self.new - seconds, nanoseconds, offset = Time.compute_seconds_nanoseconds_and_offset - new(seconds: seconds + offset, nanoseconds: nanoseconds, kind: Kind::Local) + def self.new(location = Location.local) + seconds, nanoseconds = Crystal::System::Time.compute_utc_seconds_and_nanoseconds + new(seconds: seconds, nanoseconds: nanoseconds, location: location) end - def self.new(year, month, day, hour = 0, minute = 0, second = 0, *, nanosecond = 0, kind = Kind::Unspecified) + def self.new(year, month, day, hour = 0, minute = 0, second = 0, *, nanosecond = 0, location = Location.local) unless 1 <= year <= 9999 && 1 <= month <= 12 && 1 <= day <= Time.days_in_month(year, month) && @@ -205,19 +181,22 @@ struct Time SECONDS_PER_MINUTE * minute + second - new(seconds: seconds, nanoseconds: nanosecond.to_i, kind: kind) + # Normalize internal representation to UTC + seconds = seconds - zone_offset_at(seconds, location) + + new(seconds: seconds, nanoseconds: nanosecond.to_i, location: location) end {% unless flag?(:win32) %} # :nodoc: - def self.new(time : LibC::Timespec, kind = Kind::Unspecified) + def self.new(time : LibC::Timespec, location = Location.local) seconds = UNIX_SECONDS + time.tv_sec nanoseconds = time.tv_nsec.to_i - new(seconds: seconds, nanoseconds: nanoseconds, kind: kind) + new(seconds: seconds, nanoseconds: nanoseconds, location: location) end {% end %} - def initialize(*, @seconds : Int64, @nanoseconds : Int32, @kind : Kind) + def initialize(*, @seconds : Int64, @nanoseconds : Int32, @location : Location) unless 0 <= @nanoseconds < NANOSECONDS_PER_SECOND raise ArgumentError.new "Invalid time: nanoseconds out of range" end @@ -249,12 +228,12 @@ struct Time # Returns a new `Time` instance at the specified time in UTC time zone. def self.utc(year, month, day, hour = 0, minute = 0, second = 0, *, nanosecond = 0) : Time - new(year, month, day, hour, minute, second, nanosecond: nanosecond, kind: Kind::Utc) + new(year, month, day, hour, minute, second, nanosecond: nanosecond, location: Location::UTC) end # Returns a new `Time` instance at the specified time in UTC time zone. def self.utc(*, seconds : Int64, nanoseconds : Int32) : Time - new(seconds: seconds, nanoseconds: nanoseconds, kind: Kind::Utc) + new(seconds: seconds, nanoseconds: nanoseconds, location: Location::UTC) end def clone : self @@ -299,7 +278,7 @@ struct Time day = maxday end - temp = Time.new(year, month, day, kind: kind) + temp = Time.new(year, month, day, location: location) temp + time_of_day end @@ -322,39 +301,33 @@ struct Time raise ArgumentError.new "Invalid time" end - Time.new(seconds: seconds, nanoseconds: nanoseconds.to_i, kind: kind) + Time.new(seconds: seconds, nanoseconds: nanoseconds.to_i, location: location) end # Returns the amount of time between *other* and `self`. # # The amount can be negative if `self` is a `Time` that happens before *other*. def -(other : Time) : Time::Span - if local? && other.utc? - self - other.to_local - elsif utc? && other.local? - self - other.to_utc - else - Span.new( - seconds: total_seconds - other.total_seconds, - nanoseconds: nanosecond - other.nanosecond, - ) - end + Span.new( + seconds: total_seconds - other.total_seconds, + nanoseconds: nanosecond - other.nanosecond, + ) end - # Returns the current time in the local time zone. - def self.now : Time - new + # Returns the current time in the time zone currently observed in *location*, + # using local time zone by default. + def self.now(location = Location.local) : Time + new(location) end # Returns the current time in UTC time zone. def self.utc_now : Time - seconds, nanoseconds = compute_seconds_and_nanoseconds - utc(seconds: seconds, nanoseconds: nanoseconds) + now(Location::UTC) end # Returns a copy of `self` with time-of-day components (hour, minute, ...) set to zero. def date : Time - Time.new(year, month, day, kind: kind) + Time.new(year, month, day, location: location) end # Returns the year number (in the Common Era). @@ -374,17 +347,17 @@ struct Time # Returns the hour of the day (`0..23`). def hour : Int32 - ((total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR).to_i + ((offset_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR).to_i end # Returns the minute of the hour (`0..59`). def minute : Int32 - ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE).to_i + ((offset_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE).to_i end # Returns the second of the minute (`0..59`). def second : Int32 - (total_seconds % SECONDS_PER_MINUTE).to_i + (offset_seconds % SECONDS_PER_MINUTE).to_i end # Returns the millisecond of the second (`0..999`). @@ -399,12 +372,12 @@ struct Time # Returns how much time has passed since midnight of this day. def time_of_day : Time::Span - Span.new(nanoseconds: NANOSECONDS_PER_SECOND * (total_seconds % SECONDS_PER_DAY) + nanosecond) + Span.new(nanoseconds: NANOSECONDS_PER_SECOND * (offset_seconds % SECONDS_PER_DAY) + nanosecond) end # Returns the day of the week (`Sunday..Saturday`). def day_of_week : Time::DayOfWeek - value = ((total_seconds / SECONDS_PER_DAY) + 1) % 7 + value = ((offset_seconds / SECONDS_PER_DAY) + 1) % 7 DayOfWeek.new value.to_i end @@ -413,31 +386,43 @@ struct Time year_month_day_day_year[3] end - # Returns `Kind` (UTC/local) of the instance. - def kind : Kind - @kind + # Returns `Location` of the instance. + def location : Location + @location + end + + # Returns the time zone in effect in `location` at this point in time. + def zone + location.lookup(self) end - # Returns `true` if `Kind` is set to *Utc*. + # Returns the offset from UTC (in seconds) in `location` at this point in time. + def offset : Int32 + zone.offset + end + + # Returns `true` if `#location` equals to `Location::UTC`. def utc? : Bool - kind == Kind::Utc + location.utc? end - # Returns `true` if `Kind` is set to *Local*. + # Returns `true` if this time's `#location` equals to the current + # local location as returned by `Location.local`. + # + # Since the system's settings may change during a programm's runtime, + # the result may not be identical between different invocations. def local? : Bool - kind == Kind::Local + location.local? end def <=>(other : self) - if utc? && other.local? - self <=> other.to_utc - elsif local? && other.utc? - to_utc <=> other - else - cmp = total_seconds <=> other.total_seconds - cmp = nanosecond <=> other.nanosecond if cmp == 0 - cmp - end + cmp = total_seconds <=> other.total_seconds + cmp = nanosecond <=> other.nanosecond if cmp == 0 + cmp + end + + def ==(other : self) + total_seconds == other.total_seconds && nanosecond == other.nanosecond end def_hash total_seconds, nanosecond @@ -481,13 +466,16 @@ struct Time end def inspect(io : IO) - Format.new("%F %T").format(self, io) - case when utc? - io << " UTC" - when local? - Format.new(" %:z").format(self, io) + to_s "%F %T UTC", io + else + if offset % 60 == 0 + to_s "%F %T %:z", io + else + to_s "%F %T %::z", io + end + io << ' ' << location.name unless location.fixed? || location.name == "Local" end io end @@ -512,10 +500,10 @@ struct Time # `Time::Format`). # # ``` - # Time.parse("2016-04-05", "%F") # => 2016-04-05 00:00:00 + # Time.parse("2016-04-05", "%F") # => 2016-04-05 00:00:00 +01:00 # ``` - def self.parse(time : String, pattern : String, kind = Time::Kind::Unspecified) : Time - Format.new(pattern, kind).parse(time) + def self.parse(time : String, pattern : String, location = nil) : Time + Format.new(pattern, location).parse(time) end # Returns the number of seconds since the Epoch for this time. @@ -525,7 +513,7 @@ struct Time # time.epoch # => 1452567845 # ``` def epoch : Int64 - (to_utc.total_seconds - UNIX_SECONDS).to_i64 + (total_seconds - UNIX_SECONDS).to_i64 end # Returns the number of milliseconds since the Epoch for this time. @@ -549,13 +537,31 @@ struct Time epoch.to_f + nanosecond.to_f / 1e9 end + # Retuns this instance of time represented in `Location` *location*. + # + # ``` + # time = Time.new(2018, 1, 7, 15, 51, location: Time::Location.load("Europe/Berlin")) + # time # => 2018-01-07 15:51:00 +01:00 Europe/Berlin + # time = time.in(Time::Location.load("Australia/Sydney")) + # time # => 2018-01-08 01:51:00 +11:00 Australia/Sydney + # ``` + def in(location : Location) : Time + return self if location == self.location + + Time.new( + seconds: total_seconds, + nanoseconds: nanosecond, + location: location + ) + end + # Returns a copy of this `Time` converted to UTC. def to_utc : Time if utc? self else Time.utc( - seconds: total_seconds - Time.compute_offset, + seconds: total_seconds, nanoseconds: nanosecond ) end @@ -566,11 +572,7 @@ struct Time if local? self else - Time.new( - seconds: total_seconds + Time.compute_offset, - nanoseconds: nanosecond, - kind: Kind::Local, - ) + in(Location.local) end end @@ -590,13 +592,13 @@ struct Time end end - def_at_beginning(year) { Time.new(year, 1, 1, kind: kind) } - def_at_beginning(semester) { Time.new(year, ((month - 1) / 6) * 6 + 1, 1, kind: kind) } - def_at_beginning(quarter) { Time.new(year, ((month - 1) / 3) * 3 + 1, 1, kind: kind) } - def_at_beginning(month) { Time.new(year, month, 1, kind: kind) } - def_at_beginning(day) { Time.new(year, month, day, kind: kind) } - def_at_beginning(hour) { Time.new(year, month, day, hour, kind: kind) } - def_at_beginning(minute) { Time.new(year, month, day, hour, minute, kind: kind) } + def_at_beginning(year) { Time.new(year, 1, 1, location: location) } + def_at_beginning(semester) { Time.new(year, ((month - 1) / 6) * 6 + 1, 1, location: location) } + def_at_beginning(quarter) { Time.new(year, ((month - 1) / 3) * 3 + 1, 1, location: location) } + def_at_beginning(month) { Time.new(year, month, 1, location: location) } + def_at_beginning(day) { Time.new(year, month, day, location: location) } + def_at_beginning(hour) { Time.new(year, month, day, hour, location: location) } + def_at_beginning(minute) { Time.new(year, month, day, hour, minute, location: location) } # Returns the time when the week that includes `self` starts. def at_beginning_of_week : Time @@ -608,7 +610,7 @@ struct Time end end - def_at_end(year) { Time.new(year, 12, 31, 23, 59, 59, nanosecond: 999_999_999, kind: kind) } + def_at_end(year) { Time.new(year, 12, 31, 23, 59, 59, nanosecond: 999_999_999, location: location) } # Returns the time when the half-year that includes `self` ends. def at_end_of_semester : Time @@ -618,7 +620,7 @@ struct Time else month, day = 12, 31 end - Time.new(year, month, day, 23, 59, 59, nanosecond: 999_999_999, kind: kind) + Time.new(year, month, day, 23, 59, 59, nanosecond: 999_999_999, location: location) end # Returns the time when the quarter-year that includes `self` ends. @@ -633,10 +635,10 @@ struct Time else month, day = 12, 31 end - Time.new(year, month, day, 23, 59, 59, nanosecond: 999_999_999, kind: kind) + Time.new(year, month, day, 23, 59, 59, nanosecond: 999_999_999, location: location) end - def_at_end(month) { Time.new(year, month, Time.days_in_month(year, month), 23, 59, 59, nanosecond: 999_999_999, kind: kind) } + def_at_end(month) { Time.new(year, month, Time.days_in_month(year, month), 23, 59, 59, nanosecond: 999_999_999, location: location) } # Returns the time when the week that includes `self` ends. def at_end_of_week : Time @@ -648,14 +650,14 @@ struct Time end end - def_at_end(day) { Time.new(year, month, day, 23, 59, 59, nanosecond: 999_999_999, kind: kind) } - def_at_end(hour) { Time.new(year, month, day, hour, 59, 59, nanosecond: 999_999_999, kind: kind) } - def_at_end(minute) { Time.new(year, month, day, hour, minute, 59, nanosecond: 999_999_999, kind: kind) } + def_at_end(day) { Time.new(year, month, day, 23, 59, 59, nanosecond: 999_999_999, location: location) } + def_at_end(hour) { Time.new(year, month, day, hour, 59, 59, nanosecond: 999_999_999, location: location) } + def_at_end(minute) { Time.new(year, month, day, hour, minute, 59, nanosecond: 999_999_999, location: location) } # Returns the midday (12:00) of the day represented by `self`. def at_midday : Time year, month, day = year_month_day_day_year - Time.new(year, month, day, 12, 0, 0, nanosecond: 0, kind: kind) + Time.new(year, month, day, 12, 0, 0, nanosecond: 0, location: location) end {% for name in DayOfWeek.constants %} @@ -682,11 +684,15 @@ struct Time @seconds end + protected def offset_seconds + @seconds + offset + end + private def year_month_day_day_year m = 1 days = DAYS_MONTH - totaldays = total_seconds / SECONDS_PER_DAY + totaldays = offset_seconds / SECONDS_PER_DAY num400 = totaldays / DAYS_PER_400_YEARS totaldays -= num400 * DAYS_PER_400_YEARS @@ -726,35 +732,20 @@ struct Time {year.to_i, month.to_i, day.to_i, day_year.to_i} end - # Returns the local time offset in minutes relative to GMT. - # - # ``` - # # Assume in Argentina, where it's GMT-3 - # Time.local_offset_in_minutes # => -180 - # ``` - def self.local_offset_in_minutes - compute_offset / SECONDS_PER_MINUTE - end - - # Returns `seconds, nanoseconds, offset` where - # `offset` is the number of seconds for now's timezone offset. - protected def self.compute_seconds_nanoseconds_and_offset - seconds, nanoseconds = compute_seconds_and_nanoseconds - offset = compute_offset(seconds) - {seconds, nanoseconds, offset} - end - - protected def self.compute_offset - seconds, nanoseconds = compute_seconds_and_nanoseconds - compute_offset(seconds) - end + protected def self.zone_offset_at(seconds, location) + unix = seconds - UNIX_SECONDS + zone, range = location.lookup_with_boundaries(unix) - private def self.compute_offset(seconds) - Crystal::System::Time.compute_utc_offset(seconds) - end + if zone.offset != 0 + case utc = unix - zone.offset + when .<(range[0]) + zone = location.lookup(range[0] - 1) + when .>=(range[1]) + zone = location.lookup(range[1]) + end + end - private def self.compute_seconds_and_nanoseconds - Crystal::System::Time.compute_utc_seconds_and_nanoseconds + zone.offset end end diff --git a/src/time/format.cr b/src/time/format.cr index ab80930fa879..7fb185ad7e4c 100644 --- a/src/time/format.cr +++ b/src/time/format.cr @@ -66,15 +66,15 @@ struct Time::Format getter pattern : String # Creates a new `Time::Format` with the given *pattern*. The given time - # *kind* will be used when parsing a `Time` and no time zone is found in it. - def initialize(@pattern : String, @kind = Time::Kind::Unspecified) + # *location* will be used when parsing a `Time` and no time zone is found in it. + def initialize(@pattern : String, @location : Location? = nil) end # Parses a string into a `Time`. - def parse(string, kind = @kind) : Time + def parse(string, location = @location) : Time parser = Parser.new(string) parser.visit(pattern) - parser.time(kind) + parser.time(location) end # Turns a `Time` into a `String`. diff --git a/src/time/format/formatter.cr b/src/time/format/formatter.cr index 9f8c5dc50728..7ecb8b208591 100644 --- a/src/time/format/formatter.cr +++ b/src/time/format/formatter.cr @@ -147,51 +147,51 @@ struct Time::Format io << time.epoch end - def time_zone - case time.kind - when Time::Kind::Utc, Time::Kind::Unspecified - io << "+0000" - when Time::Kind::Local - negative, hours, minutes = local_time_zone_info - io << (negative ? "-" : "+") - io << "0" if hours < 10 - io << hours - io << "0" if minutes < 10 - io << minutes + def time_zone(with_seconds = false) + negative, hours, minutes, seconds = local_time_zone_info + io << (negative ? '-' : '+') + io << '0' if hours < 10 + io << hours + io << '0' if minutes < 10 + io << minutes + if with_seconds + io << '0' if seconds < 10 + io << seconds end end - def time_zone_colon - case time.kind - when Time::Kind::Utc, Time::Kind::Unspecified - io << "+00:00" - when Time::Kind::Local - negative, hours, minutes = local_time_zone_info - io << (negative ? "-" : "+") - io << "0" if hours < 10 - io << hours - io << ":" - io << "0" if minutes < 10 - io << minutes + def time_zone_colon(with_seconds = false) + negative, hours, minutes, seconds = local_time_zone_info + io << (negative ? '-' : '+') + io << '0' if hours < 10 + io << hours + io << ':' + io << '0' if minutes < 10 + io << minutes + if with_seconds + io << ':' + io << '0' if seconds < 10 + io << seconds end end def time_zone_colon_with_seconds - time_zone_colon - io << ":00" + time_zone_colon(with_seconds: true) end def local_time_zone_info - minutes = Time.local_offset_in_minutes - if minutes < 0 - minutes = -minutes + offset = time.offset + if offset < 0 + offset = -offset negative = true else negative = false end + seconds = offset % 60 + minutes = offset / 60 hours = minutes / 60 minutes = minutes % 60 - {negative, hours, minutes} + {negative, hours, minutes, seconds} end def char(char) diff --git a/src/time/format/parser.cr b/src/time/format/parser.cr index e1d7cde04c8a..6b23fd1a60e4 100644 --- a/src/time/format/parser.cr +++ b/src/time/format/parser.cr @@ -4,6 +4,7 @@ struct Time::Format include Pattern @epoch : Int64? + @location : Location? def initialize(string) @reader = Char::Reader.new(string) @@ -17,26 +18,19 @@ struct Time::Format @pm = false end - def time(kind = Time::Kind::Unspecified) + def time(location : Location? = nil) @hour += 12 if @pm - time_kind = @kind || kind - if epoch = @epoch return Time.epoch(epoch) end - time = Time.new @year, @month, @day, @hour, @minute, @second, nanosecond: @nanosecond, kind: time_kind - - if offset_in_minutes = @offset_in_minutes - time -= offset_in_minutes.minutes if offset_in_minutes != 0 - - if (offset_in_minutes != 0) || (kind == Time::Kind::Local && !time.local?) - time = time.to_local - end + location = @location || location + if location.nil? + raise "Time format did not include time zone and no default location provided" end - time + Time.new @year, @month, @day, @hour, @minute, @second, nanosecond: @nanosecond, location: location end def year @@ -232,16 +226,14 @@ struct Time::Format @epoch = consume_number_i64(19) * (epoch_negative ? -1 : 1) end - def time_zone + def time_zone(with_seconds = false) case char = current_char when 'Z' - @offset_in_minutes = 0 - @kind = Time::Kind::Utc + @location = Location::UTC next_char when 'U' if next_char == 'T' && next_char == 'C' - @offset_in_minutes = 0 - @kind = Time::Kind::Utc + @location = Location::UTC next_char else raise "Invalid timezone" @@ -255,7 +247,7 @@ struct Time::Format char = next_char raise "Invalid timezone" unless char.ascii_number? - hours = 10*hours + char.to_i + hours = 10 * hours + char.to_i char = next_char char = next_char if char == ':' @@ -264,10 +256,22 @@ struct Time::Format char = next_char raise "Invalid timezone" unless char.ascii_number? - minutes = 10*minutes + char.to_i + minutes = 10 * minutes + char.to_i + + if with_seconds + char = next_char + char = next_char if char == ':' + raise "Invalid timezone" unless char.ascii_number? + seconds = char.to_i - @offset_in_minutes = sign * (60*hours + minutes) - @kind = Time::Kind::Utc + char = next_char + raise "Invalid timezone" unless char.ascii_number? + seconds = 10 * seconds + char.to_i + else + seconds = 0 + end + + @location = Location.fixed(sign * (3600 * hours + 60 * minutes + seconds)) char = next_char if @reader.has_next? @@ -288,7 +292,7 @@ struct Time::Format end def time_zone_colon_with_seconds - time_zone + time_zone(with_seconds: true) end def char(char) diff --git a/src/time/location.cr b/src/time/location.cr new file mode 100644 index 000000000000..f9237235dfc9 --- /dev/null +++ b/src/time/location.cr @@ -0,0 +1,295 @@ +require "./location/loader" + +# `Location` represents a specific time zone. +# +# It can be either a time zone from the IANA Time Zone database, +# a fixed offset, or `UTC`. +# +# Creating a location from timezone data: +# ``` +# location = Time::Location.load("Europe/Berlin") +# ``` +# +# Initializing a `Time` instance with specified `Location`: +# +# ``` +# time = Time.new(2016, 2, 15, 21, 1, 10, location) +# ``` +# +# Alternatively, you can switch the `Location` for any `Time` instance: +# +# ``` +# time.location # => Europe/Berlin +# time.in(Time::Location.load("Asia/Jerusalem")) +# time.location # => Asia/Jerusalem +# ``` +# +# There are also a few special conversions: +# ``` +# time.to_utc # == time.in(Location::UTC) +# time.to_local # == time.in(Location.local) +# ``` +class Time::Location + class InvalidLocationNameError < Exception + getter name, source + + def initialize(@name : String, @source : String? = nil) + msg = "Invalid location name: #{name}" + msg += " in #{source}" if source + super msg + end + end + + class InvalidTimezoneOffsetError < Exception + def initialize(offset : Int) + super "Invalid time zone offset: #{offset}" + end + end + + struct Zone + UTC = new "UTC", 0, false + + getter name : String + getter offset : Int32 + getter? dst : Bool + + def initialize(@name : String, @offset : Int32, @dst : Bool) + # Maximium offets of IANA timezone database are -12:00 and +14:00. + # +/-24 hours allows a generous padding for unexpected offsets. + # TODO: Maybe reduce to Int16 (+/- 18 hours). + raise InvalidTimezoneOffsetError.new(offset) if offset >= SECONDS_PER_DAY || offset <= -SECONDS_PER_DAY + end + + def inspect(io : IO) + io << "Time::Zone<" + io << offset + io << ", " << name + io << " (DST)" if dst? + io << '>' + end + end + + # :nodoc: + record ZoneTransition, when : Int64, index : UInt8, standard : Bool, utc : Bool do + getter? standard, utc + + def inspect(io : IO) + io << "Time::ZoneTransition<" + io << '#' << index << ", " + Time.epoch(self.when).to_s("%F %T", io) + io << ", STD" if standard? + io << ", UTC" if utc? + io << '>' + end + end + + # Describes the Coordinated Universal Time (UTC). + UTC = new "UTC", [Zone::UTC] + + property name : String + property zones : Array(Zone) + + # Most lookups will be for the current time. + # To avoid the binary search through tx, keep a + # static one-element cache that gives the correct + # zone for the time when the Location was created. + # The units for @cached_range are seconds + # since January 1, 1970 UTC, to match the argument + # to `#lookup`. + @cached_range : Tuple(Int64, Int64) + @cached_zone : Zone + + # Creates a `Location` instance named *name* with fixed *offset*. + def self.fixed(name : String, offset : Int32) + new name, [Zone.new(name, offset, false)] + end + + # Creates a `Location` instance with fixed *offset*. + def self.fixed(offset : Int32) + span = offset.abs.seconds + name = sprintf("%s%02d:%02d", offset.sign < 0 ? '-' : '+', span.hours, span.minutes) + fixed name, offset + end + + # Returns the `Location` with the given name. + # + # This uses a list of paths to look for timezone data. Each path can + # either point to a directory or an uncompressed ZIP file. + # System-specific default paths are provided by the implementation. + # + # The first timezone data matching the given name that is successfully loaded + # and parsed is returned. + # A custom lookup path can be set as environment variable `ZONEINFO`. + # + # Special names: + # * `"UTC"` and empty string `""` return `Location::UTC` + # * `"Local"` returns `Location.local` + # + # This method caches files based on the modification time, so subsequent loads + # of the same location name will return the same instance of `Location` unless + # the timezone database has been updated in between. + # + # Example: + # `ZONEINFO=/path/to/zoneinfo.zip crystal eval 'pp Location.load("Custom/Location")'` + def self.load(name : String) : Location + case name + when "", "UTC" + UTC + when "Local" + local + when .includes?(".."), .starts_with?('/'), .starts_with?('\\') + # No valid IANA Time Zone name contains a single dot, + # much less dot dot. Likewise, none begin with a slash. + raise InvalidLocationNameError.new(name) + else + if zoneinfo = ENV["ZONEINFO"]? + if location = load_from_dir_or_zip(name, zoneinfo) + return location + else + raise InvalidLocationNameError.new(name, zoneinfo) + end + end + + if location = load(name, Crystal::System::Time.zone_sources) + return location + end + + raise InvalidLocationNameError.new(name) + end + end + + # Returns the location representing the local time zone. + # + # The value is loaded on first access based on the current application environment (see `.load_local` for details). + class_property(local : Location) { load_local } + + # Loads the local location described by the the current application environment. + # + # It consults the environment variable `ENV["TZ"]` to find the time zone to use. + # * `"UTC"` and empty string `""` return `Location::UTC` + # * `"Foo/Bar"` tries to load the zoneinfo from known system locations - such as `/usr/share/zoneinfo/Foo/Bar`, + # `/usr/share/lib/zoneinfo/Foo/Bar` or `/usr/lib/locale/TZ/Foo/Bar` on unix-based operating systems. + # See `Location.load` for details. + # * If `ENV["TZ"]` is not set, the system's local timezone data will be used (`/etc/localtime` on unix-based systems). + # * If no time zone data could be found, `Location::UTC` is returned. + def self.load_local : Location + case tz = ENV["TZ"]? + when "", "UTC" + UTC + when Nil + if localtime = Crystal::System::Time.load_localtime + return localtime + end + else + if location = load?(tz, Crystal::System::Time.zone_sources) + return location + end + end + + UTC + end + + # :nodoc: + def initialize(@name : String, @zones : Array(Zone), @transitions = [] of ZoneTransition) + @cached_zone = lookup_first_zone + @cached_range = {Int64::MIN, @zones.size <= 1 ? Int64::MAX : Int64::MIN} + end + + protected def transitions + @transitions + end + + def to_s(io : IO) + io << name + end + + def inspect(io : IO) + io << "Time::Location<" + to_s(io) + io << '>' + end + + def_equals_and_hash name, zones, transitions + + # Returns the time zone in use at `time`. + def lookup(time : Time) : Zone + lookup(time.epoch) + end + + # Returns the time zone in use at `epoch` (time in seconds since UNIX epoch). + def lookup(epoch : Int) : Zone + unless @cached_range[0] <= epoch < @cached_range[1] + @cached_zone, @cached_range = lookup_with_boundaries(epoch) + end + + @cached_zone + end + + # :nodoc: + def lookup_with_boundaries(epoch : Int) + case + when zones.empty? + return Zone::UTC, {Int64::MIN, Int64::MAX} + when transitions.empty? || epoch < transitions.first.when + return lookup_first_zone, {Int64::MIN, transitions[0]?.try(&.when) || Int64::MAX} + else + tx_index = transitions.bsearch_index do |transition| + transition.when > epoch + end || transitions.size + + tx_index -= 1 unless tx_index == 0 + transition = transitions[tx_index] + range_end = transitions[tx_index + 1]?.try(&.when) || Int64::MAX + + return zones[transition.index], {transition.when, range_end} + end + end + + # Returns the time zone to use for times before the first transition + # time, or when there are no transition times. + # + # The reference implementation in localtime.c from + # http:#www.iana.org/time-zones/repository/releases/tzcode2013g.tar.gz + # implements the following algorithm for these cases: + # 1) If the first zone is unused by the transitions, use it. + # 2) Otherwise, if there are transition times, and the first + # transition is to a zone in daylight time, find the first + # non-daylight-time zone before and closest to the first transition + # zone. + # 3) Otherwise, use the first zone that is not daylight time, if + # there is one. + # 4) Otherwise, use the first zone. + private def lookup_first_zone + unless transitions.any? { |tx| tx.index == 0 } + return zones.first + end + + if (tx = transitions[0]?) && zones[tx.index].dst? + index = tx.index + while index > 0 + index -= 1 + zone = zones[index] + return zone unless zone.dst? + end + end + + first_zone_without_dst = zones.find { |tx| !tx.dst? } + + first_zone_without_dst || zones.first + end + + # Returns `true` if this location equals to `UTC`. + def utc? : Bool + self == UTC + end + + # Returns `true` if this location equals to `Location.local`. + def local? : Bool + self == Location.local + end + + # Returns `true` if this location has a fixed offset. + def fixed? : Bool + zones.size <= 1 + end +end diff --git a/src/time/location/loader.cr b/src/time/location/loader.cr new file mode 100644 index 000000000000..4ebb0e8048f2 --- /dev/null +++ b/src/time/location/loader.cr @@ -0,0 +1,236 @@ +class Time::Location + @@location_cache = {} of String => NamedTuple(time: Time, location: Location) + + class InvalidTZDataError < Exception + def self.initialize(message : String? = "Malformed time zone information", cause : Exception? = nil) + super(message, cause) + end + end + + # :nodoc: + def self.load?(name : String, sources : Enumerable(String)) + if source = find_zoneinfo_file(name, sources) + load_from_dir_or_zip(name, source) + end + end + + # :nodoc: + def self.load(name : String, sources : Enumerable(String)) + if source = find_zoneinfo_file(name, sources) + load_from_dir_or_zip(name, source) || raise InvalidLocationNameError.new(name, source) + end + end + + # :nodoc: + def self.load_from_dir_or_zip(name : String, source : String) + {% if flag?(:win32) %} + raise NotImplementedError.new("Time::Location.load_from_dir_or_zip") + {% else %} + if source.ends_with?(".zip") + open_file_cached(name, source) do |file| + read_zip_file(name, file) do |io| + read_zoneinfo(name, io) + end + end + else + path = File.expand_path(name, source) + open_file_cached(name, path) do |file| + read_zoneinfo(name, file) + end + end + {% end %} + end + + private def self.open_file_cached(name : String, path : String) + return nil unless File.exists?(path) + + mtime = File.stat(path).mtime + if (cache = @@location_cache[name]?) && cache[:time] == mtime + return cache[:location] + else + File.open(path) do |file| + location = yield file + if location + @@location_cache[name] = {time: mtime, location: location} + + return location + end + end + end + end + + # :nodoc: + def self.find_zoneinfo_file(name : String, sources : Enumerable(String)) + {% if flag?(:win32) %} + raise NotImplementedError.new("Time::Location.find_zoneinfo_file") + {% else %} + sources.each do |source| + if source.ends_with?(".zip") + return source if File.exists?(source) + else + path = File.expand_path(name, source) + return source if File.exists?(path) + end + end + {% end %} + end + + # Parse "zoneinfo" time zone file. + # This is the standard file format used by most operating systems. + # See https://data.iana.org/time-zones/tz-link.html, https://github.com/eggert/tz, tzfile(5) + + # :nodoc: + def self.read_zoneinfo(location_name : String, io : IO) + raise InvalidTZDataError.new unless io.read_string(4) == "TZif" + + # 1-byte version, then 15 bytes of padding + version = io.read_byte + raise InvalidTZDataError.new unless {0_u8, '2'.ord, '3'.ord}.includes?(version) + io.skip(15) + + # six big-endian 32-bit integers: + # number of UTC/local indicators + # number of standard/wall indicators + # number of leap seconds + # number of transition times + # number of local time zones + # number of characters of time zone abbrev strings + + num_utc_local = read_int32(io) + num_std_wall = read_int32(io) + num_leap_seconds = read_int32(io) + num_transitions = read_int32(io) + num_local_time_zones = read_int32(io) + abbrev_length = read_int32(io) + + transitionsdata = read_buffer(io, num_transitions * 4) + + # Time zone indices for transition times. + transition_indexes = Bytes.new(num_transitions) + io.read_fully(transition_indexes) + + zonedata = read_buffer(io, num_local_time_zones * 6) + + abbreviations = read_buffer(io, abbrev_length) + + leap_second_time_pairs = Bytes.new(num_leap_seconds) + io.read_fully(leap_second_time_pairs) + + isstddata = Bytes.new(num_std_wall) + io.read_fully(isstddata) + + isutcdata = Bytes.new(num_utc_local) + io.read_fully(isutcdata) + + # If version == 2 or 3, the entire file repeats, this time using + # 8-byte ints for txtimes and leap seconds. + # We won't need those until 2106. + + zones = Array(Zone).new(num_local_time_zones) do + offset = read_int32(zonedata) + is_dst = zonedata.read_byte != 0_u8 + name_idx = zonedata.read_byte + raise InvalidTZDataError.new unless name_idx && name_idx < abbreviations.size + abbreviations.pos = name_idx + name = abbreviations.gets(Char::ZERO, chomp: true) + raise InvalidTZDataError.new unless name + Zone.new(name, offset, is_dst) + end + + transitions = Array(ZoneTransition).new(num_transitions) do |transition_id| + time = read_int32(transitionsdata).to_i64 + zone_idx = transition_indexes[transition_id] + raise InvalidTZDataError.new unless zone_idx < zones.size + + isstd = !{nil, 0_u8}.includes? isstddata[transition_id]? + isutc = !{nil, 0_u8}.includes? isstddata[transition_id]? + + ZoneTransition.new(time, zone_idx, isstd, isutc) + end + + new(location_name, zones, transitions) + end + + private def self.read_int32(io : IO) + io.read_bytes(Int32, IO::ByteFormat::BigEndian) + end + + private def self.read_buffer(io : IO, size : Int) + buffer = Bytes.new(size) + io.read_fully(buffer) + IO::Memory.new(buffer) + end + + # :nodoc: + CENTRAL_DIRECTORY_HEADER_SIGNATURE = 0x02014b50 + # :nodoc: + END_OF_CENTRAL_DIRECTORY_HEADER_SIGNATURE = 0x06054b50 + # :nodoc: + ZIP_TAIL_SIZE = 22 + # :nodoc: + LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50 + # :nodoc: + COMPRESSION_METHOD_UNCOMPRESSED = 0_i16 + + # This method loads an entry from an uncompressed zip file. + # See http://www.onicos.com/staff/iz/formats/zip.html for ZIP format layout + private def self.read_zip_file(name : String, file : IO::FileDescriptor) + file.seek -ZIP_TAIL_SIZE, IO::Seek::End + + if file.read_bytes(Int32, IO::ByteFormat::LittleEndian) != END_OF_CENTRAL_DIRECTORY_HEADER_SIGNATURE + raise InvalidTZDataError.new("corrupt zip file") + end + + file.skip 6 + num_entries = file.read_bytes(Int16, IO::ByteFormat::LittleEndian) + file.skip 4 + + file.pos = file.read_bytes(Int32, IO::ByteFormat::LittleEndian) + + num_entries.times do + break if file.read_bytes(Int32, IO::ByteFormat::LittleEndian) != CENTRAL_DIRECTORY_HEADER_SIGNATURE + + file.skip 6 + compression_method = file.read_bytes(Int16, IO::ByteFormat::LittleEndian) + file.skip 12 + uncompressed_size = file.read_bytes(Int32, IO::ByteFormat::LittleEndian) + filename_length = file.read_bytes(Int16, IO::ByteFormat::LittleEndian) + extra_field_length = file.read_bytes(Int16, IO::ByteFormat::LittleEndian) + file_comment_length = file.read_bytes(Int16, IO::ByteFormat::LittleEndian) + file.skip 8 + local_file_header_pos = file.read_bytes(Int32, IO::ByteFormat::LittleEndian) + filename = file.read_string(filename_length) + + unless filename == name + file.skip extra_field_length + file_comment_length + next + end + + unless compression_method == COMPRESSION_METHOD_UNCOMPRESSED + raise InvalidTZDataError.new("Unsupported compression for #{name}") + end + + file.pos = local_file_header_pos + + unless file.read_bytes(Int32, IO::ByteFormat::LittleEndian) == LOCAL_FILE_HEADER_SIGNATURE + raise InvalidTZDataError.new("Invalid Zip file") + end + file.skip 4 + unless file.read_bytes(Int16, IO::ByteFormat::LittleEndian) == COMPRESSION_METHOD_UNCOMPRESSED + raise InvalidTZDataError.new("Invalid Zip file") + end + file.skip 16 + unless file.read_bytes(Int16, IO::ByteFormat::LittleEndian) == filename_length + raise InvalidTZDataError.new("Invalid Zip file") + end + extra_field_length = file.read_bytes(Int16, IO::ByteFormat::LittleEndian) + unless file.gets(filename_length) == name + raise InvalidTZDataError.new("Invalid Zip file") + end + + file.skip extra_field_length + + return yield file + end + end +end diff --git a/src/yaml/from_yaml.cr b/src/yaml/from_yaml.cr index b415d72fe219..195f93b3389f 100644 --- a/src/yaml/from_yaml.cr +++ b/src/yaml/from_yaml.cr @@ -213,7 +213,7 @@ struct Time::Format node.raise "Expected scalar, not #{node.class}" end - parse(node.value) + parse(node.value, Time::Location::UTC) end end diff --git a/src/yaml/to_yaml.cr b/src/yaml/to_yaml.cr index 7ca4c98f4ee7..edeccd8c447b 100644 --- a/src/yaml/to_yaml.cr +++ b/src/yaml/to_yaml.cr @@ -108,7 +108,8 @@ end struct Time def to_yaml(yaml : YAML::Nodes::Builder) - if kind.utc? || kind.unspecified? + case + when utc? if hour == 0 && minute == 0 && second == 0 && millisecond == 0 yaml.scalar Time::Format.new("%F").format(self) elsif millisecond == 0 @@ -116,7 +117,7 @@ struct Time else yaml.scalar Time::Format.new("%F %X.%L").format(self) end - elsif millisecond == 0 + when millisecond == 0 yaml.scalar Time::Format.new("%F %X %:z").format(self) else yaml.scalar Time::Format.new("%F %X.%L %:z").format(self)