Bug in TTimeStamp: days are skipped when using date and time constructor

Dear experts,

TTimeStamp somtimes gives incorrect results when it’s constructed with date and time, whether using the unsigned date and time version, or the explicit year, month, day, hours, minutes, and seconds.
Here’s a minimal reproducible example:

time_t ut = -295372800; // from GNU date 8.32: Mon 22 Aug 08:00:00 UTC 1960
TTimeStamp tts(ut, 0);
std::printf("Original time_t = %+10ld; TTimeStamp = %s\n",
   ut, tts.AsString("s"));

unsigned d = tts.GetDate();
unsigned H, M, S;
tts.GetTime(true, 0, &H, &M, &S);
std::printf("Values extracted from TTimeStamp: date = %u; H = %u, M = %u, S = %u\n",
   d, H, M, S);

ut -= H*3600 + M*60 + S;
std::printf("Shifted time_t to start of day = %+10ld; TTimeStamp = %s\n",
   ut, TTimeStamp(ut, 0).AsString("s"));

TTimeStamp tt1(d, 0u, 0u, true, 0);
std::printf("TTimeStamp(%u, 0u, 0u, true, 0) = %s, time_t = %+10ld\n",
   d, tt1.AsString("s"), (time_t)tt1);

TTimeStamp tt2(1960, 8, 22, 0, 0, 0);
std::printf("TTimeStamp(1960, 8, 22, 0, 0, 0) = %s, time_t = %+10ld\n",
   tt2.AsString("s"), (time_t)tt2);

and its output:

Original time_t = -295372800; TTimeStamp = 1960-08-22 08:00:00
Values extracted from TTimeStamp: date = 19600822; H = 8, M = 0, S = 0
Shifted time_t to start of day = -295401600; TTimeStamp = 1960-08-22 00:00:00
TTimeStamp(19600822, 0u, 0u, true, 0) = 1960-08-23 00:00:00, time_t = -295315200
TTimeStamp(1960, 8, 22, 0, 0, 0) = 1960-08-23 00:00:00, time_t = -295315200

It looks like before Jan 1st, 1970, TTimeStamp skip the January 1st for some years, when using the date and time constructor, while everything looks good using the time_t constructor:

const std::vector<int> days = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
time_t ut = -410227200; // from GNU date 8.32: Mon  1 Jan 00:00:00 UTC 1957
for (int year = 1957; year <= 1958; ++year) {
   for (int month = 1; month <= 12; ++month) {
      int add_day = month == 2 && TTimeStamp::IsLeapYear(year);
      for (int day = 1; day <= days[month-1] + add_day; ++day, ut += 86400) {
         TTimeStamp tt3(year, month, day, 0, 0, 0);
         std::string date3 = tt3.AsString("s");
         date3.erase(10);

         TTimeStamp tt4(ut, 0);
         std::string date4 = tt4.AsString("s");
         date4.erase(10);

         std::printf("In:%d-%02d-%02d Out:%s %+10ld; In:%+10ld Out:%+10ld %s\n",
            year, month, day, date3.c_str(), (time_t)tt3,
            ut, (time_t)tt4, date4.c_str());
      }
   }
}

In:1957-01-01 Out:1957-01-01 -410227200; In:-410227200 Out:-410227200 1957-01-01
[…
all days in between are ok
…]
In:1957-12-31 Out:1957-12-31 -378777600; In:-378777600 Out:-378777600 1957-12-31
In:1958-01-01 Out:1958-01-02 -378604800; In:-378691200 Out:-378691200 1958-01-01

This happens by running the attached macro in both interpreted and compiled mode.
test.C (1.8 KB)

Best,
Claudio


ROOT Version: 6.26/02, heads/latest-stable@c8d49336
Platform: Ubuntu 22.04.2
Compiler: linuxx8664gcc


I get this:

Original time_t                        = -295372800; TTimeStamp = 1960-08-22 08:00:00
Values extracted from TTimeStamp: date = 19600822; H = 8, M = 0, S = 0
Shifted time_t to start of day         = -295401600; TTimeStamp = 1960-08-22 00:00:00
TTimeStamp(19600822, 0u, 0u, true, 0)  = 1960-08-23 00:00:00, time_t = -295315200
TTimeStamp(1960, 8, 22, 0, 0, 0)       = 1960-08-23 00:00:00, time_t = -295315200

Which one is wrong?

The last two lines are wrong: you initialize a TTimeStamp with day 22, but you get day 23.
The third line is the expected value.
Here’s another way to see the problem:

time_t ut = /* any Unix time */
TTimeStamp tt_ut(ut, 0); // initialize with Unix time: always fine
TTimeStamp tt_dt(tt_ut.GetDate(), tt_ut.GetTime(), 0u, true, 0); // initialize with correct unsigned date (yyyymmdd) and time (HHMMSS)
cout << tt_dt.GetDate() == tt_ut.GetDate << endl; // this should always print 1, but it fails on some dates before Jan 1, 1970
void ts() {
   time_t ut = -295372800;  // from GNU date 8.32: Mon 22 Aug 08:00:00 UTC 1960
   TTimeStamp tt_ut(ut, 0); // initialize with Unix time: always fine
   TTimeStamp tt_dt(tt_ut.GetDate(), tt_ut.GetTime(), 0u, true, 0); // initialize with correct unsigned date (yyyymmdd) and time (HHMMSS)
   cout << tt_dt.GetDate()  << endl; // this should always print 1, but it fails on some dates before Jan 1, 1970
}

gives me:

root [0] 
Processing ts.C...
19600823

Yes, and that’s wrong: it should be 19600822, like it is for tt_ut.GetDate().

By the way, the last line in my previous post was printing the result of the test tt_dt.GetDate() == tt_ut.GetDate(), not tt_ut.GetDate().

It is an issue with leap years.

void test() {
  int delta_yr;
  for (int i=0; i<22; ++i) {
    delta_yr = i-11;
    time_t ut = -295372800 + delta_yr*365*3600*24; // -295372800 from GNU date 8.32: Mon 22 Aug 08:00:00 UTC 1960
    TTimeStamp tt_ut(ut, 0u); // initialize with Unix time: always fine
    UInt_t uda = tt_ut.GetDate();
    UInt_t uti = tt_ut.GetTime();
    TTimeStamp tt_dt(uda, uti, 0u, true, 0u); // initialize with correct unsigned date (yyyymmdd) and time (HHMMSS)
    cout << endl << tt_dt.GetDate()-tt_ut.GetDate() << endl;
    cout << tt_dt.AsString() << endl;
    cout << tt_ut.AsString() << endl;
  }
}

gives

0
Thu, 25 Aug 1949 08:00:00 +0000 (GMT) +        0 nsec
Thu, 25 Aug 1949 08:00:00 +0000 (GMT) +        0 nsec

1
Sat, 26 Aug 1950 08:00:00 +0000 (GMT) +        0 nsec
Fri, 25 Aug 1950 08:00:00 +0000 (GMT) +        0 nsec

1
Sun, 26 Aug 1951 08:00:00 +0000 (GMT) +        0 nsec
Sat, 25 Aug 1951 08:00:00 +0000 (GMT) +        0 nsec

1
Mon, 25 Aug 1952 08:00:00 +0000 (GMT) +        0 nsec
Sun, 24 Aug 1952 08:00:00 +0000 (GMT) +        0 nsec

0
Mon, 24 Aug 1953 08:00:00 +0000 (GMT) +        0 nsec
Mon, 24 Aug 1953 08:00:00 +0000 (GMT) +        0 nsec

1
Wed, 25 Aug 1954 08:00:00 +0000 (GMT) +        0 nsec
Tue, 24 Aug 1954 08:00:00 +0000 (GMT) +        0 nsec

1
Thu, 25 Aug 1955 08:00:00 +0000 (GMT) +        0 nsec
Wed, 24 Aug 1955 08:00:00 +0000 (GMT) +        0 nsec

1
Fri, 24 Aug 1956 08:00:00 +0000 (GMT) +        0 nsec
Thu, 23 Aug 1956 08:00:00 +0000 (GMT) +        0 nsec

0
Fri, 23 Aug 1957 08:00:00 +0000 (GMT) +        0 nsec
Fri, 23 Aug 1957 08:00:00 +0000 (GMT) +        0 nsec

1
Sun, 24 Aug 1958 08:00:00 +0000 (GMT) +        0 nsec
Sat, 23 Aug 1958 08:00:00 +0000 (GMT) +        0 nsec

1
Mon, 24 Aug 1959 08:00:00 +0000 (GMT) +        0 nsec
Sun, 23 Aug 1959 08:00:00 +0000 (GMT) +        0 nsec

1
Tue, 23 Aug 1960 08:00:00 +0000 (GMT) +        0 nsec
Mon, 22 Aug 1960 08:00:00 +0000 (GMT) +        0 nsec

0
Tue, 22 Aug 1961 08:00:00 +0000 (GMT) +        0 nsec
Tue, 22 Aug 1961 08:00:00 +0000 (GMT) +        0 nsec

1
Thu, 23 Aug 1962 08:00:00 +0000 (GMT) +        0 nsec
Wed, 22 Aug 1962 08:00:00 +0000 (GMT) +        0 nsec

1
Fri, 23 Aug 1963 08:00:00 +0000 (GMT) +        0 nsec
Thu, 22 Aug 1963 08:00:00 +0000 (GMT) +        0 nsec

1
Sat, 22 Aug 1964 08:00:00 +0000 (GMT) +        0 nsec
Fri, 21 Aug 1964 08:00:00 +0000 (GMT) +        0 nsec

0
Sat, 21 Aug 1965 08:00:00 +0000 (GMT) +        0 nsec
Sat, 21 Aug 1965 08:00:00 +0000 (GMT) +        0 nsec

1
Mon, 22 Aug 1966 08:00:00 +0000 (GMT) +        0 nsec
Sun, 21 Aug 1966 08:00:00 +0000 (GMT) +        0 nsec

1
Tue, 22 Aug 1967 08:00:00 +0000 (GMT) +        0 nsec
Mon, 21 Aug 1967 08:00:00 +0000 (GMT) +        0 nsec

1
Wed, 21 Aug 1968 08:00:00 +0000 (GMT) +        0 nsec
Tue, 20 Aug 1968 08:00:00 +0000 (GMT) +        0 nsec

0
Wed, 20 Aug 1969 08:00:00 +0000 (GMT) +        0 nsec
Wed, 20 Aug 1969 08:00:00 +0000 (GMT) +        0 nsec

0
Thu, 20 Aug 1970 08:00:00 +0000 (GMT) +        0 nsec
Thu, 20 Aug 1970 08:00:00 +0000 (GMT) +        0 nsec

the dates match every 4 years (1960 was a leap year, and also 1956, 1952,…), then are ok after 1968.

@dastudillo The “leap years” are completely unrelated to the “leap seconds”.

Right

Thanks @dastudillo : that indeed seems to be the case.
Looking at TTimeStamp constructor, it seems the culprit is the function MktimeFromUTC, which is finally called to get the Unix time from the tm struct initialized via Set.

The documentation for MktimeFromUTC does say:

/// This version *DOESN'T* correctly handle values that can't be
/// fit into a time_t (i.e. beyond year 2038-01-18 19:14:07, or
/// before the start of Epoch).

Indeed, the problem is in the last lines of the function:


   // Calculate seconds since the Epoch based on formula in
   // POSIX  IEEEE Std 1003.1b-1993 pg 22
 
   Int_t utc_sec = tmstruct->tm_sec +
                   tmstruct->tm_min*60 +
                   tmstruct->tm_hour*3600 +
                   tmstruct->tm_yday*86400 +
                   (tmstruct->tm_year-70)*31536000 +
                   ((tmstruct->tm_year-69)/4)*86400;

The Posix IEEE standard 1003.1b-1993 at page 22 says:

If the year < 1970 or the value is negative, the relationship is undefined. If the year >= 1970 and the value is nonnegative, the value is related to a Coordinated Universal Time name according to the expression: [see code above].

I believe the problem is in this piece ((tmstruct->tm_year-69)/4)*86400 which is supposed to add one more day for each leap year occurred since 1969.
When year is before 1970, tm_year is negative, and so this calculation counts correctly the number of leap years only after year, but since we need to subtract seconds from 1970, if year is leap, it should also be taken into account.
An easy fix is to replace ((tmstruct->tm_year-69)/4)*86400 with floor((tm_year-69)/4.), so that also year is counted as a leap year, if it is.
Here’s a test:

for (int year = 1960; year <= 1975; ++year) {
   int tm_year = year - 1900;
   int leaps_bug = (tm_year-69)/4;
   int leaps_fix = floor((tm_year-69)/4.);
   time_t ut_bug = (tm_year-70)*31536000 + leaps_bug*86400;
   time_t ut_fix = (tm_year-70)*31536000 + leaps_fix*86400;
   printf("%d-01-01 00:00:00 | leap years since 1969: %+3d (bug), %+3d (fix) | Unix time: %+11ld (bug), %+11ld (fix) | TTimeStamp from Unix time: %s (bug), %s (fix)\n",
      year, leaps_bug, leaps_fix, ut_bug, ut_fix,
      TTimeStamp(ut_bug, 0).AsString("%s"), TTimeStamp(ut_fix, 0).AsString("%s"));
}

and this is the output, which shows that the fix indeed works:

1960-01-01 00:00:00 | leap years since 1969:  -2 (bug),  -3 (fix) | Unix time:  -315532800 (bug),  -315619200 (fix) | TTimeStamp from Unix time: 1960-01-02 00:00:00 (bug), 1960-01-01 00:00:00 (fix)
1961-01-01 00:00:00 | leap years since 1969:  -2 (bug),  -2 (fix) | Unix time:  -283996800 (bug),  -283996800 (fix) | TTimeStamp from Unix time: 1961-01-01 00:00:00 (bug), 1961-01-01 00:00:00 (fix)
1962-01-01 00:00:00 | leap years since 1969:  -1 (bug),  -2 (fix) | Unix time:  -252374400 (bug),  -252460800 (fix) | TTimeStamp from Unix time: 1962-01-02 00:00:00 (bug), 1962-01-01 00:00:00 (fix)
1963-01-01 00:00:00 | leap years since 1969:  -1 (bug),  -2 (fix) | Unix time:  -220838400 (bug),  -220924800 (fix) | TTimeStamp from Unix time: 1963-01-02 00:00:00 (bug), 1963-01-01 00:00:00 (fix)
1964-01-01 00:00:00 | leap years since 1969:  -1 (bug),  -2 (fix) | Unix time:  -189302400 (bug),  -189388800 (fix) | TTimeStamp from Unix time: 1964-01-02 00:00:00 (bug), 1964-01-01 00:00:00 (fix)

1965-01-01 00:00:00 | leap years since 1969:  -1 (bug),  -1 (fix) | Unix time:  -157766400 (bug),  -157766400 (fix) | TTimeStamp from Unix time: 1965-01-01 00:00:00 (bug), 1965-01-01 00:00:00 (fix)
1966-01-01 00:00:00 | leap years since 1969:  +0 (bug),  -1 (fix) | Unix time:  -126144000 (bug),  -126230400 (fix) | TTimeStamp from Unix time: 1966-01-02 00:00:00 (bug), 1966-01-01 00:00:00 (fix)
1967-01-01 00:00:00 | leap years since 1969:  +0 (bug),  -1 (fix) | Unix time:   -94608000 (bug),   -94694400 (fix) | TTimeStamp from Unix time: 1967-01-02 00:00:00 (bug), 1967-01-01 00:00:00 (fix)
1968-01-01 00:00:00 | leap years since 1969:  +0 (bug),  -1 (fix) | Unix time:   -63072000 (bug),   -63158400 (fix) | TTimeStamp from Unix time: 1968-01-02 00:00:00 (bug), 1968-01-01 00:00:00 (fix)
1969-01-01 00:00:00 | leap years since 1969:  +0 (bug),  +0 (fix) | Unix time:   -31536000 (bug),   -31536000 (fix) | TTimeStamp from Unix time: 1969-01-01 00:00:00 (bug), 1969-01-01 00:00:00 (fix)
1970-01-01 00:00:00 | leap years since 1969:  +0 (bug),  +0 (fix) | Unix time:          +0 (bug),          +0 (fix) | TTimeStamp from Unix time: 1970-01-01 00:00:00 (bug), 1970-01-01 00:00:00 (fix)
1971-01-01 00:00:00 | leap years since 1969:  +0 (bug),  +0 (fix) | Unix time:   +31536000 (bug),   +31536000 (fix) | TTimeStamp from Unix time: 1971-01-01 00:00:00 (bug), 1971-01-01 00:00:00 (fix)
1972-01-01 00:00:00 | leap years since 1969:  +0 (bug),  +0 (fix) | Unix time:   +63072000 (bug),   +63072000 (fix) | TTimeStamp from Unix time: 1972-01-01 00:00:00 (bug), 1972-01-01 00:00:00 (fix)
1973-01-01 00:00:00 | leap years since 1969:  +1 (bug),  +1 (fix) | Unix time:   +94694400 (bug),   +94694400 (fix) | TTimeStamp from Unix time: 1973-01-01 00:00:00 (bug), 1973-01-01 00:00:00 (fix)
1974-01-01 00:00:00 | leap years since 1969:  +1 (bug),  +1 (fix) | Unix time:  +126230400 (bug),  +126230400 (fix) | TTimeStamp from Unix time: 1974-01-01 00:00:00 (bug), 1974-01-01 00:00:00 (fix)
1975-01-01 00:00:00 | leap years since 1969:  +1 (bug),  +1 (fix) | Unix time:  +157766400 (bug),  +157766400 (fix) | TTimeStamp from Unix time: 1975-01-01 00:00:00 (bug), 1975-01-01 00:00:00 (fix)

Note that this will break for years dividible by 4 which are not leap years (e.g. 1900, 2100, etc), but this happens also with the current version, since the POSIX standard guarantees accuracy only between 1970 and 2038.

PS: a simple workaround that does not involve modifying the TTimeStamp code:

unsigned date = /* ... */;
unsigned time = /* ... */;
TTimeStamp tts(date, time, 0u, true, 0);
if (tts.GetDate() > date) {
   tts.Add(TTimeStamp((time_t)-86400, 0)); // subtract one day
}

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.