From 3fdf969ea28c5cee544aadfa9cab3c35f92c1931 Mon Sep 17 00:00:00 2001 From: jules Date: Wed, 17 Feb 2016 14:41:44 +0000 Subject: [PATCH] Added some UTC and ISO8601 methods to the Time class. Also clarified some of its comments and added unit tests --- modules/juce_core/time/juce_Time.cpp | 290 +++++++++++++++++++++++---- modules/juce_core/time/juce_Time.h | 120 +++++------ 2 files changed, 312 insertions(+), 98 deletions(-) diff --git a/modules/juce_core/time/juce_Time.cpp b/modules/juce_core/time/juce_Time.cpp index e2b1d394e9..efd9f3b3a9 100644 --- a/modules/juce_core/time/juce_Time.cpp +++ b/modules/juce_core/time/juce_Time.cpp @@ -28,57 +28,98 @@ namespace TimeHelpers { - static struct tm millisToLocal (const int64 millis) noexcept + static struct tm millisecondsToTM (const int64 jdm) noexcept { struct tm result; - const int64 seconds = millis / 1000; - if (seconds < 86400LL || seconds >= 2145916800LL) - { - // use extended maths for dates beyond 1970 to 2037.. - const int timeZoneAdjustment = 31536000 - (int) (Time (1971, 0, 1, 0, 0).toMilliseconds() / 1000); - const int64 jdm = seconds + timeZoneAdjustment + 210866803200LL; + const int days = (int) (jdm / 86400LL); + const int a = 32044 + days; + const int b = (4 * a + 3) / 146097; + const int c = a - (b * 146097) / 4; + const int d = (4 * c + 3) / 1461; + const int e = c - (d * 1461) / 4; + const int m = (5 * e + 2) / 153; - const int days = (int) (jdm / 86400LL); - const int a = 32044 + days; - const int b = (4 * a + 3) / 146097; - const int c = a - (b * 146097) / 4; - const int d = (4 * c + 3) / 1461; - const int e = c - (d * 1461) / 4; - const int m = (5 * e + 2) / 153; + result.tm_mday = e - (153 * m + 2) / 5 + 1; + result.tm_mon = m + 2 - 12 * (m / 10); + result.tm_year = b * 100 + d - 6700 + (m / 10); + result.tm_wday = (days + 1) % 7; + result.tm_yday = -1; - result.tm_mday = e - (153 * m + 2) / 5 + 1; - result.tm_mon = m + 2 - 12 * (m / 10); - result.tm_year = b * 100 + d - 6700 + (m / 10); - result.tm_wday = (days + 1) % 7; - result.tm_yday = -1; - - int t = (int) (jdm % 86400LL); - result.tm_hour = t / 3600; - t %= 3600; - result.tm_min = t / 60; - result.tm_sec = t % 60; - result.tm_isdst = -1; - } - else - { - time_t now = static_cast (seconds); - - #if JUCE_WINDOWS && JUCE_MINGW - return *localtime (&now); - #elif JUCE_WINDOWS - if (now >= 0 && now <= 0x793406fff) - localtime_s (&result, &now); - else - zerostruct (result); - #else - localtime_r (&now, &result); // more thread-safe - #endif - } + int t = (int) (jdm % 86400LL); + result.tm_hour = t / 3600; + t %= 3600; + result.tm_min = t / 60; + result.tm_sec = t % 60; + result.tm_isdst = -1; return result; } + static bool isBeyond1970to2030Range (const int64 seconds) + { + return seconds < 86400LL || seconds >= 2145916800LL; + } + + static struct tm millisToLocal (const int64 millis) noexcept + { + const int64 seconds = millis / 1000; + + if (isBeyond1970to2030Range (seconds)) + { + const int timeZoneAdjustment = 31536000 - (int) (Time (1971, 0, 1, 0, 0).toMilliseconds() / 1000); + return millisecondsToTM (seconds + timeZoneAdjustment + 210866803200LL); + } + + struct tm result; + time_t now = static_cast (seconds); + + #if JUCE_WINDOWS && JUCE_MINGW + return *localtime (&now); + #elif JUCE_WINDOWS + if (now >= 0 && now <= 0x793406fff) + localtime_s (&result, &now); + else + zerostruct (result); + #else + localtime_r (&now, &result); // more thread-safe + #endif + + return result; + } + + static struct tm millisToUTC (const int64 millis) noexcept + { + const int64 seconds = millis / 1000; + + if (isBeyond1970to2030Range (seconds)) + return millisecondsToTM (seconds + 210866803200LL); + + struct tm result; + time_t now = static_cast (seconds); + + #if JUCE_WINDOWS && JUCE_MINGW + return *gmtime (&now); + #elif JUCE_WINDOWS + if (now >= 0 && now <= 0x793406fff) + gmtime_s (&result, &now); + else + zerostruct (result); + #else + gmtime_r (&now, &result); // more thread-safe + #endif + + return result; + } + + static int getUTCOffsetSeconds (const int64 millis) noexcept + { + struct tm utc = millisToUTC (millis); + utc.tm_isdst = -1; // Treat this UTC time as local to find the offset + + return (int) ((millis / 1000) - (int64) mktime (&utc)); + } + static int extendedModulo (const int64 value, const int modulo) noexcept { return (int) (value >= 0 ? (value % modulo) @@ -388,6 +429,120 @@ String Time::getTimeZone() const noexcept return zone[0].substring (0, 3); } +int Time::getUTCOffsetSeconds() const noexcept +{ + return TimeHelpers::getUTCOffsetSeconds (millisSinceEpoch); +} + +String Time::getUTCOffsetString (bool includeSemiColon) const +{ + if (int seconds = getUTCOffsetSeconds()) + { + const int minutes = seconds / 60; + + return String::formatted (includeSemiColon ? "%+03d:%02d" + : "%+03d%02d", + minutes / 60, + minutes % 60); + } + + return "Z"; +} + +String Time::toISO8601 (bool includeDividerCharacters) const +{ + return String::formatted (includeDividerCharacters ? "%04d-%02d-%02dT%02d:%02d:%02d:%03d" + : "%04d%02d%02dT%02d%02d%02d%03d", + getYear(), + getMonth() + 1, + getDayOfMonth(), + getHours(), + getMinutes(), + getSeconds(), + getMilliseconds()) + + getUTCOffsetString (includeDividerCharacters); +} + +static int parseFixedSizeIntAndSkip (String::CharPointerType& t, int numChars, char charToSkip) noexcept +{ + int n = 0; + + for (int i = numChars; --i >= 0;) + { + const int digit = (int) (*t - '0'); + + if (! isPositiveAndBelow (digit, 10)) + return -1; + + ++t; + n = n * 10 + digit; + } + + if (charToSkip != 0 && *t == (juce_wchar) charToSkip) + ++t; + + return n; +} + +Time Time::fromISO8601 (StringRef iso) noexcept +{ + String::CharPointerType t = iso.text; + + const int year = parseFixedSizeIntAndSkip (t, 4, '-'); + if (year < 0) + return Time(); + + const int month = parseFixedSizeIntAndSkip (t, 2, '-'); + if (month < 0) + return Time(); + + const int day = parseFixedSizeIntAndSkip (t, 2, 0); + if (day < 0) + return Time(); + + int hours = 0, minutes = 0, seconds = 0, milliseconds = 0; + + if (*t == 'T') + { + ++t; + hours = parseFixedSizeIntAndSkip (t, 2, ':'); + if (hours < 0) + return Time(); + + minutes = parseFixedSizeIntAndSkip (t, 2, ':'); + if (minutes < 0) + return Time(); + + seconds = parseFixedSizeIntAndSkip (t, 2, ':'); + if (seconds < 0) + return Time(); + + milliseconds = jmax (0, parseFixedSizeIntAndSkip (t, 3, 0)); + } + + Time result (year, month - 1, day, hours, minutes, seconds, milliseconds, false); + + const bool negative = *t == '-'; + + if (negative || *t == '+') + { + ++t; + + const int offsetHours = parseFixedSizeIntAndSkip (t, 2, ':'); + if (offsetHours < 0) + return Time(); + + const int offsetMinutes = parseFixedSizeIntAndSkip (t, 2, 0); + if (offsetMinutes < 0) + return Time(); + + const int offsetMs = (offsetHours * 60 + offsetMinutes) * 60 * 1000; + result.millisSinceEpoch += negative ? offsetMs : -offsetMs; // NB: this seems backwards but is correct! + } + + return result; +} + String Time::getMonthName (const bool threeLetterVersion) const { return getMonthName (getMonth(), threeLetterVersion); @@ -463,3 +618,54 @@ Time Time::getCompilationDate() timeTokens[0].getIntValue(), timeTokens[1].getIntValue()); } + + +//============================================================================== +//============================================================================== +#if JUCE_UNIT_TESTS + +class TimeTests : public UnitTest +{ +public: + TimeTests() : UnitTest ("Time") {} + + void runTest() override + { + beginTest ("Time"); + + Time t = Time::getCurrentTime(); + expect (t > Time()); + + Thread::sleep (15); + expect (Time::getCurrentTime() > t); + + expect (t.getTimeZone().isNotEmpty()); + expect (t.getUTCOffsetString (true) == "Z" || t.getUTCOffsetString (true).length() == 6); + expect (t.getUTCOffsetString (false) == "Z" || t.getUTCOffsetString (false).length() == 5); + + DBG (t.getUTCOffsetSeconds()); + DBG (t.getUTCOffsetString (true)); + + DBG (t.toISO8601 (true)); + DBG (Time::fromISO8601 (t.toISO8601 (true)).toISO8601 (true)); + DBG (t.toISO8601 (false)); + + expect (Time::fromISO8601 (t.toISO8601 (true)) == t); + expect (Time::fromISO8601 (t.toISO8601 (false)) == t); + + expect (Time::fromISO8601 ("2016-02-16") == Time (2016, 1, 16, 0, 0, 0, 0, false)); + expect (Time::fromISO8601 ("20160216") == Time (2016, 1, 16, 0, 0, 0, 0, false)); + expect (Time::fromISO8601 ("2016-02-16T15:03:57+00:00") == Time (2016, 1, 16, 15, 3, 57, 0, false)); + expect (Time::fromISO8601 ("20160216T150357+0000") == Time (2016, 1, 16, 15, 3, 57, 0, false)); + expect (Time::fromISO8601 ("2016-02-16T15:03:57:999+00:00") == Time (2016, 1, 16, 15, 3, 57, 999, false)); + expect (Time::fromISO8601 ("20160216T150357999+0000") == Time (2016, 1, 16, 15, 3, 57, 999, false)); + expect (Time::fromISO8601 ("2016-02-16T15:03:57:999Z") == Time (2016, 1, 16, 15, 3, 57, 999, false)); + expect (Time::fromISO8601 ("20160216T150357999Z") == Time (2016, 1, 16, 15, 3, 57, 999, false)); + expect (Time::fromISO8601 ("2016-02-16T15:03:57:999-02:30") == Time (2016, 1, 16, 17, 33, 57, 999, false)); + expect (Time::fromISO8601 ("20160216T150357999-0230") == Time (2016, 1, 16, 17, 33, 57, 999, false)); + } +}; + +static TimeTests timeTests; + +#endif diff --git a/modules/juce_core/time/juce_Time.h b/modules/juce_core/time/juce_Time.h index a2bdb3ab05..a71776c2e6 100644 --- a/modules/juce_core/time/juce_Time.h +++ b/modules/juce_core/time/juce_Time.h @@ -44,7 +44,7 @@ public: //============================================================================== /** Creates a Time object. - This default constructor creates a time of 1st January 1970, (which is + This default constructor creates a time of midnight Jan 1st 1970 UTC, (which is represented internally as 0ms). To create a time object representing the current time, use getCurrentTime(). @@ -55,19 +55,16 @@ public: /** Creates a time based on a number of milliseconds. - The internal millisecond count is set to 0 (1st January 1970). To create a - time object set to the current time, use getCurrentTime(). + To create a time object set to the current time, use getCurrentTime(). @param millisecondsSinceEpoch the number of milliseconds since the unix - 'epoch' (midnight Jan 1st 1970). + 'epoch' (midnight Jan 1st 1970 UTC). @see getCurrentTime, currentTimeMillis */ explicit Time (int64 millisecondsSinceEpoch) noexcept; /** Creates a time from a set of date components. - The timezone is assumed to be whatever the system is using as its locale. - @param year the year, in 4-digit format, e.g. 2004 @param month the month, in the range 0 to 11 @param day the day of the month, in the range 1 to 31 @@ -75,8 +72,8 @@ public: @param minutes minutes 0 to 59 @param seconds seconds 0 to 59 @param milliseconds milliseconds 0 to 999 - @param useLocalTime if true, encode using the current machine's local time; if - false, it will always work in GMT. + @param useLocalTime if true, assume input is in this machine's local timezone + if false, assume input is in UTC. */ Time (int year, int month, @@ -107,82 +104,71 @@ public: static Time JUCE_CALLTYPE getCurrentTime() noexcept; /** Returns the time as a number of milliseconds. - @returns the number of milliseconds this Time object represents, since - midnight jan 1st 1970. + midnight Jan 1st 1970 UTC. @see getMilliseconds */ int64 toMilliseconds() const noexcept { return millisSinceEpoch; } - /** Returns the year. - + /** Returns the year (in this machine's local timezone). A 4-digit format is used, e.g. 2004. */ int getYear() const noexcept; - /** Returns the number of the month. - + /** Returns the number of the month (in this machine's local timezone). The value returned is in the range 0 to 11. @see getMonthName */ int getMonth() const noexcept; - /** Returns the name of the month. - + /** Returns the name of the month (in this machine's local timezone). @param threeLetterVersion if true, it'll be a 3-letter abbreviation, e.g. "Jan"; if false it'll return the long form, e.g. "January" @see getMonth */ String getMonthName (bool threeLetterVersion) const; - /** Returns the day of the month. + /** Returns the day of the month (in this machine's local timezone). The value returned is in the range 1 to 31. */ int getDayOfMonth() const noexcept; - /** Returns the number of the day of the week. + /** Returns the number of the day of the week (in this machine's local timezone). The value returned is in the range 0 to 6 (0 = sunday, 1 = monday, etc). */ int getDayOfWeek() const noexcept; - /** Returns the number of the day of the year. + /** Returns the number of the day of the year (in this machine's local timezone). The value returned is in the range 0 to 365. */ int getDayOfYear() const noexcept; - /** Returns the name of the weekday. - + /** Returns the name of the weekday (in this machine's local timezone). @param threeLetterVersion if true, it'll return a 3-letter abbreviation, e.g. "Tue"; if false, it'll return the full version, e.g. "Tuesday". */ String getWeekdayName (bool threeLetterVersion) const; - /** Returns the number of hours since midnight. - + /** Returns the number of hours since midnight (in this machine's local timezone). This is in 24-hour clock format, in the range 0 to 23. - @see getHoursInAmPmFormat, isAfternoon */ int getHours() const noexcept; - /** Returns true if the time is in the afternoon. - - So it returns true for "PM", false for "AM". - + /** Returns true if the time is in the afternoon (in this machine's local timezone). + @returns true for "PM", false for "AM". @see getHoursInAmPmFormat, getHours */ bool isAfternoon() const noexcept; - /** Returns the hours in 12-hour clock format. - + /** Returns the hours in 12-hour clock format (in this machine's local timezone). This will return a value 1 to 12 - use isAfternoon() to find out whether this is in the afternoon or morning. - @see getHours, isAfternoon */ int getHoursInAmPmFormat() const noexcept; - /** Returns the number of minutes, 0 to 59. */ + /** Returns the number of minutes, 0 to 59 (in this machine's local timezone). */ int getMinutes() const noexcept; /** Returns the number of seconds, 0 to 59. */ @@ -200,11 +186,21 @@ public: /** Returns true if the local timezone uses a daylight saving correction. */ bool isDaylightSavingTime() const noexcept; + //============================================================================== /** Returns a 3-character string to indicate the local timezone. */ String getTimeZone() const noexcept; + /** Returns the local timezone offset from UTC in seconds. */ + int getUTCOffsetSeconds() const noexcept; + + /** Returns a string to indicate the offset of the local timezone from UTC. + @returns "+XX:XX", "-XX:XX" or "Z" + @param includeDividerCharacters whether to include or omit the ":" divider in the string + */ + String getUTCOffsetString (bool includeDividerCharacters) const; + //============================================================================== - /** Quick way of getting a string version of a date and time. + /** Returns a string version of this date and time, using this machine's local timezone. For a more powerful way of formatting the date and time, see the formatted() method. @@ -227,33 +223,45 @@ public: looking it up, these are the escape codes that strftime uses (other codes might work on some platforms and not others, but these are the common ones): - %a is replaced by the locale's abbreviated weekday name. - %A is replaced by the locale's full weekday name. - %b is replaced by the locale's abbreviated month name. - %B is replaced by the locale's full month name. - %c is replaced by the locale's appropriate date and time representation. - %d is replaced by the day of the month as a decimal number [01,31]. - %H is replaced by the hour (24-hour clock) as a decimal number [00,23]. - %I is replaced by the hour (12-hour clock) as a decimal number [01,12]. - %j is replaced by the day of the year as a decimal number [001,366]. - %m is replaced by the month as a decimal number [01,12]. - %M is replaced by the minute as a decimal number [00,59]. - %p is replaced by the locale's equivalent of either a.m. or p.m. - %S is replaced by the second as a decimal number [00,61]. - %U is replaced by the week number of the year (Sunday as the first day of the week) as a decimal number [00,53]. - %w is replaced by the weekday as a decimal number [0,6], with 0 representing Sunday. - %W is replaced by the week number of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday are considered to be in week 0. - %x is replaced by the locale's appropriate date representation. - %X is replaced by the locale's appropriate time representation. - %y is replaced by the year without century as a decimal number [00,99]. - %Y is replaced by the year with century as a decimal number. - %Z is replaced by the timezone name or abbreviation, or by no bytes if no timezone information exists. - %% is replaced by %. + - %a is replaced by the locale's abbreviated weekday name. + - %A is replaced by the locale's full weekday name. + - %b is replaced by the locale's abbreviated month name. + - %B is replaced by the locale's full month name. + - %c is replaced by the locale's appropriate date and time representation. + - %d is replaced by the day of the month as a decimal number [01,31]. + - %H is replaced by the hour (24-hour clock) as a decimal number [00,23]. + - %I is replaced by the hour (12-hour clock) as a decimal number [01,12]. + - %j is replaced by the day of the year as a decimal number [001,366]. + - %m is replaced by the month as a decimal number [01,12]. + - %M is replaced by the minute as a decimal number [00,59]. + - %p is replaced by the locale's equivalent of either a.m. or p.m. + - %S is replaced by the second as a decimal number [00,61]. + - %U is replaced by the week number of the year (Sunday as the first day of the week) as a decimal number [00,53]. + - %w is replaced by the weekday as a decimal number [0,6], with 0 representing Sunday. + - %W is replaced by the week number of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday are considered to be in week 0. + - %x is replaced by the locale's appropriate date representation. + - %X is replaced by the locale's appropriate time representation. + - %y is replaced by the year without century as a decimal number [00,99]. + - %Y is replaced by the year with century as a decimal number. + - %Z is replaced by the timezone name or abbreviation, or by no bytes if no timezone information exists. + - %% is replaced by %. @see toString */ String formatted (const String& format) const; + //============================================================================== + /** Returns a fully described string of this date and time in ISO-8601 format + (using the local timezone). + + @param includeDividerCharacters whether to include or omit the "-" and ":" + dividers in the string + */ + String toISO8601 (bool includeDividerCharacters) const; + + /** Parses an ISO-8601 string and returns it as a Time. */ + static Time fromISO8601 (StringRef iso8601) noexcept; + //============================================================================== /** Adds a RelativeTime to this time. */ Time& operator+= (RelativeTime delta) noexcept; @@ -290,7 +298,7 @@ public: /** Returns the current system time. - Returns the number of milliseconds since midnight jan 1st 1970. + Returns the number of milliseconds since midnight Jan 1st 1970 UTC. Should be accurate to within a few millisecs, depending on platform, hardware, etc.