From c2638be69fd276fc0dbc9454be6d575e8e5a2a5e Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Sun, 14 Feb 2021 21:02:29 +0100 Subject: [PATCH] Added espchrono.h/cpp --- CMakeLists.txt | 9 ++ src/espchrono.cpp | 250 ++++++++++++++++++++++++++++++++++++++++++++++ src/espchrono.h | 186 ++++++++++++++++++++++++++++++++++ 3 files changed, 445 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 src/espchrono.cpp create mode 100644 src/espchrono.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..cc67eee --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,9 @@ +set(headers + src/espchrono.h +) + +set(sources + src/espchrono.cpp +) + +idf_component_register(INCLUDE_DIRS src SRCS ${headers} ${sources} REQUIRES freertos esp_system cpputils date espcpputils) diff --git a/src/espchrono.cpp b/src/espchrono.cpp new file mode 100644 index 0000000..95f0189 --- /dev/null +++ b/src/espchrono.cpp @@ -0,0 +1,250 @@ +#include "espchrono.h" + +using namespace std::chrono_literals; +using namespace date; + +namespace espchrono { + +namespace { +static const uint8_t _monthDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + +bool summerTime(local_clock::time_point timeStamp) +{ + LocalDateTime _tempDateTime = toDateTime(timeStamp); + + if (_tempDateTime.date.month() < March || _tempDateTime.date.month() > October) return false; // keine Sommerzeit in Jan, Feb, Nov, Dez + if (_tempDateTime.date.month() > March && _tempDateTime.date.month() < October) return true; // Sommerzeit in Apr, Mai, Jun, Jul, Aug, Sep + return + (_tempDateTime.date.month() == March && (_tempDateTime.hour + 24 * unsigned(_tempDateTime.date.day())) >= (3 + 24 * (31 - (5 * int(_tempDateTime.date.year()) / 4 + 4) % 7))) || + (_tempDateTime.date.month() == October && (_tempDateTime.hour + 24 * unsigned(_tempDateTime.date.day())) < (3 + 24 * (31 - (5 * int(_tempDateTime.date.year()) / 4 + 1) % 7))); +} + +bool daylightSavingTime(local_clock::time_point _timeStamp) +{ + LocalDateTime _tempDateTime = toDateTime(_timeStamp); + + // see http://stackoverflow.com/questions/5590429/calculating-daylight-saving-time-from-only-date + // since 2007 DST begins on second Sunday of March and ends on first Sunday of November. + // Time change occurs at 2AM locally + if (_tempDateTime.date.month() < March || _tempDateTime.date.month() > November) return false; //January, february, and december are out. + if (_tempDateTime.date.month() > March && _tempDateTime.date.month() < November) return true; //April to October are in + + int previousSunday = unsigned(_tempDateTime.date.day()) - (_tempDateTime.dayOfWeek - 1); // dow Sunday input was 1, + // need it to be Sunday = 0. If 1st of month = Sunday, previousSunday=1-0=1 + //int previousSunday = day - (dow-1); + // -------------------- March --------------------------------------- + //In march, we are DST if our previous Sunday was = to or after the 8th. + if (_tempDateTime.date.month() == March) { // in march, if previous Sunday is after the 8th, is DST + // unless Sunday and hour < 2am + if (previousSunday >= 8) { // Sunday = 1 + // return true if day > 14 or (dow == 1 and hour >= 2) + return ((_tempDateTime.date.day() > 14_d) || + ((_tempDateTime.dayOfWeek == 1 && _tempDateTime.hour >= 2) || _tempDateTime.dayOfWeek > 1)); + } // end if ( previousSunday >= 8 && _dateTime.dayofWeek > 0 ) + else + { + // previousSunday has to be < 8 to get here + //return (previousSunday < 8 && (_tempDateTime.dayofWeek - 1) = 0 && _tempDateTime.hour >= 2) + return false; + } // end else + } // end if (_tempDateTime.date.month == 3 ) + + // ------------------------------- November ------------------------------- + + // gets here only if month = November + //In november we must be before the first Sunday to be dst. + //That means the previous Sunday must be before the 2nd. + if (previousSunday < 1) + { + // is not true for Sunday after 2am or any day after 1st Sunday any time + return ((_tempDateTime.dayOfWeek == 1 && _tempDateTime.hour < 2) || (_tempDateTime.dayOfWeek > 1)); + //return true; + } // end if (previousSunday < 1) + else + { + // return false unless after first wk and dow = Sunday and hour < 2 + return (_tempDateTime.date.day() < 8_d && _tempDateTime.dayOfWeek == 1 && _tempDateTime.hour < 2); + } // end else +} +} + +local_clock::time_point utcToLocal(utc_clock::time_point utc, time_zone timezone) +{ + local_clock::time_point local{utc.time_since_epoch() + timezone.offset, timezone, false}; + + switch (timezone.dayLightSavingMode) + { + case DayLightSavingMode::None: break; + case DayLightSavingMode::EuropeanSummerTime: if (summerTime(local)) local.dst = true; break; + case DayLightSavingMode::UsDaylightTime: if (daylightSavingTime(local)) local.dst = true; break; + } + + if (local.dst) + local += 1h; + + return local; +} + +utc_clock::time_point localToUtc(local_clock::time_point local) +{ + utc_clock::time_point utc{local.time_since_epoch()}; + + if (local.dst) + utc -= 1h; + + utc -= local.timezone.offset; + + return utc; +} + +namespace { +DateTime toDateTime(seconds ts) +{ + auto _time = ts.count(); + + uint8_t second(_time % 60); + _time /= 60; // now it is minutes + uint8_t minute(_time % 60); + _time /= 60; // now it is hours + uint8_t hour(_time % 24); + _time /= 24; // now it is _days + auto dayOfWeek = DateTime::DayOfWeek(((_time + 4) % 7) + 1); // Sunday is day 1 + + date::year year{1970}; + unsigned long _days = 0; + while ((unsigned)(_days += (year.is_leap() ? 366 : 365)) <= _time) { + year++; + } + + _days -= year.is_leap() ? 366 : 365; + _time -= _days; // now it is days in this year, starting at 0 + + _days = 0; + uint8_t _monthLength = 0; + date::month month = January; + for (; month <= December; month++) { + if (month == February) { // february + if (year.is_leap()) { + _monthLength = 29; + } else { + _monthLength = 28; + } + } else { + _monthLength = _monthDays[unsigned(month)-1]; + } + + if (_time >= _monthLength) { + _time -= _monthLength; + } else { + break; + } + } + date::day day(_time + 1); // day of month + + DateTime dateTime{year_month_day{year, month, day}}; + dateTime.hour=hour; + dateTime.minute=minute; + dateTime.second=second; + dateTime.dayOfWeek=dayOfWeek; + return dateTime; +} +} + +DateTime toDateTime(utc_clock::time_point ts) +{ + return toDateTime(ts.time_since_epoch()); +} + +LocalDateTime toDateTime(local_clock::time_point ts) +{ + LocalDateTime dateTime = LocalDateTime{toDateTime(ts.time_since_epoch())}; + dateTime.dst = ts.dst; + dateTime.timezone = ts.timezone; + return dateTime; +} + +std::optional parseDateTime(std::string_view str) +{ + // both valid: + // 2020-11-10T21:31 + // 2020-11-10T21:31:10 + + int year; + unsigned month; + unsigned day; + uint8_t hour; + uint8_t minute; + uint8_t second{}; + + constexpr auto dateTimeFormat = "%4d-%2u-%2uT%2hhu:%2hhu:%2hhu"; + if (const auto scanned = std::sscanf(str.data(), dateTimeFormat, &year, &month, &day, &hour, &minute, &second); scanned < 5) + return std::nullopt; + + return DateTime{ + .date=date::year_month_day{date::year{year}, date::month{month}, date::day{day}}, + .hour=hour, + .minute=minute, + .second=second + }; +} + +std::optional parseDaypoint(std::string_view str) +{ + int8_t hour, minute, second{}; + + constexpr auto daypointFormat = "%2hhd:%2hhd:%2hhd"; + if (const auto scanned = std::sscanf(str.data(), daypointFormat, &hour, &minute, &second); scanned < 2) + return std::nullopt; + + if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) + return std::nullopt; + + return hours{hour} + minutes{minute} + seconds{second}; +} + +std::string toString(const DateTime &dateTime) +{ + char buf[27]; + + std::sprintf(buf, "%04i-%02u-%02uT%02hhu:%02hhu:%02hhu", + int{dateTime.date.year()}, unsigned{dateTime.date.month()}, unsigned{dateTime.date.day()}, + dateTime.hour, dateTime.minute, dateTime.second); + + return std::string{buf}; +} + +std::string toString(const LocalDateTime &dateTime) +{ + char buf[34]; + + date::hh_mm_ss helper{dateTime.timezone.offset + hours{dateTime.dst ? 1 : 0}}; + + std::sprintf(buf, "%04i-%02u-%02uT%02hhu:%02hhu:%02hhu %s%02hhu:%02hhu", + int{dateTime.date.year()}, unsigned{dateTime.date.month()}, unsigned{dateTime.date.day()}, + dateTime.hour, dateTime.minute, dateTime.second, + helper.is_negative() ? "-" : "+", uint8_t(helper.hours().count()), uint8_t(helper.minutes().count())); + + return std::string{buf}; +} + +std::string toDaypointString(seconds seconds) +{ + date::hh_mm_ss helper(seconds); + char buf[10]; + std::sprintf(buf, "%s%02hhd:%02hhd:%02hhd", helper.is_negative() ? "-" : "", int8_t(helper.hours().count()), int8_t(helper.minutes().count()), int8_t(helper.seconds().count())); + return std::string{buf}; +} + +milliseconds ago(millis_clock::time_point a) +{ + return millis_clock::now() - a; +} + +std::string toString(milliseconds val) { return std::to_string(val.count()) + "ms"; } +std::string toString(seconds val) { return std::to_string(val.count()) + "s"; } +std::string toString(minutes val) { return std::to_string(val.count()) + "min"; } +std::string toString(hours val) { return std::to_string(val.count()) + "h"; } + +IMPLEMENT_TYPESAFE_ENUM(DayLightSavingMode, : uint8_t, DayLightSavingModeValues) + +} // namespace espchrono diff --git a/src/espchrono.h b/src/espchrono.h new file mode 100644 index 0000000..55b701c --- /dev/null +++ b/src/espchrono.h @@ -0,0 +1,186 @@ +#pragma once + +// system includes +#include +#include +#include +#include +#include +#include +#include + +// 3rdparty lib includes +#include + +// local includes +#include "cpptypesafeenum.h" + +namespace espchrono { + +using milliseconds = std::chrono::duration; +using seconds = std::chrono::duration; +using minutes = std::chrono::duration>; +using hours = std::chrono::duration>; + +struct utc_clock +{ + typedef seconds duration; + typedef duration::rep rep; + typedef duration::period period; + typedef std::chrono::time_point time_point; + + //static_assert(utc_clock::duration::min() < utc_clock::duration::zero(), + // "a clock's minimum duration cannot be less than its epoch"); + + static constexpr bool is_steady = false; + + static time_point now() noexcept; +}; + +#define DayLightSavingModeValues(x) \ + x(None) \ + x(EuropeanSummerTime) \ + x(UsDaylightTime) +DECLARE_TYPESAFE_ENUM(DayLightSavingMode, : uint8_t, DayLightSavingModeValues) + +struct time_zone +{ + minutes offset{}; + DayLightSavingMode dayLightSavingMode{}; + + bool operator== (const time_zone &other) const + { + return offset == other.offset && + dayLightSavingMode == other.dayLightSavingMode; + } + + bool operator!= (const time_zone &other) const + { + return offset != other.offset || + dayLightSavingMode != other.dayLightSavingMode; + } +}; + +template +struct local_time_point : std::chrono::time_point<_Clock, _Dur> +{ +private: + using base_class = std::chrono::time_point<_Clock, _Dur>; + +public: + using base_class::time_point; + + constexpr local_time_point(const local_time_point &other) = default; + + constexpr explicit local_time_point(_Dur duration, const time_zone &_timezone, bool _dst) : + base_class{duration}, timezone{_timezone}, dst{_dst} + {} + + time_zone timezone{}; + bool dst{}; + + bool operator== (const base_class &other) const = delete; + + bool operator== (const local_time_point &other) const + { + return static_cast(*this) == static_cast(other) && + timezone == other.timezone && + dst == other.dst; + } + + bool operator!= (const base_class &other) const = delete; + + bool operator!= (const local_time_point &other) const + { + return static_cast(*this) != static_cast(other) || + timezone != other.timezone || + dst != other.dst; + } +}; + +struct local_clock +{ + typedef seconds duration; + typedef duration::rep rep; + typedef duration::period period; + typedef local_time_point time_point; + + //static_assert(local_clock::duration::min() < local_clock::duration::zero(), + // "a clock's minimum duration cannot be less than its epoch"); + + static constexpr bool is_steady = false; + + static time_point now() noexcept; +}; + +struct millis_clock +{ + typedef milliseconds duration; + typedef duration::rep rep; + typedef duration::period period; + typedef std::chrono::time_point time_point; + + static constexpr bool is_steady = true; + + static time_point now() noexcept; +}; + +struct DateTime +{ + date::year_month_day date; + + uint8_t hour{}; + uint8_t minute{}; + uint8_t second{}; + + enum DayOfWeek { Sunday = 1, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }; + DayOfWeek dayOfWeek{}; + + bool operator==(const DateTime &other) const + { + return date == other.date && + hour == other.hour && + minute == other.minute && + second == other.second && + dayOfWeek == other.dayOfWeek; + } +}; + +struct LocalDateTime : public DateTime +{ + time_zone timezone{}; + bool dst{}; + + bool operator==(const LocalDateTime &other) const + { + return DateTime::operator==(other) && + timezone == other.timezone && + dst == other.dst; + } +}; + +local_clock::time_point utcToLocal(utc_clock::time_point timeStamp, time_zone timezone); +utc_clock::time_point localToUtc(local_clock::time_point local); + +DateTime toDateTime(utc_clock::time_point ts); +LocalDateTime toDateTime(local_clock::time_point ts); + +//! Returns null if string cannot be parsed +std::optional parseDateTime(std::string_view str); + +//! Returns null if string cannot be parsed +std::optional parseDaypoint(std::string_view str); + +std::string toString(const DateTime &dateTime); + +std::string toString(const LocalDateTime &dateTime); + +std::string toDaypointString(seconds seconds); + +milliseconds ago(millis_clock::time_point a); + +std::string toString(milliseconds val); +std::string toString(seconds val); +std::string toString(minutes val); +std::string toString(hours val); +} // namespace espchrono