/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/intl/TimeZone.h" #include "mozilla/Vector.h" #include #include #include "unicode/uenum.h" #if MOZ_INTL_USE_ICU_CPP_TIMEZONE # include "unicode/basictz.h" #endif namespace mozilla::intl { /* static */ Result, ICUError> TimeZone::TryCreate( Maybe> aTimeZoneOverride) { const UChar* zoneID = nullptr; int32_t zoneIDLen = 0; if (aTimeZoneOverride) { zoneIDLen = static_cast(aTimeZoneOverride->Length()); zoneID = aTimeZoneOverride->Elements(); } #if MOZ_INTL_USE_ICU_CPP_TIMEZONE UniquePtr tz; if (zoneID) { tz.reset( icu::TimeZone::createTimeZone(icu::UnicodeString(zoneID, zoneIDLen))); } else { tz.reset(icu::TimeZone::createDefault()); } MOZ_ASSERT(tz); if (*tz == icu::TimeZone::getUnknown()) { return Err(ICUError::InternalError); } return MakeUnique(std::move(tz)); #else // An empty string is used for the root locale. This is regarded as the base // locale of all locales, and is used as the language/country neutral locale // for locale sensitive operations. const char* rootLocale = ""; UErrorCode status = U_ZERO_ERROR; UCalendar* calendar = ucal_open(zoneID, zoneIDLen, rootLocale, UCAL_DEFAULT, &status); if (U_FAILURE(status)) { 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(calendar); #endif } Result TimeZone::GetRawOffsetMs() { #if MOZ_INTL_USE_ICU_CPP_TIMEZONE return mTimeZone->getRawOffset(); #else // 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)); } return offset; #endif } Result TimeZone::GetDSTOffsetMs(int64_t aUTCMilliseconds) { UDate date = UDate(aUTCMilliseconds); #if MOZ_INTL_USE_ICU_CPP_TIMEZONE constexpr bool dateIsLocalTime = false; int32_t rawOffset, dstOffset; UErrorCode status = U_ZERO_ERROR; mTimeZone->getOffset(date, dateIsLocalTime, rawOffset, dstOffset, status); if (U_FAILURE(status)) { return Err(ToICUError(status)); } return dstOffset; #else 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; #endif } Result TimeZone::GetOffsetMs(int64_t aUTCMilliseconds) { UDate date = UDate(aUTCMilliseconds); #if MOZ_INTL_USE_ICU_CPP_TIMEZONE constexpr bool dateIsLocalTime = false; int32_t rawOffset, dstOffset; UErrorCode status = U_ZERO_ERROR; mTimeZone->getOffset(date, dateIsLocalTime, rawOffset, dstOffset, status); if (U_FAILURE(status)) { return Err(ToICUError(status)); } return rawOffset + dstOffset; #else 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; #endif } Result 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. constexpr LocalOption skippedTime = LocalOption::Former; constexpr LocalOption repeatedTime = LocalOption::Former; return GetUTCOffsetMs(aLocalMilliseconds, skippedTime, repeatedTime); } static UTimeZoneLocalOption ToUTimeZoneLocalOption( TimeZone::LocalOption aOption) { switch (aOption) { case TimeZone::LocalOption::Former: return UTimeZoneLocalOption::UCAL_TZ_LOCAL_FORMER; case TimeZone::LocalOption::Latter: return UTimeZoneLocalOption::UCAL_TZ_LOCAL_LATTER; } MOZ_CRASH("Unexpected TimeZone::LocalOption"); } Result TimeZone::GetUTCOffsetMs(int64_t aLocalMilliseconds, LocalOption aSkippedTime, LocalOption aRepeatedTime) { UDate date = UDate(aLocalMilliseconds); UTimeZoneLocalOption skippedTime = ToUTimeZoneLocalOption(aSkippedTime); UTimeZoneLocalOption repeatedTime = ToUTimeZoneLocalOption(aRepeatedTime); #if MOZ_INTL_USE_ICU_CPP_TIMEZONE int32_t rawOffset, dstOffset; UErrorCode status = U_ZERO_ERROR; // All ICU TimeZone classes derive from BasicTimeZone, so we can safely // perform the static_cast. // Once is fixed we // can remove this extra cast. auto* basicTz = static_cast(mTimeZone.get()); basicTz->getOffsetFromLocal(date, skippedTime, repeatedTime, rawOffset, dstOffset, status); if (U_FAILURE(status)) { return Err(ToICUError(status)); } return rawOffset + dstOffset; #else 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; #endif } Result, ICUError> TimeZone::GetPreviousTransition( int64_t aUTCMilliseconds) { UDate date = UDate(aUTCMilliseconds); #if MOZ_INTL_USE_ICU_CPP_TIMEZONE // All ICU TimeZone classes derive from BasicTimeZone, so we can safely // perform the static_cast. auto* basicTz = static_cast(mTimeZone.get()); constexpr bool inclusive = false; icu::TimeZoneTransition transition; if (!basicTz->getPreviousTransition(date, inclusive, transition)) { return Maybe(); } return Some(int64_t(transition.getTime())); #else UDate transition = 0; UErrorCode status = U_ZERO_ERROR; bool found = ucal_getTimeZoneTransitionDate( mCalendar, UCAL_TZ_TRANSITION_PREVIOUS, &transition, &status); if (U_FAILURE(status)) { return Err(ToICUError(status)); } if (!found) { return Maybe(); } return Some(int64_t(transition)); #endif } Result, ICUError> TimeZone::GetNextTransition( int64_t aUTCMilliseconds) { UDate date = UDate(aUTCMilliseconds); #if MOZ_INTL_USE_ICU_CPP_TIMEZONE // All ICU TimeZone classes derive from BasicTimeZone, so we can safely // perform the static_cast. auto* basicTz = static_cast(mTimeZone.get()); constexpr bool inclusive = false; icu::TimeZoneTransition transition; if (!basicTz->getNextTransition(date, inclusive, transition)) { return Maybe(); } return Some(int64_t(transition.getTime())); #else UDate transition = 0; UErrorCode status = U_ZERO_ERROR; bool found = ucal_getTimeZoneTransitionDate( mCalendar, UCAL_TZ_TRANSITION_NEXT, &transition, &status); if (U_FAILURE(status)) { return Err(ToICUError(status)); } if (!found) { return Maybe(); } return Some(int64_t(transition)); #endif } using TimeZoneIdentifierVector = Vector; #if !MOZ_INTL_USE_ICU_CPP_TIMEZONE static bool IsUnknownTimeZone(const TimeZoneIdentifierVector& timeZone) { constexpr std::string_view unknownTimeZone = UCAL_UNKNOWN_ZONE_ID; return timeZone.length() == unknownTimeZone.length() && std::equal(timeZone.begin(), timeZone.end(), unknownTimeZone.begin(), unknownTimeZone.end()); } static ICUResult SetDefaultTimeZone(TimeZoneIdentifierVector& timeZone) { // The string mustn't already be null-terminated. MOZ_ASSERT_IF(!timeZone.empty(), timeZone.end()[-1] != '\0'); // The time zone identifier must be a null-terminated string. if (!timeZone.append('\0')) { return Err(ICUError::OutOfMemory); } UErrorCode status = U_ZERO_ERROR; ucal_setDefaultTimeZone(timeZone.begin(), &status); if (U_FAILURE(status)) { return Err(ToICUError(status)); } return Ok{}; } #endif Result TimeZone::SetDefaultTimeZone( Span aTimeZone) { #if MOZ_INTL_USE_ICU_CPP_TIMEZONE icu::UnicodeString tzid(aTimeZone.data(), aTimeZone.size(), US_INV); if (tzid.isBogus()) { return Err(ICUError::OutOfMemory); } UniquePtr newTimeZone(icu::TimeZone::createTimeZone(tzid)); MOZ_ASSERT(newTimeZone); if (*newTimeZone != icu::TimeZone::getUnknown()) { // adoptDefault() takes ownership of the time zone. icu::TimeZone::adoptDefault(newTimeZone.release()); return true; } #else TimeZoneIdentifierVector tzid; if (!tzid.append(aTimeZone.data(), aTimeZone.size())) { return Err(ICUError::OutOfMemory); } // Retrieve the current default time zone in case we need to restore it. TimeZoneIdentifierVector defaultTimeZone; MOZ_TRY(FillBufferWithICUCall(defaultTimeZone, ucal_getDefaultTimeZone)); // Try to set the new time zone. MOZ_TRY(mozilla::intl::SetDefaultTimeZone(tzid)); // Check if the time zone was actually applied. TimeZoneIdentifierVector newTimeZone; MOZ_TRY(FillBufferWithICUCall(newTimeZone, ucal_getDefaultTimeZone)); // Return if the new time zone was successfully applied. if (!IsUnknownTimeZone(newTimeZone)) { return true; } // Otherwise restore the original time zone. MOZ_TRY(mozilla::intl::SetDefaultTimeZone(defaultTimeZone)); #endif return false; } ICUResult TimeZone::SetDefaultTimeZoneFromHostTimeZone() { #if MOZ_INTL_USE_ICU_CPP_TIMEZONE if (icu::TimeZone* defaultZone = icu::TimeZone::detectHostTimeZone()) { icu::TimeZone::adoptDefault(defaultZone); } #else TimeZoneIdentifierVector hostTimeZone; MOZ_TRY(FillBufferWithICUCall(hostTimeZone, ucal_getHostTimeZone)); MOZ_TRY(mozilla::intl::SetDefaultTimeZone(hostTimeZone)); #endif return Ok{}; } Result, ICUError> TimeZone::GetTZDataVersion() { UErrorCode status = U_ZERO_ERROR; const char* tzdataVersion = ucal_getTZDataVersion(&status); if (U_FAILURE(status)) { return Err(ToICUError(status)); } return MakeStringSpan(tzdataVersion); } Result, ICUError> TimeZone::GetAvailableTimeZones( const char* aRegion) { // Get the time zones that are commonly used in the given region. Uses the // UCAL_ZONE_TYPE_ANY filter so we have more fine-grained control over the // returned time zones and don't omit time zones which are considered links in // ICU, but are treated as proper zones in IANA. UErrorCode status = U_ZERO_ERROR; UEnumeration* enumeration = ucal_openTimeZoneIDEnumeration( UCAL_ZONE_TYPE_ANY, aRegion, nullptr, &status); if (U_FAILURE(status)) { return Err(ToICUError(status)); } return SpanEnumeration(enumeration); } Result, ICUError> TimeZone::GetAvailableTimeZones() { UErrorCode status = U_ZERO_ERROR; UEnumeration* enumeration = ucal_openTimeZones(&status); if (U_FAILURE(status)) { return Err(ToICUError(status)); } return SpanEnumeration(enumeration); } #if !MOZ_INTL_USE_ICU_CPP_TIMEZONE TimeZone::~TimeZone() { MOZ_ASSERT(mCalendar); ucal_close(mCalendar); } #endif } // namespace mozilla::intl