TimeZone.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. /*
  2. * Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
  3. * Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
  4. * Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
  5. *
  6. * SPDX-License-Identifier: BSD-2-Clause
  7. */
  8. #include <LibJS/Runtime/AbstractOperations.h>
  9. #include <LibJS/Runtime/Date.h>
  10. #include <LibJS/Runtime/Intl/AbstractOperations.h>
  11. #include <LibJS/Runtime/Temporal/AbstractOperations.h>
  12. #include <LibJS/Runtime/Temporal/ISO8601.h>
  13. #include <LibJS/Runtime/Temporal/Instant.h>
  14. #include <LibJS/Runtime/Temporal/PlainDateTime.h>
  15. #include <LibJS/Runtime/Temporal/TimeZone.h>
  16. #include <LibJS/Runtime/VM.h>
  17. namespace JS::Temporal {
  18. // 11.1.5 FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] ), https://tc39.es/proposal-temporal/#sec-temporal-formatoffsettimezoneidentifier
  19. String format_offset_time_zone_identifier(i64 offset_minutes, Optional<TimeStyle> style)
  20. {
  21. // 1. If offsetMinutes ≥ 0, let sign be the code unit 0x002B (PLUS SIGN); otherwise, let sign be the code unit 0x002D (HYPHEN-MINUS).
  22. auto sign = offset_minutes >= 0 ? '+' : '-';
  23. // 2. Let absoluteMinutes be abs(offsetMinutes).
  24. auto absolute_minutes = abs(offset_minutes);
  25. // 3. Let hour be floor(absoluteMinutes / 60).
  26. auto hour = static_cast<u8>(floor(static_cast<double>(absolute_minutes) / 60.0));
  27. // 4. Let minute be absoluteMinutes modulo 60.
  28. auto minute = static_cast<u8>(modulo(static_cast<double>(absolute_minutes), 60.0));
  29. // 5. Let timeString be FormatTimeString(hour, minute, 0, 0, MINUTE, style).
  30. auto time_string = format_time_string(hour, minute, 0, 0, SecondsStringPrecision::Minute {}, style);
  31. // 6. Return the string-concatenation of sign and timeString.
  32. return MUST(String::formatted("{}{}", sign, time_string));
  33. }
  34. // 11.1.8 ToTemporalTimeZoneIdentifier ( temporalTimeZoneLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaltimezoneidentifier
  35. ThrowCompletionOr<String> to_temporal_time_zone_identifier(VM& vm, Value temporal_time_zone_like)
  36. {
  37. // 1. If temporalTimeZoneLike is an Object, then
  38. if (temporal_time_zone_like.is_object()) {
  39. // FIXME: a. If temporalTimeZoneLike has an [[InitializedTemporalZonedDateTime]] internal slot, then
  40. // FIXME: i. Return temporalTimeZoneLike.[[TimeZone]].
  41. }
  42. // 2. If temporalTimeZoneLike is not a String, throw a TypeError exception.
  43. if (!temporal_time_zone_like.is_string())
  44. return vm.throw_completion<TypeError>(ErrorType::TemporalInvalidTimeZoneName, temporal_time_zone_like);
  45. return to_temporal_time_zone_identifier(vm, temporal_time_zone_like.as_string().utf8_string_view());
  46. }
  47. // 11.1.8 ToTemporalTimeZoneIdentifier ( temporalTimeZoneLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaltimezoneidentifier
  48. ThrowCompletionOr<String> to_temporal_time_zone_identifier(VM& vm, StringView temporal_time_zone_like)
  49. {
  50. // 3. Let parseResult be ? ParseTemporalTimeZoneString(temporalTimeZoneLike).
  51. auto parse_result = TRY(parse_temporal_time_zone_string(vm, temporal_time_zone_like));
  52. // 4. Let offsetMinutes be parseResult.[[OffsetMinutes]].
  53. // 5. If offsetMinutes is not empty, return FormatOffsetTimeZoneIdentifier(offsetMinutes).
  54. if (parse_result.offset_minutes.has_value())
  55. return format_offset_time_zone_identifier(*parse_result.offset_minutes);
  56. // 6. Let name be parseResult.[[Name]].
  57. // 7. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(name).
  58. auto time_zone_identifier_record = Intl::get_available_named_time_zone_identifier(*parse_result.name);
  59. // 8. If timeZoneIdentifierRecord is empty, throw a RangeError exception.
  60. if (!time_zone_identifier_record.has_value())
  61. return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidTimeZoneName, temporal_time_zone_like);
  62. // 9. Return timeZoneIdentifierRecord.[[Identifier]].
  63. return time_zone_identifier_record->identifier;
  64. }
  65. // 11.1.11 GetEpochNanosecondsFor ( timeZone, isoDateTime, disambiguation ), https://tc39.es/proposal-temporal/#sec-temporal-getepochnanosecondsfor
  66. ThrowCompletionOr<Crypto::SignedBigInteger> get_epoch_nanoseconds_for(VM& vm, StringView time_zone, ISODateTime const& iso_date_time, Disambiguation disambiguation)
  67. {
  68. // 1. Let possibleEpochNs be ? GetPossibleEpochNanoseconds(timeZone, isoDateTime).
  69. auto possible_epoch_ns = TRY(get_possible_epoch_nanoseconds(vm, time_zone, iso_date_time));
  70. // 2. Return ? DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation).
  71. return TRY(disambiguate_possible_epoch_nanoseconds(vm, move(possible_epoch_ns), time_zone, iso_date_time, disambiguation));
  72. }
  73. // 11.1.12 DisambiguatePossibleEpochNanoseconds ( possibleEpochNs, timeZone, isoDateTime, disambiguation ), https://tc39.es/proposal-temporal/#sec-temporal-disambiguatepossibleepochnanoseconds
  74. ThrowCompletionOr<Crypto::SignedBigInteger> disambiguate_possible_epoch_nanoseconds(VM& vm, Vector<Crypto::SignedBigInteger> possible_epoch_ns, StringView time_zone, ISODateTime const& iso_date_time, Disambiguation disambiguation)
  75. {
  76. // 1. Let n be possibleEpochNs's length.
  77. auto n = possible_epoch_ns.size();
  78. // 2. If n = 1, then
  79. if (n == 1) {
  80. // a. Return possibleEpochNs[0].
  81. return move(possible_epoch_ns[0]);
  82. }
  83. // 3. If n ≠ 0, then
  84. if (n != 0) {
  85. // a. If disambiguation is EARLIER or COMPATIBLE, then
  86. if (disambiguation == Disambiguation::Earlier || disambiguation == Disambiguation::Compatible) {
  87. // i. Return possibleEpochNs[0].
  88. return move(possible_epoch_ns[0]);
  89. }
  90. // b. If disambiguation is LATER, then
  91. if (disambiguation == Disambiguation::Later) {
  92. // i. Return possibleEpochNs[n - 1].
  93. return move(possible_epoch_ns[n - 1]);
  94. }
  95. // c. Assert: disambiguation is REJECT.
  96. VERIFY(disambiguation == Disambiguation::Reject);
  97. // d. Throw a RangeError exception.
  98. return vm.throw_completion<RangeError>(ErrorType::TemporalDisambiguatePossibleEpochNSRejectMoreThanOne);
  99. }
  100. // 4. Assert: n = 0.
  101. VERIFY(n == 0);
  102. // 5. If disambiguation is REJECT, then
  103. if (disambiguation == Disambiguation::Reject) {
  104. // a. Throw a RangeError exception.
  105. return vm.throw_completion<RangeError>(ErrorType::TemporalDisambiguatePossibleEpochNSRejectZero);
  106. }
  107. // FIXME: GetNamedTimeZoneEpochNanoseconds currently does not produce zero instants.
  108. (void)time_zone;
  109. (void)iso_date_time;
  110. TODO();
  111. }
  112. // 11.1.13 GetPossibleEpochNanoseconds ( timeZone, isoDateTime ), https://tc39.es/proposal-temporal/#sec-temporal-getpossibleepochnanoseconds
  113. ThrowCompletionOr<Vector<Crypto::SignedBigInteger>> get_possible_epoch_nanoseconds(VM& vm, StringView time_zone, ISODateTime const& iso_date_time)
  114. {
  115. Vector<Crypto::SignedBigInteger> possible_epoch_nanoseconds;
  116. // 1. Let parseResult be ! ParseTimeZoneIdentifier(timeZone).
  117. auto parse_result = parse_time_zone_identifier(time_zone);
  118. // 2. If parseResult.[[OffsetMinutes]] is not empty, then
  119. if (parse_result.offset_minutes.has_value()) {
  120. // a. Let balanced be BalanceISODateTime(isoDateTime.[[ISODate]].[[Year]], isoDateTime.[[ISODate]].[[Month]], isoDateTime.[[ISODate]].[[Day]], isoDateTime.[[Time]].[[Hour]], isoDateTime.[[Time]].[[Minute]] - parseResult.[[OffsetMinutes]], isoDateTime.[[Time]].[[Second]], isoDateTime.[[Time]].[[Millisecond]], isoDateTime.[[Time]].[[Microsecond]], isoDateTime.[[Time]].[[Nanosecond]]).
  121. auto balanced = balance_iso_date_time(
  122. iso_date_time.iso_date.year,
  123. iso_date_time.iso_date.month,
  124. iso_date_time.iso_date.day,
  125. iso_date_time.time.hour,
  126. static_cast<double>(iso_date_time.time.minute) - static_cast<double>(*parse_result.offset_minutes),
  127. iso_date_time.time.second,
  128. iso_date_time.time.millisecond,
  129. iso_date_time.time.microsecond,
  130. iso_date_time.time.nanosecond);
  131. // b. Perform ? CheckISODaysRange(balanced.[[ISODate]]).
  132. TRY(check_iso_days_range(vm, balanced.iso_date));
  133. // c. Let epochNanoseconds be GetUTCEpochNanoseconds(balanced).
  134. auto epoch_nanoseconds = get_utc_epoch_nanoseconds(balanced);
  135. // d. Let possibleEpochNanoseconds be « epochNanoseconds ».
  136. possible_epoch_nanoseconds.append(move(epoch_nanoseconds));
  137. }
  138. // 3. Else,
  139. else {
  140. // a. Perform ? CheckISODaysRange(isoDateTime.[[ISODate]]).
  141. TRY(check_iso_days_range(vm, iso_date_time.iso_date));
  142. // b. Let possibleEpochNanoseconds be GetNamedTimeZoneEpochNanoseconds(parseResult.[[Name]], isoDateTime).
  143. possible_epoch_nanoseconds = get_named_time_zone_epoch_nanoseconds(
  144. *parse_result.name,
  145. iso_date_time.iso_date.year,
  146. iso_date_time.iso_date.month,
  147. iso_date_time.iso_date.day,
  148. iso_date_time.time.hour,
  149. iso_date_time.time.minute,
  150. iso_date_time.time.second,
  151. iso_date_time.time.millisecond,
  152. iso_date_time.time.microsecond,
  153. iso_date_time.time.nanosecond);
  154. }
  155. // 4. For each value epochNanoseconds in possibleEpochNanoseconds, do
  156. for (auto const& epoch_nanoseconds : possible_epoch_nanoseconds) {
  157. // a. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception.
  158. if (!is_valid_epoch_nanoseconds(epoch_nanoseconds))
  159. return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidEpochNanoseconds);
  160. }
  161. // 5. Return possibleEpochNanoseconds.
  162. return possible_epoch_nanoseconds;
  163. }
  164. // 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier
  165. ThrowCompletionOr<TimeZone> parse_time_zone_identifier(VM& vm, StringView identifier)
  166. {
  167. // 1. Let parseResult be ParseText(StringToCodePoints(identifier), TimeZoneIdentifier).
  168. auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, identifier);
  169. // 2. If parseResult is a List of errors, throw a RangeError exception.
  170. if (!parse_result.has_value())
  171. return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidTimeZoneString, identifier);
  172. return parse_time_zone_identifier(*parse_result);
  173. }
  174. // 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier
  175. TimeZone parse_time_zone_identifier(StringView identifier)
  176. {
  177. // OPTIMIZATION: Some callers can assume that parsing will succeed.
  178. // 1. Let parseResult be ParseText(StringToCodePoints(identifier), TimeZoneIdentifier).
  179. auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, identifier);
  180. VERIFY(parse_result.has_value());
  181. return parse_time_zone_identifier(*parse_result);
  182. }
  183. // 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier
  184. TimeZone parse_time_zone_identifier(ParseResult const& parse_result)
  185. {
  186. // OPTIMIZATION: Some callers will have already parsed and validated the time zone identifier.
  187. // 3. If parseResult contains a TimeZoneIANAName Parse Node, then
  188. if (parse_result.time_zone_iana_name.has_value()) {
  189. // a. Let name be the source text matched by the TimeZoneIANAName Parse Node contained within parseResult.
  190. // b. NOTE: name is syntactically valid, but does not necessarily conform to IANA Time Zone Database naming
  191. // guidelines or correspond with an available named time zone identifier.
  192. // c. Return the Record { [[Name]]: CodePointsToString(name), [[OffsetMinutes]]: empty }.
  193. return TimeZone { .name = String::from_utf8_without_validation(parse_result.time_zone_iana_name->bytes()), .offset_minutes = {} };
  194. }
  195. // 4. Else,
  196. else {
  197. // a. Assert: parseResult contains a UTCOffset[~SubMinutePrecision] Parse Node.
  198. VERIFY(parse_result.time_zone_offset.has_value());
  199. // b. Let offsetString be the source text matched by the UTCOffset[~SubMinutePrecision] Parse Node contained within parseResult.
  200. // c. Let offsetNanoseconds be ! ParseDateTimeUTCOffset(offsetString).
  201. auto offset_nanoseconds = parse_date_time_utc_offset(parse_result.time_zone_offset->source_text);
  202. // d. Let offsetMinutes be offsetNanoseconds / (60 × 10**9).
  203. auto offset_minutes = offset_nanoseconds / 60'000'000'000;
  204. // e. Assert: offsetMinutes is an integer.
  205. VERIFY(trunc(offset_minutes) == offset_minutes);
  206. // f. Return the Record { [[Name]]: empty, [[OffsetMinutes]]: offsetMinutes }.
  207. return TimeZone { .name = {}, .offset_minutes = static_cast<i64>(offset_minutes) };
  208. }
  209. }
  210. }