TimeZone.cpp (13212B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #include "mozilla/intl/TimeZone.h" 6 7 #include "mozilla/Vector.h" 8 9 #include <algorithm> 10 #include <string_view> 11 12 #include "unicode/uenum.h" 13 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 14 # include "unicode/basictz.h" 15 #endif 16 17 namespace mozilla::intl { 18 19 /* static */ 20 Result<UniquePtr<TimeZone>, ICUError> TimeZone::TryCreate( 21 Maybe<Span<const char>> aTimeZoneOverride) { 22 const char* zoneID = nullptr; 23 int32_t zoneIDLen = 0; 24 if (aTimeZoneOverride) { 25 zoneIDLen = static_cast<int32_t>(aTimeZoneOverride->Length()); 26 zoneID = aTimeZoneOverride->Elements(); 27 } 28 29 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 30 UniquePtr<icu::TimeZone> tz; 31 if (zoneID) { 32 tz.reset(icu::TimeZone::createTimeZone( 33 icu::UnicodeString(zoneID, zoneIDLen, icu::UnicodeString::kInvariant))); 34 } else { 35 tz.reset(icu::TimeZone::createDefault()); 36 } 37 MOZ_ASSERT(tz); 38 39 if (*tz == icu::TimeZone::getUnknown()) { 40 return Err(ICUError::InternalError); 41 } 42 43 return MakeUnique<TimeZone>(std::move(tz)); 44 #else 45 const char16_t* zoneIDChar16 = nullptr; 46 Vector<char16_t, TimeZoneIdentifierLength> zoneIDChars; 47 if (zoneID) { 48 if (!zoneIDChars.append(zoneID, zoneIDLen)) { 49 return Err(ICUError::OutOfMemory); 50 } 51 zoneIDChar16 = zoneIDChars.begin(); 52 } 53 54 // An empty string is used for the root locale. This is regarded as the base 55 // locale of all locales, and is used as the language/country neutral locale 56 // for locale sensitive operations. 57 const char* rootLocale = ""; 58 59 UErrorCode status = U_ZERO_ERROR; 60 UCalendar* calendar = 61 ucal_open(zoneIDChar16, zoneIDLen, rootLocale, UCAL_DEFAULT, &status); 62 63 if (U_FAILURE(status)) { 64 return Err(ToICUError(status)); 65 } 66 67 // https://tc39.es/ecma262/#sec-time-values-and-time-range 68 // 69 // A time value supports a slightly smaller range of -8,640,000,000,000,000 to 70 // 8,640,000,000,000,000 milliseconds. 71 constexpr double StartOfTime = -8.64e15; 72 73 // Ensure all computations are performed in the proleptic Gregorian calendar. 74 ucal_setGregorianChange(calendar, StartOfTime, &status); 75 76 if (U_FAILURE(status)) { 77 return Err(ToICUError(status)); 78 } 79 80 return MakeUnique<TimeZone>(calendar); 81 #endif 82 } 83 84 Result<int32_t, ICUError> TimeZone::GetRawOffsetMs() { 85 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 86 return mTimeZone->getRawOffset(); 87 #else 88 // Reset the time in case the calendar has been modified. 89 UErrorCode status = U_ZERO_ERROR; 90 ucal_setMillis(mCalendar, ucal_getNow(), &status); 91 if (U_FAILURE(status)) { 92 return Err(ToICUError(status)); 93 } 94 95 int32_t offset = ucal_get(mCalendar, UCAL_ZONE_OFFSET, &status); 96 if (U_FAILURE(status)) { 97 return Err(ToICUError(status)); 98 } 99 100 return offset; 101 #endif 102 } 103 104 Result<int32_t, ICUError> TimeZone::GetDSTOffsetMs(int64_t aUTCMilliseconds) { 105 UDate date = UDate(aUTCMilliseconds); 106 107 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 108 constexpr bool dateIsLocalTime = false; 109 int32_t rawOffset, dstOffset; 110 UErrorCode status = U_ZERO_ERROR; 111 112 mTimeZone->getOffset(date, dateIsLocalTime, rawOffset, dstOffset, status); 113 if (U_FAILURE(status)) { 114 return Err(ToICUError(status)); 115 } 116 117 return dstOffset; 118 #else 119 UErrorCode status = U_ZERO_ERROR; 120 ucal_setMillis(mCalendar, date, &status); 121 if (U_FAILURE(status)) { 122 return Err(ToICUError(status)); 123 } 124 125 int32_t dstOffset = ucal_get(mCalendar, UCAL_DST_OFFSET, &status); 126 if (U_FAILURE(status)) { 127 return Err(ToICUError(status)); 128 } 129 130 return dstOffset; 131 #endif 132 } 133 134 Result<int32_t, ICUError> TimeZone::GetOffsetMs(int64_t aUTCMilliseconds) { 135 UDate date = UDate(aUTCMilliseconds); 136 137 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 138 constexpr bool dateIsLocalTime = false; 139 int32_t rawOffset, dstOffset; 140 UErrorCode status = U_ZERO_ERROR; 141 142 mTimeZone->getOffset(date, dateIsLocalTime, rawOffset, dstOffset, status); 143 if (U_FAILURE(status)) { 144 return Err(ToICUError(status)); 145 } 146 147 return rawOffset + dstOffset; 148 #else 149 UErrorCode status = U_ZERO_ERROR; 150 ucal_setMillis(mCalendar, date, &status); 151 if (U_FAILURE(status)) { 152 return Err(ToICUError(status)); 153 } 154 155 int32_t rawOffset = ucal_get(mCalendar, UCAL_ZONE_OFFSET, &status); 156 if (U_FAILURE(status)) { 157 return Err(ToICUError(status)); 158 } 159 160 int32_t dstOffset = ucal_get(mCalendar, UCAL_DST_OFFSET, &status); 161 if (U_FAILURE(status)) { 162 return Err(ToICUError(status)); 163 } 164 165 return rawOffset + dstOffset; 166 #endif 167 } 168 169 Result<int32_t, ICUError> TimeZone::GetUTCOffsetMs(int64_t aLocalMilliseconds) { 170 // https://tc39.es/ecma262/#sec-local-time-zone-adjustment 171 // 172 // LocalTZA ( t, isUTC ) 173 // 174 // When t_local represents local time repeating multiple times at a negative 175 // time zone transition (e.g. when the daylight saving time ends or the time 176 // zone offset is decreased due to a time zone rule change) or skipped local 177 // time at a positive time zone transitions (e.g. when the daylight saving 178 // time starts or the time zone offset is increased due to a time zone rule 179 // change), t_local must be interpreted using the time zone offset before the 180 // transition. 181 constexpr LocalOption skippedTime = LocalOption::Former; 182 constexpr LocalOption repeatedTime = LocalOption::Former; 183 184 return GetUTCOffsetMs(aLocalMilliseconds, skippedTime, repeatedTime); 185 } 186 187 static UTimeZoneLocalOption ToUTimeZoneLocalOption( 188 TimeZone::LocalOption aOption) { 189 switch (aOption) { 190 case TimeZone::LocalOption::Former: 191 return UTimeZoneLocalOption::UCAL_TZ_LOCAL_FORMER; 192 case TimeZone::LocalOption::Latter: 193 return UTimeZoneLocalOption::UCAL_TZ_LOCAL_LATTER; 194 } 195 MOZ_CRASH("Unexpected TimeZone::LocalOption"); 196 } 197 198 Result<int32_t, ICUError> TimeZone::GetUTCOffsetMs(int64_t aLocalMilliseconds, 199 LocalOption aSkippedTime, 200 LocalOption aRepeatedTime) { 201 UDate date = UDate(aLocalMilliseconds); 202 UTimeZoneLocalOption skippedTime = ToUTimeZoneLocalOption(aSkippedTime); 203 UTimeZoneLocalOption repeatedTime = ToUTimeZoneLocalOption(aRepeatedTime); 204 205 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 206 int32_t rawOffset, dstOffset; 207 UErrorCode status = U_ZERO_ERROR; 208 209 // All ICU TimeZone classes derive from BasicTimeZone, so we can safely 210 // perform the static_cast. 211 // Once <https://unicode-org.atlassian.net/browse/ICU-13705> is fixed we 212 // can remove this extra cast. 213 auto* basicTz = static_cast<icu::BasicTimeZone*>(mTimeZone.get()); 214 basicTz->getOffsetFromLocal(date, skippedTime, repeatedTime, rawOffset, 215 dstOffset, status); 216 if (U_FAILURE(status)) { 217 return Err(ToICUError(status)); 218 } 219 220 return rawOffset + dstOffset; 221 #else 222 UErrorCode status = U_ZERO_ERROR; 223 ucal_setMillis(mCalendar, date, &status); 224 if (U_FAILURE(status)) { 225 return Err(ToICUError(status)); 226 } 227 228 int32_t rawOffset, dstOffset; 229 ucal_getTimeZoneOffsetFromLocal(mCalendar, skippedTime, repeatedTime, 230 &rawOffset, &dstOffset, &status); 231 if (U_FAILURE(status)) { 232 return Err(ToICUError(status)); 233 } 234 235 return rawOffset + dstOffset; 236 #endif 237 } 238 239 Result<Maybe<int64_t>, ICUError> TimeZone::GetPreviousTransition( 240 int64_t aUTCMilliseconds) { 241 UDate date = UDate(aUTCMilliseconds); 242 243 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 244 // All ICU TimeZone classes derive from BasicTimeZone, so we can safely 245 // perform the static_cast. 246 auto* basicTz = static_cast<icu::BasicTimeZone*>(mTimeZone.get()); 247 248 constexpr bool inclusive = false; 249 icu::TimeZoneTransition transition; 250 if (!basicTz->getPreviousTransition(date, inclusive, transition)) { 251 return Maybe<int64_t>(); 252 } 253 return Some(int64_t(transition.getTime())); 254 #else 255 UDate transition = 0; 256 UErrorCode status = U_ZERO_ERROR; 257 bool found = ucal_getTimeZoneTransitionDate( 258 mCalendar, UCAL_TZ_TRANSITION_PREVIOUS, &transition, &status); 259 if (U_FAILURE(status)) { 260 return Err(ToICUError(status)); 261 } 262 if (!found) { 263 return Maybe<int64_t>(); 264 } 265 return Some(int64_t(transition)); 266 #endif 267 } 268 269 Result<Maybe<int64_t>, ICUError> TimeZone::GetNextTransition( 270 int64_t aUTCMilliseconds) { 271 UDate date = UDate(aUTCMilliseconds); 272 273 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 274 // All ICU TimeZone classes derive from BasicTimeZone, so we can safely 275 // perform the static_cast. 276 auto* basicTz = static_cast<icu::BasicTimeZone*>(mTimeZone.get()); 277 278 constexpr bool inclusive = false; 279 icu::TimeZoneTransition transition; 280 if (!basicTz->getNextTransition(date, inclusive, transition)) { 281 return Maybe<int64_t>(); 282 } 283 return Some(int64_t(transition.getTime())); 284 #else 285 UDate transition = 0; 286 UErrorCode status = U_ZERO_ERROR; 287 bool found = ucal_getTimeZoneTransitionDate( 288 mCalendar, UCAL_TZ_TRANSITION_NEXT, &transition, &status); 289 if (U_FAILURE(status)) { 290 return Err(ToICUError(status)); 291 } 292 if (!found) { 293 return Maybe<int64_t>(); 294 } 295 return Some(int64_t(transition)); 296 #endif 297 } 298 299 using TimeZoneIdentifierVector = 300 Vector<char16_t, TimeZone::TimeZoneIdentifierLength>; 301 302 #if !MOZ_INTL_USE_ICU_CPP_TIMEZONE 303 static bool IsUnknownTimeZone(const TimeZoneIdentifierVector& timeZone) { 304 constexpr std::string_view unknownTimeZone = UCAL_UNKNOWN_ZONE_ID; 305 306 return timeZone.length() == unknownTimeZone.length() && 307 std::equal(timeZone.begin(), timeZone.end(), unknownTimeZone.begin(), 308 unknownTimeZone.end()); 309 } 310 311 static ICUResult SetDefaultTimeZone(TimeZoneIdentifierVector& timeZone) { 312 // The string mustn't already be null-terminated. 313 MOZ_ASSERT_IF(!timeZone.empty(), timeZone.end()[-1] != '\0'); 314 315 // The time zone identifier must be a null-terminated string. 316 if (!timeZone.append('\0')) { 317 return Err(ICUError::OutOfMemory); 318 } 319 320 UErrorCode status = U_ZERO_ERROR; 321 ucal_setDefaultTimeZone(timeZone.begin(), &status); 322 if (U_FAILURE(status)) { 323 return Err(ToICUError(status)); 324 } 325 326 return Ok{}; 327 } 328 #endif 329 330 Result<bool, ICUError> TimeZone::SetDefaultTimeZone( 331 Span<const char> aTimeZone) { 332 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 333 icu::UnicodeString tzid(aTimeZone.data(), aTimeZone.size(), US_INV); 334 if (tzid.isBogus()) { 335 return Err(ICUError::OutOfMemory); 336 } 337 338 UniquePtr<icu::TimeZone> newTimeZone(icu::TimeZone::createTimeZone(tzid)); 339 MOZ_ASSERT(newTimeZone); 340 341 if (*newTimeZone != icu::TimeZone::getUnknown()) { 342 // adoptDefault() takes ownership of the time zone. 343 icu::TimeZone::adoptDefault(newTimeZone.release()); 344 return true; 345 } 346 #else 347 TimeZoneIdentifierVector tzid; 348 if (!tzid.append(aTimeZone.data(), aTimeZone.size())) { 349 return Err(ICUError::OutOfMemory); 350 } 351 352 // Retrieve the current default time zone in case we need to restore it. 353 TimeZoneIdentifierVector defaultTimeZone; 354 MOZ_TRY(FillBufferWithICUCall(defaultTimeZone, ucal_getDefaultTimeZone)); 355 356 // Try to set the new time zone. 357 MOZ_TRY(mozilla::intl::SetDefaultTimeZone(tzid)); 358 359 // Check if the time zone was actually applied. 360 TimeZoneIdentifierVector newTimeZone; 361 MOZ_TRY(FillBufferWithICUCall(newTimeZone, ucal_getDefaultTimeZone)); 362 363 // Return if the new time zone was successfully applied. 364 if (!IsUnknownTimeZone(newTimeZone)) { 365 return true; 366 } 367 368 // Otherwise restore the original time zone. 369 MOZ_TRY(mozilla::intl::SetDefaultTimeZone(defaultTimeZone)); 370 #endif 371 372 return false; 373 } 374 375 ICUResult TimeZone::SetDefaultTimeZoneFromHostTimeZone() { 376 #if MOZ_INTL_USE_ICU_CPP_TIMEZONE 377 if (icu::TimeZone* defaultZone = icu::TimeZone::detectHostTimeZone()) { 378 icu::TimeZone::adoptDefault(defaultZone); 379 } 380 #else 381 TimeZoneIdentifierVector hostTimeZone; 382 MOZ_TRY(FillBufferWithICUCall(hostTimeZone, ucal_getHostTimeZone)); 383 384 MOZ_TRY(mozilla::intl::SetDefaultTimeZone(hostTimeZone)); 385 #endif 386 387 return Ok{}; 388 } 389 390 Result<Span<const char>, ICUError> TimeZone::GetTZDataVersion() { 391 UErrorCode status = U_ZERO_ERROR; 392 const char* tzdataVersion = ucal_getTZDataVersion(&status); 393 if (U_FAILURE(status)) { 394 return Err(ToICUError(status)); 395 } 396 return MakeStringSpan(tzdataVersion); 397 } 398 399 Result<SpanEnumeration<char>, ICUError> TimeZone::GetAvailableTimeZones( 400 const char* aRegion) { 401 // Get the time zones that are commonly used in the given region. Uses the 402 // UCAL_ZONE_TYPE_ANY filter so we have more fine-grained control over the 403 // returned time zones and don't omit time zones which are considered links in 404 // ICU, but are treated as proper zones in IANA. 405 UErrorCode status = U_ZERO_ERROR; 406 UEnumeration* enumeration = ucal_openTimeZoneIDEnumeration( 407 UCAL_ZONE_TYPE_ANY, aRegion, nullptr, &status); 408 if (U_FAILURE(status)) { 409 return Err(ToICUError(status)); 410 } 411 412 return SpanEnumeration<char>(enumeration); 413 } 414 415 Result<SpanEnumeration<char>, ICUError> TimeZone::GetAvailableTimeZones() { 416 UErrorCode status = U_ZERO_ERROR; 417 UEnumeration* enumeration = ucal_openTimeZones(&status); 418 if (U_FAILURE(status)) { 419 return Err(ToICUError(status)); 420 } 421 422 return SpanEnumeration<char>(enumeration); 423 } 424 425 #if !MOZ_INTL_USE_ICU_CPP_TIMEZONE 426 TimeZone::~TimeZone() { 427 MOZ_ASSERT(mCalendar); 428 ucal_close(mCalendar); 429 } 430 #endif 431 432 } // namespace mozilla::intl