Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
/* 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/Span.h"
#include "builtin/Array.h"
#include "builtin/Date.h"
#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/FormatBuffer.h"
#include "builtin/intl/LanguageTag.h"
#include "builtin/intl/LocaleNegotiation.h"
#include "builtin/intl/ParameterNegotiation.h"
#include "builtin/intl/SharedIntlData.h"
#include "builtin/intl/UsingEnum.h"
#include "builtin/temporal/Calendar.h"
#include "builtin/temporal/Instant.h"
#include "builtin/temporal/PlainDate.h"
#include "builtin/temporal/PlainDateTime.h"
#include "builtin/temporal/PlainMonthDay.h"
#include "builtin/temporal/PlainTime.h"
#include "builtin/temporal/PlainYearMonth.h"
#include "builtin/temporal/Temporal.h"
#include "builtin/temporal/TemporalParser.h"
#include "builtin/temporal/TimeZone.h"
#include "builtin/temporal/ZonedDateTime.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 "js/Wrapper.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/Warnings.h"
#include "vm/GeckoProfiler-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"
using namespace js;
using namespace js::intl;
using namespace js::temporal;
using JS::ClippedTime;
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_BACKGROUND_FINALIZE,
&DateTimeFormatObject::classOps_,
&DateTimeFormatObject::classSpec_,
};
const JSClass& DateTimeFormatObject::protoClass_ = PlainObject::class_;
static bool dateTimeFormat_supportedLocalesOf(JSContext* cx, unsigned argc,
Value* vp);
static bool dateTimeFormat_format(JSContext* cx, unsigned argc, Value* vp);
static bool dateTimeFormat_formatToParts(JSContext* cx, unsigned argc,
Value* vp);
static bool dateTimeFormat_formatRange(JSContext* cx, unsigned argc, Value* vp);
static bool dateTimeFormat_formatRangeToParts(JSContext* cx, unsigned argc,
Value* vp);
static bool dateTimeFormat_resolvedOptions(JSContext* cx, unsigned argc,
Value* vp);
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_FN("supportedLocalesOf", dateTimeFormat_supportedLocalesOf, 1, 0),
JS_FS_END,
};
static const JSFunctionSpec dateTimeFormat_methods[] = {
JS_FN("resolvedOptions", dateTimeFormat_resolvedOptions, 0, 0),
JS_FN("formatToParts", dateTimeFormat_formatToParts, 1, 0),
JS_FN("formatRange", dateTimeFormat_formatRange, 2, 0),
JS_FN("formatRangeToParts", dateTimeFormat_formatRangeToParts, 2, 0),
JS_FN("toSource", dateTimeFormat_toSource, 0, 0),
JS_FS_END,
};
static const JSPropertySpec dateTimeFormat_properties[] = {
JS_PSG("format", dateTimeFormat_format, 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,
};
static constexpr std::string_view HourCycleToString(
DateTimeFormatOptions::HourCycle hourCycle) {
#ifndef USING_ENUM
using enum DateTimeFormatOptions::HourCycle;
#else
USING_ENUM(DateTimeFormatOptions::HourCycle, H11, H12, H23, H24);
#endif
switch (hourCycle) {
case H11:
return "h11";
case H12:
return "h12";
case H23:
return "h23";
case H24:
return "h24";
}
MOZ_CRASH("invalid date time format hour cycle");
}
template <typename DateOrTimeStyle>
static constexpr std::string_view DateOrTimeStyleToString(
DateOrTimeStyle dateOrTimeStyle) {
switch (dateOrTimeStyle) {
case DateOrTimeStyle::Full:
return "full";
case DateOrTimeStyle::Long:
return "long";
case DateOrTimeStyle::Medium:
return "medium";
case DateOrTimeStyle::Short:
return "short";
}
MOZ_CRASH("invalid date time format date or time style");
}
static constexpr std::string_view DateStyleToString(
DateTimeFormatOptions::DateStyle dateStyle) {
return DateOrTimeStyleToString(dateStyle);
}
static constexpr std::string_view TimeStyleToString(
DateTimeFormatOptions::TimeStyle timeStyle) {
return DateOrTimeStyleToString(timeStyle);
}
template <typename TextComponent>
static constexpr std::string_view TextComponentToString(
TextComponent textComponent) {
switch (textComponent) {
case TextComponent::Narrow:
return "narrow";
case TextComponent::Short:
return "short";
case TextComponent::Long:
return "long";
}
MOZ_CRASH("invalid date time format text component");
}
template <typename NumericComponent>
static constexpr std::string_view NumericComponentToString(
NumericComponent numericComponent) {
switch (numericComponent) {
case NumericComponent::TwoDigit:
return "2-digit";
case NumericComponent::Numeric:
return "numeric";
}
MOZ_CRASH("invalid date time format numeric component");
}
static constexpr std::string_view WeekdayToString(
DateTimeFormatOptions::Weekday weekday) {
return TextComponentToString(weekday);
}
static constexpr std::string_view EraToString(DateTimeFormatOptions::Era era) {
return TextComponentToString(era);
}
static constexpr std::string_view YearToString(
DateTimeFormatOptions::Year year) {
return NumericComponentToString(year);
}
static constexpr std::string_view MonthToString(
DateTimeFormatOptions::Month month) {
#ifndef USING_ENUM
using enum DateTimeFormatOptions::Month;
#else
USING_ENUM(DateTimeFormatOptions::Month, TwoDigit, Numeric, Narrow, Short,
Long);
#endif
switch (month) {
case TwoDigit:
return "2-digit";
case Numeric:
return "numeric";
case Narrow:
return "narrow";
case Short:
return "short";
case Long:
return "long";
}
MOZ_CRASH("invalid date time format month");
}
static constexpr std::string_view DayToString(DateTimeFormatOptions::Day day) {
return NumericComponentToString(day);
}
static constexpr std::string_view DayPeriodToString(
DateTimeFormatOptions::DayPeriod dayPeriod) {
return TextComponentToString(dayPeriod);
}
static constexpr std::string_view HourToString(
DateTimeFormatOptions::Hour hour) {
return NumericComponentToString(hour);
}
static constexpr std::string_view MinuteToString(
DateTimeFormatOptions::Minute minute) {
return NumericComponentToString(minute);
}
static constexpr std::string_view SecondToString(
DateTimeFormatOptions::Second second) {
return NumericComponentToString(second);
}
static constexpr std::string_view TimeZoneNameToString(
DateTimeFormatOptions::TimeZoneName timeZoneName) {
#ifndef USING_ENUM
using enum DateTimeFormatOptions::TimeZoneName;
#else
USING_ENUM(DateTimeFormatOptions::TimeZoneName, Short, Long, ShortOffset,
LongOffset, ShortGeneric, LongGeneric);
#endif
switch (timeZoneName) {
case Short:
return "short";
case Long:
return "long";
case ShortOffset:
return "shortOffset";
case LongOffset:
return "longOffset";
case ShortGeneric:
return "shortGeneric";
case LongGeneric:
return "longGeneric";
}
MOZ_CRASH("invalid date time format time zone name");
}
enum class FormatMatcher { Basic, BestFit };
static constexpr std::string_view FormatMatcherToString(
FormatMatcher formatMatcher) {
#ifndef USING_ENUM
using enum FormatMatcher;
#else
USING_ENUM(FormatMatcher, Basic, BestFit);
#endif
switch (formatMatcher) {
case Basic:
return "basic";
case BestFit:
return "best fit";
}
MOZ_CRASH("invalid format matcher");
}
enum class DateTimeFormatConstructorKind {
Standard,
EnableMozExtensions,
};
/**
* CreateDateTimeFormat ( newTarget, locales, options, required, defaults [ ,
* toLocaleStringTimeZone ] )
*/
static bool CreateDateTimeFormat(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
Handle<JS::Value> locales, Handle<JS::Value> optionsValue,
DateTimeFormatOptions::Required required,
DateTimeFormatOptions::Defaults defaults,
Handle<JSLinearString*> toLocaleStringTimeZone = nullptr,
DateTimeFormatConstructorKind dtfKind =
DateTimeFormatConstructorKind::Standard) {
// Step 1. (Performed in caller)
// Steps 2-4. (Inlined ResolveOptions)
// ResolveOptions, step 1.
Rooted<LocalesList> requestedLocales(cx, cx);
if (!CanonicalizeLocaleList(cx, locales, &requestedLocales)) {
return false;
}
Rooted<ArrayObject*> requestedLocalesArray(
cx, LocalesListToArray(cx, requestedLocales));
if (!requestedLocalesArray) {
return false;
}
dateTimeFormat->setRequestedLocales(requestedLocalesArray);
auto dtfOptions = cx->make_unique<DateTimeFormatOptions>();
if (!dtfOptions) {
return false;
}
dtfOptions->required = required;
dtfOptions->defaults = defaults;
if (!optionsValue.isUndefined()) {
// ResolveOptions, steps 2-3.
Rooted<JSObject*> options(cx, JS::ToObject(cx, optionsValue));
if (!options) {
return false;
}
// ResolveOptions, step 4.
LocaleMatcher matcher;
if (!GetLocaleMatcherOption(cx, options, &matcher)) {
return false;
}
// ResolveOptions, step 5.
//
// This implementation only supports the "lookup" locale matcher, therefore
// the "localeMatcher" option doesn't need to be stored.
// ResolveOptions, step 6.
Rooted<JSLinearString*> calendar(cx);
if (!GetUnicodeExtensionOption(cx, options, UnicodeExtensionKey::Calendar,
&calendar)) {
return false;
}
if (calendar) {
dateTimeFormat->setCalendar(calendar);
}
Rooted<JSLinearString*> numberingSystem(cx);
if (!GetUnicodeExtensionOption(cx, options,
UnicodeExtensionKey::NumberingSystem,
&numberingSystem)) {
return false;
}
if (numberingSystem) {
dateTimeFormat->setNumberingSystem(numberingSystem);
}
if (!GetBooleanOption(cx, options, cx->names().hour12,
&dtfOptions->hour12)) {
return false;
}
static constexpr auto hourCycles =
MapOptions<HourCycleToString>(DateTimeFormatOptions::HourCycle::H11,
DateTimeFormatOptions::HourCycle::H12,
DateTimeFormatOptions::HourCycle::H23,
DateTimeFormatOptions::HourCycle::H24);
if (!GetStringOption(cx, options, cx->names().hourCycle, hourCycles,
&dtfOptions->hourCycle)) {
return false;
}
// ResolveOptions, step 7.
if (dtfOptions->hour12.isSome()) {
// The "hourCycle" option is ignored if "hour12" is also present.
dtfOptions->hourCycle = mozilla::Nothing();
}
// ResolveOptions, step 8. (Performed in ResolveLocale)
// ResolveOptions, step 9. (Return)
// Step 5. (Not applicable when ResolveOptions is inlined.)
// Step 6-14. (Performed in ResolveLocale)
// Step 15.
Rooted<JS::Value> timeZoneOption(cx);
if (!GetProperty(cx, options, options, cx->names().timeZone,
&timeZoneOption)) {
return false;
}
// Steps 16-19.
JSLinearString* timeZone = nullptr;
if (timeZoneOption.isUndefined()) {
// Step 16.
if (toLocaleStringTimeZone) {
timeZone = toLocaleStringTimeZone;
} else {
timeZone = cx->global()->globalIntlData().defaultTimeZone(cx);
if (!timeZone) {
return false;
}
}
// Steps 18-19. (Not applicable in our implementation.)
} else {
// Step 17.
if (toLocaleStringTimeZone) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_INVALID_DATETIME_OPTION, "timeZone",
"Temporal.ZonedDateTime.toLocaleString");
return false;
}
Rooted<JSString*> timeZoneStr(cx, JS::ToString(cx, timeZoneOption));
if (!timeZoneStr) {
return false;
}
// Steps 18-19.
timeZone = temporal::ToValidCanonicalTimeZoneIdentifier(cx, timeZoneStr);
if (!timeZone) {
return false;
}
}
// Step 20.
dateTimeFormat->setTimeZone(timeZone);
if (dtfKind == DateTimeFormatConstructorKind::EnableMozExtensions) {
Rooted<JS::Value> patternOption(cx);
if (!GetProperty(cx, options, options, cx->names().pattern,
&patternOption)) {
return false;
}
if (!patternOption.isUndefined()) {
auto* pattern = JS::ToString(cx, patternOption);
if (!pattern) {
return false;
}
dateTimeFormat->setPattern(pattern);
}
}
// Steps 21-22. (Not applicable in our implementation.)
// Step 23. (Moved below)
// Step 24.
static constexpr auto weekdays =
MapOptions<WeekdayToString>(DateTimeFormatOptions::Weekday::Narrow,
DateTimeFormatOptions::Weekday::Short,
DateTimeFormatOptions::Weekday::Long);
if (!GetStringOption(cx, options, cx->names().weekday, weekdays,
&dtfOptions->weekday)) {
return false;
}
static constexpr auto eras = MapOptions<EraToString>(
DateTimeFormatOptions::Era::Narrow, DateTimeFormatOptions::Era::Short,
DateTimeFormatOptions::Era::Long);
if (!GetStringOption(cx, options, cx->names().era, eras,
&dtfOptions->era)) {
return false;
}
static constexpr auto years =
MapOptions<YearToString>(DateTimeFormatOptions::Year::TwoDigit,
DateTimeFormatOptions::Year::Numeric);
if (!GetStringOption(cx, options, cx->names().year, years,
&dtfOptions->year)) {
return false;
}
static constexpr auto months =
MapOptions<MonthToString>(DateTimeFormatOptions::Month::TwoDigit,
DateTimeFormatOptions::Month::Numeric,
DateTimeFormatOptions::Month::Narrow,
DateTimeFormatOptions::Month::Short,
DateTimeFormatOptions::Month::Long);
if (!GetStringOption(cx, options, cx->names().month, months,
&dtfOptions->month)) {
return false;
}
static constexpr auto days =
MapOptions<DayToString>(DateTimeFormatOptions::Day::TwoDigit,
DateTimeFormatOptions::Day::Numeric);
if (!GetStringOption(cx, options, cx->names().day, days,
&dtfOptions->day)) {
return false;
}
static constexpr auto dayPeriods =
MapOptions<DayPeriodToString>(DateTimeFormatOptions::DayPeriod::Narrow,
DateTimeFormatOptions::DayPeriod::Short,
DateTimeFormatOptions::DayPeriod::Long);
if (!GetStringOption(cx, options, cx->names().dayPeriod, dayPeriods,
&dtfOptions->dayPeriod)) {
return false;
}
static constexpr auto hours =
MapOptions<HourToString>(DateTimeFormatOptions::Hour::TwoDigit,
DateTimeFormatOptions::Hour::Numeric);
if (!GetStringOption(cx, options, cx->names().hour, hours,
&dtfOptions->hour)) {
return false;
}
static constexpr auto minutes =
MapOptions<MinuteToString>(DateTimeFormatOptions::Minute::TwoDigit,
DateTimeFormatOptions::Minute::Numeric);
if (!GetStringOption(cx, options, cx->names().minute, minutes,
&dtfOptions->minute)) {
return false;
}
static constexpr auto seconds =
MapOptions<SecondToString>(DateTimeFormatOptions::Second::TwoDigit,
DateTimeFormatOptions::Second::Numeric);
if (!GetStringOption(cx, options, cx->names().second, seconds,
&dtfOptions->second)) {
return false;
}
mozilla::Maybe<int32_t> fractionalSecondDigits{};
if (!GetNumberOption(cx, options, cx->names().fractionalSecondDigits, 1, 3,
&fractionalSecondDigits)) {
return false;
}
dtfOptions->fractionalSecondDigits = fractionalSecondDigits;
static constexpr auto timeZoneNames = MapOptions<TimeZoneNameToString>(
DateTimeFormatOptions::TimeZoneName::Short,
DateTimeFormatOptions::TimeZoneName::Long,
DateTimeFormatOptions::TimeZoneName::ShortOffset,
DateTimeFormatOptions::TimeZoneName::LongOffset,
DateTimeFormatOptions::TimeZoneName::ShortGeneric,
DateTimeFormatOptions::TimeZoneName::LongGeneric);
if (!GetStringOption(cx, options, cx->names().timeZoneName, timeZoneNames,
&dtfOptions->timeZoneName)) {
return false;
}
// Step 25.
//
// This implementation only supports the "best fit" format matcher,
// therefore the "formatMatcher" option doesn't need to be stored.
//
static constexpr auto formatMatchers = MapOptions<FormatMatcherToString>(
FormatMatcher::Basic, FormatMatcher::BestFit);
FormatMatcher formatMatcher;
if (!GetStringOption(cx, options, cx->names().formatMatcher, formatMatchers,
FormatMatcher::BestFit, &formatMatcher)) {
return false;
}
// Steps 26-27.
static constexpr auto dateStyles =
MapOptions<DateStyleToString>(DateTimeFormatOptions::DateStyle::Full,
DateTimeFormatOptions::DateStyle::Long,
DateTimeFormatOptions::DateStyle::Medium,
DateTimeFormatOptions::DateStyle::Short);
if (!GetStringOption(cx, options, cx->names().dateStyle, dateStyles,
&dtfOptions->dateStyle)) {
return false;
}
// Steps 28-29.
static constexpr auto timeStyles =
MapOptions<TimeStyleToString>(DateTimeFormatOptions::TimeStyle::Full,
DateTimeFormatOptions::TimeStyle::Long,
DateTimeFormatOptions::TimeStyle::Medium,
DateTimeFormatOptions::TimeStyle::Short);
if (!GetStringOption(cx, options, cx->names().timeStyle, timeStyles,
&dtfOptions->timeStyle)) {
return false;
}
// Step 30.
if (dtfOptions->dateStyle.isSome() || dtfOptions->timeStyle.isSome()) {
// Step 23.
const char* explicitFormatComponent = ([&]() -> const char* {
if (dtfOptions->weekday.isSome()) {
return "weekday";
}
if (dtfOptions->era.isSome()) {
return "era";
}
if (dtfOptions->year.isSome()) {
return "year";
}
if (dtfOptions->month.isSome()) {
return "month";
}
if (dtfOptions->day.isSome()) {
return "day";
}
if (dtfOptions->dayPeriod.isSome()) {
return "dayPeriod";
}
if (dtfOptions->hour.isSome()) {
return "hour";
}
if (dtfOptions->minute.isSome()) {
return "minute";
}
if (dtfOptions->second.isSome()) {
return "second";
}
if (dtfOptions->fractionalSecondDigits.isSome()) {
return "fractionalSecondDigits";
}
if (dtfOptions->timeZoneName.isSome()) {
return "timeZoneName";
}
return nullptr;
})();
// Step 30.a.
if (explicitFormatComponent) {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_INVALID_DATETIME_OPTION,
explicitFormatComponent,
dtfOptions->dateStyle.isSome() ? "dateStyle" : "timeStyle");
return false;
}
// Step 30.b.
if (required == DateTimeFormatOptions::Required::Date &&
dtfOptions->timeStyle.isSome()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_INVALID_DATETIME_STYLE, "timeStyle",
"date");
return false;
}
// Step 30.c.
if (required == DateTimeFormatOptions::Required::Time &&
dtfOptions->dateStyle.isSome()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_INVALID_DATETIME_STYLE, "dateStyle",
"time");
return false;
}
}
// Steps 31-33. (See ResolveDateTimeFormat)
} else {
// Absent |options| object only requires to store the current time zone.
// Step 16.
JSLinearString* timeZone;
if (toLocaleStringTimeZone) {
timeZone = toLocaleStringTimeZone;
} else {
timeZone = cx->global()->globalIntlData().defaultTimeZone(cx);
if (!timeZone) {
return false;
}
}
// Step 20.
dateTimeFormat->setTimeZone(timeZone);
}
dateTimeFormat->setOptions(dtfOptions.release());
AddCellMemory(dateTimeFormat, sizeof(DateTimeFormatOptions),
MemoryUse::IntlOptions);
// Step 34.
return true;
}
/**
* 12.2.1 Intl.DateTimeFormat([ locales [, options]])
*
* ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
*/
static bool DateTimeFormat(JSContext* cx, const CallArgs& args,
DateTimeFormatOptions::Required required,
DateTimeFormatOptions::Defaults defaults,
DateTimeFormatConstructorKind dtfKind) {
AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.DateTimeFormat");
// Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
// Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
JSProtoKey protoKey = dtfKind == DateTimeFormatConstructorKind::Standard
? JSProto_DateTimeFormat
: JSProto_Null;
Rooted<JSObject*> proto(cx);
if (!GetPrototypeFromBuiltinConstructor(cx, args, protoKey, &proto)) {
return false;
}
Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
dateTimeFormat = NewObjectWithClassProto<DateTimeFormatObject>(cx, proto);
if (!dateTimeFormat) {
return false;
}
// Step 2.
if (!CreateDateTimeFormat(cx, dateTimeFormat, args.get(0), args.get(1),
required, defaults, nullptr, dtfKind)) {
return false;
}
// Step 3.
if (dtfKind == DateTimeFormatConstructorKind::Standard) {
return ChainLegacyIntlFormat(cx, JSProto_DateTimeFormat, args,
dateTimeFormat);
}
// Step 4.
args.rval().setObject(*dateTimeFormat);
return true;
}
static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
auto required = DateTimeFormatOptions::Required::Any;
auto defaults = DateTimeFormatOptions::Defaults::Date;
return DateTimeFormat(cx, args, required, defaults,
DateTimeFormatConstructorKind::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;
}
auto required = DateTimeFormatOptions::Required::Any;
auto defaults = DateTimeFormatOptions::Defaults::Date;
return DateTimeFormat(cx, args, required, defaults,
DateTimeFormatConstructorKind::EnableMozExtensions);
}
static auto ToRequired(DateTimeFormatKind kind) {
#ifndef USING_ENUM
using enum DateTimeFormatOptions::Required;
#else
USING_ENUM(DateTimeFormatOptions::Required, Any, Date, Time);
#endif
switch (kind) {
case DateTimeFormatKind::All:
return Any;
case DateTimeFormatKind::Date:
return Date;
case DateTimeFormatKind::Time:
return Time;
}
MOZ_CRASH("invalid date time format kind");
}
static auto ToDefaults(DateTimeFormatKind kind) {
#ifndef USING_ENUM
using enum DateTimeFormatOptions::Defaults;
#else
USING_ENUM(DateTimeFormatOptions::Defaults, All, Date, Time);
#endif
switch (kind) {
case DateTimeFormatKind::All:
return All;
case DateTimeFormatKind::Date:
return Date;
case DateTimeFormatKind::Time:
return Time;
}
MOZ_CRASH("invalid date time format kind");
}
static DateTimeFormatObject* CreateDateTimeFormat(
JSContext* cx, Handle<Value> locales, Handle<Value> options,
Handle<JSLinearString*> toLocaleStringTimeZone, DateTimeFormatKind kind) {
Rooted<DateTimeFormatObject*> dateTimeFormat(
cx, NewBuiltinClassInstance<DateTimeFormatObject>(cx));
if (!dateTimeFormat) {
return nullptr;
}
auto required = ToRequired(kind);
auto defaults = ToDefaults(kind);
if (!CreateDateTimeFormat(cx, dateTimeFormat, locales, options, required,
defaults, toLocaleStringTimeZone,
DateTimeFormatConstructorKind::Standard)) {
return nullptr;
}
return dateTimeFormat;
}
DateTimeFormatObject* js::intl::CreateDateTimeFormat(JSContext* cx,
Handle<Value> locales,
Handle<Value> options,
DateTimeFormatKind kind) {
return CreateDateTimeFormat(cx, locales, options, nullptr, kind);
}
DateTimeFormatObject* js::intl::GetOrCreateDateTimeFormat(
JSContext* cx, Handle<Value> locales, Handle<Value> options,
DateTimeFormatKind kind) {
// Try to use a cached instance when |locales| is either undefined or a
// string, and |options| is undefined.
if ((locales.isUndefined() || locales.isString()) && options.isUndefined()) {
Rooted<JSLinearString*> locale(cx);
if (locales.isString()) {
locale = locales.toString()->ensureLinear(cx);
if (!locale) {
return nullptr;
}
}
return cx->global()->globalIntlData().getOrCreateDateTimeFormat(cx, kind,
locale);
}
// Create a new Intl.DateTimeFormat instance.
return CreateDateTimeFormat(cx, locales, options, nullptr, kind);
}
void js::intl::DateTimeFormatObject::finalize(JS::GCContext* gcx,
JSObject* obj) {
auto* dateTimeFormat = &obj->as<DateTimeFormatObject>();
auto* df = dateTimeFormat->getDateFormat();
auto* dif = dateTimeFormat->getDateIntervalFormat();
if (auto* options = dateTimeFormat->getOptions()) {
gcx->delete_(obj, options, MemoryUse::IntlOptions);
}
if (df) {
RemoveICUCellMemory(gcx, obj,
DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
delete df;
}
if (dif) {
RemoveICUCellMemory(
gcx, obj, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
delete dif;
}
}
bool JS::AddMozDateTimeFormatConstructor(JSContext* cx,
JS::Handle<JSObject*> intl) {
Rooted<JSObject*> ctor(
cx, GlobalObject::createConstructor(cx, MozDateTimeFormat,
cx->names().DateTimeFormat, 0));
if (!ctor) {
return false;
}
Rooted<JSObject*> 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;
}
Rooted<JS::Value> ctorValue(cx, ObjectValue(*ctor));
return DefineDataProperty(cx, intl, cx->names().DateTimeFormat, ctorValue, 0);
}
/**
* Resolve the actual locale to finish initialization of the DateTimeFormat.
*/
static bool ResolveLocale(JSContext* cx,
Handle<DateTimeFormatObject*> dateTimeFormat) {
// Return if the locale was already resolved.
if (dateTimeFormat->isLocaleResolved()) {
return true;
}
auto* dtfOptions = dateTimeFormat->getOptions();
Rooted<ArrayObject*> requestedLocales(
cx, &dateTimeFormat->getRequestedLocales()->as<ArrayObject>());
// %Intl.DateTimeFormat%.[[RelevantExtensionKeys]] is « "ca", "hc", "nu" ».
mozilla::EnumSet<UnicodeExtensionKey> relevantExtensionKeys{
UnicodeExtensionKey::Calendar,
UnicodeExtensionKey::HourCycle,
UnicodeExtensionKey::NumberingSystem,
};
// Initialize locale options from constructor arguments.
Rooted<LocaleOptions> localeOptions(cx);
if (auto* ca = dateTimeFormat->getCalendar()) {
localeOptions.setUnicodeExtension(UnicodeExtensionKey::Calendar, ca);
}
if (dtfOptions->hour12.isSome()) {
// Explicitly opt-out of hourCycle if the hour12 option is present, because
// the latter takes precedence over hourCycle.
localeOptions.setUnicodeExtension(UnicodeExtensionKey::HourCycle, nullptr);
} else {
if (auto hourCycle = dtfOptions->hourCycle) {
#ifndef USING_ENUM
using enum DateTimeFormatOptions::HourCycle;
#else
USING_ENUM(DateTimeFormatOptions::HourCycle, H11, H12, H23, H24);
#endif
JSLinearString* hc;
switch (*hourCycle) {
case H11:
hc = cx->names().h11;
break;
case H12:
hc = cx->names().h12;
break;
case H23:
hc = cx->names().h23;
break;
case H24:
hc = cx->names().h24;
break;
}
localeOptions.setUnicodeExtension(UnicodeExtensionKey::HourCycle, hc);
}
}
if (auto* nu = dateTimeFormat->getNumberingSystem()) {
localeOptions.setUnicodeExtension(UnicodeExtensionKey::NumberingSystem, nu);
}
// Use the default locale data.
auto localeData = LocaleData::Default;
// Resolve the actual locale.
Rooted<ResolvedLocale> resolved(cx);
if (!ResolveLocale(cx, AvailableLocaleKind::DateTimeFormat, requestedLocales,
localeOptions, relevantExtensionKeys, localeData,
&resolved)) {
return false;
}
// Finish initialization by setting the actual locale and extensions.
// Changes from "Intl era and monthCode" proposal.
//
auto ca = resolved.extension(UnicodeExtensionKey::Calendar);
MOZ_ASSERT(ca, "resolved calendar is non-null");
if (StringEqualsLiteral(ca, "islamic")) {
if (!WarnNumberASCII(cx, JSMSG_ISLAMIC_FALLBACK)) {
return false;
}
// Fallback to "islamic-tbla" calendar.
auto* str = NewStringCopyZ<CanGC>(cx, "islamic-tbla");
if (!str) {
return false;
}
dateTimeFormat->setCalendar(str);
} else if (StringEqualsLiteral(ca, "islamic-rgsa")) {
// Fallback to "islamic-tbla" calendar for 147 uplift compatibility.
// The above warning text isn't suitable, and per 2025-12 TG2 meeting
auto* str = NewStringCopyZ<CanGC>(cx, "islamic-tbla");
if (!str) {
return false;
}
dateTimeFormat->setCalendar(str);
} else {
dateTimeFormat->setCalendar(ca);
}
auto hc = resolved.extension(UnicodeExtensionKey::HourCycle);
if (hc) {
MOZ_ASSERT(dtfOptions->hour12.isNothing());
#ifndef USING_ENUM
using enum DateTimeFormatOptions::HourCycle;
#else
USING_ENUM(DateTimeFormatOptions::HourCycle, H11, H12, H23, H24);
#endif
if (StringEqualsLiteral(hc, "h11")) {
dtfOptions->hourCycle = mozilla::Some(H11);
} else if (StringEqualsLiteral(hc, "h12")) {
dtfOptions->hourCycle = mozilla::Some(H12);
} else if (StringEqualsLiteral(hc, "h23")) {
dtfOptions->hourCycle = mozilla::Some(H23);
} else {
MOZ_ASSERT(StringEqualsLiteral(hc, "h24"));
dtfOptions->hourCycle = mozilla::Some(H24);
}
}
auto nu = resolved.extension(UnicodeExtensionKey::NumberingSystem);
MOZ_ASSERT(nu, "resolved numbering system is non-null");
dateTimeFormat->setNumberingSystem(nu);
auto* locale = resolved.toLocale(cx);
if (!locale) {
return false;
}
dateTimeFormat->setLocale(locale);
MOZ_ASSERT(dateTimeFormat->isLocaleResolved(),
"locale successfully resolved");
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, Handle<DateTimeFormatObject*> dateTimeFormat,
mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hourCycle =
mozilla::Nothing()) {
MOZ_ASSERT(dateTimeFormat->isLocaleResolved());
// ICU expects calendar, numberingSystem, and hourCycle as Unicode locale
// extensions on locale.
JS::RootedVector<UnicodeExtensionKeyword> keywords(cx);
if (!keywords.emplaceBack("ca", dateTimeFormat->getCalendar())) {
return nullptr;
}
if (!keywords.emplaceBack("nu", dateTimeFormat->getNumberingSystem())) {
return nullptr;
}
if (hourCycle) {
#ifndef USING_ENUM
using enum mozilla::intl::DateTimeFormat::HourCycle;
#else
USING_ENUM(mozilla::intl::DateTimeFormat::HourCycle, H11, H12, H23, H24);
#endif
JSAtom* hourCycleStr;
switch (*hourCycle) {
case H11:
hourCycleStr = cx->names().h11;
break;
case H12:
hourCycleStr = cx->names().h12;
break;
case H23:
hourCycleStr = cx->names().h23;
break;
case H24:
hourCycleStr = cx->names().h24;
break;
}
if (!keywords.emplaceBack("hc", hourCycleStr)) {
return nullptr;
}
}
Rooted<JSLinearString*> localeStr(cx, dateTimeFormat->getLocale());
return FormatLocale(cx, localeStr, keywords);
}
template <typename TextComponent>
static auto ToTextComponent(TextComponent value) {
#ifndef USING_ENUM
using enum mozilla::intl::DateTimeFormat::Text;
#else
USING_ENUM(mozilla::intl::DateTimeFormat::Text, Narrow, Short, Long);
#endif
switch (value) {
case TextComponent::Narrow:
return Narrow;
case TextComponent::Short:
return Short;
case TextComponent::Long:
return Long;
}
MOZ_CRASH("invalid text component");
}
template <typename NumericComponent>
static auto ToNumericComponent(NumericComponent value) {
#ifndef USING_ENUM
using enum mozilla::intl::DateTimeFormat::Numeric;
#else
USING_ENUM(mozilla::intl::DateTimeFormat::Numeric, TwoDigit, Numeric);
#endif
switch (value) {
case NumericComponent::TwoDigit:
return TwoDigit;
case NumericComponent::Numeric:
return Numeric;
}
MOZ_CRASH("invalid numeric component");
}
static auto ToMonthComponent(DateTimeFormatOptions::Month value) {
#ifndef USING_ENUM
using enum mozilla::intl::DateTimeFormat::Month;
#else
USING_ENUM(mozilla::intl::DateTimeFormat::Month, TwoDigit, Numeric, Narrow,
Short, Long);
#endif
switch (value) {
case DateTimeFormatOptions::Month::TwoDigit:
return TwoDigit;
case DateTimeFormatOptions::Month::Numeric:
return Numeric;
case DateTimeFormatOptions::Month::Narrow:
return Narrow;
case DateTimeFormatOptions::Month::Short:
return Short;
case DateTimeFormatOptions::Month::Long:
return Long;
}
MOZ_CRASH("invalid month component");
}
static auto ToTimeZoneNameComponent(DateTimeFormatOptions::TimeZoneName value) {
#ifndef USING_ENUM
using enum mozilla::intl::DateTimeFormat::TimeZoneName;
#else
USING_ENUM(mozilla::intl::DateTimeFormat::TimeZoneName, Short, Long,
ShortOffset, LongOffset, ShortGeneric, LongGeneric);
#endif
switch (value) {
case DateTimeFormatOptions::TimeZoneName::Short:
return Short;
case DateTimeFormatOptions::TimeZoneName::Long:
return Long;
case DateTimeFormatOptions::TimeZoneName::ShortOffset:
return ShortOffset;
case DateTimeFormatOptions::TimeZoneName::LongOffset:
return LongOffset;
case DateTimeFormatOptions::TimeZoneName::ShortGeneric:
return ShortGeneric;
case DateTimeFormatOptions::TimeZoneName::LongGeneric:
return LongGeneric;
}
MOZ_CRASH("invalid time zone name component");
}
static auto ToHourCycleComponent(DateTimeFormatOptions::HourCycle value) {
#ifndef USING_ENUM
using enum mozilla::intl::DateTimeFormat::HourCycle;
#else
USING_ENUM(mozilla::intl::DateTimeFormat::HourCycle, H11, H12, H23, H24);
#endif
switch (value) {
case DateTimeFormatOptions::HourCycle::H11:
return H11;
case DateTimeFormatOptions::HourCycle::H12:
return H12;
case DateTimeFormatOptions::HourCycle::H23:
return H23;
case DateTimeFormatOptions::HourCycle::H24:
return H24;
}
MOZ_CRASH("invalid hour cycle component");
}
template <typename DateTimeStyle>
static auto ToDateTimeStyle(DateTimeStyle value) {
#ifndef USING_ENUM
using enum mozilla::intl::DateTimeFormat::Style;
#else
USING_ENUM(mozilla::intl::DateTimeFormat::Style, Full, Long, Medium, Short);
#endif
switch (value) {
case DateTimeStyle::Full:
return Full;
case DateTimeStyle::Long:
return Long;
case DateTimeStyle::Medium:
return Medium;
case DateTimeStyle::Short:
return Short;
}
MOZ_CRASH("invalid date/time style");
}
enum class Required { Date, Time, YearMonth, MonthDay, Any };
enum class Defaults { Date, Time, YearMonth, MonthDay, ZonedDateTime, All };
enum class Inherit { All, Relevant };
struct DateTimeFormatArgs {
Required required;
Defaults defaults;
Inherit inherit;
};
/**
* Get the "required" argument passed to CreateDateTimeFormat.
*/
static auto GetRequired(DateTimeFormatOptions::Required required) {
#ifndef USING_ENUM
using enum Required;
#else
USING_ENUM(Required, Date, Time, Any);
#endif
switch (required) {
case DateTimeFormatOptions::Required::Date:
return Date;
case DateTimeFormatOptions::Required::Time:
return Time;
case DateTimeFormatOptions::Required::Any:
return Any;
}
MOZ_CRASH("invalid date time format required");
}
/**
* Get the "defaults" argument passed to CreateDateTimeFormat.
*/
static auto GetDefaults(DateTimeFormatOptions::Defaults defaults) {
#ifndef USING_ENUM
using enum Defaults;
#else
USING_ENUM(Defaults, Date, Time, All);
#endif
switch (defaults) {
case DateTimeFormatOptions::Defaults::Date:
return Date;
case DateTimeFormatOptions::Defaults::Time:
return Time;
case DateTimeFormatOptions::Defaults::All:
return All;
}
MOZ_CRASH("invalid date time format defaults");
}
/**
* Compute the (required, defaults, inherit) arguments passed to
* GetDateTimeFormat.
*/
static DateTimeFormatArgs GetDateTimeFormatArgs(
const DateTimeFormatOptions& options, DateTimeValueKind kind) {
switch (kind) {
case DateTimeValueKind::Number:
return {GetRequired(options.required), GetDefaults(options.defaults),
Inherit::All};
case DateTimeValueKind::TemporalDate:
return {Required::Date, Defaults::Date, Inherit::Relevant};
case DateTimeValueKind::TemporalTime:
return {Required::Time, Defaults::Time, Inherit::Relevant};
case DateTimeValueKind::TemporalDateTime:
return {Required::Any, Defaults::All, Inherit::Relevant};
case DateTimeValueKind::TemporalYearMonth:
return {Required::YearMonth, Defaults::YearMonth, Inherit::Relevant};
case DateTimeValueKind::TemporalMonthDay:
return {Required::MonthDay, Defaults::MonthDay, Inherit::Relevant};
case DateTimeValueKind::TemporalZonedDateTime:
return {Required::Any, Defaults::ZonedDateTime, Inherit::All};
case DateTimeValueKind::TemporalInstant:
return {Required::Any, Defaults::All, Inherit::All};
}
MOZ_CRASH("invalid date-time value kind");
}
enum class DateTimeField {
Weekday,
Era,
Year,
Month,
Day,
DayPeriod,
Hour,
Minute,
Second,
FractionalSecondDigits,
};
/**
* GetDateTimeFormat ( formats, matcher, options, required, defaults, inherit )
*
*/
static mozilla::Maybe<mozilla::intl::DateTimeFormat::ComponentsBag>
GetDateTimeFormat(const mozilla::intl::DateTimeFormat::ComponentsBag& options,
Required required, Defaults defaults, Inherit inherit) {
// Steps 1-5.
mozilla::EnumSet<DateTimeField> requiredOptions;
switch (required) {
case Required::Date:
requiredOptions = {
DateTimeField::Weekday,
DateTimeField::Year,
DateTimeField::Month,
DateTimeField::Day,
};
break;
case Required::Time:
requiredOptions = {
DateTimeField::DayPeriod,
DateTimeField::Hour,
DateTimeField::Minute,
DateTimeField::Second,
DateTimeField::FractionalSecondDigits,
};
break;
case Required::YearMonth:
requiredOptions = {
DateTimeField::Year,
DateTimeField::Month,
};
break;
case Required::MonthDay:
requiredOptions = {
DateTimeField::Month,
DateTimeField::Day,
};
break;
case Required::Any:
requiredOptions = {
DateTimeField::Weekday,
DateTimeField::Year,
DateTimeField::Month,
DateTimeField::Day,
DateTimeField::DayPeriod,
DateTimeField::Hour,
DateTimeField::Minute,
DateTimeField::Second,
DateTimeField::FractionalSecondDigits,
};
break;
}
MOZ_ASSERT(!requiredOptions.contains(DateTimeField::Era),
"standalone era not supported");
// Steps 6-10.
mozilla::EnumSet<DateTimeField> defaultOptions;
switch (defaults) {
case Defaults::Date:
defaultOptions = {
DateTimeField::Year,
DateTimeField::Month,
DateTimeField::Day,
};
break;
case Defaults::Time:
defaultOptions = {
DateTimeField::Hour,
DateTimeField::Minute,
DateTimeField::Second,
};
break;
case Defaults::YearMonth:
defaultOptions = {
DateTimeField::Year,
DateTimeField::Month,
};
break;
case Defaults::MonthDay:
defaultOptions = {
DateTimeField::Month,
DateTimeField::Day,
};
break;
case Defaults::ZonedDateTime:
case Defaults::All:
defaultOptions = {
DateTimeField::Year, DateTimeField::Month, DateTimeField::Day,
DateTimeField::Hour, DateTimeField::Minute, DateTimeField::Second,
};
break;
}
MOZ_ASSERT(!defaultOptions.contains(DateTimeField::Weekday));
MOZ_ASSERT(!defaultOptions.contains(DateTimeField::Era));
MOZ_ASSERT(!defaultOptions.contains(DateTimeField::DayPeriod));
MOZ_ASSERT(!defaultOptions.contains(DateTimeField::FractionalSecondDigits));
// Steps 11-12.
mozilla::intl::DateTimeFormat::ComponentsBag formatOptions;
if (inherit == Inherit::All) {
// Step 11.a.
formatOptions = options;
} else {
// Step 12.a. (Implicit)
// Step 12.b.
switch (required) {
case Required::Date:
case Required::YearMonth:
case Required::Any:
formatOptions.era = options.era;
break;
case Required::Time:
case Required::MonthDay:
// |era| option not applicable for these types.
break;
}
// Step 12.c.
switch (required) {
case Required::Time:
case Required::Any:
formatOptions.hourCycle = options.hourCycle;
formatOptions.hour12 = options.hour12;
break;
case Required::Date:
case Required::YearMonth:
case Required::MonthDay:
// |hourCycle| and |hour12| options not applicable for these types.
break;
}
}
// Steps 13-14.
bool anyPresent = options.weekday || options.year || options.month ||
options.day || options.dayPeriod || options.hour ||
options.minute || options.second ||
options.fractionalSecondDigits;
// Step 15.
bool needDefaults = true;
// Step 16. (Loop unrolled)
if (requiredOptions.contains(DateTimeField::Weekday) && options.weekday) {
formatOptions.weekday = options.weekday;
needDefaults = false;
}
if (requiredOptions.contains(DateTimeField::Year) && options.year) {
formatOptions.year = options.year;
needDefaults = false;
}
if (requiredOptions.contains(DateTimeField::Month) && options.month) {
formatOptions.month = options.month;
needDefaults = false;
}
if (requiredOptions.contains(DateTimeField::Day) && options.day) {
formatOptions.day = options.day;
needDefaults = false;
}
if (requiredOptions.contains(DateTimeField::DayPeriod) && options.dayPeriod) {
formatOptions.dayPeriod = options.dayPeriod;
needDefaults = false;
}
if (requiredOptions.contains(DateTimeField::Hour) && options.hour) {
formatOptions.hour = options.hour;
needDefaults = false;
}
if (requiredOptions.contains(DateTimeField::Minute) && options.minute) {
formatOptions.minute = options.minute;
needDefaults = false;
}
if (requiredOptions.contains(DateTimeField::Second) && options.second) {
formatOptions.second = options.second;
needDefaults = false;
}
if (requiredOptions.contains(DateTimeField::FractionalSecondDigits) &&
options.fractionalSecondDigits) {
formatOptions.fractionalSecondDigits = options.fractionalSecondDigits;
needDefaults = false;
}
// Step 17.
if (needDefaults) {
// Step 17.a.
if (anyPresent && inherit == Inherit::Relevant) {
return mozilla::Nothing();
}
// Step 17.b. (Loop unrolled)
auto numericOption =
mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::Numeric);
if (defaultOptions.contains(DateTimeField::Year)) {
formatOptions.year = numericOption;
}
if (defaultOptions.contains(DateTimeField::Month)) {
formatOptions.month =
mozilla::Some(mozilla::intl::DateTimeFormat::Month::Numeric);
}
if (defaultOptions.contains(DateTimeField::Day)) {
formatOptions.day = numericOption;
}
if (defaultOptions.contains(DateTimeField::Hour)) {
formatOptions.hour = numericOption;
}
if (defaultOptions.contains(DateTimeField::Minute)) {
formatOptions.minute = numericOption;
}
if (defaultOptions.contains(DateTimeField::Second)) {
formatOptions.second = numericOption;
}
// Step 17.c.
if (defaults == Defaults::ZonedDateTime && !formatOptions.timeZoneName) {
formatOptions.timeZoneName =
mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Short);
}
}
// Steps 18-20. (Performed in caller).
return mozilla::Some(formatOptions);
}
/**
* AdjustDateTimeStyleFormat ( formats, baseFormat, matcher, allowedOptions )
*
*/
static mozilla::Result<mozilla::intl::DateTimeFormat::ComponentsBag,
mozilla::intl::ICUError>
AdjustDateTimeStyleFormat(mozilla::intl::DateTimeFormat* baseFormat,
mozilla::EnumSet<DateTimeField> allowedOptions) {
// Step 1.
mozilla::intl::DateTimeFormat::ComponentsBag formatOptions;
// Step 2. (Loop unrolled)
auto result = baseFormat->ResolveComponents();
if (result.isErr()) {
return result.propagateErr();
}
auto options = result.unwrap();
if (allowedOptions.contains(DateTimeField::Era) && options.era) {
formatOptions.era = options.era;
}
if (allowedOptions.contains(DateTimeField::Weekday) && options.weekday) {
formatOptions.weekday = options.weekday;
}
if (allowedOptions.contains(DateTimeField::Year) && options.year) {
formatOptions.year = options.year;
}
if (allowedOptions.contains(DateTimeField::Month) && options.month) {
formatOptions.month = options.month;
}
if (allowedOptions.contains(DateTimeField::Day) && options.day) {
formatOptions.day = options.day;
}
if (allowedOptions.contains(DateTimeField::DayPeriod) && options.dayPeriod) {
formatOptions.dayPeriod = options.dayPeriod;
}
if (allowedOptions.contains(DateTimeField::Hour) && options.hour) {
formatOptions.hour = options.hour;
formatOptions.hourCycle = options.hourCycle;
}
if (allowedOptions.contains(DateTimeField::Minute) && options.minute) {
formatOptions.minute = options.minute;
}
if (allowedOptions.contains(DateTimeField::Second) && options.second) {
formatOptions.second = options.second;
}
if (allowedOptions.contains(DateTimeField::FractionalSecondDigits) &&
options.fractionalSecondDigits) {
formatOptions.fractionalSecondDigits = options.fractionalSecondDigits;
}
// Steps 3-5. (Performed in caller)
return formatOptions;
}
static const char* DateTimeValueKindToString(DateTimeValueKind kind) {
switch (kind) {
case DateTimeValueKind::Number:
return "number";
case DateTimeValueKind::TemporalDate:
return "Temporal.PlainDate";
case DateTimeValueKind::TemporalTime:
return "Temporal.PlainTime";
case DateTimeValueKind::TemporalDateTime:
return "Temporal.PlainDateTime";
case DateTimeValueKind::TemporalYearMonth:
return "Temporal.PlainYearMonth";
case DateTimeValueKind::TemporalMonthDay:
return "Temporal.PlainMonthDay";
case DateTimeValueKind::TemporalZonedDateTime:
return "Temporal.ZonedDateTime";
case DateTimeValueKind::TemporalInstant:
return "Temporal.Instant";
}
MOZ_CRASH("invalid date-time value kind");
}
class TimeZoneOffsetString {
static constexpr std::u16string_view GMT = u"GMT";
// Time zone offset string format is "±hh:mm".
static constexpr size_t offsetLength = 6;
// ICU custom time zones are in the format "GMT±hh:mm".
char16_t timeZone_[GMT.size() + offsetLength] = {};
TimeZoneOffsetString() = default;
public:
TimeZoneOffsetString(const TimeZoneOffsetString& other) { *this = other; }
TimeZoneOffsetString& operator=(const TimeZoneOffsetString& other) {
std::copy_n(other.timeZone_, std::size(timeZone_), timeZone_);
return *this;
}
operator mozilla::Span<const char16_t>() const {
return mozilla::Span(timeZone_);
}
static TimeZoneOffsetString GMTZero() {
static constexpr std::u16string_view gmtZero = u"GMT+00:00";
static_assert(gmtZero.length() ==
std::extent_v<decltype(TimeZoneOffsetString::timeZone_)>);
TimeZoneOffsetString result{};
gmtZero.copy(result.timeZone_, gmtZero.length());
return result;
}
/**
* |timeZone| is either a canonical IANA time zone identifier or a normalized
* time zone offset string.
*/
static mozilla::Maybe<TimeZoneOffsetString> from(
const JSLinearString* timeZone) {
MOZ_RELEASE_ASSERT(!timeZone->empty(), "time zone is a non-empty string");
// If the time zone string starts with either "+" or "-", it is a normalized
// time zone offset string, because (canonical) IANA time zone identifiers
// can't start with "+" or "-".
char16_t timeZoneSign = timeZone->latin1OrTwoByteChar(0);
MOZ_ASSERT(timeZoneSign != 0x2212,
"Minus sign is normalized to Ascii minus");
if (timeZoneSign != '+' && timeZoneSign != '-') {
return mozilla::Nothing();
}
// Release assert because we don't want CopyChars to write out-of-bounds.
MOZ_RELEASE_ASSERT(timeZone->length() == offsetLength);
// ToValidCanonicalTimeZoneIdentifier normalizes offset strings to the
// format "±hh:mm".
MOZ_ASSERT(mozilla::IsAsciiDigit(timeZone->latin1OrTwoByteChar(1)));
MOZ_ASSERT(mozilla::IsAsciiDigit(timeZone->latin1OrTwoByteChar(2)));
MOZ_ASSERT(timeZone->latin1OrTwoByteChar(3) == ':');
MOZ_ASSERT(mozilla::IsAsciiDigit(timeZone->latin1OrTwoByteChar(4)));
MOZ_ASSERT(mozilla::IsAsciiDigit(timeZone->latin1OrTwoByteChar(5)));
// ToValidCanonicalTimeZoneIdentifier verifies the offset is at most ±23:59.
#ifdef DEBUG
auto twoDigit = [&](size_t offset) {
auto c1 = timeZone->latin1OrTwoByteChar(offset);
auto c2 = timeZone->latin1OrTwoByteChar(offset + 1);
return mozilla::AsciiAlphanumericToNumber(c1) * 10 +
mozilla::AsciiAlphanumericToNumber(c2);
};
int32_t hours = twoDigit(1);
MOZ_ASSERT(0 <= hours && hours <= 23);
int32_t minutes = twoDigit(4);
MOZ_ASSERT(0 <= minutes && minutes <= 59);
#endif
TimeZoneOffsetString result{};
// Copy the string "GMT" followed by the offset string.
size_t copied = GMT.copy(result.timeZone_, GMT.size());
CopyChars(result.timeZone_ + copied, *timeZone);
return mozilla::Some(result);
}
};
class TimeZoneChars final {
JS::AutoStableStringChars timeZone_;
mozilla::Maybe<TimeZoneOffsetString> timeZoneOffset_;
mozilla::Maybe<mozilla::Span<const char16_t>> maybeSpan() const {
if (timeZone_.isTwoByte()) {
return mozilla::Some(timeZone_.twoByteRange());
}
if (timeZoneOffset_) {
return timeZoneOffset_;
}
return mozilla::Nothing();
}
public:
explicit TimeZoneChars(JSContext* cx) : timeZone_(cx) {}
operator mozilla::Span<const char16_t>() const {
return maybeSpan().valueOr(mozilla::Span<const char16_t>{});
}
operator mozilla::Maybe<mozilla::Span<const char16_t>>() const {
return maybeSpan();
}
bool init(JSContext* cx, JSLinearString* timeZone) {
auto timeZoneOffset = TimeZoneOffsetString::from(timeZone);
if (timeZoneOffset) {
timeZoneOffset_ = timeZoneOffset;
return true;
}
return timeZone_.initTwoByte(cx, timeZone);
}
void setToOffset(const TimeZoneOffsetString& offset) {
timeZoneOffset_ = mozilla::Some(offset);
}
};
static bool IsPlainDateTimeValue(DateTimeValueKind kind) {
switch (kind) {
case DateTimeValueKind::Number:
case DateTimeValueKind::TemporalZonedDateTime:
case DateTimeValueKind::TemporalInstant:
return false;
case DateTimeValueKind::TemporalDate:
case DateTimeValueKind::TemporalTime:
case DateTimeValueKind::TemporalDateTime:
case DateTimeValueKind::TemporalYearMonth:
case DateTimeValueKind::TemporalMonthDay:
return true;
}
MOZ_CRASH("invalid date time value kind");
}
/**
* 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<DateTimeFormatObject*> dateTimeFormat,
DateTimeValueKind kind) {
if (!ResolveLocale(cx, dateTimeFormat)) {
return nullptr;
}
auto dtfOptions = *dateTimeFormat->getOptions();
auto locale = DateTimeFormatLocale(cx, dateTimeFormat);
if (!locale) {
return nullptr;
}
TimeZoneChars timeZone(cx);
if (IsPlainDateTimeValue(kind)) {
timeZone.setToOffset(TimeZoneOffsetString::GMTZero());
} else {
if (!timeZone.init(cx, dateTimeFormat->getTimeZone())) {
return nullptr;
}
}
// This is a DateTimeFormat defined by a pattern option. This is internal
// to Mozilla, and not part of the ECMA-402 API.
if (auto* patternString = dateTimeFormat->getPattern()) {
JS::AutoStableStringChars pattern(cx);
if (!pattern.initTwoByte(cx, patternString)) {
return nullptr;
}
auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromPattern(
mozilla::MakeStringSpan(locale.get()), pattern.twoByteRange(),
timeZone);
if (dfResult.isErr()) {
ReportInternalError(cx, dfResult.unwrapErr());
return nullptr;
}
return dfResult.unwrap().release();
}
// This is a DateTimeFormat defined by a time style or date style.
if (dtfOptions.dateStyle.isSome() || dtfOptions.timeStyle.isSome()) {
switch (kind) {
case DateTimeValueKind::TemporalDate:
case DateTimeValueKind::TemporalYearMonth:
case DateTimeValueKind::TemporalMonthDay: {
if (dtfOptions.dateStyle.isNothing()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_INVALID_FORMAT_OPTIONS,
DateTimeValueKindToString(kind));
return nullptr;
}
break;
}
case DateTimeValueKind::TemporalTime: {
if (dtfOptions.timeStyle.isNothing()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_INVALID_FORMAT_OPTIONS,
DateTimeValueKindToString(kind));
return nullptr;
}
break;
}
case DateTimeValueKind::Number:
case DateTimeValueKind::TemporalDateTime:
case DateTimeValueKind::TemporalZonedDateTime:
case DateTimeValueKind::TemporalInstant:
break;
}
static auto ToDateStyle = ToDateTimeStyle<DateTimeFormatOptions::DateStyle>;
static auto ToTimeStyle = ToDateTimeStyle<DateTimeFormatOptions::TimeStyle>;
mozilla::intl::DateTimeFormat::StyleBag style = {
.date = dtfOptions.dateStyle.map(ToDateStyle),
.time = dtfOptions.timeStyle.map(ToTimeStyle),
.hourCycle = dtfOptions.hourCycle.map(ToHourCycleComponent),
.hour12 = dtfOptions.hour12,
};
auto& sharedIntlData = cx->runtime()->sharedIntlData.ref();
auto* dtpg = sharedIntlData.getDateTimePatternGenerator(cx, locale.get());
if (!dtpg) {
return nullptr;
}
auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromStyle(
mozilla::MakeStringSpan(locale.get()), style, dtpg, timeZone);
if (dfResult.isErr()) {
ReportInternalError(cx, dfResult.unwrapErr());
return nullptr;
}
auto df = dfResult.unwrap();
mozilla::EnumSet<DateTimeField> allowedOptions;
switch (kind) {
case DateTimeValueKind::TemporalDate:
allowedOptions = {
DateTimeField::Weekday, DateTimeField::Era, DateTimeField::Year,
DateTimeField::Month, DateTimeField::Day,
};
break;
case DateTimeValueKind::TemporalTime:
allowedOptions = {
DateTimeField::DayPeriod,
DateTimeField::Hour,
DateTimeField::Minute,
DateTimeField::Second,
DateTimeField::FractionalSecondDigits,
};
break;
case DateTimeValueKind::TemporalDateTime:
allowedOptions = {
DateTimeField::Weekday, DateTimeField::Era,
DateTimeField::Year, DateTimeField::Month,
DateTimeField::Day, DateTimeField::DayPeriod,
DateTimeField::Hour, DateTimeField::Minute,
DateTimeField::Second, DateTimeField::FractionalSecondDigits,
};
break;
case DateTimeValueKind::TemporalYearMonth:
allowedOptions = {
DateTimeField::Era,
DateTimeField::Year,
DateTimeField::Month,
};
break;
case DateTimeValueKind::TemporalMonthDay:
allowedOptions = {
DateTimeField::Month,
DateTimeField::Day,
};
break;
case DateTimeValueKind::Number:
case DateTimeValueKind::TemporalZonedDateTime:
case DateTimeValueKind::TemporalInstant:
break;
}
if (allowedOptions.isEmpty()) {
return df.release();
}
auto adjusted = AdjustDateTimeStyleFormat(df.get(), allowedOptions);
if (adjusted.isErr()) {
ReportInternalError(cx, adjusted.unwrapErr());
return nullptr;
}
auto bag = adjusted.unwrap();
auto dfAdjustedResult =
mozilla::intl::DateTimeFormat::TryCreateFromComponents(
mozilla::MakeStringSpan(locale.get()), bag, dtpg, timeZone);
if (dfAdjustedResult.isErr()) {
ReportInternalError(cx, dfAdjustedResult.unwrapErr());
return nullptr;
}
return dfAdjustedResult.unwrap().release();
}
static auto ToEra = ToTextComponent<DateTimeFormatOptions::Era>;
static auto ToYear = ToNumericComponent<DateTimeFormatOptions::Year>;
static auto ToDay = ToNumericComponent<DateTimeFormatOptions::Day>;
static auto ToWeekday = ToTextComponent<DateTimeFormatOptions::Weekday>;
static auto ToHour = ToNumericComponent<DateTimeFormatOptions::Hour>;
static auto ToMinute = ToNumericComponent<DateTimeFormatOptions::Minute>;
static auto ToSecond = ToNumericComponent<DateTimeFormatOptions::Second>;
static auto ToDayPeriod = ToTextComponent<DateTimeFormatOptions::DayPeriod>;
// This is a DateTimeFormat defined by a components bag.
mozilla::intl::DateTimeFormat::ComponentsBag bag = {
.era = dtfOptions.era.map(ToEra),
.year = dtfOptions.year.map(ToYear),
.month = dtfOptions.month.map(ToMonthComponent),
.day = dtfOptions.day.map(ToDay),
.weekday = dtfOptions.weekday.map(ToWeekday),
.hour = dtfOptions.hour.map(ToHour),
.minute = dtfOptions.minute.map(ToMinute),
.second = dtfOptions.second.map(ToSecond),
.timeZoneName = dtfOptions.timeZoneName.map(ToTimeZoneNameComponent),
.hour12 = dtfOptions.hour12,
.hourCycle = dtfOptions.hourCycle.map(ToHourCycleComponent),
.dayPeriod = dtfOptions.dayPeriod.map(ToDayPeriod),
.fractionalSecondDigits = dtfOptions.fractionalSecondDigits,
};
auto [required, defaults, inherit] = GetDateTimeFormatArgs(dtfOptions, kind);
auto resolvedBag = GetDateTimeFormat(bag, required, defaults, inherit);
if (!resolvedBag) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_INVALID_FORMAT_OPTIONS,
DateTimeValueKindToString(kind));
return nullptr;
}
bag = *resolvedBag;
auto& 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, timeZone);
if (dfResult.isErr()) {
ReportInternalError(cx, dfResult.unwrapErr());
return nullptr;
}
return dfResult.unwrap().release();
}
void js::intl::DateTimeFormatObject::maybeClearCache(DateTimeValueKind kind) {
if (getDateTimeValueKind() == kind) {
return;
}
setDateTimeValueKind(kind);
if (auto* df = getDateFormat()) {
RemoveICUCellMemory(this,
DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
delete df;
setDateFormat(nullptr);
}
if (auto* dif = getDateIntervalFormat()) {
RemoveICUCellMemory(
this, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse);
delete dif;
setDateIntervalFormat(nullptr);
}
}
static mozilla::intl::DateTimeFormat* GetOrCreateDateTimeFormat(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
DateTimeValueKind kind) {
// Clear previously created formatters if their type doesn't match.
dateTimeFormat->maybeClearCache(kind);
// Obtain a cached mozilla::intl::DateTimeFormat object.
if (auto* df = dateTimeFormat->getDateFormat()) {
return df;
}
auto* df = NewDateTimeFormat(cx, dateTimeFormat, kind);
if (!df) {
return nullptr;
}
dateTimeFormat->setDateFormat(df);
AddICUCellMemory(dateTimeFormat,
DateTimeFormatObject::UDateFormatEstimatedMemoryUse);
return df;
}
template <typename T>
static bool SetOptionsProperty(JSContext* cx,
MutableHandle<IdValueVector> options,
Handle<PropertyName*> name,
mozilla::Maybe<T> intlProp) {
if (!intlProp) {
return true;
}
auto* str = NewStringCopyZ<CanGC>(
cx, mozilla::intl::DateTimeFormat::ToString(*intlProp));
if (!str) {
return false;
}
return options.emplaceBack(NameToId(name), StringValue(str));
}
enum class IncludeDateTimeFields : bool { No, Yes };
/**
* Extracts the resolved components from a DateTimeFormat and applies them to
* the object for resolved components.
*/
static bool ResolveDateTimeFormatComponents(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
MutableHandle<IdValueVector> options,
IncludeDateTimeFields includeDateTimeFields) {
auto* df =
GetOrCreateDateTimeFormat(cx, dateTimeFormat, DateTimeValueKind::Number);
if (!df) {
return false;
}
auto result = df->ResolveComponents();
if (result.isErr()) {
ReportInternalError(cx, result.unwrapErr());
return false;
}
auto 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:
if (!SetOptionsProperty(cx, options, cx->names().hourCycle,
components.hourCycle)) {
return false;
}
if (components.hour12) {
if (!options.emplaceBack(NameToId(cx->names().hour12),
BooleanValue(*components.hour12))) {
return false;
}
}
if (includeDateTimeFields == IncludeDateTimeFields::No) {
// Do not include date time fields.
return true;
}
if (!SetOptionsProperty(cx, options, cx->names().weekday,
components.weekday)) {
return false;
}
if (!SetOptionsProperty(cx, options, cx->names().era, components.era)) {
return false;
}
if (!SetOptionsProperty(cx, options, cx->names().year, components.year)) {
return false;
}
if (!SetOptionsProperty(cx, options, cx->names().month, components.month)) {
return false;
}
if (!SetOptionsProperty(cx, options, cx->names().day, components.day)) {
return false;
}
if (!SetOptionsProperty(cx, options, cx->names().dayPeriod,
components.dayPeriod)) {
return false;
}
if (!SetOptionsProperty(cx, options, cx->names().hour, components.hour)) {
return false;
}
if (!SetOptionsProperty(cx, options, cx->names().minute, components.minute)) {
return false;
}
if (!SetOptionsProperty(cx, options, cx->names().second, components.second)) {
return false;
}
if (!SetOptionsProperty(cx, options, cx->names().timeZoneName,
components.timeZoneName)) {
return false;
}
if (components.fractionalSecondDigits) {
if (!options.emplaceBack(NameToId(cx->names().fractionalSecondDigits),
Int32Value(*components.fractionalSecondDigits))) {
return false;
}
}
return true;
}
/**
* ToDateTimeFormattable ( value )
*
*/
static auto ToDateTimeFormattable(const Value& value) {
MOZ_ASSERT(!value.isUndefined());
// Step 1. (Inlined IsTemporalObject)
if (value.isObject()) {
auto* obj = CheckedUnwrapStatic(&value.toObject());
if (obj) {
if (obj->is<PlainDateObject>()) {
return DateTimeValueKind::TemporalDate;
}
if (obj->is<PlainDateTimeObject>()) {
return DateTimeValueKind::TemporalDateTime;
}
if (obj->is<PlainTimeObject>()) {
return DateTimeValueKind::TemporalTime;
}
if (obj->is<PlainYearMonthObject>()) {
return DateTimeValueKind::TemporalYearMonth;
}
if (obj->is<PlainMonthDayObject>()) {
return DateTimeValueKind::TemporalMonthDay;
}
if (obj->is<ZonedDateTimeObject>()) {
return DateTimeValueKind::TemporalZonedDateTime;
}
if (obj->is<InstantObject>()) {
return DateTimeValueKind::TemporalInstant;
}
return DateTimeValueKind::Number;
}
}
// Step 2. (ToNumber performed in caller)
return DateTimeValueKind::Number;
}
/**
* Ensure the calendar value is resolved.
*/
static bool ResolveCalendarValue(JSContext* cx,
Handle<DateTimeFormatObject*> dateTimeFormat) {
if (dateTimeFormat->getCalendarValue()) {
return true;
}
// Ensure the calendar option is resolved.
if (!ResolveLocale(cx, dateTimeFormat)) {
return false;
}
Rooted<JSString*> calendarString(cx, dateTimeFormat->getCalendar());
Rooted<CalendarValue> calendar(cx);
if (!CanonicalizeCalendar(cx, calendarString, &calendar)) {
return false;
}
dateTimeFormat->setCalendarValue(calendar);
return true;
}
/**
* HandleDateTimeTemporalDate ( dateTimeFormat, temporalDate )
*
*/
static bool HandleDateTimeTemporalDate(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
Handle<PlainDateObject*> unwrappedTemporalDate, ClippedTime* result) {
auto isoDate = unwrappedTemporalDate->date();
auto calendarId = unwrappedTemporalDate->calendar().identifier();
if (!ResolveCalendarValue(cx, dateTimeFormat)) {
return false;
}
Rooted<CalendarValue> calendar(cx, dateTimeFormat->getCalendarValue());
// Step 1.
if (calendarId != CalendarId::ISO8601 &&
calendarId != calendar.identifier()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE,
CalendarIdentifier(calendarId).data(),
CalendarIdentifier(calendar).data());
return false;
}
// Step 2.
auto isoDateTime = ISODateTime{isoDate, {12, 0, 0}};
// Step 3.
auto epochNs = GetUTCEpochNanoseconds(isoDateTime);
// Steps 4-5. (Performed in NewDateTimeFormat)
// Step 6.
int64_t milliseconds = epochNs.floorToMilliseconds();
*result = JS::TimeClip(double(milliseconds));
return true;
}
/**
* HandleDateTimeTemporalYearMonth ( dateTimeFormat, temporalYearMonth )
*
*/
static bool HandleDateTimeTemporalYearMonth(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
Handle<PlainYearMonthObject*> unwrappedTemporalYearMonth,
ClippedTime* result) {
auto isoDate = unwrappedTemporalYearMonth->date();
auto calendarId = unwrappedTemporalYearMonth->calendar().identifier();
if (!ResolveCalendarValue(cx, dateTimeFormat)) {
return false;
}
Rooted<CalendarValue> calendar(cx, dateTimeFormat->getCalendarValue());
// Step 1.
if (calendarId != calendar.identifier()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE,
CalendarIdentifier(calendarId).data(),
CalendarIdentifier(calendar).data());
return false;
}
// Step 2.
auto isoDateTime = ISODateTime{isoDate, {12, 0, 0}};
// Step 3.
auto epochNs = GetUTCEpochNanoseconds(isoDateTime);
// Steps 4-5. (Performed in NewDateTimeFormat)
// Step 6.
int64_t milliseconds = epochNs.floorToMilliseconds();
*result = JS::TimeClip(double(milliseconds));
return true;
}
/**
* HandleDateTimeTemporalMonthDay ( dateTimeFormat, temporalMonthDay )
*
*/
static bool HandleDateTimeTemporalMonthDay(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
Handle<PlainMonthDayObject*> unwrappedTemporalMonthDay,
ClippedTime* result) {
auto isoDate = unwrappedTemporalMonthDay->date();
auto calendarId = unwrappedTemporalMonthDay->calendar().identifier();
if (!ResolveCalendarValue(cx, dateTimeFormat)) {
return false;
}
Rooted<CalendarValue> calendar(cx, dateTimeFormat->getCalendarValue());
// Step 1.
if (calendarId != calendar.identifier()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE,
CalendarIdentifier(calendarId).data(),
CalendarIdentifier(calendar).data());
return false;
}
// Step 2.
auto isoDateTime = ISODateTime{isoDate, {12, 0, 0}};
// Step 3.
auto epochNs = GetUTCEpochNanoseconds(isoDateTime);
// Steps 4-5. (Performed in NewDateTimeFormat)
// Step 6.
int64_t milliseconds = epochNs.floorToMilliseconds();
*result = JS::TimeClip(double(milliseconds));
return true;
}
/**
* HandleDateTimeTemporalTime ( dateTimeFormat, temporalTime )
*
*/
static bool HandleDateTimeTemporalTime(PlainTimeObject* unwrappedTemporalTime,
ClippedTime* result) {
auto time = unwrappedTemporalTime->time();
// Steps 1-2.
auto isoDateTime = ISODateTime{{1970, 1, 1}, time};
// Step 3.
auto epochNs = GetUTCEpochNanoseconds(isoDateTime);
// Steps 4-5. (Performed in NewDateTimeFormat)
// Step 6.
int64_t milliseconds = epochNs.floorToMilliseconds();
*result = JS::TimeClip(double(milliseconds));
return true;
}
/**
* HandleDateTimeTemporalDateTime ( dateTimeFormat, dateTime )
*
*/
static bool HandleDateTimeTemporalDateTime(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
Handle<PlainDateTimeObject*> unwrappedDateTime, ClippedTime* result) {
auto isoDateTime = unwrappedDateTime->dateTime();
auto calendarId = unwrappedDateTime->calendar().identifier();
if (!ResolveCalendarValue(cx, dateTimeFormat)) {
return false;
}
Rooted<CalendarValue> calendar(cx, dateTimeFormat->getCalendarValue());
// Step 1.
if (calendarId != CalendarId::ISO8601 &&
calendarId != calendar.identifier()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE,
CalendarIdentifier(calendarId).data(),
CalendarIdentifier(calendar).data());
return false;
}
// Step 2.
auto epochNs = GetUTCEpochNanoseconds(isoDateTime);
// Step 3. (Performed in NewDateTimeFormat)
// Step 4.
int64_t milliseconds = epochNs.floorToMilliseconds();
*result = JS::TimeClip(double(milliseconds));
return true;
}
/**
* HandleDateTimeTemporalInstant ( dateTimeFormat, instant )
*
*/
static bool HandleDateTimeTemporalInstant(InstantObject* unwrappedInstant,
ClippedTime* result) {
// Step 1. (Performed in NewDateTimeFormat)
// Step 2.
auto epochNs = unwrappedInstant->epochNanoseconds();
int64_t milliseconds = epochNs.floorToMilliseconds();
*result = JS::TimeClip(double(milliseconds));
return true;
}
/**
* Temporal.ZonedDateTime.prototype.toLocaleString ( [ locales [ , options ] ] )
*/
static bool HandleDateTimeTemporalZonedDateTime(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
Handle<ZonedDateTimeObject*> unwrappedZonedDateTime, ClippedTime* result) {
auto epochNs = unwrappedZonedDateTime->epochNanoseconds();
auto calendarId = unwrappedZonedDateTime->calendar().identifier();
if (!ResolveCalendarValue(cx, dateTimeFormat)) {
return false;
}
Rooted<CalendarValue> calendar(cx, dateTimeFormat->getCalendarValue());
// Step 4.
if (calendarId != CalendarId::ISO8601 &&
calendarId != calendar.identifier()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE,
CalendarIdentifier(calendarId).data(),
CalendarIdentifier(calendar).data());
return false;
}
// Step 5.
int64_t milliseconds = epochNs.floorToMilliseconds();
*result = JS::TimeClip(double(milliseconds));
return true;
}
/**
* HandleDateTimeOthers ( dateTimeFormat, x )
*
*/
static bool HandleDateTimeOthers(JSContext* cx, const char* method, double x,
ClippedTime* result) {
// Step 1.
auto clipped = JS::TimeClip(x);
// Step 2.
if (!clipped.isValid()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DATE_NOT_FINITE, "DateTimeFormat", method);
return false;
}
// Step 4. (Performed in NewDateTimeFormat)
// Steps 3 and 5.
*result = clipped;
return true;
}
/**
* HandleDateTimeValue ( dateTimeFormat, x )
*
*/
static bool HandleDateTimeValue(JSContext* cx, const char* method,
Handle<DateTimeFormatObject*> dateTimeFormat,
JSObject* x, ClippedTime* result) {
// Step 1.
Rooted<JSObject*> unwrapped(cx, CheckedUnwrapStatic(x));
if (!unwrapped) {
ReportAccessDenied(cx);
return false;
}
// Step 1.a.
if (unwrapped->is<PlainDateObject>()) {
return HandleDateTimeTemporalDate(cx, dateTimeFormat,
unwrapped.as<PlainDateObject>(), result);
}
// Step 1.b.
if (unwrapped->is<PlainYearMonthObject>()) {
return HandleDateTimeTemporalYearMonth(
cx, dateTimeFormat, unwrapped.as<PlainYearMonthObject>(), result);
}
// Step 1.c.
if (unwrapped->is<PlainMonthDayObject>()) {
return HandleDateTimeTemporalMonthDay(
cx, dateTimeFormat, unwrapped.as<PlainMonthDayObject>(), result);
}
// Step 1.d.
if (unwrapped->is<PlainTimeObject>()) {
return HandleDateTimeTemporalTime(&unwrapped->as<PlainTimeObject>(),
result);
}
// Step 1.e.
if (unwrapped->is<PlainDateTimeObject>()) {
return HandleDateTimeTemporalDateTime(
cx, dateTimeFormat, unwrapped.as<PlainDateTimeObject>(), result);
}
// Step 1.f.
if (unwrapped->is<InstantObject>()) {
return HandleDateTimeTemporalInstant(&unwrapped->as<InstantObject>(),
result);
}
// Step 1.g.
MOZ_ASSERT(unwrapped->is<ZonedDateTimeObject>());
// Step 1.h.
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
"object", unwrapped->getClass()->name);
return false;
}
struct DateTimeValue {
ClippedTime time;
DateTimeValueKind kind{};
};
/**
* DateTime Format Functions, steps 3-4 and 5 (partial).
* Intl.DateTimeFormat.prototype.formatToParts, steps 3-4 and 5 (partial).
*/
static bool ToDateTimeValue(JSContext* cx, const char* method,
Handle<DateTimeFormatObject*> dateTimeFormat,
Handle<JS::Value> date, DateTimeValue* result) {
// DateTime Format Functions, step 3.
// Intl.DateTimeFormat.prototype.formatToParts, step 3.
if (date.isUndefined()) {
result->time = DateNow(cx);
result->kind = DateTimeValueKind::Number;
return true;
}
// DateTime Format Functions, step 4.
// Intl.DTF.prototype.formatToParts, step 4.
auto kind = ToDateTimeFormattable(date);
result->kind = kind;
// DateTime Format Functions, step 5. (Call to FormatDateTime)
// - FormatDateTime, step 1. (Call to PartitionDateTimePattern)
// -- PartitionDateTimePattern, step 1. (Call to HandleDateTimeValue)
// --- HandleDateTimeValue, step 1.
//
// Intl.DTF.prototype.formatToParts, step 5. (Call to FormatDateTimeToParts)
// - FormatDateTimeToParts, step 1. (Call to PartitionDateTimePattern)
// -- PartitionDateTimePattern, step 1. (Call to HandleDateTimeValue)
// --- HandleDateTimeValue, step 1.
if (kind != DateTimeValueKind::Number) {
MOZ_ASSERT(date.isObject());
return HandleDateTimeValue(cx, method, dateTimeFormat, &date.toObject(),
&result->time);
}
// ToDateTimeFormattable, step 2. (Call to ToNumber)
double num;
if (!JS::ToNumber(cx, date, &num)) {
return false;
}
// DateTime Format Functions, step 5. (Call to FormatDateTime)
// - FormatDateTime, step 1. (Call to PartitionDateTimePattern)
// -- PartitionDateTimePattern, step 1. (Call to HandleDateTimeValue)
// --- HandleDateTimeValue, step 2. (Call to HandleDateTimeOthers)
//
// Intl.DTF.prototype.formatToParts, step 5. (Call to FormatDateTimeToParts)
// - FormatDateTimeToParts, step 1. (Call to PartitionDateTimePattern)
// -- PartitionDateTimePattern, step 1. (Call to HandleDateTimeValue)
// --- HandleDateTimeValue, step 2. (Call to HandleDateTimeOthers)
return HandleDateTimeOthers(cx, method, num, &result->time);
}
/**
* FormatDateTime ( dateTimeFormat, x )
* PartitionDateTimePattern ( dateTimeFormat, x )
*
* Returns a String value representing x according to the effective locale and
* the formatting options of the given DateTimeFormat.
*/
static bool FormatDateTime(JSContext* cx,
const mozilla::intl::DateTimeFormat* df,
ClippedTime x, MutableHandle<JS::Value> result) {
// FormatDateTime, step 1. (Inlined call to PartitionDateTimePattern)
// PartitionDateTimePattern, steps 1-2.
MOZ_ASSERT(x.isValid());
// PartitionDateTimePattern, steps 3-8.
FormatBuffer<char16_t, INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
auto dfResult = df->TryFormat(x.toDouble(), buffer);
if (dfResult.isErr()) {
ReportInternalError(cx, dfResult.unwrapErr());
return false;
}
// FormatDateTime, steps 2-4.
auto* str = buffer.toString(cx);
if (!str) {
return false;
}
result.setString(str);
return true;
}
static bool FormatDateTime(JSContext* cx,
Handle<DateTimeFormatObject*> dateTimeFormat,
const DateTimeValue& date,
MutableHandle<JS::Value> result) {
auto* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat, date.kind);
if (!df) {
return false;
}
return FormatDateTime(cx, df, date.time, result);
}
using FieldType = js::ImmutableTenuredPtr<PropertyName*> 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");
}
enum class DateTimeSource : bool { No, Yes };
/**
* FormatDateTimeToParts ( dateTimeFormat, x )
* FormatDateTimeRangeToParts ( dateTimeFormat, x, y )
*
* Create the part object for FormatDateTime{Range}ToParts.
*/
static PlainObject* CreateDateTimePart(JSContext* cx,
const mozilla::intl::DateTimePart& part,
Handle<JSString*> value,
DateTimeSource dateTimeSource) {
Rooted<IdValueVector> properties(cx, cx);
FieldType typeName = GetFieldTypeForPartType(part.mType);
if (!properties.emplaceBack(NameToId(cx->names().type),
StringValue(cx->names().*typeName))) {
return nullptr;
}
if (!properties.emplaceBack(NameToId(cx->names().value),
StringValue(value))) {
return nullptr;
}
if (dateTimeSource == DateTimeSource::Yes) {
FieldType sourceName = GetFieldTypeForPartSource(part.mSource);
if (!properties.emplaceBack(NameToId(cx->names().source),
StringValue(cx->names().*sourceName))) {
return nullptr;
}
}
return NewPlainObjectWithUniqueNames(cx, properties);
}
/**
* FormatDateTimeToParts ( dateTimeFormat, x )
* FormatDateTimeRangeToParts ( dateTimeFormat, x, y )
*
* Create the parts array for FormatDateTime{Range}ToParts.
*/
static bool CreateDateTimePartArray(
JSContext* cx, mozilla::Span<const char16_t> formattedSpan,
DateTimeSource dateTimeSource,
const mozilla::intl::DateTimePartVector& parts,
MutableHandle<JS::Value> result) {
Rooted<JSString*> overallResult(cx, NewStringCopy<CanGC>(cx, formattedSpan));
if (!overallResult) {
return false;
}
Rooted<ArrayObject*> partsArray(
cx, NewDenseFullyAllocatedArray(cx, parts.length()));
if (!partsArray) {
return false;
}
partsArray->ensureDenseInitializedLength(0, parts.length());
if (overallResult->empty()) {
// An empty string contains no parts, so avoid extra work below.
result.setObject(*partsArray);
return true;
}
Rooted<JSString*> value(cx);
size_t index = 0;
size_t beginIndex = 0;
for (const auto& part : parts) {
MOZ_ASSERT(part.mEndIndex > beginIndex);
value = NewDependentString(cx, overallResult, beginIndex,
part.mEndIndex - beginIndex);
if (!value) {
return false;
}
beginIndex = part.mEndIndex;
auto* obj = CreateDateTimePart(cx, part, value, dateTimeSource);
if (!obj) {
return false;
}
partsArray->initDenseElement(index++, ObjectValue(*obj));
}
MOZ_ASSERT(index == parts.length());
MOZ_ASSERT(beginIndex == formattedSpan.size());
result.setObject(*partsArray);
return true;
}
static bool FormatToPartsDateTime(JSContext* cx,
const mozilla::intl::DateTimeFormat* df,
ClippedTime x, DateTimeSource dateTimeSource,
MutableHandle<JS::Value> result) {
MOZ_ASSERT(x.isValid());
FormatBuffer<char16_t, INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
mozilla::intl::DateTimePartVector parts;
auto r = df->TryFormatToParts(x.toDouble(), buffer, parts);
if (r.isErr()) {
ReportInternalError(cx, r.unwrapErr());
return false;
}
return CreateDateTimePartArray(cx, buffer, dateTimeSource, parts, result);
}
static bool FormatToPartsDateTime(JSContext* cx,
Handle<DateTimeFormatObject*> dateTimeFormat,
const DateTimeValue& date,
MutableHandle<JS::Value> result) {
auto* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat, date.kind);
if (!df) {
return false;
}
return FormatToPartsDateTime(cx, df, date.time, DateTimeSource::No, result);
}
struct DateTimeRangeValue {
ClippedTime start;
ClippedTime end;
DateTimeValueKind kind{};
};
/**
* Intl.DateTimeFormat.prototype.formatRange, steps 3-5 and 6 (partial).
* Intl.DateTimeFormat.prototype.formatRangeToParts, steps 3-5 and 6 (partial).
*/
static bool ToDateTimeRangeValue(JSContext* cx, const char* method,
Handle<DateTimeFormatObject*> dateTimeFormat,
Handle<JS::Value> start, Handle<JS::Value> end,
DateTimeRangeValue* result) {
// Intl.DateTimeFormat.prototype.formatRange, step 3.
// Intl.DateTimeFormat.prototype.formatRangeToParts, step 3.
if (start.isUndefined() || end.isUndefined()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_UNDEFINED_DATE,
start.isUndefined() ? "start" : "end", method);
return false;
}
// Intl.DateTimeFormat.prototype.formatRange, step 4.
// Intl.DateTimeFormat.prototype.formatRangeToParts, step 4.
auto startKind = ToDateTimeFormattable(start);
// Intl.DateTimeFormat.prototype.formatRange, step 5.
// Intl.DateTimeFormat.prototype.formatRangeToParts, step 5.
auto endKind = ToDateTimeFormattable(end);
// ToDateTimeFormattable, step 2. (Call to ToNumber)
double startNum;
if (startKind == DateTimeValueKind::Number) {
if (!JS::ToNumber(cx, start, &startNum)) {
return false;
}
}
// ToDateTimeFormattable, step 2. (Call to ToNumber)
double endNum;
if (endKind == DateTimeValueKind::Number) {
if (!JS::ToNumber(cx, end, &endNum)) {
return false;
}
}
// clang-format off
//
// Intl.DTF.prototype.formatRange, step 6. (Call to FormatDateTimeRange)
// - FormatDateTimeRange, step 1. (Call to PartitionDateTimeRangePattern)
//
// Intl.DTF.prototype.formatRangeToParts, step 6. (Call to FormatDateTimeRangeToParts)
// - FormatDateTimeRangeToParts, step 1. (Call to PartitionDateTimeRangePattern)
//
// clang-format on
// PartitionDateTimeRangePattern, step 1.
if (startKind != endKind) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_NOT_EXPECTED_TYPE, method,
DateTimeValueKindToString(startKind),
DateTimeValueKindToString(endKind));
return false;
}
result->kind = startKind;
// PartitionDateTimeRangePattern, steps 2-3. (Call to HandleDateTimeValue)
// HandleDateTimeValue, step 1.
if (startKind != DateTimeValueKind::Number) {
MOZ_ASSERT(start.isObject());
MOZ_ASSERT(end.isObject());
return HandleDateTimeValue(cx, method, dateTimeFormat, &start.toObject(),
&result->start) &&
HandleDateTimeValue(cx, method, dateTimeFormat, &end.toObject(),
&result->end);
}
// HandleDateTimeValue, step 2.
return HandleDateTimeOthers(cx, method, startNum, &result->start) &&
HandleDateTimeOthers(cx, method, endNum, &result->end);
}
bool js::intl::FormatDateTime(JSContext* cx,
Handle<DateTimeFormatObject*> dateTimeFormat,
double millis, MutableHandle<Value> result) {
auto x = JS::TimeClip(millis);
MOZ_ASSERT(x.isValid());
return FormatDateTime(cx, dateTimeFormat, {x, DateTimeValueKind::Number},
result);
}
/**
* Returns a new DateIntervalFormat with the locale and date-time formatting
* options of the given DateTimeFormat.
*/
static mozilla::intl::DateIntervalFormat* NewDateIntervalFormat(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
mozilla::intl::DateTimeFormat& mozDtf, DateTimeValueKind kind) {
if (!ResolveLocale(cx, dateTimeFormat)) {
return nullptr;
}
FormatBuffer<char16_t, INITIAL_CHAR_BUFFER_SIZE> pattern(cx);
auto result = mozDtf.GetPattern(pattern);
if (result.isErr()) {
ReportInternalError(cx, result.unwrapErr());
return nullptr;
}
// Determine the hour cycle used in the resolved pattern.
auto hcPattern = mozilla::intl::DateTimeFormat::HourCycleFromPattern(pattern);
auto locale = DateTimeFormatLocale(cx, dateTimeFormat, hcPattern);
if (!locale) {
return nullptr;
}
TimeZoneChars timeZone(cx);
if (IsPlainDateTimeValue(kind)) {
timeZone.setToOffset(TimeZoneOffsetString::GMTZero());
} else {
if (!timeZone.init(cx, dateTimeFormat->getTimeZone())) {
return nullptr;
}
}
FormatBuffer<char16_t, INITIAL_CHAR_BUFFER_SIZE> skeleton(cx);
auto skelResult = mozDtf.GetOriginalSkeleton(skeleton);
if (skelResult.isErr()) {
ReportInternalError(cx, skelResult.unwrapErr());
return nullptr;
}
auto dif = mozilla::intl::DateIntervalFormat::TryCreate(
mozilla::MakeStringSpan(locale.get()), skeleton, timeZone);
if (dif.isErr()) {
ReportInternalError(cx, dif.unwrapErr());
return nullptr;
}
return dif.unwrap().release();
}
static mozilla::intl::DateIntervalFormat* GetOrCreateDateIntervalFormat(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
mozilla::intl::DateTimeFormat& mozDtf, DateTimeValueKind kind) {
dateTimeFormat->maybeClearCache(kind);
// Obtain a cached DateIntervalFormat object.
if (auto* dif = dateTimeFormat->getDateIntervalFormat()) {
return dif;
}
auto* dif = NewDateIntervalFormat(cx, dateTimeFormat, mozDtf, kind);
if (!dif) {
return nullptr;
}
dateTimeFormat->setDateIntervalFormat(dif);
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());
auto result =
dif->TryFormatDateTime(x.toDouble(), y.toDouble(), df, formatted, equal);
if (result.isErr()) {
ReportInternalError(cx, result.unwrapErr());
return false;
}
return true;
}
/**
* FormatDateTimeRange( dateTimeFormat, x, y )
*
* Returns a String value representing the range between x and y according to
* the effective locale and the formatting options of the given DateTimeFormat.
*/
static bool FormatDateTimeRange(JSContext* cx,
Handle<DateTimeFormatObject*> dateTimeFormat,
const DateTimeRangeValue& values,
MutableHandle<JS::Value> result) {
auto* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat, values.kind);
if (!df) {
return false;
}
auto* dif =
GetOrCreateDateIntervalFormat(cx, dateTimeFormat, *df, values.kind);
if (!dif) {
return false;
}
mozilla::intl::AutoFormattedDateInterval formatted;
if (!formatted.IsValid()) {
ReportInternalError(cx, formatted.GetError());
return false;
}
bool equal;
if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, values.start,
values.end, &equal)) {
return false;
}
// PartitionDateTimeRangePattern, step 12.
if (equal) {
return FormatDateTime(cx, df, values.start, result);
}
auto spanResult = formatted.ToSpan();
if (spanResult.isErr()) {
ReportInternalError(cx, spanResult.unwrapErr());
return false;
}
auto* resultStr = NewStringCopy<CanGC>(cx, spanResult.unwrap());
if (!resultStr) {
return false;
}
result.setString(resultStr);
return true;
}
/**
* FormatDateTimeRangeToParts ( dateTimeFormat, x, y )
*/
static bool FormatDateTimeRangeToParts(
JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat,
const DateTimeRangeValue& values, MutableHandle<JS::Value> result) {
auto* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat, values.kind);
if (!df) {
return false;
}
auto* dif =
GetOrCreateDateIntervalFormat(cx, dateTimeFormat, *df, values.kind);
if (!dif) {
return false;
}
mozilla::intl::AutoFormattedDateInterval formatted;
if (!formatted.IsValid()) {
ReportInternalError(cx, formatted.GetError());
return false;
}
bool equal;
if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, values.start,
values.end, &equal)) {
return false;
}
// PartitionDateTimeRangePattern, step 12.
if (equal) {
return FormatToPartsDateTime(cx, df, values.start, DateTimeSource::Yes,
result);
}
mozilla::intl::DateTimePartVector parts;
auto r = dif->TryFormattedToParts(formatted, parts);
if (r.isErr()) {
ReportInternalError(cx, r.unwrapErr());
return false;
}
auto spanResult = formatted.ToSpan();
if (spanResult.isErr()) {
ReportInternalError(cx, spanResult.unwrapErr());
return false;
}
return CreateDateTimePartArray(cx, spanResult.unwrap(), DateTimeSource::Yes,
parts, result);
}
static bool IsDateTimeFormat(Handle<JS::Value> v) {
return v.isObject() && v.toObject().is<DateTimeFormatObject>();
}
/**
* UnwrapDateTimeFormat ( dtf )
*/
static bool UnwrapDateTimeFormat(JSContext* cx, MutableHandle<JS::Value> dtf) {
// Step 1. (Error handling moved to caller)
if (!dtf.isObject()) {
return true;
}
auto* obj = &dtf.toObject();
if (obj->canUnwrapAs<DateTimeFormatObject>()) {
return true;
}
Rooted<JSObject*> format(cx, obj);
return UnwrapLegacyIntlFormat(cx, JSProto_DateTimeFormat, format, dtf);
}
static constexpr uint32_t DateTimeFormatFunction_DateTimeFormat = 0;
/**
* DateTime Format Functions
*/
static bool DateTimeCompareFunction(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Steps 1-2.
auto* compare = &args.callee().as<JSFunction>();
auto dtfValue =
compare->getExtendedSlot(DateTimeFormatFunction_DateTimeFormat);
Rooted<DateTimeFormatObject*> dateTimeFormat(
cx, &dtfValue.toObject().as<DateTimeFormatObject>());
// Steps 3-4.
DateTimeValue x;
if (!ToDateTimeValue(cx, "format", dateTimeFormat, args.get(0), &x)) {
return false;
}
// Step 5.
return FormatDateTime(cx, dateTimeFormat, x, args.rval());
}
/**
* get Intl.DateTimeFormat.prototype.format
*/
static bool dateTimeFormat_format(JSContext* cx, const CallArgs& args) {
Rooted<DateTimeFormatObject*> dateTimeFormat(
cx, &args.thisv().toObject().as<DateTimeFormatObject>());
// Step 4.
auto* boundFormat = dateTimeFormat->getBoundFormat();
if (!boundFormat) {
Handle<PropertyName*> funName = cx->names().empty_;
auto* fn =
NewNativeFunction(cx, DateTimeCompareFunction, 1, funName,
gc::AllocKind::FUNCTION_EXTENDED, GenericObject);
if (!fn) {
return false;
}
fn->initExtendedSlot(DateTimeFormatFunction_DateTimeFormat,
ObjectValue(*dateTimeFormat));
dateTimeFormat->setBoundFormat(fn);
boundFormat = fn;
}
// Step 5.
args.rval().setObject(*boundFormat);
return true;
}
/**
* get Intl.DateTimeFormat.prototype.format
*/
static bool dateTimeFormat_format(JSContext* cx, unsigned argc, Value* vp) {
// Steps 1-3.
CallArgs args = CallArgsFromVp(argc, vp);
if (!UnwrapDateTimeFormat(cx, args.mutableThisv())) {
return false;
}
return CallNonGenericMethod<IsDateTimeFormat, dateTimeFormat_format>(cx,
args);
}
/**
* Intl.DateTimeFormat.prototype.formatToParts ( date )
*/
static bool dateTimeFormat_formatToParts(JSContext* cx, const CallArgs& args) {
Rooted<DateTimeFormatObject*> dateTimeFormat(
cx, &args.thisv().toObject().as<DateTimeFormatObject>());
// Steps 3-4.
DateTimeValue x;
if (!ToDateTimeValue(cx, "formatRange", dateTimeFormat, args.get(0), &x)) {
return false;
}
// Step 5.
return FormatToPartsDateTime(cx, dateTimeFormat, x, args.rval());
}
/**
* Intl.DateTimeFormat.prototype.formatToParts ( date )
*/
static bool dateTimeFormat_formatToParts(JSContext* cx, unsigned argc,
Value* vp) {
// Steps 1-2.
CallArgs args = CallArgsFromVp(argc, vp);
return CallNonGenericMethod<IsDateTimeFormat, dateTimeFormat_formatToParts>(
cx, args);
}
/**
* Intl.DateTimeFormat.prototype.formatRange ( startDate, endDate )
*/
static bool dateTimeFormat_formatRange(JSContext* cx, const CallArgs& args) {
Rooted<DateTimeFormatObject*> dateTimeFormat(
cx, &args.thisv().toObject().as<DateTimeFormatObject>());
// Steps 3-5.
DateTimeRangeValue values;
if (!ToDateTimeRangeValue(cx, "formatRange", dateTimeFormat, args.get(0),
args.get(1), &values)) {
return false;
}
// Step 6.
return FormatDateTimeRange(cx, dateTimeFormat, values, args.rval());
}
/**
* Intl.DateTimeFormat.prototype.formatRange ( startDate, endDate )
*/
static bool dateTimeFormat_formatRange(JSContext* cx, unsigned argc,
Value* vp) {
// Steps 1-2.
CallArgs args = CallArgsFromVp(argc, vp);
return CallNonGenericMethod<IsDateTimeFormat, dateTimeFormat_formatRange>(
cx, args);
}
/**
* Intl.DateTimeFormat.prototype.formatRangeToParts ( date )
*/
static bool dateTimeFormat_formatRangeToParts(JSContext* cx,
const CallArgs& args) {
Rooted<DateTimeFormatObject*> dateTimeFormat(
cx, &args.thisv().toObject().as<DateTimeFormatObject>());
// Steps 3-5.
DateTimeRangeValue values;
if (!ToDateTimeRangeValue(cx, "formatRangeToParts", dateTimeFormat,
args.get(0), args.get(1), &values)) {
return false;
}
// Step 6.
return FormatDateTimeRangeToParts(cx, dateTimeFormat, values, args.rval());
}
/**
* Intl.DateTimeFormat.prototype.formatRangeToParts ( date )
*/
static bool dateTimeFormat_formatRangeToParts(JSContext* cx, unsigned argc,
Value* vp) {
// Steps 1-2.
CallArgs args = CallArgsFromVp(argc, vp);
return CallNonGenericMethod<IsDateTimeFormat,
dateTimeFormat_formatRangeToParts>(cx, args);
}
/**
* Intl.DateTimeFormat.prototype.resolvedOptions ( )
*/
static bool dateTimeFormat_resolvedOptions(JSContext* cx,
const CallArgs& args) {
Rooted<DateTimeFormatObject*> dateTimeFormat(
cx, &args.thisv().toObject().as<DateTimeFormatObject>());
if (!ResolveLocale(cx, dateTimeFormat)) {
return false;
}
auto dtfOptions = *dateTimeFormat->getOptions();
// Step 3.
Rooted<IdValueVector> options(cx, cx);
// Step 4.
if (!options.emplaceBack(NameToId(cx->names().locale),
StringValue(dateTimeFormat->getLocale()))) {
return false;
}
if (!options.emplaceBack(NameToId(cx->names().calendar),
StringValue(dateTimeFormat->getCalendar()))) {
return false;
}
if (!options.emplaceBack(NameToId(cx->names().numberingSystem),
StringValue(dateTimeFormat->getNumberingSystem()))) {
return false;
}
if (!options.emplaceBack(NameToId(cx->names().timeZone),
StringValue(dateTimeFormat->getTimeZone()))) {
return false;
}
// The raw pattern option is only internal to Mozilla, and not part of the
// ECMA-402 API.
if (auto* pattern = dateTimeFormat->getPattern()) {
if (!options.emplaceBack(NameToId(cx->names().pattern),
StringValue(pattern))) {
return false;
}
}
bool hasDateStyle = dtfOptions.dateStyle.isSome();
bool hasTimeStyle = dtfOptions.timeStyle.isSome();
if (hasDateStyle || hasTimeStyle) {
if (hasTimeStyle) {
// timeStyle (unlike dateStyle) requires resolving the pattern to ensure
// "hourCycle" and "hour12" properties are added to |result|.
if (!ResolveDateTimeFormatComponents(cx, dateTimeFormat, &options,
IncludeDateTimeFields::No)) {
return false;
}
}
if (hasDateStyle) {
auto* dateStyle =
NewStringCopy<CanGC>(cx, DateStyleToString(*dtfOptions.dateStyle));
if (!dateStyle) {
return false;
}
if (!options.emplaceBack(NameToId(cx->names().dateStyle),
StringValue(dateStyle))) {
return false;
}
}
if (hasTimeStyle) {
auto* timeStyle =
NewStringCopy<CanGC>(cx, TimeStyleToString(*dtfOptions.timeStyle));
if (!timeStyle) {
return false;
}
if (!options.emplaceBack(NameToId(cx->names().timeStyle),
StringValue(timeStyle))) {
return false;
}
}
} else {
if (!ResolveDateTimeFormatComponents(cx, dateTimeFormat, &options,
IncludeDateTimeFields::Yes)) {
return false;
}
}
// Step 6.
auto* result = NewPlainObjectWithUniqueNames(cx, options);
if (!result) {
return false;
}
args.rval().setObject(*result);
return true;
}
/**
* Intl.DateTimeFormat.prototype.resolvedOptions ( )
*/
static bool dateTimeFormat_resolvedOptions(JSContext* cx, unsigned argc,
Value* vp) {
// Steps 1-2.
CallArgs args = CallArgsFromVp(argc, vp);
if (!UnwrapDateTimeFormat(cx, args.mutableThisv())) {
return false;
}
return CallNonGenericMethod<IsDateTimeFormat, dateTimeFormat_resolvedOptions>(
cx, args);
}
/**
* Intl.DateTimeFormat.supportedLocalesOf ( locales [ , options ] )
*/
static bool dateTimeFormat_supportedLocalesOf(JSContext* cx, unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Steps 1-3.
auto* array = SupportedLocalesOf(cx, AvailableLocaleKind::DateTimeFormat,
args.get(0), args.get(1));
if (!array) {
return false;
}
args.rval().setObject(*array);
return true;
}
bool js::intl::TemporalObjectToLocaleString(
JSContext* cx, const CallArgs& args, DateTimeFormatKind formatKind,
Handle<JSLinearString*> toLocaleStringTimeZone) {
Rooted<JSObject*> thisValue(cx, &args.thisv().toObject());
auto kind = ToDateTimeFormattable(args.thisv());
MOZ_ASSERT(kind != DateTimeValueKind::Number);
MOZ_ASSERT_IF(kind != DateTimeValueKind::TemporalZonedDateTime,
toLocaleStringTimeZone == nullptr);
MOZ_ASSERT_IF(kind == DateTimeValueKind::TemporalZonedDateTime,
toLocaleStringTimeZone != nullptr);
auto locales = args.get(0);
auto options = args.get(1);
Rooted<DateTimeFormatObject*> dateTimeFormat(cx);
if (kind != DateTimeValueKind::TemporalZonedDateTime) {
dateTimeFormat =
GetOrCreateDateTimeFormat(cx, locales, options, formatKind);
} else {
// Cache doesn't yet support Temporal.ZonedDateTime.
dateTimeFormat = ::CreateDateTimeFormat(cx, locales, options,
toLocaleStringTimeZone, formatKind);
}
if (!dateTimeFormat) {
return false;
}
JS::ClippedTime x;
if (kind == DateTimeValueKind::TemporalZonedDateTime) {
auto zonedDateTime = thisValue.as<ZonedDateTimeObject>();
if (!HandleDateTimeTemporalZonedDateTime(cx, dateTimeFormat, zonedDateTime,
&x)) {
return false;
}
} else {
if (!HandleDateTimeValue(cx, "toLocaleString", dateTimeFormat, thisValue,
&x)) {
return false;
}
}
return FormatDateTime(cx, dateTimeFormat, {x, kind}, args.rval());
}