forked from crystal-lang/crystal
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 crystal-lang#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.
- Loading branch information
1 parent
075a003
commit c1e3265
Showing
24 changed files
with
1,323 additions
and
499 deletions.
There are no files selected for viewing
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
require "spec" | ||
require "./spec_helper" | ||
|
||
class Time::Location | ||
describe Time::Location do | ||
describe ".load" do | ||
it "loads Europe/Berlin" do | ||
location = Location.load("Europe/Berlin") | ||
|
||
location.name.should eq "Europe/Berlin" | ||
standard_time = location.lookup(Time.new(2017, 11, 22)) | ||
standard_time.name.should eq "CET" | ||
standard_time.offset.should eq 3600 | ||
standard_time.dst?.should be_false | ||
|
||
summer_time = location.lookup(Time.new(2017, 10, 22)) | ||
summer_time.name.should eq "CEST" | ||
summer_time.offset.should eq 7200 | ||
summer_time.dst?.should be_true | ||
|
||
location.utc?.should be_false | ||
location.fixed?.should be_false | ||
|
||
with_env("TZ", nil) do | ||
location.local?.should be_false | ||
end | ||
|
||
with_env("TZ", "Europe/Berlin") do | ||
location.local?.should be_true | ||
end | ||
|
||
Location.load?("Europe/Berlin", Crystal::System::Time.zone_sources).should eq location | ||
end | ||
|
||
it "invalid timezone identifier" do | ||
expect_raises(InvalidLocationNameError, "Foobar/Baz") do | ||
Location.load("Foobar/Baz") | ||
end | ||
|
||
Location.load?("Foobar/Baz", Crystal::System::Time.zone_sources).should be_nil | ||
end | ||
|
||
it "treats UTC as special case" do | ||
Location.load("UTC").should eq Location::UTC | ||
Location.load("").should eq Location::UTC | ||
|
||
# Etc/UTC could be pointing to anything | ||
Location.load("Etc/UTC").should_not eq Location::UTC | ||
end | ||
|
||
describe "validating name" do | ||
it "absolute path" do | ||
expect_raises(InvalidLocationNameError) do | ||
Location.load("/America/New_York") | ||
end | ||
expect_raises(InvalidLocationNameError) do | ||
Location.load("\\Zulu") | ||
end | ||
end | ||
it "dot dot" do | ||
expect_raises(InvalidLocationNameError) do | ||
Location.load("../zoneinfo/America/New_York") | ||
end | ||
expect_raises(InvalidLocationNameError) do | ||
Location.load("a..") | ||
end | ||
end | ||
end | ||
|
||
context "with ZONEINFO" do | ||
it "loads from custom directory" do | ||
with_zoneinfo(File.join(__DIR__, "..", "data", "zoneinfo")) do | ||
location = Location.load("Foo/Bar") | ||
location.name.should eq "Foo/Bar" | ||
end | ||
end | ||
|
||
it "loads from custom zipfile" do | ||
with_zoneinfo(ZONEINFO_ZIP) do | ||
location = Location.load("Asia/Jerusalem") | ||
location.not_nil!.name.should eq "Asia/Jerusalem" | ||
end | ||
end | ||
|
||
it "raises if not available" do | ||
with_zoneinfo(ZONEINFO_ZIP) do | ||
expect_raises(InvalidLocationNameError) do | ||
Location.load("Foo/Bar") | ||
end | ||
Location.load?("Foo/Bar", Crystal::System::Time.zone_sources).should be_nil | ||
end | ||
end | ||
|
||
it "does not fall back to default sources" do | ||
with_zoneinfo(File.join(__DIR__, "..", "data", "zoneinfo")) do | ||
expect_raises(InvalidLocationNameError) do | ||
Location.load("Europe/Berlin") | ||
end | ||
end | ||
|
||
with_zoneinfo("nonexising_zipfile.zip") do | ||
expect_raises(InvalidLocationNameError) do | ||
Location.load("Europe/Berlin") | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
it "UTC" do | ||
location = Location::UTC | ||
location.name.should eq "UTC" | ||
|
||
location.utc?.should be_true | ||
location.fixed?.should be_true | ||
|
||
# this could fail if no source for localtime is available | ||
unless Location.local.utc? | ||
location.local?.should be_false | ||
end | ||
|
||
zone = location.lookup(Time.now) | ||
zone.name.should eq "UTC" | ||
zone.offset.should eq 0 | ||
zone.dst?.should be_false | ||
end | ||
|
||
it ".local" do | ||
with_env("TZ", nil) do | ||
Location.local.name.should eq "Local" | ||
end | ||
with_env("TZ", "Europe/Berlin") do | ||
Location.local.name.should eq "Europe/Berlin" | ||
end | ||
with_env("TZ", "") do | ||
Location.local.utc?.should be_true | ||
end | ||
end | ||
|
||
describe ".fixed" do | ||
it "accepts a name" do | ||
location = Location.fixed("Fixed", 1800) | ||
location.name.should eq "Fixed" | ||
location.zones.should eq [Zone.new("Fixed", 1800, false)] | ||
location.transitions.size.should eq 0 | ||
|
||
location.utc?.should be_false | ||
location.fixed?.should be_true | ||
location.local?.should be_false | ||
end | ||
|
||
it "positive" do | ||
location = Location.fixed 8000 | ||
location.name.should eq "+02:13" | ||
location.zones.first.offset.should eq 8000 | ||
end | ||
|
||
it "ngeative" do | ||
location = Location.fixed -7539 | ||
location.name.should eq "-02:05" | ||
location.zones.first.offset.should eq -7539 | ||
end | ||
|
||
it "raises if offset to large" do | ||
expect_raises(InvalidTimezoneOffsetError, "86401") do | ||
Location.fixed(86401) | ||
end | ||
expect_raises(InvalidTimezoneOffsetError, "-90000") do | ||
Location.fixed(-90000) | ||
end | ||
end | ||
end | ||
|
||
describe "#lookup" do | ||
it "looks up" do | ||
with_zoneinfo do | ||
location = Location.load("Europe/Berlin") | ||
zone, range = location.lookup_with_boundaries(Time.utc(2017, 11, 23, 22, 6, 12).epoch) | ||
zone.should eq Zone.new("CET", 3600, false) | ||
range.should eq({1509238800_i64, 1521939600_i64}) | ||
end | ||
end | ||
|
||
it "handles dst change" do | ||
with_zoneinfo do | ||
location = Location.load("Europe/Berlin") | ||
time = Time.utc(2017, 10, 29, 1, 0, 0) | ||
|
||
summer = location.lookup(time - 1.second) | ||
summer.name.should eq "CEST" | ||
summer.offset.should eq 2 * SECONDS_PER_HOUR | ||
summer.dst?.should be_true | ||
|
||
winter = location.lookup(time) | ||
winter.name.should eq "CET" | ||
winter.offset.should eq 1 * SECONDS_PER_HOUR | ||
winter.dst?.should be_false | ||
|
||
last_ns = location.lookup(time - 1.nanosecond) | ||
last_ns.name.should eq "CEST" | ||
last_ns.offset.should eq 2 * SECONDS_PER_HOUR | ||
last_ns.dst?.should be_true | ||
end | ||
end | ||
|
||
it "handles value after last transition" do | ||
with_zoneinfo do | ||
location = Location.load("America/Buenos_Aires") | ||
zone = location.lookup(Time.utc(5000, 1, 1)) | ||
zone.name.should eq "-03" | ||
zone.offset.should eq -3 * 3600 | ||
end | ||
end | ||
|
||
# Test that we get the correct results for times before the first | ||
# transition time. To do this we explicitly check early dates in a | ||
# couple of specific timezones. | ||
context "first zone" do | ||
it "PST8PDT" do | ||
with_zoneinfo do | ||
location = Location.load("PST8PDT") | ||
zone1 = location.lookup(-1633269601) | ||
zone2 = location.lookup(-1633269601 + 1) | ||
zone1.name.should eq "PST" | ||
zone1.offset.should eq -8 * SECONDS_PER_HOUR | ||
zone2.name.should eq "PDT" | ||
zone2.offset.should eq -7 * SECONDS_PER_HOUR | ||
end | ||
end | ||
|
||
it "Pacific/Fakaofo" do | ||
with_zoneinfo do | ||
location = Location.load("Pacific/Fakaofo") | ||
zone1 = location.lookup(1325242799) | ||
zone2 = location.lookup(1325242799 + 1) | ||
zone1.name.should eq "-11" | ||
zone1.offset.should eq -11 * SECONDS_PER_HOUR | ||
zone2.name.should eq "+13" | ||
zone2.offset.should eq 13 * SECONDS_PER_HOUR | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
def with_env(name, value) | ||
previous = ENV[name]? | ||
begin | ||
ENV[name] = value | ||
yield | ||
ensure | ||
ENV[name] = previous | ||
end | ||
end | ||
|
||
ZONEINFO_ZIP = File.join(__DIR__, "..", "data", "zoneinfo.zip") | ||
|
||
def with_zoneinfo(path = ZONEINFO_ZIP) | ||
with_env("ZONEINFO", path) do | ||
yield | ||
end | ||
end |
Oops, something went wrong.