[tz] strftime %s

Steve Summit scs at eskimo.com
Sun Jan 14 14:20:11 UTC 2024


Robert Elz wrote:
> date +%s isn't (usually) for humans, it is needed for scripts
> to work with time_t values...

Ah, yes.  Good point.

I remember now that it's recommended -- by no less than the
comp.unix.shell FAQ list -- that if you need the current time_t
value but you don't have date +%s available, you can instead use
the not-at-all-obvious

	awk 'BEGIN { print srand(srand()) }'

(But I'm bringing this up merely as a curiosity and for humor
value, not as a serious topic of discussion!)

> | (to my mind, anyway) still vastly preferable to trying to have
> | strftime handle %s by itself, because strftime just doesn't have
> | the right information available to it.
>
> It does if the correct elements of the struct are filled in,
> as mktime() can reconstruct the time_t from a struct tm.
> However it does that assuming that the struct expresses the
> current local time (as defined by the TZ setting) - and not
> some other random zone.

Right.  I confess I had forgotten this point.  For anyone else
who (like me) wasn't fully paying attention, let me state it
another way:

    The struct tm you hand to strftime() is usually one you just
    got from localtime() or gmtime() -- BUT IT MIGHT NOT BE.

In particular, if the struct tm handed to strftime() is one
that, say, the caller just has hand-constructed, it is rather
*un*likely to contain proper values for things like tm_gmtoff
or the hypothetical tm_time_t I was mentioning and that Brooks
picked up on.  So (as others have said several times),
strftime %s really does have no choice but to do a mktime()
based on barely-adequate information -- and part of that
information is, alas, the global TZ environment variable.

An implication is that if you want to implement strftime %s,
you can *not* make it easier on yourself by having localtime
or gmtime helpfully stash extra information in struct tm, using
fields like tm_gmtoff or tm_time_t.  (I feel foolish saying this,
because I'm one of the ones who was just suggesting making things
easier by filling in extra information like tm_gmtoff or tm_time_t.)

And then the other implication is that, dammit, the global TZ
environment variable still really matters.  You still have to
ensure that it's the same for corresponding localtime and mktime
calls -- and, if you're using %s, for corresponding strftime
calls as well.  Keeping it the same might not seem so hard --
who changes environment variables out from under a running
program, anyway? -- except that changing TZ is the recommended
and pretty much only way of dealing with a time zone other than
the current one.

(It sounds like Posix is *finally* standardizing the vital
tm_gmtoff field.  I wonder how many more years we'll have to
wait for someone's blessing of BSD's variant _z functions?)

> There are people who don't understand that, and insist that
> it must also work for other zones - but it simply doesn't.

Right.  But go easy on them -- it really is almost inhumanly
difficult to keep all of the sloppily dovetailing constraints in
mind at the same time.  In particular, it's absurdly difficult to
remember how badly these functions all depend on the global TZ
variable, and to remember that there really is no good way of
working with time zones other than the current one.

Me, I find myself half wishing for a big, bold warning on the
strftime man page:

	The struct tm handed to strftime must be one returned by
	an immediately preceding call to localtime or gmtime.

But that's not even true.  That's the warning that would allow an
implementor to look at tm_gmtoff (or the hypothetical tm_time_t)
when writing strftime %s.  But, in fact, callers *are* allowed to
pass handcrafted struct tm values to strftime, and implementors
are obliged to make this work -- even if there's a %s in the
format string.  (Which brings me back to my conclusion that %s
shouldn't exist, because it's impossible to implement correctly.
But, as the saying goes, people who believe a thing to be
impossible should not stand in the way of those who are doing it.)

So, in fact, the necessary big, bold warning is not one that
goes on the strftime man page.  No, the big, bold warning is for
people like me, who keep dreaming of a set of time-conversion
functins that's halfway sane and coherent.  I'm not sure where
the warning goes, but it would say something like:

    You might think that the sequence

        struct tm *tm = localtime(&t);
	strftime(buf, sizeof buf, "%s", tm);

    is fundamentally guaranteed to place a decimal representation
    of t into buf, where "fundamentally" implies that it just
    *has* to work, even in the face of serious bugs in other,
    unrelated parts of the time-conversion logic.  But no, this
    sequence is in fact utterly vulnerable to bugs in other
    parts of the time-conversion logic, because it is necessarily
    equivalent to the sequence

        struct tm *tm = localtime(&t);
	time_t t2 = mktime(tm);

    which sets t2 == t only in the presence of a perfectly-
    implemented mktime, and also given certain other constraints,
    such as that TZ has not changed.

The bottom line is that a call to localtime followed by a call
to strftime %s is an only barely, skating-on-thin-ice-ily
information-preserving transformation.  It'll probably work,
but really, it's more in the category of

	float f = atof(str);
	sprintf(str2, "%f", f);

That is, what you're looking at when you use strftime %s is *not*
a straight passing-through of data; you're probably looking at a
pair of nominally-inverse but delicate and potentially lossy
transformations.

Perhaps there *is* a warning worthy of putting on the strftime
man page, which is

	Please rely on %s only if you're the implementor of
	date(1) or the equivalent.  If you're using %s to print a
	time_t value that your program has explicitly, it is far
	less error-prone to print that value directly, than to
	convert it to a struct tm and print it with strftime %s.

Apologies for the long message, and for taking so long to
understand that, yes, strftime %s really does have to do a
full-blown mktime, with all the unreliability and imprecision
that implies -- it can *not* do the easy thing and look at
tm_gmtoff.

(Stay tuned for our next exciting episode, in which the
programmers who used to clamor for the nonstandard timegm
function now request a strftime variant whose %s specifier
assumes UTC.)



More information about the tz mailing list