diff --git a/src/modm/math/calendar/date_time.hpp b/src/modm/math/calendar/date_time.hpp new file mode 100644 index 0000000000..8750054020 --- /dev/null +++ b/src/modm/math/calendar/date_time.hpp @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2024, Niklas Hauser + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include +#include +#include + +namespace modm +{ + +/// Efficient representation of a date and time +/// @ingroup modm_math_calendar +class DateTime +{ +public: + using duration = std::chrono::milliseconds; + + constexpr DateTime() = default; + + constexpr explicit + DateTime(uint16_t year, uint8_t month, uint8_t day, + uint8_t hour = 0, uint8_t minute = 0, uint8_t second = 0, + uint16_t millisecond = 0, uint8_t weekday = 0) + : data(year - epoch, month, day, hour, minute, second, millisecond), _weekday(weekday) + {} + + constexpr std::chrono::year + year() const + { return std::chrono::year{epoch + data.year}; } + + constexpr std::chrono::month + month() const + { return std::chrono::month{data.month}; } + + constexpr std::chrono::day + day() const + { return std::chrono::day{data.day}; } + + constexpr std::chrono::year_month_day + year_month_day() const + { return std::chrono::year_month_day{year(), month(), day()}; } + + constexpr std::chrono::weekday + weekday() const + { return std::chrono::weekday{_weekday}; } + + constexpr std::chrono::days + day_of_year() const + { + uint16_t yday = m2d[data.month] + data.day - 1u; + if ((data.year & 0b11) == 0b10 and data.month > 2u) yday++; + return std::chrono::days{yday}; + } + + + constexpr std::chrono::hours + hour() const + { return std::chrono::hours{data.hour}; } + + constexpr std::chrono::minutes + minute() const + { return std::chrono::minutes{data.minute}; } + + constexpr std::chrono::seconds + second() const + { return std::chrono::seconds{data.second}; } + + constexpr std::chrono::milliseconds + millisecond() const + { return std::chrono::milliseconds{data.millisecond}; } + + constexpr std::chrono::hh_mm_ss + hh_mm_ss() const + { return std::chrono::hh_mm_ss{time_since_epoch()}; } + + + constexpr std::tm + tm() const + { + std::tm tm{}; + + tm.tm_sec = data.second; + tm.tm_min = data.minute; + tm.tm_hour = data.hour; + + tm.tm_mday = data.day; + tm.tm_mon = data.month; + tm.tm_year = data.year + epoch - 1900u; + + tm.tm_wday = weekday().c_encoding(); + + tm.tm_yday = day_of_year().count(); + + return tm; + } + + constexpr std::time_t + time_t() const + { + const uint8_t leap_days_since_epoch = (data.year < 2u) ? 0u : (data.year - 2u) / 4u; + return (data.year * seconds_per_year + + (leap_days_since_epoch + day_of_year().count()) * seconds_per_day + + (data.hour * 60l + data.minute) * 60l + data.second); + } + + constexpr duration + time_since_epoch() const + { + return duration{time_t() * 1000ll + data.millisecond}; + } + + + constexpr auto operator<=>(const DateTime& other) const + { return data.value <=> other.data.value; } + + constexpr auto operator==(const DateTime& other) const + { return data.value == other.data.value; } + +private: + union Data + { + constexpr Data() = default; + constexpr explicit + Data(uint8_t year, uint8_t month, uint8_t day, + uint8_t hour, uint8_t minute, uint8_t second, uint16_t millisecond) + : millisecond(millisecond), second(second), minute(minute), hour(hour), + day(day), month(month), year(year) {} + struct + { + uint16_t millisecond; + uint8_t second; + uint8_t minute; + uint8_t hour; + + uint8_t day; + uint8_t month; + uint8_t year; + } modm_packed; + uint64_t value; + }; + + Data data{}; + uint8_t _weekday{}; + + static constexpr uint16_t epoch{1970}; + static constexpr uint32_t seconds_per_day{24*60*60}; + static constexpr uint64_t seconds_per_year{365*seconds_per_day}; + // accumulated (non-leap) days per month, 1-indexed! + static constexpr uint16_t m2d[] = {0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + +public: + static constexpr DateTime + from_tm(const std::tm& tm) + { + return DateTime{ + uint16_t(tm.tm_year + 1900 - epoch), uint8_t(tm.tm_mon), uint8_t(tm.tm_mday), + uint8_t(tm.tm_hour), uint8_t(tm.tm_min), uint8_t(tm.tm_sec), 0u, uint8_t(tm.tm_wday)}; + } + + // static constexpr DateTime + // from_time_t(std::time_t time) + // { + // return DateTime{}; + // } + + static consteval DateTime + fromBuildTime() + { + // Example: "Mon Dec 23 17:45:35 2024" + static constexpr std::string_view timestamp{__TIMESTAMP__}; + constexpr auto to_uint = [&](uint8_t offset, uint8_t length) -> uint16_t + { + const auto str = timestamp.substr(offset, length); + int integer; + (void) std::from_chars(str.begin(), str.end(), integer); + return uint16_t(integer); + }; + // All easy to parse integers + const uint16_t cyear{to_uint(20, 4)}; + const uint8_t cday{to_uint(8, 2)}; + const uint8_t chour{to_uint(11, 2)}; + const uint8_t cminute{to_uint(14, 2)}; + const uint8_t csecond{to_uint(17, 2)}; + + // Annoying to parse strings + static constexpr std::string_view months[] + {"", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + uint8_t cmonth{1}; + for (; months[cmonth] != timestamp.substr(4, 3) and cmonth <= 12; ++cmonth) ; + + static constexpr std::string_view weekdays[] + {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + uint8_t cweekday{}; + for (; weekdays[cweekday] != timestamp.substr(0, 3) and cweekday < 7; ++cweekday) ; + + return DateTime{cyear, cmonth, cday, chour, cminute, csecond, 0, cweekday}; + } +}; + +} // namespace modm + +#if MODM_HAS_IOSTREAM +#include +#include + +namespace modm +{ + +/// @ingroup modm_math_calendar +inline modm::IOStream& +operator << (modm::IOStream& s, const DateTime& dt) +{ + // ISO encoding: 2024-12-22 18:39:21.342 + s.printf("%04" PRIu16 "-%02" PRIu8 "-%02" PRIu8 " %02" PRIu8 ":%02" PRIu8 ":%02" PRIu8 ".%03" PRIu16, + uint16_t(int(dt.year())), uint8_t(unsigned(dt.month())), uint8_t(unsigned(dt.day())), + uint8_t(dt.hour().count()), uint8_t(dt.minute().count()), uint8_t(dt.second().count()), + uint16_t(dt.millisecond().count())); + return s; +} + +} // modm namespace + +#endif diff --git a/src/modm/math/calendar/module.lb b/src/modm/math/calendar/module.lb new file mode 100644 index 0000000000..32fd1f88f5 --- /dev/null +++ b/src/modm/math/calendar/module.lb @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2024, Niklas Hauser +# +# This file is part of the modm project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# ----------------------------------------------------------------------------- + +def init(module): + module.name = ":math:calendar" + module.description = "Calendar Operations" + +def prepare(module, options): + module.depends(":stdc++") + # libstdc++ does not provide support for + return options[":target"].identifier.platform != "avr" + +def build(env): + env.outbasepath = "modm/src/modm/math/calendar" + env.copy(".") diff --git a/test/modm/math/calendar/datetime_test.cpp b/test/modm/math/calendar/datetime_test.cpp new file mode 100644 index 0000000000..fd5fc98d9d --- /dev/null +++ b/test/modm/math/calendar/datetime_test.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2009-2011, Fabian Greif + * Copyright (c) 2010, Martin Rosekeit + * Copyright (c) 2012, Niklas Hauser + * Copyright (c) 2012, Sascha Schade + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#include "datetime_test.hpp" +#include + +void +DateTimeTest::testConversion() +{ + const auto dt1 = modm::DateTime(1970, 1, 1, 0, 0, 0); + TEST_ASSERT_EQUALS(dt1.day_of_year().count(), 0l); + TEST_ASSERT_EQUALS(dt1.time_t(), 0l); + + const auto dt2 = modm::DateTime(1970, 1, 1, 0, 0, 1); + TEST_ASSERT_EQUALS(dt2.day_of_year().count(), 0l); + TEST_ASSERT_EQUALS(dt2.time_t(), 1l); + TEST_ASSERT_TRUE(dt1 < dt2); + TEST_ASSERT_TRUE(dt1 <= dt2); + TEST_ASSERT_FALSE(dt1 == dt2); + TEST_ASSERT_FALSE(dt1 >= dt2); + TEST_ASSERT_FALSE(dt1 > dt2); + + // first leap year since epoch + const auto dt3 = modm::DateTime(1972, 3, 1, 0, 0, 0); + TEST_ASSERT_EQUALS(dt3.day_of_year().count(), 31l+29l); + TEST_ASSERT_EQUALS(dt3.time_t(), 24*60*60*(365*2+31+29)); // 1 day too long? + TEST_ASSERT_TRUE(dt1 < dt3); + TEST_ASSERT_TRUE(dt2 < dt3); + + const auto dt4 = modm::DateTime(2024, 12, 24, 12, 24, 12); + TEST_ASSERT_EQUALS(dt4.day_of_year().count(), 358); + TEST_ASSERT_EQUALS(dt4.time_t(), 1735043052); + TEST_ASSERT_TRUE(dt1 < dt4); + TEST_ASSERT_TRUE(dt2 < dt4); + TEST_ASSERT_TRUE(dt3 < dt4); + TEST_ASSERT_EQUALS(dt4, dt4); +} diff --git a/test/modm/math/calendar/datetime_test.hpp b/test/modm/math/calendar/datetime_test.hpp new file mode 100644 index 0000000000..92d8b698e4 --- /dev/null +++ b/test/modm/math/calendar/datetime_test.hpp @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024, Niklas Hauser + * + * This file is part of the modm project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +// ---------------------------------------------------------------------------- + +#include + +/// @ingroup modm_test_test_math +class DateTimeTest : public unittest::TestSuite +{ +public: + void + testConversion(); +}; diff --git a/test/modm/math/module.lb b/test/modm/math/module.lb index 5dd07445e1..4099eb9238 100644 --- a/test/modm/math/module.lb +++ b/test/modm/math/module.lb @@ -25,9 +25,16 @@ def prepare(module, options): "modm:math:matrix", "modm:math:algorithm", "modm:math:utils") + if options[":target"].identifier.platform != "avr": + # libstdc++ does not provide support for + module.depends("modm:math:calendar") return True def build(env): env.outbasepath = "modm-test/src/modm-test/math" - env.copy('.') + patterns = [] + if env[":target"].identifier["platform"] == "avr": + # libstdc++ does not provide support for + patterns += ["*calendar*"] + env.copy('.', ignore=env.ignore_patterns(*patterns))