From c981fb13ea11d3fab393a63c47bfdc2dd29efb7e Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 00:14:14 +0100 Subject: [PATCH] Issue #2046 - Move Intl.DateTimeFormat functionality into builtin/intl/DateTimeFormat.* --- js/src/builtin/Intl.cpp | 785 +----------------------- js/src/builtin/Intl.h | 96 --- js/src/builtin/intl/DateTimeFormat.cpp | 819 +++++++++++++++++++++++++ js/src/builtin/intl/DateTimeFormat.h | 143 +++++ js/src/moz.build | 1 + js/src/vm/SelfHosting.cpp | 1 + 6 files changed, 965 insertions(+), 880 deletions(-) create mode 100644 js/src/builtin/intl/DateTimeFormat.cpp create mode 100644 js/src/builtin/intl/DateTimeFormat.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 48f8791ac4..3bf86b53a3 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -24,10 +24,10 @@ #include "builtin/intl/Collator.h" #include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/DateTimeFormat.h" #include "builtin/intl/ICUHeader.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/ScopedICUObject.h" -#include "builtin/intl/SharedIntlData.h" #include "ds/Sort.h" #include "vm/DateTime.h" #include "vm/GlobalObject.h" @@ -45,796 +45,13 @@ using namespace js; using mozilla::AssertedCast; using mozilla::IsFinite; using mozilla::IsNegativeZero; -using mozilla::PodCopy; using js::intl::CallICU; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; -using js::intl::SharedIntlData; using js::intl::StringsAreEqual; -/******************** DateTimeFormat ********************/ - -static void dateTimeFormat_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t UDATE_FORMAT_SLOT = 0; -static const uint32_t DATE_TIME_FORMAT_SLOTS_COUNT = 1; - -static const ClassOps DateTimeFormatClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - dateTimeFormat_finalize -}; - -static const Class DateTimeFormatClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(DATE_TIME_FORMAT_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &DateTimeFormatClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().DateTimeFormat); - return true; -} -#endif - -static const JSFunctionSpec dateTimeFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_DateTimeFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec dateTimeFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 0, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct) -{ - RootedObject obj(cx); - - // We're following ECMA-402 1st Edition when DateTimeFormat is called - // because of backward compatibility issues. - // See https://github.com/tc39/ecma402/issues/57 - if (!construct) { - // ES Intl 1st ed., 12.1.2.1 step 3 - JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); - if (!intl) - return false; - RootedValue self(cx, args.thisv()); - if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { - // ES Intl 1st ed., 12.1.2.1 step 4 - obj = ToObject(cx, self); - if (!obj) - return false; - - // ES Intl 1st ed., 12.1.2.1 step 5 - bool extensible; - if (!IsExtensible(cx, obj, &extensible)) - return false; - if (!extensible) - return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); - } else { - // ES Intl 1st ed., 12.1.2.1 step 3.a - construct = true; - } - } - if (construct) { - // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateDateTimeFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - obj = NewObjectWithGivenProto(cx, &DateTimeFormatClass, proto); - if (!obj) - return false; - - obj->as().setReservedSlot(UDATE_FORMAT_SLOT, PrivateValue(nullptr)); - } - - RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); - RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); - - // Step 3. - if (!intl::InitializeObject(cx, obj, cx->names().InitializeDateTimeFormat, locales, options)) - return false; - - args.rval().setObject(*obj); - return true; -} - -static bool -DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return DateTimeFormat(cx, args, args.isConstructing()); -} - -bool -js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it - // cannot be used with "new", but it still has to be treated as a - // constructor. - return DateTimeFormat(cx, args, true); -} - -static void -dateTimeFormat_finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - // This is-undefined check shouldn't be necessary, but for internal - // brokenness in object allocation code. For the moment, hack around it by - // explicitly guarding against the possibility of the reserved slot not - // containing a private. See bug 949220. - const Value& slot = obj->as().getReservedSlot(UDATE_FORMAT_SLOT); - if (!slot.isUndefined()) { - if (UDateFormat* df = static_cast(slot.toPrivate())) - udat_close(df); - } -} - -static JSObject* -CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx); - ctor = GlobalObject::createConstructor(cx, &DateTimeFormat, cx->names().DateTimeFormat, 0); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, - &DateTimeFormatClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(UDATE_FORMAT_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 12.2.2 - if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) - return nullptr; - - // 12.3.2 and 12.3.3 - if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) - return nullptr; - - // Install a getter for DateTimeFormat.prototype.format that returns a - // formatting function bound to a specified DateTimeFormat object (suitable - // for passing to methods like Array.prototype.map). - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().DateTimeFormatFormatGet, - &getter)) - { - return nullptr; - } - if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, - JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), - nullptr, JSPROP_GETTER | JSPROP_SHARED)) - { - return nullptr; - } - - RootedValue options(cx); - if (!intl::CreateDefaultOptions(cx, &options)) - return nullptr; - - // 12.2.1 and 12.3 - if (!intl::InitializeObject(cx, proto, cx->names().InitializeDateTimeFormat, UndefinedHandleValue, - options)) - { - return nullptr; - } - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().DateTimeFormat, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - return proto; -} - -bool -js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - if (!GetAvailableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - -// ICU returns old-style keyword values; map them to BCP 47 equivalents -// (see http://bugs.icu-project.org/trac/ticket/9620). -static const char* -bcp47CalendarName(const char* icuName) -{ - if (StringsAreEqual(icuName, "ethiopic-amete-alem")) - return "ethioaa"; - if (StringsAreEqual(icuName, "gregorian")) - return "gregory"; - if (StringsAreEqual(icuName, "islamic-civil")) - return "islamicc"; - return icuName; -} - -bool -js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - RootedObject calendars(cx, NewDenseEmptyArray(cx)); - if (!calendars) - return false; - uint32_t index = 0; - - // We need the default calendar for the locale as the first result. - UErrorCode status = U_ZERO_ERROR; - RootedString jscalendar(cx); - { - UCalendar* cal = ucal_open(nullptr, 0, locale.ptr(), UCAL_DEFAULT, &status); - - // This correctly handles nullptr |cal| when opening failed. - ScopedICUObject closeCalendar(cal); - - const char* calendar = ucal_getType(cal, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); - if (!jscalendar) - return false; - } - - RootedValue element(cx, StringValue(jscalendar)); - if (!DefineElement(cx, calendars, index++, element)) - return false; - - // Now get the calendars that "would make a difference", i.e., not the default. - UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(values); - - uint32_t count = uenum_count(values, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - for (; count > 0; count--) { - const char* calendar = uenum_next(values, nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); - if (!jscalendar) - return false; - element = StringValue(jscalendar); - if (!DefineElement(cx, calendars, index++, element)) - return false; - } - - args.rval().setObject(*calendars); - return true; -} - -bool -js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - SharedIntlData& sharedIntlData = cx->sharedIntlData; - - RootedString timeZone(cx, args[0].toString()); - RootedString validatedTimeZone(cx); - if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) - return false; - - if (validatedTimeZone) - args.rval().setString(validatedTimeZone); - else - args.rval().setNull(); - - return true; -} - -bool -js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - SharedIntlData& sharedIntlData = cx->sharedIntlData; - - // Some time zone names are canonicalized differently by ICU -- handle - // those first: - RootedString timeZone(cx, args[0].toString()); - RootedString ianaTimeZone(cx); - if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(cx, timeZone, &ianaTimeZone)) - return false; - - if (ianaTimeZone) { - args.rval().setString(ianaTimeZone); - return true; - } - - AutoStableStringChars stableChars(cx); - if (!stableChars.initTwoByte(cx, timeZone)) - return false; - - mozilla::Range tzchars = stableChars.twoByteRange(); - - JSString* str = CallICU(cx, [&tzchars](UChar* chars, uint32_t size, UErrorCode* status) { - return ucal_getCanonicalTimeZoneID(tzchars.begin().get(), tzchars.length(), - chars, size, nullptr, status); - }); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -bool -js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - // The current default might be stale, because JS::ResetTimeZone() doesn't - // immediately update ICU's default time zone. So perform an update if - // needed. - js::ResyncICUDefaultTimeZone(); - - JSString* str = CallICU(cx, ucal_getDefaultTimeZone); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -bool -js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - UErrorCode status = U_ZERO_ERROR; - const UChar* uTimeZone = nullptr; - int32_t uTimeZoneLength = 0; - const char* rootLocale = ""; - UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(cal); - - int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - args.rval().setInt32(offset); - return true; -} - -bool -js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(args[0].isString()); - MOZ_ASSERT(args[1].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - JSFlatString* skeletonFlat = args[1].toString()->ensureFlat(cx); - if (!skeletonFlat) - return false; - - AutoStableStringChars stableChars(cx); - if (!stableChars.initTwoByte(cx, skeletonFlat)) - return false; - - mozilla::Range skeletonChars = stableChars.twoByteRange(); - uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); - - UErrorCode status = U_ZERO_ERROR; - UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(gen); - - JSString* str = - CallICU(cx, [gen, &skeletonChars, skeletonLen](UChar* chars, uint32_t size, UErrorCode* status) { - return udatpg_getBestPattern(gen, skeletonChars.begin().get(), skeletonLen, - chars, size, status); - }); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -/** - * Returns a new UDateFormat with the locale and date-time formatting options - * of the given DateTimeFormat. - */ -static UDateFormat* -NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) -{ - RootedValue value(cx); - - RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - // We don't need to look at calendar and numberingSystem - they can only be - // set via the Unicode locale extension and are therefore already set on - // locale. - - if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) - return nullptr; - - AutoStableStringChars timeZoneChars(cx); - Rooted timeZoneFlat(cx, value.toString()->ensureFlat(cx)); - if (!timeZoneFlat || !timeZoneChars.initTwoByte(cx, timeZoneFlat)) - return nullptr; - - const UChar* uTimeZone = Char16ToUChar(timeZoneChars.twoByteRange().begin().get()); - uint32_t uTimeZoneLength = u_strlen(uTimeZone); - - if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) - return nullptr; - - AutoStableStringChars patternChars(cx); - Rooted patternFlat(cx, value.toString()->ensureFlat(cx)); - if (!patternFlat || !patternChars.initTwoByte(cx, patternFlat)) - return nullptr; - - const UChar* uPattern = Char16ToUChar(patternChars.twoByteRange().begin().get()); - uint32_t uPatternLength = u_strlen(uPattern); - - UErrorCode status = U_ZERO_ERROR; - UDateFormat* df = - udat_open(UDAT_PATTERN, UDAT_PATTERN, IcuLocale(locale.ptr()), uTimeZone, uTimeZoneLength, - uPattern, uPatternLength, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - - // ECMAScript requires the Gregorian calendar to be used from the beginning - // of ECMAScript time. - UCalendar* cal = const_cast(udat_getCalendar(df)); - ucal_setGregorianChange(cal, StartOfTime, &status); - - // An error here means the calendar is not Gregorian, so we don't care. - - return df; -} - -static bool -intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) -{ - if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); - return false; - } - - JSString* str = CallICU(cx, [df, x](UChar* chars, int32_t size, UErrorCode* status) { - return udat_format(df, x, chars, size, nullptr, status); - }); - if (!str) - return false; - - result.setString(str); - return true; -} - -using FieldType = ImmutablePropertyNamePtr JSAtomState::*; - -static FieldType -GetFieldTypeForFormatField(UDateFormatField fieldName) -{ - // See intl/icu/source/i18n/unicode/udat.h for a detailed field list. This - // switch is deliberately exhaustive: cases might have to be added/removed - // if this code is compiled with a different ICU with more - // UDateFormatField enum initializers. Please guard such cases with - // appropriate ICU version-testing #ifdefs, should cross-version divergence - // occur. - switch (fieldName) { - case UDAT_ERA_FIELD: - return &JSAtomState::era; - case UDAT_YEAR_FIELD: - case UDAT_YEAR_WOY_FIELD: - case UDAT_EXTENDED_YEAR_FIELD: - case UDAT_YEAR_NAME_FIELD: - return &JSAtomState::year; - - case UDAT_MONTH_FIELD: - case UDAT_STANDALONE_MONTH_FIELD: - return &JSAtomState::month; - - case UDAT_DATE_FIELD: - case UDAT_JULIAN_DAY_FIELD: - return &JSAtomState::day; - - case UDAT_HOUR_OF_DAY1_FIELD: - case UDAT_HOUR_OF_DAY0_FIELD: - case UDAT_HOUR1_FIELD: - case UDAT_HOUR0_FIELD: - return &JSAtomState::hour; - - case UDAT_MINUTE_FIELD: - return &JSAtomState::minute; - - case UDAT_SECOND_FIELD: - return &JSAtomState::second; - - case UDAT_DAY_OF_WEEK_FIELD: - case UDAT_STANDALONE_DAY_FIELD: - case UDAT_DOW_LOCAL_FIELD: - case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD: - return &JSAtomState::weekday; - - case UDAT_AM_PM_FIELD: - return &JSAtomState::dayPeriod; - - case UDAT_TIMEZONE_FIELD: - return &JSAtomState::timeZoneName; - - case UDAT_FRACTIONAL_SECOND_FIELD: - case UDAT_DAY_OF_YEAR_FIELD: - case UDAT_WEEK_OF_YEAR_FIELD: - case UDAT_WEEK_OF_MONTH_FIELD: - case UDAT_MILLISECONDS_IN_DAY_FIELD: - case UDAT_TIMEZONE_RFC_FIELD: - case UDAT_TIMEZONE_GENERIC_FIELD: - case UDAT_QUARTER_FIELD: - case UDAT_STANDALONE_QUARTER_FIELD: - case UDAT_TIMEZONE_SPECIAL_FIELD: - case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: - case UDAT_TIMEZONE_ISO_FIELD: - case UDAT_TIMEZONE_ISO_LOCAL_FIELD: -#ifndef U_HIDE_INTERNAL_API - case UDAT_RELATED_YEAR_FIELD: -#endif -#ifndef U_HIDE_DRAFT_API - case UDAT_AM_PM_MIDNIGHT_NOON_FIELD: - case UDAT_FLEXIBLE_DAY_PERIOD_FIELD: -#endif -#ifndef U_HIDE_INTERNAL_API - case UDAT_TIME_SEPARATOR_FIELD: -#endif - // These fields are all unsupported. - return nullptr; - - case UDAT_FIELD_COUNT: - MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " - "iterator!"); - } - - MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " - "by iterator"); - return nullptr; -} - -static bool -intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) -{ - if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); - return false; - } - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - UErrorCode status = U_ZERO_ERROR; - UFieldPositionIterator* fpositer = ufieldpositer_open(&status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(fpositer); - - RootedString overallResult(cx); - overallResult = CallICU(cx, [df, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { - return udat_formatForFields(df, x, chars, size, fpositer, status); - }); - if (!overallResult) - return false; - - RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); - if (!partsArray) - return false; - if (overallResult->length() == 0) { - // An empty string contains no parts, so avoid extra work below. - result.setObject(*partsArray); - return true; - } - - size_t lastEndIndex = 0; - - uint32_t partIndex = 0; - RootedObject singlePart(cx); - RootedValue partType(cx); - RootedValue val(cx); - - auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) { - singlePart = NewBuiltinClassInstance(cx); - if (!singlePart) - return false; - - partType = StringValue(cx->names().*type); - if (!DefineProperty(cx, singlePart, cx->names().type, partType)) - return false; - - JSLinearString* partSubstr = - NewDependentString(cx, overallResult, beginIndex, endIndex - beginIndex); - if (!partSubstr) - return false; - - val = StringValue(partSubstr); - if (!DefineProperty(cx, singlePart, cx->names().value, val)) - return false; - - val = ObjectValue(*singlePart); - if (!DefineElement(cx, partsArray, partIndex, val)) - return false; - - lastEndIndex = endIndex; - partIndex++; - return true; - }; - - int32_t fieldInt, beginIndexInt, endIndexInt; - while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { - MOZ_ASSERT(beginIndexInt >= 0); - MOZ_ASSERT(endIndexInt >= 0); - MOZ_ASSERT(beginIndexInt <= endIndexInt, - "field iterator returning invalid range"); - - size_t beginIndex(beginIndexInt); - size_t endIndex(endIndexInt); - - // Technically this isn't guaranteed. But it appears true in pratice, - // and http://bugs.icu-project.org/trac/ticket/12024 is expected to - // correct the documentation lapse. - MOZ_ASSERT(lastEndIndex <= beginIndex, - "field iteration didn't return fields in order start to " - "finish as expected"); - - if (FieldType type = GetFieldTypeForFormatField(static_cast(fieldInt))) { - if (lastEndIndex < beginIndex) { - if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) - return false; - } - - if (!AppendPart(type, beginIndex, endIndex)) - return false; - } - } - - // Append any final literal. - if (lastEndIndex < overallResult->length()) { - if (!AppendPart(&JSAtomState::literal, lastEndIndex, overallResult->length())) - return false; - } - - result.setObject(*partsArray); - return true; -} - -bool -js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - MOZ_ASSERT(args[0].isObject()); - MOZ_ASSERT(args[1].isNumber()); - MOZ_ASSERT(args[2].isBoolean()); - - RootedObject dateTimeFormat(cx, &args[0].toObject()); - - // Obtain a UDateFormat object, cached if possible. - bool isDateTimeFormatInstance = dateTimeFormat->getClass() == &DateTimeFormatClass; - UDateFormat* df; - if (isDateTimeFormatInstance) { - void* priv = - dateTimeFormat->as().getReservedSlot(UDATE_FORMAT_SLOT).toPrivate(); - df = static_cast(priv); - if (!df) { - df = NewUDateFormat(cx, dateTimeFormat); - if (!df) - return false; - dateTimeFormat->as().setReservedSlot(UDATE_FORMAT_SLOT, PrivateValue(df)); - } - } else { - // There's no good place to cache the ICU date-time format for an object - // that has been initialized as a DateTimeFormat but is not a - // DateTimeFormat instance. One possibility might be to add a - // DateTimeFormat instance as an internal property to each such object. - df = NewUDateFormat(cx, dateTimeFormat); - if (!df) - return false; - } - - // Use the UDateFormat to actually format the time stamp. - RootedValue result(cx); - bool success = args[2].toBoolean() - ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), &result) - : intl_FormatDateTime(cx, df, args[1].toNumber(), &result); - - if (!isDateTimeFormatInstance) - udat_close(df); - if (!success) - return false; - args.rval().set(result); - return true; -} - /**************** PluralRules *****************/ static void pluralRules_finalize(FreeOp* fop, JSObject* obj); diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 0f934debfb..49a76dfb5c 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -35,102 +35,6 @@ InitIntlClass(JSContext* cx, HandleObject obj); * The following functions are for use by self-hosted code. */ - -/******************** DateTimeFormat ********************/ - -/** - * Returns a new instance of the standard built-in DateTimeFormat constructor. - * Self-hosted code cannot cache this constructor (as it does for others in - * Utilities.js) because it is initialized after self-hosted code is compiled. - * - * Usage: dateTimeFormat = intl_DateTimeFormat(locales, options) - */ -extern MOZ_MUST_USE bool -intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an object indicating the supported locales for date and time - * formatting by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_DateTimeFormat_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an array with the calendar type identifiers per Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * supported calendars for the given locale. The default calendar is - * element 0. - * - * Usage: calendars = intl_availableCalendars(locale) - */ -extern MOZ_MUST_USE bool -intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp); - -/** - * 6.4.1 IsValidTimeZoneName ( timeZone ) - * - * Verifies that the given string is a valid time zone name. If it is a valid - * time zone name, its IANA time zone name is returned. Otherwise returns null. - * - * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 - * - * Usage: ianaTimeZone = intl_IsValidTimeZoneName(timeZone) - */ -extern MOZ_MUST_USE bool -intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return the canonicalized time zone name. Canonicalization resolves link - * names to their target time zones. - * - * Usage: ianaTimeZone = intl_canonicalizeTimeZone(timeZone) - */ -extern MOZ_MUST_USE bool -intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return the default time zone name. The time zone name is not canonicalized. - * - * Usage: icuDefaultTimeZone = intl_defaultTimeZone() - */ -extern MOZ_MUST_USE bool -intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return the raw offset from GMT in milliseconds for the default time zone. - * - * Usage: defaultTimeZoneOffset = intl_defaultTimeZoneOffset() - */ -extern MOZ_MUST_USE bool -intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return a pattern in the date-time format pattern language of Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * best-fit date-time format pattern corresponding to skeleton for the - * given locale. - * - * Usage: pattern = intl_patternForSkeleton(locale, skeleton) - */ -extern MOZ_MUST_USE bool -intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns a String value representing x (which must be a Number value) - * according to the effective locale and the formatting options of the - * given DateTimeFormat. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. - * - * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x) - */ -extern MOZ_MUST_USE bool -intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); - /******************** PluralRules ********************/ /** diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp new file mode 100644 index 0000000000..167beea0c1 --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -0,0 +1,819 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Intl.DateTimeFormat implementation. */ + +#include "builtin/intl/DateTimeFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Range.h" + +#include "jscntxt.h" +#include "jsfriendapi.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" +#include "builtin/IntlTimeZoneData.h" +#include "vm/GlobalObject.h" +#include "vm/Runtime.h" + +#include "jsobjinlines.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::IsFinite; + +using JS::ClippedTime; +using JS::TimeClip; + +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::SharedIntlData; +using js::intl::StringsAreEqual; + +/******************** DateTimeFormat ********************/ + +const ClassOps DateTimeFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + DateTimeFormatObject::finalize +}; + +const Class DateTimeFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &DateTimeFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().DateTimeFormat); + return true; +} +#endif + +static const JSFunctionSpec dateTimeFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_DateTimeFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec dateTimeFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 0, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct) +{ + RootedObject obj(cx); + + // We're following ECMA-402 1st Edition when DateTimeFormat is called + // because of backward compatibility issues. + // See https://github.com/tc39/ecma402/issues/57 + if (!construct) { + // ES Intl 1st ed., 12.1.2.1 step 3 + JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); + if (!intl) + return false; + RootedValue self(cx, args.thisv()); + if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { + // ES Intl 1st ed., 12.1.2.1 step 4 + obj = ToObject(cx, self); + if (!obj) + return false; + + // ES Intl 1st ed., 12.1.2.1 step 5 + bool extensible; + if (!IsExtensible(cx, obj, &extensible)) + return false; + if (!extensible) + return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); + } else { + // ES Intl 1st ed., 12.1.2.1 step 3.a + construct = true; + } + } + if (construct) { + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateDateTimeFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + obj = NewObjectWithGivenProto(cx, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(DateTimeFormatObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(nullptr)); + } + + RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); + RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); + + // Step 3. + if (!intl::InitializeObject(cx, obj, cx->names().InitializeDateTimeFormat, locales, options)) + return false; + + args.rval().setObject(*obj); + return true; +} + +static bool +DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return DateTimeFormat(cx, args, args.isConstructing()); +} + +bool +js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it + // cannot be used with "new", but it still has to be treated as a + // constructor. + return DateTimeFormat(cx, args, true); +} + +void +js::DateTimeFormatObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onMainThread()); + + // This is-undefined check shouldn't be necessary, but for internal + // brokenness in object allocation code. For the moment, hack around it by + // explicitly guarding against the possibility of the reserved slot not + // containing a private. See bug 949220. + const Value& slot = obj->as().getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT); + if (!slot.isUndefined()) { + if (UDateFormat* df = static_cast(slot.toPrivate())) + udat_close(df); + } +} + +JSObject* +js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = GlobalObject::createConstructor(cx, &DateTimeFormat, cx->names().DateTimeFormat, 0); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, + &DateTimeFormatObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 12.2.2 + if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) + return nullptr; + + // 12.3.2 and 12.3.3 + if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) + return nullptr; + + // Install a getter for DateTimeFormat.prototype.format that returns a + // formatting function bound to a specified DateTimeFormat object (suitable + // for passing to methods like Array.prototype.map). + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().DateTimeFormatFormatGet, + &getter)) + { + return nullptr; + } + if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, + JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), + nullptr, JSPROP_GETTER | JSPROP_SHARED)) + { + return nullptr; + } + + RootedValue options(cx); + if (!intl::CreateDefaultOptions(cx, &options)) + return nullptr; + + // 12.2.1 and 12.3 + if (!intl::InitializeObject(cx, proto, cx->names().InitializeDateTimeFormat, UndefinedHandleValue, + options)) + { + return nullptr; + } + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().DateTimeFormat, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + if (!GetAvailableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +// ICU returns old-style keyword values; map them to BCP 47 equivalents +// (see http://bugs.icu-project.org/trac/ticket/9620). +static const char* +bcp47CalendarName(const char* icuName) +{ + if (StringsAreEqual(icuName, "ethiopic-amete-alem")) + return "ethioaa"; + if (StringsAreEqual(icuName, "gregorian")) + return "gregory"; + if (StringsAreEqual(icuName, "islamic-civil")) + return "islamicc"; + return icuName; +} + +bool +js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + RootedObject calendars(cx, NewDenseEmptyArray(cx)); + if (!calendars) + return false; + uint32_t index = 0; + + // We need the default calendar for the locale as the first result. + UErrorCode status = U_ZERO_ERROR; + RootedString jscalendar(cx); + { + UCalendar* cal = ucal_open(nullptr, 0, locale.ptr(), UCAL_DEFAULT, &status); + + // This correctly handles nullptr |cal| when opening failed. + ScopedICUObject closeCalendar(cal); + + const char* calendar = ucal_getType(cal, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); + if (!jscalendar) + return false; + } + + RootedValue element(cx, StringValue(jscalendar)); + if (!DefineElement(cx, calendars, index++, element)) + return false; + + // Now get the calendars that "would make a difference", i.e., not the default. + UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + uint32_t count = uenum_count(values, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + for (; count > 0; count--) { + const char* calendar = uenum_next(values, nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); + if (!jscalendar) + return false; + element = StringValue(jscalendar); + if (!DefineElement(cx, calendars, index++, element)) + return false; + } + + args.rval().setObject(*calendars); + return true; +} + +bool +js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->sharedIntlData; + + RootedString timeZone(cx, args[0].toString()); + RootedString validatedTimeZone(cx); + if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) + return false; + + if (validatedTimeZone) + args.rval().setString(validatedTimeZone); + else + args.rval().setNull(); + + return true; +} + +bool +js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->sharedIntlData; + + // Some time zone names are canonicalized differently by ICU -- handle + // those first: + RootedString timeZone(cx, args[0].toString()); + RootedString ianaTimeZone(cx); + if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(cx, timeZone, &ianaTimeZone)) + return false; + + if (ianaTimeZone) { + args.rval().setString(ianaTimeZone); + return true; + } + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, timeZone)) + return false; + + mozilla::Range tzchars = stableChars.twoByteRange(); + + JSString* str = CallICU(cx, [&tzchars](UChar* chars, uint32_t size, UErrorCode* status) { + return ucal_getCanonicalTimeZoneID(tzchars.begin().get(), tzchars.length(), + chars, size, nullptr, status); + }); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +bool +js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + // The current default might be stale, because JS::ResetTimeZone() doesn't + // immediately update ICU's default time zone. So perform an update if + // needed. + js::ResyncICUDefaultTimeZone(); + + JSString* str = CallICU(cx, ucal_getDefaultTimeZone); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +bool +js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + UErrorCode status = U_ZERO_ERROR; + const UChar* uTimeZone = nullptr; + int32_t uTimeZoneLength = 0; + const char* rootLocale = ""; + UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(cal); + + int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + args.rval().setInt32(offset); + return true; +} + +bool +js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + JSFlatString* skeletonFlat = args[1].toString()->ensureFlat(cx); + if (!skeletonFlat) + return false; + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, skeletonFlat)) + return false; + + mozilla::Range skeletonChars = stableChars.twoByteRange(); + uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); + + UErrorCode status = U_ZERO_ERROR; + UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(gen); + + JSString* str = + CallICU(cx, [gen, &skeletonChars, skeletonLen](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getBestPattern(gen, skeletonChars.begin().get(), skeletonLen, + chars, size, status); + }); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +/** + * Returns a new UDateFormat with the locale and date-time formatting options + * of the given DateTimeFormat. + */ +static UDateFormat* +NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + // We don't need to look at calendar and numberingSystem - they can only be + // set via the Unicode locale extension and are therefore already set on + // locale. + + if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) + return nullptr; + + AutoStableStringChars timeZoneChars(cx); + Rooted timeZoneFlat(cx, value.toString()->ensureFlat(cx)); + if (!timeZoneFlat || !timeZoneChars.initTwoByte(cx, timeZoneFlat)) + return nullptr; + + const UChar* uTimeZone = Char16ToUChar(timeZoneChars.twoByteRange().begin().get()); + uint32_t uTimeZoneLength = u_strlen(uTimeZone); + + if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) + return nullptr; + + AutoStableStringChars patternChars(cx); + Rooted patternFlat(cx, value.toString()->ensureFlat(cx)); + if (!patternFlat || !patternChars.initTwoByte(cx, patternFlat)) + return nullptr; + + const UChar* uPattern = Char16ToUChar(patternChars.twoByteRange().begin().get()); + uint32_t uPatternLength = u_strlen(uPattern); + + UErrorCode status = U_ZERO_ERROR; + UDateFormat* df = + udat_open(UDAT_PATTERN, UDAT_PATTERN, IcuLocale(locale.ptr()), uTimeZone, uTimeZoneLength, + uPattern, uPatternLength, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + + // ECMAScript requires the Gregorian calendar to be used from the beginning + // of ECMAScript time. + UCalendar* cal = const_cast(udat_getCalendar(df)); + ucal_setGregorianChange(cal, StartOfTime, &status); + + // An error here means the calendar is not Gregorian, so we don't care. + + return df; +} + +static bool +intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) +{ + if (!IsFinite(x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); + return false; + } + + JSString* str = CallICU(cx, [df, x](UChar* chars, int32_t size, UErrorCode* status) { + return udat_format(df, x, chars, size, nullptr, status); + }); + if (!str) + return false; + + result.setString(str); + return true; +} + +using FieldType = ImmutablePropertyNamePtr JSAtomState::*; + +static FieldType +GetFieldTypeForFormatField(UDateFormatField fieldName) +{ + // See intl/icu/source/i18n/unicode/udat.h for a detailed field list. This + // switch is deliberately exhaustive: cases might have to be added/removed + // if this code is compiled with a different ICU with more + // UDateFormatField enum initializers. Please guard such cases with + // appropriate ICU version-testing #ifdefs, should cross-version divergence + // occur. + switch (fieldName) { + case UDAT_ERA_FIELD: + return &JSAtomState::era; + case UDAT_YEAR_FIELD: + case UDAT_YEAR_WOY_FIELD: + case UDAT_EXTENDED_YEAR_FIELD: + case UDAT_YEAR_NAME_FIELD: + return &JSAtomState::year; + + case UDAT_MONTH_FIELD: + case UDAT_STANDALONE_MONTH_FIELD: + return &JSAtomState::month; + + case UDAT_DATE_FIELD: + case UDAT_JULIAN_DAY_FIELD: + return &JSAtomState::day; + + case UDAT_HOUR_OF_DAY1_FIELD: + case UDAT_HOUR_OF_DAY0_FIELD: + case UDAT_HOUR1_FIELD: + case UDAT_HOUR0_FIELD: + return &JSAtomState::hour; + + case UDAT_MINUTE_FIELD: + return &JSAtomState::minute; + + case UDAT_SECOND_FIELD: + return &JSAtomState::second; + + case UDAT_DAY_OF_WEEK_FIELD: + case UDAT_STANDALONE_DAY_FIELD: + case UDAT_DOW_LOCAL_FIELD: + case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD: + return &JSAtomState::weekday; + + case UDAT_AM_PM_FIELD: + return &JSAtomState::dayPeriod; + + case UDAT_TIMEZONE_FIELD: + return &JSAtomState::timeZoneName; + + case UDAT_FRACTIONAL_SECOND_FIELD: + case UDAT_DAY_OF_YEAR_FIELD: + case UDAT_WEEK_OF_YEAR_FIELD: + case UDAT_WEEK_OF_MONTH_FIELD: + case UDAT_MILLISECONDS_IN_DAY_FIELD: + case UDAT_TIMEZONE_RFC_FIELD: + case UDAT_TIMEZONE_GENERIC_FIELD: + case UDAT_QUARTER_FIELD: + case UDAT_STANDALONE_QUARTER_FIELD: + case UDAT_TIMEZONE_SPECIAL_FIELD: + case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: + case UDAT_TIMEZONE_ISO_FIELD: + case UDAT_TIMEZONE_ISO_LOCAL_FIELD: +#ifndef U_HIDE_INTERNAL_API + case UDAT_RELATED_YEAR_FIELD: +#endif +#ifndef U_HIDE_DRAFT_API + case UDAT_AM_PM_MIDNIGHT_NOON_FIELD: + case UDAT_FLEXIBLE_DAY_PERIOD_FIELD: +#endif +#ifndef U_HIDE_INTERNAL_API + case UDAT_TIME_SEPARATOR_FIELD: +#endif + // These fields are all unsupported. + return nullptr; + + case UDAT_FIELD_COUNT: + MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " + "iterator!"); + } + + MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " + "by iterator"); + return nullptr; +} + +static bool +intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) +{ + if (!IsFinite(x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); + return false; + } + + Vector chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + + UErrorCode status = U_ZERO_ERROR; + UFieldPositionIterator* fpositer = ufieldpositer_open(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(fpositer); + + RootedString overallResult(cx); + overallResult = CallICU(cx, [df, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return udat_formatForFields(df, x, chars, size, fpositer, status); + }); + if (!overallResult) + return false; + + RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); + if (!partsArray) + return false; + if (overallResult->length() == 0) { + // An empty string contains no parts, so avoid extra work below. + result.setObject(*partsArray); + return true; + } + + size_t lastEndIndex = 0; + + uint32_t partIndex = 0; + RootedObject singlePart(cx); + RootedValue partType(cx); + RootedValue val(cx); + + auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) { + singlePart = NewBuiltinClassInstance(cx); + if (!singlePart) + return false; + + partType = StringValue(cx->names().*type); + if (!DefineProperty(cx, singlePart, cx->names().type, partType)) + return false; + + JSLinearString* partSubstr = + NewDependentString(cx, overallResult, beginIndex, endIndex - beginIndex); + if (!partSubstr) + return false; + + val = StringValue(partSubstr); + if (!DefineProperty(cx, singlePart, cx->names().value, val)) + return false; + + val = ObjectValue(*singlePart); + if (!DefineElement(cx, partsArray, partIndex, val)) + return false; + + lastEndIndex = endIndex; + partIndex++; + return true; + }; + + int32_t fieldInt, beginIndexInt, endIndexInt; + while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { + MOZ_ASSERT(beginIndexInt >= 0); + MOZ_ASSERT(endIndexInt >= 0); + MOZ_ASSERT(beginIndexInt <= endIndexInt, + "field iterator returning invalid range"); + + size_t beginIndex(beginIndexInt); + size_t endIndex(endIndexInt); + + // Technically this isn't guaranteed. But it appears true in pratice, + // and http://bugs.icu-project.org/trac/ticket/12024 is expected to + // correct the documentation lapse. + MOZ_ASSERT(lastEndIndex <= beginIndex, + "field iteration didn't return fields in order start to " + "finish as expected"); + + if (FieldType type = GetFieldTypeForFormatField(static_cast(fieldInt))) { + if (lastEndIndex < beginIndex) { + if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) + return false; + } + + if (!AppendPart(type, beginIndex, endIndex)) + return false; + } + } + + // Append any final literal. + if (lastEndIndex < overallResult->length()) { + if (!AppendPart(&JSAtomState::literal, lastEndIndex, overallResult->length())) + return false; + } + + result.setObject(*partsArray); + return true; +} + +bool +js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isNumber()); + MOZ_ASSERT(args[2].isBoolean()); + + RootedObject dateTimeFormat(cx, &args[0].toObject()); + + // Obtain a UDateFormat object, cached if possible. + bool isDateTimeFormatInstance = dateTimeFormat->getClass() == &DateTimeFormatObject::class_; + UDateFormat* df; + if (isDateTimeFormatInstance) { + void* priv = + dateTimeFormat->as().getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT).toPrivate(); + df = static_cast(priv); + if (!df) { + df = NewUDateFormat(cx, dateTimeFormat); + if (!df) + return false; + dateTimeFormat->as().setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(df)); + } + } else { + // There's no good place to cache the ICU date-time format for an object + // that has been initialized as a DateTimeFormat but is not a + // DateTimeFormat instance. One possibility might be to add a + // DateTimeFormat instance as an internal property to each such object. + df = NewUDateFormat(cx, dateTimeFormat); + if (!df) + return false; + } + + // Use the UDateFormat to actually format the time stamp. + RootedValue result(cx); + bool success = args[2].toBoolean() + ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), &result) + : intl_FormatDateTime(cx, df, args[1].toNumber(), &result); + + if (!isDateTimeFormatInstance) + udat_close(df); + if (!success) + return false; + args.rval().set(result); + return true; +} + diff --git a/js/src/builtin/intl/DateTimeFormat.h b/js/src/builtin/intl/DateTimeFormat.h new file mode 100644 index 0000000000..9bdddc58f2 --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.h @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_intl_DateTimeFormat_h +#define builtin_intl_DateTimeFormat_h + +#include "mozilla/Attributes.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "js/RootingAPI.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; +class GlobalObject; + +/******************** DateTimeFormat ********************/ + +class DateTimeFormatObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UDATE_FORMAT_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals object slot"); + private: + static const ClassOps classOps_; + + static void finalize(FreeOp* fop, JSObject* obj); +}; + +extern JSObject* + +CreateDateTimeFormatPrototype(JSContext* cx, JS::Handle Intl, + JS::Handle global); + +/** + * Returns a new instance of the standard built-in DateTimeFormat constructor. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: dateTimeFormat = intl_DateTimeFormat(locales, options) + */ +extern MOZ_MUST_USE bool +intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an object indicating the supported locales for date and time + * formatting by having a true-valued property for each such locale with the + * canonicalized language tag as the property name. The object has no + * prototype. + * + * Usage: availableLocales = intl_DateTimeFormat_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an array with the calendar type identifiers per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * supported calendars for the given locale. The default calendar is + * element 0. + * + * Usage: calendars = intl_availableCalendars(locale) + */ +extern MOZ_MUST_USE bool +intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp); + +/** + * 6.4.1 IsValidTimeZoneName ( timeZone ) + * + * Verifies that the given string is a valid time zone name. If it is a valid + * time zone name, its IANA time zone name is returned. Otherwise returns null. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + * + * Usage: ianaTimeZone = intl_IsValidTimeZoneName(timeZone) + */ +extern MOZ_MUST_USE bool +intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return the canonicalized time zone name. Canonicalization resolves link + * names to their target time zones. + * + * Usage: ianaTimeZone = intl_canonicalizeTimeZone(timeZone) + */ +extern MOZ_MUST_USE bool +intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return the default time zone name. The time zone name is not canonicalized. + * + * Usage: icuDefaultTimeZone = intl_defaultTimeZone() + */ +extern MOZ_MUST_USE bool +intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return the raw offset from GMT in milliseconds for the default time zone. + * + * Usage: defaultTimeZoneOffset = intl_defaultTimeZoneOffset() + */ +extern MOZ_MUST_USE bool +intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return a pattern in the date-time format pattern language of Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * best-fit date-time format pattern corresponding to skeleton for the + * given locale. + * + * Usage: pattern = intl_patternForSkeleton(locale, skeleton) + */ +extern MOZ_MUST_USE bool +intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns a String value representing x (which must be a Number value) + * according to the effective locale and the formatting options of the + * given DateTimeFormat. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + * + * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x) + */ +extern MOZ_MUST_USE bool +intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); + + +} // namespace js + +#endif /* builtin_intl_DateTimeFormat_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index 0b80c31802..54b7423730 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -117,6 +117,7 @@ UNIFIED_SOURCES += [ 'builtin/Intl.cpp', 'builtin/intl/Collator.cpp', 'builtin/intl/CommonFunctions.cpp', + 'builtin/intl/DateTimeFormat.cpp', 'builtin/intl/NumberFormat.cpp', 'builtin/intl/SharedIntlData.cpp', 'builtin/MapObject.cpp', diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 6e7da8f053..a8a5e5abff 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -24,6 +24,7 @@ #include "builtin/Intl.h" #include "builtin/intl/Collator.h" +#include "builtin/intl/DateTimeFormat.h" #include "builtin/intl/NumberFormat.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h"