diff --git a/include/asterisk/time.h b/include/asterisk/time.h index f49d68951bd54583e0d44ba28788727fb882e3ea..aec6c7db2bb735b49fad45f1f7b1daa912bdb801 100644 --- a/include/asterisk/time.h +++ b/include/asterisk/time.h @@ -237,4 +237,83 @@ struct timeval ast_samp2tv(unsigned int _nsamp, unsigned int _rate), } ) +/*! + * \brief Time units enumeration. + */ +enum TIME_UNIT { + TIME_UNIT_ERROR = -1, + TIME_UNIT_NANOSECOND, + TIME_UNIT_MICROSECOND, + TIME_UNIT_MILLISECOND, + TIME_UNIT_SECOND, + TIME_UNIT_MINUTE, + TIME_UNIT_HOUR, + TIME_UNIT_DAY, + TIME_UNIT_WEEK, + TIME_UNIT_MONTH, + TIME_UNIT_YEAR, +}; + +/*! + * \brief Convert a string to a time unit enumeration value. + * + * This method attempts to be as flexible, and forgiving as possible when + * converting. In most cases the algorithm will match on the beginning of + * up to three strings (short, medium, long form). So that means if the + * given string at least starts with one of the form values it will match. + * + * For example: us, usec, microsecond will all map to TIME_UNIT_MICROSECOND. + * So will uss, usecs, miscroseconds, or even microsecondvals + * + * Matching is also not case sensitive. + * + * \param unit The string to map to an enumeration + * + * \return A time unit enumeration + */ +enum TIME_UNIT ast_time_str_to_unit(const char *unit); + +/*! + * \brief Convert a timeval structure to microseconds + * + * \param tv The timeval to convert + * + * \return The time in microseconds + */ +ast_suseconds_t ast_time_tv_to_usec(const struct timeval *tv); + +/*! + * \brief Create a timeval object initialized to given values. + * + * \param sec The timeval seconds value + * \param usec The timeval microseconds value + * + * \return A timeval object + */ +struct timeval ast_time_create(ast_time_t sec, ast_suseconds_t usec); + +/*! + * \brief Convert the given unit value, and create a timeval object from it. + * + * \param val The value to convert to a timeval + * \param unit The time unit type of val + * + * \return A timeval object + */ +struct timeval ast_time_create_by_unit(unsigned long val, enum TIME_UNIT unit); + +/*! + * \brief Convert the given unit value, and create a timeval object from it. + * + * This will first attempt to convert the unit from a string to a TIME_UNIT + * enumeration. If that conversion fails then a zeroed out timeval object + * is returned. + * + * \param val The value to convert to a timeval + * \param unit The time unit type of val + * + * \return A timeval object + */ +struct timeval ast_time_create_by_unit_str(unsigned long val, const char *unit); + #endif /* _ASTERISK_TIME_H */ diff --git a/main/time.c b/main/time.c new file mode 100644 index 0000000000000000000000000000000000000000..b2a7868b3d528246043f67308f81d7bf0abb1f94 --- /dev/null +++ b/main/time.c @@ -0,0 +1,145 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Sangoma Technologies Corporation + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Date/Time utility functions + */ + +/*** MODULEINFO + <support_level>core</support_level> + ***/ + +#include <inttypes.h> +#include <string.h> +#include <time.h> + +#include "asterisk/time.h" + +const char *nanosecond_labels[] = {"ns", "nsec", "nanosecond"}; +const char *microsecond_labels[] = {"us", "usec", "microsecond"}; +const char *millisecond_labels[] = {"ms", "msec", "millisecond"}; +const char *second_labels[] = {"s", "sec", "second"}; +const char *minute_labels[] = {"m", "min", "minute"}; +const char *hour_labels[] = {"h", "hr", "hour"}; +const char *day_labels[] = {"d", "", "day"}; +const char *week_labels[] = {"w", "wk", "week"}; +const char *month_labels[] = {"mo", "mth", "month"}; +const char *year_labels[] = {"y", "yr", "year"}; + +#define MAX_UNIT_LABELS 3 + +struct time_unit_labels { + enum TIME_UNIT unit; + const char **values; +}; + +static struct time_unit_labels unit_labels[] = { + { TIME_UNIT_NANOSECOND, nanosecond_labels }, + { TIME_UNIT_MICROSECOND, microsecond_labels }, + { TIME_UNIT_MILLISECOND, millisecond_labels }, + { TIME_UNIT_MONTH, month_labels }, /* Here so "mo" matches before "m" */ + { TIME_UNIT_SECOND, second_labels }, + { TIME_UNIT_MINUTE, minute_labels }, + { TIME_UNIT_HOUR, hour_labels }, + { TIME_UNIT_DAY, day_labels }, + { TIME_UNIT_WEEK, week_labels }, + { TIME_UNIT_YEAR, year_labels }, +}; + +const unsigned int unit_labels_size = sizeof(unit_labels) / sizeof(0[unit_labels]); + +enum TIME_UNIT ast_time_str_to_unit(const char *unit) +{ + size_t i, j; + + if (!unit) { + return TIME_UNIT_ERROR; + } + + for (i = 0; i < unit_labels_size; ++i) { + for (j = 0; j < MAX_UNIT_LABELS; ++j) { + /* + * A lazy pluralization check. If the given unit string at least starts + * with a label assume a match. + */ + if (*unit_labels[i].values[j] && !strncasecmp(unit, unit_labels[i].values[j], + strlen(unit_labels[i].values[j]))) { + return unit_labels[i].unit; + } + } + } + + return TIME_UNIT_ERROR; +} + +ast_suseconds_t ast_time_tv_to_usec(const struct timeval *tv) +{ + return tv->tv_sec * 1000000 + tv->tv_usec; +} + +struct timeval ast_time_create(ast_time_t sec, ast_suseconds_t usec) +{ + return ast_tv(sec, usec); +} + +/*! + * \brief Create a timeval first onverting the given microsecond value + * into seconds and microseconds + * + * \param usec microsecond value + * + * \return A timeval structure + */ +static struct timeval normalize_and_create(unsigned long usec) +{ + return ast_time_create(usec / 1000000, usec % 1000000); +} + +struct timeval ast_time_create_by_unit(unsigned long val, enum TIME_UNIT unit) +{ + switch (unit) { + case TIME_UNIT_NANOSECOND: + return normalize_and_create(val / 1000); + case TIME_UNIT_MICROSECOND: + return normalize_and_create(val); + case TIME_UNIT_MILLISECOND: + return normalize_and_create(val * 1000); + case TIME_UNIT_SECOND: + return ast_time_create(val, 0); + case TIME_UNIT_MINUTE: + return ast_time_create(val * 60, 0); + case TIME_UNIT_HOUR: + return ast_time_create(val * 3600, 0); + case TIME_UNIT_DAY: + return ast_time_create(val * 86400, 0); + case TIME_UNIT_WEEK: + return ast_time_create(val * 604800, 0); + case TIME_UNIT_MONTH: + /* Using Gregorian mean month - 30.436875 * 86400 */ + return ast_time_create(val * 2629746, 0); + case TIME_UNIT_YEAR: + /* Using Gregorian year - 365.2425 * 86400 */ + return ast_time_create(val * 31556952, 0); + default: + return ast_time_create(0, 0); + } +} + +struct timeval ast_time_create_by_unit_str(unsigned long val, const char *unit) +{ + return ast_time_create_by_unit(val, ast_time_str_to_unit(unit)); +} diff --git a/tests/test_time.c b/tests/test_time.c index b58a4732b0861632a10337308dc7b74f906a9f3c..ceb2ae2a5342a74e413b2e6bcec16d16e1be8cee 100644 --- a/tests/test_time.c +++ b/tests/test_time.c @@ -111,8 +111,174 @@ AST_TEST_DEFINE(test_timezone_watch) return res; } +AST_TEST_DEFINE(test_time_str_to_unit) +{ + switch (cmd) { + case TEST_INIT: + info->name = "time_str_to_unit"; + info->category = "/main/stdtime/"; + info->summary = "Verify string to time unit conversions"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + /* Nominal */ + ast_test_validate(test, ast_time_str_to_unit("ns") == TIME_UNIT_NANOSECOND); + ast_test_validate(test, ast_time_str_to_unit("us") == TIME_UNIT_MICROSECOND); + ast_test_validate(test, ast_time_str_to_unit("ms") == TIME_UNIT_MILLISECOND); + ast_test_validate(test, ast_time_str_to_unit("s") == TIME_UNIT_SECOND); + ast_test_validate(test, ast_time_str_to_unit("m") == TIME_UNIT_MINUTE); + ast_test_validate(test, ast_time_str_to_unit("h") == TIME_UNIT_HOUR); + ast_test_validate(test, ast_time_str_to_unit("d") == TIME_UNIT_DAY); + ast_test_validate(test, ast_time_str_to_unit("w") == TIME_UNIT_WEEK); + ast_test_validate(test, ast_time_str_to_unit("mo") == TIME_UNIT_MONTH); + ast_test_validate(test, ast_time_str_to_unit("y") == TIME_UNIT_YEAR); + + /* Plural */ + ast_test_validate(test, ast_time_str_to_unit("nanoseconds") == TIME_UNIT_NANOSECOND); + ast_test_validate(test, ast_time_str_to_unit("microseconds") == TIME_UNIT_MICROSECOND); + ast_test_validate(test, ast_time_str_to_unit("milliseconds") == TIME_UNIT_MILLISECOND); + ast_test_validate(test, ast_time_str_to_unit("seconds") == TIME_UNIT_SECOND); + ast_test_validate(test, ast_time_str_to_unit("minutes") == TIME_UNIT_MINUTE); + ast_test_validate(test, ast_time_str_to_unit("hours") == TIME_UNIT_HOUR); + ast_test_validate(test, ast_time_str_to_unit("days") == TIME_UNIT_DAY); + ast_test_validate(test, ast_time_str_to_unit("weeks") == TIME_UNIT_WEEK); + ast_test_validate(test, ast_time_str_to_unit("months") == TIME_UNIT_MONTH); + ast_test_validate(test, ast_time_str_to_unit("years") == TIME_UNIT_YEAR); + + /* Case */ + ast_test_validate(test, ast_time_str_to_unit("Nsec") == TIME_UNIT_NANOSECOND); + ast_test_validate(test, ast_time_str_to_unit("Usec") == TIME_UNIT_MICROSECOND); + ast_test_validate(test, ast_time_str_to_unit("Msec") == TIME_UNIT_MILLISECOND); + ast_test_validate(test, ast_time_str_to_unit("Sec") == TIME_UNIT_SECOND); + ast_test_validate(test, ast_time_str_to_unit("Min") == TIME_UNIT_MINUTE); + ast_test_validate(test, ast_time_str_to_unit("Hr") == TIME_UNIT_HOUR); + ast_test_validate(test, ast_time_str_to_unit("Day") == TIME_UNIT_DAY); + ast_test_validate(test, ast_time_str_to_unit("Wk") == TIME_UNIT_WEEK); + ast_test_validate(test, ast_time_str_to_unit("Mth") == TIME_UNIT_MONTH); + ast_test_validate(test, ast_time_str_to_unit("Yr") == TIME_UNIT_YEAR); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(test_time_create_by_unit) +{ + struct timeval tv; + + switch (cmd) { + case TEST_INIT: + info->name = "time_create_by_unit"; + info->category = "/main/stdtime/"; + info->summary = "Verify unit value to timeval conversions"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + /* Nominal */ + ast_test_validate(test, ast_time_create_by_unit(1000, TIME_UNIT_NANOSECOND).tv_usec == 1); + ast_test_validate(test, ast_time_create_by_unit(1, TIME_UNIT_MICROSECOND).tv_usec == 1); + ast_test_validate(test, ast_time_create_by_unit(1, TIME_UNIT_MILLISECOND).tv_usec == 1000); + ast_test_validate(test, ast_time_create_by_unit(1, TIME_UNIT_SECOND).tv_sec == 1); + ast_test_validate(test, ast_time_create_by_unit(1, TIME_UNIT_MINUTE).tv_sec == 60); + ast_test_validate(test, ast_time_create_by_unit(1, TIME_UNIT_HOUR).tv_sec == 3600); + ast_test_validate(test, ast_time_create_by_unit(1, TIME_UNIT_DAY).tv_sec == 86400); + ast_test_validate(test, ast_time_create_by_unit(1, TIME_UNIT_WEEK).tv_sec == 604800); + ast_test_validate(test, ast_time_create_by_unit(1, TIME_UNIT_MONTH).tv_sec == 2629746); + ast_test_validate(test, ast_time_create_by_unit(1, TIME_UNIT_YEAR).tv_sec == 31556952); + + /* timeval normalization */ + tv = ast_time_create_by_unit(1500000000, TIME_UNIT_NANOSECOND); + ast_test_validate(test, tv.tv_sec == 1 && tv.tv_usec == 500000); + + tv = ast_time_create_by_unit(1500000, TIME_UNIT_MICROSECOND); + ast_test_validate(test, tv.tv_sec == 1 && tv.tv_usec == 500000); + + tv = ast_time_create_by_unit(1500, TIME_UNIT_MILLISECOND); + ast_test_validate(test, tv.tv_sec == 1 && tv.tv_usec == 500000); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(test_time_create_by_unit_str) +{ + struct timeval tv; + + switch (cmd) { + case TEST_INIT: + info->name = "time_create_by_unit_str"; + info->category = "/main/stdtime/"; + info->summary = "Verify value with unit as a string to timeval conversions"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + /* Nominal */ + ast_test_validate(test, ast_time_create_by_unit_str(1000, "ns").tv_usec == 1); + ast_test_validate(test, ast_time_create_by_unit_str(1, "us").tv_usec == 1); + ast_test_validate(test, ast_time_create_by_unit_str(1, "ms").tv_usec == 1000); + ast_test_validate(test, ast_time_create_by_unit_str(1, "s").tv_sec == 1); + ast_test_validate(test, ast_time_create_by_unit_str(1, "m").tv_sec == 60); + ast_test_validate(test, ast_time_create_by_unit_str(1, "h").tv_sec == 3600); + ast_test_validate(test, ast_time_create_by_unit_str(1, "d").tv_sec == 86400); + ast_test_validate(test, ast_time_create_by_unit_str(1, "w").tv_sec == 604800); + ast_test_validate(test, ast_time_create_by_unit_str(1, "mo").tv_sec == 2629746); + ast_test_validate(test, ast_time_create_by_unit_str(1, "yr").tv_sec == 31556952); + + /* timeval normalization */ + tv = ast_time_create_by_unit_str(1500000000, "ns"); + ast_test_validate(test, tv.tv_sec == 1 && tv.tv_usec == 500000); + + tv = ast_time_create_by_unit_str(1500000, "us"); + ast_test_validate(test, tv.tv_sec == 1 && tv.tv_usec == 500000); + + tv = ast_time_create_by_unit_str(1500, "ms"); + ast_test_validate(test, tv.tv_sec == 1 && tv.tv_usec == 500000); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(test_time_tv_to_usec) +{ + struct timeval tv; + + switch (cmd) { + case TEST_INIT: + info->name = "time_tv_to_usec"; + info->category = "/main/stdtime/"; + info->summary = "Verify conversion of a timeval structure to microseconds"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + tv = ast_time_create(0, 0); + ast_test_validate(test, ast_time_tv_to_usec(&tv) == 0); + + tv = ast_time_create(0, 1); + ast_test_validate(test, ast_time_tv_to_usec(&tv) == 1); + + tv = ast_time_create(1, 0); + ast_test_validate(test, ast_time_tv_to_usec(&tv) == 1000000); + + tv = ast_time_create(1, 1); + ast_test_validate(test, ast_time_tv_to_usec(&tv) == 1000001); + + return AST_TEST_PASS; +} + static int unload_module(void) { + AST_TEST_UNREGISTER(test_time_create_by_unit_str); + AST_TEST_UNREGISTER(test_time_create_by_unit); + AST_TEST_UNREGISTER(test_time_str_to_unit); + AST_TEST_UNREGISTER(test_time_tv_to_usec); AST_TEST_UNREGISTER(test_timezone_watch); return 0; } @@ -120,6 +286,10 @@ static int unload_module(void) static int load_module(void) { AST_TEST_REGISTER(test_timezone_watch); + AST_TEST_REGISTER(test_time_tv_to_usec); + AST_TEST_REGISTER(test_time_str_to_unit); + AST_TEST_REGISTER(test_time_create_by_unit); + AST_TEST_REGISTER(test_time_create_by_unit_str); return AST_MODULE_LOAD_SUCCESS; }