The branch main has been updated by des:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=38839c872e7af6a1424009bf07d6b4450e9ca61d

commit 38839c872e7af6a1424009bf07d6b4450e9ca61d
Author:     Dag-Erling Smørgrav <[email protected]>
AuthorDate: 2025-11-11 14:58:23 +0000
Commit:     Dag-Erling Smørgrav <[email protected]>
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"

Reply via email to