date: Improve nanosecond support

Add support for a field width, which defaults to 9 if unspecified or
zero.  If the width is not exactly 9, we have to either cut off digits
or append zeroes to make up the difference.  If the width is a dash,
we pick a width based on the clock's reported resolution.  This brings
us in line with GNU coreutils.

PR:		287080
MFC after:	1 week
Reviewed by:	0mp
Differential Revision:	https://reviews.freebsd.org/D53667
This commit is contained in:
Dag-Erling Smørgrav 2025-11-11 15:58:23 +01:00
parent eb95b990f8
commit 38839c872e
3 changed files with 147 additions and 53 deletions

View file

@ -29,7 +29,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.Dd November 5, 2025
.Dd November 10, 2025
.Dt DATE 1
.Os
.Sh NAME
@ -186,7 +186,7 @@ Print the date and time represented by
.Ar seconds ,
where
.Ar seconds
is the number of seconds since the Epoch
is the number of seconds since the Unix Epoch
(00:00:00 UTC, January 1, 1970;
see
.Xr time 3 ) ,
@ -321,20 +321,43 @@ Refer to the examples below for further details.
.Pp
An operand with a leading plus
.Pq Sq +
sign signals a user-defined format string
sign specifies a user-defined format string
which specifies the format in which to display the date and time.
The format string may contain any of the conversion specifications
described in the
.Xr strftime 3
manual page and
.Ql \&%N
for nanoseconds, as well as any arbitrary text.
manual page, as well as any arbitrary text.
.Pp
The following extensions to the regular
.Xr strftime 3
syntax are supported:
.Bl -tag -width "xxxx"
.It Cm \&% Ns Ar n Ns Cm N
Replaced by the
.Ar n Ns
-digit fractional part of the number of seconds since the Unix Epoch.
If
.Ar n
is omitted or zero, a default value of 9 is used, resulting in a
number with nanosecond resolution (hence the choice of the letter
.Sq N
for this conversion).
Note that the underlying clock may not necessarily support nanosecond
resolution.
.It Cm \&%-N
As above, but automatically choose the precision based on the reported
resolution of the underlying clock.
If the
.Fl r
option was specified, the default precision of 9 digits is used.
.El
.Pp
A newline
.Pq Ql \en
character is always output after the characters specified by
the format string.
The format string for the default display is
.Dq +%+ .
.Dq %+ .
.Pp
If an operand does not have a leading plus sign, it is interpreted as
a value for setting the system's notion of the current date and time.
@ -448,6 +471,13 @@ The
utility exits 0 on success, 1 if unable to set the date, and 2
if able to set the local date, but unable to set it globally.
.Sh EXAMPLES
The command
.Pp
.Dl "date +%s.%3N"
.Pp
will print the time elapsed since the Unix Epoch with millisecond
precision.
.Pp
The command:
.Pp
.Dl "date ""+DATE: %Y-%m-%d%nTIME: %H:%M:%S"""
@ -619,3 +649,9 @@ The
.Ql \&%N
conversion specification was added in
.Fx 14.1 .
Support for the
.Ql \&% Ns Ar n Ns Cm N
and
.Ql \&%-N
variants was added in
.Fx 15.1 .

View file

@ -36,6 +36,7 @@
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <inttypes.h>
#include <locale.h>
#include <stdbool.h>
#include <stdio.h>
@ -55,10 +56,10 @@ static void badformat(void);
static void iso8601_usage(const char *) __dead2;
static void multipleformats(void);
static void printdate(const char *);
static void printisodate(struct tm *, long);
static void printisodate(struct tm *, long, long);
static void setthetime(const char *, const char *, int, struct timespec *);
static size_t strftime_ns(char * __restrict, size_t, const char * __restrict,
const struct tm * __restrict, long);
const struct tm * __restrict, long, long);
static void usage(void) __dead2;
static const struct iso8601_fmt {
@ -78,26 +79,24 @@ static const char *rfc2822_format = "%a, %d %b %Y %T %z";
int
main(int argc, char *argv[])
{
struct timespec ts;
struct timespec ts = { 0, 0 }, tres = { 0, 1 };
int ch, rflag;
bool Iflag, jflag, Rflag;
const char *format;
char buf[1024];
char *fmt, *outzone = NULL;
char *tmp;
char *end, *fmt, *outzone = NULL;
struct vary *v;
const struct vary *badv;
struct tm *lt;
struct stat sb;
size_t i;
intmax_t number;
v = NULL;
fmt = NULL;
(void) setlocale(LC_TIME, "");
rflag = 0;
Iflag = jflag = Rflag = 0;
ts.tv_sec = 0;
ts.tv_nsec = 0;
while ((ch = getopt(argc, argv, "f:I::jnRr:uv:z:")) != -1)
switch((char)ch) {
case 'f':
@ -131,13 +130,15 @@ main(int argc, char *argv[])
break;
case 'r': /* user specified seconds */
rflag = 1;
ts.tv_sec = strtoq(optarg, &tmp, 0);
if (*tmp != 0) {
if (stat(optarg, &sb) == 0) {
ts.tv_sec = sb.st_mtim.tv_sec;
ts.tv_nsec = sb.st_mtim.tv_nsec;
} else
usage();
number = strtoimax(optarg, &end, 0);
if (end > optarg && *end == '\0') {
ts.tv_sec = number;
ts.tv_nsec = 0;
} else if (stat(optarg, &sb) == 0) {
ts.tv_sec = sb.st_mtim.tv_sec;
ts.tv_nsec = sb.st_mtim.tv_nsec;
} else {
usage();
}
break;
case 'u': /* do everything in UTC */
@ -155,8 +156,12 @@ main(int argc, char *argv[])
argc -= optind;
argv += optind;
if (!rflag && clock_gettime(CLOCK_REALTIME, &ts) == -1)
err(1, "clock_gettime");
if (!rflag) {
if (clock_gettime(CLOCK_REALTIME, &ts) == -1)
err(1, "clock_gettime");
if (clock_getres(CLOCK_REALTIME, &tres) == -1)
err(1, "clock_getres");
}
format = "%+";
@ -191,14 +196,14 @@ main(int argc, char *argv[])
badv = vary_apply(v, lt);
if (badv) {
fprintf(stderr, "%s: Cannot apply date adjustment\n",
badv->arg);
badv->arg);
vary_destroy(v);
usage();
}
vary_destroy(v);
if (Iflag)
printisodate(lt, ts.tv_nsec);
printisodate(lt, ts.tv_nsec, tres.tv_nsec);
if (format == rfc2822_format)
/*
@ -208,7 +213,8 @@ main(int argc, char *argv[])
setlocale(LC_TIME, "C");
(void)strftime_ns(buf, sizeof(buf), format, lt, ts.tv_nsec);
(void)strftime_ns(buf, sizeof(buf), format, lt,
ts.tv_nsec, tres.tv_nsec);
printdate(buf);
}
@ -222,7 +228,7 @@ printdate(const char *buf)
}
static void
printisodate(struct tm *lt, long nsec)
printisodate(struct tm *lt, long nsec, long res)
{
const struct iso8601_fmt *it;
char fmtbuf[64], buf[64], tzbuf[8];
@ -231,10 +237,10 @@ printisodate(struct tm *lt, long nsec)
for (it = iso8601_fmts; it <= iso8601_selected; it++)
strlcat(fmtbuf, it->format_string, sizeof(fmtbuf));
(void)strftime_ns(buf, sizeof(buf), fmtbuf, lt, nsec);
(void)strftime_ns(buf, sizeof(buf), fmtbuf, lt, nsec, res);
if (iso8601_selected > iso8601_fmts) {
(void)strftime_ns(tzbuf, sizeof(tzbuf), "%z", lt, nsec);
(void)strftime_ns(tzbuf, sizeof(tzbuf), "%z", lt, nsec, res);
memmove(&tzbuf[4], &tzbuf[3], 3);
tzbuf[3] = ':';
strlcat(buf, tzbuf, sizeof(buf));
@ -370,16 +376,17 @@ setthetime(const char *fmt, const char *p, int jflag, struct timespec *ts)
*/
static size_t
strftime_ns(char * __restrict s, size_t maxsize, const char * __restrict format,
const struct tm * __restrict t, long nsec)
const struct tm * __restrict t, long nsec, long res)
{
size_t prefixlen;
size_t ret;
char *newformat;
char *oldformat;
const char *prefix;
const char *suffix;
const char *tok;
bool seen_percent;
long number;
int i, len, prefixlen, width, zeroes;
bool seen_percent, seen_dash, seen_width;
seen_percent = false;
if ((newformat = strdup(format)) == NULL)
@ -392,36 +399,85 @@ strftime_ns(char * __restrict s, size_t maxsize, const char * __restrict format,
* If the previous token was a percent sign,
* then there are two percent tokens in a row.
*/
if (seen_percent)
if (seen_percent) {
seen_percent = false;
else
} else {
seen_percent = true;
seen_dash = seen_width = false;
prefixlen = tok - newformat;
width = 0;
}
break;
case 'N':
if (seen_percent) {
oldformat = newformat;
prefix = oldformat;
prefixlen = tok - oldformat - 1;
suffix = tok + 1;
if (!seen_percent)
break;
oldformat = newformat;
prefix = oldformat;
suffix = tok + 1;
/*
* Prepare the number we are about to print. If
* the requested width is less than 9, we need to
* cut off the least significant digits. If it is
* more than 9, we will have to append zeroes.
*/
if (seen_dash) {
/*
* Construct a new format string from the
* prefix (i.e., the part of the old format
* from its beginning to the currently handled
* "%N" conversion specification), the
* nanoseconds, and the suffix (i.e., the part
* of the old format from the next token to the
* end).
* Calculate number of singificant digits
* based on res which is the clock's
* resolution in nanoseconds.
*/
if (asprintf(&newformat, "%.*s%.9ld%s",
(int)prefixlen, prefix, nsec,
suffix) < 0) {
err(1, "asprintf");
}
free(oldformat);
tok = newformat + prefixlen + 9;
for (width = 9, number = res;
width > 0 && number > 0;
width--, number /= 10)
/* nothing */;
}
number = nsec;
zeroes = 0;
if (width == 0) {
width = 9;
} else if (width > 9) {
zeroes = width - 9;
width = 9;
} else {
for (i = 0; i < 9 - width; i++)
number /= 10;
}
/*
* Construct a new format string from the prefix
* (i.e., the part of the old format from its
* beginning to the currently handled "%N"
* conversion specification), the nanoseconds, and
* the suffix (i.e., the part of the old format
* from the next token to the end).
*/
asprintf(&newformat, "%.*s%.*ld%.*d%n%s", prefixlen,
prefix, width, number, zeroes, 0, &len, suffix);
if (newformat == NULL)
err(1, "asprintf");
free(oldformat);
tok = newformat + len - 1;
seen_percent = false;
break;
case '-':
if (seen_percent) {
if (seen_dash || seen_width) {
seen_percent = false;
break;
}
seen_dash = true;
}
break;
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
if (seen_percent) {
if (seen_dash) {
seen_percent = false;
break;
}
width = width * 10 + *tok - '0';
seen_width = true;
}
break;
default:
seen_percent = false;
break;

View file

@ -132,6 +132,8 @@ atf_init_test_cases()
format_string_test M M 04 20
format_string_test m m 02 11
format_string_test N N 000000000 000000000
format_string_test 3N 3N 000 000
format_string_test 12N 12N 000000000000 000000000000
format_string_test p p AM PM
format_string_test R R 07:04 21:20
format_string_test r r "07:04:03 AM" "09:20:00 PM"