Commit 891af836 authored by Libor Peltan's avatar Libor Peltan Committed by Daniel Salzman

time: added knot_time_parse() and knot_time_print() functions

parent 5fc62cd4
......@@ -14,6 +14,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <assert.h>
#include <ctype.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "contrib/time.h"
#ifndef HAVE_CLOCK_GETTIME
#include <sys/time.h>
......@@ -56,3 +63,330 @@ double time_diff_ms(const struct timespec *begin, const struct timespec *end)
return (result.tv_sec * 1e3) + (result.tv_nsec / 1e6);
}
typedef struct {
const char *format;
const char *timespec;
const char *parsed;
knot_timediff_t offset;
char offset_sign;
char offset_unit;
struct tm calendar;
int error;
} time_ctx_t;
// After casting (struct tm) to (int []), we can use indexes...
static int calendar_index(char ind)
{
switch (ind) {
case 'Y': return 5;
case 'M': return 4;
case 'D': return 3;
case 'h': return 2;
case 'm': return 1;
case 's': return 0;
default: assert(0); return 6;
}
}
static size_t calendar_digits(int index)
{
return index == 5 ? 4 : 2;
}
static size_t unit_value(char unit)
{
size_t val = 1;
switch (unit) {
case 'M':
return 3600 * 24 * 30;
case 'Y':
val *= 365;
case 'D':
val *= 24;
case 'h':
val *= 60;
case 'm':
val *= 60;
case 's':
default:
return val;
}
}
static knot_time_t time_ctx_finalize(time_ctx_t *ctx)
{
if (ctx->offset_sign) {
ctx->offset *= unit_value(ctx->offset_unit);
return knot_time_add(knot_time(), (ctx->offset_sign == '-' ? -1 : 1) * ctx->offset);
} else if (ctx->offset) {
return (knot_time_t)ctx->offset;
} else if (ctx->calendar.tm_year != 0) {
ctx->calendar.tm_isdst = -1;
ctx->calendar.tm_year -= 1900;
ctx->calendar.tm_mon -= 1;
return (knot_time_t)mktime(&ctx->calendar);
} else {
return (knot_time_t)0;
}
}
static void time_ctx_reset(time_ctx_t *ctx)
{
ctx->parsed = ctx->timespec;
ctx->offset = 0;
ctx->offset_sign = 0;
memset(&ctx->calendar, 0, sizeof(ctx->calendar));
ctx->error = 0;
}
static void parse_quote(time_ctx_t *ctx)
{
while (*ctx->format != '|' && *ctx->format != '\0') {
if (*ctx->format == '\'') {
ctx->format++;
return;
}
if (*ctx->format++ != *ctx->parsed++) {
ctx->error = -1;
return;
}
}
ctx->error = -2;
return;
}
static void parse_offset(time_ctx_t *ctx)
{
ctx->offset = 0;
ctx->error = -1;
while (isdigit(*ctx->parsed)) {
ctx->offset *= 10;
ctx->offset += *ctx->parsed++ - '0';
ctx->error = 0;
}
}
static void parse_calendar(time_ctx_t *ctx, int index)
{
int *cal_arr = (int *)&ctx->calendar;
cal_arr[index] = 0;
for (size_t i = 0; i < calendar_digits(index); i++) {
if (!isdigit(*ctx->parsed)) {
ctx->error = -1;
return;
}
cal_arr[index] *= 10;
cal_arr[index] += *ctx->parsed++ - '0';
}
}
static void parse_sign(time_ctx_t *ctx)
{
char sign1 = *(ctx->format - 1), sign2 = *ctx->format;
bool use_sign2 = (sign2 == '+' || sign2 == '-');
bool allow_plus = (sign1 == '+' || (sign1 == '-' && sign2 == '+'));
bool allow_minus = (sign1 == '-' || (sign1 == '+' && sign2 == '-'));
assert(sign1 == '+' || sign1 == '-');
if ((*ctx->parsed == '+' && allow_plus) || (*ctx->parsed == '-' && allow_minus)) {
ctx->offset_sign = *ctx->parsed++;
ctx->format += (use_sign2 ? 1 : 0);
} else {
ctx->error = -11;
}
}
static void parse_unit1(time_ctx_t *ctx)
{
char u = *ctx->parsed++;
switch (u) {
case 'Y':
case 'M':
case 'D':
case 'h':
case 'm':
case 's':
ctx->offset_unit = u;
break;
default:
ctx->error = -1;
}
}
static void parse_unit2(time_ctx_t *ctx)
{
char u = *ctx->parsed++;
switch (u) {
case 'y':
case 'd':
ctx->offset_unit = toupper(u);
break;
case 'h':
case 's':
ctx->offset_unit = u;
break;
case 'm':
switch (*ctx->parsed++) {
case 'o':
ctx->offset_unit = 'M';
break;
case 'i':
ctx->offset_unit = 'm';
break;
default:
ctx->error = -1;
}
break;
default:
ctx->error = -1;
}
}
int knot_time_parse(const char *format, const char *timespec, knot_time_t *time)
{
if (format == NULL || timespec == NULL || time == NULL) {
return -1;
}
time_ctx_t ctx = {
.format = format,
.timespec = timespec,
.parsed = timespec,
.offset = 0,
.offset_sign = 0,
// we hope that .calendar is zeroed by default
.error = 0,
};
while (ctx.error == 0 && *ctx.format != '\0') {
switch (*ctx.format++) {
case '|':
if (*ctx.parsed == '\0') {
*time = time_ctx_finalize(&ctx);
return 0;
} else {
time_ctx_reset(&ctx);
}
break;
case '\'':
parse_quote(&ctx);
break;
case '#':
parse_offset(&ctx);
break;
case 'Y':
case 'M':
case 'D':
case 'h':
case 'm':
case 's':
parse_calendar(&ctx, calendar_index(*(ctx.format - 1)));
break;
case '+':
case '-':
parse_sign(&ctx);
break;
case 'U':
parse_unit1(&ctx);
break;
case 'u':
parse_unit2(&ctx);
break;
default:
return -1;
}
if (ctx.error < 0) {
while (*ctx.format != '|' && *ctx.format != '\0') {
ctx.format++;
}
time_ctx_reset(&ctx);
ctx.error = (*ctx.format == '\0' ? -1 : 0);
}
}
if (ctx.error == 0 && *ctx.parsed == '\0') {
*time = time_ctx_finalize(&ctx);
return 0;
}
return -1;
}
static char *unit_names_mixed[] = { "Y", "M", "D", "h", "m", "s" };
static char *unit_names_lower[] = { "y", "mo", "d", "h", "mi", "s" };
static size_t unit_sizes[] = { 3600*24*365, 3600*24*30, 3600*24, 3600, 60, 1 };
static const size_t unit_count = 6;
static int print_unit(char *dst, size_t dst_len, char *unit_names[unit_count],
size_t max_units, knot_time_t time)
{
int ret;
if (time == 0) {
ret = snprintf(dst, dst_len, "0");
return (ret < 0 || ret >= dst_len ? -1 : 0);
}
knot_timediff_t diff = knot_time_diff(time, knot_time());
if (dst_len-- < 1) {
return -1;
}
*dst++ = (diff < 0 ? '-' : '+');
if (diff < 0) {
diff = -diff;
} else if (diff == 0) {
ret = snprintf(dst, dst_len, "0%s", unit_names[unit_count - 1]);
return (ret < 0 || ret >= dst_len ? -1 : 0);
}
size_t curr_unit = 0, used_units = 0;
while (curr_unit < unit_count && used_units < max_units) {
if (diff >= unit_sizes[curr_unit]) {
ret = snprintf(dst, dst_len, "%"KNOT_TIMEDIFF_PRINTF"%s",
diff / unit_sizes[curr_unit],
unit_names[curr_unit]);
if (ret < 0 || ret >= dst_len) {
return -1;
}
dst += ret;
dst_len -= ret;
used_units++;
diff %= unit_sizes[curr_unit];
}
curr_unit++;
}
return 0;
}
int knot_time_print(knot_time_print_t format, knot_time_t time, char *dst, size_t dst_len)
{
if (dst == NULL) {
return -1;
}
int ret;
switch (format) {
case TIME_PRINT_UNIX:
ret = snprintf(dst, dst_len, "%"KNOT_TIME_PRINTF, time);
return ((ret >= 0 && ret < dst_len) ? 0 : -1);
case TIME_PRINT_ISO8601:
if (time > LONG_MAX) {
return -1;
}
struct tm lt;
time_t tt = (time_t)time;
ret = (localtime_r(&tt, &lt) == NULL ? -1 :
strftime(dst, dst_len, "%Y-%m-%dT%H:%M:%S", &lt));
return (ret > 0 ? 0 : -1);
case TIME_PRINT_RELSEC:
ret = snprintf(dst, dst_len, "%+"KNOT_TIMEDIFF_PRINTF,
knot_time_diff(time, knot_time()));
return ((ret >= 0 && ret < dst_len) ? 0 : -1);
case TIME_PRINT_HUMAN_MIXED:
return print_unit(dst, dst_len, unit_names_mixed, unit_count, time);
case TIME_PRINT_HUMAN_LOWER:
return print_unit(dst, dst_len, unit_names_lower, unit_count, time);
default:
return -1;
}
}
......@@ -18,6 +18,18 @@
#include <stdint.h>
#include <time.h>
#include <inttypes.h>
/*!
* \brief Specify output format for knot_time_print().
*/
typedef enum {
TIME_PRINT_UNIX, // numeric UNIX time
TIME_PRINT_ISO8601, // 2016-12-31T23:59:00
TIME_PRINT_RELSEC, // relative +6523
TIME_PRINT_HUMAN_MIXED, // relative with mixed-case units
TIME_PRINT_HUMAN_LOWER, // relative with lower-case units
} knot_time_print_t;
/*!
* \brief Get current time.
......@@ -50,6 +62,9 @@ typedef int64_t knot_timediff_t;
#define KNOT_TIMEDIFF_MIN INT64_MIN
#define KNOT_TIMEDIFF_MAX INT64_MAX
#define KNOT_TIME_PRINTF PRIu64
#define KNOT_TIMEDIFF_PRINTF PRId64
/*!
* \brief Returns current time sice epoch.
*/
......@@ -115,3 +130,42 @@ inline static knot_time_t knot_time_from_u32(uint32_t u32time)
{
return (knot_time_t)u32time;
}
/*!
* \brief Parse a text-formatted timestamp to knot_time_t using format specification.
*
* \param format The timestamp text format specification.
* \param timespec Text-formatted timestamp.
* \param time The parsed timestamp.
*
* The format specification basics:
* <format 1>|<format2> - The pipe sign separates two time format specifications. Leftmost
* specification matching the timespec is used.
* '<a string>' - Matches exactly <a string> (not containing apostrophes) in timespec.
* # - Hashtag matches for a number in timespec, stands for either a UNIX timestamp,
* or, within a context of an unit, as a number of such units.
* Y, M, D, h, m, s - Matches a number, stands for a number of years, months, days, hours,
* minutes and seconds, respectively.
* +, - - The + and - signs declaring that following timespec is relative to "now".
* A single sign can be used to limit the timestamp being in future or in past,
* or both +- allow the timestamp to select any (just one) of them.
* U - Matches one of Y, M, D, h, m, s in the timespec standing for a time unit.
* u - Like U, but the unit in the timestamp is from: y, mo, d, h, mi, s.
*
* \retval -1 An error occured, out_time has no sense.
* \return 0 OK, timestamp parsed successfully.
*/
int knot_time_parse(const char *format, const char *timespec, knot_time_t *time);
/*!
* \brief Print the timestamp in specified format into a string buffer.
*
* \param format The timestamp text format specification.
* \param time The timestamp to be printed.
* \param dst The destination buffer pointer with text-formatted timestamp.
* \param dst_len The destination buffer length.
*
* \retval -1 An error occured, the buffer may be filled with nonsense.
* \return 0 OK, timestamp printed successfully.
*/
int knot_time_print(knot_time_print_t format, knot_time_t time, char *dst, size_t dst_len);
......@@ -16,6 +16,8 @@
#include <tap/basic.h>
#include <string.h>
#include "contrib/time.h"
static void test_now(void)
......@@ -121,6 +123,67 @@ static void test_knot_time(void)
ok(d == KNOT_TIMEDIFF_MIN, "negative infty diff");
}
static void test_time_parse_expect(int ret, knot_time_t res,
knot_time_t expected, const char *msg)
{
ok(ret == 0, "time_parse %s ok", msg);
ok(res == expected, "time_parse %s result", msg);
}
static void test_time_parse(void)
{
knot_time_t res;
int ret;
ret = knot_time_parse("", "", &res);
test_time_parse_expect(ret, res, 0, "nihilist");
ret = knot_time_parse("#", "12345", &res);
test_time_parse_expect(ret, res, 12345, "unix");
ret = knot_time_parse("+-#U", "-1h", &res);
test_time_parse_expect(ret, res, knot_time() - 3600, "hour");
ret = knot_time_parse("+-#u'nths'|+-#u'nutes'", "+1minutes", &res);
test_time_parse_expect(ret, res, knot_time() + 60, "minute");
}
static void test_time_print_expect(int ret, const char *res, int res_len,
const char *expected, const char *msg)
{
ok(ret == 0, "time_print %s ok", msg);
ok(strncmp(res, expected, res_len) == 0, "time_print %s result", msg);
}
static void test_time_print(void)
{
char buff[100];
int bufl = sizeof(buff);
int ret;
knot_time_t t = 44000, t2 = knot_time_add(knot_time(), -10000);
knot_time_t big = knot_time_add(knot_time(), 2 * 365 * 24 * 3600 + 1);
ret = knot_time_print(TIME_PRINT_UNIX, t, buff, bufl);
test_time_print_expect(ret, buff, bufl, "44000", "unix");
ret = knot_time_print(TIME_PRINT_RELSEC, t2, buff, bufl);
test_time_print_expect(ret, buff, bufl, "-10000", "relsec");
ret = knot_time_print(TIME_PRINT_ISO8601, t, buff, bufl);
buff[11] = '0', buff[12] = '0'; // zeroing 'hours' field to avoid locality issues
test_time_print_expect(ret, buff, bufl, "1970-01-01T00:13:20", "iso");
ret = knot_time_print(TIME_PRINT_HUMAN_MIXED, t2, buff, bufl);
test_time_print_expect(ret, buff, bufl, "-2h46m40s", "negative human mixed");
ret = knot_time_print(TIME_PRINT_HUMAN_MIXED, big, buff, bufl);
test_time_print_expect(ret, buff, bufl, "+2Y1s", "big human mixed");
ret = knot_time_print(TIME_PRINT_HUMAN_LOWER, t2, buff, bufl);
test_time_print_expect(ret, buff, bufl, "-2h46mi40s", "negative human lower");
ret = knot_time_print(TIME_PRINT_HUMAN_LOWER, big, buff, bufl);
test_time_print_expect(ret, buff, bufl, "+2y1s", "big human lower");
}
int main(int argc, char *argv[])
{
plan_lazy();
......@@ -129,6 +192,8 @@ int main(int argc, char *argv[])
test_diff();
test_diff_ms();
test_knot_time();
test_time_parse();
test_time_print();
return 0;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment