libhmd-c

C human-readable date string library
git clone https://git.sinitax.com/sinitax/libhmd-c
Log | Files | Refs | Submodules | LICENSE | sfeed.txt

commit 3433153426fe443a551a4bd1104959058e111858
Author: Louis Burda <quent.burda@gmail.com>
Date:   Mon,  5 Jun 2023 18:38:46 +0200

Add initial version with _parse and _str api

Diffstat:
A.gitignore | 4++++
A.gitmodules | 3+++
Abuild.jst.tmpl | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfigure | 10++++++++++
Ainclude/hmd.h | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/liballoc | 1+
Alibhmd.api | 14++++++++++++++
Alibhmd.lds | 16++++++++++++++++
Asrc/hmd.c | 509+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/test.c | 17+++++++++++++++++
10 files changed, 734 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +.cache +build +build.jst +compile_commands.json diff --git a/.gitmodules b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/liballoc"] + path = lib/liballoc + url = git@sinitax.com:sinitax/liballoc diff --git a/build.jst.tmpl b/build.jst.tmpl @@ -0,0 +1,69 @@ +#default PREFIX /usr/local +#default INCLDIR /include +#default LIBDIR /lib +#default CC gcc + +#ifdef libhmd_DEBUG +#define DEBUG 1 +#endif + +#ifdef DEBUG +#define OPT_CFLAGS -Og -g +#else +#define OPT_CFLAGS -O2 +#endif + +cflags = -Wunused-function -Wunused-variable -Wconversion -Wformat + -I include -I lib/liballoc/include -fPIC + #{OPT_CFLAGS} #{libhmd_EXTRA_CFLAGS} #{EXTRA_CFLAGS} + +rule liba + #{CC} -o $out.tmp.o $in $cflags -r + objcopy --keep-global-symbols=libhmd.api $out.tmp.o $out.fixed.o + ar rcs $out $out.fixed.o + rm $out.tmp.o $out.fixed.o + +rule libso + #{CC} -o $out $in $cflags -shared -Wl,-version-script libhmd.lds + +rule cc + #{CC} -o $out $in $cflags + +rule mkdir + mkdir $out + +target build + mkdir + +target lib/liballoc/build/liballoc.a + just lib/liballoc + +target build/libhmd.a + liba src/hmd.c | include/hmd.h build + +target build/libhmd.so + libso src/hmd.c | include/hmd.h build + +target build/test + cc src/test.c build/libhmd.a lib/liballoc/build/liballoc.a | build + +command clean + rm -rf build + +command cleanall + just clean + just -C lib/liballoc cleanall + +command install + install -m755 build/libhmd.a -t "#{DESTDIR}#{PREFIX}#{LIBDIR}" + install -m755 build/libhmd.so -t "#{DESTDIR}#{PREFIX}#{LIBDIR}" + install -m644 include/hmd.h -t "#{DESTDIR}#{PREFIX}#{INCLDIR}" + +command uninstall + rm -f "#{DESTDIR}#{PREFIX}#{LIBDIR}/libhmd.a" + rm -f "#{DESTDIR}#{PREFIX}#{LIBDIR}/libhmd.so" + rm -f "#{DESTDIR}#{PREFIX}#{INCLDIR}/hmd.h" + +command all + just build/libhmd.a build/libhmd.so build/test + diff --git a/configure b/configure @@ -0,0 +1,10 @@ +#!/bin/sh + +tmpl "$@" build.jst.tmpl > build.jst +if [ "$1" != "shallow" ]; then + for lib in ./lib/*; do + pushd $lib + ./configure "$@" + popd + done +fi diff --git a/include/hmd.h b/include/hmd.h @@ -0,0 +1,91 @@ +#pragma once + +#include "allocator.h" + +#include <stdint.h> + +#define HMD_SEC_SPN (1) +#define HMD_MIN_SPN (60) +#define HMD_HOUR_SPN (HMD_MIN_SPN * 60) +#define HMD_DAY_SPN (HMD_HOUR_SPN * 24) +#define HMD_WEEK_SPN (HMD_DAY_SPN * 7) + +enum hmd_err { + HMD_OK, + HMD_ERR_OOB, + HMD_ERR_FMT, + HMD_ERR_SYS, + HMD_ERR_INT +}; + +enum hmd_mon { + HMD_JAN, + HMD_FEB, + HMD_MAR, + HMD_APR, + HMD_MAY, + HMD_JUN, + HMD_JUL, + HMD_AUG, + HMD_SEP, + HMD_OCT, + HMD_NOV, + HMD_DEC +}; + +enum hmd_wday { + HMD_SUN, + HMD_MON, + HMD_TUE, + HMD_WED, + HMD_THU, + HMD_FRI, + HMD_SAT, +}; + +struct hmd_date { + uint64_t ts; /* unix epoch */ + uint64_t spn; +}; + +struct hmd_dd { + uint16_t year; + uint8_t month; + uint8_t day; +}; + +struct hmd_tod { + uint8_t hour; + uint8_t min; + uint8_t sec; + uint16_t spn; +}; + +struct hmd_ival { + uint16_t year; + uint16_t day; + uint8_t hour; + uint8_t min; + uint8_t sec; +}; + +enum hmd_err hmd_date_parse_daydate_reltime_weekday( + struct hmd_date *date, char *arg); +enum hmd_err hmd_date_parse_daydate_reltime_coarse( + struct hmd_date *date, char *arg); +enum hmd_err hmd_date_parse_daydate_iso8601(struct hmd_date *date, char *arg); +enum hmd_err hmd_timeofday_parse(struct hmd_tod *tod, char *arg); +enum hmd_err hmd_date_parse(struct hmd_date *date, char *arg); + +char *hmd_date_str_reltime_exact(struct hmd_date *date, + const struct allocator *allocator, int *rc); +char *hmd_date_str_reltime_coarse(struct hmd_date *date, + const struct allocator *allocator, int *rc); +char *hmd_date_str_iso8601(struct hmd_date *date, + const struct allocator *allocator, int *rc); +char *hmd_date_str(struct hmd_date *date, + const struct allocator *allocator, int *rc); + +extern const char *hmd_wdnames[7]; +extern const char *hmd_mnames[12]; +extern const int hmd_mdays[12]; diff --git a/lib/liballoc b/lib/liballoc @@ -0,0 +1 @@ +Subproject commit 3f388a2659ae2d121322101930d33412815d84e6 diff --git a/libhmd.api b/libhmd.api @@ -0,0 +1,14 @@ +hmd_date_parse_daydate_reltime_weekday +hmd_date_parse_daydate_reltime_coarse +hmd_date_parse_daydate_iso8601 +hmd_timeofday_parse +hmd_date_parse + +hmd_date_str_reltime_exact +hmd_date_str_reltime_coarse +hmd_date_str_iso8601 +hmd_date_str + +hmd_wdnames +hmd_mnames +hmd_mdays diff --git a/libhmd.lds b/libhmd.lds @@ -0,0 +1,16 @@ +LIBHMD_0.1.0 { + hmd_date_parse_daydate_reltime_weekday; + hmd_date_parse_daydate_reltime_coarse; + hmd_date_parse_daydate_iso8601; + hmd_timeofday_parse; + hmd_date_parse; + + hmd_date_str_reltime_exact; + hmd_date_str_reltime_coarse; + hmd_date_str_iso8601; + hmd_date_str; + + hmd_wdnames; + hmd_mnames; + hmd_mdays; +}; diff --git a/src/hmd.c b/src/hmd.c @@ -0,0 +1,509 @@ +#include "hmd.h" + +#include <asm-generic/errno.h> +#include <time.h> +#include <errno.h> +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <stdbool.h> +#include <stdlib.h> + +#define ABS(x) ((x) > 0 ? (x) : -(x)) +#define DAYS(tm) ((tm)->tm_year * 365 + (tm)->tm_yday) +#define BETWEEN(x, start, end) (((x) >= (start)) && ((x) < (end))) + +const char *hmd_wdnames[7] = { + [HMD_SUN] = "sunday", + [HMD_MON] = "monday", + [HMD_TUE] = "tuesday", + [HMD_WED] = "wednesday", + [HMD_THU] = "thursday", + [HMD_FRI] = "friday", + [HMD_SAT] = "saturday" +}; + +const char *hmd_mnames[12] = { + [HMD_JAN] = "january", + [HMD_FEB] = "february", + [HMD_MAR] = "march", + [HMD_APR] = "april", + [HMD_MAY] = "may", + [HMD_JUN] = "june", + [HMD_JUL] = "july", + [HMD_AUG] = "august", + [HMD_SEP] = "september", + [HMD_OCT] = "october", + [HMD_NOV] = "november", + [HMD_DEC] = "december" +}; + +const int hmd_mdays[12] = { + [HMD_JAN] = 31, + [HMD_FEB] = 28, + [HMD_MAR] = 31, + [HMD_APR] = 30, + [HMD_MAY] = 31, + [HMD_JUN] = 30, + [HMD_JUL] = 31, + [HMD_AUG] = 31, + [HMD_SEP] = 30, + [HMD_OCT] = 31, + [HMD_NOV] = 30, + [HMD_DEC] = 31, +}; + +static char * +strfmt(const struct allocator *allocator, int *rc, const char *fmtstr, ...) +{ + va_list ap, cpy; + char *str; + ssize_t n; + + va_copy(cpy, ap); + + va_start(cpy, fmtstr); + n = vsnprintf(NULL, 0, fmtstr, cpy); + va_end(cpy); + + if (n <= 0) { + if (rc) *rc = HMD_ERR_INT; + return NULL; + } + + str = allocator->alloc(allocator, (size_t) (n + 1), rc); + if (!str && rc) *rc = -*rc; + if (!str) return NULL; + + va_start(ap, fmtstr); + vsnprintf(str, (size_t) (n + 1), fmtstr, ap); + va_end(ap); + + return str; +} + +static bool +has_prefix(const char *text, const char *prefix) +{ + size_t len; + + len = strlen(prefix); + if (len > strlen(text)) + return false; + + return !strncmp(text, prefix, len); +} + +static bool +is_prefix(const char *prefix, const char *text, int minlen) +{ + size_t len; + + len = strlen(prefix); + if (len < minlen || len > strlen(text)) + return false; + + return !strncmp(prefix, text, len); +} + +static struct tm +startofday(struct tm tm) +{ + tm.tm_hour = 0; + tm.tm_min = 0; + tm.tm_sec = 0; + + return tm; +} + +static enum hmd_err +tm2time(uint64_t *time, struct tm tm) +{ + time_t res; + + tm.tm_isdst = -1; /* infer timezone */ + res = mktime(&tm); + if (res == (time_t) -1) + return HMD_ERR_OOB; + *time = (uint64_t) res; + + return HMD_OK; +} + +static enum hmd_err +time2tm(struct tm *tm, uint64_t time) +{ + struct tm *res; + time_t tmp; + + tmp = (time_t) time; + res = localtime(&tmp); + if (!res) return HMD_ERR_OOB; + *tm = *res; + + return HMD_OK; +} + +enum hmd_err +hmd_date_parse_daydate_reltime_weekday(struct hmd_date *date, char *arg) +{ + struct tm datetm, nowtm; + enum hmd_err err; + time_t now; + int i; + + now = time(NULL); + nowtm = *localtime(&now); + + for (i = 0; i < 7; i++) { + if (is_prefix(arg, hmd_wdnames[i], 3)) { + datetm = startofday(nowtm); + datetm.tm_mday += i - datetm.tm_wday; + datetm.tm_mday += (i < datetm.tm_wday ? 7 : 0); + break; + } else if (has_prefix(arg, "next-") + && is_prefix(arg + 5, hmd_wdnames[i], 3)) { + datetm = startofday(nowtm); + datetm.tm_mday += i - datetm.tm_wday; + datetm.tm_mday += (i < datetm.tm_wday ? 7 : 0) + 7; + break; + } else if (has_prefix(arg, "last-") + && is_prefix(arg + 5, hmd_wdnames[i], 3)) { + datetm = startofday(nowtm); + datetm.tm_mday += i - datetm.tm_wday; + datetm.tm_mday += (i < datetm.tm_wday ? 7 : 0) - 7; + break; + } + } + + if (i == 7) return HMD_ERR_FMT; + + err = tm2time(&date->ts, datetm); + if (err) return err; + + date->spn = HMD_DAY_SPN; + + return HMD_OK; +} + +enum hmd_err +hmd_date_parse_daydate_reltime_coarse(struct hmd_date *date, char *arg) +{ + time_t now; + struct tm datetm, nowtm; + uint64_t next; + enum hmd_err err; + size_t i; + + now = time(NULL); + nowtm = *localtime(&now); + + if (is_prefix(arg, "tomorrow", 3)) { + datetm = startofday(nowtm); + datetm.tm_mday += 1; + date->spn = HMD_DAY_SPN; + err = tm2time(&date->ts, datetm); + if (err) return err; + } else if (is_prefix(arg, "today", 3)) { + datetm = startofday(nowtm); + date->spn = HMD_DAY_SPN; + err = tm2time(&date->ts, datetm); + if (err) return err; + } else if (is_prefix(arg, "yesterday", 3)) { + datetm = startofday(nowtm); + datetm.tm_mday -= 1; + date->spn = HMD_DAY_SPN; + err = tm2time(&date->ts, datetm); + if (err) return err; + } else { + for (i = 0; i < 12; i++) { + if (is_prefix(arg, hmd_mnames[i], 3)) { + datetm = startofday(nowtm); + datetm.tm_mday = 1; + datetm.tm_mon = (uint8_t) i; + err = tm2time(&date->ts, datetm); + if (err) return err; + datetm.tm_mon += 1; + err = tm2time(&next, datetm); + if (err) return err; + date->spn = next - date->ts; + break; + } + } + + if (i == 12) { + err = hmd_date_parse_daydate_reltime_weekday(date, arg); + if (err) return err; + } + } + + return HMD_OK; +} + +enum hmd_err +hmd_date_parse_daydate_iso8601(struct hmd_date *date, char *arg) +{ + struct tm nowtm, tm; + int year, month, day; + enum hmd_err err; + uint64_t next; + time_t now; + char end; + + now = time(NULL); + nowtm = *localtime(&now); + + if (sscanf(arg, "%u-%u-%u%c", &year, &month, &day, &end) == 3) { + tm = startofday(nowtm); + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + err = tm2time(&date->ts, tm); + if (err) return err; + date->spn = HMD_DAY_SPN; + } else if (sscanf(arg, "%u-%u%c", &year, &month, &end) == 2) { + tm = startofday(nowtm); + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = 1; + err = tm2time(&date->ts, tm); + if (err) return err; + tm.tm_mon++; + err = tm2time(&next, tm); + if (err) return err; + date->spn = next - date->ts; + } else if (sscanf(arg, "%u%c", &year, &end) == 1) { + tm = startofday(nowtm); + tm.tm_year = year - 1900; + tm.tm_mon = 0; + tm.tm_mday = 1; + err = tm2time(&date->ts, tm); + if (err) return err; + tm.tm_year++; + err = tm2time(&next, tm); + if (err) return err; + date->spn = next - date->ts; + } else { + return HMD_ERR_FMT; + } + + return HMD_OK; +} + +enum hmd_err +hmd_date_parse_daydate(struct hmd_date *date, char *arg) +{ + enum hmd_err err; + + err = hmd_date_parse_daydate_reltime_coarse(date, arg); + if (err) err = hmd_date_parse_daydate_iso8601(date, arg); + if (err) return err; + + return HMD_OK; +} + +enum hmd_err +hmd_timeofday_parse(struct hmd_tod *tod, char *arg) +{ + unsigned int hour, min, sec; + char end; + + hour = min = sec = 0; + if (sscanf(arg, "%u:%u:%u%c", &hour, &min, &sec, &end) == 3) { + tod->spn = HMD_SEC_SPN; + } else if (sscanf(arg, "%u:%u%c", &hour, &min, &end) == 2) { + tod->spn = HMD_MIN_SPN; + } else if (sscanf(arg, "%u%c", &hour, &end) == 1) { + tod->spn = HMD_HOUR_SPN; + } else { + return HMD_ERR_FMT; + } + + if (hour >= 24 || min >= 60 || sec >= 60) + return HMD_ERR_OOB; + + tod->hour = (uint8_t) hour; + tod->min = (uint8_t) min; + tod->sec = (uint8_t) sec; + + return HMD_OK; +} + +enum hmd_err +hmd_date_parse(struct hmd_date *date, char *arg) +{ + struct hmd_tod tod; + char *sep; + enum hmd_err err; + + err = HMD_OK; + sep = strchr(arg, '@'); + if (sep) *sep = '\0'; + + err = hmd_date_parse_daydate(date, arg); + if (err) goto exit; + + if (sep) { + /* ambiguous due to DST */ + err = hmd_timeofday_parse(&tod, sep + 1); + if (err) goto exit; + + date->ts += (uint64_t) tod.hour * HMD_HOUR_SPN + + tod.min * HMD_MIN_SPN + tod.sec; + date->spn = tod.spn; + } + +exit: + if (sep) *sep = '@'; + return err; +} + +char * +hmd_date_str_reltime_exact(struct hmd_date *date, + const struct allocator *allocator, int *rc) +{ + time_t days, hours, min, sec; + time_t now, diff; + char *datestr; + + now = time(NULL); + diff = (time_t) date->ts - now; + + days = diff / HMD_DAY_SPN; + hours = (diff % HMD_DAY_SPN) / HMD_HOUR_SPN; + min = (diff % HMD_HOUR_SPN) / HMD_MIN_SPN; + sec = diff % HMD_MIN_SPN; + + if (days) { + datestr = strfmt(allocator, rc, "%i day%s%s", ABS(days), + ABS(days) > 1 ? "s" : "", days < 0 ? " ago" : ""); + } else if (hours) { + datestr = strfmt(allocator, rc, "%i hour%s%s", ABS(hours), + ABS(hours) > 1 ? "s" : "" , hours < 0 ? " ago" : ""); + } else if (min) { + datestr = strfmt(allocator, rc, "%i min%s%s", ABS(min), + ABS(min) > 1 ? "s" : "", min < 0 ? " ago" : ""); + } else if (sec) { + datestr = strfmt(allocator, rc, "%i sec%s%s", ABS(sec), + ABS(sec) > 1 ? "s" : "", sec < 0 ? " ago" : ""); + } else { + datestr = strfmt(allocator, rc, "now"); + } + + return datestr; +} + +char * +hmd_date_str_reltime_coarse(struct hmd_date *date, + const struct allocator *allocator, int *rc) +{ + struct tm nowtm, datetm; + enum hmd_err err; + char *datestr; + time_t now; + + now = time(NULL); + + err = time2tm(&nowtm, (uint64_t) now); + if (err) { + *rc = (int) err; + return NULL; + } + + err = time2tm(&datetm, date->ts); + if (err) { + *rc = (int) err; + return NULL; + } + + if (DAYS(&datetm) == DAYS(&nowtm) - 1) { + datestr = strfmt(allocator, rc, "yesterday"); + } else if (DAYS(&datetm) == DAYS(&nowtm) + 1) { + datestr = strfmt(allocator, rc, "tomorrow"); + } else if (DAYS(&datetm) == DAYS(&nowtm)) { + datestr = strfmt(allocator, rc, "today"); + } else if (BETWEEN(DAYS(&datetm) - DAYS(&nowtm), -7, 0)) { + datestr = strfmt(allocator, rc, + "last %s", hmd_wdnames[datetm.tm_wday]); + } else if (BETWEEN(DAYS(&datetm) - DAYS(&nowtm), 0, 7)) { + datestr = strfmt(allocator, rc, + "%s", hmd_wdnames[datetm.tm_wday]); + } else if (BETWEEN(DAYS(&datetm) - DAYS(&nowtm), 7, 14)) { + datestr = strfmt(allocator, rc, + "next %s", hmd_wdnames[datetm.tm_wday]); + } else { + *rc = HMD_ERR_FMT; + return NULL; + } + + return datestr; +} + +char * +hmd_date_str_iso8601(struct hmd_date *date, + const struct allocator *allocator, int *rc) +{ + struct tm tm; + enum hmd_err err; + size_t len; + char *str; + + err = time2tm(&tm, date->ts); + if (err) { + *rc = (int) err; + return NULL; + } + + str = allocator->alloc(allocator, 20, rc); + if (!str && rc) *rc = -*rc; + if (!str) return NULL; + + if (date->spn < HMD_MIN_SPN) { + len = strftime(str, 21, "%Y-%m-%d %H:%M:%S", &tm); + } else if (date->spn >= HMD_MIN_SPN && date->spn < HMD_DAY_SPN) { + len = strftime(str, 21, "%Y-%m-%d %H:%M", &tm); + } else { + len = strftime(str, 21, "%Y-%m-%d", &tm); + } + + if (!len) { + if (rc) *rc = HMD_ERR_INT; + allocator->free(allocator, str); + return NULL; + } + + return str; +} + +char * +hmd_date_str(struct hmd_date *date, + const struct allocator *allocator, int *rc) +{ + struct tm nowtm, datetm; + enum hmd_err err; + time_t now; + char *str; + + now = time(NULL); + + err = time2tm(&nowtm, (uint64_t) now); + if (err) { + *rc = (int) err; + return NULL; + } + + err = time2tm(&datetm, date->ts); + if (err) { + *rc = (int) err; + return NULL; + } + + if (DAYS(&nowtm) == DAYS(&datetm)) + return hmd_date_str_reltime_exact(date, allocator, rc); + + str = hmd_date_str_reltime_coarse(date, allocator, rc); + if (!str) str = hmd_date_str_iso8601(date, allocator, rc); + + return str; +} + diff --git a/src/test.c b/src/test.c @@ -0,0 +1,17 @@ +#include "hmd.h" + +#include <err.h> +#include <stdio.h> + +int +main(int argc, char **argv) +{ + struct hmd_date date; + char **arg; + + for (arg = argv + 1; *arg; arg++) { + if (hmd_date_parse(&date, *arg)) + err(1, "invalid date '%s'", *arg); + printf("%lu %lu\n", date.ts, date.spn); + } +}