1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2025-12-14 17:13:50 +01:00

Merge pull request #21391 from fabian18/pr/fmt_time

sys/fmt: add {fmt,scn}_time_tm_iso8601
This commit is contained in:
fabian18 2025-04-10 19:28:17 +00:00 committed by GitHub
commit db0f497c61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 245 additions and 21 deletions

View File

@ -24,6 +24,7 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#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()`.

View File

@ -43,6 +43,7 @@
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include <unistd.h>
#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\<separator\>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
*

View File

@ -6,6 +6,7 @@ FEATURES_OPTIONAL += periph_rtc_mem
DISABLE_MODULE += periph_init_rtc
USEMODULE += fmt
USEMODULE += xtimer
# avoid running Kconfig by default

View File

@ -25,6 +25,7 @@
#include <time.h>
#include <string.h>
#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("");

View File

@ -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);