summaryrefslogtreecommitdiffstats
path: root/js/src/builtin/intl/DateTimeFormat.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/builtin/intl/DateTimeFormat.cpp')
-rw-r--r--js/src/builtin/intl/DateTimeFormat.cpp1884
1 files changed, 1884 insertions, 0 deletions
diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp
new file mode 100644
index 0000000000..678c90abb5
--- /dev/null
+++ b/js/src/builtin/intl/DateTimeFormat.cpp
@@ -0,0 +1,1884 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Intl.DateTimeFormat implementation. */
+
+#include "builtin/intl/DateTimeFormat.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Range.h"
+#include "mozilla/Span.h"
+
+#include "jsfriendapi.h"
+
+#include "builtin/Array.h"
+#include "builtin/intl/CommonFunctions.h"
+#include "builtin/intl/LanguageTag.h"
+#include "builtin/intl/ScopedICUObject.h"
+#include "builtin/intl/SharedIntlData.h"
+#include "builtin/intl/TimeZoneDataGenerated.h"
+#include "gc/FreeOp.h"
+#include "js/CharacterEncoding.h"
+#include "js/Date.h"
+#include "js/experimental/Intl.h" // JS::AddMozDateTimeFormatConstructor
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
+#include "js/GCAPI.h"
+#include "js/PropertySpec.h"
+#include "js/StableStringChars.h"
+#include "unicode/ucal.h"
+#include "unicode/udat.h"
+#include "unicode/udateintervalformat.h"
+#include "unicode/udatpg.h"
+#include "unicode/uenum.h"
+#include "unicode/ufieldpositer.h"
+#include "unicode/uloc.h"
+#include "unicode/utypes.h"
+#include "vm/DateTime.h"
+#include "vm/GlobalObject.h"
+#include "vm/JSContext.h"
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/Runtime.h"
+
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+using JS::AutoStableStringChars;
+using JS::ClippedTime;
+using JS::TimeClip;
+
+using js::intl::CallICU;
+using js::intl::DateTimeFormatOptions;
+using js::intl::IcuLocale;
+using js::intl::INITIAL_CHAR_BUFFER_SIZE;
+using js::intl::SharedIntlData;
+using js::intl::StringsAreEqual;
+
+const JSClassOps DateTimeFormatObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ DateTimeFormatObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // hasInstance
+ nullptr, // construct
+ nullptr, // trace
+};
+
+const JSClass DateTimeFormatObject::class_ = {
+ "Intl.DateTimeFormat",
+ JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) |
+ JSCLASS_HAS_CACHED_PROTO(JSProto_DateTimeFormat) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &DateTimeFormatObject::classOps_, &DateTimeFormatObject::classSpec_};
+
+const JSClass& DateTimeFormatObject::protoClass_ = PlainObject::class_;
+
+static bool dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().DateTimeFormat);
+ return true;
+}
+
+static const JSFunctionSpec dateTimeFormat_static_methods[] = {
+ JS_SELF_HOSTED_FN("supportedLocalesOf",
+ "Intl_DateTimeFormat_supportedLocalesOf", 1, 0),
+ JS_FS_END};
+
+static const JSFunctionSpec dateTimeFormat_methods[] = {
+ JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions",
+ 0, 0),
+ JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 1,
+ 0),
+#ifdef NIGHTLY_BUILD
+# ifndef U_HIDE_DRAFT_API
+ JS_SELF_HOSTED_FN("formatRange", "Intl_DateTimeFormat_formatRange", 2, 0),
+ JS_SELF_HOSTED_FN("formatRangeToParts",
+ "Intl_DateTimeFormat_formatRangeToParts", 2, 0),
+# endif
+#endif
+ JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0),
+ JS_FS_END};
+
+static const JSPropertySpec dateTimeFormat_properties[] = {
+ JS_SELF_HOSTED_GET("format", "$Intl_DateTimeFormat_format_get", 0),
+ JS_STRING_SYM_PS(toStringTag, "Intl.DateTimeFormat", JSPROP_READONLY),
+ JS_PS_END};
+
+static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp);
+
+const ClassSpec DateTimeFormatObject::classSpec_ = {
+ GenericCreateConstructor<DateTimeFormat, 0, gc::AllocKind::FUNCTION>,
+ GenericCreatePrototype<DateTimeFormatObject>,
+ dateTimeFormat_static_methods,
+ nullptr,
+ dateTimeFormat_methods,
+ dateTimeFormat_properties,
+ nullptr,
+ ClassSpec::DontDefineConstructor};
+
+/**
+ * 12.2.1 Intl.DateTimeFormat([ locales [, options]])
+ *
+ * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
+ */
+static bool DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct,
+ DateTimeFormatOptions dtfOptions) {
+ // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
+
+ // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
+ JSProtoKey protoKey = dtfOptions == DateTimeFormatOptions::Standard
+ ? JSProto_DateTimeFormat
+ : JSProto_Null;
+ RootedObject proto(cx);
+ if (!GetPrototypeFromBuiltinConstructor(cx, args, protoKey, &proto)) {
+ return false;
+ }
+
+ Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
+ dateTimeFormat = NewObjectWithClassProto<DateTimeFormatObject>(cx, proto);
+ if (!dateTimeFormat) {
+ return false;
+ }
+
+ RootedValue thisValue(
+ cx, construct ? ObjectValue(*dateTimeFormat) : args.thisv());
+ HandleValue locales = args.get(0);
+ HandleValue options = args.get(1);
+
+ // Step 3.
+ return intl::LegacyInitializeObject(
+ cx, dateTimeFormat, cx->names().InitializeDateTimeFormat, thisValue,
+ locales, options, dtfOptions, args.rval());
+}
+
+static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ return DateTimeFormat(cx, args, args.isConstructing(),
+ DateTimeFormatOptions::Standard);
+}
+
+static bool MozDateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // Don't allow to call mozIntl.DateTimeFormat as a function. That way we
+ // don't need to worry how to handle the legacy initialization semantics
+ // when applied on mozIntl.DateTimeFormat.
+ if (!ThrowIfNotConstructing(cx, args, "mozIntl.DateTimeFormat")) {
+ return false;
+ }
+
+ return DateTimeFormat(cx, args, true,
+ DateTimeFormatOptions::EnableMozExtensions);
+}
+
+bool js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 2);
+ MOZ_ASSERT(!args.isConstructing());
+ // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it
+ // cannot be used with "new", but it still has to be treated as a
+ // constructor.
+ return DateTimeFormat(cx, args, true, DateTimeFormatOptions::Standard);
+}
+
+void js::DateTimeFormatObject::finalize(JSFreeOp* fop, JSObject* obj) {
+ MOZ_ASSERT(fop->onMainThread());
+
+ auto* dateTimeFormat = &obj->as<DateTimeFormatObject>();
+ UDateFormat* df = dateTimeFormat->getDateFormat();
+ UDateIntervalFormat* dif = dateTimeFormat->getDateIntervalFormat();
+
+ if (df) {
+ intl::RemoveICUCellMemory(
+ fop, obj, DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
+
+ udat_close(df);
+ }
+
+ if (dif) {
+ intl::RemoveICUCellMemory(
+ fop, obj, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
+
+ udtitvfmt_close(dif);
+ }
+}
+
+bool JS::AddMozDateTimeFormatConstructor(JSContext* cx,
+ JS::Handle<JSObject*> intl) {
+ RootedObject ctor(
+ cx, GlobalObject::createConstructor(cx, MozDateTimeFormat,
+ cx->names().DateTimeFormat, 0));
+ if (!ctor) {
+ return false;
+ }
+
+ RootedObject proto(
+ cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global()));
+ if (!proto) {
+ return false;
+ }
+
+ if (!LinkConstructorAndPrototype(cx, ctor, proto)) {
+ return false;
+ }
+
+ // 12.3.2
+ if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) {
+ return false;
+ }
+
+ // 12.4.4 and 12.4.5
+ if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) {
+ return false;
+ }
+
+ // 12.4.2 and 12.4.3
+ if (!JS_DefineProperties(cx, proto, dateTimeFormat_properties)) {
+ return false;
+ }
+
+ RootedValue ctorValue(cx, ObjectValue(*ctor));
+ return DefineDataProperty(cx, intl, cx->names().DateTimeFormat, ctorValue, 0);
+}
+
+static bool DefaultCalendar(JSContext* cx, const UniqueChars& locale,
+ MutableHandleValue rval) {
+ UErrorCode status = U_ZERO_ERROR;
+ UCalendar* cal = ucal_open(nullptr, 0, locale.get(), UCAL_DEFAULT, &status);
+
+ // This correctly handles nullptr |cal| when opening failed.
+ ScopedICUObject<UCalendar, ucal_close> closeCalendar(cal);
+
+ const char* calendar = ucal_getType(cal, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ // ICU returns old-style keyword values; map them to BCP 47 equivalents
+ calendar = uloc_toUnicodeLocaleType("ca", calendar);
+ if (!calendar) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ JSString* str = NewStringCopyZ<CanGC>(cx, calendar);
+ if (!str) {
+ return false;
+ }
+
+ rval.setString(str);
+ return true;
+}
+
+struct CalendarAlias {
+ const char* const calendar;
+ const char* const alias;
+};
+
+const CalendarAlias calendarAliases[] = {{"islamic-civil", "islamicc"},
+ {"ethioaa", "ethiopic-amete-alem"}};
+
+bool js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
+ if (!locale) {
+ return false;
+ }
+
+ RootedObject calendars(cx, NewDenseEmptyArray(cx));
+ if (!calendars) {
+ return false;
+ }
+
+ // We need the default calendar for the locale as the first result.
+ RootedValue defaultCalendar(cx);
+ if (!DefaultCalendar(cx, locale, &defaultCalendar)) {
+ return false;
+ }
+
+ if (!NewbornArrayPush(cx, calendars, defaultCalendar)) {
+ return false;
+ }
+
+ // Now get the calendars that "would make a difference", i.e., not the
+ // default.
+ UErrorCode status = U_ZERO_ERROR;
+ UEnumeration* values =
+ ucal_getKeywordValuesForLocale("ca", locale.get(), false, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ ScopedICUObject<UEnumeration, uenum_close> toClose(values);
+
+ uint32_t count = uenum_count(values, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ for (; count > 0; count--) {
+ const char* calendar = uenum_next(values, nullptr, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ // ICU returns old-style keyword values; map them to BCP 47 equivalents
+ calendar = uloc_toUnicodeLocaleType("ca", calendar);
+ if (!calendar) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ JSString* jscalendar = NewStringCopyZ<CanGC>(cx, calendar);
+ if (!jscalendar) {
+ return false;
+ }
+ if (!NewbornArrayPush(cx, calendars, StringValue(jscalendar))) {
+ return false;
+ }
+
+ // ICU doesn't return calendar aliases, append them here.
+ for (const auto& calendarAlias : calendarAliases) {
+ if (StringsAreEqual(calendar, calendarAlias.calendar)) {
+ JSString* jscalendar = NewStringCopyZ<CanGC>(cx, calendarAlias.alias);
+ if (!jscalendar) {
+ return false;
+ }
+ if (!NewbornArrayPush(cx, calendars, StringValue(jscalendar))) {
+ return false;
+ }
+ }
+ }
+ }
+
+ args.rval().setObject(*calendars);
+ return true;
+}
+
+bool js::intl_defaultCalendar(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
+ if (!locale) {
+ return false;
+ }
+
+ return DefaultCalendar(cx, locale, args.rval());
+}
+
+bool js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+
+ RootedString timeZone(cx, args[0].toString());
+ RootedAtom validatedTimeZone(cx);
+ if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) {
+ return false;
+ }
+
+ if (validatedTimeZone) {
+ cx->markAtom(validatedTimeZone);
+ args.rval().setString(validatedTimeZone);
+ } else {
+ args.rval().setNull();
+ }
+
+ return true;
+}
+
+bool js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+
+ // Some time zone names are canonicalized differently by ICU -- handle
+ // those first:
+ RootedString timeZone(cx, args[0].toString());
+ RootedAtom ianaTimeZone(cx);
+ if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(
+ cx, timeZone, &ianaTimeZone)) {
+ return false;
+ }
+
+ if (ianaTimeZone) {
+ cx->markAtom(ianaTimeZone);
+ args.rval().setString(ianaTimeZone);
+ return true;
+ }
+
+ AutoStableStringChars stableChars(cx);
+ if (!stableChars.initTwoByte(cx, timeZone)) {
+ return false;
+ }
+
+ mozilla::Range<const char16_t> tzchars = stableChars.twoByteRange();
+
+ JSString* str = CallICU(cx, [&tzchars](UChar* chars, uint32_t size,
+ UErrorCode* status) {
+ return ucal_getCanonicalTimeZoneID(tzchars.begin().get(), tzchars.length(),
+ chars, size, nullptr, status);
+ });
+ if (!str) {
+ return false;
+ }
+
+ args.rval().setString(str);
+ return true;
+}
+
+bool js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 0);
+
+ // The current default might be stale, because JS::ResetTimeZone() doesn't
+ // immediately update ICU's default time zone. So perform an update if
+ // needed.
+ js::ResyncICUDefaultTimeZone();
+
+ JSString* str = CallICU(cx, ucal_getDefaultTimeZone);
+ if (!str) {
+ return false;
+ }
+
+ args.rval().setString(str);
+ return true;
+}
+
+bool js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 0);
+
+ UErrorCode status = U_ZERO_ERROR;
+ const UChar* uTimeZone = nullptr;
+ int32_t uTimeZoneLength = 0;
+ const char* rootLocale = "";
+ UCalendar* cal =
+ ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ ScopedICUObject<UCalendar, ucal_close> toClose(cal);
+
+ int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ args.rval().setInt32(offset);
+ return true;
+}
+
+bool js::intl_isDefaultTimeZone(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString() || args[0].isUndefined());
+
+ // |undefined| is the default value when the Intl runtime caches haven't
+ // yet been initialized. Handle it the same way as a cache miss.
+ if (args[0].isUndefined()) {
+ args.rval().setBoolean(false);
+ return true;
+ }
+
+ // The current default might be stale, because JS::ResetTimeZone() doesn't
+ // immediately update ICU's default time zone. So perform an update if
+ // needed.
+ js::ResyncICUDefaultTimeZone();
+
+ Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE> chars(cx);
+ MOZ_ALWAYS_TRUE(chars.resize(INITIAL_CHAR_BUFFER_SIZE));
+
+ int32_t size = CallICU(cx, ucal_getDefaultTimeZone, chars);
+ if (size < 0) {
+ return false;
+ }
+
+ JSLinearString* str = args[0].toString()->ensureLinear(cx);
+ if (!str) {
+ return false;
+ }
+
+ bool equals;
+ if (str->length() == size_t(size)) {
+ JS::AutoCheckCannotGC nogc;
+ equals =
+ str->hasLatin1Chars()
+ ? EqualChars(str->latin1Chars(nogc), chars.begin(), str->length())
+ : EqualChars(str->twoByteChars(nogc), chars.begin(), str->length());
+ } else {
+ equals = false;
+ }
+
+ args.rval().setBoolean(equals);
+ return true;
+}
+
+enum class HourCycle {
+ // 12 hour cycle, from 0 to 11.
+ H11,
+
+ // 12 hour cycle, from 1 to 12.
+ H12,
+
+ // 24 hour cycle, from 0 to 23.
+ H23,
+
+ // 24 hour cycle, from 1 to 24.
+ H24
+};
+
+static bool IsHour12(HourCycle hc) {
+ return hc == HourCycle::H11 || hc == HourCycle::H12;
+}
+
+static char16_t HourSymbol(HourCycle hc) {
+ switch (hc) {
+ case HourCycle::H11:
+ return 'K';
+ case HourCycle::H12:
+ return 'h';
+ case HourCycle::H23:
+ return 'H';
+ case HourCycle::H24:
+ return 'k';
+ }
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("unexpected hour cycle");
+}
+
+/**
+ * Parse a pattern according to the format specified in
+ * <https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns>.
+ */
+template <typename CharT>
+class PatternIterator {
+ CharT* iter_;
+ const CharT* const end_;
+
+ public:
+ explicit PatternIterator(mozilla::Span<CharT> pattern)
+ : iter_(pattern.data()), end_(pattern.data() + pattern.size()) {}
+
+ CharT* next() {
+ MOZ_ASSERT(iter_ != nullptr);
+
+ bool inQuote = false;
+ while (iter_ < end_) {
+ CharT* cur = iter_++;
+ if (*cur == '\'') {
+ inQuote = !inQuote;
+ } else if (!inQuote) {
+ return cur;
+ }
+ }
+
+ iter_ = nullptr;
+ return nullptr;
+ }
+};
+
+/**
+ * Return the hour cycle for the given option string.
+ */
+static HourCycle HourCycleFromOption(JSLinearString* str) {
+ if (StringEqualsLiteral(str, "h11")) {
+ return HourCycle::H11;
+ }
+ if (StringEqualsLiteral(str, "h12")) {
+ return HourCycle::H12;
+ }
+ if (StringEqualsLiteral(str, "h23")) {
+ return HourCycle::H23;
+ }
+ MOZ_ASSERT(StringEqualsLiteral(str, "h24"));
+ return HourCycle::H24;
+}
+
+/**
+ * Return the hour cycle used in the input pattern or Nothing if none was found.
+ */
+template <typename CharT>
+static mozilla::Maybe<HourCycle> HourCycleFromPattern(
+ mozilla::Span<const CharT> pattern) {
+ PatternIterator<const CharT> iter(pattern);
+ while (const auto* ptr = iter.next()) {
+ switch (*ptr) {
+ case 'K':
+ return mozilla::Some(HourCycle::H11);
+ case 'h':
+ return mozilla::Some(HourCycle::H12);
+ case 'H':
+ return mozilla::Some(HourCycle::H23);
+ case 'k':
+ return mozilla::Some(HourCycle::H24);
+ }
+ }
+ return mozilla::Nothing();
+}
+
+/**
+ * Replaces all hour pattern characters in |patternOrSkeleton| to use the
+ * matching hour representation for |hourCycle|.
+ */
+static void ReplaceHourSymbol(mozilla::Span<char16_t> patternOrSkeleton,
+ HourCycle hc) {
+ char16_t replacement = HourSymbol(hc);
+ PatternIterator<char16_t> iter(patternOrSkeleton);
+ while (auto* ptr = iter.next()) {
+ char16_t ch = *ptr;
+ if (ch == 'K' || ch == 'h' || ch == 'H' || ch == 'k' || ch == 'j') {
+ *ptr = replacement;
+ }
+ }
+}
+
+bool js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+ MOZ_ASSERT(args[0].isString());
+ MOZ_ASSERT(args[1].isString());
+ MOZ_ASSERT(args[2].isString() || args[2].isUndefined());
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
+ if (!locale) {
+ return false;
+ }
+
+ AutoStableStringChars skeleton(cx);
+ if (!skeleton.initTwoByte(cx, args[1].toString())) {
+ return false;
+ }
+
+ mozilla::Maybe<HourCycle> hourCycle;
+ if (args[2].isString()) {
+ JSLinearString* hourCycleStr = args[2].toString()->ensureLinear(cx);
+ if (!hourCycleStr) {
+ return false;
+ }
+
+ hourCycle.emplace(HourCycleFromOption(hourCycleStr));
+ }
+
+ mozilla::Range<const char16_t> skelChars = skeleton.twoByteRange();
+
+ SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+ UDateTimePatternGenerator* gen =
+ sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
+ if (!gen) {
+ return false;
+ }
+
+ Vector<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> pattern(cx);
+ MOZ_ALWAYS_TRUE(pattern.resize(intl::INITIAL_CHAR_BUFFER_SIZE));
+
+ int32_t patternSize = CallICU(
+ cx,
+ [gen, &skelChars](UChar* chars, uint32_t size, UErrorCode* status) {
+ return udatpg_getBestPattern(gen, skelChars.begin().get(),
+ skelChars.length(), chars, size, status);
+ },
+ pattern);
+ if (patternSize < 0) {
+ return false;
+ }
+ pattern.shrinkTo(size_t(patternSize));
+
+ // If the hourCycle option was set, adjust the resolved pattern to use the
+ // requested hour cycle representation.
+ if (hourCycle) {
+ ReplaceHourSymbol(pattern, hourCycle.value());
+ }
+
+ JSString* str = NewStringCopyN<CanGC>(cx, pattern.begin(), pattern.length());
+ if (!str) {
+ return false;
+ }
+ args.rval().setString(str);
+ return true;
+}
+
+/**
+ * Find a matching pattern using the requested hour-12 options.
+ *
+ * This function is needed to work around the following two issues.
+ * - https://unicode-org.atlassian.net/browse/ICU-21023
+ * - https://unicode-org.atlassian.net/browse/CLDR-13425
+ *
+ * We're currently using a relatively simple workaround, which doesn't give the
+ * most accurate results. For example:
+ *
+ * ```
+ * var dtf = new Intl.DateTimeFormat("en", {
+ * timeZone: "UTC",
+ * dateStyle: "long",
+ * timeStyle: "long",
+ * hourCycle: "h12",
+ * });
+ * print(dtf.format(new Date("2020-01-01T00:00Z")));
+ * ```
+ *
+ * Returns the pattern "MMMM d, y 'at' h:mm:ss a z", but when going through
+ * |udatpg_getSkeleton| and then |udatpg_getBestPattern| to find an equivalent
+ * pattern for "h23", we'll end up with the pattern "MMMM d, y, HH:mm:ss z", so
+ * the combinator element " 'at' " was lost in the process.
+ */
+template <size_t N>
+static bool FindPatternWithHourCycle(JSContext* cx, const char* locale,
+ Vector<char16_t, N>& pattern,
+ bool hour12) {
+ SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
+ UDateTimePatternGenerator* gen =
+ sharedIntlData.getDateTimePatternGenerator(cx, locale);
+ if (!gen) {
+ return false;
+ }
+
+ Vector<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> skeleton(cx);
+ MOZ_ALWAYS_TRUE(skeleton.resize(intl::INITIAL_CHAR_BUFFER_SIZE));
+
+ int32_t skeletonSize = CallICU(
+ cx,
+ [&pattern](UChar* chars, uint32_t size, UErrorCode* status) {
+ return udatpg_getSkeleton(nullptr, pattern.begin(), pattern.length(),
+ chars, size, status);
+ },
+ skeleton);
+ if (skeletonSize < 0) {
+ return false;
+ }
+ skeleton.shrinkTo(size_t(skeletonSize));
+
+ // Input skeletons don't differentiate between "K" and "h" resp. "k" and "H".
+ ReplaceHourSymbol(skeleton, hour12 ? HourCycle::H12 : HourCycle::H23);
+
+ MOZ_ALWAYS_TRUE(pattern.resize(N));
+
+ int32_t patternSize = CallICU(
+ cx,
+ [gen, &skeleton](UChar* chars, uint32_t size, UErrorCode* status) {
+ return udatpg_getBestPattern(gen, skeleton.begin(), skeleton.length(),
+ chars, size, status);
+ },
+ pattern);
+ if (patternSize < 0) {
+ return false;
+ }
+ pattern.shrinkTo(size_t(patternSize));
+
+ return true;
+}
+
+bool js::intl_patternForStyle(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 6);
+ MOZ_ASSERT(args[0].isString());
+ MOZ_ASSERT(args[1].isString() || args[1].isUndefined());
+ MOZ_ASSERT(args[2].isString() || args[2].isUndefined());
+ MOZ_ASSERT(args[3].isString());
+ MOZ_ASSERT(args[4].isBoolean() || args[4].isUndefined());
+ MOZ_ASSERT(args[5].isString() || args[5].isUndefined());
+
+ UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
+ if (!locale) {
+ return false;
+ }
+
+ auto toDateFormatStyle = [](JSLinearString* str) {
+ if (StringEqualsLiteral(str, "full")) {
+ return UDAT_FULL;
+ }
+ if (StringEqualsLiteral(str, "long")) {
+ return UDAT_LONG;
+ }
+ if (StringEqualsLiteral(str, "medium")) {
+ return UDAT_MEDIUM;
+ }
+ MOZ_ASSERT(StringEqualsLiteral(str, "short"));
+ return UDAT_SHORT;
+ };
+
+ UDateFormatStyle dateStyle = UDAT_NONE;
+ if (args[1].isString()) {
+ JSLinearString* dateStyleStr = args[1].toString()->ensureLinear(cx);
+ if (!dateStyleStr) {
+ return false;
+ }
+
+ dateStyle = toDateFormatStyle(dateStyleStr);
+ }
+
+ UDateFormatStyle timeStyle = UDAT_NONE;
+ if (args[2].isString()) {
+ JSLinearString* timeStyleStr = args[2].toString()->ensureLinear(cx);
+ if (!timeStyleStr) {
+ return false;
+ }
+
+ timeStyle = toDateFormatStyle(timeStyleStr);
+ }
+
+ AutoStableStringChars timeZone(cx);
+ if (!timeZone.initTwoByte(cx, args[3].toString())) {
+ return false;
+ }
+
+ mozilla::Maybe<bool> hour12;
+ if (args[4].isBoolean()) {
+ hour12.emplace(args[4].toBoolean());
+ }
+
+ mozilla::Maybe<HourCycle> hourCycle;
+ if (args[5].isString()) {
+ JSLinearString* hourCycleStr = args[5].toString()->ensureLinear(cx);
+ if (!hourCycleStr) {
+ return false;
+ }
+
+ hourCycle.emplace(HourCycleFromOption(hourCycleStr));
+ }
+
+ mozilla::Range<const char16_t> timeZoneChars = timeZone.twoByteRange();
+
+ UErrorCode status = U_ZERO_ERROR;
+ UDateFormat* df = udat_open(timeStyle, dateStyle, IcuLocale(locale.get()),
+ timeZoneChars.begin().get(),
+ timeZoneChars.length(), nullptr, -1, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ ScopedICUObject<UDateFormat, udat_close> toClose(df);
+
+ Vector<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> pattern(cx);
+ MOZ_ALWAYS_TRUE(pattern.resize(intl::INITIAL_CHAR_BUFFER_SIZE));
+
+ int32_t patternSize = CallICU(
+ cx,
+ [df](UChar* chars, uint32_t size, UErrorCode* status) {
+ return udat_toPattern(df, false, chars, size, status);
+ },
+ pattern);
+ if (patternSize < 0) {
+ return false;
+ }
+ pattern.shrinkTo(size_t(patternSize));
+
+ // If a specific hour cycle was requested and this hour cycle doesn't match
+ // the hour cycle used in the resolved pattern, find an equivalent pattern
+ // with the correct hour cycle.
+ if (timeStyle != UDAT_NONE && (hour12 || hourCycle)) {
+ if (auto hcPattern = HourCycleFromPattern<char16_t>(pattern)) {
+ bool wantHour12 = hour12 ? hour12.value() : IsHour12(hourCycle.value());
+ if (wantHour12 != IsHour12(hcPattern.value())) {
+ if (!FindPatternWithHourCycle(cx, locale.get(), pattern, wantHour12)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ // If the hourCycle option was set, adjust the resolved pattern to use the
+ // requested hour cycle representation.
+ if (hourCycle) {
+ ReplaceHourSymbol(pattern, hourCycle.value());
+ }
+
+ JSString* str = NewStringCopyN<CanGC>(cx, pattern.begin(), pattern.length());
+ if (!str) {
+ return false;
+ }
+ args.rval().setString(str);
+ return true;
+}
+
+bool js::intl_skeletonForPattern(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 1);
+ MOZ_ASSERT(args[0].isString());
+
+ AutoStableStringChars pattern(cx);
+ if (!pattern.initTwoByte(cx, args[0].toString())) {
+ return false;
+ }
+ mozilla::Range<const char16_t> patternChars = pattern.twoByteRange();
+
+ JSString* skeleton = CallICU(
+ cx, [&patternChars](char16_t* chars, int32_t size, UErrorCode* status) {
+ return udatpg_getSkeleton(nullptr, patternChars.begin().get(),
+ patternChars.length(), chars, size, status);
+ });
+ if (!skeleton) {
+ return false;
+ }
+
+ args.rval().setString(skeleton);
+ return true;
+}
+
+static UniqueChars DateTimeFormatLocale(
+ JSContext* cx, HandleObject internals,
+ mozilla::Maybe<HourCycle> hourCycle = mozilla::Nothing()) {
+ RootedValue value(cx);
+ if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
+ return nullptr;
+ }
+
+ // ICU expects calendar, numberingSystem, and hourCycle as Unicode locale
+ // extensions on locale.
+
+ intl::LanguageTag tag(cx);
+ {
+ JSLinearString* locale = value.toString()->ensureLinear(cx);
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!intl::LanguageTagParser::parse(cx, locale, tag)) {
+ return nullptr;
+ }
+ }
+
+ JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
+
+ if (!GetProperty(cx, internals, internals, cx->names().calendar, &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* calendar = value.toString()->ensureLinear(cx);
+ if (!calendar) {
+ return nullptr;
+ }
+
+ if (!keywords.emplaceBack("ca", calendar)) {
+ return nullptr;
+ }
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
+ &value)) {
+ return nullptr;
+ }
+
+ {
+ JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
+ if (!numberingSystem) {
+ return nullptr;
+ }
+
+ if (!keywords.emplaceBack("nu", numberingSystem)) {
+ return nullptr;
+ }
+ }
+
+ if (hourCycle) {
+ JSAtom* hourCycleStr;
+ switch (*hourCycle) {
+ case HourCycle::H11:
+ hourCycleStr = cx->names().h11;
+ break;
+ case HourCycle::H12:
+ hourCycleStr = cx->names().h12;
+ break;
+ case HourCycle::H23:
+ hourCycleStr = cx->names().h23;
+ break;
+ case HourCycle::H24:
+ hourCycleStr = cx->names().h24;
+ break;
+ }
+
+ if (!keywords.emplaceBack("hc", hourCycleStr)) {
+ return nullptr;
+ }
+ }
+
+ // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of
+ // the Unicode extension subtag. We're then relying on ICU to follow RFC
+ // 6067, which states that any trailing keywords using the same key
+ // should be ignored.
+ if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
+ return nullptr;
+ }
+
+ return tag.toStringZ(cx);
+}
+
+/**
+ * Returns a new UDateFormat with the locale and date-time formatting options
+ * of the given DateTimeFormat.
+ */
+static UDateFormat* NewUDateFormat(
+ JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) {
+ RootedValue value(cx);
+
+ RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat));
+ if (!internals) {
+ return nullptr;
+ }
+
+ UniqueChars locale = DateTimeFormatLocale(cx, internals);
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) {
+ return nullptr;
+ }
+
+ AutoStableStringChars timeZone(cx);
+ if (!timeZone.initTwoByte(cx, value.toString())) {
+ return nullptr;
+ }
+
+ mozilla::Range<const char16_t> timeZoneChars = timeZone.twoByteRange();
+
+ if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) {
+ return nullptr;
+ }
+
+ AutoStableStringChars pattern(cx);
+ if (!pattern.initTwoByte(cx, value.toString())) {
+ return nullptr;
+ }
+
+ mozilla::Range<const char16_t> patternChars = pattern.twoByteRange();
+
+ UErrorCode status = U_ZERO_ERROR;
+ UDateFormat* df =
+ udat_open(UDAT_PATTERN, UDAT_PATTERN, IcuLocale(locale.get()),
+ timeZoneChars.begin().get(), timeZoneChars.length(),
+ patternChars.begin().get(), patternChars.length(), &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return nullptr;
+ }
+
+ // ECMAScript requires the Gregorian calendar to be used from the beginning
+ // of ECMAScript time.
+ UCalendar* cal = const_cast<UCalendar*>(udat_getCalendar(df));
+ ucal_setGregorianChange(cal, StartOfTime, &status);
+
+ // An error here means the calendar is not Gregorian, so we don't care.
+
+ return df;
+}
+
+static bool intl_FormatDateTime(JSContext* cx, const UDateFormat* df,
+ ClippedTime x, MutableHandleValue result) {
+ MOZ_ASSERT(x.isValid());
+
+ JSString* str =
+ CallICU(cx, [df, x](UChar* chars, int32_t size, UErrorCode* status) {
+ return udat_format(df, x.toDouble(), chars, size, nullptr, status);
+ });
+ if (!str) {
+ return false;
+ }
+
+ result.setString(str);
+ return true;
+}
+
+using FieldType = js::ImmutablePropertyNamePtr JSAtomState::*;
+
+static FieldType GetFieldTypeForFormatField(UDateFormatField fieldName) {
+ // See intl/icu/source/i18n/unicode/udat.h for a detailed field list. This
+ // switch is deliberately exhaustive: cases might have to be added/removed
+ // if this code is compiled with a different ICU with more
+ // UDateFormatField enum initializers. Please guard such cases with
+ // appropriate ICU version-testing #ifdefs, should cross-version divergence
+ // occur.
+ switch (fieldName) {
+ case UDAT_ERA_FIELD:
+ return &JSAtomState::era;
+
+ case UDAT_YEAR_FIELD:
+ case UDAT_YEAR_WOY_FIELD:
+ case UDAT_EXTENDED_YEAR_FIELD:
+ return &JSAtomState::year;
+
+ case UDAT_YEAR_NAME_FIELD:
+ return &JSAtomState::yearName;
+
+ case UDAT_MONTH_FIELD:
+ case UDAT_STANDALONE_MONTH_FIELD:
+ return &JSAtomState::month;
+
+ case UDAT_DATE_FIELD:
+ case UDAT_JULIAN_DAY_FIELD:
+ return &JSAtomState::day;
+
+ case UDAT_HOUR_OF_DAY1_FIELD:
+ case UDAT_HOUR_OF_DAY0_FIELD:
+ case UDAT_HOUR1_FIELD:
+ case UDAT_HOUR0_FIELD:
+ return &JSAtomState::hour;
+
+ case UDAT_MINUTE_FIELD:
+ return &JSAtomState::minute;
+
+ case UDAT_SECOND_FIELD:
+ return &JSAtomState::second;
+
+ case UDAT_DAY_OF_WEEK_FIELD:
+ case UDAT_STANDALONE_DAY_FIELD:
+ case UDAT_DOW_LOCAL_FIELD:
+ case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD:
+ return &JSAtomState::weekday;
+
+ case UDAT_AM_PM_FIELD:
+ return &JSAtomState::dayPeriod;
+
+ case UDAT_TIMEZONE_FIELD:
+ return &JSAtomState::timeZoneName;
+
+ case UDAT_FRACTIONAL_SECOND_FIELD:
+ return &JSAtomState::fractionalSecond;
+
+ case UDAT_FLEXIBLE_DAY_PERIOD_FIELD:
+#ifdef NIGHTLY_BUILD
+ return &JSAtomState::dayPeriod;
+#else
+ // Currently restricted to Nightly.
+ return &JSAtomState::unknown;
+#endif
+
+#ifndef U_HIDE_INTERNAL_API
+ case UDAT_RELATED_YEAR_FIELD:
+ return &JSAtomState::relatedYear;
+#endif
+
+ case UDAT_DAY_OF_YEAR_FIELD:
+ case UDAT_WEEK_OF_YEAR_FIELD:
+ case UDAT_WEEK_OF_MONTH_FIELD:
+ case UDAT_MILLISECONDS_IN_DAY_FIELD:
+ case UDAT_TIMEZONE_RFC_FIELD:
+ case UDAT_TIMEZONE_GENERIC_FIELD:
+ case UDAT_QUARTER_FIELD:
+ case UDAT_STANDALONE_QUARTER_FIELD:
+ case UDAT_TIMEZONE_SPECIAL_FIELD:
+ case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD:
+ case UDAT_TIMEZONE_ISO_FIELD:
+ case UDAT_TIMEZONE_ISO_LOCAL_FIELD:
+ case UDAT_AM_PM_MIDNIGHT_NOON_FIELD:
+#ifndef U_HIDE_INTERNAL_API
+ case UDAT_TIME_SEPARATOR_FIELD:
+#endif
+ // These fields are all unsupported.
+ return &JSAtomState::unknown;
+
+#ifndef U_HIDE_DEPRECATED_API
+ case UDAT_FIELD_COUNT:
+ MOZ_ASSERT_UNREACHABLE(
+ "format field sentinel value returned by "
+ "iterator!");
+#endif
+ }
+
+ MOZ_ASSERT_UNREACHABLE(
+ "unenumerated, undocumented format field returned "
+ "by iterator");
+ return nullptr;
+}
+
+static bool intl_FormatToPartsDateTime(JSContext* cx, const UDateFormat* df,
+ ClippedTime x, FieldType source,
+ MutableHandleValue result) {
+ MOZ_ASSERT(x.isValid());
+
+ UErrorCode status = U_ZERO_ERROR;
+ UFieldPositionIterator* fpositer = ufieldpositer_open(&status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ ScopedICUObject<UFieldPositionIterator, ufieldpositer_close> toClose(
+ fpositer);
+
+ RootedString overallResult(cx);
+ overallResult = CallICU(
+ cx, [df, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) {
+ return udat_formatForFields(df, x.toDouble(), chars, size, fpositer,
+ status);
+ });
+ if (!overallResult) {
+ return false;
+ }
+
+ RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx));
+ if (!partsArray) {
+ return false;
+ }
+
+ if (overallResult->length() == 0) {
+ // An empty string contains no parts, so avoid extra work below.
+ result.setObject(*partsArray);
+ return true;
+ }
+
+ size_t lastEndIndex = 0;
+
+ RootedObject singlePart(cx);
+ RootedValue val(cx);
+
+ auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) {
+ singlePart = NewBuiltinClassInstance<PlainObject>(cx);
+ if (!singlePart) {
+ return false;
+ }
+
+ val = StringValue(cx->names().*type);
+ if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) {
+ return false;
+ }
+
+ JSLinearString* partSubstr = NewDependentString(
+ cx, overallResult, beginIndex, endIndex - beginIndex);
+ if (!partSubstr) {
+ return false;
+ }
+
+ val = StringValue(partSubstr);
+ if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) {
+ return false;
+ }
+
+ if (source) {
+ val = StringValue(cx->names().*source);
+ if (!DefineDataProperty(cx, singlePart, cx->names().source, val)) {
+ return false;
+ }
+ }
+
+ if (!NewbornArrayPush(cx, partsArray, ObjectValue(*singlePart))) {
+ return false;
+ }
+
+ lastEndIndex = endIndex;
+ return true;
+ };
+
+ int32_t fieldInt, beginIndexInt, endIndexInt;
+ while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt,
+ &endIndexInt)) >= 0) {
+ MOZ_ASSERT(beginIndexInt >= 0);
+ MOZ_ASSERT(endIndexInt >= 0);
+ MOZ_ASSERT(beginIndexInt <= endIndexInt,
+ "field iterator returning invalid range");
+
+ size_t beginIndex(beginIndexInt);
+ size_t endIndex(endIndexInt);
+
+ // Technically this isn't guaranteed. But it appears true in pratice,
+ // and http://bugs.icu-project.org/trac/ticket/12024 is expected to
+ // correct the documentation lapse.
+ MOZ_ASSERT(lastEndIndex <= beginIndex,
+ "field iteration didn't return fields in order start to "
+ "finish as expected");
+
+ if (FieldType type = GetFieldTypeForFormatField(
+ static_cast<UDateFormatField>(fieldInt))) {
+ if (lastEndIndex < beginIndex) {
+ if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) {
+ return false;
+ }
+ }
+
+ if (!AppendPart(type, beginIndex, endIndex)) {
+ return false;
+ }
+ }
+ }
+
+ // Append any final literal.
+ if (lastEndIndex < overallResult->length()) {
+ if (!AppendPart(&JSAtomState::literal, lastEndIndex,
+ overallResult->length())) {
+ return false;
+ }
+ }
+
+ result.setObject(*partsArray);
+ return true;
+}
+
+bool js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 3);
+ MOZ_ASSERT(args[0].isObject());
+ MOZ_ASSERT(args[1].isNumber());
+ MOZ_ASSERT(args[2].isBoolean());
+
+ Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
+ dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
+
+ bool formatToParts = args[2].toBoolean();
+
+ ClippedTime x = TimeClip(args[1].toNumber());
+ if (!x.isValid()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
+ formatToParts ? "formatToParts" : "format");
+ return false;
+ }
+
+ // Obtain a cached UDateFormat object.
+ UDateFormat* df = dateTimeFormat->getDateFormat();
+ if (!df) {
+ df = NewUDateFormat(cx, dateTimeFormat);
+ if (!df) {
+ return false;
+ }
+ dateTimeFormat->setDateFormat(df);
+
+ intl::AddICUCellMemory(dateTimeFormat,
+ DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
+ }
+
+ // Use the UDateFormat to actually format the time stamp.
+ FieldType source = nullptr;
+ return formatToParts
+ ? intl_FormatToPartsDateTime(cx, df, x, source, args.rval())
+ : intl_FormatDateTime(cx, df, x, args.rval());
+}
+
+#ifndef U_HIDE_DRAFT_API
+/**
+ * Returns a new UDateIntervalFormat with the locale and date-time formatting
+ * options of the given DateTimeFormat.
+ */
+static UDateIntervalFormat* NewUDateIntervalFormat(
+ JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) {
+ RootedValue value(cx);
+
+ RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat));
+ if (!internals) {
+ return nullptr;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) {
+ return nullptr;
+ }
+
+ // Determine the hour cycle used in the resolved pattern. This is needed to
+ // workaround <https://unicode-org.atlassian.net/browse/ICU-21154> and
+ // <https://unicode-org.atlassian.net/browse/ICU-21155>.
+ mozilla::Maybe<HourCycle> hcPattern;
+ {
+ JSLinearString* pattern = value.toString()->ensureLinear(cx);
+ if (!pattern) {
+ return nullptr;
+ }
+
+ JS::AutoCheckCannotGC nogc;
+ if (pattern->hasLatin1Chars()) {
+ hcPattern = HourCycleFromPattern<Latin1Char>(pattern->latin1Range(nogc));
+ } else {
+ hcPattern = HourCycleFromPattern<char16_t>(pattern->twoByteRange(nogc));
+ }
+ }
+
+ UniqueChars locale = DateTimeFormatLocale(cx, internals, hcPattern);
+ if (!locale) {
+ return nullptr;
+ }
+
+ if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) {
+ return nullptr;
+ }
+
+ AutoStableStringChars timeZone(cx);
+ if (!timeZone.initTwoByte(cx, value.toString())) {
+ return nullptr;
+ }
+ mozilla::Span<const char16_t> timeZoneChars = timeZone.twoByteRange();
+
+ if (!GetProperty(cx, internals, internals, cx->names().skeleton, &value)) {
+ return nullptr;
+ }
+
+ AutoStableStringChars skeleton(cx);
+ if (!skeleton.initTwoByte(cx, value.toString())) {
+ return nullptr;
+ }
+ mozilla::Span<const char16_t> skeletonChars = skeleton.twoByteRange();
+
+ Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE> newSkeleton(cx);
+ if (hcPattern) {
+ if (!newSkeleton.append(skeletonChars.data(), skeletonChars.size())) {
+ return nullptr;
+ }
+
+ ReplaceHourSymbol(newSkeleton, *hcPattern);
+ skeletonChars = newSkeleton;
+ }
+
+ UErrorCode status = U_ZERO_ERROR;
+ UDateIntervalFormat* dif = udtitvfmt_open(
+ IcuLocale(locale.get()), skeletonChars.data(), skeletonChars.size(),
+ timeZoneChars.data(), timeZoneChars.size(), &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return nullptr;
+ }
+
+ return dif;
+}
+
+static UCalendar* CreateCalendar(JSContext* cx, const UCalendar* cal,
+ ClippedTime t) {
+ UErrorCode status = U_ZERO_ERROR;
+ UCalendar* clone = ucal_clone(cal, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return nullptr;
+ }
+ ScopedICUObject<UCalendar, ucal_close> toClose(clone);
+
+ ucal_setMillis(clone, t.toDouble(), &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return nullptr;
+ }
+ return toClose.forget();
+}
+
+/**
+ * PartitionDateTimeRangePattern ( dateTimeFormat, x, y )
+ */
+static const UFormattedValue* PartitionDateTimeRangePattern(
+ JSContext* cx, const UDateFormat* df, const UDateIntervalFormat* dif,
+ UFormattedDateInterval* formatted, ClippedTime x, ClippedTime y) {
+ MOZ_ASSERT(x.isValid());
+ MOZ_ASSERT(y.isValid());
+ MOZ_ASSERT(x.toDouble() <= y.toDouble());
+
+ // We can't access the calendar used by UDateIntervalFormat to change it to a
+ // proleptic Gregorian calendar. Instead we need to call a different formatter
+ // function which accepts UCalendar instead of UDate.
+ // But creating new UCalendar objects for each call is slow, so when we can
+ // ensure that the input dates are later than the Gregorian change date,
+ // directly call the formatter functions taking UDate.
+
+ // The Gregorian change date "1582-10-15T00:00:00.000Z".
+ constexpr double GregorianChangeDate = -12219292800000.0;
+
+ // Add a full day to account for time zone offsets.
+ constexpr double GregorianChangeDatePlusOneDay =
+ GregorianChangeDate + msPerDay;
+
+ UErrorCode status = U_ZERO_ERROR;
+ if (x.toDouble() < GregorianChangeDatePlusOneDay) {
+ // Create calendar objects for the start and end date by cloning the date
+ // formatter calendar. The date formatter calendar already has the correct
+ // time zone set and was changed to use a proleptic Gregorian calendar.
+ const UCalendar* cal = udat_getCalendar(df);
+
+ UCalendar* startCal = CreateCalendar(cx, cal, x);
+ if (!startCal) {
+ return nullptr;
+ }
+ ScopedICUObject<UCalendar, ucal_close> toCloseStart(startCal);
+
+ UCalendar* endCal = CreateCalendar(cx, cal, y);
+ if (!endCal) {
+ return nullptr;
+ }
+ ScopedICUObject<UCalendar, ucal_close> toCloseEnd(endCal);
+
+ udtitvfmt_formatCalendarToResult(dif, startCal, endCal, formatted, &status);
+ } else {
+ // The common fast path which doesn't require creating calendar objects.
+ udtitvfmt_formatToResult(dif, x.toDouble(), y.toDouble(), formatted,
+ &status);
+ }
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return nullptr;
+ }
+
+ const UFormattedValue* formattedValue =
+ udtitvfmt_resultAsValue(formatted, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return nullptr;
+ }
+
+ return formattedValue;
+}
+
+/**
+ * PartitionDateTimeRangePattern ( dateTimeFormat, x, y ), steps 9-11.
+ *
+ * Examine the formatted value to see if any interval span field is present.
+ */
+static bool DateFieldsPracticallyEqual(JSContext* cx,
+ const UFormattedValue* formattedValue,
+ bool* equal) {
+ UErrorCode status = U_ZERO_ERROR;
+ UConstrainedFieldPosition* fpos = ucfpos_open(&status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ ScopedICUObject<UConstrainedFieldPosition, ucfpos_close> toCloseFpos(fpos);
+
+ // We're only interested in UFIELD_CATEGORY_DATE_INTERVAL_SPAN fields.
+ ucfpos_constrainCategory(fpos, UFIELD_CATEGORY_DATE_INTERVAL_SPAN, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ bool hasSpan = ufmtval_nextPosition(formattedValue, fpos, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ // When no date interval span field was found, both dates are "practically
+ // equal" per PartitionDateTimeRangePattern.
+ *equal = !hasSpan;
+ return true;
+}
+
+/**
+ * FormatDateTimeRange( dateTimeFormat, x, y )
+ */
+static bool FormatDateTimeRange(JSContext* cx, const UDateFormat* df,
+ const UDateIntervalFormat* dif, ClippedTime x,
+ ClippedTime y, MutableHandleValue result) {
+ UErrorCode status = U_ZERO_ERROR;
+ UFormattedDateInterval* formatted = udtitvfmt_openResult(&status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ ScopedICUObject<UFormattedDateInterval, udtitvfmt_closeResult> toClose(
+ formatted);
+
+ const UFormattedValue* formattedValue =
+ PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y);
+ if (!formattedValue) {
+ return false;
+ }
+
+ // PartitionDateTimeRangePattern, steps 9-11.
+ bool equal;
+ if (!DateFieldsPracticallyEqual(cx, formattedValue, &equal)) {
+ return false;
+ }
+
+ // PartitionDateTimeRangePattern, step 12.
+ if (equal) {
+ return intl_FormatDateTime(cx, df, x, result);
+ }
+
+ JSString* resultStr = intl::FormattedValueToString(cx, formattedValue);
+ if (!resultStr) {
+ return false;
+ }
+
+ result.setString(resultStr);
+ return true;
+}
+
+/**
+ * FormatDateTimeRangeToParts ( dateTimeFormat, x, y )
+ */
+static bool FormatDateTimeRangeToParts(JSContext* cx, const UDateFormat* df,
+ const UDateIntervalFormat* dif,
+ ClippedTime x, ClippedTime y,
+ MutableHandleValue result) {
+ UErrorCode status = U_ZERO_ERROR;
+ UFormattedDateInterval* formatted = udtitvfmt_openResult(&status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ ScopedICUObject<UFormattedDateInterval, udtitvfmt_closeResult> toClose(
+ formatted);
+
+ const UFormattedValue* formattedValue =
+ PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y);
+ if (!formattedValue) {
+ return false;
+ }
+
+ // PartitionDateTimeRangePattern, steps 9-11.
+ bool equal;
+ if (!DateFieldsPracticallyEqual(cx, formattedValue, &equal)) {
+ return false;
+ }
+
+ // PartitionDateTimeRangePattern, step 12.
+ if (equal) {
+ FieldType source = &JSAtomState::shared;
+ return intl_FormatToPartsDateTime(cx, df, x, source, result);
+ }
+
+ RootedString overallResult(cx,
+ intl::FormattedValueToString(cx, formattedValue));
+ if (!overallResult) {
+ return false;
+ }
+
+ RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx));
+ if (!partsArray) {
+ return false;
+ }
+
+ size_t lastEndIndex = 0;
+ RootedObject singlePart(cx);
+ RootedValue val(cx);
+
+ auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex,
+ FieldType source) {
+ singlePart = NewBuiltinClassInstance<PlainObject>(cx);
+ if (!singlePart) {
+ return false;
+ }
+
+ val = StringValue(cx->names().*type);
+ if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) {
+ return false;
+ }
+
+ JSLinearString* partSubstr = NewDependentString(
+ cx, overallResult, beginIndex, endIndex - beginIndex);
+ if (!partSubstr) {
+ return false;
+ }
+
+ val = StringValue(partSubstr);
+ if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) {
+ return false;
+ }
+
+ val = StringValue(cx->names().*source);
+ if (!DefineDataProperty(cx, singlePart, cx->names().source, val)) {
+ return false;
+ }
+
+ if (!NewbornArrayPush(cx, partsArray, ObjectValue(*singlePart))) {
+ return false;
+ }
+
+ lastEndIndex = endIndex;
+ return true;
+ };
+
+ UConstrainedFieldPosition* fpos = ucfpos_open(&status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ ScopedICUObject<UConstrainedFieldPosition, ucfpos_close> toCloseFpos(fpos);
+
+ size_t categoryEndIndex = 0;
+ FieldType source = &JSAtomState::shared;
+
+ while (true) {
+ bool hasMore = ufmtval_nextPosition(formattedValue, fpos, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+ if (!hasMore) {
+ break;
+ }
+
+ int32_t category = ucfpos_getCategory(fpos, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ int32_t field = ucfpos_getField(fpos, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ int32_t beginIndexInt, endIndexInt;
+ ucfpos_getIndexes(fpos, &beginIndexInt, &endIndexInt, &status);
+ if (U_FAILURE(status)) {
+ intl::ReportInternalError(cx);
+ return false;
+ }
+
+ MOZ_ASSERT(beginIndexInt >= 0);
+ MOZ_ASSERT(endIndexInt >= 0);
+ MOZ_ASSERT(beginIndexInt <= endIndexInt,
+ "field iterator returning invalid range");
+
+ size_t beginIndex = size_t(beginIndexInt);
+ size_t endIndex = size_t(endIndexInt);
+
+ // Indices are guaranteed to be returned in order (from left to right).
+ MOZ_ASSERT(lastEndIndex <= beginIndex,
+ "field iteration didn't return fields in order start to "
+ "finish as expected");
+
+ if (category == UFIELD_CATEGORY_DATE_INTERVAL_SPAN) {
+ // Append any remaining literal parts before changing the source kind.
+ if (lastEndIndex < beginIndex) {
+ if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex,
+ source)) {
+ return false;
+ }
+ }
+
+ // The special field category UFIELD_CATEGORY_DATE_INTERVAL_SPAN has only
+ // two allowed values (0 or 1), indicating the begin of the start- resp.
+ // end-date.
+ MOZ_ASSERT(field == 0 || field == 1,
+ "span category has unexpected value");
+
+ source = field == 0 ? &JSAtomState::startRange : &JSAtomState::endRange;
+ categoryEndIndex = endIndex;
+ continue;
+ }
+
+ // Ignore categories other than UFIELD_CATEGORY_DATE.
+ if (category != UFIELD_CATEGORY_DATE) {
+ continue;
+ }
+
+ // Append the field if supported. If not supported, append it as part of the
+ // next literal part.
+ if (FieldType type =
+ GetFieldTypeForFormatField(static_cast<UDateFormatField>(field))) {
+ if (lastEndIndex < beginIndex) {
+ if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex,
+ source)) {
+ return false;
+ }
+ }
+
+ if (!AppendPart(type, beginIndex, endIndex, source)) {
+ return false;
+ }
+ }
+
+ if (endIndex == categoryEndIndex) {
+ // Append any remaining literal parts before changing the source kind.
+ if (lastEndIndex < endIndex) {
+ if (!AppendPart(&JSAtomState::literal, lastEndIndex, endIndex,
+ source)) {
+ return false;
+ }
+ }
+
+ source = &JSAtomState::shared;
+ }
+ }
+
+ // Append any final literal.
+ if (lastEndIndex < overallResult->length()) {
+ if (!AppendPart(&JSAtomState::literal, lastEndIndex,
+ overallResult->length(), source)) {
+ return false;
+ }
+ }
+
+ result.setObject(*partsArray);
+ return true;
+}
+
+bool js::intl_FormatDateTimeRange(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 4);
+ MOZ_ASSERT(args[0].isObject());
+ MOZ_ASSERT(args[1].isNumber());
+ MOZ_ASSERT(args[2].isNumber());
+ MOZ_ASSERT(args[3].isBoolean());
+
+ Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
+ dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>();
+
+ bool formatToParts = args[3].toBoolean();
+
+ // PartitionDateTimeRangePattern, steps 1-2.
+ ClippedTime x = TimeClip(args[1].toNumber());
+ if (!x.isValid()) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
+ formatToParts ? "formatRangeToParts" : "formatRange");
+ return false;
+ }
+
+ // PartitionDateTimeRangePattern, steps 3-4.
+ ClippedTime y = TimeClip(args[2].toNumber());
+ if (!y.isValid()) {
+ JS_ReportErrorNumberASCII(
+ cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat",
+ formatToParts ? "formatRangeToParts" : "formatRange");
+ return false;
+ }
+
+ // Self-hosted code should have checked this condition.
+ MOZ_ASSERT(x.toDouble() <= y.toDouble(),
+ "start date mustn't be after the end date");
+
+ // Obtain a cached UDateFormat object.
+ UDateFormat* df = dateTimeFormat->getDateFormat();
+ if (!df) {
+ df = NewUDateFormat(cx, dateTimeFormat);
+ if (!df) {
+ return false;
+ }
+ dateTimeFormat->setDateFormat(df);
+
+ intl::AddICUCellMemory(dateTimeFormat,
+ DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
+ }
+
+ // Obtain a cached UDateIntervalFormat object.
+ UDateIntervalFormat* dif = dateTimeFormat->getDateIntervalFormat();
+ if (!dif) {
+ dif = NewUDateIntervalFormat(cx, dateTimeFormat);
+ if (!dif) {
+ return false;
+ }
+ dateTimeFormat->setDateIntervalFormat(dif);
+
+ intl::AddICUCellMemory(
+ dateTimeFormat,
+ DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
+ }
+
+ // Use the UDateIntervalFormat to actually format the time range.
+ return formatToParts
+ ? FormatDateTimeRangeToParts(cx, df, dif, x, y, args.rval())
+ : FormatDateTimeRange(cx, df, dif, x, y, args.rval());
+}
+
+#else
+bool js::intl_FormatDateTimeRange(JSContext* cx, unsigned argc, Value* vp) {
+ MOZ_CRASH("FormatDateTimeRange requires ICU draft APIs");
+}
+#endif // U_HIDE_DRAFT_API