From 3b6e63e4ada9aa71e616cc675898ade08af3e6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 24 Nov 2017 20:21:15 +0100 Subject: [PATCH] Let Time internals always be in UTC --- spec/std/file_spec.cr | 10 +- spec/std/time/location_spec.cr | 19 +- spec/std/time/spec_helper.cr | 17 ++ spec/std/time/time_spec.cr | 342 ++++++++++++++++++--------------- src/time.cr | 127 +++++++----- 5 files changed, 289 insertions(+), 226 deletions(-) create mode 100644 spec/std/time/spec_helper.cr diff --git a/spec/std/file_spec.cr b/spec/std/file_spec.cr index 4a08b7d10110..268c3707a6fa 100644 --- a/spec/std/file_spec.cr +++ b/spec/std/file_spec.cr @@ -958,8 +958,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) @@ -971,8 +971,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") @@ -994,7 +994,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) diff --git a/spec/std/time/location_spec.cr b/spec/std/time/location_spec.cr index 8ed025faa935..fede5477bcd3 100644 --- a/spec/std/time/location_spec.cr +++ b/spec/std/time/location_spec.cr @@ -1,22 +1,5 @@ require "spec" - -private def with_env(name, value) - previous = ENV[name]? - begin - ENV[name] = value - yield - ensure - ENV[name] = previous - end -end - -private ZONEINFO_ZIP = File.join(__DIR__, "..", "data", "zoneinfo.zip") - -private def with_zoneinfo(path = ZONEINFO_ZIP) - with_env("ZONEINFO", path) do - yield - end -end +require "./spec_helper" class Time::Location def __cached_range diff --git a/spec/std/time/spec_helper.cr b/spec/std/time/spec_helper.cr new file mode 100644 index 000000000000..b8eb5110cd34 --- /dev/null +++ b/spec/std/time/spec_helper.cr @@ -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 diff --git a/spec/std/time/time_spec.cr b/spec/std/time/time_spec.cr index 689621b0bc02..353f9c091279 100644 --- a/spec/std/time/time_spec.cr +++ b/spec/std/time/time_spec.cr @@ -1,4 +1,5 @@ require "spec" +require "./spec_helper" def Time.expect_invalid expect_raises ArgumentError, "Invalid time" do @@ -138,6 +139,16 @@ describe Time do t1.second.should eq(13) end + pending "add days over dst" do + with_zoneinfo do + location = Time::Location.load("Europe/Berlin") + reference = Time.new(2017, 10, 28, 13, 37, location: location) + next_day = Time.new(2017, 10, 28, 13, 37, location: location) + + (reference + 1.day).should eq next_day + end + end + it "add days out of range 1" do t1 = Time.new(2002, 2, 25, 15, 25, 13) expect_raises ArgumentError do @@ -263,6 +274,11 @@ describe Time do t1.epoch_f.should be_close(t1.to_utc.epoch_f, 1e-01) end + it "current time is similar in differnt locations" do + (Time.now - Time.utc_now).should be_close(0.seconds, 1.second) + (Time.now - Time.now(Time::Location.fixed(1234))).should be_close(0.seconds, 1.second) + end + describe "to_s" do it "prints floating time" do t = Time.new 2014, 10, 30, 21, 18, 13 @@ -295,12 +311,14 @@ describe Time do 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") + with_zoneinfo 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") + 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 end it "prints offset" do @@ -310,130 +328,132 @@ describe Time do end it "formats" do - t = Time.new 2014, 1, 2, 3, 4, 5, nanosecond: 6_000_000 - t2 = Time.new 2014, 1, 2, 15, 4, 5, nanosecond: 6_000_000 - t3 = Time.new 2014, 1, 2, 12, 4, 5, nanosecond: 6_000_000 - - t.to_s("%Y").should eq("2014") - Time.new(1, 1, 2, 3, 4, 5, nanosecond: 6).to_s("%Y").should eq("0001") - - t.to_s("%C").should eq("20") - t.to_s("%y").should eq("14") - t.to_s("%m").should eq("01") - t.to_s("%_m").should eq(" 1") - t.to_s("%_%_m2").should eq("%_ 12") - t.to_s("%-m").should eq("1") - t.to_s("%-%-m2").should eq("%-12") - t.to_s("%B").should eq("January") - t.to_s("%^B").should eq("JANUARY") - t.to_s("%^%^B2").should eq("%^JANUARY2") - t.to_s("%b").should eq("Jan") - t.to_s("%^b").should eq("JAN") - t.to_s("%h").should eq("Jan") - t.to_s("%^h").should eq("JAN") - t.to_s("%d").should eq("02") - t.to_s("%-d").should eq("2") - t.to_s("%e").should eq(" 2") - t.to_s("%j").should eq("002") - t.to_s("%H").should eq("03") - - t.to_s("%k").should eq(" 3") - t2.to_s("%k").should eq("15") - - t.to_s("%I").should eq("03") - t2.to_s("%I").should eq("03") - t3.to_s("%I").should eq("12") - - t.to_s("%l").should eq(" 3") - t2.to_s("%l").should eq(" 3") - t3.to_s("%l").should eq("12") - - # Note: we purposely match %p to am/pm and %P to AM/PM (makes more sense) - t.to_s("%p").should eq("am") - t2.to_s("%p").should eq("pm") - - t.to_s("%P").should eq("AM") - t2.to_s("%P").should eq("PM") - - t.to_s("%M").to_s.should eq("04") - t.to_s("%S").to_s.should eq("05") - 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 + with_zoneinfo do + t = Time.new 2014, 1, 2, 3, 4, 5, nanosecond: 6_000_000 + t2 = Time.new 2014, 1, 2, 15, 4, 5, nanosecond: 6_000_000 + t3 = Time.new 2014, 1, 2, 12, 4, 5, nanosecond: 6_000_000 + + t.to_s("%Y").should eq("2014") + Time.new(1, 1, 2, 3, 4, 5, nanosecond: 6).to_s("%Y").should eq("0001") + + t.to_s("%C").should eq("20") + t.to_s("%y").should eq("14") + t.to_s("%m").should eq("01") + t.to_s("%_m").should eq(" 1") + t.to_s("%_%_m2").should eq("%_ 12") + t.to_s("%-m").should eq("1") + t.to_s("%-%-m2").should eq("%-12") + t.to_s("%B").should eq("January") + t.to_s("%^B").should eq("JANUARY") + t.to_s("%^%^B2").should eq("%^JANUARY2") + t.to_s("%b").should eq("Jan") + t.to_s("%^b").should eq("JAN") + t.to_s("%h").should eq("Jan") + t.to_s("%^h").should eq("JAN") + t.to_s("%d").should eq("02") + t.to_s("%-d").should eq("2") + t.to_s("%e").should eq(" 2") + t.to_s("%j").should eq("002") + t.to_s("%H").should eq("03") + + t.to_s("%k").should eq(" 3") + t2.to_s("%k").should eq("15") + + t.to_s("%I").should eq("03") + t2.to_s("%I").should eq("03") + t3.to_s("%I").should eq("12") + + t.to_s("%l").should eq(" 3") + t2.to_s("%l").should eq(" 3") + t3.to_s("%l").should eq("12") + + # Note: we purposely match %p to am/pm and %P to AM/PM (makes more sense) + t.to_s("%p").should eq("am") + t2.to_s("%p").should eq("pm") + + t.to_s("%P").should eq("AM") + t2.to_s("%P").should eq("PM") + + t.to_s("%M").to_s.should eq("04") + t.to_s("%S").to_s.should eq("05") + 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 - - t.to_s("%A").to_s.should eq("Thursday") - t.to_s("%^A").to_s.should eq("THURSDAY") - t.to_s("%a").to_s.should eq("Thu") - t.to_s("%^a").to_s.should eq("THU") - t.to_s("%u").to_s.should eq("4") - t.to_s("%w").to_s.should eq("4") - - t3 = Time.new 2014, 1, 5 # A Sunday - t3.to_s("%u").to_s.should eq("7") - t3.to_s("%w").to_s.should eq("0") - - # TODO %G - # TODO %g - # TODO %V - # TODO %U - # TODO %W - # TODO %s - # TODO %n - # TODO %t - # TODO %% - - t.to_s("%%").should eq("%") - t.to_s("%c").should eq(t.to_s("%a %b %e %T %Y")) - t.to_s("%D").should eq(t.to_s("%m/%d/%y")) - t.to_s("%F").should eq(t.to_s("%Y-%m-%d")) - # TODO %v - t.to_s("%x").should eq(t.to_s("%D")) - t.to_s("%X").should eq(t.to_s("%T")) - t.to_s("%r").should eq(t.to_s("%I:%M:%S %P")) - t.to_s("%R").should eq(t.to_s("%H:%M")) - t.to_s("%T").should eq(t.to_s("%H:%M:%S")) - - t.to_s("%Y-%m-hello").should eq("2014-01-hello") - - t = Time.utc 2014, 1, 2, 3, 4, 5, nanosecond: 6 - t.to_s("%s").should eq("1388631845") + 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 + + t.to_s("%A").to_s.should eq("Thursday") + t.to_s("%^A").to_s.should eq("THURSDAY") + t.to_s("%a").to_s.should eq("Thu") + t.to_s("%^a").to_s.should eq("THU") + t.to_s("%u").to_s.should eq("4") + t.to_s("%w").to_s.should eq("4") + + t3 = Time.new 2014, 1, 5 # A Sunday + t3.to_s("%u").to_s.should eq("7") + t3.to_s("%w").to_s.should eq("0") + + # TODO %G + # TODO %g + # TODO %V + # TODO %U + # TODO %W + # TODO %s + # TODO %n + # TODO %t + # TODO %% + + t.to_s("%%").should eq("%") + t.to_s("%c").should eq(t.to_s("%a %b %e %T %Y")) + t.to_s("%D").should eq(t.to_s("%m/%d/%y")) + t.to_s("%F").should eq(t.to_s("%Y-%m-%d")) + # TODO %v + t.to_s("%x").should eq(t.to_s("%D")) + t.to_s("%X").should eq(t.to_s("%T")) + t.to_s("%r").should eq(t.to_s("%I:%M:%S %P")) + t.to_s("%R").should eq(t.to_s("%H:%M")) + t.to_s("%T").should eq(t.to_s("%H:%M:%S")) + + t.to_s("%Y-%m-hello").should eq("2014-01-hello") + + t = Time.utc 2014, 1, 2, 3, 4, 5, nanosecond: 6 + t.to_s("%s").should eq("1388631845") + end end it "parses empty" do @@ -646,15 +666,17 @@ describe Time do end 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 + with_zoneinfo 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 + 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) + time = Time.parse("2016-11-24 14:32:02 +01:00", "%F %T %:z", location) + time.location.should eq Time::Location.fixed(3600) + end end it "at" do @@ -759,22 +781,6 @@ describe Time do (time.to_utc <=> 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 @@ -782,6 +788,40 @@ 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 + + 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 + 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/src/time.cr b/src/time.cr index 95e23fdd4261..df6dd0db7c2a 100644 --- a/src/time.cr +++ b/src/time.cr @@ -160,9 +160,9 @@ struct Time monotonic - start end - def self.new - seconds, nanoseconds, offset = Time.compute_seconds_nanoseconds_and_offset - new(seconds: seconds + offset, nanoseconds: nanoseconds, location: Location.local) + def self.new(location = Location.local) + seconds, nanoseconds = Time.compute_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, location = Location::UNSPECIFIED) @@ -184,6 +184,9 @@ struct Time SECONDS_PER_MINUTE * minute + second + # Normalize internal representation to UTC + seconds = seconds - zone_offset_at(seconds, location) + new(seconds: seconds, nanoseconds: nanosecond.to_i, location: location) end @@ -311,10 +314,8 @@ struct Time # # 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 + if floating? != other.floating? + to_floating - other.to_floating else Span.new( seconds: total_seconds - other.total_seconds, @@ -323,15 +324,15 @@ struct Time end 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. @@ -356,17 +357,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`). @@ -381,12 +382,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 @@ -445,10 +446,8 @@ struct Time end def <=>(other : self) - if utc? && other.local? - self <=> other.to_utc - elsif local? && other.utc? - to_utc <=> other + if floating? != other.floating? + to_floating <=> other.to_floating else cmp = total_seconds <=> other.total_seconds cmp = nanosecond <=> other.nanosecond if cmp == 0 @@ -456,6 +455,14 @@ struct Time end end + def ==(other : self) + if floating? != other.floating? + to_floating == other.to_floating + else + total_seconds == other.total_seconds && nanosecond == other.nanosecond + end + end + def_hash total_seconds, nanosecond, floating? # Returns how many days this *month* (`1..12`) of this *year* has (28, 29, 30 or 31). @@ -542,7 +549,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. @@ -566,14 +573,29 @@ struct Time epoch.to_f + nanosecond.to_f / 1e9 end + def in(location : Location) : Time + if floating? + Time.new( + seconds: total_seconds - Time.zone_offset_at(total_seconds, location), + nanoseconds: nanosecond, + location: location + ) + else + Time.new( + seconds: total_seconds, + nanoseconds: nanosecond, + location: location + ) + end + end + # Returns a copy of this `Time` converted to UTC. def to_utc : Time if utc? self else Time.utc( - # FIXME: Use proper zone lookup distance - seconds: total_seconds - location.lookup(total_seconds - UNIX_SECONDS).offset, + seconds: total_seconds, nanoseconds: nanosecond ) end @@ -583,11 +605,19 @@ struct Time def to_local : Time if local? self + else + in(Location.local) + end + end + + def to_floating : Time + if floating? + self else Time.new( - seconds: total_seconds + offset, + seconds: offset_seconds, nanoseconds: nanosecond, - location: Location.local, + location: Location::UNSPECIFIED ) end end @@ -700,11 +730,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 @@ -744,35 +778,24 @@ 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} + protected def self.compute_seconds_and_nanoseconds + Crystal::System::Time.compute_utc_seconds_and_nanoseconds 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