CalendarFields.cpp (18099B)
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/CalendarFields.h" 8 9 #include "mozilla/Assertions.h" 10 #include "mozilla/EnumTypeTraits.h" 11 #include "mozilla/Maybe.h" 12 #include "mozilla/Range.h" 13 #include "mozilla/TextUtils.h" 14 15 #include <stdint.h> 16 #include <string_view> 17 18 #include "jspubtd.h" 19 #include "NamespaceImports.h" 20 21 #include "builtin/temporal/Calendar.h" 22 #include "builtin/temporal/Era.h" 23 #include "builtin/temporal/Temporal.h" 24 #include "builtin/temporal/TemporalParser.h" 25 #include "builtin/temporal/TimeZone.h" 26 #include "gc/Barrier.h" 27 #include "gc/Tracer.h" 28 #include "js/Conversions.h" 29 #include "js/ErrorReport.h" 30 #include "js/friend/ErrorMessages.h" 31 #include "js/GCAPI.h" 32 #include "js/Printer.h" 33 #include "js/RootingAPI.h" 34 #include "js/Value.h" 35 #include "util/Text.h" 36 #include "vm/BytecodeUtil.h" 37 #include "vm/JSAtomState.h" 38 #include "vm/JSContext.h" 39 #include "vm/JSObject.h" 40 #include "vm/StringType.h" 41 42 #include "vm/JSObject-inl.h" 43 #include "vm/ObjectOperations-inl.h" 44 45 using namespace js; 46 using namespace js::temporal; 47 48 void CalendarFields::trace(JSTracer* trc) { 49 TraceNullableRoot(trc, &era_, "CalendarFields::era"); 50 timeZone_.trace(trc); 51 } 52 53 void CalendarFields::setFrom(CalendarField field, 54 const CalendarFields& source) { 55 MOZ_ASSERT(source.has(field)); 56 57 switch (field) { 58 case CalendarField::Era: 59 setEra(source.era()); 60 return; 61 case CalendarField::EraYear: 62 setEraYear(source.eraYear()); 63 return; 64 case CalendarField::Year: 65 setYear(source.year()); 66 return; 67 case CalendarField::Month: 68 setMonth(source.month()); 69 return; 70 case CalendarField::MonthCode: 71 setMonthCode(source.monthCode()); 72 return; 73 case CalendarField::Day: 74 setDay(source.day()); 75 return; 76 case CalendarField::Hour: 77 setHour(source.hour()); 78 return; 79 case CalendarField::Minute: 80 setMinute(source.minute()); 81 return; 82 case CalendarField::Second: 83 setSecond(source.second()); 84 return; 85 case CalendarField::Millisecond: 86 setMillisecond(source.millisecond()); 87 return; 88 case CalendarField::Microsecond: 89 setMicrosecond(source.microsecond()); 90 return; 91 case CalendarField::Nanosecond: 92 setNanosecond(source.nanosecond()); 93 return; 94 case CalendarField::Offset: 95 setOffset(source.offset()); 96 return; 97 case CalendarField::TimeZone: 98 setTimeZone(source.timeZone()); 99 return; 100 } 101 MOZ_CRASH("invalid temporal field"); 102 } 103 104 static PropertyName* ToPropertyName(JSContext* cx, CalendarField field) { 105 switch (field) { 106 case CalendarField::Era: 107 return cx->names().era; 108 case CalendarField::EraYear: 109 return cx->names().eraYear; 110 case CalendarField::Year: 111 return cx->names().year; 112 case CalendarField::Month: 113 return cx->names().month; 114 case CalendarField::MonthCode: 115 return cx->names().monthCode; 116 case CalendarField::Day: 117 return cx->names().day; 118 case CalendarField::Hour: 119 return cx->names().hour; 120 case CalendarField::Minute: 121 return cx->names().minute; 122 case CalendarField::Second: 123 return cx->names().second; 124 case CalendarField::Millisecond: 125 return cx->names().millisecond; 126 case CalendarField::Microsecond: 127 return cx->names().microsecond; 128 case CalendarField::Nanosecond: 129 return cx->names().nanosecond; 130 case CalendarField::Offset: 131 return cx->names().offset; 132 case CalendarField::TimeZone: 133 return cx->names().timeZone; 134 } 135 MOZ_CRASH("invalid temporal field name"); 136 } 137 138 static constexpr const char* ToCString(CalendarField field) { 139 switch (field) { 140 case CalendarField::Era: 141 return "era"; 142 case CalendarField::EraYear: 143 return "eraYear"; 144 case CalendarField::Year: 145 return "year"; 146 case CalendarField::Month: 147 return "month"; 148 case CalendarField::MonthCode: 149 return "monthCode"; 150 case CalendarField::Day: 151 return "day"; 152 case CalendarField::Hour: 153 return "hour"; 154 case CalendarField::Minute: 155 return "minute"; 156 case CalendarField::Second: 157 return "second"; 158 case CalendarField::Millisecond: 159 return "millisecond"; 160 case CalendarField::Microsecond: 161 return "microsecond"; 162 case CalendarField::Nanosecond: 163 return "nanosecond"; 164 case CalendarField::Offset: 165 return "offset"; 166 case CalendarField::TimeZone: 167 return "timeZone"; 168 } 169 MOZ_CRASH("invalid temporal field name"); 170 } 171 172 static constexpr bool CalendarFieldsAreSorted() { 173 constexpr auto min = mozilla::ContiguousEnumValues<CalendarField>::min; 174 constexpr auto max = mozilla::ContiguousEnumValues<CalendarField>::max; 175 176 auto field = min; 177 while (field != max) { 178 auto next = static_cast<CalendarField>(mozilla::UnderlyingValue(field) + 1); 179 180 auto a = std::string_view{ToCString(field)}; 181 auto b = std::string_view{ToCString(next)}; 182 if (a.compare(b) >= 0) { 183 return false; 184 } 185 field = next; 186 } 187 return true; 188 } 189 190 /** 191 * CalendarExtraFields ( calendar, fields ) 192 */ 193 static mozilla::EnumSet<CalendarField> CalendarExtraFields( 194 CalendarId calendar, mozilla::EnumSet<CalendarField> fields) { 195 // Step 1. 196 if (calendar == CalendarId::ISO8601) { 197 return {}; 198 } 199 200 // Step 2. 201 202 // "era" and "eraYear" are relevant for calendars supporting eras when 203 // "year" is present. 204 if (fields.contains(CalendarField::Year) && CalendarSupportsEra(calendar)) { 205 return {CalendarField::Era, CalendarField::EraYear}; 206 } 207 return {}; 208 } 209 210 /** 211 * ParseMonthCode ( argument ) 212 */ 213 template <typename CharT> 214 static mozilla::Maybe<MonthCodeField> ParseMonthCode( 215 mozilla::Range<const CharT> chars) { 216 // Steps 1-2. (Not applicable) 217 218 // Steps 3-6. 219 if (chars.length() < 3 || chars.length() > 4) { 220 return mozilla::Nothing(); 221 } 222 223 // Starts with capital letter 'M'. Leap months end with capital letter 'L'. 224 bool isLeapMonth = chars.length() == 4; 225 if (chars[0] != 'M' || (isLeapMonth && chars[3] != 'L')) { 226 return mozilla::Nothing(); 227 } 228 229 // Month numbers are ASCII digits. 230 if (!mozilla::IsAsciiDigit(chars[1]) || !mozilla::IsAsciiDigit(chars[2])) { 231 return mozilla::Nothing(); 232 } 233 234 // Steps 6-7. 235 int32_t ordinal = 236 AsciiDigitToNumber(chars[1]) * 10 + AsciiDigitToNumber(chars[2]); 237 238 // Step 8. 239 if (ordinal == 0 && !isLeapMonth) { 240 return mozilla::Nothing(); 241 } 242 243 // Step 9. 244 return mozilla::Some(MonthCodeField{ordinal, isLeapMonth}); 245 } 246 247 /** 248 * ParseMonthCode ( argument ) 249 */ 250 static auto ParseMonthCode(const JSLinearString* linear) { 251 JS::AutoCheckCannotGC nogc; 252 253 if (linear->hasLatin1Chars()) { 254 return ParseMonthCode(linear->latin1Range(nogc)); 255 } 256 return ParseMonthCode(linear->twoByteRange(nogc)); 257 } 258 259 /** 260 * ParseMonthCode ( argument ) 261 */ 262 static bool ParseMonthCode(JSContext* cx, Handle<Value> value, 263 MonthCodeField* result) { 264 auto reportInvalidMonthCode = [&](JSLinearString* monthCode) { 265 if (auto code = QuoteString(cx, monthCode)) { 266 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 267 JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE, 268 code.get()); 269 } 270 return false; 271 }; 272 273 // Step 1. 274 Rooted<Value> monthCode(cx, value); 275 if (!ToPrimitive(cx, JSTYPE_STRING, &monthCode)) { 276 return false; 277 } 278 279 // Step 2. 280 if (!monthCode.isString()) { 281 ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, monthCode, 282 nullptr, "not a string"); 283 return false; 284 } 285 286 JSLinearString* monthCodeStr = monthCode.toString()->ensureLinear(cx); 287 if (!monthCodeStr) { 288 return false; 289 } 290 291 // Steps 3-9. 292 auto parsed = ParseMonthCode(monthCodeStr); 293 if (!parsed) { 294 return reportInvalidMonthCode(monthCodeStr); 295 } 296 297 *result = *parsed; 298 return true; 299 } 300 301 /** 302 * ToOffsetString ( argument ) 303 */ 304 static bool ToOffsetString(JSContext* cx, Handle<Value> value, 305 int64_t* result) { 306 // Step 1. 307 Rooted<Value> offset(cx, value); 308 if (!ToPrimitive(cx, JSTYPE_STRING, &offset)) { 309 return false; 310 } 311 312 // Step 2. 313 if (!offset.isString()) { 314 ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, offset, 315 nullptr, "not a string"); 316 return false; 317 } 318 Rooted<JSString*> offsetStr(cx, offset.toString()); 319 320 // Steps 3-4. 321 return ParseDateTimeUTCOffset(cx, offsetStr, result); 322 } 323 324 enum class Partial : bool { No, Yes }; 325 326 /** 327 * PrepareCalendarFields ( calendar, fields, calendarFieldNames, 328 * nonCalendarFieldNames, requiredFieldNames ) 329 */ 330 static bool PrepareCalendarFields( 331 JSContext* cx, Handle<CalendarValue> calendar, Handle<JSObject*> fields, 332 mozilla::EnumSet<CalendarField> fieldNames, 333 mozilla::EnumSet<CalendarField> requiredFields, Partial partial, 334 MutableHandle<CalendarFields> result) { 335 MOZ_ASSERT_IF(partial == Partial::Yes, requiredFields.isEmpty()); 336 337 // Steps 1-2. (Not applicable in our implementation.) 338 339 // Step 3. 340 auto extraFieldNames = CalendarExtraFields(calendar.identifier(), fieldNames); 341 342 // Step 4. 343 fieldNames += extraFieldNames; 344 345 // Step 5. (Not applicable in our implementation.) 346 347 // Step 6. 348 // 349 // Default initialize the result. 350 result.set(CalendarFields{}); 351 352 // Step 7. (Not applicable in our implementation.) 353 354 // Step 8. 355 static_assert(CalendarFieldsAreSorted(), 356 "EnumSet<CalendarField> iteration is sorted"); 357 358 // Step 9. 359 Rooted<Value> value(cx); 360 for (auto fieldName : fieldNames) { 361 auto* propertyName = ToPropertyName(cx, fieldName); 362 const auto* cstr = ToCString(fieldName); 363 364 // Step 9.a. (Not applicable in our implementation.) 365 366 // Step 9.b. 367 if (!GetProperty(cx, fields, fields, propertyName, &value)) { 368 return false; 369 } 370 371 // Steps 9.c-d. 372 if (!value.isUndefined()) { 373 // Steps 9.c.i-ii. (Not applicable in our implementation.) 374 375 // Steps 9.c.iii-ix. 376 switch (fieldName) { 377 case CalendarField::Era: { 378 JSString* era = ToString(cx, value); 379 if (!era) { 380 return false; 381 } 382 result.setEra(era); 383 break; 384 } 385 case CalendarField::EraYear: { 386 double eraYear; 387 if (!ToIntegerWithTruncation(cx, value, cstr, &eraYear)) { 388 return false; 389 } 390 result.setEraYear(eraYear); 391 break; 392 } 393 case CalendarField::Year: { 394 double year; 395 if (!ToIntegerWithTruncation(cx, value, cstr, &year)) { 396 return false; 397 } 398 result.setYear(year); 399 break; 400 } 401 case CalendarField::Month: { 402 double month; 403 if (!ToPositiveIntegerWithTruncation(cx, value, cstr, &month)) { 404 return false; 405 } 406 result.setMonth(month); 407 break; 408 } 409 case CalendarField::MonthCode: { 410 MonthCodeField monthCode; 411 if (!ParseMonthCode(cx, value, &monthCode)) { 412 return false; 413 } 414 result.setMonthCode(monthCode); 415 break; 416 } 417 case CalendarField::Day: { 418 double day; 419 if (!ToPositiveIntegerWithTruncation(cx, value, cstr, &day)) { 420 return false; 421 } 422 result.setDay(day); 423 break; 424 } 425 case CalendarField::Hour: { 426 double hour; 427 if (!ToIntegerWithTruncation(cx, value, cstr, &hour)) { 428 return false; 429 } 430 result.setHour(hour); 431 break; 432 } 433 case CalendarField::Minute: { 434 double minute; 435 if (!ToIntegerWithTruncation(cx, value, cstr, &minute)) { 436 return false; 437 } 438 result.setMinute(minute); 439 break; 440 } 441 case CalendarField::Second: { 442 double second; 443 if (!ToIntegerWithTruncation(cx, value, cstr, &second)) { 444 return false; 445 } 446 result.setSecond(second); 447 break; 448 } 449 case CalendarField::Millisecond: { 450 double millisecond; 451 if (!ToIntegerWithTruncation(cx, value, cstr, &millisecond)) { 452 return false; 453 } 454 result.setMillisecond(millisecond); 455 break; 456 } 457 case CalendarField::Microsecond: { 458 double microsecond; 459 if (!ToIntegerWithTruncation(cx, value, cstr, µsecond)) { 460 return false; 461 } 462 result.setMicrosecond(microsecond); 463 break; 464 } 465 case CalendarField::Nanosecond: { 466 double nanosecond; 467 if (!ToIntegerWithTruncation(cx, value, cstr, &nanosecond)) { 468 return false; 469 } 470 result.setNanosecond(nanosecond); 471 break; 472 } 473 case CalendarField::Offset: { 474 int64_t offset; 475 if (!ToOffsetString(cx, value, &offset)) { 476 return false; 477 } 478 result.setOffset(OffsetField{offset}); 479 break; 480 } 481 case CalendarField::TimeZone: 482 Rooted<TimeZoneValue> timeZone(cx); 483 if (!ToTemporalTimeZone(cx, value, &timeZone)) { 484 return false; 485 } 486 result.setTimeZone(timeZone); 487 break; 488 } 489 } else if (partial == Partial::No) { 490 // Step 9.d.i. 491 if (requiredFields.contains(fieldName)) { 492 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 493 JSMSG_TEMPORAL_MISSING_PROPERTY, cstr); 494 return false; 495 } 496 497 // Step 9.d.ii. 498 result.setDefault(fieldName); 499 } 500 } 501 502 // Step 10. 503 if (partial == Partial::Yes && result.keys().isEmpty()) { 504 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 505 JSMSG_TEMPORAL_MISSING_TEMPORAL_FIELDS); 506 return false; 507 } 508 509 // Step 11. 510 return true; 511 } 512 513 /** 514 * PrepareCalendarFields ( calendar, fields, calendarFieldNames, 515 * nonCalendarFieldNames, requiredFieldNames ) 516 */ 517 bool js::temporal::PrepareCalendarFields( 518 JSContext* cx, Handle<CalendarValue> calendar, Handle<JSObject*> fields, 519 mozilla::EnumSet<CalendarField> fieldNames, 520 mozilla::EnumSet<CalendarField> requiredFields, 521 MutableHandle<CalendarFields> result) { 522 return PrepareCalendarFields(cx, calendar, fields, fieldNames, requiredFields, 523 Partial::No, result); 524 } 525 526 /** 527 * PrepareCalendarFields ( calendar, fields, calendarFieldNames, 528 * nonCalendarFieldNames, requiredFieldNames ) 529 */ 530 bool js::temporal::PreparePartialCalendarFields( 531 JSContext* cx, Handle<CalendarValue> calendar, Handle<JSObject*> fields, 532 mozilla::EnumSet<CalendarField> fieldNames, 533 JS::MutableHandle<CalendarFields> result) { 534 return PrepareCalendarFields(cx, calendar, fields, fieldNames, {}, 535 Partial::Yes, result); 536 } 537 538 /** 539 * NonISOFieldKeysToIgnore ( calendar, keys ) 540 */ 541 static auto NonISOFieldKeysToIgnore(CalendarId calendar, 542 mozilla::EnumSet<CalendarField> keys) { 543 static constexpr auto eraOrEraYear = mozilla::EnumSet{ 544 CalendarField::Era, 545 CalendarField::EraYear, 546 }; 547 548 static constexpr auto eraOrAnyYear = mozilla::EnumSet{ 549 CalendarField::Era, 550 CalendarField::EraYear, 551 CalendarField::Year, 552 }; 553 554 static constexpr auto monthOrMonthCode = mozilla::EnumSet{ 555 CalendarField::Month, 556 CalendarField::MonthCode, 557 }; 558 559 static constexpr auto dayOrAnyMonth = mozilla::EnumSet{ 560 CalendarField::Day, 561 CalendarField::Month, 562 CalendarField::MonthCode, 563 }; 564 565 // A field always invalidates at least itself, so start with ignoring all 566 // input fields. 567 auto result = keys; 568 569 // "month" and "monthCode" are mutually exclusive. 570 if (!(keys & monthOrMonthCode).isEmpty()) { 571 result += monthOrMonthCode; 572 } 573 574 // "era", "eraYear", and "year" are mutually exclusive when the calendar 575 // supports eras. 576 if (CalendarSupportsEra(calendar) && !(keys & eraOrAnyYear).isEmpty()) { 577 result += eraOrAnyYear; 578 } 579 580 // If eras can start in the middle of the year, we have to ignore "era" and 581 // "eraYear" if any of "day", "month", or "monthCode" is present. 582 if (CalendarHasMidYearEras(calendar) && !(keys & dayOrAnyMonth).isEmpty()) { 583 result += eraOrEraYear; 584 } 585 586 return result; 587 } 588 589 /** 590 * CalendarFieldKeysToIgnore ( calendar, keys ) 591 */ 592 static auto CalendarFieldKeysToIgnore(CalendarId calendar, 593 mozilla::EnumSet<CalendarField> keys) { 594 // Step 1. 595 if (calendar == CalendarId::ISO8601) { 596 // Steps 1.a and 1.b.i. 597 auto ignoredKeys = keys; 598 599 // Step 1.b.ii. 600 if (keys.contains(CalendarField::Month)) { 601 ignoredKeys += CalendarField::MonthCode; 602 } 603 604 // Step 1.b.iii. 605 else if (keys.contains(CalendarField::MonthCode)) { 606 ignoredKeys += CalendarField::Month; 607 } 608 609 // Steps 1.c-d. 610 return ignoredKeys; 611 } 612 613 // Step 2. 614 return NonISOFieldKeysToIgnore(calendar, keys); 615 } 616 617 /** 618 * CalendarMergeFields ( calendar, fields, additionalFields ) 619 */ 620 CalendarFields js::temporal::CalendarMergeFields( 621 const CalendarValue& calendar, const CalendarFields& fields, 622 const CalendarFields& additionalFields) { 623 auto calendarId = calendar.identifier(); 624 625 // Steps 1. 626 auto additionalKeys = additionalFields.keys(); 627 628 // Step 2. 629 auto overriddenKeys = CalendarFieldKeysToIgnore(calendarId, additionalKeys); 630 MOZ_ASSERT(overriddenKeys.contains(additionalKeys)); 631 632 // Step 3. 633 auto merged = CalendarFields{}; 634 635 // Step 4. 636 auto fieldsKeys = fields.keys(); 637 638 // Step 5.b. 639 for (auto key : (fieldsKeys - overriddenKeys)) { 640 merged.setFrom(key, fields); 641 } 642 643 // Step 5.c. 644 for (auto key : additionalKeys) { 645 merged.setFrom(key, additionalFields); 646 } 647 648 // Step 6. 649 return merged; 650 }