git: 758d7ce63a61 - stable/15 - date: Improve nanosecond support

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Wed, 19 Nov 2025 10:57:51 UTC
The branch stable/15 has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=758d7ce63a617a5bb1d18b5259216a753587e057

commit 758d7ce63a617a5bb1d18b5259216a753587e057
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-19 10:56:50 +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
    
    (cherry picked from commit 38839c872e7af6a1424009bf07d6b4450e9ca61d)
---
 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"