diff --git a/sys/include/phydat.h b/sys/include/phydat.h index db8036a06ccff..0743c5c56fa0c 100644 --- a/sys/include/phydat.h +++ b/sys/include/phydat.h @@ -288,6 +288,35 @@ void phydat_fit(phydat_t *dat, const int32_t *values, unsigned int dim); */ size_t phydat_to_json(const phydat_t *data, size_t dim, char *buf); +/** + * @brief Convert a date and time contained in phydat structs to a Unix timestamp. + * See phydat_unix() for the date notation and peculiarities. + * + * @param date Date to use in the timestamp. + * @param time Time to use in the timestamp. + * @param offset_seconds Timezone offset in seconds to use in the timestamp. + * + * @return A unix timestamp + */ +int64_t phydat_date_time_to_unix(phydat_t *date, phydat_t *time, int32_t offset_seconds); + +/** + * @brief Convert a date and time (per ISO8601) to a Unix timestamp (seconds since 1970). + * + * @param year Year in the Common Era (CE). Note that 0 is 1 BCE, 1 is 2 BCE, etc. + * @param month Month of the year. + * @param day Day of the month. + * @param hour Hour of the day. + * @param minute Minute of the hour. + * @param second Second of the minute. + * @param offset Timezone offset in seconds. + * + * @return A Unix timestamp (seconds since 1970). + */ +int64_t phydat_unix(int16_t year, int16_t month, int16_t day, + int16_t hour, int16_t minute, int16_t second, + int32_t offset); + #ifdef __cplusplus } #endif diff --git a/sys/phydat/phydat_unix.c b/sys/phydat/phydat_unix.c new file mode 100644 index 0000000000000..aeee0450b671a --- /dev/null +++ b/sys/phydat/phydat_unix.c @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 Silke Hofstra + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +#include +#include + +#include "phydat.h" + +/** + * Offsets of the first day of the month starting with January. + * Months after February have a negative offset to efficiently handle leap years. + */ +static int16_t month_to_yday[] = { 0, 31, -306, -275, -245, -214, + -184, -153, -122, -92, -61, -31 }; + +static inline int16_t phydat_unscale(int16_t value, int16_t scale) +{ + if (scale > 0) { + return value * pow(10, scale); + } + + if (scale < 0) { + return value / pow(10, -scale); + } + + return value; +} + +int64_t phydat_date_time_to_unix(phydat_t *date, phydat_t *time, int32_t offset_seconds) +{ + return phydat_unix( + phydat_unscale(date->val[2], date->scale), + phydat_unscale(date->val[1], date->scale), + phydat_unscale(date->val[0], date->scale), + phydat_unscale(time->val[2], time->scale), + phydat_unscale(time->val[1], time->scale), + phydat_unscale(time->val[0], time->scale), + offset_seconds); +} + +int64_t phydat_unix(int16_t year, int16_t month, int16_t day, + int16_t hour, int16_t minute, int16_t second, + int32_t offset_seconds) +{ + /* Make the year relative to 1900. */ + /* Add a year for months after Feb to deal with leap years. */ + year += (month > 2 ? 1 : 0) - 1900; + + /* Calculate the day of the year based on the month */ + day += month_to_yday[(month - 1) % 12] - 1; + + /* POSIX calculation of a UNIX timestamp. */ + /* See section 4.16 of The Open Group Base Specifications Issue 7. */ + int16_t leap_days = ((year - 69) / 4) - ((year - 1) / 100) + ((year + 299) / 400); + + return (int64_t)(day + leap_days) * 86400 + + (int64_t)(year - 70) * 31536000 + + (int64_t)(hour) * 3600 + + (int64_t)(minute) * 60 + + (int64_t)(second) - + offset_seconds; +} diff --git a/tests/phydat_unix/Makefile b/tests/phydat_unix/Makefile new file mode 100644 index 0000000000000..a9b130fdfc062 --- /dev/null +++ b/tests/phydat_unix/Makefile @@ -0,0 +1,6 @@ +include ../Makefile.tests_common + +USEMODULE += phydat +USEMODULE += embunit + +include $(RIOTBASE)/Makefile.include diff --git a/tests/phydat_unix/Makefile.ci b/tests/phydat_unix/Makefile.ci new file mode 100644 index 0000000000000..89a09fdb8c991 --- /dev/null +++ b/tests/phydat_unix/Makefile.ci @@ -0,0 +1 @@ +BOARD_INSUFFICIENT_MEMORY := diff --git a/tests/phydat_unix/app.config.test b/tests/phydat_unix/app.config.test new file mode 100644 index 0000000000000..b71b9c3d1366d --- /dev/null +++ b/tests/phydat_unix/app.config.test @@ -0,0 +1,2 @@ +CONFIG_MODULE_PHYDAT=y +CONFIG_MODULE_EMBUNIT=y diff --git a/tests/phydat_unix/main.c b/tests/phydat_unix/main.c new file mode 100644 index 0000000000000..6b145a319480a --- /dev/null +++ b/tests/phydat_unix/main.c @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2023 Silke Hofstra + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Phydat Unix timestamp tests + * + * @author Silke Hofstra + * + * @} + */ + +#include +#include +#include + +#include "phydat.h" +#include "embUnit.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +#ifndef PRIi64 +#define PRIi64 "lli" +#endif + +typedef struct { + phydat_t date; + phydat_t time; + int32_t offset; + int64_t ts; +} test_t; + +static test_t tests[] = { + /* Test various ways of writing 0 */ + { + .date = { { 1, 1, 1970 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = 0, + }, + { + .date = { { 1, 1, 1970 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 2 }, UNIT_TIME, 0 }, + .offset = 7200, // UTC +0200 + .ts = 0, + }, + { + .date = { { 31, 12, 1969 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 22 }, UNIT_TIME, 0 }, + .offset = -7200, // UTC -0200 + .ts = 0, + }, + { + .date = { { 1, 1, 1970 }, UNIT_DATE, 0 }, + .time = { { 3600, 60, 0 }, UNIT_TIME, 0 }, + .offset = 7200, // UTC +0200 + .ts = 0, + }, + { + .date = { { 31, 12, 1969 }, UNIT_DATE, 0 }, + .time = { { 3600, 120, 19 }, UNIT_TIME, 0 }, + .offset = -7200, // UTC -0200 + .ts = 0, + }, + + /* Test well-known dates */ + { + .date = { { 28, 4, 2021 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = 1619568000, + }, + { + .date = { { 29, 2, 2020 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = 1582934400, + }, + { + .date = { { 1, 3, 2020 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = 1583020800, + }, + + /* Test the first of every month */ + { + .date = { { 1, 1, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2208988800, + }, + { + .date = { { 1, 2, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2206310400, + }, + { + .date = { { 1, 3, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2203891200, + }, + { + .date = { { 1, 4, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2201212800, + }, + { + .date = { { 1, 5, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2198620800, + }, + { + .date = { { 1, 6, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2195942400, + }, + { + .date = { { 1, 7, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2193350400, + }, + { + .date = { { 1, 8, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2190672000, + }, + { + .date = { { 1, 9, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2187993600, + }, + { + .date = { { 1, 10, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2185401600, + }, + { + .date = { { 1, 11, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2182723200, + }, + { + .date = { { 1, 12, 1900 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = -2180131200, + }, + + /* Test scale correction */ + { + .date = { { 1, 1, 197 }, UNIT_DATE, 1 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = 24364800, + }, + { + .date = { { 10, 10, 19700 }, UNIT_DATE, -1 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = 0, + }, + { + .date = { { 1, 1, 1970 }, UNIT_DATE, 0 }, + .time = { { 36, 0, 0 }, UNIT_TIME, 2 }, + .ts = 3600, + }, + + /* An invalid date that might go out of bounds on the day of the year lookup table */ + { + .date = { { 1, 13, 1969 }, UNIT_DATE, 0 }, + .time = { { 0, 0, 0 }, UNIT_TIME, 0 }, + .ts = 0, + }, +}; + +void test_phydat_date_time_to_unix(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(tests); i++) { + int64_t result = phydat_date_time_to_unix( + &(tests[i].date), &(tests[i].time), tests[i].offset); + + int32_t offset_hours = tests[i].offset / 3600; + int32_t offset_minutes = (tests[i].offset % 3600) / 60; + + DEBUG("Datetime: %04" PRIi16 "-%02" PRIi16 "-%02" PRIi16 "e%" PRIi16 " " + "%02" PRIi16 ":%02" PRIi16 ":%02" PRIi16 "e%" PRIi16 " " + "%+03" PRIi32 ":%02" PRIi32 " -> %" PRIi64 "\n", + tests[i].date.val[2], tests[i].date.val[1], tests[i].date.val[0], tests[i].date.scale, + tests[i].time.val[2], tests[i].time.val[1], tests[i].time.val[0], tests[i].time.scale, + offset_hours, offset_minutes, result); + + TEST_ASSERT_EQUAL_INT(tests[i].ts, result); + } +} + +Test *tests_phydat_unix(void) +{ + EMB_UNIT_TESTFIXTURES(fixtures) { + new_TestFixture(test_phydat_date_time_to_unix), + }; + EMB_UNIT_TESTCALLER(senml_tests, NULL, NULL, fixtures); + return (Test *)&senml_tests; +} + +int main(void) +{ + TESTS_START(); + TESTS_RUN(tests_phydat_unix()); + TESTS_END(); + return 0; +} diff --git a/tests/phydat_unix/tests/01-run.py b/tests/phydat_unix/tests/01-run.py new file mode 100755 index 0000000000000..8b4e23f2e7bce --- /dev/null +++ b/tests/phydat_unix/tests/01-run.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2017 Freie Universität Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +import sys +from testrunner import run_check_unittests + + +if __name__ == "__main__": + sys.exit(run_check_unittests())