diff --git a/calendar/locales/en-US/chrome/calendar/timezones.properties b/calendar/locales/en-US/chrome/calendar/timezones.properties index 6f33e3a345..f0b29ff808 100644 --- a/calendar/locales/en-US/chrome/calendar/timezones.properties +++ b/calendar/locales/en-US/chrome/calendar/timezones.properties @@ -480,3 +480,6 @@ pref.timezone.Asia.Atyrau=Asia/Atyrau #added with 2.2017b pref.timezone.America.Punta_Arenas=America/Punta Arenas + +#added with 2.2018i +pref.timezone.Asia.Qostanay=Asia/Qostanay diff --git a/calendar/test/unit/test_timezone_changes.js b/calendar/test/unit/test_timezone_changes.js new file mode 100644 index 0000000000..a0179c89bf --- /dev/null +++ b/calendar/test/unit/test_timezone_changes.js @@ -0,0 +1,117 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const FEBRUARY = 1; +const OCTOBER = 9; +const NOVEMBER = 10; + +const UTC_MINUS_3 = -3 * 3600; +const UTC_MINUS_2 = -2 * 3600; + +function run_test() { + do_calendar_startup(run_next_test); +} + + +// This test requires timezone data going back to 2016. It's been kept here as an example. +/* add_test(function testCaracas() { + let time = cal.createDateTime(); + let zone = cal.getTimezoneService().getTimezone("America/Caracas"); + + for (let month = JANUARY; month <= DECEMBER; month++) { + time.resetTo(2015, month, 1, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_430, time.toString()); + } + + for (let month = JANUARY; month <= APRIL; month++) { + time.resetTo(2016, month, 1, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_430, time.toString()); + } + + time.resetTo(2016, MAY, 1, 1, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_430, time.toString()); + + time.resetTo(2016, MAY, 1, 3, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_4, time.toString()); + + for (let month = JUNE; month <= DECEMBER; month++) { + time.resetTo(2016, month, 1, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_4, time.toString()); + } + + for (let month = JANUARY; month <= DECEMBER; month++) { + time.resetTo(2017, month, 1, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_4, time.toString()); + } + + run_next_test(); +}); */ + +// Brazil's rules are complicated. This tests every change in the time range we have data for. +add_test(function testSaoPaulo() { + let time = cal.createDateTime(); + let zone = cal.getTimezoneService().getTimezone("America/Sao_Paulo"); + + time.resetTo(2018, FEBRUARY, 17, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + time.resetTo(2018, FEBRUARY, 18, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2018, NOVEMBER, 3, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2018, NOVEMBER, 4, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + time.resetTo(2019, FEBRUARY, 16, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + time.resetTo(2019, FEBRUARY, 17, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2019, NOVEMBER, 2, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2019, NOVEMBER, 3, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + time.resetTo(2020, FEBRUARY, 15, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + time.resetTo(2020, FEBRUARY, 16, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2020, OCTOBER, 31, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2020, NOVEMBER, 1, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + time.resetTo(2021, FEBRUARY, 20, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + time.resetTo(2021, FEBRUARY, 21, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2021, NOVEMBER, 6, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2021, NOVEMBER, 7, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + time.resetTo(2022, FEBRUARY, 19, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + time.resetTo(2022, FEBRUARY, 20, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2022, NOVEMBER, 5, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_3, time.toString()); + + time.resetTo(2022, NOVEMBER, 6, 0, 0, 0, zone); + equal(time.timezoneOffset, UTC_MINUS_2, time.toString()); + + run_next_test(); +}); diff --git a/calendar/test/unit/xpcshell-shared.ini b/calendar/test/unit/xpcshell-shared.ini index 5fb4dec926..58914af3c6 100644 --- a/calendar/test/unit/xpcshell-shared.ini +++ b/calendar/test/unit/xpcshell-shared.ini @@ -49,6 +49,7 @@ skip-if = true # See bug 1481180. requesttimeoutfactor = 2 [test_startup_service.js] [test_storage.js] [test_timezone.js] +[test_timezone_changes.js] [test_timezone_definition.js] [test_unifinder_utils.js] [test_utils.js] diff --git a/calendar/timezones/update-zones.py b/calendar/timezones/update-zones.py index 5f1a395310..c83560f0b8 100755 --- a/calendar/timezones/update-zones.py +++ b/calendar/timezones/update-zones.py @@ -15,15 +15,22 @@ Otherwise manual corrections will get dropped when pushing the update. """ +from __future__ import absolute_import + import argparse, ftplib, json, os, os.path, re, shutil, subprocess, sys, tarfile, tempfile from collections import OrderedDict +from datetime import date, timedelta + +# Keep timezone changes from this date onwards. If the zones.json file is becoming +# too large, consider changing to a later date. +HISTORY_CUTOFF = 20180101 +FUTURE_CUTOFF = 20221231 class TimezoneUpdater(object): """ Timezone updater class, use the run method to do everything automatically""" - def __init__(self, tzdata_path, zoneinfo_path, zoneinfo_pure_path): + def __init__(self, tzdata_path, zoneinfo_pure_path): self.tzdata_path = tzdata_path - self.zoneinfo_path = zoneinfo_path self.zoneinfo_pure_path = zoneinfo_pure_path def download_tzdata(self): @@ -55,14 +62,7 @@ class TimezoneUpdater(object): def run_vzic(self, vzic_path): """Use vzic to create ICS versions of the data.""" - # Use `vzic` to create 'pure' and 'non-pure' zone files. - sys.stderr.write("Exporting zone info to %s\n" % self.zoneinfo_path) - subprocess.check_call([ - vzic_path, - "--olson-dir", self.tzdata_path, - "--output-dir", self.zoneinfo_path - ], stdout=sys.stderr) - + # Use `vzic` to create zone files. sys.stderr.write("Exporting pure zone info to %s\n" % self.zoneinfo_pure_path) subprocess.check_call([ vzic_path, @@ -85,7 +85,7 @@ class TimezoneUpdater(object): def read_zones_tab(self): """Read zones.tab for latitude and longitude data.""" lat_long_data = {} - with open(os.path.join(self.zoneinfo_path, "zones.tab"), "r") as tab: + with open(os.path.join(self.zoneinfo_pure_path, "zones.tab"), "r") as tab: for line in tab: if len(line) < 19: sys.stderr.write("Line in zones.tab not long enough: %s\n" % line.strip()) @@ -97,40 +97,137 @@ class TimezoneUpdater(object): return lat_long_data def read_ics(self, filename, lat_long_data): - """Read a single zone's ICS files. - - We keep only the lines we want, and we use the pure version of RRULE if - the versions differ. See Asia/Jerusalem for an example.""" - with open(os.path.join(self.zoneinfo_path, filename), "r") as zone: - zoneinfo = zone.readlines() - + """Read a single zone's ICS files.""" with open(os.path.join(self.zoneinfo_pure_path, filename), "r") as zone: zoneinfo_pure = zone.readlines() - ics_data = [] - for i in range(0, len(zoneinfo)): - line = zoneinfo[i] - key = line[:line.find(":")] + # Loop through the lines of the file, splitting it into components. + components = [] + current_component = None + for i in range(0, len(zoneinfo_pure)): + line = zoneinfo_pure[i].rstrip() + [key, value] = line.split(":", 1) - if key == "BEGIN": - if line != "BEGIN:VCALENDAR\r\n": - ics_data.append(line) - elif key == "END": - if line != "END:VCALENDAR\r\n": - ics_data.append(line) - elif key in ("TZID", "TZOFFSETFROM", "TZOFFSETTO", "TZNAME", "DTSTART"): - ics_data.append(line) - elif key == "RRULE": - if line == zoneinfo_pure[i]: - ics_data.append(line) + if line in ["BEGIN:STANDARD", "BEGIN:DAYLIGHT"]: + current_component = {"line": i, "type": value} + + elif line in ["END:STANDARD", "END:DAYLIGHT"]: + components.append(current_component) + current_component = None + + elif current_component: + if key == "RDATE": + if "rdates" not in current_component: + current_component["rdates"] = [] + current_component["rdates"].append(value) else: - sys.stderr.write("Using pure version of %s\n" % filename[:-4]) - ics_data.append(zoneinfo_pure[i]) + current_component[key] = value + + # Create a copy of each component for every date that it started. + # Later, we'll sort them into order of starting date. + components_by_start_date = {} + for component in components: + max_rdate = int(component["DTSTART"][0:8]) + components_by_start_date[max_rdate] = component + if "rdates" in component: + for rdate in component["rdates"]: + rdate = int(rdate[0:8]) + max_rdate = max(rdate, max_rdate) + components_by_start_date[rdate] = component + component["valid_rdates"] = filter( + lambda rd: FUTURE_CUTOFF >= int(rd[0:8]) >= HISTORY_CUTOFF, + component["rdates"] + ) + component["max_date"] = max_rdate + + # Sort, and keep only the components in use since the cutoff date. + kept_components = [] + finished = False + for key in sorted(components_by_start_date.keys(), reverse=True): + if key > FUTURE_CUTOFF: + continue + component = components_by_start_date[key] + if finished and "RRULE" not in component: + continue + if "used" in component: + continue + component["used"] = True + kept_components.append(component) + if key <= HISTORY_CUTOFF: + finished = True + + for i in range(len(kept_components)): + component = kept_components[i] + last = i == len(kept_components) - 1 + # In this block of code, we attempt to match what vzic does when + # creating "Outlook-compatible" timezone files. This is to minimise + # changes in our zones.json file. And to be more Outlook-compatible. + if int(component["DTSTART"][0:8]) < HISTORY_CUTOFF: + if not last and "valid_rdates" in component and len(component["valid_rdates"]) > 0: + component["DTSTART"] = component["valid_rdates"][0] + continue + + # Change the start date to what it would've been in 1970. + start_date = "19700101" + start_time = "T000000" + + if "RRULE" in component: + rrule = dict(part.split("=") for part in component["RRULE"].split(";")) + bymonth = int(rrule["BYMONTH"]) + weekday = rrule["BYDAY"].lstrip("-012345") + weekday_index = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"].index(weekday) + + if "BYMONTHDAY" in rrule: + bymonthday = list(int(d) for d in rrule["BYMONTHDAY"].split(",")) + for day in bymonthday: + test_day = date(1970, bymonth, day) + if test_day.weekday() == weekday_index: + start_date = test_day.strftime("%Y%m%d") + start_time = component["DTSTART"][8:] + break + elif "BYDAY" in rrule: + which_weekday = int(rrule["BYDAY"].rstrip("AEFHMORSTUW")) + days_matching = [0] + test_day = date(1970, bymonth, 1) + while test_day.month == bymonth: + if test_day.weekday() == weekday_index: + days_matching.append(test_day) + test_day = test_day + timedelta(days=1) + start_date = days_matching[which_weekday].strftime("%Y%m%d") + start_time = component["DTSTART"][8:] + + component["DTSTART"] = start_date + start_time + + # Sort the components back into the order they appeared in the original file. + # This is to minimise changes in our zones.json file. + kept_components.sort(key=lambda b: b["line"]) + + zone_name = filename[:-4] + ics = [] + for component in kept_components: + ics_lines = [] + ics_lines.append("BEGIN:%s" % component["type"]) + if len(kept_components) == 1 or len(component["TZOFFSETFROM"]) != 5: + ics_lines.append("TZOFFSETFROM:%s" % component["TZOFFSETTO"]) + else: + ics_lines.append("TZOFFSETFROM:%s" % component["TZOFFSETFROM"]) + ics_lines.append("TZOFFSETTO:%s" % component["TZOFFSETTO"]) + + if "TZNAME" in component: + ics_lines.append("TZNAME:%s" % component["TZNAME"]) + ics_lines.append("DTSTART:%s" % component["DTSTART"]) + if "RRULE" in component: + ics_lines.append("RRULE:%s" % component["RRULE"]) + elif len(kept_components) > 1 and "valid_rdates" in component: + for rdate in component["valid_rdates"]: + ics_lines.append("RDATE:%s" % rdate) + + ics_lines.append("END:%s" % component["type"]) + ics.append("\r\n".join(ics_lines)) zone_data = { - "ics": "".join(ics_data).rstrip() + "ics": ics, } - zone_name = filename[:-4] if zone_name in lat_long_data: zone_data["latitude"] = lat_long_data[zone_name][0] zone_data["longitude"] = lat_long_data[zone_name][1] @@ -144,6 +241,8 @@ class TimezoneUpdater(object): zones = {} for entry in os.listdir(path): + if entry == "Etc": + continue fullpath = os.path.join(path, entry) if os.path.isdir(fullpath): zones.update(self.read_dir(fullpath, process_zone, os.path.join(prefix, entry))) @@ -219,7 +318,7 @@ class TimezoneUpdater(object): self.run_vzic(vzic_path) lat_long_data = self.read_zones_tab() - newzones = self.read_dir(self.zoneinfo_path, + newzones = self.read_dir(self.zoneinfo_pure_path, lambda fn: self.read_ics(fn, lat_long_data)) newaliases = self.link_removed_zones(zonesjson["zones"], newzones, links) @@ -307,21 +406,13 @@ def main(): # A test data update must occur before the zones.json file gets updated to have meaningful data create_test_data(json_file) - zoneinfo_path = tempfile.mkdtemp(prefix="zones") zoneinfo_pure_path = tempfile.mkdtemp(prefix="zones") - updater = TimezoneUpdater(args.tzdata_path, zoneinfo_path, zoneinfo_pure_path) + updater = TimezoneUpdater(args.tzdata_path, zoneinfo_pure_path) updater.run(json_file, tzprops_file, args.vzic_path) # Clean up. - shutil.rmtree(zoneinfo_path) shutil.rmtree(zoneinfo_pure_path) - print """ -When updating timezone definitions, please check the zones America/Campo_Grande, -America/Cuiaba, and America/Sao_Paulo are not reset to their previous buggy state. -See bug 1515937 for more information. -""" - if __name__ == "__main__": main() diff --git a/calendar/timezones/zones.json b/calendar/timezones/zones.json index f63fa61365..b8b8e0f25f 100644 --- a/calendar/timezones/zones.json +++ b/calendar/timezones/zones.json @@ -498,7 +498,12 @@ }, "Africa/Casablanca": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20180325T020000\r\nRDATE:20180325T020000\r\nRDATE:20180617T020000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20180513T030000\r\nRDATE:20180513T030000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20190609T020000\r\nRDATE:20190609T020000\r\nRDATE:20200524T020000\r\nRDATE:20210516T020000\r\nRDATE:20220508T020000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20181028T030000\r\nRDATE:20181028T030000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20190505T030000\r\nRDATE:20190505T030000\r\nRDATE:20200419T030000\r\nRDATE:20210411T030000\r\nRDATE:20220327T030000\r\nEND:DAYLIGHT" ], "latitude": "+0333900", "longitude": "-0073500" @@ -548,7 +553,12 @@ }, "Africa/El_Aaiun": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20180325T020000\r\nRDATE:20180325T020000\r\nRDATE:20180617T020000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20180513T030000\r\nRDATE:20180513T030000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20181028T030000\r\nRDATE:20181028T030000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20190505T030000\r\nRDATE:20190505T030000\r\nRDATE:20200419T030000\r\nRDATE:20210411T030000\r\nRDATE:20220327T030000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20190609T020000\r\nRDATE:20190609T020000\r\nRDATE:20200524T020000\r\nRDATE:20210516T020000\r\nRDATE:20220508T020000\r\nEND:STANDARD" ], "latitude": "+0270900", "longitude": "-0131200" @@ -744,7 +754,8 @@ }, "Africa/Sao_Tome": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:20180101T010000\r\nRDATE:20180101T010000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:20190101T020000\r\nRDATE:20190101T020000\r\nEND:STANDARD" ], "latitude": "+0002000", "longitude": "+0064400" @@ -988,8 +999,10 @@ }, "America/Campo_Grande": { "ics": [ - "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19701101T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:DAYLIGHT", - "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700215T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=2;BYDAY=3SU\r\nEND:STANDARD" + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20181104T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20200216T000000\r\nRDATE:20200216T000000\r\nRDATE:20210221T000000\r\nRDATE:20220220T000000\r\nEND:STANDARD" ], "latitude": "-0202700", "longitude": "-0543700" @@ -1054,8 +1067,10 @@ }, "America/Cuiaba": { "ics": [ - "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19701101T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:DAYLIGHT", - "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700215T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=2;BYDAY=3SU\r\nEND:STANDARD" + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20181104T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20200216T000000\r\nRDATE:20200216T000000\r\nRDATE:20210221T000000\r\nRDATE:20220220T000000\r\nEND:STANDARD" ], "latitude": "-0153500", "longitude": "-0560500" @@ -1174,8 +1189,10 @@ }, "America/Grand_Turk": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", - "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT" + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:20181104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:20190310T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:20180311T020000\r\nRDATE:20180311T020000\r\nEND:DAYLIGHT" ], "latitude": "+0212800", "longitude": "-0710800" @@ -1454,8 +1471,12 @@ }, "America/Metlakatla": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", - "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT" + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:20191103T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:20200308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:20181104T020000\r\nRDATE:20181104T020000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:20180311T020000\r\nRDATE:20180311T020000\r\nEND:DAYLIGHT", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:20190310T020000\r\nRDATE:20190310T020000\r\nEND:DAYLIGHT" ], "latitude": "+0550737", "longitude": "-1313435" @@ -1696,8 +1717,11 @@ }, "America/Santiago": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700405T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYMONTHDAY=2,3,4,5,6,7,8;BYDAY=SU\r\nEND:STANDARD", - "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700906T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYMONTHDAY=2,3,4,5,6,7,8;BYDAY=SU\r\nEND:DAYLIGHT" + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20190407T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYMONTHDAY=2,3,4,5,6,7,8;BYDAY=SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20190908T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYMONTHDAY=2,3,4,5,6,7,8;BYDAY=SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20180812T000000\r\nRDATE:20180812T000000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180513T000000\r\nRDATE:20180513T000000\r\nEND:STANDARD" ], "latitude": "-0332700", "longitude": "-0704000" @@ -1711,8 +1735,10 @@ }, "America/Sao_Paulo": { "ics": [ - "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19701101T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:DAYLIGHT", - "BEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700215T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=2;BYDAY=3SU\r\nEND:STANDARD" + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:20181104T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700101T000000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20200216T000000\r\nRDATE:20200216T000000\r\nRDATE:20210221T000000\r\nRDATE:20220220T000000\r\nEND:STANDARD" ], "latitude": "-0233200", "longitude": "-0463700" @@ -1871,7 +1897,8 @@ }, "Antarctica/Casey": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:20180311T040000\r\nRDATE:20180311T040000\r\nEND:STANDARD" ], "latitude": "-0661700", "longitude": "+1103100" @@ -2130,7 +2157,7 @@ "Asia/Famagusta": { "ics": [ "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD", - "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT" + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20180325T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT" ], "latitude": "+0350700", "longitude": "+0335700" @@ -2365,7 +2392,8 @@ }, "Asia/Pyongyang": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:KST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0830\r\nTZNAME:KST\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0830\r\nTZOFFSETTO:+0900\r\nTZNAME:KST\r\nDTSTART:20180504T233000\r\nRDATE:20180504T233000\r\nEND:STANDARD" ], "latitude": "+0390100", "longitude": "+1254500" @@ -2386,7 +2414,8 @@ }, "Asia/Qyzylorda": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:20181221T000000\r\nRDATE:20181221T000000\r\nEND:STANDARD" ], "latitude": "+0444800", "longitude": "+0652800" @@ -2463,7 +2492,9 @@ }, "Asia/Tehran": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:+0330\r\nTZOFFSETTO:+0330\r\nTZNAME:+0330\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0330\r\nTZNAME:+0330\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0330\r\nTZOFFSETTO:+0430\r\nTZNAME:+0430\r\nDTSTART:20180321T235959\r\nRDATE:20180321T235959\r\nRDATE:20190321T235959\r\nRDATE:20200320T235959\r\nRDATE:20210321T235959\r\nRDATE:20220321T235959\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0430\r\nTZOFFSETTO:+0330\r\nTZNAME:+0330\r\nDTSTART:20180921T235959\r\nRDATE:20180921T235959\r\nRDATE:20190921T235959\r\nRDATE:20200920T235959\r\nRDATE:20210921T235959\r\nRDATE:20220921T235959\r\nEND:STANDARD" ], "latitude": "+0354000", "longitude": "+0512600" @@ -3158,7 +3189,8 @@ }, "Europe/Volgograd": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:20181028T020000\r\nRDATE:20181028T020000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" ], "latitude": "+0484400", "longitude": "+0442500" @@ -3312,8 +3344,11 @@ }, "Pacific/Easter": { "ics": [ - "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:19700404T220000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SA\r\nEND:STANDARD", - "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700905T220000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=1SA\r\nEND:DAYLIGHT" + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:20190406T220000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SA\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:20190907T220000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=1SA\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:20180811T220000\r\nRDATE:20180811T220000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:20180512T220000\r\nRDATE:20180512T220000\r\nEND:STANDARD" ], "latitude": "-0270900", "longitude": "-1092600"