tzinfo 1.2.5 → 1.2.9

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of tzinfo might be problematic. Click here for more details.

Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +3 -3
  3. data.tar.gz.sig +0 -0
  4. data/CHANGES.md +86 -48
  5. data/LICENSE +1 -1
  6. data/README.md +9 -8
  7. data/lib/tzinfo.rb +3 -0
  8. data/lib/tzinfo/annual_rules.rb +51 -0
  9. data/lib/tzinfo/data_source.rb +1 -1
  10. data/lib/tzinfo/posix_time_zone_parser.rb +136 -0
  11. data/lib/tzinfo/ruby_core_support.rb +24 -1
  12. data/lib/tzinfo/ruby_data_source.rb +24 -20
  13. data/lib/tzinfo/time_or_datetime.rb +11 -0
  14. data/lib/tzinfo/timezone.rb +10 -6
  15. data/lib/tzinfo/transition_rule.rb +325 -0
  16. data/lib/tzinfo/zoneinfo_data_source.rb +10 -1
  17. data/lib/tzinfo/zoneinfo_timezone_info.rb +264 -40
  18. data/test/tc_annual_rules.rb +95 -0
  19. data/test/tc_country.rb +6 -2
  20. data/test/tc_posix_time_zone_parser.rb +261 -0
  21. data/test/tc_ruby_data_source.rb +26 -2
  22. data/test/tc_time_or_datetime.rb +26 -6
  23. data/test/tc_timezone.rb +13 -2
  24. data/test/tc_transition_data_timezone_info.rb +11 -1
  25. data/test/tc_transition_rule.rb +663 -0
  26. data/test/tc_zoneinfo_data_source.rb +11 -2
  27. data/test/tc_zoneinfo_timezone_info.rb +1034 -113
  28. data/test/test_utils.rb +32 -3
  29. data/test/ts_all_zoneinfo.rb +3 -1
  30. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +5 -5
  31. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +13 -1
  32. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +13 -1
  33. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +1 -1
  34. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +2 -2
  35. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +2 -2
  36. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +1 -1
  37. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +15 -3
  38. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +13 -1
  39. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +13 -1
  40. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +15 -3
  41. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +19 -4
  42. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +1 -1
  43. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +197 -184
  44. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +60 -47
  45. data/test/tzinfo-data/tzinfo/data/version.rb +9 -3
  46. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  47. data/test/zoneinfo/America/New_York +0 -0
  48. data/test/zoneinfo/Australia/Melbourne +0 -0
  49. data/test/zoneinfo/EST +0 -0
  50. data/test/zoneinfo/Etc/UTC +0 -0
  51. data/test/zoneinfo/Europe/Amsterdam +0 -0
  52. data/test/zoneinfo/Europe/Andorra +0 -0
  53. data/test/zoneinfo/Europe/London +0 -0
  54. data/test/zoneinfo/Europe/Paris +0 -0
  55. data/test/zoneinfo/Europe/Prague +0 -0
  56. data/test/zoneinfo/Factory +0 -0
  57. data/test/zoneinfo/iso3166.tab +13 -14
  58. data/test/zoneinfo/leapseconds +38 -21
  59. data/test/zoneinfo/posix/Europe/London +0 -0
  60. data/test/zoneinfo/posixrules +0 -0
  61. data/test/zoneinfo/right/Europe/London +0 -0
  62. data/test/zoneinfo/zone.tab +172 -159
  63. data/test/zoneinfo/zone1970.tab +185 -170
  64. data/tzinfo.gemspec +2 -2
  65. metadata +28 -24
  66. metadata.gz.sig +0 -0
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2005-2018 Philip Ross
1
+ Copyright (c) 2005-2020 Philip Ross
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  TZInfo - Ruby Timezone Library
2
2
  ==============================
3
3
 
4
- [![Gem Version](https://badge.fury.io/rb/tzinfo.svg)](http://badge.fury.io/rb/tzinfo) [![Build Status](https://travis-ci.org/tzinfo/tzinfo.svg?branch=master)](https://travis-ci.org/tzinfo/tzinfo)
4
+ [![RubyGems](https://img.shields.io/gem/v/tzinfo)](https://rubygems.org/gems/tzinfo) [![Travis CI Build](https://img.shields.io/travis/com/tzinfo/tzinfo/1.2?logo=travis)](https://travis-ci.com/tzinfo/tzinfo) [![AppVeyor Build](https://img.shields.io/appveyor/build/philr/tzinfo/1.2?logo=appveyor)](https://ci.appveyor.com/project/philr/tzinfo/branch/1.2)
5
5
 
6
- [TZInfo](http://tzinfo.github.io) provides daylight savings aware
6
+ [TZInfo](https://tzinfo.github.io) provides daylight savings aware
7
7
  transformations between times in different timezones.
8
8
 
9
9
 
@@ -13,10 +13,10 @@ Data Sources
13
13
  TZInfo requires a source of timezone data. There are two built-in options:
14
14
 
15
15
  1. The TZInfo::Data library (the tzinfo-data gem). TZInfo::Data contains a set
16
- of Ruby modules that are generated from the [IANA Time Zone Database](http://www.iana.org/time-zones).
16
+ of Ruby modules that are generated from the [IANA Time Zone Database](https://www.iana.org/time-zones).
17
17
  2. A zoneinfo directory. Most Unix-like systems include a zoneinfo directory
18
18
  containing timezone definitions. These are also generated from the
19
- [IANA Time Zone Database](http://www.iana.org/time-zones).
19
+ [IANA Time Zone Database](https://www.iana.org/time-zones).
20
20
 
21
21
  By default, TZInfo::Data will be used. If TZInfo::Data is not available (i.e.
22
22
  if `require 'tzinfo/data'` fails), then TZInfo will search for a zoneinfo
@@ -25,7 +25,7 @@ directory instead (using the search path specified by
25
25
 
26
26
  If no data source can be found, a `TZInfo::DataSourceNotFound` exception will be
27
27
  raised when TZInfo is used. Further information is available
28
- [in the wiki](http://tzinfo.github.io/datasourcenotfound) to help with
28
+ [in the wiki](https://tzinfo.github.io/datasourcenotfound) to help with
29
29
  resolving `TZInfo::DataSourceNotFound` errors.
30
30
 
31
31
  The default data source selection can be overridden using
@@ -60,8 +60,9 @@ of `TZInfo::Timezone`) and convert a time in UTC to local New York time:
60
60
  local = tz.utc_to_local(Time.utc(2005,8,29,15,35,0))
61
61
 
62
62
  Note that the local Time returned will have a UTC timezone (`local.zone` will
63
- return `"UTC"`). This is because the Ruby Time class only supports two timezones:
64
- UTC and the current system local timezone.
63
+ return `"UTC"`). This is because the Time class in older (but still supported by
64
+ TZInfo) versions of Ruby can only handle two timezones: UTC and the system local
65
+ timezone.
65
66
 
66
67
  To convert from a local time to UTC, the `local_to_utc` method can be used as
67
68
  follows:
@@ -129,7 +130,7 @@ thread boundaries.
129
130
  Documentation
130
131
  -------------
131
132
 
132
- API documentation for TZInfo is available on [RubyDoc.info](http://rubydoc.info/gems/tzinfo/frames).
133
+ API documentation for TZInfo is available on [RubyDoc.info](https://rubydoc.info/gems/tzinfo/frames).
133
134
 
134
135
 
135
136
  License
data/lib/tzinfo.rb CHANGED
@@ -10,6 +10,8 @@ require 'tzinfo/timezone_definition'
10
10
 
11
11
  require 'tzinfo/timezone_offset'
12
12
  require 'tzinfo/timezone_transition'
13
+ require 'tzinfo/transition_rule'
14
+ require 'tzinfo/annual_rules'
13
15
  require 'tzinfo/timezone_transition_definition'
14
16
 
15
17
  require 'tzinfo/timezone_index_definition'
@@ -22,6 +24,7 @@ require 'tzinfo/zoneinfo_timezone_info'
22
24
 
23
25
  require 'tzinfo/data_source'
24
26
  require 'tzinfo/ruby_data_source'
27
+ require 'tzinfo/posix_time_zone_parser'
25
28
  require 'tzinfo/zoneinfo_data_source'
26
29
 
27
30
  require 'tzinfo/timezone_period'
@@ -0,0 +1,51 @@
1
+ module TZInfo
2
+ # A set of rules that define when transitions occur in time zones with
3
+ # annually occurring daylight savings time.
4
+ #
5
+ # @private
6
+ class AnnualRules #:nodoc:
7
+ # Returned by #transitions. #offset is the TimezoneOffset that applies
8
+ # from the UTC TimeOrDateTime #at. #previous_offset is the prior
9
+ # TimezoneOffset.
10
+ Transition = Struct.new(:offset, :previous_offset, :at)
11
+
12
+ # The standard offset that applies when daylight savings time is not in
13
+ # force.
14
+ attr_reader :std_offset
15
+
16
+ # The offset that applies when daylight savings time is in force.
17
+ attr_reader :dst_offset
18
+
19
+ # The rule that determines when daylight savings time starts.
20
+ attr_reader :dst_start_rule
21
+
22
+ # The rule that determines when daylight savings time ends.
23
+ attr_reader :dst_end_rule
24
+
25
+ # Initializes a new {AnnualRules} instance.
26
+ def initialize(std_offset, dst_offset, dst_start_rule, dst_end_rule)
27
+ @std_offset = std_offset
28
+ @dst_offset = dst_offset
29
+ @dst_start_rule = dst_start_rule
30
+ @dst_end_rule = dst_end_rule
31
+ end
32
+
33
+ # Returns the transitions between standard and daylight savings time for a
34
+ # given year. The results are ordered by time of occurrence (earliest to
35
+ # latest).
36
+ def transitions(year)
37
+ start_dst = apply_rule(@dst_start_rule, @std_offset, @dst_offset, year)
38
+ end_dst = apply_rule(@dst_end_rule, @dst_offset, @std_offset, year)
39
+
40
+ end_dst.at < start_dst.at ? [end_dst, start_dst] : [start_dst, end_dst]
41
+ end
42
+
43
+ private
44
+
45
+ # Applies a given rule between offsets on a year.
46
+ def apply_rule(rule, from_offset, to_offset, year)
47
+ at = rule.at(from_offset, year)
48
+ Transition.new(to_offset, from_offset, at)
49
+ end
50
+ end
51
+ end
@@ -179,7 +179,7 @@ module TZInfo
179
179
  begin
180
180
  return ZoneinfoDataSource.new
181
181
  rescue ZoneinfoDirectoryNotFound
182
- raise DataSourceNotFound, "No source of timezone data could be found.\nPlease refer to http://tzinfo.github.io/datasourcenotfound for help resolving this error."
182
+ raise DataSourceNotFound, "No source of timezone data could be found.\nPlease refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error."
183
183
  end
184
184
  end
185
185
 
@@ -0,0 +1,136 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'strscan'
5
+
6
+ module TZInfo
7
+ # An {InvalidPosixTimeZone} exception is raised if an invalid POSIX-style
8
+ # time zone string is encountered.
9
+ #
10
+ # @private
11
+ class InvalidPosixTimeZone < StandardError #:nodoc:
12
+ end
13
+
14
+ # A parser for POSIX-style TZ strings used in zoneinfo files and specified
15
+ # by tzfile.5 and tzset.3.
16
+ #
17
+ # @private
18
+ class PosixTimeZoneParser #:nodoc:
19
+ # Parses a POSIX-style TZ string, returning either a TimezoneOffset or
20
+ # an AnnualRules instance.
21
+ def parse(tz_string)
22
+ raise InvalidPosixTimeZone unless tz_string.kind_of?(String)
23
+ return nil if tz_string.empty?
24
+
25
+ s = StringScanner.new(tz_string)
26
+ check_scan(s, /([^-+,\d<][^-+,\d]*) | <([^>]+)>/x)
27
+ std_abbrev = s[1] || s[2]
28
+ check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
29
+ std_offset = get_offset_from_hms(s[1], s[2], s[3])
30
+
31
+ if s.scan(/([^-+,\d<][^-+,\d]*) | <([^>]+)>/x)
32
+ dst_abbrev = s[1] || s[2]
33
+
34
+ if s.scan(/([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
35
+ dst_offset = get_offset_from_hms(s[1], s[2], s[3])
36
+ else
37
+ # POSIX is negative for ahead of UTC.
38
+ dst_offset = std_offset - 3600
39
+ end
40
+
41
+ dst_difference = std_offset - dst_offset
42
+
43
+ start_rule = parse_rule(s, 'start')
44
+ end_rule = parse_rule(s, 'end')
45
+
46
+ raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'." if s.rest?
47
+
48
+ if start_rule.is_always_first_day_of_year? && start_rule.transition_at == 0 &&
49
+ end_rule.is_always_last_day_of_year? && end_rule.transition_at == 86400 + dst_difference
50
+ # Constant daylight savings time.
51
+ # POSIX is negative for ahead of UTC.
52
+ TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev.to_sym)
53
+ else
54
+ AnnualRules.new(
55
+ TimezoneOffset.new(-std_offset, 0, std_abbrev.to_sym),
56
+ TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev.to_sym),
57
+ start_rule,
58
+ end_rule)
59
+ end
60
+ elsif !s.rest?
61
+ # Constant standard time.
62
+ # POSIX is negative for ahead of UTC.
63
+ TimezoneOffset.new(-std_offset, 0, std_abbrev.to_sym)
64
+ else
65
+ raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'."
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ # Parses the rule from the TZ string, returning a TransitionRule.
72
+ def parse_rule(s, type)
73
+ check_scan(s, /,(?: (?: J(\d+) ) | (\d+) | (?: M(\d+)\.(\d)\.(\d) ) )/x)
74
+ julian_day_of_year = s[1]
75
+ absolute_day_of_year = s[2]
76
+ month = s[3]
77
+ week = s[4]
78
+ day_of_week = s[5]
79
+
80
+ if s.scan(/\//)
81
+ check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/)
82
+ transition_at = get_seconds_after_midnight_from_hms(s[1], s[2], s[3])
83
+ else
84
+ transition_at = 7200
85
+ end
86
+
87
+ begin
88
+ if julian_day_of_year
89
+ JulianDayOfYearTransitionRule.new(julian_day_of_year.to_i, transition_at)
90
+ elsif absolute_day_of_year
91
+ AbsoluteDayOfYearTransitionRule.new(absolute_day_of_year.to_i, transition_at)
92
+ elsif week == '5'
93
+ LastDayOfMonthTransitionRule.new(month.to_i, day_of_week.to_i, transition_at)
94
+ else
95
+ DayOfMonthTransitionRule.new(month.to_i, week.to_i, day_of_week.to_i, transition_at)
96
+ end
97
+ rescue ArgumentError => e
98
+ raise InvalidPosixTimeZone, "Invalid #{type} rule in POSIX-style time zone string: #{e}"
99
+ end
100
+ end
101
+
102
+ # Returns an offset in seconds from hh:mm:ss values. The value can be
103
+ # negative. -02:33:12 would represent 2 hours, 33 minutes and 12 seconds
104
+ # ahead of UTC.
105
+ def get_offset_from_hms(h, m, s)
106
+ h = h.to_i
107
+ m = m.to_i
108
+ s = s.to_i
109
+ raise InvalidPosixTimeZone, "Invalid minute #{m} in offset for POSIX-style time zone string." if m > 59
110
+ raise InvalidPosixTimeZone, "Invalid second #{s} in offset for POSIX-style time zone string." if s > 59
111
+ magnitude = (h.abs * 60 + m) * 60 + s
112
+ h < 0 ? -magnitude : magnitude
113
+ end
114
+
115
+ # Returns the seconds from midnight from hh:mm:ss values. Hours can exceed
116
+ # 24 for a time on the following day. Hours can be negative to subtract
117
+ # hours from midnight on the given day. -02:33:12 represents 22:33:12 on
118
+ # the prior day.
119
+ def get_seconds_after_midnight_from_hms(h, m, s)
120
+ h = h.to_i
121
+ m = m.to_i
122
+ s = s.to_i
123
+ raise InvalidPosixTimeZone, "Invalid minute #{m} in time for POSIX-style time zone string." if m > 59
124
+ raise InvalidPosixTimeZone, "Invalid second #{s} in time for POSIX-style time zone string." if s > 59
125
+ (h * 3600) + m * 60 + s
126
+ end
127
+
128
+ # Scans for a pattern and raises an exception if the pattern does not
129
+ # match the input.
130
+ def check_scan(s, pattern)
131
+ result = s.scan(pattern)
132
+ raise InvalidPosixTimeZone, "Expected '#{s.rest}' to match #{pattern} in POSIX-style time zone string." unless result
133
+ result
134
+ end
135
+ end
136
+ end
@@ -137,10 +137,33 @@ module TZInfo
137
137
  def self.open_file(file_name, mode, opts, &block)
138
138
  File.open(file_name, mode, &block)
139
139
  end
140
- else
140
+ elsif RUBY_VERSION =~ /\A1\.9\./
141
141
  def self.open_file(file_name, mode, opts, &block)
142
142
  File.open(file_name, mode, opts, &block)
143
143
  end
144
+ else
145
+ # Evaluate method as a string because **opts isn't valid syntax prior to
146
+ # Ruby 2.0.
147
+ eval(<<-EOF
148
+ def self.open_file(file_name, mode, opts, &block)
149
+ File.open(file_name, mode, **opts, &block)
150
+ end
151
+ EOF
152
+ )
153
+ end
154
+
155
+
156
+ # Object#untaint is a deprecated no-op in Ruby >= 2.7 and will be removed in
157
+ # 3.2. Add a refinement to either silence the warning, or supply the method
158
+ # if needed.
159
+ if !Object.new.respond_to?(:untaint) || RUBY_VERSION =~ /\A(\d+)\.(\d+)(?:\.|\z)/ && ($1 == '2' && $2.to_i >= 7 || $1.to_i >= 3)
160
+ module UntaintExt
161
+ refine Object do
162
+ def untaint
163
+ self
164
+ end
165
+ end
166
+ end
144
167
  end
145
168
  end
146
169
  end
@@ -1,4 +1,8 @@
1
1
  module TZInfo
2
+ # Use send as a workaround for erroneous 'wrong number of arguments' errors
3
+ # with JRuby 9.0.5.0 when calling methods with Java implementations. See #114.
4
+ send(:using, RubyCoreSupport::UntaintExt) if RubyCoreSupport.const_defined?(:UntaintExt)
5
+
2
6
  # A DataSource that loads data from the set of Ruby modules included in the
3
7
  # TZInfo::Data library (tzinfo-data gem).
4
8
  #
@@ -6,15 +10,30 @@ module TZInfo
6
10
  #
7
11
  # TZInfo::DataSource.set(:ruby)
8
12
  class RubyDataSource < DataSource
9
- # Base path for require.
10
- REQUIRE_PATH = File.join('tzinfo', 'data', 'definitions')
11
-
12
13
  # Whether the timezone index has been loaded yet.
13
14
  @@timezone_index_loaded = false
14
15
 
15
16
  # Whether the country index has been loaded yet.
16
17
  @@country_index_loaded = false
17
18
 
19
+ # Initializes a new RubyDataSource instance.
20
+ def initialize
21
+ tzinfo_data = File.join('tzinfo', 'data')
22
+ begin
23
+ require(tzinfo_data)
24
+
25
+ data_file = File.join('', 'tzinfo', 'data.rb')
26
+ path = $".reverse_each.detect {|p| p.end_with?(data_file) }
27
+ if path
28
+ @base_path = File.join(File.dirname(path), 'data').untaint
29
+ else
30
+ @base_path = tzinfo_data
31
+ end
32
+ rescue LoadError
33
+ @base_path = tzinfo_data
34
+ end
35
+ end
36
+
18
37
  # Returns a TimezoneInfo instance for a given identifier.
19
38
  # Raises InvalidTimezoneIdentifier if the timezone is not found or the
20
39
  # identifier is invalid.
@@ -93,27 +112,17 @@ module TZInfo
93
112
  end
94
113
 
95
114
  # Requires an index by its name.
96
- def self.require_index(name)
115
+ def require_index(name)
97
116
  require_data(*['indexes', name])
98
117
  end
99
118
 
100
119
  # Requires a file from tzinfo/data.
101
120
  def require_data(*file)
102
- self.class.require_data(*file)
103
- end
104
-
105
- # Requires a file from tzinfo/data.
106
- def self.require_data(*file)
107
- require File.join('tzinfo', 'data', *file)
121
+ require(File.join(@base_path, *file))
108
122
  end
109
123
 
110
124
  # Loads in the index of timezones if it hasn't already been loaded.
111
125
  def load_timezone_index
112
- self.class.load_timezone_index
113
- end
114
-
115
- # Loads in the index of timezones if it hasn't already been loaded.
116
- def self.load_timezone_index
117
126
  unless @@timezone_index_loaded
118
127
  require_index('timezones')
119
128
  @@timezone_index_loaded = true
@@ -122,11 +131,6 @@ module TZInfo
122
131
 
123
132
  # Loads in the index of countries if it hasn't already been loaded.
124
133
  def load_country_index
125
- self.class.load_country_index
126
- end
127
-
128
- # Loads in the index of countries if it hasn't already been loaded.
129
- def self.load_country_index
130
134
  unless @@country_index_loaded
131
135
  require_index('countries')
132
136
  @@country_index_loaded = true
@@ -160,6 +160,17 @@ module TZInfo
160
160
  end
161
161
  end
162
162
  alias :day :mday
163
+
164
+ # Returns the day of the week (0..6 for Sunday to Saturday).
165
+ def wday
166
+ if @time
167
+ @time.wday
168
+ elsif @datetime
169
+ @datetime.wday
170
+ else
171
+ to_time.wday
172
+ end
173
+ end
163
174
 
164
175
  # Returns the hour of the day (0..23).
165
176
  def hour
@@ -571,17 +571,21 @@ module TZInfo
571
571
  # version 2.0.0, %z will be passed to Time#strftime and DateTime#strftime
572
572
  # instead. Some of the formatting options may cease to be available
573
573
  # depending on the version of Ruby in use (for example, %:::z is only
574
- # supported by Time#strftime from MRI version 2.0.0 onwards.)
574
+ # supported by Time#strftime from MRI version 2.0.0 onwards).
575
575
  def strftime(format, utc = Time.now.utc)
576
+ utc = TimeOrDateTime.wrap(utc)
576
577
  period = period_for_utc(utc)
577
- local = period.to_local(utc)
578
- local = Time.at(local).utc unless local.kind_of?(Time) || local.kind_of?(DateTime)
578
+ local_wrapped = period.to_local(utc)
579
+ local = local_wrapped.to_orig
580
+ local = local_wrapped.to_time unless local.kind_of?(Time) || local.kind_of?(DateTime)
579
581
  abbreviation = period.abbreviation.to_s.gsub(/%/, '%%')
580
582
 
581
- format = format.gsub(/%(%*)(Z|:*z)/) do
583
+ format = format.gsub(/%(%*)([sZ]|:*z)/) do
582
584
  if $1.length.odd?
583
585
  # Escaped literal percent or series of percents. Pass on to strftime.
584
586
  "#$1%#$2"
587
+ elsif $2 == "s"
588
+ "#$1#{utc.to_i}"
585
589
  elsif $2 == "Z"
586
590
  "#$1#{abbreviation}"
587
591
  else
@@ -600,8 +604,8 @@ module TZInfo
600
604
  # Passing the invalid format string through to Time#strftime or
601
605
  # DateTime#strtime would normally result in it being returned in the
602
606
  # result. However, with Ruby 1.8.7 on Windows (as tested with Ruby
603
- # 1.8.7-p374 from http://rubyinstaller.org/downloads/archives), this
604
- # causes Time#strftime to always return an empty string (e.g.
607
+ # 1.8.7-p374 from https://rubyinstaller.org/downloads/archives),
608
+ # this causes Time#strftime to always return an empty string (e.g.
605
609
  # Time.now.strftime('a %::::z b') returns '').
606
610
  #
607
611
  # Escape the percent to force it to be evaluated as a literal.