From 1324ce8c03b01e3b0e4e6bd61bdde02d6ba75f82 Mon Sep 17 00:00:00 2001 From: dadachi Date: Thu, 15 Oct 2026 17:03:33 +0900 Subject: [PATCH] Match the time zone list to Rails The list lacked Asuncion (added in Rails 7.1) and labelled five zones with outdated offsets: Greenland (-02:00 since 2023), Almaty and Astana (+05:00 since 2024), and Dublin and Casablanca (+00:00 in Rails). The offsets also drive the device default, so America/Nuuk picked Mid-Atlantic instead of Greenland and America/Asuncion picked Brasilia. The list is now ActiveSupport::TimeZone.all from nativeapptemplateapi (activesupport 8.1.4), and TimeZoneDataTest compares it with that output. Dublin and Casablanca now match the system's standard offset, so the comment in currentTimeZone no longer cites them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YDK5huvuRECyVrsQEdTN9q --- NativeAppTemplate/TimeZoneData.swift | 11 +- NativeAppTemplate/Utilities/Utility.swift | 3 +- .../Utilities/TimeZoneDataTest.swift | 195 ++++++++++++++++++ 3 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 NativeAppTemplateTests/Utilities/TimeZoneDataTest.swift diff --git a/NativeAppTemplate/TimeZoneData.swift b/NativeAppTemplate/TimeZoneData.swift index 423aaaa..31af4b2 100644 --- a/NativeAppTemplate/TimeZoneData.swift +++ b/NativeAppTemplate/TimeZoneData.swift @@ -35,13 +35,16 @@ let timeZones: [(key: String, value: String)] = [ ("Puerto Rico", "(GMT-04:00) Puerto Rico"), ("Santiago", "(GMT-04:00) Santiago"), ("Newfoundland", "(GMT-03:30) Newfoundland"), + ("Asuncion", "(GMT-03:00) Asuncion"), ("Brasilia", "(GMT-03:00) Brasilia"), ("Buenos Aires", "(GMT-03:00) Buenos Aires"), - ("Greenland", "(GMT-03:00) Greenland"), ("Montevideo", "(GMT-03:00) Montevideo"), + ("Greenland", "(GMT-02:00) Greenland"), ("Mid-Atlantic", "(GMT-02:00) Mid-Atlantic"), ("Azores", "(GMT-01:00) Azores"), ("Cape Verde Is.", "(GMT-01:00) Cape Verde Is."), + ("Casablanca", "(GMT+00:00) Casablanca"), + ("Dublin", "(GMT+00:00) Dublin"), ("Edinburgh", "(GMT+00:00) Edinburgh"), ("Lisbon", "(GMT+00:00) Lisbon"), ("London", "(GMT+00:00) London"), @@ -54,9 +57,7 @@ let timeZones: [(key: String, value: String)] = [ ("Bratislava", "(GMT+01:00) Bratislava"), ("Brussels", "(GMT+01:00) Brussels"), ("Budapest", "(GMT+01:00) Budapest"), - ("Casablanca", "(GMT+01:00) Casablanca"), ("Copenhagen", "(GMT+01:00) Copenhagen"), - ("Dublin", "(GMT+01:00) Dublin"), ("Ljubljana", "(GMT+01:00) Ljubljana"), ("Madrid", "(GMT+01:00) Madrid"), ("Paris", "(GMT+01:00) Paris"), @@ -100,6 +101,8 @@ let timeZones: [(key: String, value: String)] = [ ("Tbilisi", "(GMT+04:00) Tbilisi"), ("Yerevan", "(GMT+04:00) Yerevan"), ("Kabul", "(GMT+04:30) Kabul"), + ("Almaty", "(GMT+05:00) Almaty"), + ("Astana", "(GMT+05:00) Astana"), ("Ekaterinburg", "(GMT+05:00) Ekaterinburg"), ("Islamabad", "(GMT+05:00) Islamabad"), ("Karachi", "(GMT+05:00) Karachi"), @@ -110,8 +113,6 @@ let timeZones: [(key: String, value: String)] = [ ("New Delhi", "(GMT+05:30) New Delhi"), ("Sri Jayawardenepura", "(GMT+05:30) Sri Jayawardenepura"), ("Kathmandu", "(GMT+05:45) Kathmandu"), - ("Almaty", "(GMT+06:00) Almaty"), - ("Astana", "(GMT+06:00) Astana"), ("Dhaka", "(GMT+06:00) Dhaka"), ("Urumqi", "(GMT+06:00) Urumqi"), ("Rangoon", "(GMT+06:30) Rangoon"), diff --git a/NativeAppTemplate/Utilities/Utility.swift b/NativeAppTemplate/Utilities/Utility.swift index 61f7fbb..dbbcc72 100644 --- a/NativeAppTemplate/Utilities/Utility.swift +++ b/NativeAppTemplate/Utilities/Utility.swift @@ -26,7 +26,8 @@ enum Utility { guard !city.isEmpty else { return firstZoneWithOffset.key } // The whole name, at any offset: the table's offset for a city can differ - // from the system's (Dublin, Casablanca), and the city still names the zone + // from the system's when tzdata changes it before Rails does, and the city + // still names the zone if let zone = timeZones.first(where: { $0.key == city }) { return zone.key } diff --git a/NativeAppTemplateTests/Utilities/TimeZoneDataTest.swift b/NativeAppTemplateTests/Utilities/TimeZoneDataTest.swift new file mode 100644 index 0000000..f157720 --- /dev/null +++ b/NativeAppTemplateTests/Utilities/TimeZoneDataTest.swift @@ -0,0 +1,195 @@ +// +// TimeZoneDataTest.swift +// NativeAppTemplate +// + +import Foundation +@testable import NativeAppTemplate +import Testing + +struct TimeZoneDataTest { + /// The zones the API accepts, in Rails order, from nativeapptemplateapi + /// (activesupport 8.1.4): + /// `bin/rails runner 'ActiveSupport::TimeZone.all.each { |z| puts "#{z.name}\t#{z}" }'` + /// Regenerate this list from that command when Rails is upgraded. + private static let railsTimeZones: [(key: String, value: String)] = [ + ("International Date Line West", "(GMT-12:00) International Date Line West"), + ("American Samoa", "(GMT-11:00) American Samoa"), + ("Midway Island", "(GMT-11:00) Midway Island"), + ("Hawaii", "(GMT-10:00) Hawaii"), + ("Alaska", "(GMT-09:00) Alaska"), + ("Pacific Time (US & Canada)", "(GMT-08:00) Pacific Time (US & Canada)"), + ("Tijuana", "(GMT-08:00) Tijuana"), + ("Arizona", "(GMT-07:00) Arizona"), + ("Mazatlan", "(GMT-07:00) Mazatlan"), + ("Mountain Time (US & Canada)", "(GMT-07:00) Mountain Time (US & Canada)"), + ("Central America", "(GMT-06:00) Central America"), + ("Central Time (US & Canada)", "(GMT-06:00) Central Time (US & Canada)"), + ("Chihuahua", "(GMT-06:00) Chihuahua"), + ("Guadalajara", "(GMT-06:00) Guadalajara"), + ("Mexico City", "(GMT-06:00) Mexico City"), + ("Monterrey", "(GMT-06:00) Monterrey"), + ("Saskatchewan", "(GMT-06:00) Saskatchewan"), + ("Bogota", "(GMT-05:00) Bogota"), + ("Eastern Time (US & Canada)", "(GMT-05:00) Eastern Time (US & Canada)"), + ("Indiana (East)", "(GMT-05:00) Indiana (East)"), + ("Lima", "(GMT-05:00) Lima"), + ("Quito", "(GMT-05:00) Quito"), + ("Atlantic Time (Canada)", "(GMT-04:00) Atlantic Time (Canada)"), + ("Caracas", "(GMT-04:00) Caracas"), + ("Georgetown", "(GMT-04:00) Georgetown"), + ("La Paz", "(GMT-04:00) La Paz"), + ("Puerto Rico", "(GMT-04:00) Puerto Rico"), + ("Santiago", "(GMT-04:00) Santiago"), + ("Newfoundland", "(GMT-03:30) Newfoundland"), + ("Asuncion", "(GMT-03:00) Asuncion"), + ("Brasilia", "(GMT-03:00) Brasilia"), + ("Buenos Aires", "(GMT-03:00) Buenos Aires"), + ("Montevideo", "(GMT-03:00) Montevideo"), + ("Greenland", "(GMT-02:00) Greenland"), + ("Mid-Atlantic", "(GMT-02:00) Mid-Atlantic"), + ("Azores", "(GMT-01:00) Azores"), + ("Cape Verde Is.", "(GMT-01:00) Cape Verde Is."), + ("Casablanca", "(GMT+00:00) Casablanca"), + ("Dublin", "(GMT+00:00) Dublin"), + ("Edinburgh", "(GMT+00:00) Edinburgh"), + ("Lisbon", "(GMT+00:00) Lisbon"), + ("London", "(GMT+00:00) London"), + ("Monrovia", "(GMT+00:00) Monrovia"), + ("UTC", "(GMT+00:00) UTC"), + ("Amsterdam", "(GMT+01:00) Amsterdam"), + ("Belgrade", "(GMT+01:00) Belgrade"), + ("Berlin", "(GMT+01:00) Berlin"), + ("Bern", "(GMT+01:00) Bern"), + ("Bratislava", "(GMT+01:00) Bratislava"), + ("Brussels", "(GMT+01:00) Brussels"), + ("Budapest", "(GMT+01:00) Budapest"), + ("Copenhagen", "(GMT+01:00) Copenhagen"), + ("Ljubljana", "(GMT+01:00) Ljubljana"), + ("Madrid", "(GMT+01:00) Madrid"), + ("Paris", "(GMT+01:00) Paris"), + ("Prague", "(GMT+01:00) Prague"), + ("Rome", "(GMT+01:00) Rome"), + ("Sarajevo", "(GMT+01:00) Sarajevo"), + ("Skopje", "(GMT+01:00) Skopje"), + ("Stockholm", "(GMT+01:00) Stockholm"), + ("Vienna", "(GMT+01:00) Vienna"), + ("Warsaw", "(GMT+01:00) Warsaw"), + ("West Central Africa", "(GMT+01:00) West Central Africa"), + ("Zagreb", "(GMT+01:00) Zagreb"), + ("Zurich", "(GMT+01:00) Zurich"), + ("Athens", "(GMT+02:00) Athens"), + ("Bucharest", "(GMT+02:00) Bucharest"), + ("Cairo", "(GMT+02:00) Cairo"), + ("Harare", "(GMT+02:00) Harare"), + ("Helsinki", "(GMT+02:00) Helsinki"), + ("Jerusalem", "(GMT+02:00) Jerusalem"), + ("Kaliningrad", "(GMT+02:00) Kaliningrad"), + ("Kyiv", "(GMT+02:00) Kyiv"), + ("Pretoria", "(GMT+02:00) Pretoria"), + ("Riga", "(GMT+02:00) Riga"), + ("Sofia", "(GMT+02:00) Sofia"), + ("Tallinn", "(GMT+02:00) Tallinn"), + ("Vilnius", "(GMT+02:00) Vilnius"), + ("Baghdad", "(GMT+03:00) Baghdad"), + ("Istanbul", "(GMT+03:00) Istanbul"), + ("Kuwait", "(GMT+03:00) Kuwait"), + ("Minsk", "(GMT+03:00) Minsk"), + ("Moscow", "(GMT+03:00) Moscow"), + ("Nairobi", "(GMT+03:00) Nairobi"), + ("Riyadh", "(GMT+03:00) Riyadh"), + ("St. Petersburg", "(GMT+03:00) St. Petersburg"), + ("Volgograd", "(GMT+03:00) Volgograd"), + ("Tehran", "(GMT+03:30) Tehran"), + ("Abu Dhabi", "(GMT+04:00) Abu Dhabi"), + ("Baku", "(GMT+04:00) Baku"), + ("Muscat", "(GMT+04:00) Muscat"), + ("Samara", "(GMT+04:00) Samara"), + ("Tbilisi", "(GMT+04:00) Tbilisi"), + ("Yerevan", "(GMT+04:00) Yerevan"), + ("Kabul", "(GMT+04:30) Kabul"), + ("Almaty", "(GMT+05:00) Almaty"), + ("Astana", "(GMT+05:00) Astana"), + ("Ekaterinburg", "(GMT+05:00) Ekaterinburg"), + ("Islamabad", "(GMT+05:00) Islamabad"), + ("Karachi", "(GMT+05:00) Karachi"), + ("Tashkent", "(GMT+05:00) Tashkent"), + ("Chennai", "(GMT+05:30) Chennai"), + ("Kolkata", "(GMT+05:30) Kolkata"), + ("Mumbai", "(GMT+05:30) Mumbai"), + ("New Delhi", "(GMT+05:30) New Delhi"), + ("Sri Jayawardenepura", "(GMT+05:30) Sri Jayawardenepura"), + ("Kathmandu", "(GMT+05:45) Kathmandu"), + ("Dhaka", "(GMT+06:00) Dhaka"), + ("Urumqi", "(GMT+06:00) Urumqi"), + ("Rangoon", "(GMT+06:30) Rangoon"), + ("Bangkok", "(GMT+07:00) Bangkok"), + ("Hanoi", "(GMT+07:00) Hanoi"), + ("Jakarta", "(GMT+07:00) Jakarta"), + ("Krasnoyarsk", "(GMT+07:00) Krasnoyarsk"), + ("Novosibirsk", "(GMT+07:00) Novosibirsk"), + ("Beijing", "(GMT+08:00) Beijing"), + ("Chongqing", "(GMT+08:00) Chongqing"), + ("Hong Kong", "(GMT+08:00) Hong Kong"), + ("Irkutsk", "(GMT+08:00) Irkutsk"), + ("Kuala Lumpur", "(GMT+08:00) Kuala Lumpur"), + ("Perth", "(GMT+08:00) Perth"), + ("Singapore", "(GMT+08:00) Singapore"), + ("Taipei", "(GMT+08:00) Taipei"), + ("Ulaanbaatar", "(GMT+08:00) Ulaanbaatar"), + ("Osaka", "(GMT+09:00) Osaka"), + ("Sapporo", "(GMT+09:00) Sapporo"), + ("Seoul", "(GMT+09:00) Seoul"), + ("Tokyo", "(GMT+09:00) Tokyo"), + ("Yakutsk", "(GMT+09:00) Yakutsk"), + ("Adelaide", "(GMT+09:30) Adelaide"), + ("Darwin", "(GMT+09:30) Darwin"), + ("Brisbane", "(GMT+10:00) Brisbane"), + ("Canberra", "(GMT+10:00) Canberra"), + ("Guam", "(GMT+10:00) Guam"), + ("Hobart", "(GMT+10:00) Hobart"), + ("Melbourne", "(GMT+10:00) Melbourne"), + ("Port Moresby", "(GMT+10:00) Port Moresby"), + ("Sydney", "(GMT+10:00) Sydney"), + ("Vladivostok", "(GMT+10:00) Vladivostok"), + ("Magadan", "(GMT+11:00) Magadan"), + ("New Caledonia", "(GMT+11:00) New Caledonia"), + ("Solomon Is.", "(GMT+11:00) Solomon Is."), + ("Srednekolymsk", "(GMT+11:00) Srednekolymsk"), + ("Auckland", "(GMT+12:00) Auckland"), + ("Fiji", "(GMT+12:00) Fiji"), + ("Kamchatka", "(GMT+12:00) Kamchatka"), + ("Marshall Is.", "(GMT+12:00) Marshall Is."), + ("Wellington", "(GMT+12:00) Wellington"), + ("Chatham Is.", "(GMT+12:45) Chatham Is."), + ("Nuku'alofa", "(GMT+13:00) Nuku'alofa"), + ("Samoa", "(GMT+13:00) Samoa"), + ("Tokelau Is.", "(GMT+13:00) Tokelau Is.") + ] + + @Test + func keysMatchRails() { + #expect(timeZones.map(\.key) == Self.railsTimeZones.map(\.key)) + } + + @Test + func labelsMatchRails() { + #expect(timeZones.map(\.value) == Self.railsTimeZones.map(\.value)) + } + + /// 2026-01-15 12:00 UTC + private static let winter = Date(timeIntervalSince1970: 1_768_478_400) + + /// Rails maps "Asuncion" to America/Asuncion and "Greenland" to + /// America/Godthab (now America/Nuuk). Nuuk's city is not a zone name, so + /// it is matched by its standard offset, -02:00 since 2023, which Rails + /// lists first under "Greenland". + @Test(arguments: [ + ("America/Asuncion", "Asuncion"), + ("America/Nuuk", "Greenland") + ]) + func currentTimeZonePicksRailsZone(identifier: String, expected: String) throws { + let timeZone = try #require(TimeZone(identifier: identifier)) + #expect(Utility.currentTimeZone(timeZone, date: Self.winter) == expected) + } +}