/**
* Copyright (c) 2014, Oracle and/or its affiliates.
* All rights reserved.
*/
/*global OraDateTimeConverter: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.
* <p>
* The converter instance uses locale symbols for the locale set on the page (returned by
* {@li.nk oj.Config#getLocale}. Please note that timezone is currently not supported by the
* converter.
* </p>
* There are several ways to initialize the converter.
* <ul>
* <li>Using options defined by the ECMA 402 Specification, these would be the properties year,
* month, day, hour, minute, second, weekday, era, timeZoneName, hour12</li>
* <li>Using a custom date and/or time format pattern using the 'pattern' property</li>
* <li>Using the standard date, datetime and time format lengths defined by Unicode CLDR, these
* would include the properties formaType, dateFormat, timeFormat.</li>
* </ul>
*
* <p>
* The options when specified take precendence in the following order:<br>
* 1. pattern.<br>
* 2. ECMA options.<br>
* 3. formatType/dateFormat/timeFormat.
* <p>
* The converter provides great leniency when parsing a user input value to a date in the following
* ways: <br/>
* <ul>
* <li>Allows use of any character for separators irrespective of the separator specified in the
* associated pattern. E.g., if pattern is set to 'y-M-d', the following values are all valid -
* 2013-11-16, 2013/11-16 and 2013aaa11xxx16.</li>
* <li>Allows specifying 4 digit year in any position in relation to day and month. E.g., 11-2013-16
* or 16-11-2013</li>
* <li>Supports auto-correction of value, when month and day positions are swapped as long as the
* date is > 12 when working with the Gregorian calendar. E.g., if the pattern is 'y-M-d',
* 2013-16-11 will be auto-corrected to 2013-11-16. However if both date and month are less or equal
* to 12, no assumptions are made about the day or month and the value parsed against the exact pattern.</li>
* <li>Supports auto-correction of value, for the short and long types of weekday and month names.
* So they can are used anywhere in the value. E.g., if the expected pattern is E, MMM, d, y, all
* these values are acceptable - Tue, Nov 26 2013 or Nov, Tue 2013 26 or 2013 Tue 26 Nov. <br/>
* NOTE: Lenient parsing of narrow era, weekday or month name is not supported because of ambiguity in
* choosing the right value. So we expect for narrow era, weekday or month option that values be
* provided either in their short or long forms. E.g., Sat, March 02, 2013.
* </li>
* <li>Specifying the weekday is optional. E.g., if the expected pattern is E, MMM, d, y; then
* entering Nov 26, 2013, is automatically turned to Tuesday Nov 26, 2013. But entering an invalid
* weekday, i.e., if the weekday does not match the date, an exception is thrown.</li>
* <li>Leniency rules apply equally no matter which option is used - pattern, ECMA options or formatType</li>
* </ul>
*
* @property {Object=} options - an object literal used to provide an optional information to
* initialize the converter.<p>
* @property {string=} options.year - allowed values are "2-digit", "numeric". When no options are
* set the default value of "numeric" is used.
* @property {string=} options.month - specifies how the month is formatted. Allowed values are
* "2-digit", "numeric", "narrow", "short", "long". The last 3 values behave in the same way as for
* weekday, indicating the length of the string used. When no options are set the default value of
* "numeric" is used.
* @property {string=} options.day - specifies how the day is formatted. Allowed values are "2-digit",
* "numeric". When no options are set the default value of "numeric" is used.
* @property {string=} options.hour - specifies how the hour is formatted. Allowed values are
* "2-digit" or "numeric". The hour is displayed using the 12 or 24 hour clock, depending on the
* locale. See 'hour12' for details.
* @property {string=} options.minute - specifies how the minute is formatted. Allowed values are
* "2-digit", "numeric".
* @property {string=} options.second - specifies whether the second should be displayed as "2-digit"
* or "numeric".
* @property {string=} options.weekday - specifies how the day of the week is formatted. If absent, it
* is not included in the date formatting. Allowed values are "narrow", "short", "long" indicating the
* length of the string used.
* @property {string=} options.era - specifies how the era is included in the formatted date. If
* absent, it is not included in the date formatting. Allowed values are "narrow", "short", "long".
* @property {string=} options.timeZoneName - allowed values are "short", "long".
* @property {boolean=} options.hour12 - specifies what time notation is used for formatting the time.
* A true value uses the 12-hour clock and false uses the 24-hour clock (often called military time
* in the US). This property is undefined if the hour property is not used when formatting the date.
*
* @property {string=} options.pattern - a localized string pattern, where the the characters used in
* pattern conform to Unicode CLDR for date time formats. This will override all other options
* when present. <br/>
* 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.
*
* @property {string=} options.formatType - determines the 'standard' date and/or time format lengths
* to use. Allowed values: "date", "time", "datetime". See 'dateFormat' and 'timeFormat' options.
* When set a value must be specified.
* @property {string=} options.dateFormat - specifies the standard date format length to use when
* formatType is set to "date" or "datetime". Allowed values are : "short" (default), "medium", "long",
* "full".
* @property {string=} options.timeFormat - specifies the standard time format length to use when
* 'formatType' is set to "time" or "datetime". Allowed values: "short" (default), "medium", "long",
* "full".
*
* @example <caption>Create a date time converter using no options. This uses the default value for
* year, month, day properties</caption>
* var converterFactory = oj.Validation.converterFactory("datetime");
* converter = converterFactory.createConverter();
* var resolved = converter.resolvedOpions();
* // logs "day=numeric, month=numeric, year=numeric"
* console.log("day=" + resolved.day + ", month=" + resolved.month + ", year=" + resolved.year);
* <br/>
*
* @example <caption>Create a date time converter using the ECMA options to represent date</caption>
* var options = { year:'2-digit', month: '2-digit', day: '2-digit'};
* var converterFactory = oj.Validation.converterFactory("datetime");
* converter = converterFactory.createConverter(options);<br/>
*
* @example <caption>Create a date time converter using the 'pattern' option</caption>
* var options = {pattern: 'MM-dd-yyyy'};
* var converterFactory = oj.Validation.converterFactory("datetime");
* converter = converterFactory.createConverter(options);<br/>
*
* @example <caption>Create a date time converter using the standard format length</caption>
* var options = {formatType: 'date', dateFormat: 'medium'};
* var converterFactory = oj.Validation.converterFactory("datetime");
* converter = converterFactory.createConverter(options);<br/>
*
* @export
* @augments oj.DateTimeConverter
* @name oj.IntlDateTimeConverter
* @since 0.6
*/
oj.IntlDateTimeConverter = function(options)
{
this.Init(options);
};
// Subclass from oj.Object
oj.Object.createSubclass(oj.IntlDateTimeConverter, oj.DateTimeConverter, "oj.IntlDateTimeConverter");
oj.IntlDateTimeConverter._DEFAULT_DATE = new Date(1998, 10, 29, 15, 45, 31);
/**
* Initializes the date time 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.IntlDateTimeConverter.prototype.Init = function(options)
{
oj.IntlDateTimeConverter.superclass.Init.call(this, options);
};
// Returns the wrapped date time converter implementation object.
oj.IntlDateTimeConverter.prototype._getWrapped = function ()
{
if (!this._wrapped)
{
this._wrapped = OraDateTimeConverter.getInstance();
}
return this._wrapped;
};
/**
* Formats the local isoString value using the options provided and returns a string value.
* <p>
* Note: Application code that was previosuly passinga JavaScript Date object to this method, can
* now use the utility function oj.IntlConverterUtils.dateToLocalIso(), to get the proper isoString
* value.
*
* @param {string} value to be formatted for display which should be a local isoString
* @return {string|Object} the localized and formatted value suitable for display
*
* @throws {Error} a ConverterError both when formatting fails, and if the options provided during
* initialization cannot be resolved correctly. Also if the iso string value contains time zone
* information, like the UTC designator (Z) or timezone offsets, an error is thrown.
*
* @example <caption>To convert Javascript Date to an iso string before passing to <code class="prettyprint">format</code></caption>
* var date = new Date();
* var formatted = converter.format(oj.IntlConverterUtils.dateToLocalIso(date));
*
* @see oj.IntlConverterUtils.dateToLocalIso
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.format = function (value)
{
var localeElements = oj.LocaleData.__getBundle(), locale = oj.Config.getLocale(),
resolvedOptions = this.resolvedOptions(), converterError;
// undefined, null and empty string values all return null. If value is NaN then return "".
// TODO: Should we automatically parse() the integer value representing the number of milliseconds
// since 1 January 1970 00:00:00 UTC (Unix Epoch)?
if (value == null ||
(typeof value === "string" && (oj.StringUtils.trim("" + value)).length === 0))
{
return oj.IntlConverterUtils.__getNullFormattedValue();
}
try
{
return this._getWrapped().format(value, localeElements, resolvedOptions, locale);
}
catch (e)
{
converterError = this._processConverterError(e, value);
throw converterError;
}
};
/**
* Formats an ISOString as a relative date, using the relativeOptions.
* <p>
* Note: Application code that was previosuly passing a JavaScript Date object to this method can
* now use the utility function oj.IntlConverterUtils.dateToLocalIso to get the proper isoString
* value.
*
* @param {string} value - value to be formatted. This value is compared with the current date
* on the client to arrive at the relative formatted value.
* @param {Object} relativeOptions - an Object literal containing the following properties. The
* default options are ignored during relative formatting - <br>
* <ul>
* <li><b>formatUsing</b>: Specifies the relative formatting convention to use for (calendar or)
* the date field type. allowed values: "displayName". <br>Setting value to 'displayName' uses the
* relative display name for the instance of the dateField, and one or two past and future instances.
* </li>
* <li><b>dateField</b>: allowed values are: "day", "week", "month", "year"</li>
* </ul>
* @return {string|null} relative date. null if the value falls out side the supported relative range.
* @throws {Object} an instance of {@link oj.ConverterError}
*
* @example <caption>To convert Javascript Date to an iso string before passing to <code class="prettyprint">formatRelative</code></caption>
* var formatted = converter.formatRelative(oj.IntlConverterUtils.dateToLocalIso(new Date()));
*
* @see oj.IntlConverterUtils.dateToLocalIso
*
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.formatRelative = function(value, relativeOptions)
{
var localeElements = oj.LocaleData.__getBundle(), locale = oj.Config.getLocale(), converterError;
try
{
return this._getWrapped().formatRelative(value, localeElements, relativeOptions, locale);
}
catch (e)
{
converterError = this._processConverterError(e, value);
throw converterError;
}
};
/**
* Retrieves a hint String describing the format the value is expected to be in. THe pattern used is
* provided through resolvedOptions, except when an actual pattern is set in the options. Otherwise
* hint is empty.
*
* @return {String} a hint describing the format the value is expected to be in.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.getHint = function ()
{
var resolvedOptions = this.resolvedOptions(), patternFromOptions =
resolvedOptions["patternFromOptions"] || this.getOptions()['pattern'];
// TODO: converter hints are often shown in placeholder and is a user-friendly readable pattern.
// Until this feature is implemented use the CLDR pattern converted to lowercase
return patternFromOptions ? patternFromOptions.toLowerCase() : "";
};
// Returns the hint value.
oj.IntlDateTimeConverter.prototype._getHintValue = function()
{
var value = "";
try
{
// example date
value = this.format(oj.IntlConverterUtils.dateToLocalIso(oj.IntlDateTimeConverter._DEFAULT_DATE));
}
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
{
return value;
}
};
/**
* Returns the options called with converter initialization.
* @return {Object} an object of options.
* @export
* @memberOf oj.IntlDateTimeConverter
*
*/
oj.IntlDateTimeConverter.prototype.getOptions = function ()
{
return oj.IntlDateTimeConverter.superclass.getOptions.call(this);
};
/**
* Returns an object literal with locale and formatting options computed during initialization of
* the object. If options was not provided at the time of initialization, the properties will be
* derived from the locale defaults.
* @return {Object} an object of resolved options. Properties whose corresponding internal
* properties are not present are not assigned. More properties may be included as needed.
* <ul>
* <li><b>locale</b> - a String value with the language tag of the locale whose localization is used
* for formatting.</li>
* <li><b>numberingSystem</b>: a String value of the numbering system used. E.g. latn</li>
* <li><b>era</b>: a String value. One of allowed values - "narrow", "short", "long"</li>
* <li><b>year</b>: a String value. One of allowed values - "2-digit", "numeric"</li>
* <li><b>month</b>: a String value. One of allowed values - "2-digit", "numeric", "narrow", "short"
* , "long"</li>
* <li><b>weekday</b>: a String value. One of the allowed values - "narrow", "short", "long"</li>
* <li><b>day</b>: a String value. One of allowed values - "2-digit", "numeric"</li>
* <li><b>hour</b>: String value. One of allowed values - "2-digit", "numeric"</li>
* <li><b>minute</b>: a String value. One of allowed values - "2-digit", "numeric"</li>
* <li><b>second</b>: a String value. One of allowed values - "2-digit", "numeric"</li>
* <li><b>hour12</b>: a Boolean value indicating whether 12-hour format (true) or 24-hour format
* (false) should be used. It is only relevant when hour is also present.</li>
* <li><b>timeZoneName</b>: String value. One of allowed values - "short", "long".</li>
* </ul>
*
* @throws a oj.ConverterError when the options that the converter was initialized with are invalid.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.resolvedOptions = function ()
{
var localeElements, locale = oj.Config.getLocale(), converterError, options = this.getOptions();
// options are resolved and cached for a locale
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,
options,
locale);
this._locale = locale;
}
catch (e)
{
converterError = this._processConverterError(e);
throw converterError;
}
}
return this._resolvedOptions;
};
/**
* Returns true if a 24-hour format is set; false otherwise.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.isHourInDaySet = function()
{
var ro = this.resolvedOptions(), hour = ro['hour'], hour12 = ro['hour12'];
if (hour && !hour12)
{
// if hour12=false or not set and hour is set to some value
return true;
}
return false;
};
/**
* Returns true if 12-hour is set; false otherwise.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.isHourInAMPMSet = function()
{
var ro = this.resolvedOptions(), hour = ro['hour'], hour12 = ro['hour12'];
if (hour && hour12)
{
// if hour12==true and hour is set to some value
return true;
}
return false;
};
/**
* Returns true if minutes are shown in the time portion; false otherwise.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.isMinuteSet = function()
{
return this._isOptionSet('minute');
};
/**
* Returns true if seconds are shown in the time portion; false otherwise.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.isSecondSet = function()
{
return this._isOptionSet('second');
};
/**
* Returns true if milliseconds are shown in the time portion; false otherwise.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.isMilliSecondSet = function()
{
return this._isOptionSet('millisecond');
};
/**
* Returns true if year is shown in the date portion; false otherwise.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.isYearSet = function()
{
return this._isOptionSet('year');
};
/**
* Returns true if month is shown in the date portion; false otherwise.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.isMonthSet = function()
{
return this._isOptionSet('month');
};
/**
* Returns true if day is shown in the date portion; false otherwise.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.isDaySet = function()
{
return this._isOptionSet('day');
};
/**
* Returns true if the day name is shown in the date portion; false otherwise.
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.isDayNameSet = function()
{
return this._isOptionSet('weekday');
};
/**
* Returns the calculated week for the isoString value
*
* @param {string} value to return the calculated week of
* @return {number} calculated week.
*
* @memberOf oj.IntlDateTimeConverter
* @export
*/
oj.IntlDateTimeConverter.prototype.calculateWeek = function(value)
{
return this._getWrapped().calculateWeek(value, oj.LocaleData.__getBundle(), oj.Config.getLocale());
};
/**
* Parses the value using the options provided and returns the local date and time as a string
* expressed using the ISO-8601 format (http://en.wikipedia.org/wiki/ISO_8601).
*
* <p>
* For converter options specific to a date, the iso date representation alone is returned. <br/>
* For time only options, the iso local time representation alone is returned. <br/>
* For options that include both date and time, the iso date and local time representation is
* returned.<br/>
* </p>
*
* <p>
* For convenience, if one wishes to retrieve a JavaScript Date object from the local isoString, a
* utility function oj.IntlConverterUtils.isoToLocalDate is provided.
* </p>
*
* @param {String|string} value to parse
* @return {string|Object} the parsed value as an ISOString.
*
* @throws {Error} a ConverterError both when parsing fails, and if the options provided during
* initialization cannot be resolved correctly. Parsing can also fail when the value includes a time
* zone.
*
* @example <caption>Parse date, time and date & time values using <code class="prettyprint">parse</code> method.</caption>
* <!-- For date-time values -->
* var options = {pattern: 'MM/dd/yy hh:mm:ss a'};
* var conv = oj.Validation.converterFactory('datetime').createConverter(options);
* cnv.parse('09/11/14 03:02:01 PM'); // '2014-10-20T15:02:01'
*
* <!-- For date values -->
* var options = {pattern: 'MM/dd/yy'};
* cnv.parse('09/11/14'); // '2014-10-20'
*
* <!-- For time values -->
* var options = {pattern: 'hh:mm:ss a'};
* cnv.parse('03:02:01 PM'); // 'T15:02:01'
*
* @example <caption>Convert from iso string to Javascript Date object</caption>
* var isoString = '2014-10-20T15:02:01';
* var date = oj.IntlConverterUtils.isoToLocalDate(converter.parse(isoString));
*
* @see oj.IntlConverterUtils.isoToLocalDate
*
* @export
*/
oj.IntlDateTimeConverter.prototype.parse = function (value)
{
var result, parsed;
// undefined, null and empty string values are ignored and not parsed.
if (value == null || value === "")
{
return null;
}
var localeElements = oj.LocaleData.__getBundle(), locale = oj.Config.getLocale(),
resolvedOptions = this.resolvedOptions(), converterError;
try
{
// date converter parses the value and returns an Object with 2 fields - 'value' and 'warning'
result = this._getWrapped().parse(value, localeElements, resolvedOptions, locale);
parsed = result['value'];
if (parsed)
{
// TODO: For now log a warning when we leniently parse a value; later we plan to flash the
// field.
if (result['warning'])
{
oj.Logger.warn("The value " + value + " was leniently parsed to represent a date " +
(parsed.toString) ? parsed.toString() : parsed);
}
}
return parsed;
}
catch (e)
{
converterError = this._processConverterError(e, value);
throw converterError;
}
};
/**
* Processes the error returned by the converter implementation and throws a oj.ConverterError
* instance.
* @param {Error} e
* @param {String|string|Date|Object=} value
* @throws an instance of oj.ConverterError
* @private
*/
oj.IntlDateTimeConverter.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 (errorCode === 'datetimeOutOfRange') // TODO: NLS should use lower case time
{
// The '{value}' is out of range. Enter a value between '{minValue}' and '{maxValue}' for
// '{propertyName}'.
summary = oj.Translations.getTranslatedString("oj-converter.datetime.datetimeOutOfRange.summary",
{'propertyName': propName,
'value': parameterMap['value']});
detail = oj.Translations.getTranslatedString("oj-converter.datetime.datetimeOutOfRange.detail",
{'minValue': parameterMap['minValue'],
'maxValue': parameterMap['maxValue']});
converterError = new oj.ConverterError(summary, detail);
}
}
else if (e instanceof SyntaxError)
{
if (errorCode === "optionValueInvalid")
{
converterError = oj.IntlConverterUtils.__getConverterOptionError(errorCode, parameterMap);
}
}
else if (e instanceof Error)
{
if (errorCode === "dateFormatMismatch")
{
// The '{value}' does not match the expected date format '{format}'.
resourceKey = "oj-converter.datetime.dateFormatMismatch.summary";
}
else if (errorCode === "timeFormatMismatch")
{
// The {value} does not match the expected time format {format}.
resourceKey = "oj-converter.datetime.timeFormatMismatch.summary";
}
else if (errorCode === "datetimeFormatMismatch")
{
resourceKey = "oj-converter.datetime.datetimeFormatMismatch.summary";
}
else if (errorCode === "dateToWeekdayMismatch")
{
summary = oj.Translations.getTranslatedString("oj-converter.datetime.dateToWeekdayMismatch.summary",
{'date': parameterMap['date'], 'weekday': parameterMap['weekday']});
detail = oj.Translations.getTranslatedString("oj-converter.datetime.dateToWeekdayMismatch.detail");
converterError = new oj.ConverterError(summary, detail);
}
if (resourceKey)
{
summary = oj.Translations.getTranslatedString(resourceKey,
{'value': value || parameterMap['value'],
'format': parameterMap['format']});
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;
};
/**
* Checks to see if an option is present in the resolved options.
* @param {string} optionName
* @returns {boolean} true if optionName is present.
* @private
*/
oj.IntlDateTimeConverter.prototype._isOptionSet = function (optionName)
{
var ro = this.resolvedOptions(), hasOption = ro[optionName] ? true : false;
return hasOption;
};
Source: src/main/javascript/oracle/oj/ojvalidation/IntlDateTimeConverter.js
Oracle® JavaScript Extension Toolkit (JET)
1.1.2
E65298-01