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