TimeZone.cpp (36708B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "builtin/temporal/TimeZone.h" 8 9 #include "mozilla/Assertions.h" 10 #include "mozilla/intl/TimeZone.h" 11 #include "mozilla/Likely.h" 12 #include "mozilla/Maybe.h" 13 #include "mozilla/Span.h" 14 #include "mozilla/UniquePtr.h" 15 16 #include <cmath> 17 #include <cstdlib> 18 #include <string_view> 19 #include <utility> 20 21 #include "jsdate.h" 22 #include "jstypes.h" 23 #include "NamespaceImports.h" 24 25 #include "builtin/intl/CommonFunctions.h" 26 #include "builtin/intl/FormatBuffer.h" 27 #include "builtin/intl/SharedIntlData.h" 28 #include "builtin/temporal/Calendar.h" 29 #include "builtin/temporal/Instant.h" 30 #include "builtin/temporal/PlainDate.h" 31 #include "builtin/temporal/PlainDateTime.h" 32 #include "builtin/temporal/PlainTime.h" 33 #include "builtin/temporal/Temporal.h" 34 #include "builtin/temporal/TemporalParser.h" 35 #include "builtin/temporal/TemporalTypes.h" 36 #include "builtin/temporal/TemporalUnit.h" 37 #include "builtin/temporal/ZonedDateTime.h" 38 #include "gc/Barrier.h" 39 #include "gc/GCContext.h" 40 #include "gc/GCEnum.h" 41 #include "gc/Tracer.h" 42 #include "js/AllocPolicy.h" 43 #include "js/Class.h" 44 #include "js/ErrorReport.h" 45 #include "js/friend/ErrorMessages.h" 46 #include "js/Printer.h" 47 #include "js/RootingAPI.h" 48 #include "js/StableStringChars.h" 49 #include "vm/BytecodeUtil.h" 50 #include "vm/Compartment.h" 51 #include "vm/DateTime.h" 52 #include "vm/JSAtomState.h" 53 #include "vm/JSContext.h" 54 #include "vm/JSObject.h" 55 #include "vm/Runtime.h" 56 #include "vm/StringType.h" 57 58 #include "vm/JSObject-inl.h" 59 60 using namespace js; 61 using namespace js::temporal; 62 63 void js::temporal::TimeZoneValue::trace(JSTracer* trc) { 64 TraceNullableRoot(trc, &object_, "TimeZoneValue::object"); 65 } 66 67 /** 68 * FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] ) 69 */ 70 static JSLinearString* FormatOffsetTimeZoneIdentifier(JSContext* cx, 71 int32_t offsetMinutes) { 72 MOZ_ASSERT(std::abs(offsetMinutes) < UnitsPerDay(TemporalUnit::Minute)); 73 74 // Step 1. 75 char sign = offsetMinutes >= 0 ? '+' : '-'; 76 77 // Step 2. 78 int32_t absoluteMinutes = std::abs(offsetMinutes); 79 80 // Step 3. 81 int32_t hour = absoluteMinutes / 60; 82 83 // Step 4. 84 int32_t minute = absoluteMinutes % 60; 85 86 // Step 5. (Inlined FormatTimeString). 87 // 88 // Format: "sign hour{2} : minute{2}" 89 char result[] = { 90 sign, char('0' + (hour / 10)), char('0' + (hour % 10)), 91 ':', char('0' + (minute / 10)), char('0' + (minute % 10)), 92 }; 93 94 // Step 6. 95 return NewStringCopyN<CanGC>(cx, result, std::size(result)); 96 } 97 98 TimeZoneObject* js::temporal::CreateTimeZoneObject( 99 JSContext* cx, Handle<JSLinearString*> identifier, 100 Handle<JSLinearString*> primaryIdentifier) { 101 auto* object = NewObjectWithGivenProto<TimeZoneObject>(cx, nullptr); 102 if (!object) { 103 return nullptr; 104 } 105 106 object->initFixedSlot(TimeZoneObject::IDENTIFIER_SLOT, 107 StringValue(identifier)); 108 109 object->initFixedSlot(TimeZoneObject::PRIMARY_IDENTIFIER_SLOT, 110 StringValue(primaryIdentifier)); 111 112 object->initFixedSlot(TimeZoneObject::OFFSET_MINUTES_SLOT, UndefinedValue()); 113 114 return object; 115 } 116 117 static TimeZoneObject* GetOrCreateTimeZoneObject( 118 JSContext* cx, Handle<JSLinearString*> identifier, 119 Handle<JSLinearString*> primaryIdentifier) { 120 return cx->global()->globalIntlData().getOrCreateTimeZone(cx, identifier, 121 primaryIdentifier); 122 } 123 124 static TimeZoneObject* CreateTimeZoneObject(JSContext* cx, 125 int32_t offsetMinutes) { 126 // TODO: It's unclear if offset time zones should also be cached. Real world 127 // experience will tell if a cache should be added. 128 129 MOZ_ASSERT(std::abs(offsetMinutes) < UnitsPerDay(TemporalUnit::Minute)); 130 131 Rooted<JSLinearString*> identifier( 132 cx, FormatOffsetTimeZoneIdentifier(cx, offsetMinutes)); 133 if (!identifier) { 134 return nullptr; 135 } 136 137 auto* object = NewObjectWithGivenProto<TimeZoneObject>(cx, nullptr); 138 if (!object) { 139 return nullptr; 140 } 141 142 object->initFixedSlot(TimeZoneObject::IDENTIFIER_SLOT, 143 StringValue(identifier)); 144 145 object->initFixedSlot(TimeZoneObject::PRIMARY_IDENTIFIER_SLOT, 146 UndefinedValue()); 147 148 object->initFixedSlot(TimeZoneObject::OFFSET_MINUTES_SLOT, 149 Int32Value(offsetMinutes)); 150 151 return object; 152 } 153 154 static mozilla::UniquePtr<mozilla::intl::TimeZone> CreateIntlTimeZone( 155 JSContext* cx, JSLinearString* identifier) { 156 MOZ_ASSERT(StringIsAscii(identifier)); 157 158 Vector<char, mozilla::intl::TimeZone::TimeZoneIdentifierLength> chars(cx); 159 if (!chars.resize(identifier->length())) { 160 return nullptr; 161 } 162 163 js::CopyChars(reinterpret_cast<JS::Latin1Char*>(chars.begin()), *identifier); 164 165 auto result = mozilla::intl::TimeZone::TryCreate( 166 mozilla::Some(static_cast<mozilla::Span<const char>>(chars))); 167 if (result.isErr()) { 168 intl::ReportInternalError(cx, result.unwrapErr()); 169 return nullptr; 170 } 171 return result.unwrap(); 172 } 173 174 static mozilla::intl::TimeZone* GetOrCreateIntlTimeZone( 175 JSContext* cx, Handle<TimeZoneValue> timeZone) { 176 MOZ_ASSERT(!timeZone.isOffset()); 177 178 // Obtain a cached mozilla::intl::TimeZone object. 179 if (auto* tz = timeZone.getTimeZone()) { 180 return tz; 181 } 182 183 auto* tz = CreateIntlTimeZone(cx, timeZone.primaryIdentifier()).release(); 184 if (!tz) { 185 return nullptr; 186 } 187 188 auto* obj = timeZone.get().toTimeZoneObject(); 189 obj->setTimeZone(tz); 190 191 intl::AddICUCellMemory(obj, TimeZoneObject::EstimatedMemoryUse); 192 return tz; 193 } 194 195 /** 196 * IsValidTimeZoneName ( timeZone ) 197 * IsAvailableTimeZoneName ( timeZone ) 198 * CanonicalizeTimeZoneName ( timeZone ) 199 */ 200 static bool ValidateAndCanonicalizeTimeZoneName( 201 JSContext* cx, Handle<JSLinearString*> timeZone, 202 MutableHandle<JSLinearString*> identifier, 203 MutableHandle<JSLinearString*> primaryIdentifier) { 204 Rooted<JSAtom*> availableTimeZone(cx); 205 Rooted<JSAtom*> primaryTimeZone(cx); 206 intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); 207 if (!sharedIntlData.validateAndCanonicalizeTimeZone( 208 cx, timeZone, &availableTimeZone, &primaryTimeZone)) { 209 return false; 210 } 211 212 if (!primaryTimeZone) { 213 if (auto chars = QuoteString(cx, timeZone)) { 214 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 215 JSMSG_TEMPORAL_TIMEZONE_INVALID_IDENTIFIER, 216 chars.get()); 217 } 218 return false; 219 } 220 MOZ_ASSERT(availableTimeZone); 221 222 // Links to UTC are handled by SharedIntlData. 223 MOZ_ASSERT(!StringEqualsLiteral(primaryTimeZone, "Etc/UTC")); 224 MOZ_ASSERT(!StringEqualsLiteral(primaryTimeZone, "Etc/GMT")); 225 226 // We don't need to check against "GMT", because ICU uses the tzdata rearguard 227 // format, where "GMT" is a link to "Etc/GMT". 228 MOZ_ASSERT(!StringEqualsLiteral(primaryTimeZone, "GMT")); 229 230 identifier.set(availableTimeZone); 231 primaryIdentifier.set(primaryTimeZone); 232 return true; 233 } 234 235 static bool SystemTimeZoneOffset(JSContext* cx, int32_t* offset) { 236 auto rawOffset = DateTimeInfo::getRawOffsetMs(cx->realm()->getDateTimeInfo()); 237 if (rawOffset.isErr()) { 238 intl::ReportInternalError(cx); 239 return false; 240 } 241 242 *offset = rawOffset.unwrap(); 243 return true; 244 } 245 246 /** 247 * SystemTimeZoneIdentifier ( ) 248 * 249 * Returns the IANA time zone name for the host environment's current time zone. 250 */ 251 JSLinearString* js::temporal::ComputeSystemTimeZoneIdentifier(JSContext* cx) { 252 TimeZoneIdentifierVector timeZoneId; 253 if (!DateTimeInfo::timeZoneId(cx->realm()->getDateTimeInfo(), timeZoneId)) { 254 ReportOutOfMemory(cx); 255 return nullptr; 256 } 257 258 Rooted<JSAtom*> availableTimeZone(cx); 259 Rooted<JSAtom*> primaryTimeZone(cx); 260 intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); 261 if (!sharedIntlData.validateAndCanonicalizeTimeZone( 262 cx, static_cast<mozilla::Span<const char>>(timeZoneId), 263 &availableTimeZone, &primaryTimeZone)) { 264 return nullptr; 265 } 266 if (primaryTimeZone) { 267 return primaryTimeZone; 268 } 269 270 // Before defaulting to "UTC", try to represent the system time zone using 271 // the Etc/GMT + offset format. This format only accepts full hour offsets. 272 int32_t offset; 273 if (!SystemTimeZoneOffset(cx, &offset)) { 274 return nullptr; 275 } 276 277 constexpr int32_t msPerHour = 60 * 60 * 1000; 278 int32_t offsetHours = std::abs(offset / msPerHour); 279 int32_t offsetHoursFraction = offset % msPerHour; 280 if (offsetHoursFraction == 0 && offsetHours < 24) { 281 // Etc/GMT + offset uses POSIX-style signs, i.e. a positive offset 282 // means a location west of GMT. 283 constexpr std::string_view etcGMT = "Etc/GMT"; 284 285 char offsetString[etcGMT.length() + 3]; 286 287 size_t n = etcGMT.copy(offsetString, etcGMT.length()); 288 offsetString[n++] = offset < 0 ? '+' : '-'; 289 if (offsetHours >= 10) { 290 offsetString[n++] = char('0' + (offsetHours / 10)); 291 } 292 offsetString[n++] = char('0' + (offsetHours % 10)); 293 294 MOZ_ASSERT(n == etcGMT.length() + 2 || n == etcGMT.length() + 3); 295 296 // Check if the fallback is valid. 297 if (!sharedIntlData.validateAndCanonicalizeTimeZone( 298 cx, mozilla::Span<const char>{offsetString, n}, &availableTimeZone, 299 &primaryTimeZone)) { 300 return nullptr; 301 } 302 if (primaryTimeZone) { 303 return primaryTimeZone; 304 } 305 } 306 307 // Fallback to "UTC" if everything else fails. 308 return cx->names().UTC; 309 } 310 311 /** 312 * SystemTimeZoneIdentifier ( ) 313 * 314 * Returns the IANA time zone name for the host environment's current time zone. 315 */ 316 JSLinearString* js::temporal::SystemTimeZoneIdentifier(JSContext* cx) { 317 return cx->global()->globalIntlData().defaultTimeZone(cx); 318 } 319 320 /** 321 * SystemTimeZoneIdentifier ( ) 322 */ 323 bool js::temporal::SystemTimeZone(JSContext* cx, 324 MutableHandle<TimeZoneValue> result) { 325 auto* timeZone = 326 cx->global()->globalIntlData().getOrCreateDefaultTimeZone(cx); 327 if (!timeZone) { 328 return false; 329 } 330 331 result.set(TimeZoneValue(timeZone)); 332 return true; 333 } 334 335 /** 336 * GetNamedTimeZoneEpochNanoseconds ( timeZoneIdentifier, isoDateTime ) 337 */ 338 static bool GetNamedTimeZoneEpochNanoseconds(JSContext* cx, 339 Handle<TimeZoneValue> timeZone, 340 const ISODateTime& isoDateTime, 341 PossibleEpochNanoseconds* result) { 342 MOZ_ASSERT(!timeZone.isOffset()); 343 MOZ_ASSERT(IsValidISODateTime(isoDateTime)); 344 MOZ_ASSERT(ISODateTimeWithinLimits(isoDateTime)); 345 346 // FIXME: spec issue - assert ISODateTimeWithinLimits instead of 347 // IsValidISODate 348 349 int64_t ms = MakeDate(isoDateTime); 350 351 auto* tz = GetOrCreateIntlTimeZone(cx, timeZone); 352 if (!tz) { 353 return false; 354 } 355 356 auto getOffset = [&](mozilla::intl::TimeZone::LocalOption skippedTime, 357 mozilla::intl::TimeZone::LocalOption repeatedTime, 358 int32_t* offset) { 359 auto result = tz->GetUTCOffsetMs(ms, skippedTime, repeatedTime); 360 if (result.isErr()) { 361 intl::ReportInternalError(cx, result.unwrapErr()); 362 return false; 363 } 364 365 *offset = result.unwrap(); 366 MOZ_ASSERT(std::abs(*offset) < UnitsPerDay(TemporalUnit::Millisecond)); 367 368 return true; 369 }; 370 371 constexpr auto formerTime = mozilla::intl::TimeZone::LocalOption::Former; 372 constexpr auto latterTime = mozilla::intl::TimeZone::LocalOption::Latter; 373 374 int32_t formerOffset; 375 if (!getOffset(formerTime, formerTime, &formerOffset)) { 376 return false; 377 } 378 379 int32_t latterOffset; 380 if (!getOffset(latterTime, latterTime, &latterOffset)) { 381 return false; 382 } 383 384 if (formerOffset == latterOffset) { 385 auto epochNs = GetUTCEpochNanoseconds(isoDateTime) - 386 EpochDuration::fromMilliseconds(formerOffset); 387 *result = PossibleEpochNanoseconds{epochNs}; 388 return true; 389 } 390 391 int32_t disambiguationOffset; 392 if (!getOffset(formerTime, latterTime, &disambiguationOffset)) { 393 return false; 394 } 395 396 // Skipped time. 397 if (disambiguationOffset == formerOffset) { 398 *result = {}; 399 return true; 400 } 401 402 // Repeated time. 403 auto formerInstant = GetUTCEpochNanoseconds(isoDateTime) - 404 EpochDuration::fromMilliseconds(formerOffset); 405 auto latterInstant = GetUTCEpochNanoseconds(isoDateTime) - 406 EpochDuration::fromMilliseconds(latterOffset); 407 408 // Ensure the returned epoch nanoseconds are sorted in numerical order. 409 if (formerInstant > latterInstant) { 410 std::swap(formerInstant, latterInstant); 411 } 412 413 *result = PossibleEpochNanoseconds{formerInstant, latterInstant}; 414 return true; 415 } 416 417 /** 418 * GetNamedTimeZoneOffsetNanoseconds ( timeZoneIdentifier, epochNanoseconds ) 419 */ 420 static bool GetNamedTimeZoneOffsetNanoseconds( 421 JSContext* cx, Handle<TimeZoneValue> timeZone, 422 const EpochNanoseconds& epochNanoseconds, int64_t* offset) { 423 MOZ_ASSERT(!timeZone.isOffset()); 424 425 // Round down (floor) to the previous full milliseconds. 426 int64_t millis = epochNanoseconds.floorToMilliseconds(); 427 428 auto* tz = GetOrCreateIntlTimeZone(cx, timeZone); 429 if (!tz) { 430 return false; 431 } 432 433 auto result = tz->GetOffsetMs(millis); 434 if (result.isErr()) { 435 intl::ReportInternalError(cx, result.unwrapErr()); 436 return false; 437 } 438 439 // FIXME: spec issue - should constrain the range to not exceed 24-hours. 440 // https://github.com/tc39/ecma262/issues/3101 441 442 int64_t nanoPerMs = 1'000'000; 443 *offset = result.unwrap() * nanoPerMs; 444 return true; 445 } 446 447 /** 448 * Check if the time zone offset at UTC time |utcMilliseconds1| is the same as 449 * the time zone offset at UTC time |utcMilliseconds2|. 450 */ 451 static bool EqualTimeZoneOffset(JSContext* cx, 452 mozilla::intl::TimeZone* timeZone, 453 int64_t utcMilliseconds1, 454 int64_t utcMilliseconds2, bool* result) { 455 auto offset1 = timeZone->GetOffsetMs(utcMilliseconds1); 456 if (offset1.isErr()) { 457 intl::ReportInternalError(cx, offset1.unwrapErr()); 458 return false; 459 } 460 461 auto offset2 = timeZone->GetOffsetMs(utcMilliseconds2); 462 if (offset2.isErr()) { 463 intl::ReportInternalError(cx, offset2.unwrapErr()); 464 return false; 465 } 466 467 *result = offset1.unwrap() == offset2.unwrap(); 468 return true; 469 } 470 471 /** 472 * GetNamedTimeZoneNextTransition ( timeZoneIdentifier, epochNanoseconds ) 473 */ 474 bool js::temporal::GetNamedTimeZoneNextTransition( 475 JSContext* cx, Handle<TimeZoneValue> timeZone, 476 const EpochNanoseconds& epochNanoseconds, 477 mozilla::Maybe<EpochNanoseconds>* result) { 478 MOZ_ASSERT(!timeZone.isOffset()); 479 480 // Round down (floor) to the previous full millisecond. 481 // 482 // IANA has experimental support for transitions at sub-second precision, but 483 // the default configuration doesn't enable it, therefore it's safe to round 484 // to milliseconds here. In addition to that, ICU also only supports 485 // transitions at millisecond precision. 486 int64_t millis = epochNanoseconds.floorToMilliseconds(); 487 488 auto* tz = GetOrCreateIntlTimeZone(cx, timeZone); 489 if (!tz) { 490 return false; 491 } 492 493 // Skip over transitions which don't change the time zone offset. 494 // 495 // ICU4C returns all time zone rule changes as transitions, even if the 496 // actual time zone offset didn't change. Temporal requires to ignore these 497 // rule changes and instead only return transitions if the time zone offset 498 // did change. 499 while (true) { 500 auto next = tz->GetNextTransition(millis); 501 if (next.isErr()) { 502 intl::ReportInternalError(cx, next.unwrapErr()); 503 return false; 504 } 505 506 // If there's no next transition, we're done. 507 auto transition = next.unwrap(); 508 if (!transition) { 509 *result = mozilla::Nothing(); 510 return true; 511 } 512 513 // Check if the time offset at the next transition is equal to the current 514 // time zone offset. 515 bool equalOffset; 516 if (!EqualTimeZoneOffset(cx, tz, millis, *transition, &equalOffset)) { 517 return false; 518 } 519 520 // If the time zone offset is equal, then search for the next transition 521 // after |transition|. 522 if (equalOffset) { 523 millis = *transition; 524 continue; 525 } 526 527 // Otherwise return |transition| as the next transition. 528 auto transitionInstant = EpochNanoseconds::fromMilliseconds(*transition); 529 if (!IsValidEpochNanoseconds(transitionInstant)) { 530 *result = mozilla::Nothing(); 531 return true; 532 } 533 534 *result = mozilla::Some(transitionInstant); 535 return true; 536 } 537 } 538 539 /** 540 * GetNamedTimeZonePreviousTransition ( timeZoneIdentifier, epochNanoseconds ) 541 */ 542 bool js::temporal::GetNamedTimeZonePreviousTransition( 543 JSContext* cx, Handle<TimeZoneValue> timeZone, 544 const EpochNanoseconds& epochNanoseconds, 545 mozilla::Maybe<EpochNanoseconds>* result) { 546 MOZ_ASSERT(!timeZone.isOffset()); 547 548 // Round up (ceil) to the next full millisecond. 549 // 550 // IANA has experimental support for transitions at sub-second precision, but 551 // the default configuration doesn't enable it, therefore it's safe to round 552 // to milliseconds here. In addition to that, ICU also only supports 553 // transitions at millisecond precision. 554 int64_t millis = epochNanoseconds.ceilToMilliseconds(); 555 556 auto* tz = GetOrCreateIntlTimeZone(cx, timeZone); 557 if (!tz) { 558 return false; 559 } 560 561 auto previous = tz->GetPreviousTransition(millis); 562 if (previous.isErr()) { 563 intl::ReportInternalError(cx, previous.unwrapErr()); 564 return false; 565 } 566 567 // If there's no previous transition, we're done. 568 auto transition = previous.unwrap(); 569 if (!transition) { 570 *result = mozilla::Nothing(); 571 return true; 572 } 573 574 // Skip over transitions which don't change the time zone offset. 575 // 576 // ICU4C returns all time zone rule changes as transitions, even if the 577 // actual time zone offset didn't change. Temporal requires to ignore these 578 // rule changes and instead only return transitions if the time zone offset 579 // did change. 580 while (true) { 581 // Request the transition before |transition|. 582 auto beforePrevious = tz->GetPreviousTransition(*transition); 583 if (beforePrevious.isErr()) { 584 intl::ReportInternalError(cx, beforePrevious.unwrapErr()); 585 return false; 586 } 587 588 // If there's no before transition, stop searching. 589 auto beforePreviousTransition = beforePrevious.unwrap(); 590 if (!beforePreviousTransition) { 591 break; 592 } 593 594 // Check if the time zone offset at both transition points is equal. 595 bool equalOffset; 596 if (!EqualTimeZoneOffset(cx, tz, *transition, *beforePreviousTransition, 597 &equalOffset)) { 598 return false; 599 } 600 601 // If time zone offset is not equal, then return |transition|. 602 if (!equalOffset) { 603 break; 604 } 605 606 // Otherwise continue searching from |beforePreviousTransition|. 607 transition = beforePreviousTransition; 608 } 609 610 auto transitionInstant = EpochNanoseconds::fromMilliseconds(*transition); 611 if (!IsValidEpochNanoseconds(transitionInstant)) { 612 *result = mozilla::Nothing(); 613 return true; 614 } 615 616 *result = mozilla::Some(transitionInstant); 617 return true; 618 } 619 620 /** 621 * GetStartOfDay ( timeZone, isoDate ) 622 */ 623 bool js::temporal::GetStartOfDay(JSContext* cx, Handle<TimeZoneValue> timeZone, 624 const ISODate& isoDate, 625 EpochNanoseconds* result) { 626 MOZ_ASSERT(IsValidISODate(isoDate)); 627 628 // Step 1. 629 auto isoDateTime = ISODateTime{isoDate, {}}; 630 631 // Step 2. 632 PossibleEpochNanoseconds possibleEpochNs; 633 if (!GetPossibleEpochNanoseconds(cx, timeZone, isoDateTime, 634 &possibleEpochNs)) { 635 return false; 636 } 637 MOZ_ASSERT(ISODateTimeWithinLimits(isoDateTime)); 638 639 // Step 3. 640 if (!possibleEpochNs.empty()) { 641 *result = possibleEpochNs[0]; 642 return true; 643 } 644 645 // Step 4. 646 MOZ_ASSERT(!timeZone.isOffset()); 647 648 constexpr auto oneDay = EpochDuration::fromDays(1); 649 650 // Step 5. 651 auto previousDayEpochNs = GetUTCEpochNanoseconds(isoDateTime) - oneDay; 652 mozilla::Maybe<EpochNanoseconds> transition{}; 653 if (!GetNamedTimeZoneNextTransition(cx, timeZone, previousDayEpochNs, 654 &transition)) { 655 return false; 656 } 657 658 // Step 6. 659 MOZ_ASSERT(transition, "time zone transition not found"); 660 661 // Step 7. 662 *result = *transition; 663 return true; 664 } 665 666 /** 667 * ToTemporalTimeZoneIdentifier ( temporalTimeZoneLike ) 668 */ 669 bool js::temporal::ToTemporalTimeZone(JSContext* cx, 670 Handle<ParsedTimeZone> string, 671 MutableHandle<TimeZoneValue> result) { 672 // Steps 1-3. (Not applicable) 673 674 // Steps 4-5. 675 if (!string.name()) { 676 auto* obj = CreateTimeZoneObject(cx, string.offset()); 677 if (!obj) { 678 return false; 679 } 680 681 result.set(TimeZoneValue(obj)); 682 return true; 683 } 684 685 // Steps 6-8. 686 Rooted<JSLinearString*> identifier(cx); 687 Rooted<JSLinearString*> primaryIdentifier(cx); 688 if (!ValidateAndCanonicalizeTimeZoneName(cx, string.name(), &identifier, 689 &primaryIdentifier)) { 690 return false; 691 } 692 693 // Step 9. 694 auto* obj = GetOrCreateTimeZoneObject(cx, identifier, primaryIdentifier); 695 if (!obj) { 696 return false; 697 } 698 699 result.set(TimeZoneValue(obj)); 700 return true; 701 } 702 703 /** 704 * ToTemporalTimeZoneIdentifier ( temporalTimeZoneLike ) 705 */ 706 bool js::temporal::ToTemporalTimeZone(JSContext* cx, 707 Handle<Value> temporalTimeZoneLike, 708 MutableHandle<TimeZoneValue> result) { 709 // Step 1. 710 if (temporalTimeZoneLike.isObject()) { 711 JSObject* obj = &temporalTimeZoneLike.toObject(); 712 713 // Step 1.a. 714 if (auto* zonedDateTime = obj->maybeUnwrapIf<ZonedDateTimeObject>()) { 715 result.set(zonedDateTime->timeZone()); 716 return result.wrap(cx); 717 } 718 } 719 720 // Step 2. 721 if (!temporalTimeZoneLike.isString()) { 722 ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, 723 temporalTimeZoneLike, nullptr, "not a string"); 724 return false; 725 } 726 Rooted<JSString*> identifier(cx, temporalTimeZoneLike.toString()); 727 728 // Step 3. 729 Rooted<ParsedTimeZone> timeZoneName(cx); 730 if (!ParseTemporalTimeZoneString(cx, identifier, &timeZoneName)) { 731 return false; 732 } 733 734 // Steps 4-9. 735 return ToTemporalTimeZone(cx, timeZoneName, result); 736 } 737 738 JSLinearString* js::temporal::ToValidCanonicalTimeZoneIdentifier( 739 JSContext* cx, Handle<JSString*> timeZone) { 740 Rooted<ParsedTimeZone> parsedTimeZone(cx); 741 if (!ParseTimeZoneIdentifier(cx, timeZone, &parsedTimeZone)) { 742 // TODO: Test262 expects the time zone string is part of the error message, 743 // so we have to overwrite the error message. 744 // 745 // https://github.com/tc39/test262/pull/4463 746 if (!cx->isExceptionPending() || cx->isThrowingOutOfMemory()) { 747 return nullptr; 748 } 749 750 // Clear the previous exception to ensure the error stack is recomputed. 751 cx->clearPendingException(); 752 753 if (auto chars = QuoteString(cx, timeZone)) { 754 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 755 JSMSG_TEMPORAL_TIMEZONE_INVALID_IDENTIFIER, 756 chars.get()); 757 } 758 return nullptr; 759 } 760 761 auto timeZoneId = parsedTimeZone.name(); 762 if (timeZoneId) { 763 Rooted<JSLinearString*> identifier(cx); 764 Rooted<JSLinearString*> primaryIdentifier(cx); 765 if (!ValidateAndCanonicalizeTimeZoneName(cx, timeZoneId, &identifier, 766 &primaryIdentifier)) { 767 return nullptr; 768 } 769 return primaryIdentifier; 770 } 771 772 int32_t offsetMinutes = parsedTimeZone.offset(); 773 MOZ_ASSERT(std::abs(offsetMinutes) < UnitsPerDay(TemporalUnit::Minute)); 774 775 return FormatOffsetTimeZoneIdentifier(cx, offsetMinutes); 776 } 777 778 /** 779 * GetOffsetNanosecondsFor ( timeZone, epochNs ) 780 */ 781 bool js::temporal::GetOffsetNanosecondsFor(JSContext* cx, 782 Handle<TimeZoneValue> timeZone, 783 const EpochNanoseconds& epochNs, 784 int64_t* offsetNanoseconds) { 785 // Step 1. (Not applicable) 786 787 // Step 2. 788 if (timeZone.isOffset()) { 789 int32_t offset = timeZone.offsetMinutes(); 790 MOZ_ASSERT(std::abs(offset) < UnitsPerDay(TemporalUnit::Minute)); 791 792 *offsetNanoseconds = int64_t(offset) * ToNanoseconds(TemporalUnit::Minute); 793 return true; 794 } 795 796 // Step 3. 797 int64_t offset; 798 if (!GetNamedTimeZoneOffsetNanoseconds(cx, timeZone, epochNs, &offset)) { 799 return false; 800 } 801 MOZ_ASSERT(std::abs(offset) < ToNanoseconds(TemporalUnit::Day)); 802 803 *offsetNanoseconds = offset; 804 return true; 805 } 806 807 /** 808 * TimeZoneEquals ( one, two ) 809 */ 810 bool js::temporal::TimeZoneEquals(const TimeZoneValue& one, 811 const TimeZoneValue& two) { 812 // Steps 1-3. (Not applicable in our implementation.) 813 814 // Step 4. 815 if (!one.isOffset() && !two.isOffset()) { 816 return EqualStrings(one.primaryIdentifier(), two.primaryIdentifier()); 817 } 818 819 // Step 5. 820 if (one.isOffset() && two.isOffset()) { 821 return one.offsetMinutes() == two.offsetMinutes(); 822 } 823 824 // Step 6. 825 return false; 826 } 827 828 /** 829 * GetISOPartsFromEpoch ( epochNanoseconds ) 830 */ 831 static ISODateTime GetISOPartsFromEpoch( 832 const EpochNanoseconds& epochNanoseconds, int64_t offsetNanoseconds) { 833 // Step 1. 834 MOZ_ASSERT(IsValidEpochNanoseconds(epochNanoseconds)); 835 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); 836 837 auto totalNanoseconds = 838 epochNanoseconds + EpochDuration::fromNanoseconds(offsetNanoseconds); 839 840 // Step 2. 841 int32_t remainderNs = totalNanoseconds.nanoseconds % 1'000'000; 842 843 // Step 10. (Reordered) 844 // 845 // Reordered so the compiler can merge the divisons in steps 2, 3, and 10. 846 int32_t millisecond = totalNanoseconds.nanoseconds / 1'000'000; 847 848 // Step 3. 849 int64_t epochMilliseconds = totalNanoseconds.floorToMilliseconds(); 850 851 // Steps 4-6. 852 auto [year, month, day] = ToYearMonthDay(epochMilliseconds); 853 854 // Steps 7-9. 855 auto [hour, minute, second] = ToHourMinuteSecond(epochMilliseconds); 856 857 // Step 10. (Moved above) 858 859 // Steps 11-12. 860 int32_t microsecond = remainderNs / 1000; 861 862 // Step 13. 863 int32_t nanosecond = remainderNs % 1000; 864 865 // Step 14. 866 auto isoDate = ISODate{year, month + 1, day}; 867 MOZ_ASSERT(IsValidISODate(isoDate)); 868 869 // Step 15. 870 auto time = Time{hour, minute, second, millisecond, microsecond, nanosecond}; 871 MOZ_ASSERT(IsValidTime(time)); 872 873 // Step 16. 874 auto result = ISODateTime{isoDate, time}; 875 876 // Always within date-time limits when the epoch nanoseconds are within limit. 877 MOZ_ASSERT(ISODateTimeWithinLimits(result)); 878 879 return result; 880 } 881 882 /** 883 * GetISODateTimeFor ( timeZone, epochNs ) 884 */ 885 ISODateTime js::temporal::GetISODateTimeFor(const EpochNanoseconds& epochNs, 886 int64_t offsetNanoseconds) { 887 MOZ_ASSERT(IsValidEpochNanoseconds(epochNs)); 888 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); 889 890 // Step 1. (Not applicable) 891 892 // Steps 2-3. 893 return GetISOPartsFromEpoch(epochNs, offsetNanoseconds); 894 } 895 896 /** 897 * GetISODateTimeFor ( timeZone, epochNs ) 898 */ 899 bool js::temporal::GetISODateTimeFor(JSContext* cx, 900 Handle<TimeZoneValue> timeZone, 901 const EpochNanoseconds& epochNs, 902 ISODateTime* result) { 903 MOZ_ASSERT(IsValidEpochNanoseconds(epochNs)); 904 905 // Step 1. 906 int64_t offsetNanoseconds; 907 if (!GetOffsetNanosecondsFor(cx, timeZone, epochNs, &offsetNanoseconds)) { 908 return false; 909 } 910 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); 911 912 // Steps 2-3. 913 *result = GetISODateTimeFor(epochNs, offsetNanoseconds); 914 return true; 915 } 916 917 /** 918 * GetPossibleEpochNanoseconds ( timeZone, isoDateTime ) 919 */ 920 bool js::temporal::GetPossibleEpochNanoseconds( 921 JSContext* cx, Handle<TimeZoneValue> timeZone, 922 const ISODateTime& isoDateTime, PossibleEpochNanoseconds* result) { 923 // TODO: https://github.com/tc39/proposal-temporal/pull/3014 924 if (!ISODateTimeWithinLimits(isoDateTime)) { 925 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 926 JSMSG_TEMPORAL_PLAIN_DATE_TIME_INVALID); 927 return false; 928 } 929 930 // Step 1. (Not applicable) 931 932 // Step 2. 933 PossibleEpochNanoseconds possibleEpochNanoseconds; 934 if (timeZone.isOffset()) { 935 int32_t offsetMin = timeZone.offsetMinutes(); 936 MOZ_ASSERT(std::abs(offsetMin) < UnitsPerDay(TemporalUnit::Minute)); 937 938 // Step 2.a. 939 auto epochInstant = GetUTCEpochNanoseconds(isoDateTime) - 940 EpochDuration::fromMinutes(offsetMin); 941 942 // Step 2.b. 943 possibleEpochNanoseconds = PossibleEpochNanoseconds{epochInstant}; 944 } else { 945 // Step 3. 946 if (!GetNamedTimeZoneEpochNanoseconds(cx, timeZone, isoDateTime, 947 &possibleEpochNanoseconds)) { 948 return false; 949 } 950 } 951 952 MOZ_ASSERT(possibleEpochNanoseconds.length() <= 2); 953 954 // Step 4. 955 for (const auto& epochInstant : possibleEpochNanoseconds) { 956 if (!IsValidEpochNanoseconds(epochInstant)) { 957 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 958 JSMSG_TEMPORAL_INSTANT_INVALID); 959 return false; 960 } 961 } 962 963 // Step 5. 964 *result = possibleEpochNanoseconds; 965 return true; 966 } 967 968 /** 969 * AddTime ( time, timeDuration ) 970 */ 971 static auto AddTime(const Time& time, int64_t nanoseconds) { 972 MOZ_ASSERT(IsValidTime(time)); 973 MOZ_ASSERT(std::abs(nanoseconds) <= ToNanoseconds(TemporalUnit::Day)); 974 975 // Steps 1-2. 976 return BalanceTime(time, nanoseconds); 977 } 978 979 /** 980 * DisambiguatePossibleEpochNanoseconds ( possibleEpochNs, timeZone, 981 * isoDateTime, disambiguation ) 982 */ 983 bool js::temporal::DisambiguatePossibleEpochNanoseconds( 984 JSContext* cx, const PossibleEpochNanoseconds& possibleEpochNs, 985 Handle<TimeZoneValue> timeZone, const ISODateTime& isoDateTime, 986 TemporalDisambiguation disambiguation, EpochNanoseconds* result) { 987 MOZ_ASSERT(IsValidISODateTime(isoDateTime)); 988 989 // Steps 1-2. 990 if (possibleEpochNs.length() == 1) { 991 *result = possibleEpochNs.front(); 992 return true; 993 } 994 995 // Steps 3-4. 996 if (!possibleEpochNs.empty()) { 997 // Step 3.a. 998 if (disambiguation == TemporalDisambiguation::Earlier || 999 disambiguation == TemporalDisambiguation::Compatible) { 1000 *result = possibleEpochNs.front(); 1001 return true; 1002 } 1003 1004 // Step 3.b. 1005 if (disambiguation == TemporalDisambiguation::Later) { 1006 *result = possibleEpochNs.back(); 1007 return true; 1008 } 1009 1010 // Step 3.c. 1011 MOZ_ASSERT(disambiguation == TemporalDisambiguation::Reject); 1012 1013 // Step 3.d. 1014 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 1015 JSMSG_TEMPORAL_TIMEZONE_INSTANT_AMBIGUOUS); 1016 return false; 1017 } 1018 1019 // Step 5. 1020 if (disambiguation == TemporalDisambiguation::Reject) { 1021 JS_ReportErrorNumberASCII( 1022 cx, GetErrorMessage, nullptr, 1023 JSMSG_TEMPORAL_TIMEZONE_INSTANT_AMBIGUOUS_DATE_SKIPPED); 1024 return false; 1025 } 1026 1027 constexpr auto oneDay = EpochDuration::fromDays(1); 1028 1029 auto epochNanoseconds = GetUTCEpochNanoseconds(isoDateTime); 1030 1031 // Step 6 and 8-9. 1032 auto dayBefore = epochNanoseconds - oneDay; 1033 MOZ_ASSERT(IsValidEpochNanoseconds(dayBefore)); 1034 1035 // Step 7 and 10-11. 1036 auto dayAfter = epochNanoseconds + oneDay; 1037 MOZ_ASSERT(IsValidEpochNanoseconds(dayAfter)); 1038 1039 // Step 12. 1040 int64_t offsetBefore; 1041 if (!GetOffsetNanosecondsFor(cx, timeZone, dayBefore, &offsetBefore)) { 1042 return false; 1043 } 1044 MOZ_ASSERT(std::abs(offsetBefore) < ToNanoseconds(TemporalUnit::Day)); 1045 1046 // Step 13. 1047 int64_t offsetAfter; 1048 if (!GetOffsetNanosecondsFor(cx, timeZone, dayAfter, &offsetAfter)) { 1049 return false; 1050 } 1051 MOZ_ASSERT(std::abs(offsetAfter) < ToNanoseconds(TemporalUnit::Day)); 1052 1053 // Step 14. 1054 int64_t nanoseconds = offsetAfter - offsetBefore; 1055 1056 // Step 15. 1057 MOZ_ASSERT(std::abs(nanoseconds) <= ToNanoseconds(TemporalUnit::Day)); 1058 1059 // Step 16. 1060 if (disambiguation == TemporalDisambiguation::Earlier) { 1061 // Steps 16.a-b. 1062 auto earlierTime = ::AddTime(isoDateTime.time, -nanoseconds); 1063 MOZ_ASSERT(std::abs(earlierTime.days) <= 1, 1064 "subtracting nanoseconds is at most one day"); 1065 1066 // Step 16.c. 1067 auto earlierDate = BalanceISODate(isoDateTime.date, earlierTime.days); 1068 1069 // Step 16.d. 1070 auto earlierDateTime = ISODateTime{earlierDate, earlierTime.time}; 1071 1072 // Step 16.e. 1073 PossibleEpochNanoseconds earlierEpochNs; 1074 if (!GetPossibleEpochNanoseconds(cx, timeZone, earlierDateTime, 1075 &earlierEpochNs)) { 1076 return false; 1077 } 1078 1079 // Step 16.f. 1080 MOZ_ASSERT(!earlierEpochNs.empty()); 1081 1082 // Step 16.g. 1083 *result = earlierEpochNs.front(); 1084 return true; 1085 } 1086 1087 // Step 17. 1088 MOZ_ASSERT(disambiguation == TemporalDisambiguation::Compatible || 1089 disambiguation == TemporalDisambiguation::Later); 1090 1091 // Steps 18-19. 1092 auto laterTime = ::AddTime(isoDateTime.time, nanoseconds); 1093 MOZ_ASSERT(std::abs(laterTime.days) <= 1, 1094 "adding nanoseconds is at most one day"); 1095 1096 // Step 20. 1097 auto laterDate = BalanceISODate(isoDateTime.date, laterTime.days); 1098 1099 // Step 21. 1100 auto laterDateTime = ISODateTime{laterDate, laterTime.time}; 1101 1102 // Step 22. 1103 PossibleEpochNanoseconds laterEpochNs; 1104 if (!GetPossibleEpochNanoseconds(cx, timeZone, laterDateTime, 1105 &laterEpochNs)) { 1106 return false; 1107 } 1108 1109 // Steps 23-24. 1110 MOZ_ASSERT(!laterEpochNs.empty()); 1111 1112 // Step 25. 1113 *result = laterEpochNs.back(); 1114 return true; 1115 } 1116 1117 /** 1118 * GetEpochNanosecondsFor ( timeZone, isoDateTime, disambiguation ) 1119 */ 1120 bool js::temporal::GetEpochNanosecondsFor(JSContext* cx, 1121 Handle<TimeZoneValue> timeZone, 1122 const ISODateTime& isoDateTime, 1123 TemporalDisambiguation disambiguation, 1124 EpochNanoseconds* result) { 1125 // Step 1. 1126 PossibleEpochNanoseconds possibleEpochNs; 1127 if (!GetPossibleEpochNanoseconds(cx, timeZone, isoDateTime, 1128 &possibleEpochNs)) { 1129 return false; 1130 } 1131 1132 // Step 2. 1133 return DisambiguatePossibleEpochNanoseconds( 1134 cx, possibleEpochNs, timeZone, isoDateTime, disambiguation, result); 1135 } 1136 1137 bool js::temporal::WrapTimeZoneValueObject( 1138 JSContext* cx, MutableHandle<TimeZoneObject*> timeZone) { 1139 // Handle the common case when |timeZone| is from the current compartment. 1140 if (MOZ_LIKELY(timeZone->compartment() == cx->compartment())) { 1141 return true; 1142 } 1143 1144 if (timeZone->isOffset()) { 1145 auto* obj = CreateTimeZoneObject(cx, timeZone->offsetMinutes()); 1146 if (!obj) { 1147 return false; 1148 } 1149 1150 timeZone.set(obj); 1151 return true; 1152 } 1153 1154 Rooted<JSString*> identifier(cx, timeZone->identifier()); 1155 if (!cx->compartment()->wrap(cx, &identifier)) { 1156 return false; 1157 } 1158 1159 Rooted<JSString*> primaryIdentifier(cx, timeZone->primaryIdentifier()); 1160 if (!cx->compartment()->wrap(cx, &primaryIdentifier)) { 1161 return false; 1162 } 1163 1164 Rooted<JSLinearString*> identifierLinear(cx, identifier->ensureLinear(cx)); 1165 if (!identifierLinear) { 1166 return false; 1167 } 1168 1169 Rooted<JSLinearString*> primaryIdentifierLinear( 1170 cx, primaryIdentifier->ensureLinear(cx)); 1171 if (!primaryIdentifierLinear) { 1172 return false; 1173 } 1174 1175 auto* obj = 1176 GetOrCreateTimeZoneObject(cx, identifierLinear, primaryIdentifierLinear); 1177 if (!obj) { 1178 return false; 1179 } 1180 1181 timeZone.set(obj); 1182 return true; 1183 } 1184 1185 void js::temporal::TimeZoneObject::finalize(JS::GCContext* gcx, JSObject* obj) { 1186 MOZ_ASSERT(gcx->onMainThread()); 1187 1188 if (auto* timeZone = obj->as<TimeZoneObject>().getTimeZone()) { 1189 intl::RemoveICUCellMemory(gcx, obj, EstimatedMemoryUse); 1190 delete timeZone; 1191 } 1192 } 1193 1194 const JSClassOps TimeZoneObject::classOps_ = { 1195 nullptr, // addProperty 1196 nullptr, // delProperty 1197 nullptr, // enumerate 1198 nullptr, // newEnumerate 1199 nullptr, // resolve 1200 nullptr, // mayResolve 1201 TimeZoneObject::finalize, // finalize 1202 nullptr, // call 1203 nullptr, // construct 1204 nullptr, // trace 1205 }; 1206 1207 const JSClass TimeZoneObject::class_ = { 1208 "Temporal.TimeZone", 1209 JSCLASS_HAS_RESERVED_SLOTS(TimeZoneObject::SLOT_COUNT) | 1210 JSCLASS_FOREGROUND_FINALIZE, 1211 &TimeZoneObject::classOps_, 1212 };