Skip to content

Commit 7487b31

Browse files
authored
Fix IANA time zone names for Windows system time zones (#15914)
1 parent 33f016f commit 7487b31

File tree

5 files changed

+653
-483
lines changed

5 files changed

+653
-483
lines changed

scripts/generate_windows_zone_names.cr

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,28 @@ entries = nodes.flat_map do |node|
2525
end
2626
end.sort!
2727

28-
iana_to_windows_items = entries.map do |tzdata_name, territory, windows_name|
29-
{tzdata_name, windows_name}
28+
ENV["ZONEINFO"] = ZONEINFO_ZIP
29+
iana_to_windows_items = entries.compact_map do |tzdata_name, territory, windows_name|
30+
location = Time::Location.load(tzdata_name)
31+
next unless location
32+
33+
time = Time.local(location).at_beginning_of_year
34+
zone1 = time.zone
35+
zone2 = (time + 6.months).zone
36+
37+
# southern hemisphere
38+
if zone1.offset > zone2.offset
39+
zone1, zone2 = zone2, zone1
40+
end
41+
42+
{tzdata_name, windows_name, zone1.name, zone2.name}
3043
end.uniq!
3144

32-
ENV["ZONEINFO"] = ZONEINFO_ZIP
45+
windows_to_iana_items = entries.compact_map do |tzdata_name, territory, windows_name|
46+
{windows_name, tzdata_name} if territory == "001"
47+
end.uniq!
48+
49+
# TODO: remove in 1.17
3350
windows_zone_names_items = entries.compact_map do |tzdata_name, territory, windows_name|
3451
next unless territory == "001"
3552
location = Time::Location.load(tzdata_name)

scripts/windows_zone_names.ecr

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,31 @@
55
# DO NOT EDIT
66

77
module Crystal::System::Time
8-
# These mappings from IANA to Windows time zone names are based on
8+
# These mappings from IANA to Windows time zone names and tzdata abbreviations
9+
# are based on
910
# <%= WINDOWS_ZONE_NAMES_SOURCE %>
10-
private class_getter iana_to_windows : Hash(String, String) do
11-
data = Hash(String, String).new(initial_capacity: <%= iana_to_windows_items.size %>)
12-
<%- iana_to_windows_items.each do |tzdata_name, windows_name| -%>
13-
put(data, <%= tzdata_name.inspect %>, <%= windows_name.inspect %>)
11+
private class_getter iana_to_windows : Hash(String, {String, String, String}) do
12+
data = Hash(String, {String, String, String}).new(initial_capacity: <%= iana_to_windows_items.size %>)
13+
<%- iana_to_windows_items.each do |tzdata_name, windows_name, zone1, zone2| -%>
14+
put(data, <%= tzdata_name.inspect %>, <%= windows_name.inspect %>, <%= zone1.inspect %>, <%= zone2.inspect %>)
15+
<%- end -%>
16+
data
17+
end
18+
19+
# These canonical mappings from Windows to IANA time zone names, used for the
20+
# local time zone, are based on
21+
# <%= WINDOWS_ZONE_NAMES_SOURCE %>
22+
private class_getter windows_to_iana : Hash(String, String) do
23+
data = Hash(String, String).new(initial_capacity: <%= windows_to_iana_items.size %>)
24+
<%- windows_to_iana_items.each do |windows_name, tzdata_name| -%>
25+
put(data, <%= windows_name.inspect %>, <%= tzdata_name.inspect %>)
1426
<%- end -%>
1527
data
1628
end
1729

1830
# These mappings from Windows time zone names to tzdata abbreviations are based on
1931
# <%= WINDOWS_ZONE_NAMES_SOURCE %>
32+
# TODO: remove in 1.17
2033
private class_getter windows_zone_names : Hash(String, {String, String}) do
2134
data = Hash(String, {String, String}).new(initial_capacity: <%= windows_zone_names_items.size %>)
2235
<%- windows_zone_names_items.each do |windows_name, zone1, zone2, tzdata_name| -%>

spec/std/time/location_spec.cr

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ class Time::Location
6767
location.utc?.should be_false
6868
location.fixed?.should be_false
6969
end
70+
71+
it "uses IANA zone names for Windows system time zones (#15911)" do
72+
location = Location.load("Europe/Lisbon")
73+
std_zone = location.zones.find(&.dst?.!).should_not be_nil
74+
dst_zone = location.zones.find(&.dst?).should_not be_nil
75+
std_zone.name.should eq("WET")
76+
dst_zone.name.should eq("WEST")
77+
end
7078
{% end %}
7179

7280
it "invalid timezone identifier" do

src/crystal/system/win32/time.cr

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ module Crystal::System::Time
7272
def self.load_localtime : ::Time::Location?
7373
if LibC.GetDynamicTimeZoneInformation(out info) != LibC::TIME_ZONE_ID_INVALID
7474
windows_name = String.from_utf16(info.timeZoneKeyName.to_slice, truncate_at_null: true)
75-
initialize_location_from_TZI(pointerof(info).as(LibC::TIME_ZONE_INFORMATION*).value, "Local", windows_name)
75+
76+
return unless canonical_iana_name = windows_to_iana[windows_name]?
77+
return unless windows_info = iana_to_windows[canonical_iana_name]?
78+
_, stdname, dstname = windows_info
79+
80+
initialize_location_from_TZI(pointerof(info).as(LibC::TIME_ZONE_INFORMATION*).value, "Local", windows_name, stdname, dstname)
7681
end
7782
end
7883

@@ -90,7 +95,8 @@ module Crystal::System::Time
9095
daylightDate : LibC::SYSTEMTIME
9196

9297
def self.load_iana_zone(iana_name : String) : ::Time::Location?
93-
return unless windows_name = iana_to_windows[iana_name]?
98+
return unless windows_info = iana_to_windows[iana_name]?
99+
windows_name, stdname, dstname = windows_info
94100

95101
WindowsRegistry.open?(LibC::HKEY_LOCAL_MACHINE, REGISTRY_TIME_ZONES) do |key_handle|
96102
WindowsRegistry.open?(key_handle, windows_name.to_utf16) do |sub_handle|
@@ -106,14 +112,12 @@ module Crystal::System::Time
106112
)
107113
WindowsRegistry.get_raw(sub_handle, Std, tzi.standardName.to_slice.to_unsafe_bytes)
108114
WindowsRegistry.get_raw(sub_handle, Dlt, tzi.daylightName.to_slice.to_unsafe_bytes)
109-
initialize_location_from_TZI(tzi, iana_name, windows_name)
115+
initialize_location_from_TZI(tzi, iana_name, windows_name, stdname, dstname)
110116
end
111117
end
112118
end
113119

114-
private def self.initialize_location_from_TZI(info, name, windows_name)
115-
stdname, dstname = normalize_zone_names(info)
116-
120+
private def self.initialize_location_from_TZI(info, name, windows_name, stdname, dstname)
117121
if info.standardDate.wMonth == 0_u16 || info.daylightDate.wMonth == 0_u16
118122
# No DST
119123
zone = ::Time::Location::Zone.new(stdname, info.bias * BIAS_TO_OFFSET_FACTOR, false)
@@ -140,27 +144,6 @@ module Crystal::System::Time
140144
::Time::TZ::MonthWeekDay.new(time.wMonth.to_i8, time.wDay.to_i8, time.wDayOfWeek.to_i8, seconds)
141145
end
142146

143-
# Normalizes the names of the standard and dst zones.
144-
private def self.normalize_zone_names(info : LibC::TIME_ZONE_INFORMATION) : Tuple(String, String)
145-
stdname, _ = String.from_utf16(info.standardName.to_slice.to_unsafe)
146-
147-
if normalized_names = windows_zone_names[stdname]?
148-
return normalized_names
149-
end
150-
151-
dstname, _ = String.from_utf16(info.daylightName.to_slice.to_unsafe)
152-
153-
if english_name = translate_zone_name(stdname, dstname)
154-
if normalized_names = windows_zone_names[english_name]?
155-
return normalized_names
156-
end
157-
end
158-
159-
# As a last resort, return the raw names as provided by TIME_ZONE_INFORMATION.
160-
# They are most probably localized and we couldn't find a translation.
161-
return stdname, dstname
162-
end
163-
164147
REGISTRY_TIME_ZONES = System.wstr_literal %q(SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones)
165148
Std = System.wstr_literal "Std"
166149
Dlt = System.wstr_literal "Dlt"

0 commit comments

Comments
 (0)