Skip to content

Commit

Permalink
Remove representation of floating time from Time
Browse files Browse the repository at this point in the history
Floating time should not be represented as an instance of `Time` to avoid undefined operations through type safety (see crystal-lang#5332).
This commit removes all implementation details regarding floating time, including the special location `Location::UNSPECIFIED`.

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
straight-shoota committed Dec 4, 2017
1 parent 7e6ecc6 commit 4d35025
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 229 deletions.
22 changes: 3 additions & 19 deletions spec/std/time/location_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ class Time::Location
summer_time.offset.should eq 7200
summer_time.dst?.should be_true

location.unspecified?.should be_false
location.utc?.should be_false
location.fixed?.should be_false

Expand Down Expand Up @@ -81,12 +80,14 @@ class Time::Location
end

context "with ZONEINFO" do
it "loads from custom source" 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"
Expand Down Expand Up @@ -122,7 +123,6 @@ class Time::Location
location = Location::UTC
location.name.should eq "UTC"

location.unspecified?.should be_false
location.utc?.should be_true
location.fixed?.should be_true

Expand All @@ -137,21 +137,6 @@ class Time::Location
zone.dst?.should be_false
end

it "UNSPECIFIED" do
location = Location::UNSPECIFIED
location.name.should eq "UNSPECIFIED"

location.unspecified?.should be_true
location.utc?.should be_false
location.fixed?.should be_false
location.local?.should be_false

zone = location.lookup(Time.now)
zone.name.should eq "UNSPECIFIED"
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"
Expand All @@ -171,7 +156,6 @@ class Time::Location
location.zones.should eq [Zone.new("Fixed", 1800, false)]
location.transitions.size.should eq 0

location.unspecified?.should be_false
location.utc?.should be_false
location.fixed?.should be_true
location.local?.should be_false
Expand Down
169 changes: 69 additions & 100 deletions spec/std/time/time_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ def Time.expect_invalid
end
end

private def parse_time(format, string)
Time.parse(format, string, Time::Location.local)
end

describe Time do
it "initialize" do
t1 = Time.new 2002, 2, 25
t1.year.should eq(2002)
t1.month.should eq(2)
t1.day.should eq(25)
t1.floating?.should be_true
t1.local?.should be_true

t2 = Time.new 2002, 2, 25, 15, 25, 13, nanosecond: 8
t2.year.should eq(2002)
Expand All @@ -23,7 +27,7 @@ describe Time do
t2.minute.should eq(25)
t2.second.should eq(13)
t2.nanosecond.should eq(8)
t2.floating?.should be_true
t2.local?.should be_true
end

it "initialize max" do
Expand Down Expand Up @@ -379,17 +383,6 @@ describe Time do
t.to_s("%L").to_s.should eq("006")
t.to_s("%N").to_s.should eq("006000000")

floating = Time.new(2017, 11, 24, 13, 5, 6, location: Time::Location::UNSPECIFIED)
expect_raises(Time::FloatingTimeConversionError) do
floating.to_s("%z")
end
expect_raises(Time::FloatingTimeConversionError) do
floating.to_s("%:z")
end
expect_raises(Time::FloatingTimeConversionError) do
floating.to_s("%::z")
end

Time.utc_now.to_s("%z").should eq("+0000")
Time.utc_now.to_s("%:z").should eq("+00:00")
Time.utc_now.to_s("%::z").should eq("+00:00:00")
Expand Down Expand Up @@ -457,61 +450,71 @@ describe Time do
end

it "parses empty" do
t = Time.parse("", "")
t = Time.parse("", "", Time::Location.local)
t.year.should eq(1)
t.month.should eq(1)
t.day.should eq(1)
t.hour.should eq(0)
t.minute.should eq(0)
t.second.should eq(0)
t.millisecond.should eq(0)
t.floating?.should be_true
end

it { Time.parse("2014", "%Y").year.should eq(2014) }
it { Time.parse("19", "%C").year.should eq(1900) }
it { Time.parse("14", "%y").year.should eq(2014) }
it { Time.parse("09", "%m").month.should eq(9) }
it { Time.parse(" 9", "%_m").month.should eq(9) }
it { Time.parse("9", "%-m").month.should eq(9) }
it { Time.parse("February", "%B").month.should eq(2) }
it { Time.parse("March", "%B").month.should eq(3) }
it { Time.parse("MaRcH", "%B").month.should eq(3) }
it { Time.parse("MaR", "%B").month.should eq(3) }
it { Time.parse("MARCH", "%^B").month.should eq(3) }
it { Time.parse("Mar", "%b").month.should eq(3) }
it { Time.parse("Mar", "%^b").month.should eq(3) }
it { Time.parse("MAR", "%^b").month.should eq(3) }
it { Time.parse("MAR", "%h").month.should eq(3) }
it { Time.parse("MAR", "%^h").month.should eq(3) }
it { Time.parse("2", "%d").day.should eq(2) }
it { Time.parse("02", "%d").day.should eq(2) }
it { Time.parse("02", "%-d").day.should eq(2) }
it { Time.parse(" 2", "%e").day.should eq(2) }
it { Time.parse("9", "%H").hour.should eq(9) }
it { Time.parse(" 9", "%k").hour.should eq(9) }
it { Time.parse("09", "%I").hour.should eq(9) }
it { Time.parse(" 9", "%l").hour.should eq(9) }
it { Time.parse("9pm", "%l%p").hour.should eq(21) }
it { Time.parse("9PM", "%l%P").hour.should eq(21) }
it { Time.parse("09", "%M").minute.should eq(9) }
it { Time.parse("09", "%S").second.should eq(9) }
it { Time.parse("123", "%L").millisecond.should eq(123) }
it { Time.parse("321", "%N").nanosecond.should eq(321) }
it { Time.parse("Fri Oct 31 23:00:24 2014", "%c").to_s.should eq("2014-10-31 23:00:24") }
it { Time.parse("10/31/14", "%D").to_s.should eq("2014-10-31 00:00:00") }
it { Time.parse("10/31/69", "%D").to_s.should eq("1969-10-31 00:00:00") }
it { Time.parse("2014-10-31", "%F").to_s.should eq("2014-10-31 00:00:00") }
it { Time.parse("2014-10-31", "%F").to_s.should eq("2014-10-31 00:00:00") }
it { Time.parse("10/31/14", "%x").to_s.should eq("2014-10-31 00:00:00") }
it { Time.parse("10:11:12", "%X").to_s.should eq("0001-01-01 10:11:12") }
it { Time.parse("11:14:01 PM", "%r").to_s.should eq("0001-01-01 23:14:01") }
it { Time.parse("11:14", "%R").to_s.should eq("0001-01-01 11:14:00") }
it { Time.parse("11:12:13", "%T").to_s.should eq("0001-01-01 11:12:13") }
it { Time.parse("This was done on Friday, October 31, 2014", "This was done on %A, %B %d, %Y").to_s.should eq("2014-10-31 00:00:00") }
it { Time.parse("今は Friday, October 31, 2014", "今は %A, %B %d, %Y").to_s.should eq("2014-10-31 00:00:00") }
it { Time.parse("epoch: 1459864667", "epoch: %s").epoch.should eq(1459864667) }
it { Time.parse("epoch: -1459864667", "epoch: %s").epoch.should eq(-1459864667) }
t.local?.should be_true
end

it "parse fails without time zone" do
expect_raises(Time::Format::Error, "no default location provided") do
Time.parse("2017-12-01 20:15:13", "%F %T")
end
Time.parse("2017-12-01 20:15:13", "%F %T", Time::Location.local).to_s("%F %T").should eq "2017-12-01 20:15:13"
Time.parse("2017-12-01 20:15:13 +01:00", "%F %T %:z").to_s("%F %T %:z").should eq "2017-12-01 20:15:13 +01:00"
end

it "parses" do
parse_time("2014", "%Y").year.should eq(2014)
parse_time("19", "%C").year.should eq(1900)
parse_time("14", "%y").year.should eq(2014)
parse_time("09", "%m").month.should eq(9)
parse_time(" 9", "%_m").month.should eq(9)
parse_time("9", "%-m").month.should eq(9)
parse_time("February", "%B").month.should eq(2)
parse_time("March", "%B").month.should eq(3)
parse_time("MaRcH", "%B").month.should eq(3)
parse_time("MaR", "%B").month.should eq(3)
parse_time("MARCH", "%^B").month.should eq(3)
parse_time("Mar", "%b").month.should eq(3)
parse_time("Mar", "%^b").month.should eq(3)
parse_time("MAR", "%^b").month.should eq(3)
parse_time("MAR", "%h").month.should eq(3)
parse_time("MAR", "%^h").month.should eq(3)
parse_time("2", "%d").day.should eq(2)
parse_time("02", "%d").day.should eq(2)
parse_time("02", "%-d").day.should eq(2)
parse_time(" 2", "%e").day.should eq(2)
parse_time("9", "%H").hour.should eq(9)
parse_time(" 9", "%k").hour.should eq(9)
parse_time("09", "%I").hour.should eq(9)
parse_time(" 9", "%l").hour.should eq(9)
parse_time("9pm", "%l%p").hour.should eq(21)
parse_time("9PM", "%l%P").hour.should eq(21)
parse_time("09", "%M").minute.should eq(9)
parse_time("09", "%S").second.should eq(9)
parse_time("123", "%L").millisecond.should eq(123)
parse_time("321", "%N").nanosecond.should eq(321)
parse_time("Fri Oct 31 23:00:24 2014", "%c").to_s.should eq("2014-10-31 23:00:24")
parse_time("10/31/14", "%D").to_s.should eq("2014-10-31 00:00:00")
parse_time("10/31/69", "%D").to_s.should eq("1969-10-31 00:00:00")
parse_time("2014-10-31", "%F").to_s.should eq("2014-10-31 00:00:00")
parse_time("2014-10-31", "%F").to_s.should eq("2014-10-31 00:00:00")
parse_time("10/31/14", "%x").to_s.should eq("2014-10-31 00:00:00")
parse_time("10:11:12", "%X").to_s.should eq("0001-01-01 10:11:12")
parse_time("11:14:01 PM", "%r").to_s.should eq("0001-01-01 23:14:01")
parse_time("11:14", "%R").to_s.should eq("0001-01-01 11:14:00")
parse_time("11:12:13", "%T").to_s.should eq("0001-01-01 11:12:13")
parse_time("This was done on Friday, October 31, 2014", "This was done on %A, %B %d, %Y").to_s.should eq("2014-10-31 00:00:00")
parse_time("今は Friday, October 31, 2014", "今は %A, %B %d, %Y").to_s.should eq("2014-10-31 00:00:00")
parse_time("epoch: 1459864667", "epoch: %s").epoch.should eq(1459864667)
parse_time("epoch: -1459864667", "epoch: %s").epoch.should eq(-1459864667)
end

it "parses timezone" do
patterns = {"%z", "%:z", "%::z"}
Expand All @@ -521,46 +524,40 @@ describe Time do
time.offset.should eq 0
time.utc?.should be_false
time.location.fixed?.should be_true
time.incremental?.should be_true
end

{"-0000", "-00:00", "-00:00:00"}.zip(patterns) do |string, pattern|
time = Time.parse(string, pattern)
time.offset.should eq 0
time.utc?.should be_false
time.location.fixed?.should be_true
time.incremental?.should be_true
end

{"-0200", "-02:00", "-02:00:00"}.zip(patterns) do |string, pattern|
time = Time.parse(string, pattern)
time.offset.should eq -2 * 3600
time.utc?.should be_false
time.location.fixed?.should be_true
time.incremental?.should be_true
end

{"Z", "Z", "Z"}.zip(patterns) do |string, pattern|
time = Time.parse(string, pattern)
time.offset.should eq 0
time.utc?.should be_true
time.location.fixed?.should be_true
time.incremental?.should be_true
end

{"UTC", "UTC", "UTC"}.zip(patterns) do |string, pattern|
time = Time.parse(string, pattern)
time.offset.should eq 0
time.utc?.should be_true
time.location.fixed?.should be_true
time.incremental?.should be_true
end

time = Time.parse("+04:12:39", "%::z")
time.offset.should eq 4 * 3600 + 12 * 60 + 39
time.utc?.should be_false
time.location.fixed?.should be_true
time.incremental?.should be_true
end

# TODO %N
Expand Down Expand Up @@ -590,7 +587,6 @@ describe Time do

it do
time = Time.parse("2014-10-31 10:11:12 -06:00 hi", "%F %T %z hi")
time.incremental?.should be_true
time.utc?.should be_false
time.location.fixed?.should be_true
time.offset.should eq -6 * 3600
Expand All @@ -599,7 +595,6 @@ describe Time do

it do
time = Time.parse("2014-10-31 10:11:12 +05:00 hi", "%F %T %z hi")
time.incremental?.should be_true
time.utc?.should be_false
time.location.fixed?.should be_true
time.offset.should eq 5 * 3600
Expand All @@ -608,7 +603,6 @@ describe Time do

it do
time = Time.parse("2014-10-31 10:11:12 -06:00:00 hi", "%F %T %z hi")
time.incremental?.should be_true
time.utc?.should be_false
time.location.fixed?.should be_true
time.offset.should eq -6 * 3600
Expand All @@ -617,7 +611,6 @@ describe Time do

it do
time = Time.parse("2014-10-31 10:11:12 -060000 hi", "%F %T %z hi")
time.incremental?.should be_true
time.utc?.should be_false
time.location.fixed?.should be_true
time.offset.should eq -6 * 3600
Expand All @@ -631,35 +624,35 @@ describe Time do
end

it "parses the correct amount of digits (#853)" do
time = Time.parse("20150624", "%Y%m%d")
time = Time.parse("20150624", "%Y%m%d", Time::Location::UTC)
time.year.should eq(2015)
time.month.should eq(6)
time.day.should eq(24)
end

it "parses month blank padded" do
time = Time.parse("2015 624", "%Y%_m%d")
time = Time.parse("2015 624", "%Y%_m%d", Time::Location::UTC)
time.year.should eq(2015)
time.month.should eq(6)
time.day.should eq(24)
end

it "parses day of month blank padded" do
time = Time.parse("201506 4", "%Y%m%e")
time = Time.parse("201506 4", "%Y%m%e", Time::Location::UTC)
time.year.should eq(2015)
time.month.should eq(6)
time.day.should eq(4)
end

it "parses hour 24 blank padded" do
time = Time.parse(" 31112", "%k%M%S")
time = Time.parse(" 31112", "%k%M%S", Time::Location::UTC)
time.hour.should eq(3)
time.minute.should eq(11)
time.second.should eq(12)
end

it "parses hour 12 blank padded" do
time = Time.parse(" 31112", "%l%M%S")
time = Time.parse(" 31112", "%l%M%S", Time::Location::UTC)
time.hour.should eq(3)
time.minute.should eq(11)
time.second.should eq(12)
Expand Down Expand Up @@ -799,34 +792,10 @@ describe Time do
time2.should eq(time1)
time2.location.should eq(location2)
end

it "works with floating" do
location = Time::Location.fixed(3611)
time1 = Time.new(2017, 11, 24, 15, 47)
time1.floating?.should be_true

time2 = time1.in(location)

# Both could be possible... handling of floating time is not clear
# time2.should_not eq(time1)
# (time2 + 3611.seconds).to_floating.should eq time1
time2.should eq(time1)
(time2.to_utc + 3611.seconds).should eq time1

time2.location.should eq(location)
time2.floating?.should be_false

time_utc = time1.to_utc
time_utc.should eq time1
(time2 - time_utc).should eq -3611.seconds
end
end

it "#to_s" do
with_zoneinfo do
time = Time.new(2017, 11, 25, 22, 6, 17, location: Time::Location::UNSPECIFIED)
time.to_s.should eq "2017-11-25 22:06:17"

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"

Expand Down
Loading

0 comments on commit 4d35025

Please sign in to comment.