From da1828a99dd9397cbe0f6e6b2b5652542370f41b Mon Sep 17 00:00:00 2001 From: Phil Ross Date: Thu, 16 Mar 2017 20:08:00 +0000 Subject: [PATCH] Improved algorithm for deriving the utc_offset for zoneinfo files. Resolves #66, correctly handling Pacific/Apia switching from one side of the International Date Line to the other whilst observing daylight savings time. The previous algorithm only looked one transition forward and back and always preferred the prior period to define the utc_offset of a DST period. The new algorithm considers all transitions forward and back until a non-DST period is found and defines utc_offset of a DST period based on the smallest difference. (cherry picked from commit 45dcc3d393414bfef259f5062f57b7c4cc6fd755) --- lib/tzinfo/zoneinfo_timezone_info.rb | 149 ++++++++---- test/tc_zoneinfo_timezone_info.rb | 337 +++++++++++++++++++++++++-- 2 files changed, 419 insertions(+), 67 deletions(-) diff --git a/lib/tzinfo/zoneinfo_timezone_info.rb b/lib/tzinfo/zoneinfo_timezone_info.rb index 9061e4e7..6273ebfb 100644 --- a/lib/tzinfo/zoneinfo_timezone_info.rb +++ b/lib/tzinfo/zoneinfo_timezone_info.rb @@ -56,39 +56,109 @@ def check_read(file, bytes) result end - # Zoneinfo doesn't include the offset from standard time (std_offset). - # Derive the missing offsets by looking at changes in the total UTC - # offset. + # Zoneinfo files don't include the offset from standard time (std_offset) + # for DST periods. Derive the base offset (utc_offset) where DST is + # observed from either the previous or next non-DST period. # - # This will be run through forwards and then backwards by the parse - # method. - def derive_offsets(transitions, offsets) - previous_offset = nil + # Returns the index of the offset to be used prior to the first + # transition. + def derive_offsets(transitions, offsets) + # The first non-DST offset (if there is one) is the offset observed + # before the first transition. Fallback to the first DST offset if there + # are no non-DST offsets. + first_non_dst_offset_index = offsets.index {|o| !o[:is_dst] } + first_offset = first_non_dst_offset_index || 0 + return first_offset if transitions.empty? - transitions.each do |t| - offset = offsets[t[:offset]] + # Determine the utc_offset of the next non-dst offset at each transition. + utc_offset_from_next = nil - if !offset[:std_offset] && offset[:is_dst] && previous_offset - difference = offset[:utc_total_offset] - previous_offset[:utc_total_offset] - - if previous_offset[:is_dst] - if previous_offset[:std_offset] - std_offset = previous_offset[:std_offset] + difference - else - std_offset = nil - end - else - std_offset = difference + transitions.reverse_each do |transition| + offset = offsets[transition[:offset]] + if offset[:is_dst] + transition[:utc_offset_from_next] = utc_offset_from_next if utc_offset_from_next + else + utc_offset_from_next = offset[:utc_total_offset] + end + end + + utc_offset_from_previous = first_non_dst_offset_index ? offsets[first_non_dst_offset_index][:utc_total_offset] : nil + defined_offsets = {} + + transitions.each do |transition| + offset_index = transition[:offset] + offset = offsets[offset_index] + utc_total_offset = offset[:utc_total_offset] + + if offset[:is_dst] + utc_offset_from_next = transition[:utc_offset_from_next] + + difference_to_previous = utc_total_offset - (utc_offset_from_previous || utc_total_offset) + difference_to_next = utc_total_offset - (utc_offset_from_next || utc_total_offset) + + utc_offset = if difference_to_previous > 0 && difference_to_next > 0 + difference_to_previous < difference_to_next ? utc_offset_from_previous : utc_offset_from_next + elsif difference_to_previous > 0 + utc_offset_from_previous + elsif difference_to_next > 0 + utc_offset_from_next + else # difference_to_previous <= 0 && difference_to_next <= 0 + # DST, but the either the offset has stayed the same or decreased + # relative to both the previous and next used base utc offset, or + # there are no non-DST offsets. Assume a 1 hour offset from base. + utc_total_offset - 3600 end - - if std_offset && std_offset > 0 - offset[:std_offset] = std_offset - offset[:utc_offset] = offset[:utc_total_offset] - std_offset + + if !offset[:utc_offset] + offset[:utc_offset] = utc_offset + defined_offsets[offset] = offset_index + elsif offset[:utc_offset] != utc_offset + # An earlier transition has already derived a different + # utc_offset. Define a new offset or reuse an existing identically + # defined offset. + new_offset = offset.dup + new_offset[:utc_offset] = utc_offset + + offset_index = defined_offsets[new_offset] + + unless offset_index + offsets << new_offset + offset_index = offsets.length - 1 + defined_offsets[new_offset] = offset_index + end + + transition[:offset] = offset_index end + else + utc_offset_from_previous = utc_total_offset end - - previous_offset = offset end + + first_offset + end + + # Defines an offset for the timezone based on the given index and offset + # Hash. + def define_offset(index, offset) + utc_total_offset = offset[:utc_total_offset] + utc_offset = offset[:utc_offset] + + if utc_offset + # DST offset with base utc_offset derived by derive_offsets. + std_offset = utc_total_offset - utc_offset + elsif offset[:is_dst] + # DST offset unreferenced by a transition (offset in use before the + # first transition). No derived base UTC offset, so assume 1 hour + # DST. + utc_offset = utc_total_offset - 3600 + std_offset = 3600 + else + # Non-DST offset. + utc_offset = utc_total_offset + std_offset = 0 + end + + offset index, utc_offset, std_offset, offset[:abbr].untaint.to_sym end # Parses a zoneinfo file and intializes the DataTimezoneInfo structures. @@ -179,33 +249,12 @@ def parse(file) end # Derive the offsets from standard time (std_offset). - derive_offsets(transitions, offsets) - derive_offsets(transitions.reverse, offsets) + first_offset_index = derive_offsets(transitions, offsets) - # Assign anything left a standard offset of one hour - offsets.each do |o| - if !o[:std_offset] && o[:is_dst] - o[:std_offset] = 3600 - o[:utc_offset] = o[:utc_total_offset] - 3600 - end - end - - # Find the first non-dst offset. This is used as the offset for the time - # before the first transition. - first = nil - offsets.each_with_index do |o, i| - if !o[:is_dst] - first = i - break - end - end - - if first - offset first, offsets[first][:utc_offset], offsets[first][:std_offset], offsets[first][:abbr].untaint.to_sym - end + define_offset(first_offset_index, offsets[first_offset_index]) offsets.each_with_index do |o, i| - offset i, o[:utc_offset], o[:std_offset], o[:abbr].untaint.to_sym unless i == first + define_offset(i, o) unless i == first_offset_index end if !using_64bit && !RubyCoreSupport.time_supports_negative diff --git a/test/tc_zoneinfo_timezone_info.rb b/test/tc_zoneinfo_timezone_info.rb index 077e41f5..cc928966 100644 --- a/test/tc_zoneinfo_timezone_info.rb +++ b/test/tc_zoneinfo_timezone_info.rb @@ -707,27 +707,39 @@ def test_load_starts_two_hour_std_offset end end - def test_load_starts_all_same_dst_offset + def test_load_starts_only_dst_transition_with_lmt # The zoneinfo files don't include the offset from standard time, so this # has to be derived by looking at changes in the total UTC offset. - # - # If there are no changes in the UTC offset (ignoring the first offset, - # which is usually local mean time), then a value of 1 hour is used as the - # standard time offset. - + offsets = [ {:gmtoff => 3542, :isdst => false, :abbrev => 'LMT'}, - {:gmtoff => 7200, :isdst => true, :abbrev => 'XDDT'}] - - transitions = [ - {:at => Time.utc(2000, 1, 1), :offset_index => 1}] - - tzif_test(offsets, transitions) do |path, format| - info = ZoneinfoTimezoneInfo.new('Zone/DoubleDaylight', path) - assert_equal('Zone/DoubleDaylight', info.identifier) - - assert_period(:LMT, 3542, 0, false, nil, Time.utc(2000, 1, 1), info) - assert_period(:XDDT, 3600, 3600, true, Time.utc(2000, 1, 1), nil, info) + {:gmtoff => 7200, :isdst => true, :abbrev => 'XDT'}] + + transitions = [{:at => Time.utc(2000, 1, 1), :offset_index => 1}] + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/OnlyDST', path) + assert_equal('Zone/OnlyDST', info.identifier) + + assert_period(:LMT, 3542, 0, false, nil, Time.utc(2000, 1, 1), info) + assert_period(:XDT, 3542, 3658, true, Time.utc(2000, 1, 1), nil, info) + end + end + + def test_load_starts_only_dst_transition_without_lmt + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + offsets = [{:gmtoff => 7200, :isdst => true, :abbrev => 'XDT'}] + + transitions = [{:at => Time.utc(2000, 1, 1), :offset_index => 0}] + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/OnlyDST', path) + assert_equal('Zone/OnlyDST', info.identifier) + + assert_period(:XDT, 3600, 3600, true, nil, Time.utc(2000, 1, 1), info) + assert_period(:XDT, 3600, 3600, true, Time.utc(2000, 1, 1), nil, info) end end @@ -756,6 +768,297 @@ def test_load_switch_to_dst_and_change_utc_offset assert_period(:XDT, 0, 3600, true, Time.utc(2000, 2, 1), nil, info) end end + + def test_load_apia_international_dateline_change + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + # Pacific/Apia moved across the International Date Line whilst observing + # daylight savings time. + + offsets = [ + {:gmtoff => 45184, :isdst => false, :abbrev => 'LMT'}, + {:gmtoff => -39600, :isdst => false, :abbrev => '-11'}, + {:gmtoff => -36000, :isdst => true, :abbrev => '-10'}, + {:gmtoff => 50400, :isdst => true, :abbrev => '+14'}, + {:gmtoff => 46800, :isdst => false, :abbrev => '+13'}] + + transitions = [ + {:at => Time.utc(2011, 4, 2, 14, 0, 0), :offset_index => 1}, + {:at => Time.utc(2011, 9, 24, 14, 0, 0), :offset_index => 2}, + {:at => Time.utc(2011, 12, 30, 10, 0, 0), :offset_index => 3}, + {:at => Time.utc(2012, 3, 31, 14, 0, 0), :offset_index => 4}] + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Test/Pacific/Apia', path) + assert_equal('Test/Pacific/Apia', info.identifier) + + assert_period( :LMT, 45184, 0, false, nil, Time.utc(2011, 4, 2, 14, 0, 0), info) + assert_period(:'-11', -39600, 0, false, Time.utc(2011, 4, 2, 14, 0, 0), Time.utc(2011, 9, 24, 14, 0, 0), info) + assert_period(:'-10', -39600, 3600, true, Time.utc(2011, 9, 24, 14, 0, 0), Time.utc(2011, 12, 30, 10, 0, 0), info) + assert_period(:'+14', 46800, 3600, true, Time.utc(2011, 12, 30, 10, 0, 0), Time.utc(2012, 3, 31, 14, 0, 0), info) + assert_period(:'+13', 46800, 0, false, Time.utc(2012, 3, 31, 14, 0, 0), nil, info) + end + end + + def test_load_offset_split_for_different_utc_offset + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + offsets = [ + {:gmtoff => 3542, :isdst => false, :abbrev => 'LMT'}, + {:gmtoff => 3600, :isdst => false, :abbrev => 'XST1'}, + {:gmtoff => 7200, :isdst => false, :abbrev => 'XST2'}, + {:gmtoff => 10800, :isdst => true, :abbrev => 'XDT'}] + + transitions = [ + {:at => Time.utc(2000, 1, 1), :offset_index => 1}, + {:at => Time.utc(2000, 2, 1), :offset_index => 3}, + {:at => Time.utc(2000, 3, 1), :offset_index => 1}, + {:at => Time.utc(2000, 4, 1), :offset_index => 2}, + {:at => Time.utc(2000, 5, 1), :offset_index => 3}, + {:at => Time.utc(2000, 6, 1), :offset_index => 2}, + {:at => Time.utc(2000, 7, 1), :offset_index => 1}, + {:at => Time.utc(2000, 8, 1), :offset_index => 3}, + {:at => Time.utc(2000, 9, 1), :offset_index => 1}, + {:at => Time.utc(2000, 10, 1), :offset_index => 2}, + {:at => Time.utc(2000, 11, 1), :offset_index => 3}, + {:at => Time.utc(2000, 12, 1), :offset_index => 2}] + + # XDT will be split and defined according to its surrounding standard time + # offsets. + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/SplitUtcOffset', path) + assert_equal('Zone/SplitUtcOffset', info.identifier) + + assert_period( :LMT, 3542, 0, false, nil, Time.utc(2000, 1, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 1, 1), Time.utc(2000, 2, 1), info) + assert_period( :XDT, 3600, 7200, true, Time.utc(2000, 2, 1), Time.utc(2000, 3, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 3, 1), Time.utc(2000, 4, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 4, 1), Time.utc(2000, 5, 1), info) + assert_period( :XDT, 7200, 3600, true, Time.utc(2000, 5, 1), Time.utc(2000, 6, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 6, 1), Time.utc(2000, 7, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 7, 1), Time.utc(2000, 8, 1), info) + assert_period( :XDT, 3600, 7200, true, Time.utc(2000, 8, 1), Time.utc(2000, 9, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 9, 1), Time.utc(2000, 10, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 10, 1), Time.utc(2000, 11, 1), info) + assert_period( :XDT, 7200, 3600, true, Time.utc(2000, 11, 1), Time.utc(2000, 12, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 12, 1), nil, info) + + 1.upto(6) do |i| + assert_same(info.period_for_utc(Time.utc(2000, i, 1)).offset, info.period_for_utc(Time.utc(2000, i + 6, 1)).offset) + end + end + end + + def test_load_offset_utc_offset_taken_from_minimum_difference_minimum_after + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + offsets = [ + {:gmtoff => 3542, :isdst => false, :abbrev => 'LMT'}, + {:gmtoff => 3600, :isdst => false, :abbrev => 'XST1'}, + {:gmtoff => 7200, :isdst => false, :abbrev => 'XST2'}, + {:gmtoff => 10800, :isdst => true, :abbrev => 'XDT'}] + + transitions = [ + {:at => Time.utc(2000, 1, 1), :offset_index => 1}, + {:at => Time.utc(2000, 2, 1), :offset_index => 3}, + {:at => Time.utc(2000, 3, 1), :offset_index => 2}] + + # XDT should use the closest utc_offset (7200) (and not an equivalent + # utc_offset of 3600 and std_offset of 7200). + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/MinimumUtcOffset', path) + assert_equal('Zone/MinimumUtcOffset', info.identifier) + + assert_period( :LMT, 3542, 0, false, nil, Time.utc(2000, 1, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 1, 1), Time.utc(2000, 2, 1), info) + assert_period( :XDT, 7200, 3600, true, Time.utc(2000, 2, 1), Time.utc(2000, 3, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 3, 1), nil, info) + end + end + + def test_load_offset_utc_offset_taken_from_minimum_difference_minimum_before + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + offsets = [ + {:gmtoff => 3542, :isdst => false, :abbrev => 'LMT'}, + {:gmtoff => 3600, :isdst => false, :abbrev => 'XST1'}, + {:gmtoff => 7200, :isdst => false, :abbrev => 'XST2'}, + {:gmtoff => 10800, :isdst => true, :abbrev => 'XDT'}] + + transitions = [ + {:at => Time.utc(2000, 1, 1), :offset_index => 2}, + {:at => Time.utc(2000, 2, 1), :offset_index => 3}, + {:at => Time.utc(2000, 3, 1), :offset_index => 1}] + + # XDT should use the closest utc_offset (7200) (and not an equivalent + # utc_offset of 3600 and std_offset of 7200). + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/MinimumUtcOffset', path) + assert_equal('Zone/MinimumUtcOffset', info.identifier) + + assert_period( :LMT, 3542, 0, false, nil, Time.utc(2000, 1, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 1, 1), Time.utc(2000, 2, 1), info) + assert_period( :XDT, 7200, 3600, true, Time.utc(2000, 2, 1), Time.utc(2000, 3, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 3, 1), nil, info) + end + end + + def test_load_offset_does_not_use_equal_utc_total_offset_equal_after + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + offsets = [ + {:gmtoff => 3542, :isdst => false, :abbrev => 'LMT'}, + {:gmtoff => 3600, :isdst => false, :abbrev => 'XST1'}, + {:gmtoff => 7200, :isdst => false, :abbrev => 'XST2'}, + {:gmtoff => 7200, :isdst => true, :abbrev => 'XDT'}] + + transitions = [ + {:at => Time.utc(2000, 1, 1), :offset_index => 1}, + {:at => Time.utc(2000, 2, 1), :offset_index => 3}, + {:at => Time.utc(2000, 3, 1), :offset_index => 2}] + + # XDT will be based on the utc_offset of XST1 even though XST2 has an + # equivalent (or greater) utc_total_offset. + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/UtcOffsetEqual', path) + assert_equal('Zone/UtcOffsetEqual', info.identifier) + + assert_period( :LMT, 3542, 0, false, nil, Time.utc(2000, 1, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 1, 1), Time.utc(2000, 2, 1), info) + assert_period( :XDT, 3600, 3600, true, Time.utc(2000, 2, 1), Time.utc(2000, 3, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 3, 1), nil, info) + end + end + + def test_load_offset_does_not_use_equal_utc_total_offset_equal_before + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + offsets = [ + {:gmtoff => 3542, :isdst => false, :abbrev => 'LMT'}, + {:gmtoff => 3600, :isdst => false, :abbrev => 'XST1'}, + {:gmtoff => 7200, :isdst => false, :abbrev => 'XST2'}, + {:gmtoff => 7200, :isdst => true, :abbrev => 'XDT'}] + + transitions = [ + {:at => Time.utc(2000, 1, 1), :offset_index => 2}, + {:at => Time.utc(2000, 2, 1), :offset_index => 3}, + {:at => Time.utc(2000, 3, 1), :offset_index => 1}] + + # XDT will be based on the utc_offset of XST1 even though XST2 has an + # equivalent (or greater) utc_total_offset. + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/UtcOffsetEqual', path) + assert_equal('Zone/UtcOffsetEqual', info.identifier) + + assert_period( :LMT, 3542, 0, false, nil, Time.utc(2000, 1, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 1, 1), Time.utc(2000, 2, 1), info) + assert_period( :XDT, 3600, 3600, true, Time.utc(2000, 2, 1), Time.utc(2000, 3, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 3, 1), nil, info) + end + end + + def test_load_offset_both_adjacent_non_dst_equal_utc_total_offset + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + offsets = [ + {:gmtoff => 7142, :isdst => false, :abbrev => 'LMT'}, + {:gmtoff => 7200, :isdst => false, :abbrev => 'XST'}, + {:gmtoff => 7200, :isdst => true, :abbrev => 'XDT'}] + + transitions = [ + {:at => Time.utc(2000, 1, 1), :offset_index => 1}, + {:at => Time.utc(2000, 2, 1), :offset_index => 2}, + {:at => Time.utc(2000, 3, 1), :offset_index => 1}] + + # XDT will just assume an std_offset of +1 hour and calculate the utc_offset + # from utc_total_offset - std_offset. + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/AdjacentEqual', path) + assert_equal('Zone/AdjacentEqual', info.identifier) + + assert_period(:LMT, 7142, 0, false, nil, Time.utc(2000, 1, 1), info) + assert_period(:XST, 7200, 0, false, Time.utc(2000, 1, 1), Time.utc(2000, 2, 1), info) + assert_period(:XDT, 3600, 3600, true, Time.utc(2000, 2, 1), Time.utc(2000, 3, 1), info) + assert_period(:XST, 7200, 0, false, Time.utc(2000, 3, 1), nil, info) + end + end + + def test_load_offset_utc_offset_preserved_from_next + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + offsets = [ + {:gmtoff => 3542, :isdst => false, :abbrev => 'LMT'}, + {:gmtoff => 3600, :isdst => false, :abbrev => 'XST1'}, + {:gmtoff => 7200, :isdst => false, :abbrev => 'XST2'}, + {:gmtoff => 10800, :isdst => true, :abbrev => 'XDT1'}, + {:gmtoff => 10800, :isdst => true, :abbrev => 'XDT2'}] + + transitions = [ + {:at => Time.utc(2000, 1, 1), :offset_index => 1}, + {:at => Time.utc(2000, 2, 1), :offset_index => 3}, + {:at => Time.utc(2000, 3, 1), :offset_index => 4}, + {:at => Time.utc(2000, 4, 1), :offset_index => 2}] + + # Both XDT1 and XDT2 should both use the closest utc_offset (7200) (and not + # an equivalent utc_offset of 3600 and std_offset of 7200). + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/UtcOffsetPreserved', path) + assert_equal('Zone/UtcOffsetPreserved', info.identifier) + + assert_period( :LMT, 3542, 0, false, nil, Time.utc(2000, 1, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 1, 1), Time.utc(2000, 2, 1), info) + assert_period(:XDT1, 7200, 3600, true, Time.utc(2000, 2, 1), Time.utc(2000, 3, 1), info) + assert_period(:XDT2, 7200, 3600, true, Time.utc(2000, 3, 1), Time.utc(2000, 4, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 4, 1), nil, info) + end + end + + def test_load_offset_utc_offset_preserved_from_previous + # The zoneinfo files don't include the offset from standard time, so this + # has to be derived by looking at changes in the total UTC offset. + + offsets = [ + {:gmtoff => 3542, :isdst => false, :abbrev => 'LMT'}, + {:gmtoff => 3600, :isdst => false, :abbrev => 'XST1'}, + {:gmtoff => 7200, :isdst => false, :abbrev => 'XST2'}, + {:gmtoff => 10800, :isdst => true, :abbrev => 'XDT1'}, + {:gmtoff => 10800, :isdst => true, :abbrev => 'XDT2'}] + + transitions = [ + {:at => Time.utc(2000, 1, 1), :offset_index => 2}, + {:at => Time.utc(2000, 2, 1), :offset_index => 3}, + {:at => Time.utc(2000, 3, 1), :offset_index => 4}, + {:at => Time.utc(2000, 4, 1), :offset_index => 1}] + + # Both XDT1 and XDT2 should both use the closest utc_offset (7200) (and not + # an equivalent utc_offset of 3600 and std_offset of 7200). + + tzif_test(offsets, transitions) do |path, format| + info = ZoneinfoTimezoneInfo.new('Zone/UtcOffsetPreserved', path) + assert_equal('Zone/UtcOffsetPreserved', info.identifier) + + assert_period( :LMT, 3542, 0, false, nil, Time.utc(2000, 1, 1), info) + assert_period(:XST2, 7200, 0, false, Time.utc(2000, 1, 1), Time.utc(2000, 2, 1), info) + assert_period(:XDT1, 7200, 3600, true, Time.utc(2000, 2, 1), Time.utc(2000, 3, 1), info) + assert_period(:XDT2, 7200, 3600, true, Time.utc(2000, 3, 1), Time.utc(2000, 4, 1), info) + assert_period(:XST1, 3600, 0, false, Time.utc(2000, 4, 1), nil, info) + end + end def test_load_in_safe_mode offsets = [{:gmtoff => -12094, :isdst => false, :abbrev => 'LT'}]