git: 38839c872e7a - main - date: Improve nanosecond support
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 11 Nov 2025 14:58:43 UTC
The branch main has been updated by des:
URL: https://cgit.FreeBSD.org/src/commit/?id=38839c872e7af6a1424009bf07d6b4450e9ca61d
commit 38839c872e7af6a1424009bf07d6b4450e9ca61d
Author: Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-11-11 14:58:23 +0000
Commit: Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-11-11 14:58:35 +0000
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
---
bin/date/date.1 | 50 ++++++++++--
bin/date/date.c | 148 ++++++++++++++++++++++++-----------
bin/date/tests/format_string_test.sh | 2 +
3 files changed, 147 insertions(+), 53 deletions(-)
diff --git a/bin/date/date.1 b/bin/date/date.1
index f68892bd408d..374a687fcbdc 100644
--- a/bin/date/date.1
+++ b/bin/date/date.1
@@ -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 .
diff --git a/bin/date/date.c b/bin/date/date.c
index 01797084c0d6..9a40ac43d58b 100644
--- a/bin/date/date.c
+++ b/bin/date/date.c
@@ -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;
diff --git a/bin/date/tests/format_string_test.sh b/bin/date/tests/format_string_test.sh
index c2fe2111373f..5f199a3b5fd6 100755
--- a/bin/date/tests/format_string_test.sh
+++ b/bin/date/tests/format_string_test.sh
@@ -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"