Skip to content

Commit

Permalink
Add time zones support (#5324)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
straight-shoota authored and RX14 committed Jan 18, 2018
1 parent 6a574f2 commit f3168b6
Show file tree
Hide file tree
Showing 27 changed files with 1,874 additions and 503 deletions.
76 changes: 76 additions & 0 deletions scripts/generate_windows_zone_names.cr
Original file line number Diff line number Diff line change
@@ -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)
Binary file added spec/std/data/zoneinfo.zip
Binary file not shown.
Binary file added spec/std/data/zoneinfo/Foo/Bar
Binary file not shown.
10 changes: 5 additions & 5 deletions spec/std/file_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1033,8 +1033,8 @@ describe "File" do
filename = "#{__DIR__}/data/temp_write.txt"
File.write(filename, "")

atime = Time.new(2000, 1, 2)
mtime = Time.new(2000, 3, 4)
atime = Time.utc(2000, 1, 2)
mtime = Time.utc(2000, 3, 4)

File.utime(atime, mtime, filename)

Expand All @@ -1046,8 +1046,8 @@ describe "File" do
end

it "raises if file not found" do
atime = Time.new(2000, 1, 2)
mtime = Time.new(2000, 3, 4)
atime = Time.utc(2000, 1, 2)
mtime = Time.utc(2000, 3, 4)

expect_raises Errno, "Error setting time to file" do
File.utime(atime, mtime, "#{__DIR__}/nonexistent_file")
Expand All @@ -1069,7 +1069,7 @@ describe "File" do

it "sets file times to given time" do
filename = "#{__DIR__}/data/temp_touch.txt"
time = Time.new(2000, 3, 4)
time = Time.utc(2000, 3, 4)
begin
File.touch(filename, time)

Expand Down
14 changes: 3 additions & 11 deletions spec/std/http/http_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe HTTP do
it "parses and is local (#2744)" do
date = "Mon, 09 Sep 2011 23:36:00 -0300"
parsed_time = HTTP.parse_time(date).not_nil!
parsed_time.local?.should be_true
parsed_time.offset.should eq -3 * 3600
parsed_time.to_utc.to_s.should eq("2011-09-10 02:36:00 UTC")
end

Expand All @@ -44,16 +44,8 @@ describe HTTP do
end

it "with local time zone" do
tz = ENV["TZ"]?
ENV["TZ"] = "Europe/Berlin"
LibC.tzset
begin
time = Time.new(1994, 11, 6, 8, 49, 37, nanosecond: 0, kind: Time::Kind::Local)
HTTP.rfc1123_date(time).should eq(time.to_utc.to_s("%a, %d %b %Y %H:%M:%S GMT"))
ensure
ENV["TZ"] = tz
LibC.tzset
end
time = Time.new(1994, 11, 6, 8, 49, 37, nanosecond: 0, location: Time::Location.load("Europe/Berlin"))
HTTP.rfc1123_date(time).should eq(time.to_utc.to_s("%a, %d %b %Y %H:%M:%S GMT"))
end
end

Expand Down
2 changes: 1 addition & 1 deletion spec/std/json/mapping_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ describe "JSON mapping" do
it "parses json with Time::Format converter" do
json = JSONWithTime.from_json(%({"value": "2014-10-31 23:37:16"}))
json.value.should be_a(Time)
json.value.to_s.should eq("2014-10-31 23:37:16")
json.value.to_s.should eq("2014-10-31 23:37:16 UTC")
json.to_json.should eq(%({"value":"2014-10-31 23:37:16"}))
end

Expand Down
Loading

0 comments on commit f3168b6

Please sign in to comment.