Skip to content

Commit

Permalink
Bug 1731620 - Part 1: Add more time zone functions to mozilla::intl::…
Browse files Browse the repository at this point in the history
…TimeZone. r=platform-i18n-reviewers,gregtatum

Add four additional methods to `mozilla::intl::TimeZone`:

1. `GetDSTOffsetMs()` to return the daylight saving offset at a specific UTC time.
2. `GetOffsetMs()` to return the time zone offset at a specific UTC time.
3. `GetUTCOffsetMs()` to return the UTC offset at a specific local time.
4. `GetDisplayName()` to return the display name of a time zone.

All four methods will be used to replace ICU calls in "js/src/vm/DateTime.cpp".

Differential Revision: https://phabricator.services.mozilla.com/D126191
  • Loading branch information
anba committed Sep 23, 2021
1 parent c8c3f41 commit 2167149
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 4 deletions.
112 changes: 108 additions & 4 deletions intl/components/gtest/TestTimeZone.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,121 @@

namespace mozilla::intl {

// Firefox 1.0 release date.
static constexpr int64_t RELEASE_DATE = 1'032'800'850'000;

// Date.UTC(2021, 11-1, 7, 2, 0, 0)
static constexpr int64_t DST_CHANGE_DATE = 1'636'250'400'000;

// These tests are dependent on the machine that this test is being run on.
// Unwrap the results to ensure it doesn't fail, but don't check the values.
TEST(IntlTimeZone, SystemDependentTests)
{
// e.g. "America/Chicago"
TestBuffer<char16_t> buffer;
TimeZone::GetDefaultTimeZone(buffer).unwrap();
}

TEST(IntlTimeZone, GetRawOffsetMs)
{
auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();
ASSERT_EQ(timeZone->GetRawOffsetMs().unwrap(), 3 * 60 * 60 * 1000);

timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();
ASSERT_EQ(timeZone->GetRawOffsetMs().unwrap(), -(3 * 60 * 60 * 1000));

timeZone =
TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();
ASSERT_EQ(timeZone->GetRawOffsetMs().unwrap(), -(5 * 60 * 60 * 1000));
}

TEST(IntlTimeZone, GetDSTOffsetMs)
{
auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();
ASSERT_EQ(timeZone->GetDSTOffsetMs(0).unwrap(), 0);

timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();
ASSERT_EQ(timeZone->GetDSTOffsetMs(0).unwrap(), 0);

timeZone =
TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();
ASSERT_EQ(timeZone->GetDSTOffsetMs(0).unwrap(), 0);
ASSERT_EQ(timeZone->GetDSTOffsetMs(RELEASE_DATE).unwrap(),
1 * 60 * 60 * 1000);
ASSERT_EQ(timeZone->GetDSTOffsetMs(DST_CHANGE_DATE).unwrap(),
1 * 60 * 60 * 1000);
}

TEST(IntlTimeZone, GetOffsetMs)
{
auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();
ASSERT_EQ(timeZone->GetOffsetMs(0).unwrap(), 3 * 60 * 60 * 1000);

timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();
ASSERT_EQ(timeZone->GetOffsetMs(0).unwrap(), -(3 * 60 * 60 * 1000));

timeZone =
TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();
ASSERT_EQ(timeZone->GetOffsetMs(0).unwrap(), -(5 * 60 * 60 * 1000));
ASSERT_EQ(timeZone->GetOffsetMs(RELEASE_DATE).unwrap(),
-(4 * 60 * 60 * 1000));
ASSERT_EQ(timeZone->GetOffsetMs(DST_CHANGE_DATE).unwrap(),
-(4 * 60 * 60 * 1000));
}

TEST(IntlTimeZone, GetUTCOffsetMs)
{
auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();
ASSERT_EQ(timeZone->GetUTCOffsetMs(0).unwrap(), 3 * 60 * 60 * 1000);

timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();
ASSERT_EQ(timeZone->GetUTCOffsetMs(0).unwrap(), -(3 * 60 * 60 * 1000));

timeZone =
TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();
ASSERT_EQ(timeZone->GetUTCOffsetMs(0).unwrap(), -(5 * 60 * 60 * 1000));
ASSERT_EQ(timeZone->GetUTCOffsetMs(RELEASE_DATE).unwrap(),
-(4 * 60 * 60 * 1000));
ASSERT_EQ(timeZone->GetUTCOffsetMs(DST_CHANGE_DATE).unwrap(),
-(5 * 60 * 60 * 1000));
}

TEST(IntlTimeZone, GetDisplayName)
{
using DaylightSavings = TimeZone::DaylightSavings;

TestBuffer<char16_t> buffer;
// e.g. For America/Chicago: 1000 * 60 * 60 * -6
timeZone->GetRawOffsetMs().unwrap();

// e.g. "America/Chicago"
TimeZone::GetDefaultTimeZone(buffer).unwrap();
auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();

buffer.clear();
timeZone->GetDisplayName("en", DaylightSavings::No, buffer).unwrap();
ASSERT_EQ(buffer.get_string_view(), u"GMT+03:00");

buffer.clear();
timeZone->GetDisplayName("en", DaylightSavings::Yes, buffer).unwrap();
ASSERT_EQ(buffer.get_string_view(), u"GMT+03:00");

timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();

buffer.clear();
timeZone->GetDisplayName("en", DaylightSavings::No, buffer).unwrap();
ASSERT_EQ(buffer.get_string_view(), u"GMT-03:00");

buffer.clear();
timeZone->GetDisplayName("en", DaylightSavings::Yes, buffer).unwrap();
ASSERT_EQ(buffer.get_string_view(), u"GMT-03:00");

timeZone =
TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();

buffer.clear();
timeZone->GetDisplayName("en", DaylightSavings::No, buffer).unwrap();
ASSERT_EQ(buffer.get_string_view(), u"Eastern Standard Time");

buffer.clear();
timeZone->GetDisplayName("en", DaylightSavings::Yes, buffer).unwrap();
ASSERT_EQ(buffer.get_string_view(), u"Eastern Daylight Time");
}

TEST(IntlTimeZone, GetCanonicalTimeZoneID)
Expand Down
96 changes: 96 additions & 0 deletions intl/components/src/TimeZone.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,30 @@ Result<UniquePtr<TimeZone>, ICUError> TimeZone::TryCreate(
return Err(ToICUError(status));
}

// https://tc39.es/ecma262/#sec-time-values-and-time-range
//
// A time value supports a slightly smaller range of -8,640,000,000,000,000 to
// 8,640,000,000,000,000 milliseconds.
constexpr double StartOfTime = -8.64e15;

// Ensure all computations are performed in the proleptic Gregorian calendar.
ucal_setGregorianChange(calendar, StartOfTime, &status);

if (U_FAILURE(status)) {
return Err(ToICUError(status));
}

return MakeUnique<TimeZone>(calendar);
}

Result<int32_t, ICUError> TimeZone::GetRawOffsetMs() {
// Reset the time in case the calendar has been modified.
UErrorCode status = U_ZERO_ERROR;
ucal_setMillis(mCalendar, ucal_getNow(), &status);
if (U_FAILURE(status)) {
return Err(ToICUError(status));
}

int32_t offset = ucal_get(mCalendar, UCAL_ZONE_OFFSET, &status);
if (U_FAILURE(status)) {
return Err(ToICUError(status));
Expand All @@ -44,6 +63,83 @@ Result<int32_t, ICUError> TimeZone::GetRawOffsetMs() {
return offset;
}

Result<int32_t, ICUError> TimeZone::GetDSTOffsetMs(int64_t aUTCMilliseconds) {
UDate date = UDate(aUTCMilliseconds);

UErrorCode status = U_ZERO_ERROR;
ucal_setMillis(mCalendar, date, &status);
if (U_FAILURE(status)) {
return Err(ToICUError(status));
}

int32_t dstOffset = ucal_get(mCalendar, UCAL_DST_OFFSET, &status);
if (U_FAILURE(status)) {
return Err(ToICUError(status));
}

return dstOffset;
}

Result<int32_t, ICUError> TimeZone::GetOffsetMs(int64_t aUTCMilliseconds) {
UDate date = UDate(aUTCMilliseconds);

UErrorCode status = U_ZERO_ERROR;
ucal_setMillis(mCalendar, date, &status);
if (U_FAILURE(status)) {
return Err(ToICUError(status));
}

int32_t rawOffset = ucal_get(mCalendar, UCAL_ZONE_OFFSET, &status);
if (U_FAILURE(status)) {
return Err(ToICUError(status));
}

int32_t dstOffset = ucal_get(mCalendar, UCAL_DST_OFFSET, &status);
if (U_FAILURE(status)) {
return Err(ToICUError(status));
}

return rawOffset + dstOffset;
}

Result<int32_t, ICUError> TimeZone::GetUTCOffsetMs(int64_t aLocalMilliseconds) {
// https://tc39.es/ecma262/#sec-local-time-zone-adjustment
//
// LocalTZA ( t, isUTC )
//
// When t_local represents local time repeating multiple times at a negative
// time zone transition (e.g. when the daylight saving time ends or the time
// zone offset is decreased due to a time zone rule change) or skipped local
// time at a positive time zone transitions (e.g. when the daylight saving
// time starts or the time zone offset is increased due to a time zone rule
// change), t_local must be interpreted using the time zone offset before the
// transition.
#ifndef U_HIDE_DRAFT_API
constexpr UTimeZoneLocalOption skippedTime = UCAL_TZ_LOCAL_FORMER;
constexpr UTimeZoneLocalOption repeatedTime = UCAL_TZ_LOCAL_FORMER;
#else
constexpr UTimeZoneLocalOption skippedTime = UTimeZoneLocalOption(0x4);
constexpr UTimeZoneLocalOption repeatedTime = UTimeZoneLocalOption(0x4);
#endif

UDate date = UDate(aLocalMilliseconds);

UErrorCode status = U_ZERO_ERROR;
ucal_setMillis(mCalendar, date, &status);
if (U_FAILURE(status)) {
return Err(ToICUError(status));
}

int32_t rawOffset, dstOffset;
ucal_getTimeZoneOffsetFromLocal(mCalendar, skippedTime, repeatedTime,
&rawOffset, &dstOffset, &status);
if (U_FAILURE(status)) {
return Err(ToICUError(status));
}

return rawOffset + dstOffset;
}

Result<SpanEnumeration<char>, ICUError> TimeZone::GetAvailableTimeZones(
const char* aRegion) {
// Get the time zones that are commonly used in the given region. Uses the
Expand Down
32 changes: 32 additions & 0 deletions intl/components/src/TimeZone.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,38 @@ class TimeZone final {
*/
Result<int32_t, ICUError> GetRawOffsetMs();

/**
* Return the daylight saving offset in milliseconds at the given UTC time.
*/
Result<int32_t, ICUError> GetDSTOffsetMs(int64_t aUTCMilliseconds);

/**
* Return the local offset in milliseconds at the given UTC time.
*/
Result<int32_t, ICUError> GetOffsetMs(int64_t aUTCMilliseconds);

/**
* Return the UTC offset in milliseconds at the given local time.
*/
Result<int32_t, ICUError> GetUTCOffsetMs(int64_t aLocalMilliseconds);

enum class DaylightSavings : bool { No, Yes };

/**
* Return the display name for this time zone.
*/
template <typename B>
ICUResult GetDisplayName(const char* aLocale,
DaylightSavings aDaylightSavings, B& aBuffer) {
return FillBufferWithICUCall(
aBuffer, [&](UChar* target, int32_t length, UErrorCode* status) {
UCalendarDisplayNameType type =
static_cast<bool>(aDaylightSavings) ? UCAL_DST : UCAL_STANDARD;
return ucal_getTimeZoneDisplayName(mCalendar, type, aLocale, target,
length, status);
});
}

/**
* Fill the buffer with the system's default IANA time zone identifier, e.g.
* "America/Chicago".
Expand Down

0 comments on commit 2167149

Please sign in to comment.