/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Implementation of the Intl.RelativeTimeFormat proposal. */
#include "builtin/intl/RelativeTimeFormat.h"
#include "mozilla/Assertions.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/intl/RelativeTimeFormat.h"
#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/FormatBuffer.h"
#include "builtin/intl/LanguageTag.h"
#include "gc/GCContext.h"
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
#include "js/Printer.h"
#include "js/PropertySpec.h"
#include "vm/GlobalObject.h"
#include "vm/JSContext.h"
#include "vm/PlainObject.h" // js::PlainObject
#include "vm/StringType.h"
#include "vm/NativeObject-inl.h"
using namespace js;
/**************** RelativeTimeFormat *****************/
const JSClassOps RelativeTimeFormatObject::classOps_ = {
nullptr,
// addProperty
nullptr,
// delProperty
nullptr,
// enumerate
nullptr,
// newEnumerate
nullptr,
// resolve
nullptr,
// mayResolve
RelativeTimeFormatObject::finalize,
// finalize
nullptr,
// call
nullptr,
// construct
nullptr,
// trace
};
const JSClass RelativeTimeFormatObject::class_ = {
"Intl.RelativeTimeFormat",
JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) |
JSCLASS_HAS_CACHED_PROTO(JSProto_RelativeTimeFormat) |
JSCLASS_FOREGROUND_FINALIZE,
&RelativeTimeFormatObject::classOps_,
&RelativeTimeFormatObject::classSpec_,
};
const JSClass& RelativeTimeFormatObject::protoClass_ = PlainObject::class_;
static bool relativeTimeFormat_toSource(JSContext* cx,
unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setString(cx->names().RelativeTimeFormat);
return true;
}
static const JSFunctionSpec relativeTimeFormat_static_methods[] = {
JS_SELF_HOSTED_FN(
"supportedLocalesOf",
"Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0),
JS_FS_END,
};
static const JSFunctionSpec relativeTimeFormat_methods[] = {
JS_SELF_HOSTED_FN(
"resolvedOptions",
"Intl_RelativeTimeFormat_resolvedOptions", 0, 0),
JS_SELF_HOSTED_FN(
"format",
"Intl_RelativeTimeFormat_format", 2, 0),
JS_SELF_HOSTED_FN(
"formatToParts",
"Intl_RelativeTimeFormat_formatToParts",
2, 0),
JS_FN(
"toSource", relativeTimeFormat_toSource, 0, 0),
JS_FS_END,
};
static const JSPropertySpec relativeTimeFormat_properties[] = {
JS_STRING_SYM_PS(toStringTag,
"Intl.RelativeTimeFormat", JSPROP_READONLY),
JS_PS_END,
};
static bool RelativeTimeFormat(JSContext* cx,
unsigned argc, Value* vp);
const ClassSpec RelativeTimeFormatObject::classSpec_ = {
GenericCreateConstructor<RelativeTimeFormat, 0, gc::AllocKind::FUNCTION>,
GenericCreatePrototype<RelativeTimeFormatObject>,
relativeTimeFormat_static_methods,
nullptr,
relativeTimeFormat_methods,
relativeTimeFormat_properties,
nullptr,
ClassSpec::DontDefineConstructor,
};
/**
* RelativeTimeFormat constructor.
* Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1
*/
static bool RelativeTimeFormat(JSContext* cx,
unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Step 1.
if (!ThrowIfNotConstructing(cx, args,
"Intl.RelativeTimeFormat")) {
return false;
}
// Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
RootedObject proto(cx);
if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_RelativeTimeFormat,
&proto)) {
return false;
}
Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx);
relativeTimeFormat =
NewObjectWithClassProto<RelativeTimeFormatObject>(cx, proto);
if (!relativeTimeFormat) {
return false;
}
HandleValue locales = args.get(0);
HandleValue options = args.get(1);
// Step 3.
if (!intl::InitializeObject(cx, relativeTimeFormat,
cx->names().InitializeRelativeTimeFormat, locales,
options)) {
return false;
}
args.rval().setObject(*relativeTimeFormat);
return true;
}
void js::RelativeTimeFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) {
MOZ_ASSERT(gcx->onMainThread());
if (mozilla::intl::RelativeTimeFormat* rtf =
obj->as<RelativeTimeFormatObject>().getRelativeTimeFormatter()) {
intl::RemoveICUCellMemory(gcx, obj,
RelativeTimeFormatObject::EstimatedMemoryUse);
// This was allocated using `new` in mozilla::intl::RelativeTimeFormat,
// so we delete here.
delete rtf;
}
}
/**
* Returns a new URelativeDateTimeFormatter with the locale and options of the
* given RelativeTimeFormatObject.
*/
static mozilla::intl::RelativeTimeFormat* NewRelativeTimeFormatter(
JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) {
RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat));
if (!internals) {
return nullptr;
}
RootedValue value(cx);
if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
return nullptr;
}
// ICU expects numberingSystem as a Unicode locale extensions on locale.
mozilla::intl::Locale tag;
{
Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx));
if (!locale) {
return nullptr;
}
if (!intl::ParseLocale(cx, locale, tag)) {
return nullptr;
}
}
JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
if (!GetProperty(cx, internals, internals, cx->names().numberingSystem,
&value)) {
return nullptr;
}
{
JSLinearString* numberingSystem = value.toString()->ensureLinear(cx);
if (!numberingSystem) {
return nullptr;
}
if (!keywords.emplaceBack(
"nu", numberingSystem)) {
return nullptr;
}
}
// |ApplyUnicodeExtensionToTag| applies the new keywords to the front of the
// Unicode extension subtag. We're then relying on ICU to follow RFC 6067,
// which states that any trailing keywords using the same key should be
// ignored.
if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
return nullptr;
}
intl::FormatBuffer<
char> buffer(cx);
if (
auto result = tag.ToString(buffer); result.isErr()) {
intl::ReportInternalError(cx, result.unwrapErr());
return nullptr;
}
UniqueChars locale = buffer.extractStringZ();
if (!locale) {
return nullptr;
}
if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
return nullptr;
}
using RelativeTimeFormatOptions = mozilla::intl::RelativeTimeFormatOptions;
RelativeTimeFormatOptions options;
{
JSLinearString* style = value.toString()->ensureLinear(cx);
if (!style) {
return nullptr;
}
if (StringEqualsLiteral(style,
"short")) {
options.style = RelativeTimeFormatOptions::Style::
Short;
}
else if (StringEqualsLiteral(style,
"narrow")) {
options.style = RelativeTimeFormatOptions::Style::Narrow;
}
else {
MOZ_ASSERT(StringEqualsLiteral(style,
"long"));
options.style = RelativeTimeFormatOptions::Style::
Long;
}
}
if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) {
return nullptr;
}
{
JSLinearString* numeric = value.toString()->ensureLinear(cx);
if (!numeric) {
return nullptr;
}
if (StringEqualsLiteral(numeric,
"auto")) {
options.numeric = RelativeTimeFormatOptions::Numeric::
Auto;
}
else {
MOZ_ASSERT(StringEqualsLiteral(numeric,
"always"));
options.numeric = RelativeTimeFormatOptions::Numeric::Always;
}
}
using RelativeTimeFormat = mozilla::intl::RelativeTimeFormat;
mozilla::Result<mozilla::UniquePtr<RelativeTimeFormat>,
mozilla::intl::ICUError>
result = RelativeTimeFormat::TryCreate(locale.get(), options);
if (result.isOk()) {
return result.unwrap().release();
}
intl::ReportInternalError(cx, result.unwrapErr());
return nullptr;
}
static mozilla::intl::RelativeTimeFormat* GetOrCreateRelativeTimeFormat(
JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) {
// Obtain a cached RelativeDateTimeFormatter object.
mozilla::intl::RelativeTimeFormat* rtf =
relativeTimeFormat->getRelativeTimeFormatter();
if (rtf) {
return rtf;
}
rtf = NewRelativeTimeFormatter(cx, relativeTimeFormat);
if (!rtf) {
return nullptr;
}
relativeTimeFormat->setRelativeTimeFormatter(rtf);
intl::AddICUCellMemory(relativeTimeFormat,
RelativeTimeFormatObject::EstimatedMemoryUse);
return rtf;
}
bool js::intl_FormatRelativeTime(JSContext* cx,
unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
MOZ_ASSERT(args.length() == 4);
MOZ_ASSERT(args[0].isObject());
MOZ_ASSERT(args[1].isNumber());
MOZ_ASSERT(args[2].isString());
MOZ_ASSERT(args[3].isBoolean());
Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx);
relativeTimeFormat = &args[0].toObject().as<RelativeTimeFormatObject>();
bool formatToParts = args[3].toBoolean();
// PartitionRelativeTimePattern, step 4.
double t = args[1].toNumber();
if (!std::isfinite(t)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DATE_NOT_FINITE,
"RelativeTimeFormat",
formatToParts ?
"formatToParts" :
"format");
return false;
}
mozilla::intl::RelativeTimeFormat* rtf =
GetOrCreateRelativeTimeFormat(cx, relativeTimeFormat);
if (!rtf) {
return false;
}
intl::FieldType jsUnitType;
using FormatUnit = mozilla::intl::RelativeTimeFormat::FormatUnit;
FormatUnit relTimeUnit;
{
JSLinearString* unit = args[2].toString()->ensureLinear(cx);
if (!unit) {
return false;
}
// PartitionRelativeTimePattern, step 5.
if (StringEqualsLiteral(unit,
"second") ||
StringEqualsLiteral(unit,
"seconds")) {
jsUnitType = &JSAtomState::second;
relTimeUnit = FormatUnit::Second;
}
else if (StringEqualsLiteral(unit,
"minute") ||
StringEqualsLiteral(unit,
"minutes")) {
jsUnitType = &JSAtomState::minute;
relTimeUnit = FormatUnit::Minute;
}
else if (StringEqualsLiteral(unit,
"hour") ||
StringEqualsLiteral(unit,
"hours")) {
jsUnitType = &JSAtomState::hour;
relTimeUnit = FormatUnit::Hour;
}
else if (StringEqualsLiteral(unit,
"day") ||
StringEqualsLiteral(unit,
"days")) {
jsUnitType = &JSAtomState::day;
relTimeUnit = FormatUnit::Day;
}
else if (StringEqualsLiteral(unit,
"week") ||
StringEqualsLiteral(unit,
"weeks")) {
jsUnitType = &JSAtomState::week;
relTimeUnit = FormatUnit::Week;
}
else if (StringEqualsLiteral(unit,
"month") ||
StringEqualsLiteral(unit,
"months")) {
jsUnitType = &JSAtomState::month;
relTimeUnit = FormatUnit::Month;
}
else if (StringEqualsLiteral(unit,
"quarter") ||
StringEqualsLiteral(unit,
"quarters")) {
jsUnitType = &JSAtomState::quarter;
relTimeUnit = FormatUnit::Quarter;
}
else if (StringEqualsLiteral(unit,
"year") ||
StringEqualsLiteral(unit,
"years")) {
jsUnitType = &JSAtomState::year;
relTimeUnit = FormatUnit::Year;
}
else {
if (
auto unitChars = QuoteString(cx, unit,
'"')) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_INVALID_OPTION_VALUE,
"unit",
unitChars.get());
}
return false;
}
}
using ICUError = mozilla::intl::ICUError;
if (formatToParts) {
mozilla::intl::NumberPartVector parts;
mozilla::Result<mozilla::Span<
const char16_t>, ICUError> result =
rtf->formatToParts(t, relTimeUnit, parts);
if (result.isErr()) {
intl::ReportInternalError(cx, result.unwrapErr());
return false;
}
RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap()));
if (!str) {
return false;
}
return js::intl::FormattedRelativeTimeToParts(cx, str, parts, jsUnitType,
args.rval());
}
js::intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
mozilla::Result<Ok, ICUError> result = rtf->format(t, relTimeUnit, buffer);
if (result.isErr()) {
intl::ReportInternalError(cx, result.unwrapErr());
return false;
}
JSString* str = buffer.toString(cx);
if (!str) {
return false;
}
args.rval().setString(str);
return true;
}