/**
* Copyright (c) 2014, Oracle and/or its affiliates.
* All rights reserved.
*/
/*global OraNumberConverter:true*/
/**
* @export
* Placeholder here as closure compiler objects to export annotation outside of top level
*/
/**
* @constructor
* @classdesc Constructs an immutable instance and initializes it with the options provided. When initialized
* with no options, the default options for the current locale are assumed. The converters by
* default use the current page locale (returned by oj.Config.getLocale()). There are several ways
* to initialize the converter.
* <p>
* <ul>
* <li>Using options defined by the ECMA 402 Specification, these would be the properties style,
* currency, currencyDisplay, minimumIntegerDigits, minimumFractionDigits, maximumFractionDigits,
* useGrouping. NOTE: minimumSignificantDigits and maximumSignificantDigits are not supported.</li>
* <li>Using a custom decimal, currency or percent format pattern. specified using the 'pattern' property</li>
* <li>Using the decimalFormat option to define a compact pattern, such as "1M" and "1 million".</li>
* </ul>
* <p>
*
* The converter provides leniency when parsing user input value to a number in the following ways:<br/>
*
* <ul>
* <li>Prefix and suffix that do not match the pattern, are removed. E.g., when pattern is
* "#,##0.00%" (suffix is the % character), a value of "abc-123.45xyz", will be leniently parsed to
* -123.45</li>
* <li>When a value includes a symbol but the pattern doesn't require it. E.g., the options are
* {pattern: "###", currency: 'USD'}, then values ($123), (123) and -123 will be leniently parsed as
* -123.</li>
* </ul>
* <p>
* @property {Object=} options - an object literal used to provide optional information to
* initialize the converter.
* @property {string=} options.style - sets the style of number formatting. Allowed values are "decimal"
* (the default), "currency" or "percent". When a number is formatted as a decimal, the decimal
* character is replaced with the most appropriate symbol for the locale. In English this is a
* decimal point ("."), while in many locales it is a decimal comma (","). If grouping is enabled the
* locale dependent grouping separator is also used. These symbols are also used for numbers
* formatted as currency or a percentage, where appropriate.
* @property {string=} options.currency - specifies the currency that will be used when formatting the
* number. The value should be a ISO 4217 alphabetic currency code. If the style is set to currency,
* it's required that the currency property also be specified. This is because the currency used is
* not dependent on the locale. You may be using a Thai locale, but dealing in US Dollars, e.g.
* @property {string=} options.currencyDisplay - if the number is using currency formatting, specifies
* if the currency will be displayed using its "code" (as an ISO 4217 alphabetic currency code),
* "symbol" (a localized currency symbol (e.g. $ for US dollars, £ for Great British pounds, and so
* on), or "name" (a localized currency name. Allowed values are "code", "symbol" and "name".
* The default is "symbol".
* @property {string=} options.decimalFormat -
* specifies the decimal format length to use when style is set to "decimal".
* Allowed values are : "standard"(default), "short" and "long". 'standard' is equivalent to not
* specifying the 'decimalFormat' attribute, in that case the locale’s default decimal pattern
* is used for formatting.
* <p>
* The user can also specify 'minimumFractionDigits' and 'maximumFractionDigits' to display.
* When not present we use the locale's default max and min fraction digits.
* </p>
* <p>
* There is no need to specify the scale; we automatically detect greatest scale that is less or
* equal than the input number. For example 1000000 is formatted as "1M" or "1 million" and
* 1234 is formatted, with zero fractional digits, as "1K" or " 1 thousand" for
* short and long formats respectively. The pattern for the short and long number is locale dependent
* and uses plural rules for the particular locale.
* </p>
* <p>
* NOTE: Currently this option formats a value (e.g., 2000 -> 2K), but it does not parse a value
* (e.g., 2K -> 2000), so it can only be used
* in a readOnly EditableValue because readOnly EditableValue components do not call
* the converter's parse function.
* </p>
* @property {number=} options.minimumIntegerDigits - sets the minimum number of digits before the
* decimal place (known as integer digits). The number is padded with leading zeros if it would not
* otherwise have enough digits. The value must be an integer between 1 and 21.
* @property {number=} options.minimumFractionDigits - similar to 'minimumIntegerDigits', except it
* deals with the digits after the decimal place (fractional digits). It must be an integer between
* 0 and 20. The fractional digits will be padded with trailing zeros if they are less than the minimum.
* @property {number=} options.maximumFractionDigits - follows the same rules as 'minimumFractionDigits',
* but sets the maximum number of fractional digits that are allowed. The value will be rounded if
* there are more digits than the maximum specified.
* @property {boolean=} options.useGrouping - when the value is truthy, the locale dependent grouping
* separator is used when formatting the number. This is often known as the thousands separator,
* although it is up to the locale where it is placed. The ‘useGrouping’ is set to true by default.
* @property {string=} options.pattern an optional localized pattern, where the characters used in
* pattern are as defined in the Unicode CLDR for numbers, percent or currency formats. When present
* this will override the other "options". <p>
*
* - When the pattern represents a currency style the 'currency' property is required to
* be set, as not setting this will throw an error. The 'currencyDisplay' is optional. <br/>Example:
* {pattern: '¤#,##0', currency: 'USD'}. <p>
*
* - It's not mandatory for the pattern to have the special character '¤' (currency sign)
* be present. When not present, values are treated as a currency value, but are not formatted to
* show the currency symbol. <br/>Example: {pattern: '#,##0', currency: 'USD'} <p>
*
* - When the pattern represents a percent style, the percent special character ('%') needs to be
* explicitly specified in the pattern, e.g., {pattern: "#,##0%"}. If the pattern does not contain
* the percent character it's treated as a decimal pattern, unless the style is set to percent,
* in which case the value is treated as a percent value, but not formatted to show the percent symbol.
* <br/>Example: {style: 'percent', pattern: "#,##0"}. <p>
*
* - A decimal pattern or exponent pattern is specified in the pattern using the CLDR
* conventions. <br/>Example: {pattern: "#,##0.00"} or {pattern: "0.##E+0"}. <p>
*
* NOTE: 'pattern' is provided for backwards compatibility with existing apps that may want the
* convenience of specifying an explicit format mask. Setting a pattern will override the default
* locale specific format. <br/>
*
* @example <caption>Create a number converter to parse/format currencies</caption>
* var converterFactory = oj.Validation.converterFactory("number");
* var options = {style: "currency", currency: "USD", minimumIntegerDigits: 2};
* converter = converterFactory.createConverter(options);<br/>
*
* @example <caption>A number converter for percent values using a custom (CLDR) pattern</caption>
* var converterFactory = oj.Validation.converterFactory("number");
* var options = {pattern: '#,##0%'};
* converter = converterFactory.createConverter(options);<br/>
*
* @example <caption>To parse a value as percent but format it without displaying the percent character</caption>
* var options = {style: 'percent', pattern: '#,##0'};<br/>
*
* @example <caption>To parse a value as currency using a custom (CLDR) pattern</caption>
* var options = {pattern: '¤#,##0', currency: 'USD'};
*
* @example <caption>The following decimalFormat examples are in en locale.
* To format a value as short (default for fraction digits is based on the locale)</caption>
* var options = {style:’decimal’, decimalFormat:’short’};
* converter = converterFactory.createConverter(options);
* converter.format(12345);--> 12.354K<br/>
*
* @example <caption>To format a value as long (default for fraction digits is based on the locale):</caption>
* var options = {style:’decimal’, decimalFormat:’long’};
* converter = converterFactory.createConverter(options);
* converter.format(12345);--> 12.345 thousand<br/>
*
* @example <caption>To format a value as short with minimum fraction digits:</caption>
* options = { style:’decimal’, decimalFormat:’short’,
* minimumFractionDigits:4};
* converter = converterFactory.createConverter(options);
* converter.format(1234);--> 1.2340K<br/>
*
* @example <caption>To format a value as short with maximum fraction digits:</caption>
* options = { style:’decimal’, decimalFormat:’short’,
* maximumFractionDigits:0};
* converter = converterFactory.createConverter(options);
* converter.format(12345);--> 12K<br/>
*
* @example <caption>To format a value as long with minimum and maximum fraction digits:</caption>
* options = { style:’decimal’, decimalFormat:’long',
* minimumFractionDigits:2, maximumFractionDigits:4};
* converter = converterFactory.createConverter(options);
* converter.format(12000);--> 12.00 thousand<br/>
*
* @example <caption>To format a value as short with minimum and maximum fraction digits:</caption>
* options = { style:’decimal’, decimalFormat:’long',
* minimumFractionDigits:2, maximumFractionDigits:4};
* converter = converterFactory.createConverter(options);
* converter.format(12345678);--> 12.345 million<br/>
*
* @example <caption>decimal style default is standard:</caption>
* options = { style:’decimal’, decimalFormat:’standard’};
* converter = converterFactory.createConverter(options);
* converter.format(12345);--> 12,345<br/>
* @export
* @augments oj.NumberConverter
* @name oj.IntlNumberConverter
* @since 0.6
*/
oj.IntlNumberConverter = function(options)
{
this.Init(options);
};
oj.Object.createSubclass(oj.IntlNumberConverter, oj.NumberConverter, "oj.IntlNumberConverter");
/**
* Initializes the number converter instance with the set options.
* @param {Object=} options an object literal used to provide an optional information to
* initialize the converter.<p>
* @export
*/
oj.IntlNumberConverter.prototype.Init = function(options)
{
oj.IntlNumberConverter.superclass.Init.call(this, options);
};
// Returns the wrapped number converter implementation object.
oj.IntlNumberConverter.prototype._getWrapped = function ()
{
if (!this._wrapped)
{
this._wrapped = OraNumberConverter.getInstance();
}
return this._wrapped;
};
/**
* Formats a Number and returns the formatted string, using the options this converter was
* initialized with.
*
* @param {Number|number} value to be formatted for display
* @return {string} the localized and formatted value suitable for display. When the value is
* formatted as a percent it's multiplied by 100.
*
* @throws {Error} a ConverterError both when formatting fails, or if the options provided during
* initialization cannot be resolved correctly.
*
* @export
*/
oj.IntlNumberConverter.prototype.format = function (value)
{
// undefined, null and empty string values all return null. If value is NaN then return "".
if (value == null ||
(typeof value === "string" && (oj.StringUtils.trim("" + value)).length === 0) ||
(typeof value === "number" && isNaN(value)))
{
return oj.IntlConverterUtils.__getNullFormattedValue();
}
// TODO: Is this correct?
var localeElements = oj.LocaleData.__getBundle(), locale = oj.Config.getLocale(),
resolvedOptions = this.resolvedOptions(), converterError;
try
{
return this._getWrapped().format(value, localeElements, resolvedOptions, locale);
}
catch (e)
{
converterError = this._processConverterError(e, value);
throw converterError;
}
};
/**
* Retrieves a hint String describing the format the value is expected to be in.
*
* @return {String} a hint describing the format the value is expected to be in.
* @export
*/
oj.IntlNumberConverter.prototype.getHint = function ()
{
// UX does not want any hint for numbers.
// return oj.Translations.getTranslatedString("oj-converter.hint.summary",
// {'exampleValue': this._getHintValue()});
return oj.IntlNumberConverter.superclass.getHint.call(this);
};
/**
* Returns the options called with converter initialization.
* @return {Object} an object of options.
* @export
*/
oj.IntlNumberConverter.prototype.getOptions = function ()
{
return oj.IntlNumberConverter.superclass.getOptions.call(this);
};
/**
* Parses a string value to return a Number, using the options this converter was initialized with.
*
* @param {String|string} value to parse
* @return {number|null} the parsed number or null if the value was null or an empty string. When
* the value is parsed as a percent its 1/100th part is returned.
*
* @throws {Error} a ConverterError both when parsing fails, or if the options provided during
* initialization cannot be resolved correctly.
*
* @export
*/
oj.IntlNumberConverter.prototype.parse = function (value)
{
var localeElements = oj.LocaleData.__getBundle(), locale = oj.Config.getLocale(),
resolvedOptions = this.resolvedOptions(), converterError;
// null and empty string values are ignored and not parsed. It
// undefined.
if (value == null || value === "") // check for undefined, null and ""
{
return null;
}
try
{
// we want to trim the value for leading spaces before and after
return this._getWrapped().parse(oj.StringUtils.trim(value),
localeElements,
resolvedOptions,
locale);
}
catch (e)
{
converterError = this._processConverterError(e, value);
throw converterError;
}
};
/**
* Returns an object literal with properties reflecting the number formatting options computed based
* on the options parameter. If options (or pattern) is not provided, the properties will be derived
* from the locale defaults.
*
* @return {Object} An object literal containing the resolved values for the following options. Some
* of these properties may not be present, indicating that the corresponding components will not be
* represented in the formatted output.
* <ul>
* <li><b>locale</b>: a String value with the language tag of the locale whose localization is used
* for formatting.</li>
* <li><b>style</b>: a String value. One of the allowed values - "decimal", "currency" or "percent".</li>
* <li><b>currency</b>: a String value. an ISO 4217 alphabetic currency code. May be present only
* when style is currency.</li>
* <li><b>currencyDisplay</b>: a String value. One of the allowed values - "code", "symbol", or
* "name".</li>
* <li><b>numberingSystem</b>: a String value of the numbering system used. E.g. latn</li>
* <li><b>minimumIntegerDigits</b>: a non-negative integer Number value indicating the minimum
* integer digits to be used.</li>
* <li><b>minimumFractionDigits</b>: a non-negative integer Number value indicating the minimum
* fraction digits to be used.</li>
* <li><b>maximumFractionDigits</b>: a non-negative integer Number value indicating the maximum
* fraction digits to be used.</li>
* <li><b>useGrouping</b>: a Boolean value indicating whether a grouping separator is used.</li>
*
* @throws a oj.ConverterError when the options that the converter was initialized with are invalid.
* @export
*/
oj.IntlNumberConverter.prototype.resolvedOptions = function()
{
var localeElements, locale = oj.Config.getLocale(), converterError;
// options are resolved and cached for the current locale. when locale changes resolvedOptions
// is reevaluated as it contains locale specific info.
if ((locale !== this._locale) || !this._resolvedOptions)
{
localeElements = oj.LocaleData.__getBundle();
try
{
if (!localeElements)
{
oj.Logger.error("locale bundle for the current locale %s is unavailable", locale);
return {};
}
// cache if successfully resolved
this._resolvedOptions = this._getWrapped().resolvedOptions(localeElements,
this.getOptions(),
locale);
this._locale = locale;
}
catch (e)
{
converterError = this._processConverterError(e);
throw converterError;
}
}
return this._resolvedOptions;
};
/**
* Processes the error returned by the converter implementation and throws a oj.ConverterError
* instance.
*
* @param {Error} e
* @param {String|string|Number|number|Object=} value
* @throws an instance of oj.ConverterError
* @private
*/
oj.IntlNumberConverter.prototype._processConverterError = function (e, value)
{
var errorInfo = e['errorInfo'], summary, detail, errorCode, parameterMap, converterError,
propName, resourceKey;
if (errorInfo)
{
errorCode = errorInfo['errorCode'];
parameterMap = errorInfo['parameterMap'];
oj.Assert.assertObject(parameterMap);
propName = parameterMap['propertyName'];
if (e instanceof TypeError)
{
if (errorCode === "optionTypesMismatch" || errorCode === "optionTypeInvalid")
{
converterError = oj.IntlConverterUtils.__getConverterOptionError(errorCode, parameterMap);
}
}
else if (e instanceof RangeError)
{
if (errorCode === "optionOutOfRange")
{
converterError = oj.IntlConverterUtils.__getConverterOptionError(errorCode, parameterMap);
}
}
else if (e instanceof SyntaxError)
{
if (errorCode === "optionValueInvalid")
{
converterError = oj.IntlConverterUtils.__getConverterOptionError(errorCode, parameterMap);
}
}
else if (e instanceof Error)
{
if (errorCode === "decimalFormatMismatch")
{
// The '{value}' does not match the expected decimal format
resourceKey = "oj-converter.number.decimalFormatMismatch.summary";
}
else if (errorCode === "currencyFormatMismatch")
{
// The {value} does not match the expected currency format
resourceKey = "oj-converter.number.currencyFormatMismatch.summary";
}
else if (errorCode === "percentFormatMismatch")
{
resourceKey = "oj-converter.number.percentFormatMismatch.summary";
}
// TODO: We'll be able to remove this exception when this bug is fixed post V1.1:
// Bug 20815368 - implement parse() for short number converter
//
else if (errorCode === "unsupportedParseFormat")
{
summary = oj.Translations.getTranslatedString(
"oj-converter.number.decimalFormatUnsupportedParse.summary");
detail = oj.Translations.getTranslatedString(
"oj-converter.number.decimalFormatUnsupportedParse.detail");
converterError = new oj.ConverterError(summary, detail);
}
if (resourceKey)
{
summary = oj.Translations.getTranslatedString(resourceKey,
{'value': value || parameterMap['value'],
'format': parameterMap['format']});
// _getHintValue is smart. It uses the converter's 'format' function
// to get the example format to show the end user.
detail = oj.Translations.getTranslatedString("oj-converter.hint.detail",
{'exampleValue': this._getHintValue()});
converterError = new oj.ConverterError(summary, detail);
}
}
}
if (!converterError)
{
// An error we are unfamiliar with. Get the message and set as detail
summary = e.message; // TODO: What should the summary be when it's missing??
detail = e.message;
converterError = new oj.ConverterError(summary, detail);
}
return converterError;
};
// Returns the hint value. It uses the converter's format function to return a formatted
// example. For example, if the converter's style is decimal and decimalFormat is short,
// this.format(12345.98765) returns 12K, and we show 12K in the error message as an example
// of what they should type in.
oj.IntlNumberConverter.prototype._getHintValue = function()
{
var value = "";
try
{
// use .format to get a real example to show the user what format they can type in to the field.
value = this.format(12345.98765);
}
catch (e)
{
if (e instanceof oj.ConverterError)
{
// Something went wrong and we don't have a way to retrieve a valid value.
// TODO: Log an error
value = "";
}
}
finally
{
// returns the formatted value of 12345.98765
return value;
}
};
Source: src/main/javascript/oracle/oj/ojvalidation/IntlNumberConverter.js
Oracle® JavaScript Extension Toolkit (JET)
1.1.2
E65298-01