Time and the 2038 problem


#1

Here I am showing my working as I may well be wrong.

Sometimes JUCE falls back from system functions to internal logic in Time calculations. It seems JUCE is overly cautious of using the system functions (which benefit from having more complete understanding of timezones).

As an aside the JUCE function also happens to use for it's timezone offset calculation the year 1971, which in the UK was a weird year (http://www.bbc.co.uk/news/uk-scotland-11643098) giving the UK a base UTC offset of +01:00 rather than +00:00. So we would prefer to use system functions where possible.

The functions below:

static struct tm millisToLocal (const int64 millis) noexcept;

Time::Time (const int year,
    const int month,
    const int day,
    const int hours,
    const int minutes,
    const int seconds,
    const int milliseconds,
    const bool useLocalTime) noexcept;

only use the system functions before 12:00:00 am UTC, Friday, January 1, 2038. Most platforms have 64bit localtime functions now so we could extend this.


1. On OSX, linux etc:
Maximum time_t is [sizeof(time_t) == 8 ? INT64_MAX : INT_MAX] I think.
Maximum date is (32bit) 03:14:07 UTC, Tuesday, January 19, 2038 or (64bit) I have no idea.

2. On MSBuild:
Maximum date is (32bit) 03:14:07 January 19, 2038, UTC or (64bit) 23:59:59, December 31, 3000 UTC (See https://msdn.microsoft.com/en-us/library/bf12f0hc(v=vs.71).aspx).
So the maximum time_t is [sizeof(time_t) == 8 ? 32535215999LL : INT_MAX].

3. Other platforms I am not sure about but should be similar to Linux


#2

Hi Harry

OK, I just spent a couple of hours refactoring this, and removing dependencies where I could.  Would appreciate you sanity-checking what I've done, and if there's anything you think it's not covering, add a unit-test at the bottom of the file so I can reproduce + fix it. Cheers!


#3

Hello. I have given this a bit of a once over. Here are some failing unit tests and what I can find out about them. Some tests from Apple UTC time testing (https://opensource.apple.com/source/dovecot/dovecot-239/dovecot/src/lib/test-utc-mktime.c)

1. Days from Jan 1 had its leap year check the wrong way around. It should be like this with the array reordered.

static inline int daysFromJan1 (int year, int month) noexcept
    {
        const short dayOfYear[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
                                    0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 };
        return dayOfYear [(isLeapYear (year) ? 12 : 0) + month];
    }

Found with:

expect (Time (2106,  1,  7,  6, 28, 15, 0, false) == Time(4294967295000));
expect (Time (2007, 10,  7,  1,  7, 20, 0, false) == Time(1194397640000));
expect (Time (1970,  0,  1,  0,  0,  0, 0, false) == Time(0));
expect (Time (2038,  0, 19,  3, 14,  7, 0, false) == Time(2147483647000));

2. toISO8601 has a bug in the float formatter. Should be "%06.03f":

    return String::formatted (includeDividerCharacters ? "%04d-%02d-%02dT%02d:%02d:%06.03f"
                                                       : "%04d%02d%02dT%02d%02d%06.03f",
                              getYear(),
                              getMonth() + 1,
                              getDayOfMonth(),
                              getHours(),
                              getMinutes(),
                              getSeconds() + getMilliseconds() / 1000.0)
            + getUTCOffsetString (includeDividerCharacters);

this would be visible with a unit test with Time (X, X, X, X, X, 1, 222, false). In the string it would be "XXX:1.222Z" rather than "XXX:01.222Z"

3. There seems to be a bug here:

expect (Time (2016,  2,  7, 11, 20,  8, 0, false) == Time(1457349608000));

maybe something to do with Leap years again as it seems to show 1 day off. I haven't tracked it down. I am using WolframAlpha to confirm utc conversions (http://www.wolframalpha.com/input/?i=unix+1457349608.000+to+UTC)

4. This check is overly active in Time constructor:

 if (time >= 0)
    {
        millisSinceEpoch = 1000 * time + milliseconds;
    }
    else
    {
        jassertfalse; // trying to create a date that is beyond the range that mktime supports!
        millisSinceEpoch = 0;
    }

On OSX I have successfully used mktime on tm structs in the 1000s BC range so checking <0 should probably be checking == -1. This still has problems finding the last second of 1969 but that is unavoidable.

Try these to test

expect (Time (1969, 11, 31, 23, 59, 59, 0, false) == Time(-1000));
expect (Time (1901, 11, 13, 20, 45, 53, 0, false) == Time(-2147483647000));

5. Another possible (though unlikely to be needed really) function to add would be:

String Time::getYearName (int year)
{
    return TRANS (year <= 0 ? String((year * -1) + 1) + String("BC")
                            : String(year));
}

#4

Much appreciated, Harry!

I've added all your tests, and persuaded them all to pass - shout if you find any more!


#5

I’ve noticed that occasionally the toISO8601() method will not include the hyphen for the timezone.

And unexpectedly, using fromISO8601() converts whatever timezone was used in the source to whatever timezone the computer is running.
so if I call Time t = Time::fromISO8601("2016-12-28T21:33:37.962-01:00") and I’m on the east coast of the US, I’ll get back: "2016-12-28T17:33:37.962-05:00" when i call DBG(t.toISO8601(true));

It’s unexpected behavior, that’s for sure.


#6

Hi @matkatmusic.

The ISO 8601 that JUCe support looks like hh:mm:ss.sss±hh:mm. There is no hyphen for the timezone (except the minus sign I suppose).

The Time object (in its raw form) does not store the timezone. It is stored as UTC time. When outputting it always uses the computers local timezone. I agree that a version of the Time object with timezone embedded would be useful.