From 2c76ba433f2b0d0b180bca20ddc2f28751dd9b70 Mon Sep 17 00:00:00 2001 From: David Lamparter Date: Thu, 13 Jan 2022 07:51:54 +0100 Subject: [PATCH] lib: add time formatting printfrr exts Refer to docs in doc/developer for details. Signed-off-by: David Lamparter --- doc/developer/logging.rst | 116 ++++++++++++++ lib/monotime.h | 75 +++++++++ lib/printfrr.h | 4 + lib/strformat.c | 325 ++++++++++++++++++++++++++++++++++++++ tests/lib/test_printfrr.c | 110 +++++++++++++ 5 files changed, 630 insertions(+) diff --git a/doc/developer/logging.rst b/doc/developer/logging.rst index a51f51553f..5aca27d7f7 100644 --- a/doc/developer/logging.rst +++ b/doc/developer/logging.rst @@ -219,6 +219,122 @@ Networking data types :frrfmtout:`SOCK_STREAM` +Time/interval formats +^^^^^^^^^^^^^^^^^^^^^ + +.. frrfmt:: %pTS (struct timespec *) + +.. frrfmt:: %pTV (struct timeval *) + +.. frrfmt:: %pTT (time_t *) + + Above 3 options internally result in the same code being called, support + the same flags and produce equal output with one exception: ``%pTT`` + has no sub-second precision and the formatter will never print a + (nonsensical) ``.000``. + + Exactly one of ``I``, ``M`` or ``R`` must immediately follow after + ``TS``/``TV``/``TT`` to specify whether the input is an interval, monotonic + timestamp or realtime timestamp: + + ``%pTVI``: input is an interval, not a timestamp. Print interval. + + ``%pTVIs``: input is an interval, convert to wallclock by subtracting it + from current time (i.e. interval has passed **s**\ ince.) + + ``%pTVIu``: input is an interval, convert to wallclock by adding it to + current time (i.e. **u**\ ntil interval has passed.) + + ``%pTVM`` - input is a timestamp on CLOCK_MONOTONIC, convert to wallclock + time (by grabbing current CLOCK_MONOTONIC and CLOCK_REALTIME and doing the + math) and print calendaric date. + + ``%pTVMs`` - input is a timestamp on CLOCK_MONOTONIC, print interval + **s**\ ince that timestamp (elapsed.) + + ``%pTVMu`` - input is a timestamp on CLOCK_MONOTONIC, print interval + **u**\ ntil that timestamp (deadline.) + + ``%pTVR`` - input is a timestamp on CLOCK_REALTIME, print calendaric date. + + ``%pTVRs`` - input is a timestamp on CLOCK_REALTIME, print interval + **s**\ ince that timestamp. + + ``%pTVRu`` - input is a timestamp on CLOCK_REALTIME, print interval + **u**\ ntil that timestamp. + + ``%pTVA`` - reserved for CLOCK_TAI in case a PTP implementation is + interfaced to FRR. Not currently implemented. + + .. note:: + + If ``%pTVRs`` or ``%pTVRu`` are used, this is generally an indication + that a CLOCK_MONOTONIC timestamp should be used instead (or added in + parallel.) CLOCK_REALTIME might be adjusted by NTP, PTP or similar + procedures, causing bogus intervals to be printed. + + ``%pTVM`` on first look might be assumed to have the same problem, but + on closer thought the assumption is always that current system time is + correct. And since a CLOCK_MONOTONIC interval is also quite safe to + assume to be correct, the (past) absolute timestamp to be printed from + this can likely be correct even if it doesn't match what CLOCK_REALTIME + would have indicated at that point in the past. This logic does, + however, not quite work for *future* times. + + Generally speaking, almost all use cases in FRR should (and do) use + CLOCK_MONOTONIC (through :c:func:`monotime()`.) + + Flags common to printing calendar times and intervals: + + ``p``: include spaces in appropriate places (depends on selected format.) + + ``%p.3TV...``: specify sub-second resolution (use with ``FMT_NSTD`` to + suppress gcc warning.) As noted above, ``%pTT`` will never print sub-second + digits since there are none. Only some formats support printing sub-second + digits and the default may vary. + + The following flags are available for printing calendar times/dates: + + (no flag): :frrfmtout:`Sat Jan 1 00:00:00 2022` - print output from + ``ctime()``, in local time zone. Since FRR does not currently use/enable + locale support, this is always the C locale. (Locale support getting added + is unlikely for the time being and would likely break other things worse + than this.) + + ``i``: :frrfmtout:`2022-01-01T00:00:00.123` - ISO8601 timestamp in local + time zone (note there is no ``Z`` or ``+00:00`` suffix.) Defaults to + millisecond precision. + + ``ip``: :frrfmtout:`2022-01-01 00:00:00.123` - use readable form of ISO8601 + with space instead of ``T`` separator. + + The following flags are available for printing intervals: + + (no flag): :frrfmtout:`9w9d09:09:09.123` - does not match any + preexisting format; added because it does not lose precision (like ``t``) + for longer intervals without printing huge numbers (like ``h``/``m``). + Defaults to millisecond precision. The week/day fields are left off if + they're zero, ``p`` adds a space after the respective letter. + + ``t``: :frrfmtout:`9w9d09h`, :frrfmtout:`9d09h09m`, :frrfmtout:`09:09:09` - + this replaces :c:func:`frrtime_to_interval()`. ``p`` adds spaces after + week/day/hour letters. + + ``d``: print decimal number of seconds. Defaults to millisecond precision. + + ``x`` / ``tx`` / ``dx``: Like no flag / ``t`` / ``d``, but print + :frrfmtout:`-` for zero or negative intervals (for use with unset timers.) + + ``h``: :frrfmtout:`09:09:09` + + ``hx``: :frrfmtout:`09:09:09`, :frrfmtout:`--:--:--` - this replaces + :c:func:`pim_time_timer_to_hhmmss()`. + + ``m``: :frrfmtout:`09:09` + + ``mx``: :frrfmtout:`09:09`, :frrfmtout:`--:--` - this replaces + :c:func:`pim_time_timer_to_mmss()`. + General utility formats ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/lib/monotime.h b/lib/monotime.h index dda763784f..15b6933955 100644 --- a/lib/monotime.h +++ b/lib/monotime.h @@ -25,6 +25,9 @@ extern "C" { #endif +struct fbuf; +struct printfrr_eargs; + #ifndef TIMESPEC_TO_TIMEVAL /* should be in sys/time.h on BSD & Linux libcs */ #define TIMESPEC_TO_TIMEVAL(tv, ts) \ @@ -42,6 +45,31 @@ extern "C" { } while (0) #endif +/* Linux/glibc is sadly missing these timespec helpers */ +#ifndef timespecadd +#define timespecadd(tsp, usp, vsp) \ + do { \ + (vsp)->tv_sec = (tsp)->tv_sec + (usp)->tv_sec; \ + (vsp)->tv_nsec = (tsp)->tv_nsec + (usp)->tv_nsec; \ + if ((vsp)->tv_nsec >= 1000000000L) { \ + (vsp)->tv_sec++; \ + (vsp)->tv_nsec -= 1000000000L; \ + } \ + } while (0) +#endif + +#ifndef timespecsub +#define timespecsub(tsp, usp, vsp) \ + do { \ + (vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec; \ + (vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec; \ + if ((vsp)->tv_nsec < 0) { \ + (vsp)->tv_sec--; \ + (vsp)->tv_nsec += 1000000000L; \ + } \ + } while (0) +#endif + static inline time_t monotime(struct timeval *tvo) { struct timespec ts; @@ -132,6 +160,53 @@ static inline const char *frrtime_to_interval(time_t t, char *buf, return buf; } +enum { + /* n/a - input was seconds precision, don't print any fractional */ + TIMEFMT_SECONDS = (1 << 0), + /* caller is directly invoking printfrr_time and has pre-specified + * I/Iu/Is/M/Mu/Ms/R/Ru/Rs (for printing timers) + */ + TIMEFMT_PRESELECT = (1 << 1), + /* don't print any output - this is needed for invoking printfrr_time + * from another printfrr extensions to skip over flag characters + */ + TIMEFMT_SKIP = (1 << 2), + /* use spaces in appropriate places */ + TIMEFMT_SPACE = (1 << 3), + + /* input interpretations: */ + TIMEFMT_REALTIME = (1 << 8), + TIMEFMT_MONOTONIC = (1 << 9), + TIMEFMT_SINCE = (1 << 10), + TIMEFMT_UNTIL = (1 << 11), + + TIMEFMT_ABSOLUTE = TIMEFMT_REALTIME | TIMEFMT_MONOTONIC, + TIMEFMT_ANCHORS = TIMEFMT_SINCE | TIMEFMT_UNTIL, + + /* calendaric formats: */ + TIMEFMT_ISO8601 = (1 << 16), + + /* interval formats: */ + /* 't' - use [t]raditional 3-block format */ + TIMEFMT_BASIC = (1 << 24), + /* 'm' - select mm:ss */ + TIMEFMT_MMSS = (1 << 25), + /* 'h' - select hh:mm:ss */ + TIMEFMT_HHMMSS = (1 << 26), + /* 'd' - print as decimal number of seconds */ + TIMEFMT_DECIMAL = (1 << 27), + /* 'mx'/'hx' - replace zero value with "--:--" or "--:--:--" */ + TIMEFMT_DASHES = (1 << 31), + + /* helpers for reference */ + TIMEFMT_TIMER_DEADLINE = + TIMEFMT_PRESELECT | TIMEFMT_MONOTONIC | TIMEFMT_UNTIL, + TIMEFMT_TIMER_INTERVAL = TIMEFMT_PRESELECT, +}; + +extern ssize_t printfrr_time(struct fbuf *buf, struct printfrr_eargs *ea, + const struct timespec *ts, unsigned int flags); + #ifdef __cplusplus } #endif diff --git a/lib/printfrr.h b/lib/printfrr.h index 37f1f9c8cd..1b5ddb8ca4 100644 --- a/lib/printfrr.h +++ b/lib/printfrr.h @@ -286,6 +286,10 @@ struct va_format { #pragma FRR printfrr_ext "%pSE" (char *) #pragma FRR printfrr_ext "%pSQ" (char *) + +#pragma FRR printfrr_ext "%pTS" (struct timespec *) +#pragma FRR printfrr_ext "%pTV" (struct timeval *) +#pragma FRR printfrr_ext "%pTT" (time_t *) #endif /* when using non-ISO-C compatible extension specifiers... */ diff --git a/lib/strformat.c b/lib/strformat.c index 431e573a0c..7903e87ead 100644 --- a/lib/strformat.c +++ b/lib/strformat.c @@ -22,8 +22,10 @@ #include #include +#include #include "printfrr.h" +#include "monotime.h" printfrr_ext_autoreg_p("HX", printfrr_hexdump) static ssize_t printfrr_hexdump(struct fbuf *buf, struct printfrr_eargs *ea, @@ -270,3 +272,326 @@ static ssize_t printfrr_quote(struct fbuf *buf, struct printfrr_eargs *ea, ret += bputch(buf, '"'); return ret; } + +static ssize_t printfrr_abstime(struct fbuf *buf, struct printfrr_eargs *ea, + const struct timespec *ts, unsigned int flags); +static ssize_t printfrr_reltime(struct fbuf *buf, struct printfrr_eargs *ea, + const struct timespec *ts, unsigned int flags); + +ssize_t printfrr_time(struct fbuf *buf, struct printfrr_eargs *ea, + const struct timespec *ts, unsigned int flags) +{ + bool have_abs, have_anchor; + + if (!(flags & TIMEFMT_PRESELECT)) { + switch (ea->fmt[0]) { + case 'I': + /* no bit set */ + break; + case 'M': + flags |= TIMEFMT_MONOTONIC; + break; + case 'R': + flags |= TIMEFMT_REALTIME; + break; + default: + return bputs(buf, + "{invalid time format input specifier}"); + } + ea->fmt++; + + if (ea->fmt[0] == 's') { + flags |= TIMEFMT_SINCE; + ea->fmt++; + } else if (ea->fmt[0] == 'u') { + flags |= TIMEFMT_UNTIL; + ea->fmt++; + } + } + + have_abs = !!(flags & TIMEFMT_ABSOLUTE); + have_anchor = !!(flags & TIMEFMT_ANCHORS); + + if (have_abs ^ have_anchor) + return printfrr_abstime(buf, ea, ts, flags); + else + return printfrr_reltime(buf, ea, ts, flags); +} + +static ssize_t do_subsec(struct fbuf *buf, const struct timespec *ts, + int precision, unsigned int flags) +{ + unsigned long long frac; + + if (precision <= 0 || (flags & TIMEFMT_SECONDS)) + return 0; + + frac = ts->tv_nsec; + if (precision > 9) + precision = 9; + for (int i = precision; i < 9; i++) + frac /= 10; + return bprintfrr(buf, ".%0*llu", precision, frac); +} + +static ssize_t printfrr_abstime(struct fbuf *buf, struct printfrr_eargs *ea, + const struct timespec *ts, unsigned int flags) +{ + struct timespec real_ts[1]; + struct tm tm; + char cbuf[32] = ""; /* manpage says 26 for ctime_r */ + ssize_t ret = 0; + int precision = ea->precision; + + while (ea->fmt[0]) { + char ch = *ea->fmt++; + + switch (ch) { + case 'p': + flags |= TIMEFMT_SPACE; + continue; + case 'i': + flags |= TIMEFMT_ISO8601; + continue; + } + + ea->fmt--; + break; + } + + if (flags & TIMEFMT_SKIP) + return 0; + + if (flags & TIMEFMT_REALTIME) + *real_ts = *ts; + else if (flags & TIMEFMT_MONOTONIC) { + struct timespec mono_now[1]; + + clock_gettime(CLOCK_REALTIME, real_ts); + clock_gettime(CLOCK_MONOTONIC, mono_now); + + timespecsub(real_ts, mono_now, real_ts); + timespecadd(real_ts, ts, real_ts); + } else { + clock_gettime(CLOCK_REALTIME, real_ts); + + if (flags & TIMEFMT_SINCE) + timespecsub(real_ts, ts, real_ts); + else /* flags & TIMEFMT_UNTIL */ + timespecadd(real_ts, ts, real_ts); + } + + localtime_r(&real_ts->tv_sec, &tm); + + if (flags & TIMEFMT_ISO8601) { + if (flags & TIMEFMT_SPACE) + strftime(cbuf, sizeof(cbuf), "%Y-%m-%d %H:%M:%S", &tm); + else + strftime(cbuf, sizeof(cbuf), "%Y-%m-%dT%H:%M:%S", &tm); + ret += bputs(buf, cbuf); + + if (precision == -1) + precision = 3; + ret += do_subsec(buf, real_ts, precision, flags); + } else { + size_t len; + + asctime_r(&tm, cbuf); + + len = strlen(cbuf); + if (!len) + /* WTF. */ + return 0; + if (cbuf[len - 1] == '\n') + cbuf[len - 1] = '\0'; + + ret += bputs(buf, cbuf); + } + return ret; +} + +static ssize_t printfrr_reltime(struct fbuf *buf, struct printfrr_eargs *ea, + const struct timespec *ts, unsigned int flags) +{ + struct timespec real_ts[1]; + ssize_t ret = 0; + const char *space = ""; + const char *dashes = "-"; + int precision = ea->precision; + + while (ea->fmt[0]) { + char ch = *ea->fmt++; + + switch (ch) { + case 'p': + flags |= TIMEFMT_SPACE; + space = " "; + continue; + case 't': + flags |= TIMEFMT_BASIC; + continue; + case 'd': + flags |= TIMEFMT_DECIMAL; + continue; + case 'm': + flags |= TIMEFMT_MMSS; + dashes = "--:--"; + continue; + case 'h': + flags |= TIMEFMT_HHMMSS; + dashes = "--:--:--"; + continue; + case 'x': + flags |= TIMEFMT_DASHES; + continue; + } + + ea->fmt--; + break; + } + + if (flags & TIMEFMT_SKIP) + return 0; + + if (flags & TIMEFMT_ABSOLUTE) { + struct timespec anchor[1]; + + if (flags & TIMEFMT_REALTIME) + clock_gettime(CLOCK_REALTIME, anchor); + else + clock_gettime(CLOCK_MONOTONIC, anchor); + if (flags & TIMEFMT_UNTIL) + timespecsub(ts, anchor, real_ts); + else /* flags & TIMEFMT_SINCE */ + timespecsub(anchor, ts, real_ts); + } else + *real_ts = *ts; + + if (real_ts->tv_sec == 0 && real_ts->tv_nsec == 0 && + (flags & TIMEFMT_DASHES)) + return bputs(buf, dashes); + + if (real_ts->tv_sec < 0) { + if (flags & TIMEFMT_DASHES) + return bputs(buf, dashes); + + /* -0.3s is { -1s + 700ms } */ + real_ts->tv_sec = -real_ts->tv_sec - 1; + real_ts->tv_nsec = 1000000000L - real_ts->tv_nsec; + if (real_ts->tv_nsec >= 1000000000L) { + real_ts->tv_sec++; + real_ts->tv_nsec -= 1000000000L; + } + + /* all formats have a - make sense in front */ + ret += bputch(buf, '-'); + } + + if (flags & TIMEFMT_DECIMAL) { + ret += bprintfrr(buf, "%lld", (long long)real_ts->tv_sec); + if (precision == -1) + precision = 3; + ret += do_subsec(buf, real_ts, precision, flags); + return ret; + } + + /* these divisions may be slow on embedded boxes, hence only do the + * ones we need, plus the ?: zero check to hopefully skip zeros fast + */ + lldiv_t min_sec = lldiv(real_ts->tv_sec, 60); + + if (flags & TIMEFMT_MMSS) { + ret += bprintfrr(buf, "%02lld:%02lld", min_sec.quot, + min_sec.rem); + ret += do_subsec(buf, real_ts, precision, flags); + return ret; + } + + lldiv_t hour_min = min_sec.quot ? lldiv(min_sec.quot, 60) : (lldiv_t){}; + + if (flags & TIMEFMT_HHMMSS) { + ret += bprintfrr(buf, "%02lld:%02lld:%02lld", hour_min.quot, + hour_min.rem, min_sec.rem); + ret += do_subsec(buf, real_ts, precision, flags); + return ret; + } + + lldiv_t day_hour = + hour_min.quot ? lldiv(hour_min.quot, 24) : (lldiv_t){}; + lldiv_t week_day = + day_hour.quot ? lldiv(day_hour.quot, 7) : (lldiv_t){}; + + /* if sub-second precision is not supported, return */ + if (flags & TIMEFMT_BASIC) { + /* match frrtime_to_interval (without space flag) */ + if (week_day.quot) + ret += bprintfrr(buf, "%lldw%s%lldd%s%02lldh", + week_day.quot, space, week_day.rem, + space, day_hour.rem); + else if (day_hour.quot) + ret += bprintfrr(buf, "%lldd%s%02lldh%s%02lldm", + day_hour.quot, space, day_hour.rem, + space, hour_min.rem); + else + ret += bprintfrr(buf, "%02lld:%02lld:%02lld", + hour_min.quot, hour_min.rem, + min_sec.rem); + /* no sub-seconds here */ + return ret; + } + + /* default format */ + if (week_day.quot) + ret += bprintfrr(buf, "%lldw%s", week_day.quot, space); + if (week_day.rem || week_day.quot) + ret += bprintfrr(buf, "%lldd%s", week_day.rem, space); + + ret += bprintfrr(buf, "%02lld:%02lld:%02lld", day_hour.rem, + hour_min.rem, min_sec.rem); + + if (precision == -1) + precision = 3; + ret += do_subsec(buf, real_ts, precision, flags); + return ret; +} + +printfrr_ext_autoreg_p("TS", printfrr_ts) +static ssize_t printfrr_ts(struct fbuf *buf, struct printfrr_eargs *ea, + const void *vptr) +{ + const struct timespec *ts = vptr; + + if (!ts) + return bputs(buf, "(null)"); + return printfrr_time(buf, ea, ts, 0); +} + +printfrr_ext_autoreg_p("TV", printfrr_tv) +static ssize_t printfrr_tv(struct fbuf *buf, struct printfrr_eargs *ea, + const void *vptr) +{ + const struct timeval *tv = vptr; + struct timespec ts; + + if (!tv) + return bputs(buf, "(null)"); + + ts.tv_sec = tv->tv_sec; + ts.tv_nsec = tv->tv_usec * 1000; + return printfrr_time(buf, ea, &ts, 0); +} + +printfrr_ext_autoreg_p("TT", printfrr_tt) +static ssize_t printfrr_tt(struct fbuf *buf, struct printfrr_eargs *ea, + const void *vptr) +{ + const time_t *tt = vptr; + struct timespec ts; + + if (!tt) + return bputs(buf, "(null)"); + + ts.tv_sec = *tt; + ts.tv_nsec = 0; + return printfrr_time(buf, ea, &ts, TIMEFMT_SECONDS); +} diff --git a/tests/lib/test_printfrr.c b/tests/lib/test_printfrr.c index 55aa9bdefc..8f9d637afd 100644 --- a/tests/lib/test_printfrr.c +++ b/tests/lib/test_printfrr.c @@ -278,5 +278,115 @@ int main(int argc, char **argv) inet_pton(AF_INET6, "fe2c::34", &nh.gate.ipv6); printchk("fe2c::34", "%pNHcg", &nh); + /* time printing */ + + /* need a non-UTC timezone for testing */ + setenv("TZ", "TEST-01:00", 1); + tzset(); + + struct timespec ts; + struct timeval tv; + time_t tt; + + ts.tv_sec = tv.tv_sec = tt = 1642015880; + ts.tv_nsec = 123456789; + tv.tv_usec = 234567; + + printchk("Wed Jan 12 20:31:20 2022", "%pTSR", &ts); + printchk("Wed Jan 12 20:31:20 2022", "%pTVR", &tv); + printchk("Wed Jan 12 20:31:20 2022", "%pTTR", &tt); + + FMT_NSTD(printchk("Wed Jan 12 20:31:20 2022", "%.3pTSR", &ts)); + + printchk("2022-01-12T20:31:20.123", "%pTSRi", &ts); + printchk("2022-01-12 20:31:20.123", "%pTSRip", &ts); + printchk("2022-01-12 20:31:20.123", "%pTSRpi", &ts); + FMT_NSTD(printchk("2022-01-12T20:31:20", "%.0pTSRi", &ts)); + FMT_NSTD(printchk("2022-01-12T20:31:20.123456789", "%.9pTSRi", &ts)); + FMT_NSTD(printchk("2022-01-12T20:31:20", "%.3pTTRi", &tt)); + + ts.tv_sec = tv.tv_sec = tt = 9 * 86400 + 12345; + + printchk("1w 2d 03:25:45.123", "%pTSIp", &ts); + printchk("1w2d03:25:45.123", "%pTSI", &ts); + printchk("1w2d03:25:45.234", "%pTVI", &tv); + printchk("1w2d03:25:45", "%pTTI", &tt); + + printchk("1w 2d 03h", "%pTVItp", &tv); + printchk("1w2d03h", "%pTSIt", &ts); + + printchk("219:25:45", "%pTVIh", &tv); + printchk("13165:45", "%pTVIm", &tv); + + ts.tv_sec = tv.tv_sec = tt = 1 * 86400 + 12345; + + printchk("1d 03:25:45.123", "%pTSIp", &ts); + printchk("1d03:25:45.234", "%pTVI", &tv); + + printchk("1d 03h 25m", "%pTVItp", &tv); + printchk("1d03h25m", "%pTSIt", &ts); + + printchk("98745.234", "%pTVId", &tv); + + printchk("27:25:45", "%pTVIh", &tv); + printchk("1645:45", "%pTVIm", &tv); + + ts.tv_sec = tv.tv_sec = tt = 12345; + + printchk("03:25:45.123", "%pTSIp", &ts); + printchk("03:25:45.123", "%pTSI", &ts); + printchk("03:25:45.234", "%pTVI", &tv); + printchk("03:25:45", "%pTTI", &tt); + + printchk("03:25:45", "%pTSItp", &ts); + printchk("03:25:45", "%pTVIt", &tv); + + printchk("12345.234", "%pTVId", &tv); + + printchk("03:25:45", "%pTVIh", &tv); + printchk("205:45", "%pTVIm", &tv); + + ts.tv_sec = tv.tv_sec = tt = 0; + + printchk("00:00:00.123", "%pTSIp", &ts); + printchk("00:00:00.123", "%pTSI", &ts); + printchk("00:00:00.234", "%pTVI", &tv); + printchk("00:00:00", "%pTTI", &tt); + + printchk("00:00:00", "%pTVItp", &tv); + printchk("00:00:00", "%pTSIt", &ts); + + printchk("0.234", "%pTVId", &tv); + printchk("0.234", "%pTVIdx", &tv); + printchk("-", "%pTTIdx", &tt); + + printchk("00:00:00", "%pTVIhx", &tv); + printchk("--:--:--", "%pTTIhx", &tt); + printchk("00:00", "%pTVImx", &tv); + printchk("--:--", "%pTTImx", &tt); + + ts.tv_sec = tv.tv_sec = tt = -10; + + printchk("-00:00:09.876", "%pTSIp", &ts); + printchk("-00:00:09.876", "%pTSI", &ts); + printchk("-00:00:09.765", "%pTVI", &tv); + printchk("-00:00:10", "%pTTI", &tt); + + printchk("-00:00:09", "%pTSItp", &ts); + printchk("-00:00:09", "%pTSIt", &ts); + printchk("-00:00:09", "%pTVIt", &tv); + printchk("-00:00:10", "%pTTIt", &tt); + + printchk("-9.765", "%pTVId", &tv); + printchk("-", "%pTVIdx", &tv); + + printchk("-00:00:09", "%pTSIh", &ts); + printchk("--:--:--", "%pTVIhx", &tv); + printchk("--:--:--", "%pTTIhx", &tt); + + printchk("-00:09", "%pTSIm", &ts); + printchk("--:--", "%pTVImx", &tv); + printchk("--:--", "%pTTImx", &tt); + return !!errors; } -- 2.39.5