diff --git a/src/extensions/experimental/experimental.gyp b/src/extensions/experimental/experimental.gyp index ac17be3447..24fb683160 100644 --- a/src/extensions/experimental/experimental.gyp +++ b/src/extensions/experimental/experimental.gyp @@ -52,6 +52,8 @@ 'i18n-utils.h', 'language-matcher.cc', 'language-matcher.h', + 'number-format.cc', + 'number-format.h', '<(SHARED_INTERMEDIATE_DIR)/i18n-js.cc', ], 'include_dirs': [ @@ -76,7 +78,7 @@ 'type': 'none', 'toolsets': ['host'], 'variables': { - 'library_files': [ + 'js_files': [ 'i18n.js' ], }, @@ -85,7 +87,7 @@ 'action_name': 'js2c_i18n', 'inputs': [ 'i18n-js2c.py', - '<@(library_files)', + '<@(js_files)', ], 'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/i18n-js.cc', @@ -94,7 +96,7 @@ 'python', 'i18n-js2c.py', '<@(_outputs)', - '<@(library_files)' + '<@(js_files)' ], }, ], diff --git a/src/extensions/experimental/i18n-extension.cc b/src/extensions/experimental/i18n-extension.cc index d5c9a5058f..c5afcf0bfc 100644 --- a/src/extensions/experimental/i18n-extension.cc +++ b/src/extensions/experimental/i18n-extension.cc @@ -32,6 +32,7 @@ #include "src/extensions/experimental/datetime-format.h" #include "src/extensions/experimental/i18n-locale.h" #include "src/extensions/experimental/i18n-natives.h" +#include "src/extensions/experimental/number-format.h" namespace v8 { namespace internal { @@ -52,6 +53,8 @@ v8::Handle I18NExtension::GetNativeFunction( return v8::FunctionTemplate::New(Collator::JSCollator); } else if (name->Equals(v8::String::New("NativeJSDateTimeFormat"))) { return v8::FunctionTemplate::New(DateTimeFormat::JSDateTimeFormat); + } else if (name->Equals(v8::String::New("NativeJSNumberFormat"))) { + return v8::FunctionTemplate::New(NumberFormat::JSNumberFormat); } return v8::Handle(); diff --git a/src/extensions/experimental/i18n-utils.cc b/src/extensions/experimental/i18n-utils.cc index 641ee80fec..dc2be1a210 100644 --- a/src/extensions/experimental/i18n-utils.cc +++ b/src/extensions/experimental/i18n-utils.cc @@ -65,4 +65,23 @@ bool I18NUtils::ExtractStringSetting(const v8::Handle& settings, return false; } +// static +void I18NUtils::AsciiToUChar(const char* source, + int32_t source_length, + UChar* target, + int32_t target_length) { + int32_t length = + source_length < target_length ? source_length : target_length; + + if (length <= 0) { + return; + } + + for (int32_t i = 0; i < length - 1; ++i) { + target[i] = static_cast(source[i]); + } + + target[length - 1] = 0x0u; +} + } } // namespace v8::internal diff --git a/src/extensions/experimental/i18n-utils.h b/src/extensions/experimental/i18n-utils.h index be473169de..7c31528be8 100644 --- a/src/extensions/experimental/i18n-utils.h +++ b/src/extensions/experimental/i18n-utils.h @@ -53,6 +53,13 @@ class I18NUtils { const char* setting, icu::UnicodeString* result); + // Converts ASCII array into UChar array. + // Target is always \0 terminated. + static void AsciiToUChar(const char* source, + int32_t source_length, + UChar* target, + int32_t target_length); + private: I18NUtils() {} }; diff --git a/src/extensions/experimental/i18n.js b/src/extensions/experimental/i18n.js index 72e3d5d7a6..07f02e7d22 100644 --- a/src/extensions/experimental/i18n.js +++ b/src/extensions/experimental/i18n.js @@ -222,6 +222,102 @@ v8Locale.prototype.createDateTimeFormat = function(settings) { return new v8Locale.__DateTimeFormat(this, settings); }; +/** + * NumberFormat class implements locale-aware number formatting. + * Constructor is not part of public API. + * @param {Object} locale - locale object to pass to formatter. + * @param {Object} settings - formatting flags: + * - skeleton + * - pattern + * - style - decimal, currency, percent or scientific + * - currencyCode - ISO 4217 3-letter currency code + * @private + * @constructor + */ +v8Locale.__NumberFormat = function(locale, settings) { + native function NativeJSNumberFormat(); + + settings = v8Locale.__createSettingsOrDefault(settings, {}); + + var cleanSettings = {}; + if (settings.hasOwnProperty('skeleton')) { + // Assign skeleton to cleanSettings and fix invalid currency pattern + // if present - 'ooxo' becomes 'o'. + cleanSettings['skeleton'] = + settings['skeleton'].replace(/\u00a4+[^\u00a4]+\u00a4+/g, '\u00a4'); + } else if (settings.hasOwnProperty('pattern')) { + cleanSettings['pattern'] = settings['pattern']; + } else if (settings.hasOwnProperty('style')) { + var style = settings['style']; + if (!/^decimal|currency|percent|scientific$/.test(style)) { + style = 'decimal'; + } + cleanSettings['style'] = style; + } + + // Default is to show decimal style. + if (!cleanSettings.hasOwnProperty('skeleton') && + !cleanSettings.hasOwnProperty('pattern') && + !cleanSettings.hasOwnProperty('style')) { + cleanSettings = {'style': 'decimal'}; + } + + // Add currency code if available and valid (3-letter ASCII code). + if (settings.hasOwnProperty('currencyCode') && + /^[a-zA-Z]{3}$/.test(settings['currencyCode'])) { + cleanSettings['currencyCode'] = settings['currencyCode'].toUpperCase(); + } + + locale = v8Locale.__createLocaleOrDefault(locale); + // Pass in region ID for proper currency detection. Use ZZ if region is empty. + var region = locale.options.regionID !== '' ? locale.options.regionID : 'ZZ'; + var formatter = NativeJSNumberFormat( + locale.__icuLocaleID, 'und_' + region, cleanSettings); + + // ICU doesn't always uppercase the currency code. + if (formatter.options.hasOwnProperty('currencyCode')) { + formatter.options['currencyCode'] = + formatter.options['currencyCode'].toUpperCase(); + } + + for (key in cleanSettings) { + // Don't overwrite keys that are alredy in. + if (formatter.options.hasOwnProperty(key)) continue; + + formatter.options[key] = cleanSettings[key]; + } + + /** + * Clones existing number format with possible overrides for some + * of the options. + * @param {!Object} overrideSettings - overrides for current format settings. + * @returns {Object} - new or cached NumberFormat object. + * @public + */ + formatter.derive = function(overrideSettings) { + // To remove a setting user can specify undefined as its value. We'll remove + // it from the map in that case. + for (var prop in overrideSettings) { + if (settings.hasOwnProperty(prop) && !overrideSettings[prop]) { + delete settings[prop]; + } + } + return new v8Locale.__NumberFormat( + locale, v8Locale.__createSettingsOrDefault(overrideSettings, settings)); + }; + + return formatter; +}; + +/** + * Creates new NumberFormat based on current locale. + * @param {Object} - formatting flags. See constructor. + * @returns {Object} - new or cached NumberFormat object. + */ +v8Locale.prototype.createNumberFormat = function(settings) { + return new v8Locale.__NumberFormat(this, settings); +}; + /** * Merges user settings and defaults. * Settings that are not of object type are rejected. diff --git a/src/extensions/experimental/number-format.cc b/src/extensions/experimental/number-format.cc new file mode 100644 index 0000000000..51e0b959ce --- /dev/null +++ b/src/extensions/experimental/number-format.cc @@ -0,0 +1,356 @@ +// Copyright 2011 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "src/extensions/experimental/number-format.h" + +#include + +#include "src/extensions/experimental/i18n-utils.h" +#include "unicode/dcfmtsym.h" +#include "unicode/decimfmt.h" +#include "unicode/locid.h" +#include "unicode/numfmt.h" +#include "unicode/uchar.h" +#include "unicode/ucurr.h" + +namespace v8 { +namespace internal { + +const int NumberFormat::kCurrencyCodeLength = 4; + +v8::Persistent NumberFormat::number_format_template_; + +static icu::DecimalFormat* CreateNumberFormat(v8::Handle, + v8::Handle, + v8::Handle); +static icu::DecimalFormat* CreateFormatterFromSkeleton( + const icu::Locale&, const icu::UnicodeString&, UErrorCode*); +static icu::DecimalFormatSymbols* GetFormatSymbols(const icu::Locale&); +static bool GetCurrencyCode(const icu::Locale&, + const char* const, + v8::Handle, + UChar*); +static v8::Handle ThrowUnexpectedObjectError(); + +icu::DecimalFormat* NumberFormat::UnpackNumberFormat( + v8::Handle obj) { + if (number_format_template_->HasInstance(obj)) { + return static_cast( + obj->GetPointerFromInternalField(0)); + } + + return NULL; +} + +void NumberFormat::DeleteNumberFormat(v8::Persistent object, + void* param) { + v8::Persistent persistent_object = + v8::Persistent::Cast(object); + + // First delete the hidden C++ object. + // Unpacking should never return NULL here. That would only happen if + // this method is used as the weak callback for persistent handles not + // pointing to a number formatter. + delete UnpackNumberFormat(persistent_object); + + // Then dispose of the persistent handle to JS object. + persistent_object.Dispose(); +} + +v8::Handle NumberFormat::Format(const v8::Arguments& args) { + v8::HandleScope handle_scope; + + if (args.Length() != 1 || !args[0]->IsNumber()) { + // Just return NaN on invalid input. + return v8::String::New("NaN"); + } + + icu::DecimalFormat* number_format = UnpackNumberFormat(args.Holder()); + if (!number_format) { + return ThrowUnexpectedObjectError(); + } + + // ICU will handle actual NaN value properly and return NaN string. + icu::UnicodeString result; + number_format->format(args[0]->NumberValue(), result); + + return v8::String::New( + reinterpret_cast(result.getBuffer()), result.length()); +} + +v8::Handle NumberFormat::JSNumberFormat(const v8::Arguments& args) { + v8::HandleScope handle_scope; + + // Expect locale id, region id and settings. + if (args.Length() != 3 || + !args[0]->IsString() || !args[1]->IsString() || !args[2]->IsObject()) { + return v8::ThrowException(v8::Exception::SyntaxError( + v8::String::New("Locale, region and number settings are required."))); + } + + icu::DecimalFormat* number_format = CreateNumberFormat( + args[0]->ToString(), args[1]->ToString(), args[2]->ToObject()); + + if (number_format_template_.IsEmpty()) { + v8::Local raw_template(v8::FunctionTemplate::New()); + + raw_template->SetClassName(v8::String::New("v8Locale.NumberFormat")); + + // Define internal field count on instance template. + v8::Local object_template = + raw_template->InstanceTemplate(); + + // Set aside internal field for icu number formatter. + object_template->SetInternalFieldCount(1); + + // Define all of the prototype methods on prototype template. + v8::Local proto = raw_template->PrototypeTemplate(); + proto->Set(v8::String::New("format"), + v8::FunctionTemplate::New(Format)); + + number_format_template_ = + v8::Persistent::New(raw_template); + } + + // Create an empty object wrapper. + v8::Local local_object = + number_format_template_->GetFunction()->NewInstance(); + v8::Persistent wrapper = + v8::Persistent::New(local_object); + + // Set number formatter as internal field of the resulting JS object. + wrapper->SetPointerInInternalField(0, number_format); + + // Create options key. + v8::Local options = v8::Object::New(); + + // Show what ICU decided to use for easier problem tracking. + // Keep it as v8 specific extension. + icu::UnicodeString pattern; + number_format->toPattern(pattern); + options->Set(v8::String::New("v8ResolvedPattern"), + v8::String::New(reinterpret_cast( + pattern.getBuffer()), pattern.length())); + + // Set resolved currency code in options.currency if not empty. + icu::UnicodeString currency(number_format->getCurrency()); + if (!currency.isEmpty()) { + options->Set(v8::String::New("currencyCode"), + v8::String::New(reinterpret_cast( + currency.getBuffer()), currency.length())); + } + + wrapper->Set(v8::String::New("options"), options); + + // Make object handle weak so we can delete iterator once GC kicks in. + wrapper.MakeWeak(NULL, DeleteNumberFormat); + + return wrapper; +} + +// Returns DecimalFormat. +static icu::DecimalFormat* CreateNumberFormat(v8::Handle locale, + v8::Handle region, + v8::Handle settings) { + v8::HandleScope handle_scope; + + v8::String::AsciiValue ascii_locale(locale); + icu::Locale icu_locale(*ascii_locale); + + // Make formatter from skeleton. + icu::DecimalFormat* number_format = NULL; + UErrorCode status = U_ZERO_ERROR; + icu::UnicodeString setting; + + if (I18NUtils::ExtractStringSetting(settings, "skeleton", &setting)) { + // TODO(cira): Use ICU skeleton once + // http://bugs.icu-project.org/trac/ticket/8610 is resolved. + number_format = CreateFormatterFromSkeleton(icu_locale, setting, &status); + } else if (I18NUtils::ExtractStringSetting(settings, "pattern", &setting)) { + number_format = + new icu::DecimalFormat(setting, GetFormatSymbols(icu_locale), status); + } else if (I18NUtils::ExtractStringSetting(settings, "style", &setting)) { + if (setting == UNICODE_STRING_SIMPLE("currency")) { + number_format = static_cast( + icu::NumberFormat::createCurrencyInstance(icu_locale, status)); + } else if (setting == UNICODE_STRING_SIMPLE("percent")) { + number_format = static_cast( + icu::NumberFormat::createPercentInstance(icu_locale, status)); + } else if (setting == UNICODE_STRING_SIMPLE("scientific")) { + number_format = static_cast( + icu::NumberFormat::createScientificInstance(icu_locale, status)); + } else { + // Make it decimal in any other case. + number_format = static_cast( + icu::NumberFormat::createInstance(icu_locale, status)); + } + } + + if (U_FAILURE(status)) { + delete number_format; + status = U_ZERO_ERROR; + number_format = static_cast( + icu::NumberFormat::createInstance(icu_locale, status)); + } + + // Attach appropriate currency code to the formatter. + // It affects currency formatters only. + // Region is full language identifier in form 'und_' + region id. + v8::String::AsciiValue ascii_region(region); + + UChar currency_code[NumberFormat::kCurrencyCodeLength]; + if (GetCurrencyCode(icu_locale, *ascii_region, settings, currency_code)) { + number_format->setCurrency(currency_code, status); + } + + return number_format; +} + +// Generates ICU number format pattern from given skeleton. +static icu::DecimalFormat* CreateFormatterFromSkeleton( + const icu::Locale& icu_locale, + const icu::UnicodeString& skeleton, + UErrorCode* status) { + icu::DecimalFormat skeleton_format( + skeleton, GetFormatSymbols(icu_locale), *status); + + // Find out if skeleton contains currency or percent symbol and create + // proper instance to tweak. + icu::DecimalFormat* base_format = NULL; + + // UChar representation of U+00A4 currency symbol. + const UChar currency_symbol = 0xA4u; + + int32_t index = skeleton.indexOf(currency_symbol); + if (index != -1) { + // Find how many U+00A4 are there. There is at least one. + // Case of non-consecutive U+00A4 is taken care of in i18n.js. + int32_t end_index = skeleton.lastIndexOf(currency_symbol, index); + + icu::NumberFormat::EStyles style; + switch (end_index - index) { + case 0: + style = icu::NumberFormat::kCurrencyStyle; + break; + case 1: + style = icu::NumberFormat::kIsoCurrencyStyle; + break; + default: + style = icu::NumberFormat::kPluralCurrencyStyle; + } + + base_format = static_cast( + icu::NumberFormat::createInstance(icu_locale, style, *status)); + } else if (skeleton.indexOf('%') != -1) { + base_format = static_cast( + icu::NumberFormat::createPercentInstance(icu_locale, *status)); + } else { + // TODO(cira): Handle scientific skeleton. + base_format = static_cast( + icu::NumberFormat::createInstance(icu_locale, *status)); + } + + if (U_FAILURE(*status)) { + delete base_format; + return NULL; + } + + // Copy important information from skeleton to the new formatter. + // TODO(cira): copy rounding information from skeleton? + base_format->setGroupingUsed(skeleton_format.isGroupingUsed()); + + base_format->setMinimumIntegerDigits( + skeleton_format.getMinimumIntegerDigits()); + + base_format->setMinimumFractionDigits( + skeleton_format.getMinimumFractionDigits()); + + base_format->setMaximumFractionDigits( + skeleton_format.getMaximumFractionDigits()); + + return base_format; +} + +// Gets decimal symbols for a locale. +static icu::DecimalFormatSymbols* GetFormatSymbols( + const icu::Locale& icu_locale) { + UErrorCode status = U_ZERO_ERROR; + icu::DecimalFormatSymbols* symbols = + new icu::DecimalFormatSymbols(icu_locale, status); + + if (U_FAILURE(status)) { + delete symbols; + // Use symbols from default locale. + symbols = new icu::DecimalFormatSymbols(status); + } + + return symbols; +} + +// Gets currency ISO 4217 3-letter code. +// Check currencyCode setting first, then @currency=code and in the end +// try to infer currency code from locale in the form 'und_' + region id. +// Returns false in case of error. +static bool GetCurrencyCode(const icu::Locale& icu_locale, + const char* const und_region_locale, + v8::Handle settings, + UChar* code) { + UErrorCode status = U_ZERO_ERROR; + + // If there is user specified currency code, use it. + icu::UnicodeString currency; + if (I18NUtils::ExtractStringSetting(settings, "currencyCode", ¤cy)) { + currency.extract(code, NumberFormat::kCurrencyCodeLength, status); + return true; + } + + // If ICU locale has -cu- currency code use it. + char currency_code[NumberFormat::kCurrencyCodeLength]; + int32_t length = icu_locale.getKeywordValue( + "currency", currency_code, NumberFormat::kCurrencyCodeLength, status); + if (length != 0) { + I18NUtils::AsciiToUChar(currency_code, length + 1, + code, NumberFormat::kCurrencyCodeLength); + return true; + } + + // Otherwise infer currency code from the region id. + ucurr_forLocale( + und_region_locale, code, NumberFormat::kCurrencyCodeLength, &status); + + return !!U_SUCCESS(status); +} + +// Throws a JavaScript exception. +static v8::Handle ThrowUnexpectedObjectError() { + // Returns undefined, and schedules an exception to be thrown. + return v8::ThrowException(v8::Exception::Error( + v8::String::New("NumberFormat method called on an object " + "that is not a NumberFormat."))); +} + +} } // namespace v8::internal diff --git a/src/extensions/experimental/number-format.h b/src/extensions/experimental/number-format.h new file mode 100644 index 0000000000..bcfaed6fc1 --- /dev/null +++ b/src/extensions/experimental/number-format.h @@ -0,0 +1,71 @@ +// Copyright 2011 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef V8_EXTENSIONS_EXPERIMENTAL_NUMBER_FORMAT_H_ +#define V8_EXTENSIONS_EXPERIMENTAL_NUMBER_FORMAT_H_ + +#include "include/v8.h" + +#include "unicode/uversion.h" + +namespace U_ICU_NAMESPACE { +class DecimalFormat; +} + +namespace v8 { +namespace internal { + +class NumberFormat { + public: + // 3-letter ISO 4217 currency code plus \0. + static const int kCurrencyCodeLength; + + static v8::Handle JSNumberFormat(const v8::Arguments& args); + + // Helper methods for various bindings. + + // Unpacks date format object from corresponding JavaScript object. + static icu::DecimalFormat* UnpackNumberFormat( + v8::Handle obj); + + // Release memory we allocated for the NumberFormat once the JS object that + // holds the pointer gets garbage collected. + static void DeleteNumberFormat(v8::Persistent object, + void* param); + + // Formats number and returns corresponding string. + static v8::Handle Format(const v8::Arguments& args); + + private: + NumberFormat(); + + static v8::Persistent number_format_template_; +}; + +} } // namespace v8::internal + +#endif // V8_EXTENSIONS_EXPERIMENTAL_NUMBER_FORMAT_H_