1 // Copyright 2018 the V8 project authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #ifndef V8_INTL_SUPPORT
6 #error Internationalization is expected to be enabled.
7 #endif  // V8_INTL_SUPPORT
8 
9 #include "src/objects/js-plural-rules.h"
10 
11 #include "src/isolate-inl.h"
12 #include "src/objects/intl-objects.h"
13 #include "src/objects/js-plural-rules-inl.h"
14 #include "unicode/decimfmt.h"
15 #include "unicode/locid.h"
16 #include "unicode/numfmt.h"
17 #include "unicode/plurrule.h"
18 #include "unicode/strenum.h"
19 
20 namespace v8 {
21 namespace internal {
22 
23 namespace {
24 
CreateICUPluralRules(Isolate * isolate,const icu::Locale & icu_locale,const char * type_string,std::unique_ptr<icu::PluralRules> * pl,std::unique_ptr<icu::DecimalFormat> * nf)25 bool CreateICUPluralRules(Isolate* isolate, const icu::Locale& icu_locale,
26                           const char* type_string,
27                           std::unique_ptr<icu::PluralRules>* pl,
28                           std::unique_ptr<icu::DecimalFormat>* nf) {
29   // Make formatter from options. Numbering system is added
30   // to the locale as Unicode extension (if it was specified at all).
31   UErrorCode status = U_ZERO_ERROR;
32 
33   UPluralType type = UPLURAL_TYPE_CARDINAL;
34   if (strcmp(type_string, "ordinal") == 0) {
35     type = UPLURAL_TYPE_ORDINAL;
36   } else {
37     CHECK_EQ(0, strcmp(type_string, "cardinal"));
38   }
39 
40   std::unique_ptr<icu::PluralRules> plural_rules(
41       icu::PluralRules::forLocale(icu_locale, type, status));
42   if (U_FAILURE(status)) {
43     return false;
44   }
45   CHECK_NOT_NULL(plural_rules.get());
46 
47   std::unique_ptr<icu::DecimalFormat> number_format(
48       static_cast<icu::DecimalFormat*>(
49           icu::NumberFormat::createInstance(icu_locale, UNUM_DECIMAL, status)));
50   if (U_FAILURE(status)) {
51     return false;
52   }
53   CHECK_NOT_NULL(number_format.get());
54 
55   *pl = std::move(plural_rules);
56   *nf = std::move(number_format);
57 
58   return true;
59 }
60 
InitializeICUPluralRules(Isolate * isolate,Handle<String> locale,const char * type,std::unique_ptr<icu::PluralRules> * plural_rules,std::unique_ptr<icu::DecimalFormat> * number_format)61 void InitializeICUPluralRules(
62     Isolate* isolate, Handle<String> locale, const char* type,
63     std::unique_ptr<icu::PluralRules>* plural_rules,
64     std::unique_ptr<icu::DecimalFormat>* number_format) {
65   icu::Locale icu_locale = Intl::CreateICULocale(isolate, locale);
66   DCHECK(!icu_locale.isBogus());
67 
68   bool success = CreateICUPluralRules(isolate, icu_locale, type, plural_rules,
69                                       number_format);
70   if (!success) {
71     // Remove extensions and try again.
72     icu::Locale no_extension_locale(icu_locale.getBaseName());
73     success = CreateICUPluralRules(isolate, no_extension_locale, type,
74                                    plural_rules, number_format);
75 
76     if (!success) {
77       FATAL("Failed to create ICU PluralRules, are ICU data files missing?");
78     }
79   }
80 
81   CHECK_NOT_NULL((*plural_rules).get());
82   CHECK_NOT_NULL((*number_format).get());
83 }
84 
85 }  // namespace
86 
87 // static
InitializePluralRules(Isolate * isolate,Handle<JSPluralRules> plural_rules,Handle<Object> locales,Handle<Object> options_obj)88 MaybeHandle<JSPluralRules> JSPluralRules::InitializePluralRules(
89     Isolate* isolate, Handle<JSPluralRules> plural_rules,
90     Handle<Object> locales, Handle<Object> options_obj) {
91   // 1. Let requestedLocales be ? CanonicalizeLocaleList(locales).
92   // TODO(jkummerow): Port ResolveLocale, then use the C++ version of
93   // CanonicalizeLocaleList here.
94   Handle<JSObject> requested_locales;
95   ASSIGN_RETURN_ON_EXCEPTION(isolate, requested_locales,
96                              Intl::CanonicalizeLocaleListJS(isolate, locales),
97                              JSPluralRules);
98 
99   // 2. If options is undefined, then
100   if (options_obj->IsUndefined(isolate)) {
101     // 2. a. Let options be ObjectCreate(null).
102     options_obj = isolate->factory()->NewJSObjectWithNullProto();
103   } else {
104     // 3. Else
105     // 3. a. Let options be ? ToObject(options).
106     ASSIGN_RETURN_ON_EXCEPTION(
107         isolate, options_obj,
108         Object::ToObject(isolate, options_obj, "Intl.PluralRules"),
109         JSPluralRules);
110   }
111 
112   // At this point, options_obj can either be a JSObject or a JSProxy only.
113   Handle<JSReceiver> options = Handle<JSReceiver>::cast(options_obj);
114 
115   // TODO(gsathya): This is currently done as part of the
116   // Intl::ResolveLocale call below. Fix this once resolveLocale is
117   // changed to not do the lookup.
118   //
119   // 5. Let matcher be ? GetOption(options, "localeMatcher", "string",
120   // « "lookup", "best fit" », "best fit").
121   // 6. Set opt.[[localeMatcher]] to matcher.
122 
123   // 7. Let t be ? GetOption(options, "type", "string", « "cardinal",
124   // "ordinal" », "cardinal").
125   std::vector<const char*> values = {"cardinal", "ordinal"};
126   std::unique_ptr<char[]> type_str = nullptr;
127   const char* type_cstr = "cardinal";
128   Maybe<bool> found = Intl::GetStringOption(isolate, options, "type", values,
129                                             "Intl.PluralRules", &type_str);
130   MAYBE_RETURN(found, MaybeHandle<JSPluralRules>());
131   if (found.FromJust()) {
132     type_cstr = type_str.get();
133   }
134 
135   // 8. Set pluralRules.[[Type]] to t.
136   Handle<String> type =
137       isolate->factory()->NewStringFromAsciiChecked(type_cstr);
138   plural_rules->set_type(*type);
139 
140   // Note: The spec says we should do ResolveLocale after performing
141   // SetNumberFormatDigitOptions but we need the locale to create all
142   // the ICU data structures.
143   //
144   // This isn't observable so we aren't violating the spec.
145 
146   // 11. Let r be ResolveLocale(%PluralRules%.[[AvailableLocales]],
147   // requestedLocales, opt, %PluralRules%.[[RelevantExtensionKeys]],
148   // localeData).
149   Handle<JSObject> r;
150   ASSIGN_RETURN_ON_EXCEPTION(
151       isolate, r,
152       Intl::ResolveLocale(isolate, "pluralrules", requested_locales, options),
153       JSPluralRules);
154 
155   Handle<String> locale_str = isolate->factory()->locale_string();
156   Handle<Object> locale_obj = JSObject::GetDataProperty(r, locale_str);
157 
158   // The locale has to be a string. Either a user provided
159   // canonicalized string or the default locale.
160   CHECK(locale_obj->IsString());
161   Handle<String> locale = Handle<String>::cast(locale_obj);
162 
163   // 12. Set pluralRules.[[Locale]] to the value of r.[[locale]].
164   plural_rules->set_locale(*locale);
165 
166   std::unique_ptr<icu::PluralRules> icu_plural_rules;
167   std::unique_ptr<icu::DecimalFormat> icu_decimal_format;
168   InitializeICUPluralRules(isolate, locale, type_cstr, &icu_plural_rules,
169                            &icu_decimal_format);
170   CHECK_NOT_NULL(icu_plural_rules.get());
171   CHECK_NOT_NULL(icu_decimal_format.get());
172 
173   // 9. Perform ? SetNumberFormatDigitOptions(pluralRules, options, 0, 3).
174   Maybe<bool> done = Intl::SetNumberFormatDigitOptions(
175       isolate, icu_decimal_format.get(), options, 0, 3);
176   MAYBE_RETURN(done, MaybeHandle<JSPluralRules>());
177 
178   Handle<Managed<icu::PluralRules>> managed_plural_rules =
179       Managed<icu::PluralRules>::FromUniquePtr(isolate, 0,
180                                                std::move(icu_plural_rules));
181   plural_rules->set_icu_plural_rules(*managed_plural_rules);
182 
183   Handle<Managed<icu::DecimalFormat>> managed_decimal_format =
184       Managed<icu::DecimalFormat>::FromUniquePtr(isolate, 0,
185                                                  std::move(icu_decimal_format));
186   plural_rules->set_icu_decimal_format(*managed_decimal_format);
187 
188   // 13. Return pluralRules.
189   return plural_rules;
190 }
191 
ResolvePlural(Isolate * isolate,Handle<JSPluralRules> plural_rules,Handle<Object> number)192 MaybeHandle<String> JSPluralRules::ResolvePlural(
193     Isolate* isolate, Handle<JSPluralRules> plural_rules,
194     Handle<Object> number) {
195   icu::PluralRules* icu_plural_rules = plural_rules->icu_plural_rules()->raw();
196   CHECK_NOT_NULL(icu_plural_rules);
197 
198   icu::DecimalFormat* icu_decimal_format =
199       plural_rules->icu_decimal_format()->raw();
200   CHECK_NOT_NULL(icu_decimal_format);
201 
202   // Currently, PluralRules doesn't implement all the options for rounding that
203   // the Intl spec provides; format and parse the number to round to the
204   // appropriate amount, then apply PluralRules.
205   //
206   // TODO(littledan): If a future ICU version supports an extended API to avoid
207   // this step, then switch to that API. Bug thread:
208   // http://bugs.icu-project.org/trac/ticket/12763
209   icu::UnicodeString rounded_string;
210   icu_decimal_format->format(number->Number(), rounded_string);
211 
212   icu::Formattable formattable;
213   UErrorCode status = U_ZERO_ERROR;
214   icu_decimal_format->parse(rounded_string, formattable, status);
215   CHECK(U_SUCCESS(status));
216 
217   double rounded = formattable.getDouble(status);
218   CHECK(U_SUCCESS(status));
219 
220   icu::UnicodeString result = icu_plural_rules->select(rounded);
221   return isolate->factory()->NewStringFromTwoByte(Vector<const uint16_t>(
222       reinterpret_cast<const uint16_t*>(result.getBuffer()), result.length()));
223 }
224 
225 namespace {
226 
CreateDataPropertyForOptions(Isolate * isolate,Handle<JSObject> options,Handle<Object> value,const char * key)227 void CreateDataPropertyForOptions(Isolate* isolate, Handle<JSObject> options,
228                                   Handle<Object> value, const char* key) {
229   Handle<String> key_str = isolate->factory()->NewStringFromAsciiChecked(key);
230 
231   // This is a brand new JSObject that shouldn't already have the same
232   // key so this shouldn't fail.
233   CHECK(JSReceiver::CreateDataProperty(isolate, options, key_str, value,
234                                        kDontThrow)
235             .FromJust());
236 }
237 
CreateDataPropertyForOptions(Isolate * isolate,Handle<JSObject> options,int value,const char * key)238 void CreateDataPropertyForOptions(Isolate* isolate, Handle<JSObject> options,
239                                   int value, const char* key) {
240   Handle<Smi> value_smi(Smi::FromInt(value), isolate);
241   CreateDataPropertyForOptions(isolate, options, value_smi, key);
242 }
243 
244 }  // namespace
245 
ResolvedOptions(Isolate * isolate,Handle<JSPluralRules> plural_rules)246 Handle<JSObject> JSPluralRules::ResolvedOptions(
247     Isolate* isolate, Handle<JSPluralRules> plural_rules) {
248   Handle<JSObject> options =
249       isolate->factory()->NewJSObject(isolate->object_function());
250 
251   Handle<String> locale_value(plural_rules->locale(), isolate);
252   CreateDataPropertyForOptions(isolate, options, locale_value, "locale");
253 
254   Handle<String> type_value(plural_rules->type(), isolate);
255   CreateDataPropertyForOptions(isolate, options, type_value, "type");
256 
257   icu::DecimalFormat* icu_decimal_format =
258       plural_rules->icu_decimal_format()->raw();
259   CHECK_NOT_NULL(icu_decimal_format);
260 
261   // This is a safe upcast as icu::DecimalFormat inherits from
262   // icu::NumberFormat.
263   icu::NumberFormat* icu_number_format =
264       static_cast<icu::NumberFormat*>(icu_decimal_format);
265 
266   int min_int_digits = icu_number_format->getMinimumIntegerDigits();
267   CreateDataPropertyForOptions(isolate, options, min_int_digits,
268                                "minimumIntegerDigits");
269 
270   int min_fraction_digits = icu_number_format->getMinimumFractionDigits();
271   CreateDataPropertyForOptions(isolate, options, min_fraction_digits,
272                                "minimumFractionDigits");
273 
274   int max_fraction_digits = icu_number_format->getMaximumFractionDigits();
275   CreateDataPropertyForOptions(isolate, options, max_fraction_digits,
276                                "maximumFractionDigits");
277 
278   if (icu_decimal_format->areSignificantDigitsUsed()) {
279     int min_significant_digits =
280         icu_decimal_format->getMinimumSignificantDigits();
281     CreateDataPropertyForOptions(isolate, options, min_significant_digits,
282                                  "minimumSignificantDigits");
283 
284     int max_significant_digits =
285         icu_decimal_format->getMaximumSignificantDigits();
286     CreateDataPropertyForOptions(isolate, options, max_significant_digits,
287                                  "maximumSignificantDigits");
288   }
289 
290   // 6. Let pluralCategories be a List of Strings representing the
291   // possible results of PluralRuleSelect for the selected locale pr.
292   icu::PluralRules* icu_plural_rules = plural_rules->icu_plural_rules()->raw();
293   CHECK_NOT_NULL(icu_plural_rules);
294 
295   UErrorCode status = U_ZERO_ERROR;
296   std::unique_ptr<icu::StringEnumeration> categories(
297       icu_plural_rules->getKeywords(status));
298   CHECK(U_SUCCESS(status));
299   int32_t count = categories->count(status);
300   CHECK(U_SUCCESS(status));
301 
302   Handle<FixedArray> plural_categories =
303       isolate->factory()->NewFixedArray(count);
304   for (int32_t i = 0; i < count; i++) {
305     const icu::UnicodeString* category = categories->snext(status);
306     CHECK(U_SUCCESS(status));
307     if (category == nullptr) break;
308 
309     std::string keyword;
310     Handle<String> value = isolate->factory()->NewStringFromAsciiChecked(
311         category->toUTF8String(keyword).data());
312     plural_categories->set(i, *value);
313   }
314 
315   // 7. Perform ! CreateDataProperty(options, "pluralCategories",
316   // CreateArrayFromList(pluralCategories)).
317   Handle<JSArray> plural_categories_value =
318       isolate->factory()->NewJSArrayWithElements(plural_categories);
319   CreateDataPropertyForOptions(isolate, options, plural_categories_value,
320                                "pluralCategories");
321 
322   return options;
323 }
324 
325 }  // namespace internal
326 }  // namespace v8
327