From e9425abe33924623b1dce62bd817eace757c2b4e Mon Sep 17 00:00:00 2001 From: Phil Ross Date: Fri, 29 Dec 2017 12:47:10 +0000 Subject: [PATCH] Update to TZInfo v2.0.0 Co-authored-by: Jared Beck Co-authored-by: Jonathan Hefner --- Gemfile.lock | 8 +++--- activesupport/CHANGELOG.md | 13 ++++++++++ activesupport/activesupport.gemspec | 2 +- activesupport/lib/active_support.rb | 8 ++++++ .../core_ext/date_and_time/compatibility.rb | 15 +++++++++++ .../lib/active_support/time_with_zone.rb | 20 ++++++++++---- .../lib/active_support/values/time_zone.rb | 23 +++++++++------- activesupport/test/time_zone_test.rb | 26 ++++++++++++++----- activesupport/test/time_zone_test_helpers.rb | 8 ++++++ .../lib/rails/application/configuration.rb | 4 +++ .../new_framework_defaults_6_1.rb.tt | 4 +++ .../test/application/configuration_test.rb | 9 +++++++ 12 files changed, 113 insertions(+), 27 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c1ebbcde91a..9d8b7e6e77f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,7 +73,7 @@ PATH concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (~> 5.1) - tzinfo (~> 1.1) + tzinfo (~> 2.0) zeitwerk (~> 2.2, >= 2.2.2) rails (6.1.0.alpha) actioncable (= 6.1.0.alpha) @@ -490,14 +490,12 @@ GEM eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) thor (1.0.1) - thread_safe (0.3.6) - thread_safe (0.3.6-java) tilt (2.0.10) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - tzinfo (1.2.6) - thread_safe (~> 0.1) + tzinfo (2.0.0) + concurrent-ruby (~> 1.0) tzinfo-data (1.2019.3) tzinfo (>= 1.0.0) uber (0.1.0) diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index b04fce1ff69..cd5ff6fae8e 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,16 @@ +* Update to TZInfo v2.0.0. + + This changes the output of `ActiveSupport::TimeZone.utc_to_local`, but + can be controlled with the + `Rails.application.config.active_support.utc_to_local_returns_utc_offset_times` config. + + New Rails 6.1 apps have it enabled by default, existing apps can upgrade + via the config in config/initializers/new_framework_defaults_6_1.rb + + See the `utc_to_local_returns_utc_offset_times` documentation for details. + + *Phil Ross and Jared Beck* + * [Breaking change] `ActiveSupport::Callbacks#halted_callback_hook` now receive a 2nd argument: `ActiveSupport::Callbacks#halted_callback_hook` now receive the name of the callback diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index ec51fbd4837..6d83842bb6e 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -34,7 +34,7 @@ Gem::Specification.new do |s| # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves s.add_dependency "i18n", ">= 1.6", "< 2" - s.add_dependency "tzinfo", "~> 1.1" + s.add_dependency "tzinfo", "~> 2.0" s.add_dependency "minitest", "~> 5.1" s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2" s.add_dependency "zeitwerk", "~> 2.2", ">= 2.2.2" diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 03a9aa007d8..6cf116ac9e3 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -95,6 +95,14 @@ module ActiveSupport def self.to_time_preserves_timezone=(value) DateAndTime::Compatibility.preserve_timezone = value end + + def self.utc_to_local_returns_utc_offset_times + DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times + end + + def self.utc_to_local_returns_utc_offset_times=(value) + DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times = value + end end autoload :I18n, "active_support/i18n" diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb index d33c36ef739..b40a0fabb11 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb @@ -12,5 +12,20 @@ module DateAndTime # this behavior, but new apps will have an initializer that sets # this to true, because the new behavior is preferred. mattr_accessor :preserve_timezone, instance_writer: false, default: false + + # Change the output of ActiveSupport::TimeZone.utc_to_local. + # + # When `true`, it returns local times with an UTC offset, with `false` local + # times are returned as UTC. + # + # # Given this zone: + # zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + # + # # With `utc_to_local_returns_utc_offset_times = false`, local time is converted to UTC: + # zone.utc_to_local(Time.utc(2000, 1)) # => 1999-12-31 19:00:00 UTC + # + # # With `utc_to_local_returns_utc_offset_times = true`, local time is returned with UTC offset: + # zone.utc_to_local(Time.utc(2000, 1)) # => 1999-12-31 19:00:00 -0500 + mattr_accessor :utc_to_local_returns_utc_offset_times, instance_writer: false, default: false end end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 3be5f6f7b59..9a4c33ab0f1 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -57,12 +57,12 @@ module ActiveSupport # Returns a Time instance that represents the time in +time_zone+. def time - @time ||= period.to_local(@utc) + @time ||= incorporate_utc_offset(@utc, utc_offset) end # Returns a Time instance of the simultaneous time in the UTC timezone. def utc - @utc ||= period.to_utc(@time) + @utc ||= incorporate_utc_offset(@time, -utc_offset) end alias_method :comparable_time, :utc alias_method :getgm, :utc @@ -104,13 +104,13 @@ module ActiveSupport # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # Time.zone.now.utc? # => false def utc? - period.offset.abbreviation == :UTC || period.offset.abbreviation == :UCT + zone == "UTC" || zone == "UCT" end alias_method :gmt?, :utc? # Returns the offset from current time to UTC time in seconds. def utc_offset - period.utc_total_offset + period.observed_utc_offset end alias_method :gmt_offset, :utc_offset alias_method :gmtoff, :utc_offset @@ -132,7 +132,7 @@ module ActiveSupport # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)" # Time.zone.now.zone # => "EST" def zone - period.zone_identifier.to_s + period.abbreviation end # Returns a string of the object's date, time, zone, and offset from UTC. @@ -524,6 +524,16 @@ module ActiveSupport end private + SECONDS_PER_DAY = 86400 + + def incorporate_utc_offset(time, offset) + if time.kind_of?(Date) + time + Rational(offset, SECONDS_PER_DAY) + else + time + offset + end + end + def get_period_and_ensure_valid_local_time(period) # we don't want a Time.local instance enforcing its own DST rules as well, # so transfer time values to a utc constructor if necessary diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 90830b89bda..2e5d9d3e9d4 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -203,7 +203,7 @@ module ActiveSupport end def find_tzinfo(name) - TZInfo::Timezone.new(MAPPING[name] || name) + TZInfo::Timezone.get(MAPPING[name] || name) end alias_method :create, :new @@ -273,7 +273,7 @@ module ActiveSupport memo end else - create(tz_id, nil, TZInfo::Timezone.new(tz_id)) + create(tz_id, nil, TZInfo::Timezone.get(tz_id)) end end.sort! end @@ -302,11 +302,7 @@ module ActiveSupport # Returns the offset of this time zone from UTC in seconds. def utc_offset - if @utc_offset - @utc_offset - else - tzinfo.current_period.utc_offset if tzinfo && tzinfo.current_period - end + @utc_offset || tzinfo&.current_period&.base_utc_offset end # Returns a formatted string of the offset from UTC, or an alternative @@ -507,10 +503,17 @@ module ActiveSupport end # Adjust the given time to the simultaneous time in the time zone - # represented by +self+. Returns a Time.utc() instance -- if you want an - # ActiveSupport::TimeWithZone instance, use Time#in_time_zone() instead. + # represented by +self+. Returns a local time with the appropriate offset + # -- if you want an ActiveSupport::TimeWithZone instance, use + # Time#in_time_zone() instead. + # + # As of tzinfo 2, utc_to_local returns a Time with a non-zero utc_offset. + # See the `utc_to_local_returns_utc_offset_times` config for more info. def utc_to_local(time) - tzinfo.utc_to_local(time) + tzinfo.utc_to_local(time).yield_self do |t| + ActiveSupport.utc_to_local_returns_utc_offset_times ? + t : Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec, t.sec_fraction) + end end # Adjust the given time to the simultaneous time in UTC. Returns a diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 4b5f3dceee6..646b9e1d319 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -10,8 +10,15 @@ class TimeZoneTest < ActiveSupport::TestCase def test_utc_to_local zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] - assert_equal Time.utc(1999, 12, 31, 19), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500 - assert_equal Time.utc(2000, 6, 30, 20), zone.utc_to_local(Time.utc(2000, 7)) # dst offset -0400 + + with_utc_to_local_returns_utc_offset_times false do + assert_equal Time.utc(1999, 12, 31, 19), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500 + assert_equal Time.utc(2000, 6, 30, 20), zone.utc_to_local(Time.utc(2000, 7)) # dst offset -0400 + end + with_utc_to_local_returns_utc_offset_times true do + assert_equal Time.new(1999, 12, 31, 19, 0, 0, -18000), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500 + assert_equal Time.new(2000, 6, 30, 20, 0, 0, -14400), zone.utc_to_local(Time.utc(2000, 7)) # dst offset -0400 + end end def test_local_to_utc @@ -22,7 +29,7 @@ class TimeZoneTest < ActiveSupport::TestCase def test_period_for_local zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] - assert_instance_of TZInfo::TimezonePeriod, zone.period_for_local(Time.utc(2000)) + assert_kind_of TZInfo::TimezonePeriod, zone.period_for_local(Time.utc(2000)) end ActiveSupport::TimeZone::MAPPING.each_key do |name| @@ -54,7 +61,7 @@ class TimeZoneTest < ActiveSupport::TestCase define_method("test_utc_offset_for_#{name}") do period = zone.tzinfo.current_period - assert_equal period.utc_offset, zone.utc_offset + assert_equal period.base_utc_offset, zone.utc_offset end end @@ -96,8 +103,15 @@ class TimeZoneTest < ActiveSupport::TestCase zone = ActiveSupport::TimeZone["America/Montevideo"] assert_equal ActiveSupport::TimeZone, zone.class assert_equal zone.object_id, ActiveSupport::TimeZone["America/Montevideo"].object_id - assert_equal Time.utc(2010, 1, 31, 22), zone.utc_to_local(Time.utc(2010, 2)) # daylight saving offset -0200 - assert_equal Time.utc(2010, 3, 31, 21), zone.utc_to_local(Time.utc(2010, 4)) # standard offset -0300 + + with_utc_to_local_returns_utc_offset_times false do + assert_equal Time.utc(2010, 1, 31, 22), zone.utc_to_local(Time.utc(2010, 2)) # daylight saving offset -0200 + assert_equal Time.utc(2010, 3, 31, 21), zone.utc_to_local(Time.utc(2010, 4)) # standard offset -0300 + end + with_utc_to_local_returns_utc_offset_times true do + assert_equal Time.new(2010, 1, 31, 22, 0, 0, -7200), zone.utc_to_local(Time.utc(2010, 2)) # daylight saving offset -0200 + assert_equal Time.new(2010, 3, 31, 21, 0, 0, -10800), zone.utc_to_local(Time.utc(2010, 4)) # standard offset -0300 + end end def test_today diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb index 85ed727c9be..ea8b7b28b9e 100644 --- a/activesupport/test/time_zone_test_helpers.rb +++ b/activesupport/test/time_zone_test_helpers.rb @@ -36,4 +36,12 @@ module TimeZoneTestHelpers ActiveSupport::TimeZone::MAPPING.clear ActiveSupport::TimeZone::MAPPING.merge!(old_mappings) end + + def with_utc_to_local_returns_utc_offset_times(value) + old_tzinfo2_format = ActiveSupport.utc_to_local_returns_utc_offset_times + ActiveSupport.utc_to_local_returns_utc_offset_times = value + yield + ensure + ActiveSupport.utc_to_local_returns_utc_offset_times = old_tzinfo2_format + end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 3def30cbf4f..52e181a226d 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -178,6 +178,10 @@ module Rails if respond_to?(:action_dispatch) action_dispatch.cookies_same_site_protection = :lax end + + if respond_to?(:active_support) + active_support.utc_to_local_returns_utc_offset_times = true + end else raise "Unknown version #{target_version.to_s.inspect}" end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_1.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_1.rb.tt index 60af95cb10d..cf9f02f6f32 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_1.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_1.rb.tt @@ -21,3 +21,7 @@ # This change is not backwards compatible with earlier Rails versions. # It's best enabled when your entire app is migrated and stable on 6.1. # Rails.application.config.action_dispatch.cookies_same_site_protection = :lax + +# Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an +# UTC offset or a UTC time. +# Rails.application.config.active_support.utc_to_local_returns_utc_offset_times = true diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index a8910db5f8a..acfc1b71b7c 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -2295,6 +2295,15 @@ module ApplicationTests assert_equal :lax, Rails.application.config.action_dispatch.cookies_same_site_protection end + test "enables utc_to_local_returns_utc_offset_times by default" do + remove_from_config '.*config\.load_defaults.*\n' + add_to_config 'config.load_defaults "6.1"' + + app "development" + + assert_equal true, Rails.application.config.active_support.utc_to_local_returns_utc_offset_times + end + test "ActiveStorage.queues[:analysis] is :active_storage_analysis by default" do app "development"