-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support "slim" zoneinfo files produced by default by zic >= 2020b.
Use the POSIX-style TZ string to calculate transitions after the last defined transition contained within the file. At the moment these transitions are generated when the file is loaded. In a later release this will likely be changed to calculate transitions on demand. Use the 64-bit section of zoneinfo files regardless of whether the runtime supports 64-bit Times. The 32-bit section is empty in "slim" zoneinfo files. Resolves #120.
- Loading branch information
Showing
12 changed files
with
2,768 additions
and
154 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
module TZInfo | ||
# A set of rules that define when transitions occur in time zones with | ||
# annually occurring daylight savings time. | ||
# | ||
# @private | ||
class AnnualRules #:nodoc: | ||
# Returned by #transitions. #offset is the TimezoneOffset that applies | ||
# from the UTC TimeOrDateTime #at. #previous_offset is the prior | ||
# TimezoneOffset. | ||
Transition = Struct.new(:offset, :previous_offset, :at) | ||
|
||
# The standard offset that applies when daylight savings time is not in | ||
# force. | ||
attr_reader :std_offset | ||
|
||
# The offset that applies when daylight savings time is in force. | ||
attr_reader :dst_offset | ||
|
||
# The rule that determines when daylight savings time starts. | ||
attr_reader :dst_start_rule | ||
|
||
# The rule that determines when daylight savings time ends. | ||
attr_reader :dst_end_rule | ||
|
||
# Initializes a new {AnnualRules} instance. | ||
def initialize(std_offset, dst_offset, dst_start_rule, dst_end_rule) | ||
@std_offset = std_offset | ||
@dst_offset = dst_offset | ||
@dst_start_rule = dst_start_rule | ||
@dst_end_rule = dst_end_rule | ||
end | ||
|
||
# Returns the transitions between standard and daylight savings time for a | ||
# given year. The results are ordered by time of occurrence (earliest to | ||
# latest). | ||
def transitions(year) | ||
start_dst = apply_rule(@dst_start_rule, @std_offset, @dst_offset, year) | ||
end_dst = apply_rule(@dst_end_rule, @dst_offset, @std_offset, year) | ||
|
||
end_dst.at < start_dst.at ? [end_dst, start_dst] : [start_dst, end_dst] | ||
end | ||
|
||
private | ||
|
||
# Applies a given rule between offsets on a year. | ||
def apply_rule(rule, from_offset, to_offset, year) | ||
at = rule.at(from_offset, year) | ||
Transition.new(to_offset, from_offset, at) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
# encoding: UTF-8 | ||
# frozen_string_literal: true | ||
|
||
require 'strscan' | ||
|
||
module TZInfo | ||
# An {InvalidPosixTimeZone} exception is raised if an invalid POSIX-style | ||
# time zone string is encountered. | ||
# | ||
# @private | ||
class InvalidPosixTimeZone < StandardError #:nodoc: | ||
end | ||
|
||
# A parser for POSIX-style TZ strings used in zoneinfo files and specified | ||
# by tzfile.5 and tzset.3. | ||
# | ||
# @private | ||
class PosixTimeZoneParser #:nodoc: | ||
# Parses a POSIX-style TZ string, returning either a TimezoneOffset or | ||
# an AnnualRules instance. | ||
def parse(tz_string) | ||
raise InvalidPosixTimeZone unless tz_string.kind_of?(String) | ||
return nil if tz_string.empty? | ||
|
||
s = StringScanner.new(tz_string) | ||
check_scan(s, /([^-+,\d<][^-+,\d]*) | <([^>]+)>/x) | ||
std_abbrev = s[1] || s[2] | ||
check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/) | ||
std_offset = get_offset_from_hms(s[1], s[2], s[3]) | ||
|
||
if s.scan(/([^-+,\d<][^-+,\d]*) | <([^>]+)>/x) | ||
dst_abbrev = s[1] || s[2] | ||
|
||
if s.scan(/([-+]?\d+)(?::(\d+)(?::(\d+))?)?/) | ||
dst_offset = get_offset_from_hms(s[1], s[2], s[3]) | ||
else | ||
# POSIX is negative for ahead of UTC. | ||
dst_offset = std_offset - 3600 | ||
end | ||
|
||
dst_difference = std_offset - dst_offset | ||
|
||
start_rule = parse_rule(s, 'start') | ||
end_rule = parse_rule(s, 'end') | ||
|
||
raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'." if s.rest? | ||
|
||
if start_rule.is_always_first_day_of_year? && start_rule.transition_at == 0 && | ||
end_rule.is_always_last_day_of_year? && end_rule.transition_at == 86400 + dst_difference | ||
# Constant daylight savings time. | ||
# POSIX is negative for ahead of UTC. | ||
TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev.to_sym) | ||
else | ||
AnnualRules.new( | ||
TimezoneOffset.new(-std_offset, 0, std_abbrev.to_sym), | ||
TimezoneOffset.new(-std_offset, dst_difference, dst_abbrev.to_sym), | ||
start_rule, | ||
end_rule) | ||
end | ||
elsif !s.rest? | ||
# Constant standard time. | ||
# POSIX is negative for ahead of UTC. | ||
TimezoneOffset.new(-std_offset, 0, std_abbrev.to_sym) | ||
else | ||
raise InvalidPosixTimeZone, "Expected the end of a POSIX-style time zone string but found '#{s.rest}'." | ||
end | ||
end | ||
|
||
private | ||
|
||
# Parses the rule from the TZ string, returning a TransitionRule. | ||
def parse_rule(s, type) | ||
check_scan(s, /,(?: (?: J(\d+) ) | (\d+) | (?: M(\d+)\.(\d)\.(\d) ) )/x) | ||
julian_day_of_year = s[1] | ||
absolute_day_of_year = s[2] | ||
month = s[3] | ||
week = s[4] | ||
day_of_week = s[5] | ||
|
||
if s.scan(/\//) | ||
check_scan(s, /([-+]?\d+)(?::(\d+)(?::(\d+))?)?/) | ||
transition_at = get_seconds_after_midnight_from_hms(s[1], s[2], s[3]) | ||
else | ||
transition_at = 7200 | ||
end | ||
|
||
begin | ||
if julian_day_of_year | ||
JulianDayOfYearTransitionRule.new(julian_day_of_year.to_i, transition_at) | ||
elsif absolute_day_of_year | ||
AbsoluteDayOfYearTransitionRule.new(absolute_day_of_year.to_i, transition_at) | ||
elsif week == '5' | ||
LastDayOfMonthTransitionRule.new(month.to_i, day_of_week.to_i, transition_at) | ||
else | ||
DayOfMonthTransitionRule.new(month.to_i, week.to_i, day_of_week.to_i, transition_at) | ||
end | ||
rescue ArgumentError => e | ||
raise InvalidPosixTimeZone, "Invalid #{type} rule in POSIX-style time zone string: #{e}" | ||
end | ||
end | ||
|
||
# Returns an offset in seconds from hh:mm:ss values. The value can be | ||
# negative. -02:33:12 would represent 2 hours, 33 minutes and 12 seconds | ||
# ahead of UTC. | ||
def get_offset_from_hms(h, m, s) | ||
h = h.to_i | ||
m = m.to_i | ||
s = s.to_i | ||
raise InvalidPosixTimeZone, "Invalid minute #{m} in offset for POSIX-style time zone string." if m > 59 | ||
raise InvalidPosixTimeZone, "Invalid second #{s} in offset for POSIX-style time zone string." if s > 59 | ||
magnitude = (h.abs * 60 + m) * 60 + s | ||
h < 0 ? -magnitude : magnitude | ||
end | ||
|
||
# Returns the seconds from midnight from hh:mm:ss values. Hours can exceed | ||
# 24 for a time on the following day. Hours can be negative to subtract | ||
# hours from midnight on the given day. -02:33:12 represents 22:33:12 on | ||
# the prior day. | ||
def get_seconds_after_midnight_from_hms(h, m, s) | ||
h = h.to_i | ||
m = m.to_i | ||
s = s.to_i | ||
raise InvalidPosixTimeZone, "Invalid minute #{m} in time for POSIX-style time zone string." if m > 59 | ||
raise InvalidPosixTimeZone, "Invalid second #{s} in time for POSIX-style time zone string." if s > 59 | ||
(h * 3600) + m * 60 + s | ||
end | ||
|
||
# Scans for a pattern and raises an exception if the pattern does not | ||
# match the input. | ||
def check_scan(s, pattern) | ||
result = s.scan(pattern) | ||
raise InvalidPosixTimeZone, "Expected '#{s.rest}' to match #{pattern} in POSIX-style time zone string." unless result | ||
result | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.