File dp_isotime.h¶
File List > doppler > dp_isotime.h
Go to the documentation of this file
#ifndef DP_ISOTIME_H
#define DP_ISOTIME_H
/* clock_gettime is POSIX, and the feature-test macro that
exposes it is on the COMPILE LINE -- see the top-level CMakeLists.txt,
which puts it on the exported target too, so a downstream inherits it.
This header used to raise _POSIX_C_SOURCE itself, which is a no-op for any
translation unit that reached libc first (doppler#986); the comment on it
also claimed doppler "builds as strict c99", and it does not --
CMAKE_C_EXTENSIONS defaults ON, so the dialect is gnu99. */
#include <stdint.h>
#include <stdio.h>
#include <limits.h>
#include <string.h>
#include <time.h>
/* Windows has no `clock_gettime`, and its replacement lives in <time.h> --
so this header does NOT pull in <windows.h>. That matters for a public
header: windows.h is enormous and drags macros (min/max, near/far) into
every downstream translation unit that includes doppler.
*
* C11's `timespec_get(&ts, TIME_UTC)` is the standard spelling of "wall
* clock with nanoseconds" and the UCRT provides it. POSIX keeps
* clock_gettime, which is the one with a documented clock ID rather than an
* implementation-defined base. (Breaking the instant into fields needs no
* platform call at all: see dp_isotime_utc_tm_.) */
#ifdef _WIN32
static inline int
dp_isotime_wall (struct timespec *ts)
{
return timespec_get (ts, TIME_UTC) == TIME_UTC ? 0 : -1;
}
#else
static inline int
dp_isotime_wall (struct timespec *ts)
{
return clock_gettime (CLOCK_REALTIME, ts);
}
#endif
#ifdef __cplusplus
extern "C"
{
#endif
#define DP_ISOTIME_MAX 32
#define DP_ISOTIME_SEC 0u
#define DP_ISOTIME_MSEC 3u
#define DP_ISOTIME_USEC 6u
#define DP_ISOTIME_NSEC 9u
#define DP_ISOTIME_BASIC 0
#define DP_ISOTIME_EXTENDED 1
static inline int
dp_isotime_utc_tm_ (int64_t sec, struct tm *out)
{
/* floor division: 86400 * days + rem with rem in [0, 86399], also for
sec < 0, which C's truncating `/` would get wrong by one day. */
int64_t days = sec / 86400;
int64_t rem = sec % 86400;
if (rem < 0)
{
rem += 86400;
days -= 1;
}
int64_t z = days + 719468;
const int64_t era = (z >= 0 ? z : z - 146096) / 146097;
const unsigned doe = (unsigned) (z - era * 146097);
const unsigned yoe
= (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
const unsigned mp = (5 * doy + 2) / 153;
const unsigned d = doy - (153 * mp + 2) / 5 + 1;
const unsigned m = mp < 10 ? mp + 3 : mp - 9;
const int64_t y = (int64_t) yoe + era * 400 + (m <= 2);
if (y - 1900 > INT_MAX || y - 1900 < INT_MIN)
return -1;
static const int cum[12]
= { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
/* A remainder of zero is sign-agnostic in C, so this is the Gregorian
rule for negative years too. */
const int leap = (y % 4 == 0) && (y % 100 != 0 || y % 400 == 0);
memset (out, 0, sizeof *out);
out->tm_year = (int) (y - 1900);
out->tm_mon = (int) m - 1;
out->tm_mday = (int) d;
out->tm_hour = (int) (rem / 3600);
out->tm_min = (int) (rem % 3600 / 60);
out->tm_sec = (int) (rem % 60);
out->tm_yday = cum[m - 1] + (int) d - 1 + (m > 2 && leap);
out->tm_wday = (int) (((days % 7) + 7 + 4) % 7); /* 1970-01-01: Thu */
out->tm_isdst = 0;
return 0;
}
static inline int
dp_isotime_format_as (char *buf, size_t cap, int64_t sec, uint32_t nsec,
unsigned frac, int style)
{
if (!buf || nsec > 999999999u)
return -1;
if (frac != DP_ISOTIME_SEC && frac != DP_ISOTIME_MSEC
&& frac != DP_ISOTIME_USEC && frac != DP_ISOTIME_NSEC)
return -1;
if (style != DP_ISOTIME_BASIC && style != DP_ISOTIME_EXTENDED)
return -1;
struct tm tm_utc;
if (dp_isotime_utc_tm_ (sec, &tm_utc) != 0)
return -1;
/* Basic "YYYYMMDDThhmmss" is 15 chars and carries no colons; extended
"YYYY-MM-DDThh:mm:ss" is 19 and does. Only the separators differ, so
the calendar break-down above is done once for both. */
char date[24];
if (strftime (date, sizeof date,
style == DP_ISOTIME_EXTENDED ? "%Y-%m-%dT%H:%M:%S"
: "%Y%m%dT%H%M%S",
&tm_utc)
== 0)
return -1;
if (frac == DP_ISOTIME_SEC)
{
int n = snprintf (buf, cap, "%sZ", date);
return (n < 0 || (size_t) n >= cap) ? -1 : n;
}
/* TRUNCATE. `nsec / 10^(9-frac)` is integer division, so .999888777
becomes .999 at millisecond precision rather than carrying to 1.000
and stepping the seconds field. Never use a rounding conversion. */
uint32_t scale = 1u;
for (unsigned i = frac; i < DP_ISOTIME_NSEC; i++)
scale *= 10u;
int n = snprintf (buf, cap, "%s.%0*luZ", date, (int) frac,
(unsigned long) (nsec / scale));
return (n < 0 || (size_t) n >= cap) ? -1 : n;
}
static inline int
dp_isotime_format (char *buf, size_t cap, int64_t sec, uint32_t nsec,
unsigned frac)
{
return dp_isotime_format_as (buf, cap, sec, nsec, frac,
DP_ISOTIME_BASIC);
}
static inline int
dp_isotime_now (char *buf, size_t cap, unsigned frac)
{
struct timespec ts;
if (dp_isotime_wall (&ts) != 0)
return -1;
return dp_isotime_format (buf, cap, (int64_t) ts.tv_sec,
(uint32_t) ts.tv_nsec, frac);
}
/* ── parsing ───────────────────────────────────────────────────────────
*
* The inverse of the formatter above, in this file for the reason the two
* spellings share one formatter: a parser elsewhere is a second opinion
* about what this library's own timestamps mean.
*/
static inline int
dp_isotime_digits_ (const char **p, int n, int *out)
{
int v = 0;
for (int i = 0; i < n; i++)
{
char c = (*p)[i];
if (c < '0' || c > '9')
return -1;
v = v * 10 + (c - '0');
}
*p += n;
*out = v;
return 0;
}
static inline int64_t
dp_isotime_days_from_civil_ (int64_t y, unsigned m, unsigned d)
{
y -= (m <= 2);
const int64_t era = (y >= 0 ? y : y - 399) / 400;
const unsigned yoe = (unsigned)(y - era * 400);
const unsigned doy
= (153u * (m + (m > 2u ? (unsigned)-3 : 9u)) + 2u) / 5u + d - 1u;
const unsigned doe = yoe * 365u + yoe / 4u - yoe / 100u + doy;
return era * 146097 + (int64_t)doe - 719468;
}
static inline int
dp_isotime_parse (const char *s, int64_t *sec, uint32_t *nsec)
{
if (!s || !sec || !nsec)
return -1;
const char *p = s;
int Y, Mo, D, h, mi, se;
/* The shortest legal input is the basic form with no fraction,
`YYYYMMDDThhmmssZ` -- 16 characters. Checked BEFORE s[4] is read,
because a shorter string makes that read run off the end of the
caller's buffer rather than merely reaching a NUL: measured by ASan on
`dp_isotime_parse ("")`, which is one of this parser's own reject
cases. */
for (int i = 0; i < 16; i++)
if (s[i] == '\0')
return -1;
/* One format, read two ways: the separators are present or they are not,
and s[4] is where they first differ. Mixed spellings are rejected by
the very checks that skip the separators. */
const int ext = (s[4] == '-');
if (dp_isotime_digits_ (&p, 4, &Y) != 0)
return -1;
if (ext && *p++ != '-')
return -1;
if (dp_isotime_digits_ (&p, 2, &Mo) != 0)
return -1;
if (ext && *p++ != '-')
return -1;
if (dp_isotime_digits_ (&p, 2, &D) != 0)
return -1;
if (*p != 'T' && *p != 't' && *p != ' ')
return -1;
p++;
if (dp_isotime_digits_ (&p, 2, &h) != 0)
return -1;
if (ext && *p++ != ':')
return -1;
if (dp_isotime_digits_ (&p, 2, &mi) != 0)
return -1;
if (ext && *p++ != ':')
return -1;
if (dp_isotime_digits_ (&p, 2, &se) != 0)
return -1;
/* A leap second is spelled :60 and is a real instant on the wire; every
other field is range-checked so a transposed one cannot slide through
as a plausible date. */
if (Mo < 1 || Mo > 12 || D < 1 || D > 31 || h > 23 || mi > 59 || se > 60)
return -1;
uint32_t frac = 0;
if (*p == '.' || *p == ',')
{
p++;
int digits = 0;
if (*p < '0' || *p > '9')
return -1;
/* Scaled to nanoseconds as it is read, so `.5` is 500000000 and not
5: the field is a FRACTION, not an integer count of anything. */
for (; *p >= '0' && *p <= '9'; p++, digits++)
if (digits < 9)
frac = frac * 10u + (uint32_t)(*p - '0');
for (int i = digits; i < 9; i++)
frac *= 10u;
}
int64_t offset = 0;
if (*p == 'Z' || *p == 'z')
p++;
else if (*p == '+' || *p == '-')
{
const int neg = (*p == '-');
p++;
int oh, om = 0;
if (dp_isotime_digits_ (&p, 2, &oh) != 0)
return -1;
if (*p == ':')
p++;
if (*p >= '0' && *p <= '9' && dp_isotime_digits_ (&p, 2, &om) != 0)
return -1;
if (oh > 23 || om > 59)
return -1;
offset = (int64_t)oh * 3600 + (int64_t)om * 60;
if (neg)
offset = -offset;
}
else
return -1; /* no zone: see the note above -- refused, never assumed */
if (*p != '\0')
return -1;
const int64_t days
= dp_isotime_days_from_civil_ ((int64_t)Y, (unsigned)Mo, (unsigned)D);
*sec = days * 86400 + (int64_t)h * 3600 + (int64_t)mi * 60 + se - offset;
*nsec = frac;
return 0;
}
#ifdef __cplusplus
}
#endif
#endif /* DP_ISOTIME_H */