mktime

  • Thread starter Anders Wegge Keller
  • Start date
A

Anders Wegge Keller

According to the C2011 draft I have available, the values in the
struct tm, passed to mktime(), are not restricted to the normalized
ranges. (2.27.2.3 §2).

Is this to be taken literally, such that any combination of values in
the tm struct, that corresponds to something that can be expressed in
a time_t is a valid input?

As I read the following paragrahp, that seem to be the case. Which
lead me to think of exxtreme cases like tm_sec, tm_min, tm_hour,
tm_mday and tm_mon all at INT_MAX, and tm_year set to a sufficiently
large negative value.

Am I missing something, or would a conforming implementation have to
handle this?
 
J

James Kuyper

According to the C2011 draft I have available, the values in the
struct tm, passed to mktime(), are not restricted to the normalized
ranges. (2.27.2.3 §2).

Is this to be taken literally, such that any combination of values in
the tm struct, that corresponds to something that can be expressed in
a time_t is a valid input?

Yes. This feature, allows performance of date arithmetic. For instance,
take a struct tm that represents 2012-12-06 09:41, subtract 7 from
tm_mday, and pass it to mktime(), which should normalize that struct to
describe 2012-11-29 09:41. This is, in fact, the main thing I've ever
used mktime() for. mktime() has to know how many days there are in each
month, which years are leap years, what day of the week each date is; so
it saves me from having to write all that logic in my own code.
As I read the following paragrahp, that seem to be the case. Which
lead me to think of exxtreme cases like tm_sec, tm_min, tm_hour,
tm_mday and tm_mon all at INT_MAX, and tm_year set to a sufficiently
large negative value.

Am I missing something, or would a conforming implementation have to
handle this?

Yes, it would.
 
E

Eric Sosman

According to the C2011 draft I have available, the values in the
struct tm, passed to mktime(), are not restricted to the normalized
ranges. (2.27.2.3 §2).

ITYM 7.27.2.3.
Is this to be taken literally,

That's the usual way to take normative text ...
such that any combination of values in
the tm struct, that corresponds to something that can be expressed in
a time_t is a valid input?

Yes, keeping in mind that time_t's range is implementation-
defined (7.27.1p4). If the tm_xxx values correspond to a time_t
value in that implementation-defined range, 7.27.2.3p3 requires
mktime() to return that time_t value.

Clearly, the intent is to relieve the programmer of the burden
of all that date arithmetic. If you've got a struct tm representing
some time T and you want to determine the time at T plus 01:23:45,
you can blithely add 1*3600+23*60+45 to the tm_sec field and call
mktime(), and mktime() will figure things out.
As I read the following paragrahp, that seem to be the case. Which
lead me to think of exxtreme cases like tm_sec, tm_min, tm_hour,
tm_mday and tm_mon all at INT_MAX, and tm_year set to a sufficiently
large negative value.

Am I missing something, or would a conforming implementation have to
handle this?

I think so. The only out I can imagine is that if mktime()
cannot determine whether Standard or Daylight time prevails, it
may be unable to find "the" corresponding time_t value and could
then return (time_t)-1 to indicate failure. But if the question
boils down to "Must mktime() be careful about overflow in its
internal computations?" I think the answer must be "Yes."
 
A

Anders Wegge Keller

Eric Sosman said:
On 12/6/2012 9:25 AM, Anders Wegge Keller wrote:
ITYM 7.27.2.3.
yes.
That's the usual way to take normative text ...

As for legislative works, the wording of normative standards doesn't
always mean what the un-initiated takes them to mean.

...
then return (time_t)-1 to indicate failure. But if the question
boils down to "Must mktime() be careful about overflow in its
internal computations?" I think the answer must be "Yes."

I guess my question can be reduced to exactly that.
 
E

Eric Sosman

Eric Sosman said:
[...]
then return (time_t)-1 to indicate failure. But if the question
boils down to "Must mktime() be careful about overflow in its
internal computations?" I think the answer must be "Yes."

I guess my question can be reduced to exactly that.

Okay: I'm sticking with "Yes."

FWIW, PJ Plauger seems to agree (he's non-normative, but since
he served on the library subcommittee for the original ANSI Standard,
he's a pretty good indicator of intent). In "The Standard C Library"
he exhibits a sample implementation of mktime(), and in describing
its underpinnings he writes

"You can start with the year 2000, back up 2,000 months,
and advance 2 billion seconds, for example. [...] The
logic is carefully crafted to avoid integer overflow
regardless of argument values."
 
K

Keith Thompson

Eric Sosman said:
Clearly, the intent is to relieve the programmer of the burden
of all that date arithmetic. If you've got a struct tm representing
some time T and you want to determine the time at T plus 01:23:45,
you can blithely add 1*3600+23*60+45 to the tm_sec field and call
mktime(), and mktime() will figure things out.
[...]

Or you can add 1 to tm_hour, 23 to tm_min, and 45 to tm_sec, and let
mktime() figure *everything* out.
 
K

Keith Thompson

Eric Sosman said:
Eric Sosman said:
[...]
then return (time_t)-1 to indicate failure. But if the question
boils down to "Must mktime() be careful about overflow in its
internal computations?" I think the answer must be "Yes."

I guess my question can be reduced to exactly that.

Okay: I'm sticking with "Yes."

FWIW, PJ Plauger seems to agree (he's non-normative, but since
he served on the library subcommittee for the original ANSI Standard,
he's a pretty good indicator of intent). In "The Standard C Library"
he exhibits a sample implementation of mktime(), and in describing
its underpinnings he writes

"You can start with the year 2000, back up 2,000 months,
and advance 2 billion seconds, for example. [...] The
logic is carefully crafted to avoid integer overflow
regardless of argument values."

You can advance 2 billion seconds *if* int is at least 32 bit (tm_sec is
of type int).
 
A

Anders Wegge Keller

Keith Thompson said:
Eric Sosman said:
Clearly, the intent is to relieve the programmer of the burden
of all that date arithmetic. If you've got a struct tm representing
some time T and you want to determine the time at T plus 01:23:45,
you can blithely add 1*3600+23*60+45 to the tm_sec field and call
mktime(), and mktime() will figure things out.
[...]

Or you can add 1 to tm_hour, 23 to tm_min, and 45 to tm_sec, and let
mktime() figure *everything* out.

Yep :)

I should have thought about this possibility 10 years ago, and spared
myself a lot of trouble.
 
E

Eric Sosman

Eric Sosman said:
FWIW, PJ Plauger seems to agree (he's non-normative, but since
he served on the library subcommittee for the original ANSI Standard,
he's a pretty good indicator of intent). In "The Standard C Library"
he exhibits a sample implementation of mktime(), and in describing
its underpinnings he writes

"You can start with the year 2000, back up 2,000 months,
and advance 2 billion seconds, for example. [...] The
logic is carefully crafted to avoid integer overflow
regardless of argument values."

You can advance 2 billion seconds *if* int is at least 32 bit (tm_sec is
of type int).

Don't complain to me; complain to the quoted author. Besides:

for (int count = 0; count < 100; ++count) {
mktime(tm);
tm->tm_sec += 20000;
}
mktime(tm);
printf("So there, nyaaah!\n");
 
E

Eric Sosman

Eric Sosman said:
FWIW, PJ Plauger seems to agree (he's non-normative, but since
he served on the library subcommittee for the original ANSI Standard,
he's a pretty good indicator of intent). In "The Standard C Library"
he exhibits a sample implementation of mktime(), and in describing
its underpinnings he writes

"You can start with the year 2000, back up 2,000 months,
and advance 2 billion seconds, for example. [...] The
logic is carefully crafted to avoid integer overflow
regardless of argument values."

You can advance 2 billion seconds *if* int is at least 32 bit (tm_sec is
of type int).

Don't complain to me; complain to the quoted author. Besides:

for (int count = 0; count < 100; ++count) {
mktime(tm);
tm->tm_sec += 20000;
}
mktime(tm);
printf("So there, nyaaah!\n");

Oh, ratz.

for (int a = 0; a < 1000; ++a) {
for (int b = 0; b < 1000; ++b) {
mktime(tm);
tm->tm_sec += 2000;
}
}
mktime(tm);
printf("So there, this time for sure!\n");
 
H

Heinrich Wolf

Keith Thompson said:
Eric Sosman said:
Clearly, the intent is to relieve the programmer of the burden
of all that date arithmetic. If you've got a struct tm representing
some time T and you want to determine the time at T plus 01:23:45,
you can blithely add 1*3600+23*60+45 to the tm_sec field and call
mktime(), and mktime() will figure things out.
[...]

Or you can add 1 to tm_hour, 23 to tm_min, and 45 to tm_sec, and let
mktime() figure *everything* out.

These may result in different values around a leap second (at the end of
some years) or around the transition from normal time to daylight savings
time or vice versa.
 
J

James Kuyper

Keith Thompson said:
Eric Sosman said:
Clearly, the intent is to relieve the programmer of the burden
of all that date arithmetic. If you've got a struct tm representing
some time T and you want to determine the time at T plus 01:23:45,
you can blithely add 1*3600+23*60+45 to the tm_sec field and call
mktime(), and mktime() will figure things out.
[...]

Or you can add 1 to tm_hour, 23 to tm_min, and 45 to tm_sec, and let
mktime() figure *everything* out.

These may result in different values around a leap second (at the end of
some years) or around the transition from normal time to daylight savings
time or vice versa.

Yes - and I think the different result you would get in those cases may
often be the one that was actually intended.
 
J

James Kuyper

Question: If you start with 28th February 2012, then adding one year,
followed by adding two days, will get you 28th Feb 2013, then 2nd Mar
2013. But adding two days, followed by adding one year, will get you
1st Mar 2012, followed by 1st Mar 2013. Not the same.

Are there rules for this kind of thing? Is it unspecified or
undefined?

What's supposed to happen if you add one month to March 31st 2012? The
result can't be April 31st; is it April 30th, May 1st, or something
else?

No, the rules for such things aren't specified, and that lack of
specification is a problem.

given:
#include <time.h>
// Two different ways to describe 2012-03-31:
struct tm a = {.tm_year=2012-1900, .tm_month = 3-1, .tm_day = 31};
struct tm b = {.tm_year=2012-1900, .tm_month = 4-1, .tm_day = 0};
a.tm_month += 1;
mktime(&a);
b.tm_month += 1;
mktime(&b);

My preference would be that this point, 'a' represents 2012-05-01, while
'b' represents 2012-04-30. But the standard doesn't address such questions.
 
E

Eric Sosman

Question: If you start with 28th February 2012, then adding one year,
followed by adding two days, will get you 28th Feb 2013, then 2nd Mar
2013. But adding two days, followed by adding one year, will get you
1st Mar 2012, followed by 1st Mar 2013. Not the same.

mktime() converts dates, which is not the same as doing
arithmetic on them. You may imagine that you are "adding one
year" to 2012-02-28, and you may even use ...tm_year++ to do
it, but when you pass the struct to mktime() it doesn't know
the history of the tm_year value. All it sees is tm_year = 113
(2013 - 1900), and it says "Here I am in 2013; now let's look
at the other fields." If tm_mday is 30, mktime() will make an
adjustment based on the length of 2013's February; it has no
way of knowing that the struct once held a different tm_year.
Are there rules for this kind of thing? Is it unspecified or
undefined?

The only rule I see is 7.27.2.2p2: "[...] the final value
of tm_mday is not set until tm_mon and tm_year are determined,"
which I understand as meaning that day-of-month yields to
month and year rather than the other way around.
What's supposed to happen if you add one month to March 31st 2012? The
result can't be April 31st; is it April 30th, May 1st, or something
else?

Again, mktime() can't know what values formerly occupied the
struct fields before the current ones were stored. All it sees
is that tm_mon = 3 (April) and tm_mday = 31; it knows that 31 is
out of range for April and adjusts to May 1.

You could certainly get strange results by doing "the same"
set of adjustments in sequence, with intervening mktime() calls:

struct tm x = ...; // 2012-02-29
x.tm_mon++; // 2012-03-29
mktime(&x); // 2012-03-29
x.tm_mday++; // 2012-03-30
mktime(&x); // 2012-03-30

struct tm y = ...; // 2012-02-29
y.tm_mday++; // 2012-02-30
mktime(&y); // 2012-03-01 corrected
y.tm_mon++; // 2012-04-01
mktime(&y); // 2012-04-01, April Fool!

Each mktime() call has a well-defined outcome, yet the two
sequences lead to different results -- Well, that's just the
way our creaky old calendar crumbles. Still, the creakiness
shouldn't be too terribly surprising to a C programmer, who's
already sensitive to the fact that Order Matters:

int x = 12 / 5 * 3; // 6
int y = 12 * 3 / 5; // 7

If you're comfortable with the fact that doing "the same"
multiplications and divisions in different orders yields
different outcomes, maybe you can reconcile yourself to the
similar situation with date adjustments.
 
H

Heinrich Wolf

christian.bau said:
Question: If you start with 28th February 2012, then adding one year,
followed by adding two days, will get you 28th Feb 2013, then 2nd Mar
2013. But adding two days, followed by adding one year, will get you
1st Mar 2012, followed by 1st Mar 2013. Not the same.

Are there rules for this kind of thing? Is it unspecified or
undefined?

What's supposed to happen if you add one month to March 31st 2012? The
result can't be April 31st; is it April 30th, May 1st, or something
else?

I tried with my Borland C++Builder 5 on Windows XP. But that is only one of
many implementations.

2012-2-28 + 1-0-0 = 2013-2-28
2013-2-28 + 0-0-2 = mktime(2013-2-30) = 2013-3-2

2012-2-28 + 0-0-2 = mktime(2012-2-30) = 2012-3-1
2012-3-1 + 1-0-0 = 2013-3-1

2012-2-28 + 1-0-2 = mktime(2013-2-30) = 2013-3-2

2012-3-31 + 0-1-0 = mktime(2012-4-31) = 2012-5-1
 
H

Heinrich Wolf

....
These may result in different values around a leap second (at the end of
some years) or around the transition from normal time to daylight savings
time or vice versa.
....
My Fedora 14 man page for mktime tells me that leap seconds exist, but is
there anybody out there who has seen them on a system?

My Fedora 14 man page for mktime in German:
....
Die Elemente der Struktur tm sind:

tm_sec Die Anzahl der Sekunden nach der vollen Minute,
normalerweise im Bereich 0 bis 59, jedoch in Ausnahmefällen bis 61 um
Schaltsekunden zu
erlauben.
....
Translated into English:
....
The elements of struct tm are:

tm_sec The number of seconds after the full minute, normally in the
range 0 to 59, but in exceptional cases up to 61 in order to allow leap
seconds.
....

I searched for leap seconds on 2 different systems: Fedora 14 Linux, and
Windows XP with Borland C++Builder 5.
I expected the calculation localtime(mktime(1970-1-2 00-00-00) + 10000 * 24
* 60 * 60 seconds) to result in a tm struct with some odd seconds, but on
both systems I got 1997-05-20 01-00-00
 
E

Eric Sosman

...
...
My Fedora 14 man page for mktime tells me that leap seconds exist, but
is there anybody out there who has seen them on a system?

My Fedora 14 man page for mktime in German:
...
Die Elemente der Struktur tm sind:

tm_sec Die Anzahl der Sekunden nach der vollen Minute,
normalerweise im Bereich 0 bis 59, jedoch in Ausnahmefällen bis 61 um
Schaltsekunden zu
erlauben.
...
Translated into English:
...
The elements of struct tm are:

tm_sec The number of seconds after the full minute, normally in
the range 0 to 59, but in exceptional cases up to 61 in order to allow
leap seconds.
...

I searched for leap seconds on 2 different systems: Fedora 14 Linux, and
Windows XP with Borland C++Builder 5.
I expected the calculation localtime(mktime(1970-1-2 00-00-00) + 10000 *
24 * 60 * 60 seconds) to result in a tm struct with some odd seconds,
but on both systems I got 1997-05-20 01-00-00

I remember seeing the [0..61] range for tm_sec long ago, and
perhaps it still persists in some non-C documents. Even in the
original 1989 ANSI C Standard, though, the range was [0..60] in
keeping with the fact that leap seconds are added (or removed)
one at a time, never two at once.

As to the calculation -- well, timekeeping is complicated.
POSIX *defines* the day as 86400 seconds, intentionally ignoring
leap seconds (see <http://en.wikipedia.org/wiki/Unix_time>), so
we should not expect a C implementation based on POSIX time to
generate tm_sec==60. The C Standard does not require the library
to overcome this problem:

7.27.1p4: "The range and precision of times representable
in clock_t and time_t are implementation-defined."

7.27.2.4p3: "The time function returns the implementation’s
*best approximation* [emphasis mine] to the current calendar
time."

Timekeeping and time calculation on POSIX and POSIX-like systems
is "close enough for jazz," sufficiently accurate for many purposes
but not astonishingly good. C implementations don't have to do any
better (and on a strictly-conforming POSIX system, I guess they're
actually forbidden to do better).
 
A

Alan Curry

I searched for leap seconds on 2 different systems: Fedora 14 Linux, and
Windows XP with Borland C++Builder 5.
I expected the calculation localtime(mktime(1970-1-2 00-00-00) + 10000 * 24
* 60 * 60 seconds) to result in a tm struct with some odd seconds, but on
both systems I got 1997-05-20 01-00-00

On unix-like systems the time_t has two interpretations, "posix" and "right".
The posix time_t leaps with the leap seconds, making them mostly invisible
unless your program is running during a leap. Adding 86400 to a posix time_t
always gets you the same time the next day (plus or minus an hour for
daylight savings).

With the posix time_t, mktime covers up evidence of leap seconds especially
well. You can set a struct tm to out-of-range 23:59:60 on a non-leap-second
day and mktime will normalize it to 0:00:00 the next day and return the exact
same time_t it would have returned for the legitimate 23:59:60 if the day had
been a leap second day.

If you indicate with the TZ environment variable that you want the "right"
time, then mktime and localtime will adjust their behavior and the expected
odd seconds will show up.

Here's a program demonstrating the differences:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>

static void showtm(int year, int mon, int day, int hour, int min, int sec)
{
time_t t;
struct tm tm = {
.tm_year = year-1900,
.tm_mon = mon-1,
.tm_mday = day,
.tm_hour = hour,
.tm_min = min,
.tm_sec = sec
};
t = mktime(&tm);
printf("%d-%d-%d %d:%02d:%02d = %ju\n",
year, mon, day, hour, min, sec, (uintmax_t)t);
}

int main(void)
{
time_t now;
struct tm *nowtm;
time_t tenyearsago;
struct tm tenyearsagotm;
const char *tzlist[] = { "posix/Etc/GMT", "right/Etc/GMT" };
int i;

for(i=0;i<2;++i) {
printf("%sTZ=%s\n", i?"\n":"", tzlist);
setenv("TZ", tzlist, 1);
tzset();

now = time(0);
nowtm = localtime(&now);

tenyearsagotm = *nowtm;
tenyearsagotm.tm_year -= 12;
tenyearsago = mktime(&tenyearsagotm);

printf("Days between now and this time 12 years ago: %.10f\n",
(now-tenyearsago)/86400.);

printf("This is a leap second:\n");
showtm(2012, 6, 30, 23, 59, 59);
showtm(2012, 6, 30, 23, 59, 60);
showtm(2012, 7, 1, 0, 0, 0);
printf("This is not a leap second:\n");
showtm(2011, 6, 30, 23, 59, 59);
showtm(2011, 6, 30, 23, 59, 60);
showtm(2011, 7, 1, 0, 0, 0);
}

return 0;
}
 
K

Keith Thompson

Eric Sosman said:
On 12/7/2012 3:39 PM, Heinrich Wolf wrote: [...]
My Fedora 14 man page for mktime in German:
...
Die Elemente der Struktur tm sind:

tm_sec Die Anzahl der Sekunden nach der vollen Minute,
normalerweise im Bereich 0 bis 59, jedoch in Ausnahmefällen bis 61 um
Schaltsekunden zu
erlauben.
...
Translated into English:
...
The elements of struct tm are:

tm_sec The number of seconds after the full minute, normally in
the range 0 to 59, but in exceptional cases up to 61 in order to allow
leap seconds.
...

I searched for leap seconds on 2 different systems: Fedora 14 Linux, and
Windows XP with Borland C++Builder 5.
I expected the calculation localtime(mktime(1970-1-2 00-00-00) + 10000 *
24 * 60 * 60 seconds) to result in a tm struct with some odd seconds,
but on both systems I got 1997-05-20 01-00-00

I remember seeing the [0..61] range for tm_sec long ago, and
perhaps it still persists in some non-C documents. Even in the
original 1989 ANSI C Standard, though, the range was [0..60] in
keeping with the fact that leap seconds are added (or removed)
one at a time, never two at once.

The 1990 ISO C standard specifies a range of [0, 61] for tm_sec:

int tm_sec; /* seconds after the minute -- [0, 61] */

with a footnote:

The range [0,61] for tm_sec allows for as many as two leap seconds.

C99 changes the range to [0, 60] and changes the footnote to:

The range [0, 60] for tm_sec allows for a positive leap second.

There's no mention of the change in the C99 rationale.

I've heard (not sure where) that the [0, 61] range in C90 was
the result of a misunderstanding; if it were necessary to have
two leap seconds in a year (which hasn't happened so far) they'd
be added at the end of June and the end of December. I *guess*
that if more than two were required, they'd be spread more or less
evenly through the year.

Are you *sure* that the 1989 ANSI standard specifies [0, 60]?
If so, that would be an inconsistency between ANSI C89 and ISO C90,
and I didn't think there were any.

Interestingly, a 1988 ANSI C draft ("ansi.c.txt", I'm not sure
where I got it) shows a range of [0, 60].

[...]
 
H

Heinrich Wolf

Thanks a lot for the code.

With posix TZ the days between 12 years is an integer.
With right TZ the days between 12 years have a fractional part.

showtm gives different time_t values for posix TZ and right TZ.

However the time_t is always the same
for 6-30 23:59:60 and 7-1 0:00:00
regardless of the year (leap second or not).
Maybe my Fedora 14 is too old and does not know about the leap second
on 2012-6-30?
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,995
Messages
2,570,236
Members
46,821
Latest member
AleidaSchi

Latest Threads

Top