From 14f648f45b5837966f6af0c105fcfcf01246d3a3 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Sun, 28 Apr 2024 09:23:41 +0100 Subject: [PATCH 1/8] Don't restrict SystemClock timezone offset range Can vary much more than plus/minus 12 hours --- Sming/Core/SystemClock.cpp | 7 ++----- Sming/Core/SystemClock.h | 17 +++++++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Sming/Core/SystemClock.cpp b/Sming/Core/SystemClock.cpp index e109a3aa43..a59b24cec2 100644 --- a/Sming/Core/SystemClock.cpp +++ b/Sming/Core/SystemClock.cpp @@ -46,9 +46,6 @@ String SystemClockClass::getSystemTimeString(TimeZone timeType) const bool SystemClockClass::setTimeZoneOffset(int seconds) { - if((unsigned)abs(seconds) <= (12 * SECS_PER_HOUR)) { - timeZoneOffsetSecs = seconds; - return true; - } - return false; + timeZoneOffsetSecs = seconds; + return true; } diff --git a/Sming/Core/SystemClock.h b/Sming/Core/SystemClock.h index 1a79fa49c7..e2f4a732b5 100644 --- a/Sming/Core/SystemClock.h +++ b/Sming/Core/SystemClock.h @@ -58,18 +58,23 @@ class SystemClockClass String getSystemTimeString(TimeZone timeType = eTZ_Local) const; /** @brief Sets the local time zone offset - * @param seconds Offset from UTC of local time zone in seconds (-720 < offset < +720) - * @retval bool true on success, false if value out of range + * @param seconds Offset from UTC of local time zone in seconds + * @see See `setTimeZone()`. */ - bool setTimeZoneOffset(int seconds); + void setTimeZoneOffset(int seconds) + { + timeZoneOffsetSecs = seconds / SECS_PER_MIN; + } /** @brief Set the local time zone offset in hours - * @param localTimezoneOffset Offset from UTC of local time zone in hours (-12.0 < offset < +12.0) + * @param localTimezoneOffset Offset from UTC of local time zone in hours * @retval bool true on success, false if value out of range + * @note Values for local standard time may exceed +/- 12 + * For example, Pacific/Kiritimati has a 14-hour offset (in 2024). */ - bool setTimeZone(float localTimezoneOffset) + void setTimeZone(float localTimezoneOffset) { - return setTimeZoneOffset(localTimezoneOffset * SECS_PER_HOUR); + setTimeZoneOffset(localTimezoneOffset * SECS_PER_HOUR); } /** @brief Get the current time zone offset From 9aa850df1cb7a3bd756934d795821a0ffed2f46c Mon Sep 17 00:00:00 2001 From: mikee47 Date: Tue, 28 May 2024 11:22:21 +0100 Subject: [PATCH 2/8] Make core DateTime defines signed, add MINS_PER_HOUR Causes all sorts of hidden issues performing arithmetic otherwise --- Sming/Core/DateTime.h | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sming/Core/DateTime.h b/Sming/Core/DateTime.h index cceb7b28ca..cfc89bb952 100644 --- a/Sming/Core/DateTime.h +++ b/Sming/Core/DateTime.h @@ -22,13 +22,14 @@ #include /* Useful Constants */ -#define SECS_PER_MIN (60UL) -#define SECS_PER_HOUR (3600UL) -#define SECS_PER_DAY (SECS_PER_HOUR * 24L) -#define DAYS_PER_WEEK (7L) +#define SECS_PER_MIN 60 +#define SECS_PER_HOUR 3600 +#define SECS_PER_DAY (SECS_PER_HOUR * 24) +#define MINS_PER_HOUR 60 +#define DAYS_PER_WEEK 7 #define SECS_PER_WEEK (SECS_PER_DAY * DAYS_PER_WEEK) -#define SECS_PER_YEAR (SECS_PER_WEEK * 52L) -#define SECS_YR_2000 (946681200UL) +#define SECS_PER_YEAR (SECS_PER_WEEK * 52) +#define SECS_YR_2000 946681200 /** @brief Days of week */ From 56efb093322fd15fea103b3e9ab8950b7c21a67a Mon Sep 17 00:00:00 2001 From: mikee47 Date: Mon, 27 May 2024 13:09:17 +0100 Subject: [PATCH 3/8] Add timezone support to DateTime and SystemClockClass This uses `DateTime::ZoneInfo` and `ZonedTime` to provide a basic mechanism for dealing with zoned time information. Handling rules, etc. must be handled with separate library. --- Sming/Core/DateTime.cpp | 94 ++++++++++++++-- Sming/Core/DateTime.h | 86 +++++++++++++-- Sming/Core/SystemClock.cpp | 20 ++-- Sming/Core/SystemClock.h | 31 +++++- Sming/Core/ZonedTime.cpp | 3 + Sming/Core/ZonedTime.h | 139 ++++++++++++++++++++++++ docs/source/framework/core/datetime.rst | 7 ++ tests/HostTests/include/DateTimeData.h | 18 +-- tests/HostTests/modules/DateTime.cpp | 21 +++- tests/HostTests/tools/datetime-test.py | 5 +- 10 files changed, 384 insertions(+), 40 deletions(-) create mode 100644 Sming/Core/ZonedTime.cpp create mode 100644 Sming/Core/ZonedTime.h diff --git a/Sming/Core/DateTime.cpp b/Sming/Core/DateTime.cpp index 4840e84841..bb1b6fde73 100644 --- a/Sming/Core/DateTime.cpp +++ b/Sming/Core/DateTime.cpp @@ -81,6 +81,40 @@ bool isLeapCentury(uint16_t year) } // namespace +DateTime::ZoneInfo::Tag DateTime::ZoneInfo::Tag::fromString(const char* s) +{ + return s ? fromString(s, strlen(s)) : Tag{}; +} + +DateTime::ZoneInfo::Tag DateTime::ZoneInfo::Tag::fromString(const char* s, size_t len) +{ + Tag tag{}; + if(s && len) { + len = std::min(maxSize, len); + memcpy(tag.value, s, len); + tag.value[len] = '\0'; + } + return tag; +} + +String DateTime::ZoneInfo::getOffsetString(char sep) const +{ + String s; + auto mins = offsetMins; + if(mins < 0) { + s += '-'; + mins = -mins; + } else { + s += '+'; + } + s.concat(mins / MINS_PER_HOUR, DEC, 2); + if(sep) { + s += sep; + } + s.concat(mins % MINS_PER_HOUR, DEC, 2); + return s; +} + bool DateTime::isLeapYear(uint16_t year) { return year % 4 == 0 && isLeapCentury(year); @@ -206,7 +240,7 @@ bool DateTime::fromHttpDate(const String& httpDate) return true; } -bool DateTime::fromISO8601(const String& datetime) +bool DateTime::fromISO8601(const String& datetime, ZoneInfo* zone) { auto ptr = datetime.c_str(); bool notDigit{false}; @@ -275,15 +309,44 @@ bool DateTime::fromISO8601(const String& datetime) } } } - if(haveTime && notDigit) { - return false; + + int16_t offsetMins{0}; + + if(haveTime) { + if(notDigit) { + return false; + } + int sign = 0; + if(skip('-')) { + sign = -1; + } else if(skip('+')) { + sign = 1; + } else { + skip('Z'); + } + if(sign) { + auto hour = parseNumber(2); + skip(':'); + auto min = parseNumber(2); + offsetMins = sign * (int(hour) * MINS_PER_HOUR + int(min)); + } } if(*ptr != '\0') { return false; } - calcDayOfYear(); + if(zone) { + *zone = ZoneInfo{.offsetMins = offsetMins}; + } + + if(offsetMins == 0 || zone) { + // No offset to apply + calcDayOfYear(); + } else { + // Full recalculation required to correctly apply offset + setTime(toUnixTime() - int(offsetMins) * int(SECS_PER_MIN)); + } return true; } @@ -308,9 +371,9 @@ String DateTime::toFullDateTimeString() const return format(_F("%x %T")); } -String DateTime::toISO8601() const +String DateTime::toISO8601(const ZoneInfo* zone) const { - return format(_F("%FT%TZ")); + return format(zone ? _F("%FT%T%:z") : _F("%FT%TZ"), zone); } String DateTime::toHTTPDate() const @@ -392,7 +455,7 @@ time_t DateTime::toUnixTime(int sec, int min, int hour, int day, uint8_t month, return seconds; } -String DateTime::format(const char* sFormat) const +String DateTime::format(const char* sFormat, const ZoneInfo* zone) const { if(sFormat == nullptr) { return nullptr; @@ -414,7 +477,24 @@ String DateTime::format(const char* sFormat) const } c = *sFormat++; + char timesep{'\0'}; + if(c == ':') { + timesep = c; + c = *sFormat++; + } switch(c) { + // Timezone offset from UTC with or without ':' separator + case 'z': + if(zone) { + sReturn += zone->getOffsetString(timesep); + } + break; + // Timezone tag + case 'Z': + if(zone) { + sReturn += zone->tag; + } + break; // Year (not implemented: EY, Oy, Ey, EC, G, g) case 'Y': // Full year as a decimal number, e.g. 2018 sReturn += Year; diff --git a/Sming/Core/DateTime.h b/Sming/Core/DateTime.h index cfc89bb952..e94aabb961 100644 --- a/Sming/Core/DateTime.h +++ b/Sming/Core/DateTime.h @@ -136,6 +136,49 @@ inline constexpr unsigned elapsedSecsThisWeek(time_t time) class DateTime { public: + /** + * @brief Basic information required when displaying or handling local times. + */ + struct ZoneInfo { + /** + * @brief Type for timezone abbreviation such as "GMT", "EEST" + */ + struct Tag { + static constexpr size_t maxSize = 5; + char value[maxSize + 1]; + + /** + * @name String will be truncated if required and always NUL terminated. + * @{ + */ + static Tag fromString(const char* s); + static Tag fromString(const char* s, size_t len); + /** @} */ + + operator const char*() const + { + return value; + } + }; + + Tag tag{}; ///< Abbreviation such as "GMT", "EEST" shown after time + int16_t offsetMins{0}; ///< Offset from UTC in minutes + bool isDst{false}; ///< True if daylight savings is in effect + + /** + * @brief Get the offset in seconds so it can be added/subtracted directly from a time_t value + */ + int offsetSecs() const + { + return int(offsetMins) * SECS_PER_MIN; + } + + /** + * @brief Return offset in ISO8601 string format, e.g. +11:00 + */ + String getOffsetString(char sep) const; + }; + /** @brief Instantiate an uninitialised date and time object */ DateTime() @@ -193,6 +236,7 @@ class DateTime /** @brief Parse an ISO8601 date/time string * @param datetime Date and optional time in ISO8601 format, e.g. "1994-11-06", "1994-11-06T08:49:37". Separators are optional. + * @param zone If provided, returns offset component of time * @retval bool True on success * @see See https://en.wikipedia.org/wiki/ISO_8601 * @@ -211,8 +255,18 @@ class DateTime * Thh:mm or Thhmm * Thh.hhh * Thh + * + * Times with an offset: + * + *