ToString.cpp (16819B)
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/ToString.h" 8 9 #include "mozilla/Assertions.h" 10 11 #include <cstdlib> 12 #include <stddef.h> 13 #include <stdint.h> 14 #include <string_view> 15 16 #include "builtin/temporal/Calendar.h" 17 #include "builtin/temporal/Instant.h" 18 #include "builtin/temporal/PlainDate.h" 19 #include "builtin/temporal/PlainDateTime.h" 20 #include "builtin/temporal/PlainMonthDay.h" 21 #include "builtin/temporal/PlainYearMonth.h" 22 #include "builtin/temporal/Temporal.h" 23 #include "builtin/temporal/TemporalTypes.h" 24 #include "builtin/temporal/TemporalUnit.h" 25 #include "builtin/temporal/TimeZone.h" 26 #include "builtin/temporal/ZonedDateTime.h" 27 #include "js/RootingAPI.h" 28 #include "util/StringBuilder.h" 29 #include "vm/StringType.h" 30 31 using namespace js; 32 using namespace js::temporal; 33 34 enum class TemporalStringFormat { 35 None, 36 Date, 37 Time, 38 DateTime, 39 YearMonth, 40 MonthDay, 41 ZonedDateTime, 42 Instant, 43 }; 44 45 enum class Critical : bool { No, Yes }; 46 47 class TemporalStringBuilder { 48 JSStringBuilder sb_; 49 50 TemporalStringFormat kind_ = TemporalStringFormat::None; 51 52 #ifdef DEBUG 53 bool reserved_ = false; 54 #endif 55 56 static constexpr size_t reserveAmount(TemporalStringFormat format) { 57 // Note: This doesn't reserve too much space, because the string builder 58 // already internally reserves space for 64 characters. 59 60 constexpr size_t datePart = 1 + 6 + 1 + 2 + 1 + 2; // 13 61 constexpr size_t timePart = 2 + 1 + 2 + 1 + 2 + 1 + 9; // 18 62 constexpr size_t dateTimePart = datePart + 1 + timePart; // including 'T' 63 constexpr size_t timeZoneOffsetPart = 1 + 2 + 1 + 2; // 6 64 65 switch (format) { 66 case TemporalStringFormat::Date: 67 case TemporalStringFormat::YearMonth: 68 case TemporalStringFormat::MonthDay: 69 return datePart; 70 case TemporalStringFormat::Time: 71 return timePart; 72 case TemporalStringFormat::DateTime: 73 return dateTimePart; 74 case TemporalStringFormat::ZonedDateTime: 75 return dateTimePart + timeZoneOffsetPart; 76 case TemporalStringFormat::Instant: 77 return dateTimePart + timeZoneOffsetPart; 78 case TemporalStringFormat::None: 79 break; 80 } 81 MOZ_CRASH("invalid reserve amount"); 82 } 83 84 public: 85 TemporalStringBuilder(JSContext* cx, TemporalStringFormat kind) 86 : sb_(cx), kind_(kind) { 87 MOZ_ASSERT(kind != TemporalStringFormat::None); 88 } 89 90 bool reserve() { 91 MOZ_ASSERT(!reserved_); 92 93 if (!sb_.reserve(reserveAmount(kind_))) { 94 return false; 95 } 96 97 #ifdef DEBUG 98 reserved_ = true; 99 #endif 100 return true; 101 } 102 103 void append(char value) { 104 MOZ_ASSERT(reserved_); 105 sb_.infallibleAppend(value); 106 } 107 108 void appendTwoDigit(int32_t value) { 109 MOZ_ASSERT(0 <= value && value <= 99); 110 MOZ_ASSERT(reserved_); 111 112 sb_.infallibleAppend(char('0' + (value / 10))); 113 sb_.infallibleAppend(char('0' + (value % 10))); 114 } 115 116 void appendFourDigit(int32_t value) { 117 MOZ_ASSERT(0 <= value && value <= 9999); 118 MOZ_ASSERT(reserved_); 119 120 sb_.infallibleAppend(char('0' + (value / 1000))); 121 sb_.infallibleAppend(char('0' + (value % 1000) / 100)); 122 sb_.infallibleAppend(char('0' + (value % 100) / 10)); 123 sb_.infallibleAppend(char('0' + (value % 10))); 124 } 125 126 void appendSixDigit(int32_t value) { 127 MOZ_ASSERT(0 <= value && value <= 999999); 128 MOZ_ASSERT(reserved_); 129 130 sb_.infallibleAppend(char('0' + (value / 100000))); 131 sb_.infallibleAppend(char('0' + (value % 100000) / 10000)); 132 sb_.infallibleAppend(char('0' + (value % 10000) / 1000)); 133 sb_.infallibleAppend(char('0' + (value % 1000) / 100)); 134 sb_.infallibleAppend(char('0' + (value % 100) / 10)); 135 sb_.infallibleAppend(char('0' + (value % 10))); 136 } 137 138 void appendYear(int32_t year) { 139 if (0 <= year && year <= 9999) { 140 appendFourDigit(year); 141 } else { 142 append(year < 0 ? '-' : '+'); 143 appendSixDigit(std::abs(year)); 144 } 145 } 146 147 bool appendCalendarAnnnotation(std::string_view id, Critical critical) { 148 std::string_view start = bool(critical) ? "[!u-ca=" : "[u-ca="; 149 return sb_.append(start.data(), start.length()) && 150 sb_.append(id.data(), id.length()) && sb_.append(']'); 151 } 152 153 bool appendTimeZoneAnnnotation(const JSLinearString* id, Critical critical) { 154 std::string_view start = bool(critical) ? "[!" : "["; 155 return sb_.append(start.data(), start.length()) && sb_.append(id) && 156 sb_.append(']'); 157 } 158 159 auto* finishString() { return sb_.finishString(); } 160 }; 161 162 /** 163 * FormatFractionalSeconds ( subSecondNanoseconds, precision ) 164 */ 165 static void FormatFractionalSeconds(TemporalStringBuilder& result, 166 int32_t subSecondNanoseconds, 167 Precision precision) { 168 MOZ_ASSERT(0 <= subSecondNanoseconds && subSecondNanoseconds < 1'000'000'000); 169 MOZ_ASSERT(precision != Precision::Minute()); 170 171 // Steps 1-2. 172 if (precision == Precision::Auto()) { 173 // Step 1.a. 174 if (subSecondNanoseconds == 0) { 175 return; 176 } 177 178 // Step 3. (Reordered) 179 result.append('.'); 180 181 // Steps 1.b-c. 182 int32_t k = 100'000'000; 183 do { 184 result.append(char('0' + (subSecondNanoseconds / k))); 185 subSecondNanoseconds %= k; 186 k /= 10; 187 } while (subSecondNanoseconds); 188 } else { 189 // Step 2.a. 190 uint8_t p = precision.value(); 191 if (p == 0) { 192 return; 193 } 194 195 // Step 3. (Reordered) 196 result.append('.'); 197 198 // Steps 2.b-c. 199 int32_t k = 100'000'000; 200 for (uint8_t i = 0; i < p; i++) { 201 result.append(char('0' + (subSecondNanoseconds / k))); 202 subSecondNanoseconds %= k; 203 k /= 10; 204 } 205 } 206 } 207 208 /** 209 * FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision ) 210 */ 211 static void FormatTimeString(TemporalStringBuilder& result, const Time& time, 212 Precision precision) { 213 // Step 1. 214 result.appendTwoDigit(time.hour); 215 216 // Step 2. 217 result.append(':'); 218 result.appendTwoDigit(time.minute); 219 220 // Steps 4-7. 221 if (precision != Precision::Minute()) { 222 result.append(':'); 223 result.appendTwoDigit(time.second); 224 225 int32_t subSecondNanoseconds = time.millisecond * 1'000'000 + 226 time.microsecond * 1'000 + time.nanosecond; 227 FormatFractionalSeconds(result, subSecondNanoseconds, precision); 228 } 229 } 230 231 static void FormatDateString(TemporalStringBuilder& result, 232 const ISODate& date) { 233 result.appendYear(date.year); 234 result.append('-'); 235 result.appendTwoDigit(date.month); 236 result.append('-'); 237 result.appendTwoDigit(date.day); 238 } 239 240 static void FormatDateTimeString(TemporalStringBuilder& result, 241 const ISODateTime& dateTime, 242 Precision precision) { 243 FormatDateString(result, dateTime.date); 244 result.append('T'); 245 FormatTimeString(result, dateTime.time, precision); 246 } 247 248 /** 249 * FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] ) 250 */ 251 static void FormatOffsetTimeZoneIdentifier(TemporalStringBuilder& result, 252 int32_t offsetMinutes) { 253 MOZ_ASSERT(std::abs(offsetMinutes) < UnitsPerDay(TemporalUnit::Minute), 254 "time zone offset mustn't exceed 24-hours"); 255 256 // Step 1. 257 char sign = offsetMinutes >= 0 ? '+' : '-'; 258 259 // Step 2. 260 int32_t absoluteMinutes = std::abs(offsetMinutes); 261 262 // Step 3. 263 int32_t hours = absoluteMinutes / 60; 264 265 // Step 4. 266 int32_t minutes = absoluteMinutes % 60; 267 268 // Steps 5-6. (Inlined FormatTimeString) 269 result.append(sign); 270 result.appendTwoDigit(hours); 271 result.append(':'); 272 result.appendTwoDigit(minutes); 273 } 274 275 // Returns |RoundNumberToIncrement(offsetNanoseconds, 60 × 10^9, "halfExpand")| 276 // divided by |60 × 10^9|. 277 static int32_t RoundNanosecondsToMinutes(int64_t offsetNanoseconds) { 278 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); 279 280 constexpr int64_t increment = ToNanoseconds(TemporalUnit::Minute); 281 282 int64_t quotient = offsetNanoseconds / increment; 283 int64_t remainder = offsetNanoseconds % increment; 284 if (std::abs(remainder * 2) >= increment) { 285 quotient += (offsetNanoseconds > 0 ? 1 : -1); 286 } 287 return int32_t(quotient); 288 } 289 290 /** 291 * FormatDateTimeUTCOffsetRounded ( offsetNanoseconds ) 292 */ 293 static void FormatDateTimeUTCOffsetRounded(TemporalStringBuilder& result, 294 int64_t offsetNanoseconds) { 295 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); 296 297 // Steps 1-3. 298 int32_t offsetMinutes = RoundNanosecondsToMinutes(offsetNanoseconds); 299 300 // Step 4. 301 FormatOffsetTimeZoneIdentifier(result, offsetMinutes); 302 } 303 304 /** 305 * FormatCalendarAnnotation ( id, showCalendar ) 306 */ 307 static bool FormatCalendarAnnotation(TemporalStringBuilder& result, 308 const CalendarValue& calendar, 309 ShowCalendar showCalendar) { 310 switch (showCalendar) { 311 case ShowCalendar::Never: 312 return true; 313 314 case ShowCalendar::Auto: { 315 if (calendar.identifier() == CalendarId::ISO8601) { 316 return true; 317 } 318 [[fallthrough]]; 319 } 320 321 case ShowCalendar::Always: { 322 auto id = CalendarIdentifier(calendar); 323 return result.appendCalendarAnnnotation(id, Critical::No); 324 } 325 326 case ShowCalendar::Critical: { 327 auto id = CalendarIdentifier(calendar); 328 return result.appendCalendarAnnnotation(id, Critical::Yes); 329 } 330 } 331 MOZ_CRASH("bad calendar option"); 332 } 333 334 static bool FormatTimeZoneAnnotation(TemporalStringBuilder& result, 335 const TimeZoneValue& timeZone, 336 ShowTimeZoneName showTimeZone) { 337 switch (showTimeZone) { 338 case ShowTimeZoneName::Never: 339 return true; 340 341 case ShowTimeZoneName::Auto: 342 return result.appendTimeZoneAnnnotation(timeZone.identifier(), 343 Critical::No); 344 345 case ShowTimeZoneName::Critical: 346 return result.appendTimeZoneAnnnotation(timeZone.identifier(), 347 Critical::Yes); 348 } 349 MOZ_CRASH("bad time zone option"); 350 } 351 352 /** 353 * TemporalInstantToString ( instant, timeZone, precision ) 354 */ 355 JSString* js::temporal::TemporalInstantToString(JSContext* cx, 356 const EpochNanoseconds& epochNs, 357 Handle<TimeZoneValue> timeZone, 358 Precision precision) { 359 TemporalStringBuilder result(cx, TemporalStringFormat::Instant); 360 if (!result.reserve()) { 361 return nullptr; 362 } 363 364 // Steps 1-2. 365 int64_t offsetNanoseconds = 0; 366 if (timeZone) { 367 if (!GetOffsetNanosecondsFor(cx, timeZone, epochNs, &offsetNanoseconds)) { 368 return nullptr; 369 } 370 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); 371 } 372 373 // Step 3. (Not applicable in our implementation.) 374 375 // Step 4. 376 auto dateTime = GetISODateTimeFor(epochNs, offsetNanoseconds); 377 378 // Step 5. (Inlined ISODateTimeToString) 379 FormatDateTimeString(result, dateTime, precision); 380 381 // Steps 6-7. 382 Rooted<JSString*> timeZoneString(cx); 383 if (!timeZone) { 384 result.append('Z'); 385 } else { 386 FormatDateTimeUTCOffsetRounded(result, offsetNanoseconds); 387 } 388 389 // Step 8. 390 return result.finishString(); 391 } 392 393 /** 394 * TemporalDateToString ( temporalDate, showCalendar ) 395 */ 396 JSString* js::temporal::TemporalDateToString( 397 JSContext* cx, Handle<PlainDateObject*> temporalDate, 398 ShowCalendar showCalendar) { 399 auto date = temporalDate->date(); 400 401 TemporalStringBuilder result(cx, TemporalStringFormat::Date); 402 if (!result.reserve()) { 403 return nullptr; 404 } 405 406 // Steps 1-3. 407 FormatDateString(result, date); 408 409 // Step 4. 410 if (!FormatCalendarAnnotation(result, temporalDate->calendar(), 411 showCalendar)) { 412 return nullptr; 413 } 414 415 // Step 5. 416 return result.finishString(); 417 } 418 419 /** 420 * ISODateTimeToString ( isoDateTime, calendar, precision, showCalendar ) 421 */ 422 JSString* js::temporal::ISODateTimeToString(JSContext* cx, 423 const ISODateTime& isoDateTime, 424 Handle<CalendarValue> calendar, 425 Precision precision, 426 ShowCalendar showCalendar) { 427 MOZ_ASSERT(IsValidISODateTime(isoDateTime)); 428 429 TemporalStringBuilder result(cx, TemporalStringFormat::DateTime); 430 if (!result.reserve()) { 431 return nullptr; 432 } 433 434 // Steps 1-5. 435 FormatDateTimeString(result, isoDateTime, precision); 436 437 // Step 6. 438 if (!FormatCalendarAnnotation(result, calendar, showCalendar)) { 439 return nullptr; 440 } 441 442 // Step 7. 443 return result.finishString(); 444 } 445 446 /** 447 * TimeRecordToString ( time, precision ) 448 */ 449 JSString* js::temporal::TimeRecordToString(JSContext* cx, const Time& time, 450 Precision precision) { 451 TemporalStringBuilder result(cx, TemporalStringFormat::Time); 452 if (!result.reserve()) { 453 return nullptr; 454 } 455 456 // Steps 1-2. 457 FormatTimeString(result, time, precision); 458 459 return result.finishString(); 460 } 461 462 /** 463 * TemporalMonthDayToString ( monthDay, showCalendar ) 464 */ 465 JSString* js::temporal::TemporalMonthDayToString( 466 JSContext* cx, Handle<PlainMonthDayObject*> monthDay, 467 ShowCalendar showCalendar) { 468 TemporalStringBuilder result(cx, TemporalStringFormat::MonthDay); 469 if (!result.reserve()) { 470 return nullptr; 471 } 472 473 // Steps 1-4. 474 auto date = monthDay->date(); 475 if (showCalendar == ShowCalendar::Always || 476 showCalendar == ShowCalendar::Critical || 477 monthDay->calendar().identifier() != CalendarId::ISO8601) { 478 FormatDateString(result, date); 479 } else { 480 result.appendTwoDigit(date.month); 481 result.append('-'); 482 result.appendTwoDigit(date.day); 483 } 484 485 // Steps 5-6. 486 if (!FormatCalendarAnnotation(result, monthDay->calendar(), showCalendar)) { 487 return nullptr; 488 } 489 490 // Step 7. 491 return result.finishString(); 492 } 493 494 /** 495 * TemporalYearMonthToString ( yearMonth, showCalendar ) 496 */ 497 JSString* js::temporal::TemporalYearMonthToString( 498 JSContext* cx, Handle<PlainYearMonthObject*> yearMonth, 499 ShowCalendar showCalendar) { 500 TemporalStringBuilder result(cx, TemporalStringFormat::YearMonth); 501 if (!result.reserve()) { 502 return nullptr; 503 } 504 505 // Steps 1-4. 506 auto date = yearMonth->date(); 507 if (showCalendar == ShowCalendar::Always || 508 showCalendar == ShowCalendar::Critical || 509 yearMonth->calendar().identifier() != CalendarId::ISO8601) { 510 FormatDateString(result, date); 511 } else { 512 result.appendYear(date.year); 513 result.append('-'); 514 result.appendTwoDigit(date.month); 515 } 516 517 // Steps 5-6. 518 if (!FormatCalendarAnnotation(result, yearMonth->calendar(), showCalendar)) { 519 return nullptr; 520 } 521 522 // Step 7. 523 return result.finishString(); 524 } 525 526 /** 527 * TemporalZonedDateTimeToString ( zonedDateTime, precision, showCalendar, 528 * showTimeZone, showOffset [ , increment, unit, roundingMode ] ) 529 */ 530 JSString* js::temporal::TemporalZonedDateTimeToString( 531 JSContext* cx, Handle<ZonedDateTime> zonedDateTime, Precision precision, 532 ShowCalendar showCalendar, ShowTimeZoneName showTimeZone, 533 ShowOffset showOffset, Increment increment, TemporalUnit unit, 534 TemporalRoundingMode roundingMode) { 535 TemporalStringBuilder result(cx, TemporalStringFormat::ZonedDateTime); 536 if (!result.reserve()) { 537 return nullptr; 538 } 539 540 // Steps 1-3. (Not applicable in our implementation.) 541 542 // Steps 4-5. 543 auto epochNs = RoundTemporalInstant(zonedDateTime.epochNanoseconds(), 544 increment, unit, roundingMode); 545 546 // Step 6. 547 auto timeZone = zonedDateTime.timeZone(); 548 549 // Step 7. 550 int64_t offsetNanoseconds; 551 if (!GetOffsetNanosecondsFor(cx, timeZone, epochNs, &offsetNanoseconds)) { 552 return nullptr; 553 } 554 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); 555 556 // Step 8. 557 auto isoDateTime = GetISODateTimeFor(epochNs, offsetNanoseconds); 558 559 // Step 9. (Inlined ISODateTimeToString) 560 FormatDateTimeString(result, isoDateTime, precision); 561 562 // Steps 10-11. 563 if (showOffset != ShowOffset::Never) { 564 FormatDateTimeUTCOffsetRounded(result, offsetNanoseconds); 565 } 566 567 // Steps 12-13. 568 if (!FormatTimeZoneAnnotation(result, timeZone, showTimeZone)) { 569 return nullptr; 570 } 571 572 // Step 14. 573 if (!FormatCalendarAnnotation(result, zonedDateTime.calendar(), 574 showCalendar)) { 575 return nullptr; 576 } 577 578 // Step 15. 579 return result.finishString(); 580 }