/**
* Copyright (c) 2012-2016 André Bargull
* Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms.
*
* <https://github.com/anba/es6draft>
*/
package com.github.anba.es6draft.runtime.objects.intl;
import static com.github.anba.es6draft.runtime.AbstractOperations.*;
import static com.github.anba.es6draft.runtime.internal.Errors.newRangeError;
import static com.github.anba.es6draft.runtime.internal.Properties.createProperties;
import static com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.*;
import static com.github.anba.es6draft.runtime.types.builtins.ArrayObject.ArrayCreate;
import static java.util.Arrays.asList;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.CharacterIterator;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.Realm;
import com.github.anba.es6draft.runtime.internal.Initializable;
import com.github.anba.es6draft.runtime.internal.Lazy;
import com.github.anba.es6draft.runtime.internal.Messages;
import com.github.anba.es6draft.runtime.internal.Properties.Attributes;
import com.github.anba.es6draft.runtime.internal.Properties.Function;
import com.github.anba.es6draft.runtime.internal.Properties.Prototype;
import com.github.anba.es6draft.runtime.internal.Properties.Value;
import com.github.anba.es6draft.runtime.objects.intl.DateFieldSymbolTable.DateField;
import com.github.anba.es6draft.runtime.objects.intl.DateFieldSymbolTable.FieldWeight;
import com.github.anba.es6draft.runtime.objects.intl.DateFieldSymbolTable.Skeleton;
import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.ExtensionKey;
import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.LocaleData;
import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.LocaleDataInfo;
import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.OptionsRecord;
import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.ResolvedLocale;
import com.github.anba.es6draft.runtime.types.Constructor;
import com.github.anba.es6draft.runtime.types.Intrinsics;
import com.github.anba.es6draft.runtime.types.ScriptObject;
import com.github.anba.es6draft.runtime.types.Type;
import com.github.anba.es6draft.runtime.types.builtins.ArrayObject;
import com.github.anba.es6draft.runtime.types.builtins.BuiltinConstructor;
import com.github.anba.es6draft.runtime.types.builtins.BuiltinFunction;
import com.github.anba.es6draft.runtime.types.builtins.OrdinaryObject;
import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.DateTimePatternGenerator;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.text.NumberingSystem;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.ULocale;
/**
* <h1>12 DateTimeFormat Objects</h1>
* <ul>
* <li>12.1 Abstract Operations For DateTimeFormat Objects
* <li>12.2 The Intl.DateTimeFormat Constructor
* <li>12.3 Properties of the Intl.DateTimeFormat Constructor
* </ul>
*/
public final class DateTimeFormatConstructor extends BuiltinConstructor implements Initializable {
/** [[availableLocales]] */
private final Lazy<Set<String>> availableLocales = Lazy
.of(() -> GetAvailableLocales(LanguageData.getAvailableDateFormatLocales()));
/**
* [[availableLocales]]
*
* @param cx
* the execution context
* @return the set of available locales supported by {@code Intl.DateTimeFormat}
*/
public static Set<String> getAvailableLocales(ExecutionContext cx) {
return getAvailableLocalesLazy(cx).get();
}
private static Lazy<Set<String>> getAvailableLocalesLazy(ExecutionContext cx) {
DateTimeFormatConstructor dateTimeFormat = (DateTimeFormatConstructor) cx
.getIntrinsic(Intrinsics.Intl_DateTimeFormat);
return dateTimeFormat.availableLocales;
}
/** [[relevantExtensionKeys]] */
private static final List<ExtensionKey> relevantExtensionKeys = asList(ExtensionKey.ca, ExtensionKey.nu);
/**
* Calendar algorithm keys (BCP 47; CLDR, version 28)
*/
private enum CalendarAlgorithm {/* @formatter:off */
buddhist("buddhist"),
chinese("chinese"),
coptic("coptic"),
dangi("dangi"),
ethioaa("ethioaa", "ethiopic-amete-alem"),
ethiopic("ethiopic"),
gregory("gregory", "gregorian"),
hebrew("hebrew"),
indian("indian"),
islamic("islamic"),
islamic_umalqura("islamic-umalqura"),
islamic_tbla("islamic-tbla"),
// commented out, instead handled by "islamicc"
/* islamic_civil("islamic-civil"), */
islamic_rgsa("islamic-rgsa"),
iso8601("iso8601"),
japanese("japanese"),
persian("persian"),
roc("roc"),
islamicc("islamicc", "islamic-civil"), // deprecated
;
/* @formatter:on */
private final String name;
private final String alias;
private CalendarAlgorithm(String name) {
this.name = name;
this.alias = null;
}
private CalendarAlgorithm(String name, String alias) {
this.name = name;
this.alias = alias;
}
public String getName() {
return name;
}
public String getAlias() {
return alias;
}
public static CalendarAlgorithm forName(String name) {
for (CalendarAlgorithm co : values()) {
if (name.equals(co.name) || name.equals(co.alias)) {
return co;
}
}
throw new IllegalArgumentException(name);
}
}
/** [[localeData]] */
private static final class DateTimeFormatLocaleData implements LocaleData {
@Override
public LocaleDataInfo info(ULocale locale) {
return new DateTimeFormatLocaleDataInfo(locale);
}
}
/** [[localeData]] */
private static final class DateTimeFormatLocaleDataInfo implements LocaleDataInfo {
private final ULocale locale;
public DateTimeFormatLocaleDataInfo(ULocale locale) {
this.locale = locale;
}
@Override
public String defaultValue(ExtensionKey extensionKey) {
switch (extensionKey) {
case ca:
String[] values = Calendar.getKeywordValuesForLocale("calendar", locale, false);
return CalendarAlgorithm.forName(values[0]).getName();
case nu:
return NumberingSystem.getInstance(locale).getName();
default:
throw new IllegalArgumentException(extensionKey.name());
}
}
@Override
public List<String> entries(ExtensionKey extensionKey) {
switch (extensionKey) {
case ca:
return getCalendarInfo();
case nu:
return getNumberInfo();
default:
throw new IllegalArgumentException(extensionKey.name());
}
}
private List<String> getCalendarInfo() {
String[] values = Calendar.getKeywordValuesForLocale("calendar", locale, false);
ArrayList<String> result = new ArrayList<>(values.length);
for (int i = 0, len = values.length; i < len; ++i) {
String calendarName = values[i];
// Ignore "unknown" calendar entry in result set
if ("unknown".equals(calendarName)) {
continue;
}
CalendarAlgorithm algorithm = CalendarAlgorithm.forName(calendarName);
result.add(algorithm.getName());
if (algorithm.getAlias() != null) {
result.add(algorithm.getAlias());
}
}
return result;
}
private List<String> getNumberInfo() {
// ICU4J does not provide an API to retrieve the numbering systems per locale, go with
// Spidermonkey instead and return default numbering system of locale + Table 2 entries
String localeNumberingSystem = NumberingSystem.getInstance(locale).getName();
return asList(localeNumberingSystem, "arab", "arabtext", "bali", "beng", "deva", "fullwide", "gujr", "guru",
"hanidec", "khmr", "knda", "laoo", "latn", "limb", "mlym", "mong", "mymr", "orya", "tamldec",
"telu", "thai", "tibt");
}
}
/**
* Constructs a new DateTimeFormat constructor function.
*
* @param realm
* the realm object
*/
public DateTimeFormatConstructor(Realm realm) {
super(realm, "DateTimeFormat", 0);
}
@Override
public void initialize(Realm realm) {
createProperties(realm, this, Properties.class);
}
@Override
public DateTimeFormatConstructor clone() {
return new DateTimeFormatConstructor(getRealm());
}
@SafeVarargs
private static <T> T[] array(T... elements) {
return elements;
}
@SafeVarargs
private static <T> Set<T> set(T... elements) {
return new HashSet<>(asList(elements));
}
/**
* 12.1.1 InitializeDateTimeFormat (dateTimeFormat, locales, options)
*
* @param cx
* the execution context
* @param dateTimeFormat
* the date format object
* @param locales
* the locales array
* @param opts
* the options object
*/
public static void InitializeDateTimeFormat(ExecutionContext cx, DateTimeFormatObject dateTimeFormat,
Object locales, Object opts) {
/* steps 1-2 (FIXME: spec bug - unnecessary internal slot) */
/* step 3 */
Set<String> requestedLocales = CanonicalizeLocaleList(cx, locales);
/* step 4 */
ScriptObject options = ToDateTimeOptions(cx, opts, "any", "date");
/* step 6 */
String matcher = GetStringOption(cx, options, "localeMatcher", set("lookup", "best fit"), "best fit");
/* step 5, 7 */
OptionsRecord opt = new OptionsRecord(OptionsRecord.MatcherType.forName(matcher));
/* step 8 */
DateTimeFormatLocaleData localeData = new DateTimeFormatLocaleData();
/* step 9 */
ResolvedLocale r = ResolveLocale(cx.getRealm(), getAvailableLocalesLazy(cx), requestedLocales, opt,
relevantExtensionKeys, localeData);
/* step 10 */
dateTimeFormat.setLocale(r.getLocale());
/* step 11 */
dateTimeFormat.setCalendar(r.getValue(ExtensionKey.ca));
/* step 12 */
dateTimeFormat.setNumberingSystem(r.getValue(ExtensionKey.nu));
/* step 13 */
String dataLocale = r.getDataLocale();
/* step 14 */
Object tz = Get(cx, options, "timeZone");
/* steps 15-16 */
String timeZone;
if (!Type.isUndefined(tz)) {
/* step 15.a */
timeZone = ToFlatString(cx, tz);
/* step 15.b */
if (!IsValidTimeZoneName(timeZone)) {
throw newRangeError(cx, Messages.Key.IntlInvalidOption, timeZone);
}
/* step 15.c */
timeZone = CanonicalizeTimeZoneName(timeZone);
} else {
/* step 16.a */
timeZone = DefaultTimeZone(cx.getRealm());
}
/* step 17 */
dateTimeFormat.setTimeZone(timeZone);
/* step 18 (moved) */
/* step 19 */
// FIXME: spec should propably define exact iteration order here
String weekday = GetStringOption(cx, options, "weekday", set("narrow", "short", "long"), null);
String era = GetStringOption(cx, options, "era", set("narrow", "short", "long"), null);
String year = GetStringOption(cx, options, "year", set("2-digit", "numeric"), null);
String month = GetStringOption(cx, options, "month", set("2-digit", "numeric", "narrow", "short", "long"),
null);
String day = GetStringOption(cx, options, "day", set("2-digit", "numeric"), null);
String hour = GetStringOption(cx, options, "hour", set("2-digit", "numeric"), null);
String minute = GetStringOption(cx, options, "minute", set("2-digit", "numeric"), null);
String second = GetStringOption(cx, options, "second", set("2-digit", "numeric"), null);
String timeZoneName = GetStringOption(cx, options, "timeZoneName", set("short", "long"), null);
/* steps 20-21 (moved) */
/* step 22 */
String formatMatcher = GetStringOption(cx, options, "formatMatcher", set("basic", "best fit"), "best fit");
/* steps 23-26 (moved) */
/* step 27 */
Boolean hour12 = GetBooleanOption(cx, options, "hour12", null);
/* steps 18, 20-21, 23-26, 28-29 */
FormatMatcherRecord formatRecord = new FormatMatcherRecord(weekday, era, year, month, day, hour, minute, second,
timeZoneName, hour12);
Lazy<String> pattern;
if ("basic".equals(formatMatcher)) {
pattern = new BasicFormatPattern(formatRecord, dataLocale);
} else {
pattern = new BestFitFormatPattern(formatRecord, dataLocale);
}
/* step 30 */
dateTimeFormat.setPattern(pattern);
/* step 31 */
dateTimeFormat.setBoundFormat(null);
/* step 32 (FIXME: spec bug - unnecessary internal slot) */
/* step 33 (omitted) */
}
/**
* 12.1.1 InitializeDateTimeFormat (dateTimeFormat, locales, options)
*
* @param realm
* the realm instance
* @param dateTimeFormat
* the date format object
*/
public static void InitializeDefaultDateTimeFormat(Realm realm, DateTimeFormatObject dateTimeFormat) {
/* steps 1-2 (FIXME: spec bug - unnecessary internal slot) */
/* steps 3-7 (not applicable) */
/* step 8 */
DateTimeFormatLocaleData localeData = new DateTimeFormatLocaleData();
/* step 9 */
ResolvedLocale r = ResolveDefaultLocale(realm, relevantExtensionKeys, localeData);
/* step 10 */
dateTimeFormat.setLocale(r.getLocale());
/* step 11 */
dateTimeFormat.setCalendar(r.getValue(ExtensionKey.ca));
/* step 12 */
dateTimeFormat.setNumberingSystem(r.getValue(ExtensionKey.nu));
/* step 13 */
String dataLocale = r.getDataLocale();
/* steps 14-17 */
dateTimeFormat.setTimeZone(DefaultTimeZone(realm));
/* steps 18-29 */
FormatMatcherRecord formatRecord = new FormatMatcherRecord(null, null, "numeric", "numeric", "numeric", null,
null, null, null, null);
Lazy<String> pattern = new BestFitFormatPattern(formatRecord, dataLocale);
/* step 30 */
dateTimeFormat.setPattern(pattern);
/* step 31 */
dateTimeFormat.setBoundFormat(null);
/* step 32 (FIXME: spec bug - unnecessary internal slot) */
/* step 33 (omitted) */
}
/**
* 12.1.2 ToDateTimeOptions (options, required, defaults)
*
* @param cx
* the execution context
* @param opts
* the options object
* @param required
* the required date field
* @param defaults
* the default date field
* @return the date-time options script object
*/
public static ScriptObject ToDateTimeOptions(ExecutionContext cx, Object opts, String required, String defaults) {
/* steps 1-2 */
OrdinaryObject options = ObjectCreate(cx, Type.isUndefined(opts) ? null : ToObject(cx, opts));
/* step 3 */
boolean needDefaults = true;
/* step 4 */
if ("date".equals(required) || "any".equals(required)) {
// FIXME: spec vs. impl (short circuit after first undefined value?)
for (String prop : array("weekday", "year", "month", "day")) {
Object kvalue = Get(cx, options, prop);
if (!Type.isUndefined(kvalue)) {
needDefaults = false;
}
}
}
/* step 5 */
if ("time".equals(required) || "any".equals(required)) {
// FIXME: spec vs. impl (short circuit after first undefined value?)
for (String prop : array("hour", "minute", "second")) {
Object kvalue = Get(cx, options, prop);
if (!Type.isUndefined(kvalue)) {
needDefaults = false;
}
}
}
/* step 6 */
if (needDefaults && ("date".equals(defaults) || "all".equals(defaults))) {
for (String prop : array("year", "month", "day")) {
CreateDataPropertyOrThrow(cx, options, prop, "numeric");
}
}
/* step 7 */
if (needDefaults && ("time".equals(defaults) || "all".equals(defaults))) {
for (String prop : array("hour", "minute", "second")) {
CreateDataPropertyOrThrow(cx, options, prop, "numeric");
}
}
/* step 8 */
return options;
}
private static final class BasicFormatPattern extends Lazy<String> {
private final FormatMatcherRecord record;
private final String dataLocale;
BasicFormatPattern(FormatMatcherRecord record, String dataLocale) {
this.record = record;
this.dataLocale = dataLocale;
}
@Override
protected String computeValue() {
return BasicFormatMatcher(record, dataLocale);
}
}
private static final class BestFitFormatPattern extends Lazy<String> {
private final FormatMatcherRecord record;
private final String dataLocale;
BestFitFormatPattern(FormatMatcherRecord record, String dataLocale) {
this.record = record;
this.dataLocale = dataLocale;
}
@Override
protected String computeValue() {
return BestFitFormatMatcher(record, dataLocale);
}
}
private static final class FormatMatcherRecord {
private final FieldWeight weekday;
private final FieldWeight era;
private final FieldWeight year;
private final FieldWeight month;
private final FieldWeight day;
private final FieldWeight hour;
private final FieldWeight minute;
private final FieldWeight second;
private final FieldWeight timeZoneName;
private final Boolean hour12;
FormatMatcherRecord(String weekday, String era, String year, String month, String day, String hour,
String minute, String second, String timeZoneName, Boolean hour12) {
this.weekday = FieldWeight.forName(weekday);
this.era = FieldWeight.forName(era);
this.year = FieldWeight.forName(year);
this.month = FieldWeight.forName(month);
this.day = FieldWeight.forName(day);
this.hour = FieldWeight.forName(hour);
this.minute = FieldWeight.forName(minute);
this.second = FieldWeight.forName(second);
this.timeZoneName = FieldWeight.forName(timeZoneName);
this.hour12 = hour12;
}
boolean isDate() {
return (year != null || month != null || day != null);
}
boolean isTime() {
return (hour != null || minute != null || second != null);
}
boolean isHour12(ULocale locale) {
if (hour12 != null) {
return hour12;
}
char hourFormat = defaultHourFormat(locale);
return (hourFormat == 'h' || hourFormat == 'K');
}
FieldWeight getWeight(DateField field) {
switch (field) {
case Era:
return era;
case Year:
return year;
case Quarter:
return null;
case Month:
return month;
case Week:
return null;
case Day:
return day;
case Weekday:
return weekday;
case Period:
return null;
case Hour:
return hour;
case Minute:
return minute;
case Second:
return second;
case Timezone:
return timeZoneName;
default:
throw new AssertionError();
}
}
String toSkeleton() {
StringBuilder sb = new StringBuilder();
DateField.Weekday.append(sb, weekday);
DateField.Era.append(sb, era);
DateField.Year.append(sb, year);
DateField.Month.append(sb, month);
DateField.Day.append(sb, day);
DateField.Hour.append(sb, hour, hour12);
DateField.Minute.append(sb, minute);
DateField.Second.append(sb, second);
DateField.Timezone.append(sb, timeZoneName);
return sb.toString();
}
}
/**
* 12.1.3 BasicFormatMatcher (options, formats)
*
* @param formatRecord
* the format matcher record
* @param dataLocale
* the locale
* @return the basic format matcher
*/
public static String BasicFormatMatcher(FormatMatcherRecord formatRecord, String dataLocale) {
ULocale locale = ULocale.forLanguageTag(dataLocale);
DateTimePatternGenerator generator = DateTimePatternGenerator.getInstance(locale);
// ICU4J only provides access to date- or time-only skeletons, with the exception of the
// weekday property, which may also appear in time-only skeletons or as a single skeleton
// property. That means we need to handle four different cases:
// 1) formatRecord contains only date properties
// 2) formatRecord contains only time properties
// 3) formatRecord contains date and time properties
// 4) formatRecord contains only the weekday property
boolean optDate = formatRecord.isDate();
boolean optTime = formatRecord.isTime();
boolean optDateTime = optDate && optTime;
// get the preferred hour representation (12-hour-cycle or 24-hour-cycle)
boolean optHour12 = formatRecord.isHour12(locale);
// handle date and time patterns separately
int bestDateScore = Integer.MIN_VALUE, bestTimeScore = Integer.MIN_VALUE;
String bestDateFormat = null, bestTimeFormat = null;
Map<String, String> skeletons = addCanonicalSkeletons(generator.getSkeletons(null));
for (Map.Entry<String, String> entry : skeletons.entrySet()) {
Skeleton skeleton = new Skeleton(entry.getKey());
// getSkeletons() does not return any date+time skeletons
assert !(skeleton.isDate() && skeleton.isTime());
// skip skeleton if it contains unsupported fields
if (skeleton.has(DateField.Quarter) || skeleton.has(DateField.Week)) {
continue;
}
if (skeleton.has(DateField.Year) && skeleton.getSymbol(DateField.Year) != 'y') {
continue;
}
if (skeleton.has(DateField.Day) && skeleton.getSymbol(DateField.Day) != 'd') {
continue;
}
if (skeleton.has(DateField.Second) && skeleton.getSymbol(DateField.Second) != 's') {
continue;
}
if (optDateTime) {
// skip time-skeletons with weekdays if date+time was requested, weekday gets into
// the date-skeleton part
if (skeleton.isTime() && skeleton.has(DateField.Weekday)) {
continue;
}
// skip time-skeleton if hour representation does not match requested value
if (skeleton.isTime() && skeleton.isHour12() != optHour12) {
continue;
}
if (skeleton.isDate()) {
int score = computeScore(formatRecord, skeleton);
if (score > bestDateScore) {
bestDateScore = score;
bestDateFormat = entry.getValue();
}
} else {
int score = computeScore(formatRecord, skeleton);
if (score > bestTimeScore) {
bestTimeScore = score;
bestTimeFormat = entry.getValue();
}
}
} else if (optDate) {
// skip time-skeletons if only date fields were requested
if (skeleton.isTime()) {
continue;
}
int score = computeScore(formatRecord, skeleton);
if (score > bestDateScore) {
bestDateScore = score;
bestDateFormat = entry.getValue();
}
} else if (optTime) {
// skip date-skeletons if only time fields were requested
if (skeleton.isDate()) {
continue;
}
// skip time-skeleton if hour representation does not match requested value
if (skeleton.isHour12() != optHour12) {
continue;
}
int score = computeScore(formatRecord, skeleton);
if (score > bestTimeScore) {
bestTimeScore = score;
bestTimeFormat = entry.getValue();
}
} else {
// weekday-only case
int score = computeScore(formatRecord, skeleton);
if (score > bestDateScore) {
bestDateScore = score;
bestDateFormat = entry.getValue();
}
}
}
assert !optDate || bestDateFormat != null;
assert !optTime || bestTimeFormat != null;
assert !(!optDate && !optTime) || bestDateFormat != null;
if (optDateTime) {
String dateTimeFormat = generator.getDateTimeFormat();
return MessageFormat.format(dateTimeFormat, bestTimeFormat, bestDateFormat);
}
if (optTime) {
return bestTimeFormat;
}
return bestDateFormat;
}
/**
* Retrieve the default hour format character for the supplied locale.
*
* @param locale
* the locale
* @return the hour format character
* @see <a href="http://bugs.icu-project.org/trac/ticket/9997">ICU bug 9997</a>
*/
private static char defaultHourFormat(ULocale locale) {
// use short time format, just as ICU4J does internally
final int style = DateFormat.SHORT;
SimpleDateFormat df = (SimpleDateFormat) DateFormat.getTimeInstance(style, locale);
String pattern = df.toPattern();
boolean quote = false;
for (int i = 0, len = pattern.length(); i < len; ++i) {
char c = pattern.charAt(i);
if (!quote && (c == 'h' || c == 'H' || c == 'k' || c == 'K')) {
return c;
} else if (c == '\'') {
quote = !quote;
}
}
return 'H';
}
/**
* Adds canonical skeleton/pattern pairs which might have been omitted in
* {@link DateTimePatternGenerator#getSkeletons(Map)}.
*
* @param skeletons
* the skeletons map
* @return the updated skeletons map
*/
private static Map<String, String> addCanonicalSkeletons(Map<String, String> skeletons) {
final String source = "GyQMwWEdDFHmsSv"; // see DateTimePatternGenerator#CANONICAL_ITEMS
for (int i = 0, len = source.length(); i < len; ++i) {
String k = String.valueOf(source.charAt(i));
if (!skeletons.containsKey(k)) {
skeletons.put(k, k);
}
}
return skeletons;
}
/**
* 12.1.3 BasicFormatMatcher (options, formats) (score computation)
*
* @param formatRecord
* the format matcher record
* @param skeleton
* the pattern skeleton
* @return the computed score
*/
private static int computeScore(FormatMatcherRecord formatRecord, Skeleton skeleton) {
/* step 12.b */
int score = 0;
/* step 12.c */
score -= computePenalty(formatRecord, skeleton, DateField.Weekday).value();
score -= computePenalty(formatRecord, skeleton, DateField.Era).value();
score -= computePenalty(formatRecord, skeleton, DateField.Year).value();
score -= computePenalty(formatRecord, skeleton, DateField.Month).value();
score -= computePenalty(formatRecord, skeleton, DateField.Day).value();
score -= computePenalty(formatRecord, skeleton, DateField.Hour).value();
score -= computePenalty(formatRecord, skeleton, DateField.Minute).value();
score -= computePenalty(formatRecord, skeleton, DateField.Second).value();
score -= computePenalty(formatRecord, skeleton, DateField.Timezone).value();
return score;
}
private enum Penalty {
Removal(120), Addition(20), LongLess(8), LongMore(6), ShortLess(6), ShortMore(3), None(0);
private final int value;
private Penalty(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
/**
* 12.1.3 BasicFormatMatcher (options, formats) (penalty computation)
*
* @param formatRecord
* the format matcher record
* @param skeleton
* the pattern skeleton
* @param field
* the date field
* @return the computed penalty
*/
private static Penalty computePenalty(FormatMatcherRecord formatRecord, Skeleton skeleton, DateField field) {
FieldWeight optionsProp = formatRecord.getWeight(field);
FieldWeight formatProp = skeleton.getWeight(field);
/* step 12.c.iii */
if (optionsProp == null && formatProp != null) {
return Penalty.Addition;
}
/* step 12.c.iv */
if (optionsProp != null && formatProp == null) {
return Penalty.Removal;
}
/* step 12.c.v */
if (optionsProp != formatProp) {
int optionsPropIndex = optionsProp.index();
int formatPropIndex = formatProp.index();
int delta = Math.max(Math.min(formatPropIndex - optionsPropIndex, 2), -2);
if (delta == 2) {
return Penalty.LongMore;
} else if (delta == 1) {
return Penalty.ShortMore;
} else if (delta == -1) {
return Penalty.ShortLess;
} else if (delta == -2) {
return Penalty.LongLess;
}
}
return Penalty.None;
}
/**
* 12.1.4 BestFitFormatMatcher (options, formats)
*
* @param formatRecord
* the format matcher record
* @param dataLocale
* the locale
* @return the best applicable pattern
*/
public static String BestFitFormatMatcher(FormatMatcherRecord formatRecord, String dataLocale) {
// Let ICU4J compute the best applicable pattern for the requested input values
ULocale locale = ULocale.forLanguageTag(dataLocale);
DateTimePatternGenerator generator = DateTimePatternGenerator.getInstance(locale);
return generator.getBestPattern(formatRecord.toSkeleton());
}
/**
* 12.1.6 FormatDateTime (dateTimeFormat, x)
*
* @param cx
* the execution context
* @param dateTimeFormat
* the date format object
* @param x
* the number value
* @return the formatted date-time string
*/
public static String FormatDateTime(ExecutionContext cx, DateTimeFormatObject dateTimeFormat, double x) {
/* step 1 */
if (Double.isInfinite(x) || Double.isNaN(x)) {
throw newRangeError(cx, Messages.Key.InvalidDateValue);
}
/* steps 2-15 */
return dateTimeFormat.getDateFormat().format(new Date((long) x));
}
/**
* CreateDateTimeParts(dateTimeFormat, x)
*
* @param dateTimeFormat
* the date format object
* @param date
* the date object
* @return the formatted date-time object
*/
private static List<Map.Entry<String, String>> CreateDateTimeParts(DateTimeFormatObject dateTimeFormat, Date date) {
ArrayList<Map.Entry<String, String>> parts = new ArrayList<>();
DateFormat dateFormat = dateTimeFormat.getDateFormat();
AttributedCharacterIterator iterator = dateFormat.formatToCharacterIterator(date);
StringBuilder sb = new StringBuilder();
for (char ch = iterator.first(); ch != CharacterIterator.DONE; ch = iterator.next()) {
sb.append(ch);
if (iterator.getIndex() + 1 == iterator.getRunLimit()) {
Iterator<Attribute> keyIterator = iterator.getAttributes().keySet().iterator();
String key;
if (keyIterator.hasNext()) {
key = fieldToString((DateFormat.Field) keyIterator.next());
} else {
key = "separator";
}
String value = sb.toString();
sb.setLength(0);
parts.add(new AbstractMap.SimpleImmutableEntry<>(key, value));
}
}
return parts;
}
private static String fieldToString(DateFormat.Field field) {
if (field == DateFormat.Field.DAY_OF_WEEK) {
return "weekday";
}
if (field == DateFormat.Field.ERA) {
return "era";
}
if (field == DateFormat.Field.YEAR) {
return "year";
}
if (field == DateFormat.Field.MONTH) {
return "month";
}
if (field == DateFormat.Field.DAY_OF_MONTH) {
return "day";
}
if (field == DateFormat.Field.HOUR0) {
return "hour";
}
if (field == DateFormat.Field.HOUR1) {
return "hour";
}
if (field == DateFormat.Field.HOUR_OF_DAY0) {
return "hour";
}
if (field == DateFormat.Field.HOUR_OF_DAY1) {
return "hour";
}
if (field == DateFormat.Field.MINUTE) {
return "minute";
}
if (field == DateFormat.Field.SECOND) {
return "second";
}
if (field == DateFormat.Field.TIME_ZONE) {
return "timeZoneName";
}
if (field == DateFormat.Field.AM_PM) {
// FIXME: spec issue - rename to "dayPeriod" for consistency with "timeZoneName"?
return "dayperiod";
}
// Report unsupported/unexpected date fields as separators.
return "separator";
}
/**
* FormatToPartDateTime(dateTimeFormat, x)
*
* @param cx
* the execution context
* @param dateTimeFormat
* the date format object
* @param x
* the number value
* @return the formatted date-time object
*/
public static ArrayObject FormatToPartDateTime(ExecutionContext cx, DateTimeFormatObject dateTimeFormat, double x) {
if (Double.isInfinite(x) || Double.isNaN(x)) {
throw newRangeError(cx, Messages.Key.InvalidDateValue);
}
/* step 1 */
List<Map.Entry<String, String>> parts = CreateDateTimeParts(dateTimeFormat, new Date((long) x));
/* step 2 */
ArrayObject result = ArrayCreate(cx, 0);
/* step 3 */
int n = 0;
/* step 4 */
for (Map.Entry<String, String> part : parts) {
/* step 4.a */
OrdinaryObject o = ObjectCreate(cx, Intrinsics.ObjectPrototype);
/* steps 4.b-c */
CreateDataProperty(cx, o, "type", part.getKey());
/* steps 4.d-e */
CreateDataProperty(cx, o, "value", part.getValue());
/* steps 4.f-g */
CreateDataProperty(cx, result, n++, o);
}
/* step 5 */
return result;
}
/**
* 12.1.5 DateTime Format Functions
*/
public static final class FormatFunction extends BuiltinFunction {
public FormatFunction(Realm realm) {
super(realm, "format", 1);
createDefaultFunctionProperties();
}
private FormatFunction(Realm realm, Void ignore) {
super(realm, "format", 1);
}
@Override
public FormatFunction clone() {
return new FormatFunction(getRealm(), null);
}
@Override
public String call(ExecutionContext callerContext, Object thisValue, Object... args) {
ExecutionContext calleeContext = calleeContext();
Object date = argument(args, 0);
/* steps 1-2 */
assert thisValue instanceof DateTimeFormatObject;
DateTimeFormatObject dtf = (DateTimeFormatObject) thisValue;
/* steps 3-4 */
double x;
if (Type.isUndefined(date)) {
/* step 3 */
x = System.currentTimeMillis();
} else {
/* step 4 */
x = ToNumber(calleeContext, date);
}
/* step 5 */
return FormatDateTime(calleeContext, dtf, x);
}
}
/**
* DateTime FormatToParts Functions
*/
public static final class FormatToPartsFunction extends BuiltinFunction {
public FormatToPartsFunction(Realm realm) {
super(realm, "formatToParts", 1);
createDefaultFunctionProperties();
}
private FormatToPartsFunction(Realm realm, Void ignore) {
super(realm, "formatToParts", 1);
}
@Override
public FormatToPartsFunction clone() {
return new FormatToPartsFunction(getRealm(), null);
}
@Override
public ArrayObject call(ExecutionContext callerContext, Object thisValue, Object... args) {
ExecutionContext calleeContext = calleeContext();
Object date = argument(args, 0);
/* steps 1-2 */
assert thisValue instanceof DateTimeFormatObject;
DateTimeFormatObject dtf = (DateTimeFormatObject) thisValue;
/* steps 3-4 */
double x;
if (Type.isUndefined(date)) {
/* step 3 */
x = System.currentTimeMillis();
} else {
/* step 4 */
x = ToNumber(calleeContext, date);
}
/* step 5 */
return FormatToPartDateTime(calleeContext, dtf, x);
}
}
/**
* 12.2.1 Intl.DateTimeFormat([ locales [, options ]])
*/
@Override
public ScriptObject call(ExecutionContext callerContext, Object thisValue, Object... args) {
/* steps 1-3 */
return construct(callerContext, this, args);
}
/**
* 12.2.1 Intl.DateTimeFormat([ locales [, options ]])
*/
@Override
public DateTimeFormatObject construct(ExecutionContext callerContext, Constructor newTarget, Object... args) {
ExecutionContext calleeContext = calleeContext();
Object locales = argument(args, 0);
Object options = argument(args, 1);
/* step 1 (not applicable) */
/* step 2 */
DateTimeFormatObject obj = OrdinaryCreateFromConstructor(calleeContext, newTarget,
Intrinsics.Intl_DateTimeFormatPrototype, DateTimeFormatObject::new);
/* step 3 */
InitializeDateTimeFormat(calleeContext, obj, locales, options);
return obj;
}
/**
* 12.3 Properties of the Intl.DateTimeFormat Constructor
*/
public enum Properties {
;
@Prototype
public static final Intrinsics __proto__ = Intrinsics.FunctionPrototype;
@Value(name = "length", attributes = @Attributes(writable = false, enumerable = false, configurable = true))
public static final int length = 0;
@Value(name = "name", attributes = @Attributes(writable = false, enumerable = false, configurable = true))
public static final String name = "DateTimeFormat";
/**
* 12.3.1 Intl.DateTimeFormat.prototype
*/
@Value(name = "prototype",
attributes = @Attributes(writable = false, enumerable = false, configurable = false))
public static final Intrinsics prototype = Intrinsics.Intl_DateTimeFormatPrototype;
/**
* 12.3.2 Intl.DateTimeFormat.supportedLocalesOf (locales [, options])
*
* @param cx
* the execution context
* @param thisValue
* the function this-value
* @param locales
* the locales array
* @param options
* the options object
* @return the array of supported locales
*/
@Function(name = "supportedLocalesOf", arity = 1)
public static Object supportedLocalesOf(ExecutionContext cx, Object thisValue, Object locales, Object options) {
/* step 1 */
Set<String> availableLocales = getAvailableLocales(cx);
/* step 2 */
Set<String> requestedLocales = CanonicalizeLocaleList(cx, locales);
/* step 3 */
return SupportedLocales(cx, availableLocales, requestedLocales, options);
}
}
}