From e9b36822694fa0f73141ea91ce151e824ba142a2 Mon Sep 17 00:00:00 2001 From: kongr45gpen <electrovesta@gmail.com> Date: Sun, 4 Sep 2022 21:37:48 +0000 Subject: [PATCH] Fix UTC conversion --- inc/Time/Time.hpp | 5 +- inc/Time/TimeStamp.hpp | 5 +- inc/Time/TimeStamp.tpp | 52 ++---------------- inc/Time/UTCTimestamp.hpp | 97 +++++++++++++++++++++++++++++----- src/Time/UTCTimestamp.cpp | 30 +++++++++++ test/Time/TimeFormatsTests.cpp | 39 +++++++++++++- test/Time/TimeStampTests.cpp | 30 +++++------ 7 files changed, 175 insertions(+), 83 deletions(-) diff --git a/inc/Time/Time.hpp b/inc/Time/Time.hpp index 3e266581..60d4a969 100644 --- a/inc/Time/Time.hpp +++ b/inc/Time/Time.hpp @@ -75,7 +75,8 @@ namespace Time { inline constexpr uint8_t SecondsPerMinute = 60; inline constexpr uint16_t SecondsPerHour = 3600; inline constexpr uint32_t SecondsPerDay = 86400; - static constexpr uint8_t DaysOfMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + inline constexpr uint8_t MonthsPerYear = 12; + static constexpr uint8_t DaysOfMonth[MonthsPerYear] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; /** * Number of bytes used for the basic time units of the CUC header for this mission @@ -85,7 +86,7 @@ namespace Time { /** * Number of bytes used for the fractional time units of the CUC header for this mission */ - inline constexpr uint8_t CUCFractionalBytes = 1; + inline constexpr uint8_t CUCFractionalBytes = 0; /** * The system epoch (clock measurement starting time). diff --git a/inc/Time/TimeStamp.hpp b/inc/Time/TimeStamp.hpp index 52880509..5b237648 100644 --- a/inc/Time/TimeStamp.hpp +++ b/inc/Time/TimeStamp.hpp @@ -1,5 +1,4 @@ -#ifndef ECSS_SERVICES_TIME_HPP -#define ECSS_SERVICES_TIME_HPP +#pragma once #include <algorithm> #include <chrono> @@ -259,5 +258,3 @@ public: }; #include "TimeStamp.tpp" - -#endif diff --git a/inc/Time/TimeStamp.tpp b/inc/Time/TimeStamp.tpp index e4b33b4f..35a62061 100644 --- a/inc/Time/TimeStamp.tpp +++ b/inc/Time/TimeStamp.tpp @@ -1,5 +1,4 @@ #include <cmath> -#include "TimeStamp.hpp" template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> constexpr bool TimeStamp<BaseBytes, FractionBytes, Num, Denom>::areSecondsValid(TimeStamp::TAICounter_t seconds) { @@ -95,7 +94,7 @@ TimeStamp<BaseBytes, FractionBytes, Num, Denom>::TimeStamp(const UTCTimestamp& t ASSERT_INTERNAL(areSecondsValid(seconds), ErrorHandler::TimeStampOutOfBounds); - taiCounter = static_cast<TAICounter_t>(seconds) << (8 * FractionBytes); + taiCounter = std::chrono::duration_cast<RawDuration>(std::chrono::duration<TAICounter_t>(seconds)).count(); } template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> @@ -151,53 +150,12 @@ etl::array<uint8_t, Time::CUCTimestampMaximumSize> TimeStamp<BaseBytes, Fraction template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> UTCTimestamp TimeStamp<BaseBytes, FractionBytes, Num, Denom>::toUTCtimestamp() { - using namespace Time; + UTCTimestamp timestamp(Time::Epoch.year, Time::Epoch.month, Time::Epoch.day, 0, 0, 0); + timestamp += RawDuration(taiCounter); - uint32_t totalSeconds = asTAIseconds(); - - uint16_t yearUTC = Epoch.year; - uint8_t monthUTC = Epoch.month; - uint8_t dayUTC = Epoch.day; - uint8_t hour = 0; - uint8_t minute = 0; - uint8_t second = 0; - - // calculate years - while (totalSeconds >= (isLeapYear(yearUTC) ? 366 : 365) * SecondsPerDay) { - totalSeconds -= (isLeapYear(yearUTC) ? 366 : 365) * SecondsPerDay; - yearUTC++; - } - - // calculate months - int currentMonth = 0; - while (totalSeconds >= (DaysOfMonth[currentMonth] * SecondsPerDay)) { - monthUTC++; - totalSeconds -= (DaysOfMonth[currentMonth] * SecondsPerDay); - currentMonth++; - if ((currentMonth == 1U) && isLeapYear(yearUTC)) { - if (totalSeconds <= (28 * SecondsPerDay)) { - break; - } - monthUTC++; - totalSeconds -= 29 * SecondsPerDay; - currentMonth++; - } - } - - dayUTC = totalSeconds / SecondsPerDay; - totalSeconds -= dayUTC * SecondsPerDay; - dayUTC++; // add 1 day because we start count from 1 January (and not 0 January!) - - hour = totalSeconds / SecondsPerHour; - totalSeconds -= hour * SecondsPerHour; - - minute = totalSeconds / SecondsPerMinute; - totalSeconds -= minute * SecondsPerMinute; - - second = totalSeconds; - - return {yearUTC, monthUTC, dayUTC, hour, minute, second}; + return timestamp; } + template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> template <uint8_t BaseBytesIn, uint8_t FractionBytesIn, int NumIn, int DenomIn> TimeStamp<BaseBytes, FractionBytes, Num, Denom>::TimeStamp(TimeStamp<BaseBytesIn, FractionBytesIn, NumIn, DenomIn> input) { diff --git a/inc/Time/UTCTimestamp.hpp b/inc/Time/UTCTimestamp.hpp index 062402df..f8ceca62 100644 --- a/inc/Time/UTCTimestamp.hpp +++ b/inc/Time/UTCTimestamp.hpp @@ -2,6 +2,7 @@ #include <cstdint> #include <etl/String.hpp> +#include "Time.hpp" /** * A class that represents a UTC time and date according to ISO 8601 @@ -29,8 +30,7 @@ public: UTCTimestamp(); /** - * - * @todo See if this implements leap seconds + * @todo Add support for leap seconds * @todo Implement leap seconds as ST[20] parameter * @param year the year as it used in Gregorian calendar * @param month the month as it used in Gregorian calendar (1-12 inclusive) @@ -42,19 +42,92 @@ public: UTCTimestamp(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second); /** - * @param textTimestamp the timestamp to parse into a UTC date - * @todo Too expensive to implement (?). It is better to remove this and open it as another issue, or create - * a platform-specific converter that will be only used in x86. + * Add a duration to the timestamp + * + * @note Overflow checks are not performed. + * @tparam Duration A duration of type std::chrono::duration. You can use the default values offered by C++, or anything + * used by the TimeStamp class. Negative duration values are not supported. */ - explicit UTCTimestamp(etl::string<32> textTimestamp); + template<class Duration, typename = std::enable_if_t<Time::is_duration_v<Duration>>> + void operator+=(const Duration& in) { + using namespace std::chrono; + using namespace Time; + + if (in < Duration::zero()) { + ErrorHandler::reportInternalError(ErrorHandler::InvalidTimeStampInput); + return; + } + + uint64_t seconds = duration_cast<duration<uint64_t>>(in).count(); + + while (seconds >= (isLeapYear(year) ? 366 : 365) * SecondsPerDay) { + seconds -= (isLeapYear(year) ? 366 : 365) * SecondsPerDay; + year++; + } + + while (seconds >= (daysOfMonth() * SecondsPerDay)) { + seconds -= daysOfMonth() * SecondsPerDay; + month++; + + if (month > MonthsPerYear) { + // Month overflow needs to be taken care here, so that daysOfMonth() knows + // what month it is. + month -= MonthsPerYear; + year++; + } + } + + day += seconds / SecondsPerDay; + seconds -= (seconds / SecondsPerDay) * SecondsPerDay; + + hour += seconds / SecondsPerHour; + seconds -= (seconds / SecondsPerHour) * SecondsPerHour; + + minute += seconds / SecondsPerMinute; + seconds -= (seconds / SecondsPerMinute) * SecondsPerMinute; + + second += seconds; + + repair(); + } /** - * Compare two timestamps. - * @param Date the date that will be compared with the pointer `this` + * @name Comparison operators + * @{ */ bool operator<(const UTCTimestamp& Date) const; - bool operator>(const UTCTimestamp& Date) const; ///< @copydoc UTCTimestamp::operator< - bool operator==(const UTCTimestamp& Date) const; ///< @copydoc UTCTimestamp::operator< - bool operator<=(const UTCTimestamp& Date) const; ///< @copydoc UTCTimestamp::operator< - bool operator>=(const UTCTimestamp& Date) const; ///< @copydoc UTCTimestamp::operator< + bool operator>(const UTCTimestamp& Date) const; + bool operator==(const UTCTimestamp& Date) const; + bool operator<=(const UTCTimestamp& Date) const; + bool operator>=(const UTCTimestamp& Date) const; + /** + * @} + */ + +private: + /** + * Makes sure that all time fields are within their bounds + * + * For example, if `hours == 1, minutes == 63`, then this function will carry over the numbers so that + * `hours == 2, minutes == 3`. + * + * @note This performs max one propagation for every field. + * For example, if `hours == 1, minutes == 123`, then only the first 60 minutes will be carried over. + */ + void repair(); + + /** + * Find the number of days within the current @ref month. + * Includes leap year calculation. + */ + uint8_t daysOfMonth() const { + using namespace Time; + + uint8_t daysOfMonth = DaysOfMonth[month - 1]; + if (month == 2 && isLeapYear(year)) { + daysOfMonth++; + } + + return daysOfMonth; + } }; diff --git a/src/Time/UTCTimestamp.cpp b/src/Time/UTCTimestamp.cpp index 072d6b67..f534455f 100644 --- a/src/Time/UTCTimestamp.cpp +++ b/src/Time/UTCTimestamp.cpp @@ -131,3 +131,33 @@ bool UTCTimestamp::operator<=(const UTCTimestamp& Date) const { bool UTCTimestamp::operator>=(const UTCTimestamp& Date) const { return ((*this > Date) || (*this == Date)); } +void UTCTimestamp::repair() { + using namespace Time; + + if (second > Time::SecondsPerMinute) { + second -= Time::SecondsPerMinute; + minute++; + } + + const auto MinutesPerHour = SecondsPerHour / SecondsPerMinute; + if (minute >= MinutesPerHour) { + minute -= MinutesPerHour; + hour++; + } + + const auto HoursPerDay = SecondsPerDay / SecondsPerHour; + if (hour >= HoursPerDay) { + hour -= HoursPerDay; + day++; + } + + if (day > daysOfMonth()) { + day -= daysOfMonth(); + month++; + } + + if (month > MonthsPerYear) { + month -= MonthsPerYear; + year++; + } +} diff --git a/test/Time/TimeFormatsTests.cpp b/test/Time/TimeFormatsTests.cpp index 828cf885..c0134afe 100644 --- a/test/Time/TimeFormatsTests.cpp +++ b/test/Time/TimeFormatsTests.cpp @@ -27,6 +27,43 @@ TEST_CASE("UTC timestamps") { CHECK(ServiceTests::thrownError(ErrorHandler::InvalidDate)); } +TEST_CASE("UTC timestamp addition") { + using namespace std::chrono_literals; + + UTCTimestamp time1 = UTCTimestamp{2020, 1, 1, 0, 0, 0}; + UTCTimestamp time2 = UTCTimestamp{2035, 11, 19, 23, 57, 24}; + + SECTION("Valid ranges") { + auto time = time1; + time += -1s; + CHECK(ServiceTests::thrownError(ErrorHandler::InvalidTimeStampInput)); + } + + SECTION("Simple addition") { + auto time = time1; + time += 10s; + CHECK(time == UTCTimestamp{2020, 1, 1, 0, 0, 10}); + + time += 25h; + CHECK(time == UTCTimestamp{2020, 1, 2, 1, 0, 10}); + } + + SECTION("Overflow within range") { + auto time = time2; + time += 1209780s; + CHECK(time == UTCTimestamp{2035, 12, 4, 0, 0, 24}); + + time += 60 * 24h; + CHECK(time == UTCTimestamp{2036, 2, 2, 0, 0, 24}); + } + + SECTION("Future dates") { + auto time = time2; + time += 999999h; + CHECK(time == UTCTimestamp{2149, 12, 18, 14, 57, 24}); + } +} + TEST_CASE("CUC Custom Timestamp as Parameter") { Time::CustomCUC_t time; time.elapsed100msTicks = 999; @@ -39,4 +76,4 @@ TEST_CASE("CUC Custom Timestamp as Parameter") { parameter.setValueFromMessage(message); CHECK(time == parameter.getValue()); -} \ No newline at end of file +} diff --git a/test/Time/TimeStampTests.cpp b/test/Time/TimeStampTests.cpp index daf3df67..9d2b52c4 100644 --- a/test/Time/TimeStampTests.cpp +++ b/test/Time/TimeStampTests.cpp @@ -132,32 +132,37 @@ TEST_CASE("UTC idempotence") { TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(timestamp1); UTCTimestamp timestamp2 = time.toUTCtimestamp(); bool cond = (timestamp2 == timestamp1); - REQUIRE(cond); + CHECK(cond); } { UTCTimestamp timestamp1(2035, 1, 1, 0, 0, 1); // 1 Jan 2035 midnight passed; TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(timestamp1); UTCTimestamp timestamp2 = time.toUTCtimestamp(); bool cond = (timestamp2 == timestamp1); - REQUIRE(cond); + CHECK(cond); } } TEST_CASE("UTC conversion to and from seconds timestamps") { { UTCTimestamp utc(2020, 12, 5, 0, 0, 0); - TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(utc); - REQUIRE(time.asTAIseconds() == 29289600); + TimeStamp<4, 1> time(utc); + CHECK(time.asTAIseconds() == 29289600); } { UTCTimestamp utc(2020, 2, 29, 0, 0, 0); - TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(utc); - REQUIRE(time.asTAIseconds() == 5097600); + TimeStamp<4, 1> time(utc); + CHECK(time.asTAIseconds() == 5097600); } { UTCTimestamp utc(2025, 3, 10, 0, 0, 0); - TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(utc); - REQUIRE(time.asTAIseconds() == 163728000); + TimeStamp<4, 1> time(utc); + CHECK(time.asTAIseconds() == 163728000); + } + { + UTCTimestamp utc(2025, 3, 10, 0, 0, 0); + TimeStamp<4, 1, 2, 3> time(utc); + CHECK(time.asTAIseconds() == 163728000); } } @@ -166,28 +171,19 @@ TEST_CASE("UTC overflow tests") { UTCTimestamp utc(2999, 3, 11, 0, 0, 0); TimeStamp<2, 1> time(utc); REQUIRE(ServiceTests::thrownError(ErrorHandler::TimeStampOutOfBounds)); - ServiceTests::reset(); } SECTION("Seconds too high, small variable") { UTCTimestamp utc(Epoch.year, Epoch.month, Epoch.day, 0, 7, 0); TimeStamp<1, 1> time(utc); REQUIRE(ServiceTests::thrownError(ErrorHandler::TimeStampOutOfBounds)); - ServiceTests::reset(); } SECTION("Seconds too high, wide variable") { UTCTimestamp utc(Epoch.year, Epoch.month, Epoch.day, 0, 7, 0); TimeStamp<1, 4> time(utc); REQUIRE(ServiceTests::thrownError(ErrorHandler::TimeStampOutOfBounds)); - ServiceTests::reset(); } } -// SECTION("Check different templates, should break at compile"){ -// TimeStamp<1, 2> time1; -// TimeStamp<4, 4> time2; -// REQUIRE(time1==time2); -// } - TEST_CASE("Time operators") { SECTION("Same type") { TimeStamp<1, 2> time1; -- GitLab