diff --git a/Data/system/timezones.csv b/Data/system/timezones.csv index 699ac9542..1bbf681e5 100644 --- a/Data/system/timezones.csv +++ b/Data/system/timezones.csv @@ -11,14 +11,14 @@ "Africa/Brazzaville","WAT-1" "Africa/Bujumbura","CAT-2" "Africa/Cairo","EET-2EEST,M4.5.5/0,M10.5.4/24" -"Africa/Casablanca","<+01>-1" +"Africa/Casablanca","<+00>0" "Africa/Ceuta","CET-1CEST,M3.5.0,M10.5.0/3" "Africa/Conakry","GMT0" "Africa/Dakar","GMT0" "Africa/Dar_es_Salaam","EAT-3" "Africa/Djibouti","EAT-3" "Africa/Douala","WAT-1" -"Africa/El_Aaiun","<+01>-1" +"Africa/El_Aaiun","<+00>0" "Africa/Freetown","GMT0" "Africa/Gaborone","CAT-2" "Africa/Harare","CAT-2" @@ -68,7 +68,7 @@ "America/Argentina/Tucuman","<-03>3" "America/Argentina/Ushuaia","<-03>3" "America/Aruba","AST4" -"America/Asuncion","<-04>4<-03>,M10.1.0/0,M3.4.0/0" +"America/Asuncion","<-03>3" "America/Atikokan","EST5" "America/Bahia","<-03>3" "America/Bahia_Banderas","CST6" @@ -97,7 +97,7 @@ "America/Denver","MST7MDT,M3.2.0,M11.1.0" "America/Detroit","EST5EDT,M3.2.0,M11.1.0" "America/Dominica","AST4" -"America/Edmonton","MST7MDT,M3.2.0,M11.1.0" +"America/Edmonton","CST6" "America/Eirunepe","<-05>5" "America/El_Salvador","CST6" "America/Fortaleza","<-03>3" @@ -122,7 +122,7 @@ "America/Indiana/Vevay","EST5EDT,M3.2.0,M11.1.0" "America/Indiana/Vincennes","EST5EDT,M3.2.0,M11.1.0" "America/Indiana/Winamac","EST5EDT,M3.2.0,M11.1.0" -"America/Inuvik","MST7MDT,M3.2.0,M11.1.0" +"America/Inuvik","CST6" "America/Iqaluit","EST5EDT,M3.2.0,M11.1.0" "America/Jamaica","EST5" "America/Juneau","AKST9AKDT,M3.2.0,M11.1.0" @@ -169,7 +169,7 @@ "America/Porto_Velho","<-04>4" "America/Puerto_Rico","AST4" "America/Punta_Arenas","<-03>3" -"America/Rainy_River","CST6CDT,M3.2.0,M11.1.0" +"America/Rainy_River","EST5" "America/Rankin_Inlet","CST6CDT,M3.2.0,M11.1.0" "America/Recife","<-03>3" "America/Regina","CST6" @@ -194,11 +194,11 @@ "America/Tijuana","PST8PDT,M3.2.0,M11.1.0" "America/Toronto","EST5EDT,M3.2.0,M11.1.0" "America/Tortola","AST4" -"America/Vancouver","PST8PDT,M3.2.0,M11.1.0" +"America/Vancouver","MST7" "America/Whitehorse","MST7" -"America/Winnipeg","CST6CDT,M3.2.0,M11.1.0" +"America/Winnipeg","EST5" "America/Yakutat","AKST9AKDT,M3.2.0,M11.1.0" -"America/Yellowknife","MST7MDT,M3.2.0,M11.1.0" +"America/Yellowknife","CST6" "Antarctica/Casey","<+08>-8" "Antarctica/Davis","<+07>-7" "Antarctica/DumontDUrville","<+10>-10" @@ -326,7 +326,7 @@ "Europe/Bucharest","EET-2EEST,M3.5.0/3,M10.5.0/4" "Europe/Budapest","CET-1CEST,M3.5.0,M10.5.0/3" "Europe/Busingen","CET-1CEST,M3.5.0,M10.5.0/3" -"Europe/Chisinau","EET-2EEST,M3.5.0,M10.5.0/3" +"Europe/Chisinau","EET-2EEST,M3.5.0/3,M10.5.0/4" "Europe/Copenhagen","CET-1CEST,M3.5.0,M10.5.0/3" "Europe/Dublin","IST-1GMT0,M10.5.0,M3.5.0/1" "Europe/Gibraltar","CET-1CEST,M3.5.0,M10.5.0/3" diff --git a/Tactility/Private/Tactility/settings/TimePrivate.h b/Tactility/Private/Tactility/settings/TimePrivate.h index 1968e5e2e..f5384de74 100644 --- a/Tactility/Private/Tactility/settings/TimePrivate.h +++ b/Tactility/Private/Tactility/settings/TimePrivate.h @@ -1,7 +1,28 @@ #pragma once +#include +#include + namespace tt::settings { +/** Applies the saved time zone, first updating its code to the bundled timezones.csv row for its name */ void initTimeZone(); -} +/** + * Find the code of a time zone in the bundled timezones.csv + * @param[in] name the time zone name, e.g. "Europe/Amsterdam" + * @param[out] outCode the code, set only when found + * @return true when found + */ +bool findTimeZoneCode(const std::string& name, std::string& outCode); + +/** + * Find the code of a time zone in CSV text with the format of timezones.csv (LF or CRLF line endings) + * @param[in] csv the CSV text + * @param[in] name the time zone name, e.g. "Europe/Amsterdam" + * @param[out] outCode the code, set only when found + * @return true when found + */ +bool findTimeZoneCode(std::string_view csv, const std::string& name, std::string& outCode); + +} // namespace tt::settings diff --git a/Tactility/Source/settings/time.cpp b/Tactility/Source/settings/time.cpp index 1060444b8..66a34af45 100644 --- a/Tactility/Source/settings/time.cpp +++ b/Tactility/Source/settings/time.cpp @@ -11,6 +11,10 @@ #include +#include + +#include + #ifdef ESP_PLATFORM #include #endif @@ -37,11 +41,57 @@ bool getPreferencesPath(std::string& outPath) { return true; } +void saveTimeZoneCode(const std::string& code) { + std::string path; + if (getPreferencesPath(path)) { + Preferences* preferences = preferences_open(path.c_str()); + if (preferences != nullptr) { + preferences_put_string(preferences, TIMEZONE_PREFERENCES_KEY_CODE, code.c_str()); + preferences_close(preferences); + } + } +} + } // namespace +bool findTimeZoneCode(std::string_view csv, const std::string& name, std::string& outCode) { + // Lines look like "Europe/Amsterdam","CET-1CEST,M3.5.0,M10.5.0/3" + const std::string prefix = "\"" + name + "\",\""; + size_t start = 0; + while (start < csv.size()) { + size_t end = csv.find('\n', start); + if (end == std::string_view::npos) { + end = csv.size(); + } + auto line = csv.substr(start, end - start); + if (line.ends_with('\r')) { + line.remove_suffix(1); // CRLF line ending + } + if (line.starts_with(prefix) && line.size() > prefix.size() && line.ends_with('"')) { + outCode = line.substr(prefix.size(), line.size() - prefix.size() - 1); + return true; + } + start = end + 1; + } + return false; +} + +bool findTimeZoneCode(const std::string& name, std::string& outCode) { + const std::string_view csv(reinterpret_cast(asset_timezones_csv), asset_timezones_csv_size); + return findTimeZoneCode(csv, name, outCode); +} + void initTimeZone() { + auto code = getTimeZoneCode(); + // The saved code is a copy of the timezones.csv row from when the time zone was picked. When a + // country changes its rules, a firmware update ships a new row, so use that for the same name. + std::string bundled_code; + if (hasTimeZone() && findTimeZoneCode(getTimeZoneName(), bundled_code) && bundled_code != code) { + LOG_I(TAG, "Updating the time zone code from %s to %s", code.c_str(), bundled_code.c_str()); + saveTimeZoneCode(bundled_code); + code = bundled_code; + } #ifdef ESP_PLATFORM - auto code= getTimeZoneCode(); if (!code.empty()) { setenv("TZ", code.c_str(), 1); tzset(); diff --git a/Tactility/Tests/Source/TimeZoneTest.cpp b/Tactility/Tests/Source/TimeZoneTest.cpp new file mode 100644 index 000000000..60ecec101 --- /dev/null +++ b/Tactility/Tests/Source/TimeZoneTest.cpp @@ -0,0 +1,81 @@ +#include "doctest.h" + +#include +#include + +#include +#include + +#include +#include + +using namespace tt; + +namespace { + +/** Writes the time zone like an older firmware did, without the radio region and event of setTimeZone() */ +void saveTimeZone(const char* name, const char* code) { + char root[128]; + REQUIRE(paths_get_data_path(root, sizeof(root)) == ERROR_NONE); + const std::string path = std::string(root) + "/settings/time.properties"; + Preferences* preferences = preferences_open(path.c_str()); + REQUIRE(preferences != nullptr); + preferences_put_string(preferences, "tz_name", name); + preferences_put_string(preferences, "tz_code", code); + preferences_close(preferences); +} + +} // namespace + +TEST_CASE("findTimeZoneCode finds the bundled code of a time zone") { + std::string code; + CHECK(settings::findTimeZoneCode("Europe/Amsterdam", code)); + CHECK_EQ(code, "CET-1CEST,M3.5.0,M10.5.0/3"); + CHECK(settings::findTimeZoneCode("Africa/Abidjan", code)); // first row + CHECK_EQ(code, "GMT0"); + CHECK(settings::findTimeZoneCode("Etc/Zulu", code)); // last row + CHECK_EQ(code, "UTC0"); +} + +TEST_CASE("findTimeZoneCode only matches whole names") { + std::string code = "unchanged"; + CHECK_FALSE(settings::findTimeZoneCode("Europe/Amster", code)); + CHECK_FALSE(settings::findTimeZoneCode("Amsterdam", code)); + CHECK_FALSE(settings::findTimeZoneCode("", code)); + CHECK_FALSE(settings::findTimeZoneCode("Mars/Olympus_Mons", code)); + CHECK_EQ(code, "unchanged"); + CHECK(settings::findTimeZoneCode("Etc/GMT+1", code)); + CHECK_EQ(code, "<-01>1"); +} + +TEST_CASE("findTimeZoneCode accepts CRLF line endings") { + const std::string_view csv = "\"Africa/Abidjan\",\"GMT0\"\r\n\"America/Winnipeg\",\"EST5\"\r\n\"Etc/Zulu\",\"UTC0\"\r"; + std::string code; + CHECK(settings::findTimeZoneCode(csv, "Africa/Abidjan", code)); + CHECK_EQ(code, "GMT0"); + CHECK(settings::findTimeZoneCode(csv, "America/Winnipeg", code)); + CHECK_EQ(code, "EST5"); + CHECK(settings::findTimeZoneCode(csv, "Etc/Zulu", code)); // last row, no line feed + CHECK_EQ(code, "UTC0"); +} + +TEST_CASE("initTimeZone replaces an outdated saved code with the bundled one") { + saveTimeZone("America/Winnipeg", "CST6CDT,M3.2.0,M11.1.0"); + settings::initTimeZone(); + std::string bundled; + REQUIRE(settings::findTimeZoneCode("America/Winnipeg", bundled)); + CHECK_EQ(bundled, "EST5"); // CST all year from 1 November 2026 (IANA tz 2026e) + CHECK_EQ(settings::getTimeZoneCode(), bundled); + CHECK_EQ(settings::getTimeZoneName(), "America/Winnipeg"); +} + +TEST_CASE("initTimeZone keeps a saved code that is current or not in the list") { + saveTimeZone("Europe/Amsterdam", "CET-1CEST,M3.5.0,M10.5.0/3"); + settings::initTimeZone(); + CHECK_EQ(settings::getTimeZoneCode(), "CET-1CEST,M3.5.0,M10.5.0/3"); + + saveTimeZone("Custom/Zone", "<+0530>-5:30"); + settings::initTimeZone(); + CHECK_EQ(settings::getTimeZoneCode(), "<+0530>-5:30"); + CHECK_EQ(settings::getTimeZoneName(), "Custom/Zone"); +}