diff --git a/spec/std/http/http_spec.cr b/spec/std/http/http_spec.cr index 6253ad2d1e28..0d2d2b3fc6c8 100644 --- a/spec/std/http/http_spec.cr +++ b/spec/std/http/http_spec.cr @@ -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 diff --git a/spec/std/time/time_spec.cr b/spec/std/time/time_spec.cr index 275b22453262..689621b0bc02 100644 --- a/spec/std/time/time_spec.cr +++ b/spec/std/time/time_spec.cr @@ -263,24 +263,50 @@ describe Time do t1.epoch_f.should be_close(t1.to_utc.epoch_f, 1e-01) end - it "to_s" do - t = Time.new 2014, 10, 30, 21, 18, 13 - t.to_s.should eq("2014-10-30 21:18:13") + describe "to_s" do + it "prints floating time" do + t = Time.new 2014, 10, 30, 21, 18, 13 + t.to_s.should eq("2014-10-30 21:18:13") + + t = Time.new 2014, 1, 30, 21, 18, 13 + t.to_s.should eq("2014-01-30 21:18:13") - t = Time.new 2014, 1, 30, 21, 18, 13 - t.to_s.should eq("2014-01-30 21:18:13") + t = Time.new 2014, 10, 1, 21, 18, 13 + t.to_s.should eq("2014-10-01 21:18:13") - t = Time.new 2014, 10, 1, 21, 18, 13 - t.to_s.should eq("2014-10-01 21:18:13") + t = Time.new 2014, 10, 30, 1, 18, 13 + t.to_s.should eq("2014-10-30 01:18:13") - t = Time.new 2014, 10, 30, 1, 18, 13 - t.to_s.should eq("2014-10-30 01:18:13") + t = Time.new 2014, 10, 30, 21, 1, 13 + t.to_s.should eq("2014-10-30 21:01:13") - t = Time.new 2014, 10, 30, 21, 1, 13 - t.to_s.should eq("2014-10-30 21:01:13") + t = Time.new 2014, 10, 30, 21, 18, 1 + t.to_s.should eq("2014-10-30 21:18:01") + end + + it "prints without nanoseconds" do + t = Time.new 2014, 10, 30, 21, 18, 13, nanosecond: 12345 + t.to_s.should eq("2014-10-30 21:18:13") + end - t = Time.new 2014, 10, 30, 21, 18, 1 - t.to_s.should eq("2014-10-30 21:18:01") + it "prints UTC" do + t = Time.utc 2014, 10, 30, 21, 18, 13 + t.to_s.should eq("2014-10-30 21:18:13 UTC") + end + + it "prints zone" do + location = Time::Location.load("Europe/Berlin") + t = Time.new 2014, 10, 30, 21, 18, 13, location: location + t.to_s.should eq("2014-10-30 21:18:13 +01:00 Europe/Berlin") + + t = Time.new 2014, 10, 10, 21, 18, 13, location: location + t.to_s.should eq("2014-10-10 21:18:13 +02:00 Europe/Berlin") + end + + it "prints offset" do + t = Time.new 2014, 10, 30, 21, 18, 13, location: Time::Location.fixed(-9000) + t.to_s.should eq("2014-10-30 21:18:13 -02:30") + end end it "formats" do @@ -334,10 +360,41 @@ 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") + zoned = Time.new(2017, 11, 24, 13, 5, 6, location: Time::Location.load("Europe/Berlin")) + zoned.to_s("%z").should eq("+0100") + zoned.to_s("%:z").should eq("+01:00") + zoned.to_s("%::z").should eq("+01:00:00") + + zoned = Time.new(2017, 11, 24, 13, 5, 6, location: Time::Location.load("America/Buenos_Aires")) + zoned.to_s("%z").should eq("-0300") + zoned.to_s("%:z").should eq("-03:00") + zoned.to_s("%::z").should eq("-03:00:00") + + offset = Time.new(2017, 11, 24, 13, 5, 6, location: Time::Location.fixed(9000)) + offset.to_s("%z").should eq("+0230") + offset.to_s("%:z").should eq("+02:30") + offset.to_s("%::z").should eq("+02:30:00") + + offset = Time.new(2017, 11, 24, 13, 5, 6, location: Time::Location.fixed(9001)) + offset.to_s("%z").should eq("+0230") + offset.to_s("%:z").should eq("+02:30") + offset.to_s("%::z").should eq("+02:30:01") + # TODO %N # TODO %Z @@ -436,6 +493,56 @@ describe Time do it { Time.parse("epoch: 1459864667", "epoch: %s").epoch.should eq(1459864667) } it { Time.parse("epoch: -1459864667", "epoch: %s").epoch.should eq(-1459864667) } + it "parses timezone" do + patterns = {"%z", "%:z", "%::z"} + + {"+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 + + {"-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 # TODO %Z # TODO %G @@ -463,25 +570,37 @@ describe Time do it do time = Time.parse("2014-10-31 10:11:12 -06:00 hi", "%F %T %z hi") - time.local?.should be_true + time.incremental?.should be_true + time.utc?.should be_false + time.location.fixed?.should be_true + time.offset.should eq -6 * 3600 time.to_utc.to_s.should eq("2014-10-31 16:11:12 UTC") end it do time = Time.parse("2014-10-31 10:11:12 +05:00 hi", "%F %T %z hi") - time.local?.should be_true + time.incremental?.should be_true + time.utc?.should be_false + time.location.fixed?.should be_true + time.offset.should eq 5 * 3600 time.to_utc.to_s.should eq("2014-10-31 05:11:12 UTC") end it do time = Time.parse("2014-10-31 10:11:12 -06:00:00 hi", "%F %T %z hi") - time.local?.should be_true + time.incremental?.should be_true + time.utc?.should be_false + time.location.fixed?.should be_true + time.offset.should eq -6 * 3600 time.to_utc.to_s.should eq("2014-10-31 16:11:12 UTC") end it do time = Time.parse("2014-10-31 10:11:12 -060000 hi", "%F %T %z hi") - time.local?.should be_true + time.incremental?.should be_true + time.utc?.should be_false + time.location.fixed?.should be_true + time.offset.should eq -6 * 3600 time.to_utc.to_s.should eq("2014-10-31 16:11:12 UTC") end @@ -526,9 +645,16 @@ describe Time do time.second.should eq(12) end - it "can parse in UTC" do + it "can parse in location" do time = Time.parse("2014-10-31 11:12:13", "%F %T", Time::Location::UTC) time.utc?.should be_true + + location = Time::Location.load("Europe/Berlin") + time = Time.parse("2016-11-24 14:32:02", "%F %T", location) + time.location.should eq location + + time = Time.parse("2016-11-24 14:32:02 +01:00", "%F %T %:z", location) + time.location.should eq Time::Location.fixed(3600) end it "at" do diff --git a/src/time.cr b/src/time.cr index e035e8db2444..95e23fdd4261 100644 --- a/src/time.cr +++ b/src/time.cr @@ -572,7 +572,8 @@ struct Time self else Time.utc( - seconds: total_seconds, + # FIXME: Use proper zone lookup distance + seconds: total_seconds - location.lookup(total_seconds - UNIX_SECONDS).offset, nanoseconds: nanosecond ) end diff --git a/src/time/format/formatter.cr b/src/time/format/formatter.cr index 18ab3429e122..3241da81050c 100644 --- a/src/time/format/formatter.cr +++ b/src/time/format/formatter.cr @@ -139,55 +139,55 @@ struct Time::Format io << time.epoch end - def time_zone - case time - when .floating? - raise FloatingTimeConversionError.new - when .utc? - io << "+0000" - else - 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) + raise FloatingTimeConversionError.new if time.floating? + + 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 - when .floating? - raise FloatingTimeConversionError.new - when .utc? - io << "+00:00" - else - 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) + raise FloatingTimeConversionError.new if time.floating? + + 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.offset / 60 - 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 f8b77418b7bd..cae18cee01fd 100644 --- a/src/time/format/parser.cr +++ b/src/time/format/parser.cr @@ -27,17 +27,7 @@ struct Time::Format return Time.epoch(epoch) end - time = Time.new @year, @month, @day, @hour, @minute, @second, nanosecond: @nanosecond, location: time_location - - if offset_in_minutes = @offset_in_minutes - time -= offset_in_minutes.minutes if offset_in_minutes != 0 - - if (offset_in_minutes != 0) || (location == Location.local && !time.local?) - time = time.to_local - end - end - - time + Time.new @year, @month, @day, @hour, @minute, @second, nanosecond: @nanosecond, location: time_location end def year @@ -227,15 +217,13 @@ 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 @location = Location::UTC next_char when 'U' if next_char == 'T' && next_char == 'C' - @offset_in_minutes = 0 @location = Location::UTC next_char else @@ -250,7 +238,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 == ':' @@ -259,10 +247,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 - @offset_in_minutes = sign * (60*hours + minutes) - @location = Location::UTC + if with_seconds + char = next_char + char = next_char if char == ':' + raise "Invalid timezone" unless char.ascii_number? + seconds = char.to_i + + 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? @@ -283,7 +283,7 @@ struct Time::Format end def time_zone_colon_with_seconds - time_zone + time_zone(with_seconds: true) end def char(char)