From 0f81f50e3abe8b637394ef35dac9818fcef4ee25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20H=C3=BC=C3=9Fler?= Date: Tue, 8 Apr 2025 18:26:12 +0200 Subject: [PATCH 1/3] sys/fmt: add functins for ISO 8601 date and time --- sys/fmt/fmt.c | 98 +++++++++++++++++++++++++++++++++++++++++++++++ sys/include/fmt.h | 42 ++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/sys/fmt/fmt.c b/sys/fmt/fmt.c index c02acdf9c6..b961b4511e 100644 --- a/sys/fmt/fmt.c +++ b/sys/fmt/fmt.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "container.h" @@ -472,6 +473,55 @@ size_t fmt_to_lower(char *out, const char *str) return len; } +int fmt_time_tm_iso8601(char out[20], const struct tm *tm, char separator) +{ + assert(out); + assert(tm); + /* The lowest year allowed in ISO 8601 is 0000 (year zero), which represents 1 BCE */ + if ((tm->tm_year < -1900 || tm->tm_year > 9999 - 1900) || + (tm->tm_mon < -1 || tm->tm_mon > 99 - 1) || + (tm->tm_mday < 0 || tm->tm_mday > 99) || + (tm->tm_hour < 0 || tm->tm_hour > 99) || + (tm->tm_min < 0 || tm->tm_min > 99) || + (tm->tm_sec < 0 || tm->tm_sec > 99)) { + return -EINVAL; + } + char *pos = out; + int len; + + len = fmt_u16_dec(pos, tm->tm_year + 1900); + fmt_lpad(pos, len, 4, '0'); + pos += 4; + *pos++ = '-'; + + len = fmt_u16_dec(pos, tm->tm_mon + 1); + fmt_lpad(pos, len, 2, '0'); + pos += 2; + *pos++ = '-'; + + len = fmt_u16_dec(pos, tm->tm_mday); + fmt_lpad(pos, len, 2, '0'); + pos += 2; + *pos++ = separator; + + len = fmt_u16_dec(pos, tm->tm_hour); + fmt_lpad(pos, len, 2, '0'); + pos += 2; + *pos++ = ':'; + + len = fmt_u16_dec(pos, tm->tm_min); + fmt_lpad(pos, len, 2, '0'); + pos += 2; + *pos++ = ':'; + + len = fmt_u16_dec(pos, tm->tm_sec); + fmt_lpad(pos, len, 2, '0'); + pos += 2; + *pos = '\0'; + + return pos - out; +} + uint32_t scn_u32_dec(const char *str, size_t n) { uint32_t res = 0; @@ -567,6 +617,54 @@ ssize_t scn_buf_hex(void *_dest, size_t dest_len, const char *hex, size_t hex_le return len; } +int scn_time_tm_iso8601(struct tm *tm, const char *str, char separator) +{ + assert(tm); + assert(str); + memset(tm, 0, sizeof(*tm)); + tm->tm_isdst = -1; /* undefined */ + + if (!fmt_is_digit(str[0]) || !fmt_is_digit(str[1]) || + !fmt_is_digit(str[2]) || !fmt_is_digit(str[3]) || + str[4] != '-' || + !fmt_is_digit(str[5]) || !fmt_is_digit(str[6]) || + str[7] != '-' || + !fmt_is_digit(str[8]) || !fmt_is_digit(str[9])) { + return -EINVAL; + } + + uint32_t num = scn_u32_dec(str, 4); + tm->tm_year = num - 1900; + num = scn_u32_dec(str + 5, 2); + tm->tm_mon = num - 1; + num = scn_u32_dec(str + 8, 2); + tm->tm_mday = num; + + if (str[10] == '\0') { + /* no time, just date */ + return 10; + } + if (str[10] != separator) { + return -EBADF; + } + if (!fmt_is_digit(str[11]) || !fmt_is_digit(str[12]) || + str[13] != ':' || + !fmt_is_digit(str[14]) || !fmt_is_digit(str[15]) || + str[16] != ':' || + !fmt_is_digit(str[17]) || !fmt_is_digit(str[18])) { + return -EINVAL; + } + + num = scn_u32_dec(str + 11, 2); + tm->tm_hour = num; + num = scn_u32_dec(str + 14, 2); + tm->tm_min = num; + num = scn_u32_dec(str + 17, 2); + tm->tm_sec = num; + + return 19; +} + /* native gets special treatment as native's stdio code is ... special. * And when not building for RIOT, there's no `stdio_write()`. * In those cases, just defer to `printf()`. diff --git a/sys/include/fmt.h b/sys/include/fmt.h index 7cd76ecb1f..259bab78dc 100644 --- a/sys/include/fmt.h +++ b/sys/include/fmt.h @@ -43,6 +43,7 @@ #include #include +#include #include #ifdef __cplusplus @@ -404,6 +405,25 @@ size_t fmt_str(char *out, const char *str); */ size_t fmt_to_lower(char *out, const char *str); +/** + * @brief Format a time structure to an ISO 8601 string + * + * This function does only take care of format validity + * and not of date/time validity. + * + * @param[out] out Pointer to output buffer + * @param[in] tm Pointer to time structure + * @param[in] separator Date and time separator. + * Must be 'T' for ISO 8601 or may be ' ' + * + * @return nr of characters written to (or needed in) @p out + * + * @retval -EINVAL if @p tm is specifying a number for + * year, month, day, hour, minute which would yield + * to an invalid date/time format + */ +int fmt_time_tm_iso8601(char out[20], const struct tm *tm, char separator); + /** * @brief Convert string of decimal digits to uint32 * @@ -455,6 +475,28 @@ uint32_t scn_u32_hex(const char *str, size_t n); */ ssize_t scn_buf_hex(void *dest, size_t dest_len, const char *hex, size_t hex_len); +/** + * @brief Convert an ISO 8601 string to time structure + * + * This function parses a string in the format + * YYYY-MM-DD\HH:MM:SS or YYYY-MM-DD. + * + * A terminating '\0' is not required. + * + * This function does only take care of format validity + * and not of date/time validity. + * + * @param[out] tm Pointer to time structure + * @param[in] str Pointer to string to read from + * @param[in] separator Date and time separator + * + * @return Number of characters read from @p str on success (19 or 10) + * + * @retval -EINVAL if @p str has an invalid format + * @retval -EBADF if the expected separator does not match + */ +int scn_time_tm_iso8601(struct tm *tm, const char *str, char separator); + /** * @brief Print string to stdout * From 29b8af8a85263b167682734a43d9daaeff6afb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20H=C3=BC=C3=9Fler?= Date: Tue, 8 Apr 2025 18:27:25 +0200 Subject: [PATCH 2/3] tests/unittests/tests-fmt: test ISO 8601 format functions --- tests/unittests/tests-fmt/tests-fmt.c | 94 +++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/unittests/tests-fmt/tests-fmt.c b/tests/unittests/tests-fmt/tests-fmt.c index c9d4b45d98..e707e8056f 100644 --- a/tests/unittests/tests-fmt/tests-fmt.c +++ b/tests/unittests/tests-fmt/tests-fmt.c @@ -931,6 +931,98 @@ static void test_fmt_lpad(void) TEST_ASSERT_EQUAL_STRING((char*)string, "xxxx3333"); } +static void test_fmt_time_iso8601(void) +{ + char out[20] = { 0 }; + const char *expected = "2025-04-08T17:40:02"; + + struct tm time = { + .tm_year = 2025 - 1900, + .tm_mon = 4 - 1, + .tm_mday = 8, + .tm_hour = 17, + .tm_min = 40, + .tm_sec = 2 + }; + + TEST_ASSERT_EQUAL_INT(19, fmt_time_tm_iso8601(out, &time, 'T')); + TEST_ASSERT_EQUAL_STRING(expected, out); + expected = "2025-04-08 17:40:02"; + TEST_ASSERT_EQUAL_INT(19, fmt_time_tm_iso8601(out, &time, ' ')); + TEST_ASSERT_EQUAL_STRING(expected, out); + + time.tm_year = -1901; + TEST_ASSERT_EQUAL_INT(-EINVAL, fmt_time_tm_iso8601(out, &time, 'T')); + time.tm_year = 9999 - 1899; + TEST_ASSERT_EQUAL_INT(-EINVAL, fmt_time_tm_iso8601(out, &time, 'T')); +} + +static void test_scn_time_iso8601(void) +{ + struct tm time; + + memset(&time, 0, sizeof(time)); + TEST_ASSERT_EQUAL_INT(19, scn_time_tm_iso8601(&time, "2025-04-08T17:40:02", 'T')); + TEST_ASSERT_EQUAL_INT(2025 - 1900, time.tm_year); + TEST_ASSERT_EQUAL_INT(4 - 1, time.tm_mon); + TEST_ASSERT_EQUAL_INT(8, time.tm_mday); + TEST_ASSERT_EQUAL_INT(17, time.tm_hour); + TEST_ASSERT_EQUAL_INT(40, time.tm_min); + TEST_ASSERT_EQUAL_INT(2, time.tm_sec); + + memset(&time, 0, sizeof(time)); + TEST_ASSERT_EQUAL_INT(19, scn_time_tm_iso8601(&time, "2025-04-08 17:40:02", ' ')); + TEST_ASSERT_EQUAL_INT(2025 - 1900, time.tm_year); + TEST_ASSERT_EQUAL_INT(4 - 1, time.tm_mon); + TEST_ASSERT_EQUAL_INT(8, time.tm_mday); + TEST_ASSERT_EQUAL_INT(17, time.tm_hour); + TEST_ASSERT_EQUAL_INT(40, time.tm_min); + TEST_ASSERT_EQUAL_INT(2, time.tm_sec); + + memset(&time, 0, sizeof(time)); + TEST_ASSERT_EQUAL_INT(10, scn_time_tm_iso8601(&time, "2025-04-08", 'T')); + TEST_ASSERT_EQUAL_INT(2025 - 1900, time.tm_year); + TEST_ASSERT_EQUAL_INT(4 - 1, time.tm_mon); + TEST_ASSERT_EQUAL_INT(8, time.tm_mday); + + memset(&time, 0, sizeof(time)); + TEST_ASSERT_EQUAL_INT(19, scn_time_tm_iso8601(&time, "2025-13-08T17:40:02", 'T')); + TEST_ASSERT_EQUAL_INT(2025 - 1900, time.tm_year); + TEST_ASSERT_EQUAL_INT(13 - 1, time.tm_mon); + TEST_ASSERT_EQUAL_INT(8, time.tm_mday); + TEST_ASSERT_EQUAL_INT(17, time.tm_hour); + TEST_ASSERT_EQUAL_INT(40, time.tm_min); + TEST_ASSERT_EQUAL_INT(2, time.tm_sec); + + memset(&time, 0, sizeof(time)); + TEST_ASSERT_EQUAL_INT(19, scn_time_tm_iso8601(&time, "2025-04-08T17:60:02", 'T')); + TEST_ASSERT_EQUAL_INT(2025 - 1900, time.tm_year); + TEST_ASSERT_EQUAL_INT(4 - 1, time.tm_mon); + TEST_ASSERT_EQUAL_INT(8, time.tm_mday); + TEST_ASSERT_EQUAL_INT(17, time.tm_hour); + TEST_ASSERT_EQUAL_INT(60, time.tm_min); + TEST_ASSERT_EQUAL_INT(2, time.tm_sec); + + memset(&time, 0, sizeof(time)); + TEST_ASSERT_EQUAL_INT(19, scn_time_tm_iso8601(&time, "1899-04-08T17:40:02", 'T')); + TEST_ASSERT_EQUAL_INT(1899 - 1900, time.tm_year); + TEST_ASSERT_EQUAL_INT(4 - 1, time.tm_mon); + TEST_ASSERT_EQUAL_INT(8, time.tm_mday); + TEST_ASSERT_EQUAL_INT(17, time.tm_hour); + TEST_ASSERT_EQUAL_INT(40, time.tm_min); + TEST_ASSERT_EQUAL_INT(2, time.tm_sec); + + memset(&time, 0, sizeof(time)); + TEST_ASSERT_EQUAL_INT(-EBADF, scn_time_tm_iso8601(&time, "2025-04-08T", ' ')); + TEST_ASSERT_EQUAL_INT(-EBADF, scn_time_tm_iso8601(&time, "2025-04-08 17:40:02", 'T')); + TEST_ASSERT_EQUAL_INT(-EINVAL, scn_time_tm_iso8601(&time, "2025-XX-08T17:40:02", 'T')); + TEST_ASSERT_EQUAL_INT(-EINVAL, scn_time_tm_iso8601(&time, "2025-4-08T17:40:02", 'T')); + TEST_ASSERT_EQUAL_INT(-EINVAL, scn_time_tm_iso8601(&time, "2025-04-8T17:40:02", 'T')); + TEST_ASSERT_EQUAL_INT(-EINVAL, scn_time_tm_iso8601(&time, "2025-04-08T17:40:2", 'T')); + TEST_ASSERT_EQUAL_INT(-EINVAL, scn_time_tm_iso8601(&time, "-2025-04-08T17:40:02", 'T')); + TEST_ASSERT_EQUAL_INT(-EINVAL, scn_time_tm_iso8601(&time, "10000-04-08T17:40:02", 'T')); +} + Test *tests_fmt_tests(void) { EMB_UNIT_TESTFIXTURES(fixtures) { @@ -965,6 +1057,8 @@ Test *tests_fmt_tests(void) new_TestFixture(test_scn_u32_hex), new_TestFixture(test_scn_buf_hex), new_TestFixture(test_fmt_lpad), + new_TestFixture(test_fmt_time_iso8601), + new_TestFixture(test_scn_time_iso8601), }; EMB_UNIT_TESTCALLER(fmt_tests, NULL, NULL, fixtures); From d79b27df30ed6cbea994848c18be71817117a340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20H=C3=BC=C3=9Fler?= Date: Tue, 8 Apr 2025 18:28:24 +0200 Subject: [PATCH 3/3] tests/periph/rtc: use time fmt API --- tests/periph/rtc/Makefile | 1 + tests/periph/rtc/main.c | 31 ++++++++++--------------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/tests/periph/rtc/Makefile b/tests/periph/rtc/Makefile index a16ff91cda..f7e94d5d49 100644 --- a/tests/periph/rtc/Makefile +++ b/tests/periph/rtc/Makefile @@ -6,6 +6,7 @@ FEATURES_OPTIONAL += periph_rtc_mem DISABLE_MODULE += periph_init_rtc +USEMODULE += fmt USEMODULE += xtimer # avoid running Kconfig by default diff --git a/tests/periph/rtc/main.c b/tests/periph/rtc/main.c index 3e58133506..d5433fee1c 100644 --- a/tests/periph/rtc/main.c +++ b/tests/periph/rtc/main.c @@ -25,6 +25,7 @@ #include #include +#include "fmt.h" #include "mutex.h" #include "periph_conf.h" #include "periph/rtc.h" @@ -34,31 +35,20 @@ #define PERIOD (2U) #define REPEAT (4U) -#define TM_YEAR_OFFSET (1900) - static unsigned cnt = 0; static void print_time(const char *label, const struct tm *time) { - printf("%s %04d-%02d-%02d %02d:%02d:%02d\n", label, - time->tm_year + TM_YEAR_OFFSET, - time->tm_mon + 1, - time->tm_mday, - time->tm_hour, - time->tm_min, - time->tm_sec); + char tm_buf[20]; + fmt_time_tm_iso8601(tm_buf, time, ' '); + printf("%s %s\n", label, tm_buf); } static void print_time_ms(const char *label, const struct tm *time, uint16_t ms) { - printf("%s %04d-%02d-%02d %02d:%02d:%02d.%03d\n", label, - time->tm_year + TM_YEAR_OFFSET, - time->tm_mon + 1, - time->tm_mday, - time->tm_hour, - time->tm_min, - time->tm_sec, - ms); + char tm_buf[20]; + fmt_time_tm_iso8601(tm_buf, time, ' '); + printf("%s %s.%03d\n", label, tm_buf, ms); } static void inc_secs(struct tm *time, unsigned val) @@ -115,7 +105,6 @@ static void _get_rtc_mem(void) } } - puts("RTC mem OK"); } #else @@ -149,8 +138,8 @@ int main(void) } time = (struct tm){ - .tm_year = 2020 - TM_YEAR_OFFSET, /* years are counted from 1900 */ - .tm_mon = 1, /* 0 = January, 11 = December */ + .tm_year = 2020 - 1900, /* years are counted from 1900 */ + .tm_mon = 1, /* 0 = January, 11 = December */ .tm_mday = 28, .tm_hour = 23, .tm_min = 59, @@ -224,7 +213,7 @@ int main(void) /* set alarm */ rtc_get_time(&time); - inc_secs(&time, PERIOD); + inc_secs(&time, PERIOD); /* note that this increments the seconds above 60! */ rtc_set_alarm(&time, cb, &rtc_mtx); print_time(" Setting alarm to ", &time); puts("");