/* -*- 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/intl/Calendar.h" #include "mozilla/intl/DateIntervalFormat.h" #include "mozilla/intl/DateTimeFormat.h" #include "mozilla/intl/DateTimePart.h" #include "mozilla/intl/Locale.h" #include "mozilla/intl/TimeZone.h" #include "mozilla/Range.h" #include "mozilla/Span.h" #include "builtin/Array.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/FormatBuffer.h" #include "builtin/intl/LanguageTag.h" #include "builtin/intl/SharedIntlData.h" #include "gc/GCContext.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/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties #include "js/PropertySpec.h" #include "js/StableStringChars.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/WellKnownAtom.h" // js_*_str #include "vm/GeckoProfiler-inl.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::DateTimeFormatOptions; using js::intl::FormatBuffer; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::SharedIntlData; const JSClassOps DateTimeFormatObject::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve DateTimeFormatObject::finalize, // finalize nullptr, // call 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), JS_SELF_HOSTED_FN("formatRange", "Intl_DateTimeFormat_formatRange", 2, 0), JS_SELF_HOSTED_FN("formatRangeToParts", "Intl_DateTimeFormat_formatRangeToParts", 2, 0), 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, GenericCreatePrototype, 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) { AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.DateTimeFormat"); // 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 dateTimeFormat(cx); dateTimeFormat = NewObjectWithClassProto(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(JS::GCContext* gcx, JSObject* obj) { MOZ_ASSERT(gcx->onMainThread()); auto* dateTimeFormat = &obj->as(); mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat(); mozilla::intl::DateIntervalFormat* dif = dateTimeFormat->getDateIntervalFormat(); if (df) { intl::RemoveICUCellMemory( gcx, obj, DateTimeFormatObject::UDateFormatEstimatedMemoryUse); delete df; } if (dif) { intl::RemoveICUCellMemory( gcx, obj, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse); delete dif; } } bool JS::AddMozDateTimeFormatConstructor(JSContext* cx, JS::Handle intl) { RootedObject ctor( cx, GlobalObject::createConstructor(cx, MozDateTimeFormat, cx->names().DateTimeFormat, 0)); if (!ctor) { return false; } RootedObject proto( cx, GlobalObject::createBlankPrototype(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) { auto calendar = mozilla::intl::Calendar::TryCreate(locale.get()); if (calendar.isErr()) { intl::ReportInternalError(cx, calendar.unwrapErr()); return false; } auto type = calendar.unwrap()->GetBcp47Type(); if (type.isErr()) { intl::ReportInternalError(cx, type.unwrapErr()); return false; } JSString* str = NewStringCopy(cx, type.unwrap()); if (!str) { return false; } rval.setString(str); return true; } 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. auto keywords = mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale(locale.get()); if (keywords.isErr()) { intl::ReportInternalError(cx, keywords.unwrapErr()); return false; } for (auto keyword : keywords.unwrap()) { if (keyword.isErr()) { intl::ReportInternalError(cx); return false; } JSString* jscalendar = NewStringCopy(cx, keyword.unwrap()); 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()); Rooted 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()); Rooted 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; } FormatBuffer canonicalTimeZone(cx); auto result = mozilla::intl::TimeZone::GetCanonicalTimeZoneID( stableChars.twoByteRange(), canonicalTimeZone); if (result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return false; } JSString* str = canonicalTimeZone.toString(cx); 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); FormatBuffer timeZone(cx); auto result = DateTimeInfo::timeZoneId(DateTimeInfo::shouldRFP(cx->realm()), timeZone); if (result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return false; } JSString* str = timeZone.toString(cx); 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); auto offset = DateTimeInfo::getRawOffsetMs(DateTimeInfo::shouldRFP(cx->realm())); if (offset.isErr()) { intl::ReportInternalError(cx, offset.unwrapErr()); return false; } args.rval().setInt32(offset.unwrap()); 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; } FormatBuffer chars(cx); auto result = DateTimeInfo::timeZoneId(DateTimeInfo::shouldRFP(cx->realm()), chars); if (result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return false; } JSLinearString* str = args[0].toString()->ensureLinear(cx); if (!str) { return false; } bool equals; if (str->length() == chars.length()) { JS::AutoCheckCannotGC nogc; equals = str->hasLatin1Chars() ? EqualChars(str->latin1Chars(nogc), chars.data(), str->length()) : EqualChars(str->twoByteChars(nogc), chars.data(), 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 UniqueChars DateTimeFormatLocale( JSContext* cx, HandleObject internals, mozilla::Maybe 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. mozilla::intl::Locale tag; { Rooted locale(cx, value.toString()->ensureLinear(cx)); if (!locale) { return nullptr; } if (!intl::ParseLocale(cx, locale, tag)) { return nullptr; } } JS::RootedVector 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 mozilla::intl::DateTimeFormat::HourCycle::H11: hourCycleStr = cx->names().h11; break; case mozilla::intl::DateTimeFormat::HourCycle::H12: hourCycleStr = cx->names().h12; break; case mozilla::intl::DateTimeFormat::HourCycle::H23: hourCycleStr = cx->names().h23; break; case mozilla::intl::DateTimeFormat::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; } FormatBuffer buffer(cx); if (auto result = tag.ToString(buffer); result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return nullptr; } return buffer.extractStringZ(); } static bool AssignTextComponent( JSContext* cx, HandleObject internals, Handle property, mozilla::Maybe* text) { RootedValue value(cx); if (!GetProperty(cx, internals, internals, property, &value)) { return false; } if (value.isString()) { JSLinearString* string = value.toString()->ensureLinear(cx); if (!string) { return false; } if (StringEqualsLiteral(string, "narrow")) { *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Narrow); } else if (StringEqualsLiteral(string, "short")) { *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Short); } else { MOZ_ASSERT(StringEqualsLiteral(string, "long")); *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Long); } } else { MOZ_ASSERT(value.isUndefined()); } return true; } static bool AssignNumericComponent( JSContext* cx, HandleObject internals, Handle property, mozilla::Maybe* numeric) { RootedValue value(cx); if (!GetProperty(cx, internals, internals, property, &value)) { return false; } if (value.isString()) { JSLinearString* string = value.toString()->ensureLinear(cx); if (!string) { return false; } if (StringEqualsLiteral(string, "numeric")) { *numeric = mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::Numeric); } else { MOZ_ASSERT(StringEqualsLiteral(string, "2-digit")); *numeric = mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::TwoDigit); } } else { MOZ_ASSERT(value.isUndefined()); } return true; } static bool AssignMonthComponent( JSContext* cx, HandleObject internals, Handle property, mozilla::Maybe* month) { RootedValue value(cx); if (!GetProperty(cx, internals, internals, property, &value)) { return false; } if (value.isString()) { JSLinearString* string = value.toString()->ensureLinear(cx); if (!string) { return false; } if (StringEqualsLiteral(string, "numeric")) { *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Numeric); } else if (StringEqualsLiteral(string, "2-digit")) { *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::TwoDigit); } else if (StringEqualsLiteral(string, "long")) { *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Long); } else if (StringEqualsLiteral(string, "short")) { *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Short); } else { MOZ_ASSERT(StringEqualsLiteral(string, "narrow")); *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Narrow); } } else { MOZ_ASSERT(value.isUndefined()); } return true; } static bool AssignTimeZoneNameComponent( JSContext* cx, HandleObject internals, Handle property, mozilla::Maybe* tzName) { RootedValue value(cx); if (!GetProperty(cx, internals, internals, property, &value)) { return false; } if (value.isString()) { JSLinearString* string = value.toString()->ensureLinear(cx); if (!string) { return false; } if (StringEqualsLiteral(string, "long")) { *tzName = mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Long); } else if (StringEqualsLiteral(string, "short")) { *tzName = mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Short); } else if (StringEqualsLiteral(string, "shortOffset")) { *tzName = mozilla::Some( mozilla::intl::DateTimeFormat::TimeZoneName::ShortOffset); } else if (StringEqualsLiteral(string, "longOffset")) { *tzName = mozilla::Some( mozilla::intl::DateTimeFormat::TimeZoneName::LongOffset); } else if (StringEqualsLiteral(string, "shortGeneric")) { *tzName = mozilla::Some( mozilla::intl::DateTimeFormat::TimeZoneName::ShortGeneric); } else { MOZ_ASSERT(StringEqualsLiteral(string, "longGeneric")); *tzName = mozilla::Some( mozilla::intl::DateTimeFormat::TimeZoneName::LongGeneric); } } else { MOZ_ASSERT(value.isUndefined()); } return true; } static bool AssignHourCycleComponent( JSContext* cx, HandleObject internals, Handle property, mozilla::Maybe* hourCycle) { RootedValue value(cx); if (!GetProperty(cx, internals, internals, property, &value)) { return false; } if (value.isString()) { JSLinearString* string = value.toString()->ensureLinear(cx); if (!string) { return false; } if (StringEqualsLiteral(string, "h11")) { *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H11); } else if (StringEqualsLiteral(string, "h12")) { *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H12); } else if (StringEqualsLiteral(string, "h23")) { *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H23); } else { MOZ_ASSERT(StringEqualsLiteral(string, "h24")); *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H24); } } else { MOZ_ASSERT(value.isUndefined()); } return true; } static bool AssignHour12Component(JSContext* cx, HandleObject internals, mozilla::Maybe* hour12) { RootedValue value(cx); if (!GetProperty(cx, internals, internals, cx->names().hour12, &value)) { return false; } if (value.isBoolean()) { *hour12 = mozilla::Some(value.toBoolean()); } else { MOZ_ASSERT(value.isUndefined()); } return true; } static bool AssignDateTimeLength( JSContext* cx, HandleObject internals, Handle property, mozilla::Maybe* style) { RootedValue value(cx); if (!GetProperty(cx, internals, internals, property, &value)) { return false; } if (value.isString()) { JSLinearString* string = value.toString()->ensureLinear(cx); if (!string) { return false; } if (StringEqualsLiteral(string, "full")) { *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Full); } else if (StringEqualsLiteral(string, "long")) { *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long); } else if (StringEqualsLiteral(string, "medium")) { *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Medium); } else { MOZ_ASSERT(StringEqualsLiteral(string, "short")); *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); } } else { MOZ_ASSERT(value.isUndefined()); } return true; } /** * Returns a new mozilla::intl::DateTimeFormat with the locale and date-time * formatting options of the given DateTimeFormat. */ static mozilla::intl::DateTimeFormat* NewDateTimeFormat( JSContext* cx, Handle 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 timeZoneChars = timeZone.twoByteRange(); if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) { return nullptr; } bool hasPattern = value.isString(); if (!GetProperty(cx, internals, internals, cx->names().timeStyle, &value)) { return nullptr; } bool hasStyle = value.isString(); if (!hasStyle) { if (!GetProperty(cx, internals, internals, cx->names().dateStyle, &value)) { return nullptr; } hasStyle = value.isString(); } mozilla::UniquePtr df = nullptr; if (hasPattern) { // This is a DateTimeFormat defined by a pattern option. This is internal // to Mozilla, and not part of the ECMA-402 API. if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) { return nullptr; } AutoStableStringChars pattern(cx); if (!pattern.initTwoByte(cx, value.toString())) { return nullptr; } auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromPattern( mozilla::MakeStringSpan(locale.get()), pattern.twoByteRange(), mozilla::Some(timeZoneChars)); if (dfResult.isErr()) { intl::ReportInternalError(cx, dfResult.unwrapErr()); return nullptr; } df = dfResult.unwrap(); } else if (hasStyle) { // This is a DateTimeFormat defined by a time style or date style. mozilla::intl::DateTimeFormat::StyleBag style; if (!AssignDateTimeLength(cx, internals, cx->names().timeStyle, &style.time)) { return nullptr; } if (!AssignDateTimeLength(cx, internals, cx->names().dateStyle, &style.date)) { return nullptr; } if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle, &style.hourCycle)) { return nullptr; } if (!AssignHour12Component(cx, internals, &style.hour12)) { return nullptr; } SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); mozilla::intl::DateTimePatternGenerator* gen = sharedIntlData.getDateTimePatternGenerator(cx, locale.get()); if (!gen) { return nullptr; } auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromStyle( mozilla::MakeStringSpan(locale.get()), style, gen, mozilla::Some(timeZoneChars)); if (dfResult.isErr()) { intl::ReportInternalError(cx, dfResult.unwrapErr()); return nullptr; } df = dfResult.unwrap(); } else { // This is a DateTimeFormat defined by a components bag. mozilla::intl::DateTimeFormat::ComponentsBag bag; if (!AssignTextComponent(cx, internals, cx->names().era, &bag.era)) { return nullptr; } if (!AssignNumericComponent(cx, internals, cx->names().year, &bag.year)) { return nullptr; } if (!AssignMonthComponent(cx, internals, cx->names().month, &bag.month)) { return nullptr; } if (!AssignNumericComponent(cx, internals, cx->names().day, &bag.day)) { return nullptr; } if (!AssignTextComponent(cx, internals, cx->names().weekday, &bag.weekday)) { return nullptr; } if (!AssignNumericComponent(cx, internals, cx->names().hour, &bag.hour)) { return nullptr; } if (!AssignNumericComponent(cx, internals, cx->names().minute, &bag.minute)) { return nullptr; } if (!AssignNumericComponent(cx, internals, cx->names().second, &bag.second)) { return nullptr; } if (!AssignTimeZoneNameComponent(cx, internals, cx->names().timeZoneName, &bag.timeZoneName)) { return nullptr; } if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle, &bag.hourCycle)) { return nullptr; } if (!AssignTextComponent(cx, internals, cx->names().dayPeriod, &bag.dayPeriod)) { return nullptr; } if (!AssignHour12Component(cx, internals, &bag.hour12)) { return nullptr; } if (!GetProperty(cx, internals, internals, cx->names().fractionalSecondDigits, &value)) { return nullptr; } if (value.isInt32()) { bag.fractionalSecondDigits = mozilla::Some(value.toInt32()); } else { MOZ_ASSERT(value.isUndefined()); } SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); auto* dtpg = sharedIntlData.getDateTimePatternGenerator(cx, locale.get()); if (!dtpg) { return nullptr; } auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromComponents( mozilla::MakeStringSpan(locale.get()), bag, dtpg, mozilla::Some(timeZoneChars)); if (dfResult.isErr()) { intl::ReportInternalError(cx, dfResult.unwrapErr()); return nullptr; } df = dfResult.unwrap(); } // ECMAScript requires the Gregorian calendar to be used from the beginning // of ECMAScript time. df->SetStartTimeIfGregorian(StartOfTime); return df.release(); } static mozilla::intl::DateTimeFormat* GetOrCreateDateTimeFormat( JSContext* cx, Handle dateTimeFormat) { // Obtain a cached mozilla::intl::DateTimeFormat object. mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat(); if (df) { return df; } df = NewDateTimeFormat(cx, dateTimeFormat); if (!df) { return nullptr; } dateTimeFormat->setDateFormat(df); intl::AddICUCellMemory(dateTimeFormat, DateTimeFormatObject::UDateFormatEstimatedMemoryUse); return df; } template static bool SetResolvedProperty(JSContext* cx, HandleObject resolved, Handle name, mozilla::Maybe intlProp) { if (!intlProp) { return true; } JSString* str = NewStringCopyZ( cx, mozilla::intl::DateTimeFormat::ToString(*intlProp)); if (!str) { return false; } RootedValue value(cx, StringValue(str)); return DefineDataProperty(cx, resolved, name, value); } bool js::intl_resolveDateTimeFormatComponents(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].isObject()); MOZ_ASSERT(args[2].isBoolean()); Rooted dateTimeFormat(cx); dateTimeFormat = &args[0].toObject().as(); RootedObject resolved(cx, &args[1].toObject()); bool includeDateTimeFields = args[2].toBoolean(); mozilla::intl::DateTimeFormat* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat); if (!df) { return false; } auto result = df->ResolveComponents(); if (result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return false; } mozilla::intl::DateTimeFormat::ComponentsBag components = result.unwrap(); // Map the resolved mozilla::intl::DateTimeFormat::ComponentsBag to the // options object as returned by DateTimeFormat.prototype.resolvedOptions. // // Resolved options must match the ordering as defined in: // https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions if (!SetResolvedProperty(cx, resolved, cx->names().hourCycle, components.hourCycle)) { return false; } if (components.hour12) { RootedValue value(cx, BooleanValue(*components.hour12)); if (!DefineDataProperty(cx, resolved, cx->names().hour12, value)) { return false; } } if (!includeDateTimeFields) { args.rval().setUndefined(); // Do not include date time fields. return true; } if (!SetResolvedProperty(cx, resolved, cx->names().weekday, components.weekday)) { return false; } if (!SetResolvedProperty(cx, resolved, cx->names().era, components.era)) { return false; } if (!SetResolvedProperty(cx, resolved, cx->names().year, components.year)) { return false; } if (!SetResolvedProperty(cx, resolved, cx->names().month, components.month)) { return false; } if (!SetResolvedProperty(cx, resolved, cx->names().day, components.day)) { return false; } if (!SetResolvedProperty(cx, resolved, cx->names().dayPeriod, components.dayPeriod)) { return false; } if (!SetResolvedProperty(cx, resolved, cx->names().hour, components.hour)) { return false; } if (!SetResolvedProperty(cx, resolved, cx->names().minute, components.minute)) { return false; } if (!SetResolvedProperty(cx, resolved, cx->names().second, components.second)) { return false; } if (!SetResolvedProperty(cx, resolved, cx->names().timeZoneName, components.timeZoneName)) { return false; } if (components.fractionalSecondDigits) { RootedValue value(cx, Int32Value(*components.fractionalSecondDigits)); if (!DefineDataProperty(cx, resolved, cx->names().fractionalSecondDigits, value)) { return false; } } args.rval().setUndefined(); return true; } static bool intl_FormatDateTime(JSContext* cx, const mozilla::intl::DateTimeFormat* df, ClippedTime x, MutableHandleValue result) { MOZ_ASSERT(x.isValid()); FormatBuffer buffer(cx); auto dfResult = df->TryFormat(x.toDouble(), buffer); if (dfResult.isErr()) { intl::ReportInternalError(cx, dfResult.unwrapErr()); return false; } JSString* str = buffer.toString(cx); if (!str) { return false; } result.setString(str); return true; } using FieldType = js::ImmutableTenuredPtr JSAtomState::*; static FieldType GetFieldTypeForPartType(mozilla::intl::DateTimePartType type) { switch (type) { case mozilla::intl::DateTimePartType::Literal: return &JSAtomState::literal; case mozilla::intl::DateTimePartType::Era: return &JSAtomState::era; case mozilla::intl::DateTimePartType::Year: return &JSAtomState::year; case mozilla::intl::DateTimePartType::YearName: return &JSAtomState::yearName; case mozilla::intl::DateTimePartType::RelatedYear: return &JSAtomState::relatedYear; case mozilla::intl::DateTimePartType::Month: return &JSAtomState::month; case mozilla::intl::DateTimePartType::Day: return &JSAtomState::day; case mozilla::intl::DateTimePartType::Hour: return &JSAtomState::hour; case mozilla::intl::DateTimePartType::Minute: return &JSAtomState::minute; case mozilla::intl::DateTimePartType::Second: return &JSAtomState::second; case mozilla::intl::DateTimePartType::Weekday: return &JSAtomState::weekday; case mozilla::intl::DateTimePartType::DayPeriod: return &JSAtomState::dayPeriod; case mozilla::intl::DateTimePartType::TimeZoneName: return &JSAtomState::timeZoneName; case mozilla::intl::DateTimePartType::FractionalSecondDigits: return &JSAtomState::fractionalSecond; case mozilla::intl::DateTimePartType::Unknown: return &JSAtomState::unknown; } MOZ_CRASH( "unenumerated, undocumented format field returned " "by iterator"); } static FieldType GetFieldTypeForPartSource( mozilla::intl::DateTimePartSource source) { switch (source) { case mozilla::intl::DateTimePartSource::Shared: return &JSAtomState::shared; case mozilla::intl::DateTimePartSource::StartRange: return &JSAtomState::startRange; case mozilla::intl::DateTimePartSource::EndRange: return &JSAtomState::endRange; } MOZ_CRASH( "unenumerated, undocumented format field returned " "by iterator"); } // A helper function to create an ArrayObject from DateTimePart objects. // When hasNoSource is true, we don't need to create the ||Source|| property for // the DateTimePart object. static bool CreateDateTimePartArray( JSContext* cx, mozilla::Span formattedSpan, bool hasNoSource, const mozilla::intl::DateTimePartVector& parts, MutableHandleValue result) { RootedString overallResult(cx, NewStringCopy(cx, formattedSpan)); if (!overallResult) { return false; } Rooted partsArray( cx, NewDenseFullyAllocatedArray(cx, parts.length())); if (!partsArray) { return false; } partsArray->ensureDenseInitializedLength(0, parts.length()); if (overallResult->length() == 0) { // An empty string contains no parts, so avoid extra work below. result.setObject(*partsArray); return true; } RootedObject singlePart(cx); RootedValue val(cx); size_t index = 0; size_t beginIndex = 0; for (const mozilla::intl::DateTimePart& part : parts) { singlePart = NewPlainObject(cx); if (!singlePart) { return false; } FieldType type = GetFieldTypeForPartType(part.mType); val = StringValue(cx->names().*type); if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) { return false; } MOZ_ASSERT(part.mEndIndex > beginIndex); JSLinearString* partStr = NewDependentString(cx, overallResult, beginIndex, part.mEndIndex - beginIndex); if (!partStr) { return false; } val = StringValue(partStr); if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) { return false; } if (!hasNoSource) { FieldType source = GetFieldTypeForPartSource(part.mSource); val = StringValue(cx->names().*source); if (!DefineDataProperty(cx, singlePart, cx->names().source, val)) { return false; } } beginIndex = part.mEndIndex; partsArray->initDenseElement(index++, ObjectValue(*singlePart)); } MOZ_ASSERT(index == parts.length()); MOZ_ASSERT(beginIndex == formattedSpan.size()); result.setObject(*partsArray); return true; } static bool intl_FormatToPartsDateTime(JSContext* cx, const mozilla::intl::DateTimeFormat* df, ClippedTime x, bool hasNoSource, MutableHandleValue result) { MOZ_ASSERT(x.isValid()); FormatBuffer buffer(cx); mozilla::intl::DateTimePartVector parts; auto r = df->TryFormatToParts(x.toDouble(), buffer, parts); if (r.isErr()) { intl::ReportInternalError(cx, r.unwrapErr()); return false; } return CreateDateTimePartArray(cx, buffer, hasNoSource, parts, result); } 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 dateTimeFormat(cx); dateTimeFormat = &args[0].toObject().as(); 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; } mozilla::intl::DateTimeFormat* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat); if (!df) { return false; } // Use the DateTimeFormat to actually format the time stamp. return formatToParts ? intl_FormatToPartsDateTime( cx, df, x, /* hasNoSource */ true, args.rval()) : intl_FormatDateTime(cx, df, x, args.rval()); } /** * Returns a new DateIntervalFormat with the locale and date-time formatting * options of the given DateTimeFormat. */ static mozilla::intl::DateIntervalFormat* NewDateIntervalFormat( JSContext* cx, Handle dateTimeFormat, mozilla::intl::DateTimeFormat& mozDtf) { RootedValue value(cx); RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); if (!internals) { return nullptr; } FormatBuffer pattern(cx); auto result = mozDtf.GetPattern(pattern); if (result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return nullptr; } // Determine the hour cycle used in the resolved pattern. mozilla::Maybe hcPattern = mozilla::intl::DateTimeFormat::HourCycleFromPattern(pattern); 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 timeZoneChars = timeZone.twoByteRange(); FormatBuffer skeleton(cx); auto skelResult = mozDtf.GetOriginalSkeleton(skeleton); if (skelResult.isErr()) { intl::ReportInternalError(cx, skelResult.unwrapErr()); return nullptr; } auto dif = mozilla::intl::DateIntervalFormat::TryCreate( mozilla::MakeStringSpan(locale.get()), skeleton, timeZoneChars); if (dif.isErr()) { js::intl::ReportInternalError(cx, dif.unwrapErr()); return nullptr; } return dif.unwrap().release(); } static mozilla::intl::DateIntervalFormat* GetOrCreateDateIntervalFormat( JSContext* cx, Handle dateTimeFormat, mozilla::intl::DateTimeFormat& mozDtf) { // Obtain a cached DateIntervalFormat object. mozilla::intl::DateIntervalFormat* dif = dateTimeFormat->getDateIntervalFormat(); if (dif) { return dif; } dif = NewDateIntervalFormat(cx, dateTimeFormat, mozDtf); if (!dif) { return nullptr; } dateTimeFormat->setDateIntervalFormat(dif); intl::AddICUCellMemory( dateTimeFormat, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse); return dif; } /** * PartitionDateTimeRangePattern ( dateTimeFormat, x, y ) */ static bool PartitionDateTimeRangePattern( JSContext* cx, const mozilla::intl::DateTimeFormat* df, const mozilla::intl::DateIntervalFormat* dif, mozilla::intl::AutoFormattedDateInterval& formatted, ClippedTime x, ClippedTime y, bool* equal) { MOZ_ASSERT(x.isValid()); MOZ_ASSERT(y.isValid()); // 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; mozilla::intl::ICUResult result = Ok(); if (x.toDouble() < GregorianChangeDatePlusOneDay || y.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. auto startCal = df->CloneCalendar(x.toDouble()); if (startCal.isErr()) { intl::ReportInternalError(cx, startCal.unwrapErr()); return false; } auto endCal = df->CloneCalendar(y.toDouble()); if (endCal.isErr()) { intl::ReportInternalError(cx, endCal.unwrapErr()); return false; } result = dif->TryFormatCalendar(*startCal.unwrap(), *endCal.unwrap(), formatted, equal); } else { // The common fast path which doesn't require creating calendar objects. result = dif->TryFormatDateTime(x.toDouble(), y.toDouble(), formatted, equal); } if (result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return false; } return true; } /** * FormatDateTimeRange( dateTimeFormat, x, y ) */ static bool FormatDateTimeRange(JSContext* cx, const mozilla::intl::DateTimeFormat* df, const mozilla::intl::DateIntervalFormat* dif, ClippedTime x, ClippedTime y, MutableHandleValue result) { mozilla::intl::AutoFormattedDateInterval formatted; if (!formatted.IsValid()) { intl::ReportInternalError(cx, formatted.GetError()); return false; } bool equal; if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) { return false; } // PartitionDateTimeRangePattern, step 12. if (equal) { return intl_FormatDateTime(cx, df, x, result); } auto spanResult = formatted.ToSpan(); if (spanResult.isErr()) { intl::ReportInternalError(cx, spanResult.unwrapErr()); return false; } JSString* resultStr = NewStringCopy(cx, spanResult.unwrap()); if (!resultStr) { return false; } result.setString(resultStr); return true; } /** * FormatDateTimeRangeToParts ( dateTimeFormat, x, y ) */ static bool FormatDateTimeRangeToParts( JSContext* cx, const mozilla::intl::DateTimeFormat* df, const mozilla::intl::DateIntervalFormat* dif, ClippedTime x, ClippedTime y, MutableHandleValue result) { mozilla::intl::AutoFormattedDateInterval formatted; if (!formatted.IsValid()) { intl::ReportInternalError(cx, formatted.GetError()); return false; } bool equal; if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) { return false; } // PartitionDateTimeRangePattern, step 12. if (equal) { return intl_FormatToPartsDateTime(cx, df, x, /* hasNoSource */ false, result); } mozilla::intl::DateTimePartVector parts; auto r = dif->TryFormattedToParts(formatted, parts); if (r.isErr()) { intl::ReportInternalError(cx, r.unwrapErr()); return false; } auto spanResult = formatted.ToSpan(); if (spanResult.isErr()) { intl::ReportInternalError(cx, spanResult.unwrapErr()); return false; } return CreateDateTimePartArray(cx, spanResult.unwrap(), /* hasNoSource */ false, parts, result); } 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 dateTimeFormat(cx); dateTimeFormat = &args[0].toObject().as(); 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; } mozilla::intl::DateTimeFormat* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat); if (!df) { return false; } mozilla::intl::DateIntervalFormat* dif = GetOrCreateDateIntervalFormat(cx, dateTimeFormat, *df); if (!dif) { return false; } // Use the DateIntervalFormat to actually format the time range. return formatToParts ? FormatDateTimeRangeToParts(cx, df, dif, x, y, args.rval()) : FormatDateTimeRange(cx, df, dif, x, y, args.rval()); }