Skip to content

Commit

Permalink
[math] Add :math:calendar module with DateTime class
Browse files Browse the repository at this point in the history
  • Loading branch information
salkinium committed Dec 28, 2024
1 parent 5273908 commit 2a2e8c9
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 1 deletion.
235 changes: 235 additions & 0 deletions src/modm/math/calendar/date_time.hpp
Original file line number Diff line number Diff line change
@@ -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 <chrono>
#include <ctime>
#include <charconv>
#include <string_view>
#include <cstring>

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<duration>
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 <inttypes.h>
#include <modm/io/iostream.hpp>

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
24 changes: 24 additions & 0 deletions src/modm/math/calendar/module.lb
Original file line number Diff line number Diff line change
@@ -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 <charconv>
return options[":target"].identifier.platform != "avr"

def build(env):
env.outbasepath = "modm/src/modm/math/calendar"
env.copy(".")
48 changes: 48 additions & 0 deletions test/modm/math/calendar/datetime_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <modm/math/calendar/date_time.hpp>

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);
}
20 changes: 20 additions & 0 deletions test/modm/math/calendar/datetime_test.hpp
Original file line number Diff line number Diff line change
@@ -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 <unittest/testsuite.hpp>

/// @ingroup modm_test_test_math
class DateTimeTest : public unittest::TestSuite
{
public:
void
testConversion();
};
9 changes: 8 additions & 1 deletion test/modm/math/module.lb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <charconv>
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 <charconv>
patterns += ["*calendar*"]
env.copy('.', ignore=env.ignore_patterns(*patterns))

0 comments on commit 2a2e8c9

Please sign in to comment.