Skip to content

Commit 05a11a6

Browse files
committed
[math] Add :math:calendar module with DateTime class
1 parent f3eb719 commit 05a11a6

File tree

5 files changed

+452
-1
lines changed

5 files changed

+452
-1
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
/*
2+
* Copyright (c) 2024, Niklas Hauser
3+
*
4+
* This file is part of the modm project.
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public
7+
* License, v. 2.0. If a copy of the MPL was not distributed with this
8+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
9+
*/
10+
// ----------------------------------------------------------------------------
11+
12+
#pragma once
13+
14+
#include <chrono>
15+
#include <ctime>
16+
#include <string_view>
17+
#include <cstring>
18+
#include <sys/time.h>
19+
#include <charconv>
20+
21+
namespace modm
22+
{
23+
24+
/// Efficient representation of a date and time
25+
/// @ingroup modm_math_calendar
26+
class DateTime
27+
{
28+
public:
29+
using duration = std::chrono::milliseconds;
30+
using precision = typename duration::period;
31+
32+
constexpr DateTime() = default;
33+
34+
/// This is an efficient conversion.
35+
constexpr explicit
36+
DateTime(uint16_t year, uint8_t month, uint8_t day,
37+
uint8_t hour = 0, uint8_t minute = 0, uint8_t second = 0,
38+
uint16_t millisecond = 0, uint8_t weekday = 0)
39+
: data(year - epoch, month, day, hour, minute, second, millisecond), _weekday(weekday)
40+
{}
41+
42+
/// This computes the weekday from the date, which is somewhat expensive
43+
constexpr explicit
44+
DateTime(const std::chrono::year_month_day& ymd,
45+
const std::chrono::hours& hours = std::chrono::hours::zero(),
46+
const std::chrono::minutes& minutes = std::chrono::minutes::zero(),
47+
const std::chrono::seconds& seconds = std::chrono::seconds::zero(),
48+
const std::chrono::milliseconds& subseconds = std::chrono::milliseconds::zero())
49+
: DateTime(uint16_t(int(ymd.year())), uint8_t(unsigned(ymd.month())), uint8_t(unsigned(ymd.day())),
50+
uint8_t(hours.count()), uint8_t(minutes.count()), uint8_t(seconds.count()),
51+
uint16_t(subseconds.count()), std::chrono::weekday{ymd}.c_encoding())
52+
{}
53+
54+
constexpr std::chrono::year
55+
year() const
56+
{ return std::chrono::year{epoch + data.year}; }
57+
58+
constexpr std::chrono::month
59+
month() const
60+
{ return std::chrono::month{data.month}; }
61+
62+
constexpr std::chrono::day
63+
day() const
64+
{ return std::chrono::day{data.day}; }
65+
66+
/// This is an efficient conversion.
67+
constexpr std::chrono::year_month_day
68+
year_month_day() const
69+
{ return std::chrono::year_month_day{year(), month(), day()}; }
70+
71+
constexpr std::chrono::weekday
72+
weekday() const
73+
{ return std::chrono::weekday{_weekday}; }
74+
75+
constexpr std::chrono::days
76+
day_of_year() const
77+
{
78+
uint16_t yday = m2d[data.month] + data.day - 1u;
79+
if ((data.year & 0b11) == 0b10 and data.month > 2u) yday++;
80+
return std::chrono::days{yday};
81+
}
82+
83+
84+
constexpr std::chrono::hours
85+
hours() const
86+
{ return std::chrono::hours{data.hour}; }
87+
88+
constexpr std::chrono::minutes
89+
minutes() const
90+
{ return std::chrono::minutes{data.minute}; }
91+
92+
constexpr std::chrono::seconds
93+
seconds() const
94+
{ return std::chrono::seconds{data.second}; }
95+
96+
constexpr std::chrono::milliseconds
97+
subseconds() const
98+
{ return std::chrono::milliseconds{data.millisecond}; }
99+
100+
/// @warning This function is *very* inefficient due to an unnecessary conversion from hh:mm:ss.ms to ms
101+
/// then back to hh:mm:ss.ms in the constructor. This is a limitation of the stdc++ constructor.
102+
constexpr std::chrono::hh_mm_ss<duration>
103+
hh_mm_ss() const
104+
{
105+
uint32_t ms = ((data.hour * 60ul + data.minute) * 60ul + data.second) * 1000ul + data.millisecond;
106+
return std::chrono::hh_mm_ss{duration{ms}};
107+
}
108+
109+
/// This is an efficient conversion.
110+
constexpr std::tm
111+
tm() const
112+
{
113+
std::tm tm{};
114+
115+
tm.tm_sec = data.second;
116+
tm.tm_min = data.minute;
117+
tm.tm_hour = data.hour;
118+
119+
tm.tm_mday = data.day; // 1-indexed
120+
tm.tm_mon = data.month - 1u; // 0-indexed
121+
tm.tm_year = data.year + epoch - 1900u;
122+
123+
tm.tm_wday = weekday().c_encoding(); // 0-indexed
124+
125+
tm.tm_yday = day_of_year().count(); // 0-indexed
126+
127+
return tm;
128+
}
129+
130+
/// @warning This function is inefficient since it always converts the datetime to seconds.
131+
constexpr std::time_t
132+
time_t() const
133+
{
134+
return (data.year * seconds_per_year +
135+
((data.year + 1u) / 4u + day_of_year().count()) * seconds_per_day +
136+
(data.hour * 60l + data.minute) * 60l + data.second);
137+
}
138+
139+
/// @warning This function is inefficient since it always converts the datetime to microseconds.
140+
constexpr struct timeval
141+
timeval() const
142+
{
143+
return {time_t(), data.millisecond * 1000};
144+
}
145+
146+
/// @warning This function is inefficient since it always converts the datetime to milliseconds.
147+
constexpr duration
148+
time_since_epoch() const
149+
{
150+
return duration{time_t() * 1000 + data.millisecond};
151+
}
152+
153+
154+
constexpr auto operator<=>(const DateTime& other) const
155+
{ return data.value <=> other.data.value; }
156+
157+
constexpr auto operator==(const DateTime& other) const
158+
{ return data.value == other.data.value; }
159+
160+
private:
161+
union Data
162+
{
163+
constexpr Data() = default;
164+
constexpr explicit
165+
Data(uint8_t year, uint8_t month, uint8_t day,
166+
uint8_t hour, uint8_t minute, uint8_t second, uint16_t millisecond)
167+
: millisecond(millisecond), second(second), minute(minute), hour(hour),
168+
day(day), month(month), year(year) {}
169+
struct
170+
{
171+
uint16_t millisecond;
172+
uint8_t second;
173+
uint8_t minute;
174+
uint8_t hour;
175+
176+
uint8_t day;
177+
uint8_t month;
178+
uint8_t year;
179+
} modm_packed;
180+
uint64_t value;
181+
};
182+
183+
Data data{};
184+
uint8_t _weekday{};
185+
186+
static constexpr uint16_t epoch{1970};
187+
static constexpr uint32_t seconds_per_day{24*60*60};
188+
static constexpr uint64_t seconds_per_year{365*seconds_per_day};
189+
// accumulated (non-leap) days per month, 1-indexed!
190+
static constexpr uint16_t m2d[] = {0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
191+
192+
public:
193+
/// Efficient conversion
194+
static constexpr DateTime
195+
from_tm(const std::tm& tm)
196+
{
197+
return DateTime(uint16_t(tm.tm_year + 1900), uint8_t(tm.tm_mon + 1), uint8_t(tm.tm_mday),
198+
uint8_t(tm.tm_hour), uint8_t(tm.tm_min), uint8_t(tm.tm_sec), 0u, uint8_t(tm.tm_wday));
199+
}
200+
201+
/// Really expensive conversion!
202+
static constexpr DateTime
203+
from_time_t(std::time_t tt, const std::chrono::milliseconds& subseconds = std::chrono::milliseconds::zero())
204+
{
205+
const auto seconds = std::chrono::seconds(tt);
206+
const auto days = std::chrono::floor<std::chrono::days>(seconds);
207+
const auto ymd = std::chrono::year_month_day(std::chrono::sys_days(days));
208+
const auto hms = std::chrono::hh_mm_ss(seconds - days);
209+
return DateTime(ymd, hms.hours(), hms.minutes(), hms.seconds(), subseconds);
210+
}
211+
212+
/// Really expensive conversion!
213+
static constexpr DateTime
214+
from_timeval(const struct timeval& tv)
215+
{
216+
return from_time_t(std::time_t(tv.tv_sec), std::chrono::milliseconds(tv.tv_usec / 1000ul));
217+
}
218+
219+
static consteval DateTime
220+
fromBuildTime()
221+
{
222+
// Example: "Mon Dec 23 17:45:35 2024"
223+
const std::string_view timestamp{__TIMESTAMP__};
224+
const auto to_uint = [=](uint8_t offset, uint8_t length) -> uint16_t
225+
{
226+
const auto str = timestamp.substr(offset, length);
227+
int integer;
228+
(void) std::from_chars(str.begin(), str.end(), integer);
229+
return uint16_t(integer);
230+
};
231+
// All easy to parse integers
232+
const uint16_t cyear{to_uint(20, 4)};
233+
const auto cday{uint8_t(to_uint(8, 2))};
234+
const auto chour{uint8_t(to_uint(11, 2))};
235+
const auto cminute{uint8_t(to_uint(14, 2))};
236+
const auto csecond{uint8_t(to_uint(17, 2))};
237+
238+
// Annoying to parse strings
239+
const std::string_view months[]
240+
{"", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
241+
uint8_t cmonth{1u};
242+
while (months[cmonth] != timestamp.substr(4, 3) and cmonth <= 12u) ++cmonth;
243+
244+
const std::string_view weekdays[]
245+
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
246+
uint8_t cweekday{};
247+
while (weekdays[cweekday] != timestamp.substr(0, 3) and cweekday < 7u) ++cweekday;
248+
249+
return DateTime{cyear, cmonth, cday, chour, cminute, csecond, 0, cweekday};
250+
}
251+
};
252+
253+
} // namespace modm
254+
255+
#if MODM_HAS_IOSTREAM
256+
#include <inttypes.h>
257+
#include <modm/io/iostream.hpp>
258+
259+
namespace modm
260+
{
261+
262+
/// @ingroup modm_math_calendar
263+
inline modm::IOStream&
264+
operator << (modm::IOStream& s, const DateTime& dt)
265+
{
266+
// ISO encoding: 2024-12-22 18:39:21.342
267+
s.printf("%04" PRIu16 "-%02" PRIu8 "-%02" PRIu8 " %02" PRIu8 ":%02" PRIu8 ":%02" PRIu8 ".%03" PRIu16,
268+
uint16_t(int(dt.year())), uint8_t(unsigned(dt.month())), uint8_t(unsigned(dt.day())),
269+
uint8_t(dt.hours().count()), uint8_t(dt.minutes().count()), uint8_t(dt.seconds().count()),
270+
uint16_t(dt.subseconds().count()));
271+
return s;
272+
}
273+
274+
} // modm namespace
275+
276+
#endif

src/modm/math/calendar/module.lb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (c) 2024, Niklas Hauser
5+
#
6+
# This file is part of the modm project.
7+
#
8+
# This Source Code Form is subject to the terms of the Mozilla Public
9+
# License, v. 2.0. If a copy of the MPL was not distributed with this
10+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
11+
# -----------------------------------------------------------------------------
12+
13+
def init(module):
14+
module.name = ":math:calendar"
15+
module.description = "Calendar Operations"
16+
17+
def prepare(module, options):
18+
module.depends(":stdc++")
19+
# AVR libstdc++ has no support for calendar/time in <chrono>
20+
return options[":target"].identifier.platform != "avr"
21+
22+
def build(env):
23+
env.outbasepath = "modm/src/modm/math/calendar"
24+
env.copy(".")

0 commit comments

Comments
 (0)