Subject: Re: bug#7325: new test failure due to non-portability of printf formats like %05.3s

From: Paul Eggert <[email-masked]>
Date: Fri, 05 Nov 2010 19:54:28 -0700

On 11/04/2010 11:34 PM, Jim Meyering wrote:
> Pádraig Brady wrote:
>> I still slightly prefer just using %.X as
>> it's backwards compat with older coreutils (excluding 8.6).
> 
> So do I.

I built that and tried it out, and found another problem that has been
annoying me for years in other programs: 'stat' prints out unnecessary
trailing zeros for time stamps, when the file system resolution is
coarser than nanosecond resolution.  So I figured I'd fix that too,
for 'stat' (other programs can be fixed later).  With this fix, %.X
outputs the time stamp but omits trailing zeros if it can infer that
the file system would always put zeros there.  If you want a specific
number of zeros, you can use something like %.3X or %.9X (or %.100X :-).

>From d37bad3d644a98887402b5398b7280701a7b7559 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Fri, 5 Nov 2010 19:35:12 -0700
Subject: [PATCH] stat: use e.g. %.3X instead of %X.%3:X for sub-second precision

* NEWS: Document this.
* doc/coreutils.texi (stat invocation): Likewise.
* gl/lib/fstimeprec.c, gl/lib/fstimeprec.h, gl/modules/fstimeprec:
* gl/modules/fstimeprec-tests, gl/tests/test-fstimeprec.c:
New files.
* bootstrap.conf (gnulib_modules): Add fstimeprec.
* src/stat.c: Include fstimeprec.h.  Don't include xstrtol.h.
(decimal_point, decimal_point_len): New static vars.
(main): Initialize them.
(epoch_sec, out_ns): Remove.
(out_int, out_uint): Now returns whatever printf returned.
(out_minus_zero, out_epoch_secs): New functions.
(print_stat): Use out_epoch_sec instead of out_ns and epoch_sec.
(print_stat, print_it, usage): Remove the %:X-style formats.
* tests/misc/stat-nanoseconds: Set TZ=UTC0 to avoid problems
with weird time zones.  Use a time stamp near the Epoch so that we
don't have to worry about leap seconds.  Redo test cases to match
new behavior.
* tests/touch/60-seconds: Change %Y.%:Y to %.9Y, to adjust to
new behavior.
---
 NEWS                        |   10 +--
 bootstrap.conf              |    1 +
 doc/coreutils.texi          |   37 ++++----
 gl/lib/fstimeprec.c         |  178 +++++++++++++++++++++++++++++++++++
 gl/lib/fstimeprec.h         |   22 +++++
 gl/modules/fstimeprec       |   24 +++++
 gl/modules/fstimeprec-tests |   10 ++
 gl/tests/test-fstimeprec.c  |   74 +++++++++++++++
 src/stat.c                  |  218 ++++++++++++++++++++++++++-----------------
 tests/misc/stat-nanoseconds |   25 +++--
 tests/touch/60-seconds      |    2 +-
 11 files changed, 480 insertions(+), 121 deletions(-)
 create mode 100644 gl/lib/fstimeprec.c
 create mode 100644 gl/lib/fstimeprec.h
 create mode 100644 gl/modules/fstimeprec
 create mode 100644 gl/modules/fstimeprec-tests
 create mode 100644 gl/tests/test-fstimeprec.c

diff --git a/NEWS b/NEWS
index 3b9bea2..1e5a8db 100644
--- a/NEWS
+++ b/NEWS
@@ -20,16 +20,12 @@ GNU coreutils NEWS                                    -*- outline -*-
   stat's %X, %Y, and %Z directives once again print only the integer
   part of seconds since the epoch.  This reverts a change from
   coreutils-8.6, that was deemed unnecessarily disruptive.  To obtain
-  the nanoseconds portion corresponding to %X, you may now use %:X.
-  I.e., to print the floating point number of seconds using maximum
-  precision, use this format string: %X.%:X.  Likewise for %Y, %Z and %W.
+  a full resolution time stamp for %X, use %.X; if you want (say) just
+  3 fractional digits, use %.3X.  Likewise for %Y and %Z.
 
   stat's new %W format directive would print floating point seconds.
   However, with the above change to %X, %Y and %Z, we've made %W work
-  the same way:  %W now expands to seconds since the epoch (or 0 when
-  not supported), and %:W expands to the nanoseconds portion, or to
-  0 if not supported.
-
+  the same way as the others.
 
 * Noteworthy changes in release 8.6 (2010-10-15) [stable]
 
diff --git a/bootstrap.conf b/bootstrap.conf
index 18ef914..5a6ebae 100644
--- a/bootstrap.conf
+++ b/bootstrap.conf
@@ -85,6 +85,7 @@ gnulib_modules="
   freopen
   freopen-safer
   fseeko
+  fstimeprec
   fsusage
   fsync
   ftello
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index be5999f..8dfb069 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -10707,37 +10707,36 @@ The valid @var{format} directives for files with @option{--format} and
 @item %U - User name of owner
 @item %w - Time of file birth, or @samp{-} if unknown
 @item %W - Time of file birth as seconds since Epoch, or @samp{0}
-@item %:W - Time of file birth: nanoseconds remainder, or @samp{0}
 @item %x - Time of last access
 @item %X - Time of last access as seconds since Epoch
-@item %:X - Time of last access: nanoseconds remainder
 @item %y - Time of last modification
 @item %Y - Time of last modification as seconds since Epoch
-@item %:Y - Time of last modification: nanoseconds remainder
 @item %z - Time of last change
 @item %Z - Time of last change as seconds since Epoch
-@item %:Z - Time of last change: nanoseconds remainder
 @end itemize
 
-Note that each of @samp{%:W}, @samp{%:X}, @samp{%:Y} and @samp{%:Z}
-prints its zero-padded number of nanoseconds on a field of width 9.
-However, if you specify anything between the @samp{%} and @samp{:},
-e.g., @samp{%10:X}, your modifier replaces the default of @samp{09}.
-
-Here are some examples:
+The @samp{%W}, @samp{%X}, @samp{%Y}, and @samp{%Z} formats accept a
+precision preceded by a period to specify the number of digits to
+print after the decimal point. For example, @samp{%.9X} outputs the
+last access time to nanosecond precision.  If a period is given but no
+precision, @command{stat} uses the estimated precision of the file
+system. When discarding excess precision, time stamps are truncated
+toward minus infinity.
 
 @example
 zero pad:
-  $ stat -c '[%015:Y]' /usr
-  [000000031367045]
+  $ stat -c '[%015Y]' /usr
+  [000001288929712]
 space align:
-  $ stat -c '[%15:Y]' /usr
-  [       31367045]
-  $ stat -c '[%-15:Y]' /usr
-  [31367045       ]
-truncate:
-  $ stat -c '[%.3:Y]' /usr
-  [031]
+  $ stat -c '[%15Y]' /usr
+  [     1288929712]
+  $ stat -c '[%-15Y]' /usr
+  [1288929712     ]
+precision:
+  $ stat -c '[%.3Y]' /usr
+  [1288929712.114]
+  $ stat -c '[%.Y]' /usr
+  [1288929712.114951]
 @end example
 
 The mount point printed by @samp{%m} is similar to that output
diff --git a/gl/lib/fstimeprec.c b/gl/lib/fstimeprec.c
new file mode 100644
index 0000000..fd63054
--- /dev/null
+++ b/gl/lib/fstimeprec.c
@@ -0,0 +1,178 @@
+/* Determine a file system's time stamp precision.
+
+   Copyright 2010 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* written by Paul Eggert */
+
+#include <config.h>
+#include "fstimeprec.h"
+
+#include "hash.h"
+#include "stat-time.h"
+#include <limits.h>
+#include <stdlib.h>
+
+/* A hash table that maps device numbers to precisions.  */
+struct fstimeprec
+{
+  /* Map device numbers to precisions.  */
+  struct hash_table *map;
+
+  /* Cache of the most recently allocated and otherwise-unused storage
+     for probing this table.  */
+  struct ent *probe;
+};
+
+/* A pair that maps a device number to a precision.  */
+struct ent
+{
+  dev_t dev;
+  int prec;
+};
+
+/* Hash an entry.  */
+static size_t
+ent_hash (void const *x, size_t table_size)
+{
+  struct ent const *p = x;
+  dev_t dev = p->dev;
+
+  /* FIXME: This code is duplicated from di-set.c.  */
+  /* When DEV is wider than size_t, exclusive-OR the words of DEV into H.
+     This avoids loss of info, without applying % to the wider type,
+     which could be quite slow on some systems.  */
+  size_t h = dev;
+  unsigned int i;
+  unsigned int n_words = sizeof dev / sizeof h + (sizeof dev % sizeof h != 0);
+  for (i = 1; i < n_words; i++)
+    h ^= dev >> CHAR_BIT * sizeof h * i;
+
+  return h % table_size;
+}
+
+/* Return true if two entries are the same.  */
+static bool
+ent_compare (void const *x, void const *y)
+{
+  struct ent const *a = x;
+  struct ent const *b = y;
+  return a->dev == b->dev;
+}
+
+/* Using the SET table, map a device to an entry that represents
+   the file system precision.  Return NULL on error.  */
+static struct ent *
+map_device (struct fstimeprec *tab, dev_t dev)
+{
+  /* Find space for the probe, reusing the cache if available.  */
+  struct ent *ent;
+  struct ent *probe = tab->probe;
+  if (probe)
+    {
+      /* If repeating a recent query, return the cached result.   */
+      if (probe->dev == dev)
+        return probe;
+    }
+  else
+    {
+      tab->probe = probe = malloc (sizeof *probe);
+      if (! probe)
+        return NULL;
+      probe->prec = 0;
+    }
+
+  /* Probe for the device.  */
+  probe->dev = dev;
+  ent = hash_insert (tab->map, probe);
+  if (ent == probe)
+    tab->probe = NULL;
+  return ent;
+}
+
+/* Return a newly allocated table of file system time stamp
+   resolutions, or NULL if out of memory.  */
+struct fstimeprec *
+fstimeprec_alloc (void)
+{
+  struct fstimeprec *tab = malloc (sizeof *tab);
+  if (tab)
+    {
+      enum { INITIAL_DEV_MAP_SIZE = 11 };
+      tab->map = hash_initialize (INITIAL_DEV_MAP_SIZE, NULL,
+                                  ent_hash, ent_compare, free);
+      if (! tab->map)
+        {
+          free (tab);
+          return NULL;
+        }
+      tab->probe = NULL;
+    }
+  return tab;
+}
+
+/* Free TAB.  */
+void
+fstimeprec_free (struct fstimeprec *tab)
+{
+  hash_free (tab->map);
+  free (tab->probe);
+  free (tab);
+}
+
+/* Using the cached information in TAB, return the estimated precision
+   of time stamps for the file system containing the file whose status
+   info is in ST.  The returned value counts the number of decimal
+   fraction digits.  Return 0 on failure.
+
+   If TAB is null, the guess is based purely on ST.  */
+int
+fstimeprec (struct fstimeprec *tab, struct stat const *st)
+{
+  /* Map the device number to an entry.  */
+  struct ent *ent = (tab ? map_device (tab, st->st_dev) : NULL);
+
+  /* Guess the precision based on what has been seen so far.  */
+  int prec = (ent ? ent->prec : 0);
+
+  /* If the current file's timestamp resolution is higher than the
+     guess, increase the guess.  */
+  if (prec < 9)
+    {
+      struct timespec ats = get_stat_atime (st);
+      struct timespec mts = get_stat_mtime (st);
+      struct timespec cts = get_stat_ctime (st);
+      struct timespec bts = get_stat_birthtime (st);
+      unsigned int ans = ats.tv_nsec;
+      unsigned int mns = mts.tv_nsec;
+      unsigned int cns = cts.tv_nsec;
+      unsigned int bns = bts.tv_nsec < 0 ? 0 : bts.tv_nsec;
+      unsigned int power = 10;
+      int p;
+      for (p = prec + 1; p < 9; p++)
+        power *= 10;
+
+      while (ans % power | mns % power | cns % power | bns % power)
+        {
+          power /= 10;
+          prec++;
+        }
+
+      if (ent)
+        ent->prec = prec;
+    }
+
+  return prec;
+}
diff --git a/gl/lib/fstimeprec.h b/gl/lib/fstimeprec.h
new file mode 100644
index 0000000..8ec24df
--- /dev/null
+++ b/gl/lib/fstimeprec.h
@@ -0,0 +1,22 @@
+#include <sys/stat.h>
+
+#ifndef ATTRIBUTE_MALLOC
+# if __GNUC__ >= 3
+#  define ATTRIBUTE_MALLOC __attribute__ ((__malloc__))
+# else
+#  define ATTRIBUTE_MALLOC
+# endif
+#endif
+
+#ifndef _GL_ARG_NONNULL
+# if (__GNUC__ == 3 && __GNUC_MINOR__ >= 3) || __GNUC__ > 3
+#  define _GL_ARG_NONNULL(params) __attribute__ ((__nonnull__ params))
+# else
+#  define _GL_ARG_NONNULL(params)
+# endif
+#endif
+
+struct fstimeprec *fstimeprec_alloc (void) ATTRIBUTE_MALLOC;
+void fstimeprec_free (struct fstimeprec *) _GL_ARG_NONNULL ((1));
+int fstimeprec (struct fstimeprec *, struct stat const *)
+  _GL_ARG_NONNULL ((2));
diff --git a/gl/modules/fstimeprec b/gl/modules/fstimeprec
new file mode 100644
index 0000000..787c2e7
--- /dev/null
+++ b/gl/modules/fstimeprec
@@ -0,0 +1,24 @@
+Description:
+estimate precision of time stamps in file systems
+
+Files:
+lib/fstimeprec.c
+lib/fstimeprec.h
+
+Depends-on:
+hash
+stat-time
+
+configure.ac:
+
+Makefile.am:
+lib_SOURCES += fstimeprec.c fstimeprec.h
+
+Include:
+"fstimeprec.h"
+
+License
+GPL
+
+Maintainer:
+Paul Eggert and Jim Meyering
diff --git a/gl/modules/fstimeprec-tests b/gl/modules/fstimeprec-tests
new file mode 100644
index 0000000..bf6b49e
--- /dev/null
+++ b/gl/modules/fstimeprec-tests
@@ -0,0 +1,10 @@
+Files:
+tests/test-fstimeprec.c
+
+Depends-on:
+
+configure.ac:
+
+Makefile.am:
+TESTS += test-fstimeprec
+check_PROGRAMS += test-fstimeprec
diff --git a/gl/tests/test-fstimeprec.c b/gl/tests/test-fstimeprec.c
new file mode 100644
index 0000000..f699139
--- /dev/null
+++ b/gl/tests/test-fstimeprec.c
@@ -0,0 +1,74 @@
+/* Test the fstimeprec module.
+   Copyright (C) 2010 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* written by Paul Eggert */
+
+#include <config.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/stat.h>
+
+#define ASSERT(expr) \
+  do                                                                         \
+    {                                                                        \
+      if (!(expr))                                                           \
+        {                                                                    \
+          fprintf (stderr, "%s:%d: assertion failed\n", __FILE__, __LINE__); \
+          fflush (stderr);                                                   \
+          abort ();                                                          \
+        }                                                                    \
+    }                                                                        \
+  while (0)
+
+#include "fstimeprec.h"
+
+static int
+fstimeprec_file (struct fstimeprec *tab, char const *file)
+{
+  struct stat st;
+  if (stat (file, &st) != 0)
+    return 0;
+  return fstimeprec (tab, &st);
+}
+
+int
+main (void)
+{
+  struct fstimeprec *tab = fstimeprec_alloc ();
+  ASSERT (tab);
+
+  int m1 = fstimeprec_file (tab, "/");
+  int m2 = fstimeprec_file (tab, ".");
+  int m3 = fstimeprec_file (tab, "..");
+  ASSERT (0 <= m1 && m1 <= 9);
+  ASSERT (0 <= m2 && m2 <= 9);
+  ASSERT (0 <= m3 && m3 <= 9);
+
+  int n1 = fstimeprec_file (tab, "/");
+  int n2 = fstimeprec_file (tab, ".");
+  int n3 = fstimeprec_file (tab, "..");
+  ASSERT (0 <= n1 && n1 <= 9);
+  ASSERT (0 <= n2 && n2 <= 9);
+  ASSERT (0 <= n3 && n3 <= 9);
+
+  ASSERT (m1 <= n1);
+  ASSERT (m2 <= n2);
+  ASSERT (m3 <= n3);
+
+  fstimeprec_free (tab);
+
+  return 0;
+}
diff --git a/src/stat.c b/src/stat.c
index d05a93b..cbf6d25 100644
--- a/src/stat.c
+++ b/src/stat.c
@@ -63,6 +63,7 @@
 #include "file-type.h"
 #include "filemode.h"
 #include "fs.h"
+#include "fstimeprec.h"
 #include "getopt.h"
 #include "mountlist.h"
 #include "quote.h"
@@ -70,7 +71,6 @@
 #include "stat-time.h"
 #include "strftime.h"
 #include "find-mount-point.h"
-#include "xstrtol.h"
 #include "xvasprintf.h"
 
 #if USE_STATVFS
@@ -183,6 +183,10 @@ static bool interpret_backslash_escapes;
    "" for --printf=FMT, "\n" for --format=FMT (-c).  */
 static char const *trailing_delim = "";
 
+/* The representation of the decimal point in the current locale.  */
+static char const *decimal_point;
+static size_t decimal_point_len;
+
 /* Return the type of the specified file system.
    Some systems have statfvs.f_basetype[FSTYPSZ] (AIX, HP-UX, and Solaris).
    Others have statvfs.f_fstypename[_VFS_NAMELEN] (NetBSD 3.0).
@@ -463,69 +467,23 @@ human_time (struct timespec t)
   return str;
 }
 
-/* Return a string representation (in static storage)
-   of the number of seconds in T since the epoch.  */
-static char * ATTRIBUTE_WARN_UNUSED_RESULT
-epoch_sec (struct timespec t)
-{
-  static char str[INT_BUFSIZE_BOUND (time_t)];
-  return timetostr (t.tv_sec, str);
-}
-
-/* Output the number of nanoseconds, ARG.tv_nsec, honoring a
-   WIDTH.PRECISION format modifier, where PRECISION specifies
-   how many leading digits(on a field of 9) to print.  */
-static void
-out_ns (char *pformat, size_t prefix_len, struct timespec arg)
-{
-  /* If no format modifier is specified, i.e., nothing between the
-     "%" and ":" of "%:X", then use the default of zero-padding and
-     a width of 9.  Otherwise, use the specified modifier(s).
-     This is to avoid the mistake of omitting the zero padding on
-     a number with fewer digits than the field width: when printing
-     nanoseconds after a decimal place, the resulting floating point
-     fraction would be off by a factor of 10 or more.
-
-     If a precision/max width is specified, i.e., a '.' is present
-     in the modifier, then then treat the modifier as operating
-     on the default representation, i.e., a zero padded number
-     of width 9.  */
-  unsigned long int ns = arg.tv_nsec;
-
-  if (memchr (pformat, '.', prefix_len)) /* precision specified.  */
-    {
-      char tmp[INT_BUFSIZE_BOUND (uintmax_t)];
-      snprintf (tmp, sizeof tmp, "%09lu", ns);
-      strcpy (pformat + prefix_len, "s");
-      printf (pformat, tmp);
-    }
-  else
-    {
-      char const *fmt = (prefix_len == 1) ? "09lu" : "lu";
-      /* Note that pformat is big enough, as %:X -> %09lu
-         and two extra bytes are already allocated.  */
-      strcpy (pformat + prefix_len, fmt);
-      printf (pformat, ns);
-    }
-}
-
 static void
 out_string (char *pformat, size_t prefix_len, char const *arg)
 {
   strcpy (pformat + prefix_len, "s");
   printf (pformat, arg);
 }
-static void
+static int
 out_int (char *pformat, size_t prefix_len, intmax_t arg)
 {
   strcpy (pformat + prefix_len, PRIdMAX);
-  printf (pformat, arg);
+  return printf (pformat, arg);
 }
-static void
+static int
 out_uint (char *pformat, size_t prefix_len, uintmax_t arg)
 {
   strcpy (pformat + prefix_len, PRIuMAX);
-  printf (pformat, arg);
+  return printf (pformat, arg);
 }
 static void
 out_uint_o (char *pformat, size_t prefix_len, uintmax_t arg)
@@ -539,6 +497,120 @@ out_uint_x (char *pformat, size_t prefix_len, uintmax_t arg)
   strcpy (pformat + prefix_len, PRIxMAX);
   printf (pformat, arg);
 }
+static int
+out_minus_zero (char *pformat, size_t prefix_len)
+{
+  strcpy (pformat + prefix_len, ".0f");
+  return printf (pformat, -0.25);
+}
+
+/* Output the number of seconds since the Epoch, using a format that
+   acts like printf's %f format.  */
+static void
+out_epoch_sec (char *pformat, size_t prefix_len, struct stat const *statbuf,
+               struct timespec arg)
+{
+  char *dot = memchr (pformat, '.', prefix_len);
+  size_t sec_prefix_len = prefix_len;
+  int width = 0;
+  int precision = 0;
+  bool frac_left_adjust = false;
+
+  if (dot)
+    {
+      sec_prefix_len = dot - pformat;
+      pformat[prefix_len] = '\0';
+
+      if (ISDIGIT (dot[1]))
+        {
+          long int lprec = strtol (dot + 1, NULL, 10);
+          precision = (lprec <= INT_MAX ? lprec : INT_MAX);
+        }
+      else
+        {
+          static struct fstimeprec *tab;
+          if (! tab)
+            tab = fstimeprec_alloc ();
+          precision = fstimeprec (tab, statbuf);
+        }
+
+      if (precision && ISDIGIT (dot[-1]))
+        {
+          /* If a nontrivial width is given, subtract the width of the
+             decimal point and PRECISION digits that will be output
+             later.  */
+          char *p = dot;
+          *dot = '\0';
+
+          do
+            --p;
+          while (ISDIGIT (p[-1]));
+
+          long int lwidth = strtol (p, NULL, 10);
+          width = (lwidth <= INT_MAX ? lwidth : INT_MAX);
+          if (1 < width)
+            {
+              p += (*p == '0');
+              sec_prefix_len = p - pformat;
+              int w_d = (decimal_point_len < width
+                         ? width - decimal_point_len
+                         : 0);
+              if (1 < w_d)
+                {
+                  int w = w_d - precision;
+                  if (1 < w)
+                    {
+                      char *dst = pformat;
+                      for (char const *src = dst; src < p; src++)
+                        if (*src == '-')
+                          frac_left_adjust = true;
+                        else
+                          *dst++ = *src;
+                      sec_prefix_len =
+                        (dst - pformat
+                         + (frac_left_adjust ? 0 : sprintf (dst, "%d", w)));
+                    }
+                }
+            }
+        }
+    }
+
+  int divisor = 1;
+  for (int i = precision; i < 9; i++)
+    divisor *= 10;
+  int frac_sec = arg.tv_nsec / divisor;
+  int int_len;
+
+  if (TYPE_SIGNED (time_t))
+    {
+      bool minus_zero = false;
+      if (arg.tv_sec < 0 && arg.tv_nsec != 0)
+        {
+          int frac_sec_modulus = 1000000000 / divisor;
+          frac_sec = (frac_sec_modulus - frac_sec
+                      - (arg.tv_nsec % divisor != 0));
+          arg.tv_sec += (frac_sec != 0);
+          minus_zero = (arg.tv_sec == 0);
+        }
+      int_len = (minus_zero
+                 ? out_minus_zero (pformat, sec_prefix_len)
+                 : out_int (pformat, sec_prefix_len, arg.tv_sec));
+    }
+  else
+    int_len = out_uint (pformat, sec_prefix_len, arg.tv_sec);
+
+  if (precision)
+    {
+      int prec = (precision < 9 ? precision : 9);
+      int trailing_prec = precision - prec;
+      int ilen = (int_len < 0 ? 0 : int_len);
+      int trailing_width = (ilen < width && decimal_point_len < width - ilen
+                            ? width - ilen - decimal_point_len - prec
+                            : 0);
+      printf ("%s%.*d%-*.*d", decimal_point, prec, frac_sec,
+              trailing_width, trailing_prec, 0);
+    }
+}
 
 /* Print the context information of FILENAME, and return true iff the
    context could not be obtained.  */
@@ -853,38 +925,26 @@ print_stat (char *pformat, size_t prefix_len, unsigned int m,
       }
       break;
     case 'W':
-      out_string (pformat, prefix_len,
-                  epoch_sec (neg_to_zero (get_stat_birthtime (statbuf))));
-      break;
-    case 'W' + 256:
-      out_ns (pformat, prefix_len, neg_to_zero (get_stat_birthtime (statbuf)));
+      out_epoch_sec (pformat, prefix_len, statbuf,
+                     neg_to_zero (get_stat_birthtime (statbuf)));
       break;
     case 'x':
       out_string (pformat, prefix_len, human_time (get_stat_atime (statbuf)));
       break;
     case 'X':
-      out_string (pformat, prefix_len, epoch_sec (get_stat_atime (statbuf)));
-      break;
-    case 'X' + 256:
-      out_ns (pformat, prefix_len, get_stat_atime (statbuf));
+      out_epoch_sec (pformat, prefix_len, statbuf, get_stat_atime (statbuf));
       break;
     case 'y':
       out_string (pformat, prefix_len, human_time (get_stat_mtime (statbuf)));
       break;
     case 'Y':
-      out_string (pformat, prefix_len, epoch_sec (get_stat_mtime (statbuf)));
-      break;
-    case 'Y' + 256:
-      out_ns (pformat, prefix_len, get_stat_mtime (statbuf));
+      out_epoch_sec (pformat, prefix_len, statbuf, get_stat_mtime (statbuf));
       break;
     case 'z':
       out_string (pformat, prefix_len, human_time (get_stat_ctime (statbuf)));
       break;
     case 'Z':
-      out_string (pformat, prefix_len, epoch_sec (get_stat_ctime (statbuf)));
-      break;
-    case 'Z' + 256:
-      out_ns (pformat, prefix_len, get_stat_ctime (statbuf));
+      out_epoch_sec (pformat, prefix_len, statbuf, get_stat_ctime (statbuf));
       break;
     case 'C':
       fail |= out_file_context (pformat, prefix_len, filename);
@@ -968,21 +1028,9 @@ print_it (char const *format, char const *filename,
           {
             size_t len = strspn (b + 1, "#-+.I 0123456789");
             char const *fmt_char = b + len + 1;
-            unsigned int fmt_code;
+            unsigned int fmt_code = *fmt_char;
             memcpy (dest, b, len + 1);
 
-            /* The ":" modifier just before the letter in %W, %X, %Y, %Z
-               tells stat to print the nanoseconds portion of the date.  */
-            if (*fmt_char == ':' && strchr ("WXYZ", fmt_char[1]))
-              {
-                fmt_code = fmt_char[1] + 256;
-                ++fmt_char;
-              }
-            else
-              {
-                fmt_code = fmt_char[0];
-              }
-
             b = fmt_char;
             switch (fmt_code)
               {
@@ -1276,16 +1324,12 @@ The valid format sequences for files (without --file-system):\n\
   %U   User name of owner\n\
   %w   Time of file birth, human-readable; - if unknown\n\
   %W   Time of file birth, seconds since Epoch; 0 if unknown\n\
-  %:W  Time of file birth, nanoseconds remainder; 0 if unknown\n\
   %x   Time of last access, human-readable\n\
   %X   Time of last access, seconds since Epoch\n\
-  %:X  Time of last access, nanoseconds remainder\n\
   %y   Time of last modification, human-readable\n\
   %Y   Time of last modification, seconds since Epoch\n\
-  %:Y  Time of last modification, nanoseconds remainder\n\
   %z   Time of last change, human-readable\n\
   %Z   Time of last change, seconds since Epoch\n\
-  %:Z  Time of last change, nanoseconds remainder\n\
 \n\
 "), stdout);
 
@@ -1330,6 +1374,10 @@ main (int argc, char *argv[])
   bindtextdomain (PACKAGE, LOCALEDIR);
   textdomain (PACKAGE);
 
+  struct lconv const *locale = localeconv ();
+  decimal_point = (locale->decimal_point[0] ? locale->decimal_point : ".");
+  decimal_point_len = strlen (decimal_point);
+
   atexit (close_stdout);
 
   while ((c = getopt_long (argc, argv, "c:fLt", long_options, NULL)) != -1)
diff --git a/tests/misc/stat-nanoseconds b/tests/misc/stat-nanoseconds
index 314f43e..9071ae9 100755
--- a/tests/misc/stat-nanoseconds
+++ b/tests/misc/stat-nanoseconds
@@ -19,18 +19,25 @@
 test "$VERBOSE" = yes && stat --version
 . "${srcdir=.}/init.sh"; path_prepend_ ../src
 
-touch -d '2010-10-21 18:43:33.023456789' k || framework_failure_
+TZ=UTC0
+export TZ
+
+touch -d '1970-01-01 18:43:33.023456789' k || framework_failure_
 
 ls --full-time | grep 18:43:33.023456789 \
   || skip_ this file system does not support sub-second time stamps
 
-test "$(stat -c      %:X k)" = 023456789  || fail=1
-test "$(stat -c     %3:X k)" = 23456789   || fail=1
-test "$(stat -c   %3.3:X k)" = 023        || fail=1
-test "$(stat -c    %.3:X k)" = 023        || fail=1
-test "$(stat -c  %03.3:X k)" = 023        || fail=1
-test "$(stat -c  %-5.3:X k)" = '023  '    || fail=1
-test "$(stat -c  %05.3:X k)" = '  023'    || fail=1
-test "$(stat -c %010.3:X k)" = '       023' || fail=1
+test "$(stat -c       %X k)" =    67413               || fail=1
+test "$(stat -c      %.X k)" =    67413.023456789     || fail=1
+test "$(stat -c     %.1X k)" =    67413.0             || fail=1
+test "$(stat -c     %.3X k)" =    67413.023           || fail=1
+test "$(stat -c     %.6X k)" =    67413.023456        || fail=1
+test "$(stat -c     %.9X k)" =    67413.023456789     || fail=1
+test "$(stat -c   %13.6X k)" =  ' 67413.023456'       || fail=1
+test "$(stat -c  %013.6X k)" =   067413.023456        || fail=1
+test "$(stat -c  %-13.6X k)" =   '67413.023456 '      || fail=1
+test "$(stat -c  %18.10X k)" = '  67413.0234567890'   || fail=1
+test "$(stat -c %018.10X k)" =  0067413.0234567890    || fail=1
+test "$(stat -c %-18.10X k)" =   '67413.0234567890  ' || fail=1
 
 Exit $fail
diff --git a/tests/touch/60-seconds b/tests/touch/60-seconds
index d008296..e06c770 100755
--- a/tests/touch/60-seconds
+++ b/tests/touch/60-seconds
@@ -30,7 +30,7 @@ echo 60.000000000 > exp || framework_failure
 # an `invalid date format'.  Specifying 60 seconds *is* valid.
 TZ=UTC0 touch -t 197001010000.60 f || fail=1
 
-stat --p='%Y.%:Y\n' f > out || fail=1
+stat --p='%.9Y\n' f > out || fail=1
 
 compare out exp || fail=1
 
-- 
1.7.2