diff --git a/src/js/intl.js b/src/js/intl.js index 6c77fd8839..187a4dbcc1 100644 --- a/src/js/intl.js +++ b/src/js/intl.js @@ -125,42 +125,6 @@ function GetServiceRE() { return SERVICE_RE; } -/** - * Matches valid IANA time zone names. - */ -var TIMEZONE_NAME_CHECK_RE = UNDEFINED; -var GMT_OFFSET_TIMEZONE_NAME_CHECK_RE = UNDEFINED; - -function GetTimezoneNameCheckRE() { - if (IS_UNDEFINED(TIMEZONE_NAME_CHECK_RE)) { - TIMEZONE_NAME_CHECK_RE = new GlobalRegExp( - '^([A-Za-z]+)/([A-Za-z_-]+)((?:\/[A-Za-z_-]+)+)*$'); - } - return TIMEZONE_NAME_CHECK_RE; -} - -function GetGMTOffsetTimezoneNameCheckRE() { - if (IS_UNDEFINED(GMT_OFFSET_TIMEZONE_NAME_CHECK_RE)) { - GMT_OFFSET_TIMEZONE_NAME_CHECK_RE = new GlobalRegExp( - '^(?:ETC/GMT)(?0|[+-](?:[0-9]|1[0-4]))$'); - } - return GMT_OFFSET_TIMEZONE_NAME_CHECK_RE; -} - -/** - * Matches valid location parts of IANA time zone names. - */ -var TIMEZONE_NAME_LOCATION_PART_RE = UNDEFINED; - -function GetTimezoneNameLocationPartRE() { - if (IS_UNDEFINED(TIMEZONE_NAME_LOCATION_PART_RE)) { - TIMEZONE_NAME_LOCATION_PART_RE = - new GlobalRegExp('^([A-Za-z]+)((?:[_-][A-Za-z]+)+)*$'); - } - return TIMEZONE_NAME_LOCATION_PART_RE; -} - - /** * Returns a getOption function that extracts property value for given * options object. If property is missing it returns defaultValue. If value @@ -468,43 +432,6 @@ function defineWECProperty(object, property, value) { configurable: true}); } -/** - * Returns titlecased word, aMeRricA -> America. - */ -function toTitleCaseWord(word) { - return %StringToUpperCaseIntl(%_Call(StringSubstr, word, 0, 1)) + - %StringToLowerCaseIntl(%_Call(StringSubstr, word, 1)); -} - -/** - * Returns titlecased location, bueNos_airES -> Buenos_Aires - * or ho_cHi_minH -> Ho_Chi_Minh. It is locale-agnostic and only - * deals with ASCII only characters. - * 'of', 'au' and 'es' are special-cased and lowercased. - */ -function toTitleCaseTimezoneLocation(location) { - var match = %regexp_internal_match(GetTimezoneNameLocationPartRE(), location) - if (IS_NULL(match)) throw %make_range_error(kExpectedLocation, location); - - var result = toTitleCaseWord(match[1]); - if (!IS_UNDEFINED(match[2]) && 2 < match.length) { - // The first character is a separator, '_' or '-'. - // None of IANA zone names has both '_' and '-'. - var separator = %_Call(StringSubstring, match[2], 0, 1); - var parts = %StringSplit(match[2], separator, kMaxUint32); - for (var i = 1; i < parts.length; i++) { - var part = parts[i] - var lowercasedPart = %StringToLowerCaseIntl(part); - result = result + separator + - ((lowercasedPart !== 'es' && - lowercasedPart !== 'of' && lowercasedPart !== 'au') ? - toTitleCaseWord(part) : lowercasedPart); - } - } - return result; -} - - /** * Returns an InternalArray where all locales are canonicalized and duplicates * removed. @@ -594,69 +521,6 @@ DEFINE_METHOD( } ); - -/** - * Returns a string that matches LDML representation of the options object. - */ -function toLDMLString(options) { - var getOption = getGetOption(options, 'dateformat'); - - var ldmlString = ''; - - var option = getOption('weekday', 'string', ['narrow', 'short', 'long']); - ldmlString += appendToLDMLString( - option, {narrow: 'EEEEE', short: 'EEE', long: 'EEEE'}); - - option = getOption('era', 'string', ['narrow', 'short', 'long']); - ldmlString += appendToLDMLString( - option, {narrow: 'GGGGG', short: 'GGG', long: 'GGGG'}); - - option = getOption('year', 'string', ['2-digit', 'numeric']); - ldmlString += appendToLDMLString(option, {'2-digit': 'yy', 'numeric': 'y'}); - - option = getOption('month', 'string', - ['2-digit', 'numeric', 'narrow', 'short', 'long']); - ldmlString += appendToLDMLString(option, {'2-digit': 'MM', 'numeric': 'M', - 'narrow': 'MMMMM', 'short': 'MMM', 'long': 'MMMM'}); - - option = getOption('day', 'string', ['2-digit', 'numeric']); - ldmlString += appendToLDMLString( - option, {'2-digit': 'dd', 'numeric': 'd'}); - - var hr12 = getOption('hour12', 'boolean'); - option = getOption('hour', 'string', ['2-digit', 'numeric']); - if (IS_UNDEFINED(hr12)) { - ldmlString += appendToLDMLString(option, {'2-digit': 'jj', 'numeric': 'j'}); - } else if (hr12 === true) { - ldmlString += appendToLDMLString(option, {'2-digit': 'hh', 'numeric': 'h'}); - } else { - ldmlString += appendToLDMLString(option, {'2-digit': 'HH', 'numeric': 'H'}); - } - - option = getOption('minute', 'string', ['2-digit', 'numeric']); - ldmlString += appendToLDMLString(option, {'2-digit': 'mm', 'numeric': 'm'}); - - option = getOption('second', 'string', ['2-digit', 'numeric']); - ldmlString += appendToLDMLString(option, {'2-digit': 'ss', 'numeric': 's'}); - - option = getOption('timeZoneName', 'string', ['short', 'long']); - ldmlString += appendToLDMLString(option, {short: 'z', long: 'zzzz'}); - - return ldmlString; -} - - -/** - * Returns either LDML equivalent of the current option or empty string. - */ -function appendToLDMLString(option, pairs) { - if (!IS_UNDEFINED(option)) { - return pairs[option]; - } else { - return ''; - } -} - /** * Initializes the given object so it's a valid DateTimeFormat instance. * Useful for subclassing. @@ -677,14 +541,6 @@ function CreateDateTimeFormat(locales, options) { var matcher = getOption('formatMatcher', 'string', ['basic', 'best fit'], 'best fit'); - // Build LDML string for the skeleton that we pass to the formatter. - var ldmlString = toLDMLString(options); - - // Filter out supported extension keys so we know what to put in resolved - // section later on. - // We need to pass calendar and number system to the method. - var tz = canonicalizeTimeZoneID(options.timeZone); - // ICU prefers options to be passed using -u- extension key/values, so // we need to build that. var internalOptions = {__proto__: null}; @@ -708,9 +564,7 @@ function CreateDateTimeFormat(locales, options) { // to JSDateTimeFormat var resolved = {__proto__: null}; - var dateFormat = %CreateDateTimeFormat( - requestedLocale, - {__proto__: null, skeleton: ldmlString, timeZone: tz}, resolved); + var dateFormat = %CreateDateTimeFormat(requestedLocale, options, resolved); %MarkAsInitializedIntlObjectOfType(dateFormat, DATE_TIME_FORMAT_TYPE); @@ -744,58 +598,6 @@ DEFINE_METHOD( } ); - -/** - * Returns canonical Area/Location(/Location) name, or throws an exception - * if the zone name is invalid IANA name. - */ -function canonicalizeTimeZoneID(tzID) { - // Skip undefined zones. - if (IS_UNDEFINED(tzID)) { - return tzID; - } - - // Convert zone name to string. - tzID = TO_STRING(tzID); - - // Special case handling (UTC, GMT). - var upperID = %StringToUpperCaseIntl(tzID); - if (upperID === 'UTC' || upperID === 'GMT' || - upperID === 'ETC/UTC' || upperID === 'ETC/GMT') { - return 'UTC'; - } - - // We expect only _, '-' and / beside ASCII letters. - // All inputs should conform to Area/Location(/Location)*, or Etc/GMT* . - // TODO(jshin): 1. Support 'GB-Eire", 'EST5EDT", "ROK', 'US/*', 'NZ' and many - // other aliases/linked names when moving timezone validation code to C++. - // See crbug.com/364374 and crbug.com/v8/8007 . - // 2. Resolve the difference betwee CLDR/ICU and IANA time zone db. - // See http://unicode.org/cldr/trac/ticket/9892 and crbug.com/645807 . - let match = %regexp_internal_match(GetTimezoneNameCheckRE(), tzID); - if (IS_NULL(match)) { - let match = - %regexp_internal_match(GetGMTOffsetTimezoneNameCheckRE(), upperID); - if (!IS_NULL(match) && match.length == 2) - return "Etc/GMT" + match.groups.offset; - else - throw %make_range_error(kInvalidTimeZone, tzID); - } - - let result = toTitleCaseTimezoneLocation(match[1]) + '/' + - toTitleCaseTimezoneLocation(match[2]); - - if (!IS_UNDEFINED(match[3]) && 3 < match.length) { - let locations = %StringSplit(match[3], '/', kMaxUint32); - // The 1st element is empty. Starts with i=1. - for (var i = 1; i < locations.length; i++) { - result = result + '/' + toTitleCaseTimezoneLocation(locations[i]); - } - } - - return result; -} - /** * Initializes the given object so it's a valid BreakIterator instance. * Useful for subclassing. diff --git a/src/objects/intl-objects.cc b/src/objects/intl-objects.cc index 3dd7b3ef73..aa441c9a6c 100644 --- a/src/objects/intl-objects.cc +++ b/src/objects/intl-objects.cc @@ -21,6 +21,7 @@ #include "src/isolate.h" #include "src/objects-inl.h" #include "src/objects/js-collator-inl.h" +#include "src/objects/js-date-time-format-inl.h" #include "src/objects/js-number-format-inl.h" #include "src/objects/managed.h" #include "src/objects/string.h" @@ -89,69 +90,90 @@ bool ExtractStringSetting(Isolate* isolate, Handle options, return false; } -icu::SimpleDateFormat* CreateICUDateFormat(Isolate* isolate, - const icu::Locale& icu_locale, - Handle options) { +// ecma-402/#sec-isvalidtimezonename +bool IsValidTimeZoneName(const icu::TimeZone& tz) { + UErrorCode status = U_ZERO_ERROR; + icu::UnicodeString id; + tz.getID(id); + icu::UnicodeString canonical; + icu::TimeZone::getCanonicalID(id, canonical, status); + return U_SUCCESS(status) && + canonical != icu::UnicodeString("Etc/Unknown", -1, US_INV); +} + +std::unique_ptr CreateTimeZone(Isolate* isolate, + const char* timezone) { // Create time zone as specified by the user. We have to re-create time zone // since calendar takes ownership. - icu::TimeZone* tz = nullptr; - icu::UnicodeString timezone; - if (ExtractStringSetting(isolate, options, "timeZone", &timezone)) { - tz = icu::TimeZone::createTimeZone(timezone); - } else { - tz = icu::TimeZone::createDefault(); + if (timezone == nullptr) { + return std::unique_ptr(icu::TimeZone::createDefault()); } + std::string canonicalized = + JSDateTimeFormat::CanonicalizeTimeZoneID(isolate, timezone); + if (canonicalized.empty()) return std::unique_ptr(); + std::unique_ptr tz( + icu::TimeZone::createTimeZone(canonicalized.c_str())); + if (!IsValidTimeZoneName(*tz)) return std::unique_ptr(); + return tz; +} + +std::unique_ptr CreateCalendar(Isolate* isolate, + const icu::Locale& icu_locale, + const char* timezone) { + std::unique_ptr tz = CreateTimeZone(isolate, timezone); + if (tz.get() == nullptr) return std::unique_ptr(); // Create a calendar using locale, and apply time zone to it. UErrorCode status = U_ZERO_ERROR; - icu::Calendar* calendar = - icu::Calendar::createInstance(tz, icu_locale, status); + std::unique_ptr calendar( + icu::Calendar::createInstance(tz.release(), icu_locale, status)); + CHECK(U_SUCCESS(status)); + CHECK_NOT_NULL(calendar.get()); if (calendar->getDynamicClassID() == icu::GregorianCalendar::getStaticClassID()) { - icu::GregorianCalendar* gc = (icu::GregorianCalendar*)calendar; + icu::GregorianCalendar* gc = + static_cast(calendar.get()); UErrorCode status = U_ZERO_ERROR; // The beginning of ECMAScript time, namely -(2**53) const double start_of_time = -9007199254740992; gc->setGregorianChange(start_of_time, status); DCHECK(U_SUCCESS(status)); } + return calendar; +} + +std::unique_ptr CreateICUDateFormat( + Isolate* isolate, const icu::Locale& icu_locale, + const std::string skeleton) { + // See https://github.com/tc39/ecma402/issues/225 . The best pattern + // generation needs to be done in the base locale according to the + // current spec however odd it may be. See also crbug.com/826549 . + // This is a temporary work-around to get v8's external behavior to match + // the current spec, but does not follow the spec provisions mentioned + // in the above Ecma 402 issue. + // TODO(jshin): The spec may need to be revised because using the base + // locale for the pattern match is not quite right. Moreover, what to + // do with 'related year' part when 'chinese/dangi' calendar is specified + // has to be discussed. Revisit once the spec is clarified/revised. + icu::Locale no_extension_locale(icu_locale.getBaseName()); + UErrorCode status = U_ZERO_ERROR; + std::unique_ptr generator( + icu::DateTimePatternGenerator::createInstance(no_extension_locale, + status)); + icu::UnicodeString pattern; + if (U_SUCCESS(status)) { + pattern = + generator->getBestPattern(icu::UnicodeString(skeleton.c_str()), status); + } // Make formatter from skeleton. Calendar and numbering system are added // to the locale as Unicode extension (if they were specified at all). - icu::SimpleDateFormat* date_format = nullptr; - icu::UnicodeString skeleton; - if (ExtractStringSetting(isolate, options, "skeleton", &skeleton)) { - // See https://github.com/tc39/ecma402/issues/225 . The best pattern - // generation needs to be done in the base locale according to the - // current spec however odd it may be. See also crbug.com/826549 . - // This is a temporary work-around to get v8's external behavior to match - // the current spec, but does not follow the spec provisions mentioned - // in the above Ecma 402 issue. - // TODO(jshin): The spec may need to be revised because using the base - // locale for the pattern match is not quite right. Moreover, what to - // do with 'related year' part when 'chinese/dangi' calendar is specified - // has to be discussed. Revisit once the spec is clarified/revised. - icu::Locale no_extension_locale(icu_locale.getBaseName()); - std::unique_ptr generator( - icu::DateTimePatternGenerator::createInstance(no_extension_locale, - status)); - icu::UnicodeString pattern; - if (U_SUCCESS(status)) - pattern = generator->getBestPattern(skeleton, status); - - date_format = new icu::SimpleDateFormat(pattern, icu_locale, status); - if (U_SUCCESS(status)) { - date_format->adoptCalendar(calendar); - } - } - - if (U_FAILURE(status)) { - delete calendar; - delete date_format; - date_format = nullptr; - } + std::unique_ptr date_format( + new icu::SimpleDateFormat(pattern, icu_locale, status)); + if (U_FAILURE(status)) return std::unique_ptr(); + CHECK_NOT_NULL(date_format.get()); return date_format; } @@ -303,48 +325,49 @@ icu::Locale Intl::CreateICULocale(Isolate* isolate, return icu_locale; } -bool DateFormat::IsValidTimeZone(icu::SimpleDateFormat* date_format) { - UErrorCode status = U_ZERO_ERROR; - // Set time zone and calendar. - const icu::Calendar* calendar = date_format->getCalendar(); - const icu::TimeZone& tz = calendar->getTimeZone(); - icu::UnicodeString time_zone; - tz.getID(time_zone); - icu::UnicodeString canonical_time_zone; - icu::TimeZone::getCanonicalID(time_zone, canonical_time_zone, status); - std::string timezone_str; - canonical_time_zone.toUTF8String(timezone_str); - if (U_SUCCESS(status)) return timezone_str != "Etc/Unknown"; - return true; -} - // static -icu::SimpleDateFormat* DateFormat::InitializeDateTimeFormat( +Maybe DateFormat::InitializeDateTimeFormat( Isolate* isolate, Handle locale, Handle options, Handle resolved) { icu::Locale icu_locale = Intl::CreateICULocale(isolate, locale); DCHECK(!icu_locale.isBogus()); - icu::SimpleDateFormat* date_format = - CreateICUDateFormat(isolate, icu_locale, options); - if (!date_format) { - // Remove extensions and try again. - icu::Locale no_extension_locale(icu_locale.getBaseName()); - date_format = CreateICUDateFormat(isolate, no_extension_locale, options); + static std::vector empty_values = {}; + std::unique_ptr timezone = nullptr; + Maybe maybe_timezone = + Intl::GetStringOption(isolate, options, "timeZone", empty_values, + "Intl.DateTimeFormat", &timezone); + MAYBE_RETURN(maybe_timezone, Nothing()); - if (!date_format) { + Maybe maybe_skeleton = + JSDateTimeFormat::OptionsToSkeleton(isolate, options); + MAYBE_RETURN(maybe_skeleton, Nothing()); + CHECK(!maybe_skeleton.FromJust().empty()); + std::string skeleton = maybe_skeleton.FromJust(); + + std::unique_ptr calendar( + CreateCalendar(isolate, icu_locale, timezone.get())); + if (calendar.get() == nullptr) { + THROW_NEW_ERROR_RETURN_VALUE( + isolate, + NewRangeError( + MessageTemplate::kInvalidTimeZone, + isolate->factory()->NewStringFromAsciiChecked(timezone.get())), + Nothing()); + } + std::unique_ptr date_format( + CreateICUDateFormat(isolate, icu_locale, skeleton)); + if (date_format.get() == nullptr) { + // Remove extensions and try again. + icu_locale = icu::Locale(icu_locale.getBaseName()); + date_format = CreateICUDateFormat(isolate, icu_locale, skeleton); + if (date_format.get() == nullptr) { FATAL("Failed to create ICU date format, are ICU data files missing?"); } - - // Set resolved settings (pattern, numbering system, calendar). - SetResolvedDateSettings(isolate, no_extension_locale, date_format, - resolved); - } else { - SetResolvedDateSettings(isolate, icu_locale, date_format, resolved); } - - CHECK_NOT_NULL(date_format); - return date_format; + date_format->adoptCalendar(calendar.release()); + SetResolvedDateSettings(isolate, icu_locale, date_format.get(), resolved); + return Just(date_format.release()); } icu::SimpleDateFormat* DateFormat::UnpackDateFormat(Handle obj) { diff --git a/src/objects/intl-objects.h b/src/objects/intl-objects.h index adfed7915c..70b2c6e97c 100644 --- a/src/objects/intl-objects.h +++ b/src/objects/intl-objects.h @@ -21,10 +21,7 @@ namespace U_ICU_NAMESPACE { class BreakIterator; -class Collator; class DecimalFormat; -class NumberFormat; -class PluralRules; class SimpleDateFormat; class UnicodeString; } @@ -39,16 +36,13 @@ class DateFormat { public: // Create a formatter for the specificied locale and options. Returns the // resolved settings for the locale / options. - static icu::SimpleDateFormat* InitializeDateTimeFormat( + static Maybe InitializeDateTimeFormat( Isolate* isolate, Handle locale, Handle options, Handle resolved); // Unpacks date format object from corresponding JavaScript object. static icu::SimpleDateFormat* UnpackDateFormat(Handle obj); - // Determine the TimeZone is valid. - static bool IsValidTimeZone(icu::SimpleDateFormat* date_format); - // Release memory we allocated for the DateFormat once the JS object that // holds the pointer gets garbage collected. static void DeleteDateFormat(const v8::WeakCallbackInfo& data); diff --git a/src/objects/js-collator.h b/src/objects/js-collator.h index 3ef21cadd8..94cd3162ad 100644 --- a/src/objects/js-collator.h +++ b/src/objects/js-collator.h @@ -18,6 +18,10 @@ // Has to be the last include (doesn't have include guards): #include "src/objects/object-macros.h" +namespace U_ICU_NAMESPACE { +class Collator; +} // namespace U_ICU_NAMESPACE + namespace v8 { namespace internal { diff --git a/src/objects/js-date-time-format.cc b/src/objects/js-date-time-format.cc index b50aa371c2..f5ea54678c 100644 --- a/src/objects/js-date-time-format.cc +++ b/src/objects/js-date-time-format.cc @@ -95,19 +95,76 @@ static const std::vector& GetPatternItems() { return kPatternItems; } +class PatternData { + public: + PatternData(const std::string property, std::vector pairs, + std::vector* allowed_values) + : property(property), allowed_values(allowed_values) { + for (const auto& pair : pairs) { + map.insert(std::make_pair(pair.value, pair.pattern)); + } + } + virtual ~PatternData() {} + + const std::string property; + std::map map; + std::vector* allowed_values; +}; + +enum HourOption { + H_UNKNOWN, + H_12, + H_24, +}; + +static const std::vector CreateCommonData() { + std::vector build; + for (const auto& item : GetPatternItems()) { + if (item.property != "hour") { + build.push_back( + PatternData(item.property, item.pairs, item.allowed_values)); + } + } + return build; +} + +static const std::vector CreateData(const char* digit2, + const char* numeric) { + static std::vector k2DigitNumeric = {"2-digit", "numeric"}; + static const std::vector common = CreateCommonData(); + std::vector build(common); + build.push_back(PatternData( + "hour", {{digit2, "2-digit"}, {numeric, "numeric"}}, &k2DigitNumeric)); + return build; +} + +static const std::vector& GetPatternData(HourOption option) { + static const std::vector data = CreateData("jj", "j"); + static const std::vector data_h12 = CreateData("hh", "h"); + static const std::vector data_h24 = CreateData("HH", "H"); + switch (option) { + case HourOption::H_12: + return data_h12; + case HourOption::H_24: + return data_h24; + case HourOption::H_UNKNOWN: + return data; + } +} + void SetPropertyFromPattern(Isolate* isolate, const std::string& pattern, Handle options) { Factory* factory = isolate->factory(); const std::vector& items = GetPatternItems(); - for (auto item = items.cbegin(); item != items.cend(); ++item) { - for (auto pair = item->pairs.cbegin(); pair != item->pairs.cend(); ++pair) { - if (pattern.find(pair->pattern) != std::string::npos) { + for (const auto& item : items) { + for (const auto& pair : item.pairs) { + if (pattern.find(pair.pattern) != std::string::npos) { // After we find the first pair in the item which matching the pattern, // we set the property and look for the next item in kPatternItems. CHECK(JSReceiver::CreateDataProperty( isolate, options, - factory->NewStringFromAsciiChecked(item->property.c_str()), - factory->NewStringFromAsciiChecked(pair->value.c_str()), + factory->NewStringFromAsciiChecked(item.property.c_str()), + factory->NewStringFromAsciiChecked(pair.value.c_str()), kDontThrow) .FromJust()); break; @@ -133,8 +190,103 @@ void SetPropertyFromPattern(Isolate* isolate, const std::string& pattern, } } +std::string GetGMTTzID(Isolate* isolate, const std::string& input) { + std::string ret = "Etc/GMT"; + switch (input.length()) { + case 8: + if (input[7] == '0') return ret + '0'; + break; + case 9: + if ((input[7] == '+' || input[7] == '-') && + IsInRange(input[8], '0', '9')) { + return ret + input[7] + input[8]; + } + break; + case 10: + if ((input[7] == '+' || input[7] == '-') && (input[8] == '1') && + IsInRange(input[9], '0', '4')) { + return ret + input[7] + input[8] + input[9]; + } + break; + } + return ""; +} + +// Locale independenty version of isalpha for ascii range. This will return +// false if the ch is alpha but not in ascii range. +bool IsAsciiAlpha(char ch) { + return IsInRange(ch, 'A', 'Z') || IsInRange(ch, 'a', 'z'); +} + +// Locale independent toupper for ascii range. This will not return İ (dotted I) +// for i under Turkish locale while std::toupper may. +char LocaleIndependentAsciiToUpper(char ch) { + return (IsInRange(ch, 'a', 'z')) ? (ch - 'a' + 'A') : ch; +} + +// Locale independent tolower for ascii range. +char LocaleIndependentAsciiToLower(char ch) { + return (IsInRange(ch, 'A', 'Z')) ? (ch - 'A' + 'a') : ch; +} + +// Returns titlecased location, bueNos_airES -> Buenos_Aires +// or ho_cHi_minH -> Ho_Chi_Minh. It is locale-agnostic and only +// deals with ASCII only characters. +// 'of', 'au' and 'es' are special-cased and lowercased. +// ICU's timezone parsing is case sensitive, but ECMAScript is case insensitive +std::string ToTitleCaseTimezoneLocation(Isolate* isolate, + const std::string& input) { + std::string title_cased; + int word_length = 0; + for (char ch : input) { + // Convert first char to upper case, the rest to lower case + if (IsAsciiAlpha(ch)) { + title_cased += word_length == 0 ? LocaleIndependentAsciiToUpper(ch) + : LocaleIndependentAsciiToLower(ch); + word_length++; + } else if (ch == '_' || ch == '-' || ch == '/') { + // Special case Au/Es/Of to be lower case. + if (word_length == 2) { + size_t pos = title_cased.length() - 2; + std::string substr = title_cased.substr(pos, 2); + if (substr == "Of" || substr == "Es" || substr == "Au") { + title_cased[pos] = LocaleIndependentAsciiToLower(title_cased[pos]); + } + } + title_cased += ch; + word_length = 0; + } else { + // Invalid input + return std::string(); + } + } + return title_cased; +} + } // namespace +std::string JSDateTimeFormat::CanonicalizeTimeZoneID(Isolate* isolate, + const std::string& input) { + std::string upper = input; + transform(upper.begin(), upper.end(), upper.begin(), + LocaleIndependentAsciiToUpper); + if (upper == "UTC" || upper == "GMT" || upper == "ETC/UTC" || + upper == "ETC/GMT") { + return "UTC"; + } + // We expect only _, '-' and / beside ASCII letters. + // All inputs should conform to Area/Location(/Location)*, or Etc/GMT* . + // TODO(jshin): 1. Support 'GB-Eire", 'EST5EDT", "ROK', 'US/*', 'NZ' and many + // other aliases/linked names when moving timezone validation code to C++. + // See crbug.com/364374 and crbug.com/v8/8007 . + // 2. Resolve the difference betwee CLDR/ICU and IANA time zone db. + // See http://unicode.org/cldr/trac/ticket/9892 and crbug.com/645807 . + if (strncmp(upper.c_str(), "ETC/GMT", 7) == 0) { + return GetGMTTzID(isolate, input); + } + return ToTitleCaseTimezoneLocation(isolate, input); +} + MaybeHandle JSDateTimeFormat::ResolvedOptions( Isolate* isolate, Handle format_holder) { Factory* factory = isolate->factory(); @@ -270,5 +422,31 @@ MaybeHandle JSDateTimeFormat::ResolvedOptions( return options; } +Maybe JSDateTimeFormat::OptionsToSkeleton( + Isolate* isolate, Handle options) { + std::string result; + bool hour12; + Maybe maybe_get_hour12 = Intl::GetBoolOption( + isolate, options, "hour12", "Intl.DateTimeFormat", &hour12); + MAYBE_RETURN(maybe_get_hour12, Nothing()); + HourOption hour_option = HourOption::H_UNKNOWN; + if (maybe_get_hour12.FromJust()) { + hour_option = hour12 ? HourOption::H_12 : HourOption::H_24; + } + + for (const auto& item : GetPatternData(hour_option)) { + std::unique_ptr input; + Maybe maybe_get_option = Intl::GetStringOption( + isolate, options, item.property.c_str(), *(item.allowed_values), + "Intl.DateTimeFormat", &input); + MAYBE_RETURN(maybe_get_option, Nothing()); + if (maybe_get_option.FromJust()) { + DCHECK_NOT_NULL(input.get()); + result += item.map.find(input.get())->second; + } + } + return Just(result); +} + } // namespace internal } // namespace v8 diff --git a/src/objects/js-date-time-format.h b/src/objects/js-date-time-format.h index ba8a1d6e61..08ae2602e4 100644 --- a/src/objects/js-date-time-format.h +++ b/src/objects/js-date-time-format.h @@ -26,6 +26,15 @@ class JSDateTimeFormat : public JSObject { V8_WARN_UNUSED_RESULT static MaybeHandle ResolvedOptions( Isolate* isolate, Handle date_time_holder); + // Convert the options to ICU DateTimePatternGenerator skeleton. + static Maybe OptionsToSkeleton(Isolate* isolate, + Handle options); + + // Return the time zone id which match ICU's expectation of title casing + // return empty string when error. + static std::string CanonicalizeTimeZoneID(Isolate* isolate, + const std::string& input); + DECL_CAST(JSDateTimeFormat) // Layout description. diff --git a/src/objects/js-number-format.h b/src/objects/js-number-format.h index 43c190b833..41c2a90076 100644 --- a/src/objects/js-number-format.h +++ b/src/objects/js-number-format.h @@ -18,6 +18,10 @@ // Has to be the last include (doesn't have include guards): #include "src/objects/object-macros.h" +namespace U_ICU_NAMESPACE { +class NumberFormat; +} // namespace U_ICU_NAMESPACE + namespace v8 { namespace internal { diff --git a/src/objects/js-plural-rules.h b/src/objects/js-plural-rules.h index 9d5da795ab..c91ce8974c 100644 --- a/src/objects/js-plural-rules.h +++ b/src/objects/js-plural-rules.h @@ -18,6 +18,10 @@ // Has to be the last include (doesn't have include guards): #include "src/objects/object-macros.h" +namespace U_ICU_NAMESPACE { +class PluralRules; +} // namespace U_ICU_NAMESPACE + namespace v8 { namespace internal { diff --git a/src/runtime/runtime-intl.cc b/src/runtime/runtime-intl.cc index cd8201a5aa..2695eb6a8c 100644 --- a/src/runtime/runtime-intl.cc +++ b/src/runtime/runtime-intl.cc @@ -173,16 +173,11 @@ RUNTIME_FUNCTION(Runtime_CreateDateTimeFormat) { JSObject::New(constructor, constructor)); // Set date time formatter as embedder field of the resulting JS object. - icu::SimpleDateFormat* date_format = + Maybe maybe_date_format = DateFormat::InitializeDateTimeFormat(isolate, locale, options, resolved); + MAYBE_RETURN(maybe_date_format, ReadOnlyRoots(isolate).exception()); + icu::SimpleDateFormat* date_format = maybe_date_format.FromJust(); CHECK_NOT_NULL(date_format); - if (!DateFormat::IsValidTimeZone(date_format)) { - delete date_format; - THROW_NEW_ERROR_RETURN_FAILURE( - isolate, - NewRangeError(MessageTemplate::kInvalidTimeZone, - isolate->factory()->NewStringFromStaticChars("Etc/GMT"))); - } local_object->SetEmbedderField(DateFormat::kSimpleDateFormatIndex, reinterpret_cast(date_format)); diff --git a/test/intl/date-format/timezone-conversion.js b/test/intl/date-format/timezone-conversion.js new file mode 100644 index 0000000000..1638346dee --- /dev/null +++ b/test/intl/date-format/timezone-conversion.js @@ -0,0 +1,17 @@ +// Copyright 2018 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Tests time zone support with conversion. + +df = Intl.DateTimeFormat(undefined, {timeZone: 'America/Los_Angeles'}); +assertEquals('America/Los_Angeles', df.resolvedOptions().timeZone); + +df = Intl.DateTimeFormat(undefined, {timeZone: {toString() { return 'America/Los_Angeles'}}}); +assertEquals('America/Los_Angeles', df.resolvedOptions().timeZone); + +assertThrows(() => Intl.DateTimeFormat( + undefined, {timeZone: {toString() { throw new Error("should throw"); }}})); + +assertThrows(() => Intl.DateTimeFormat( + undefined, {get timeZone() { throw new Error("should throw"); }}));