[Intl] Move toLDMLString & canonicalizeTimeZoneID js->C++
Bug: v8:8066 Cq-Include-Trybots: luci.v8.try:v8_linux_noi18n_rel_ng;luci.chromium.try:linux_chromium_rel_ng Change-Id: I74fe09bb6bb4428d57c66811b77f5f35144f717f Reviewed-on: https://chromium-review.googlesource.com/1186153 Commit-Queue: Frank Tang <ftang@chromium.org> Reviewed-by: Jungshik Shin <jshin@chromium.org> Reviewed-by: Sathya Gunasekaran <gsathya@chromium.org> Reviewed-by: Adam Klein <adamk@chromium.org> Cr-Commit-Position: refs/heads/master@{#55625}
This commit is contained in:
parent
e9e583ec59
commit
d7ae63e6f2
200
src/js/intl.js
200
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)(?<offset>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.
|
||||
|
@ -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,39 +90,62 @@ bool ExtractStringSetting(Isolate* isolate, Handle<JSObject> options,
|
||||
return false;
|
||||
}
|
||||
|
||||
icu::SimpleDateFormat* CreateICUDateFormat(Isolate* isolate,
|
||||
const icu::Locale& icu_locale,
|
||||
Handle<JSObject> 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<icu::TimeZone> 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>(icu::TimeZone::createDefault());
|
||||
}
|
||||
std::string canonicalized =
|
||||
JSDateTimeFormat::CanonicalizeTimeZoneID(isolate, timezone);
|
||||
if (canonicalized.empty()) return std::unique_ptr<icu::TimeZone>();
|
||||
std::unique_ptr<icu::TimeZone> tz(
|
||||
icu::TimeZone::createTimeZone(canonicalized.c_str()));
|
||||
if (!IsValidTimeZoneName(*tz)) return std::unique_ptr<icu::TimeZone>();
|
||||
return tz;
|
||||
}
|
||||
|
||||
std::unique_ptr<icu::Calendar> CreateCalendar(Isolate* isolate,
|
||||
const icu::Locale& icu_locale,
|
||||
const char* timezone) {
|
||||
std::unique_ptr<icu::TimeZone> tz = CreateTimeZone(isolate, timezone);
|
||||
if (tz.get() == nullptr) return std::unique_ptr<icu::Calendar>();
|
||||
|
||||
// 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<icu::Calendar> 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<icu::GregorianCalendar*>(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;
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
std::unique_ptr<icu::SimpleDateFormat> 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 .
|
||||
@ -133,25 +157,23 @@ icu::SimpleDateFormat* CreateICUDateFormat(Isolate* isolate,
|
||||
// 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<icu::DateTimePatternGenerator> 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);
|
||||
}
|
||||
pattern =
|
||||
generator->getBestPattern(icu::UnicodeString(skeleton.c_str()), status);
|
||||
}
|
||||
|
||||
if (U_FAILURE(status)) {
|
||||
delete calendar;
|
||||
delete date_format;
|
||||
date_format = nullptr;
|
||||
}
|
||||
// Make formatter from skeleton. Calendar and numbering system are added
|
||||
// to the locale as Unicode extension (if they were specified at all).
|
||||
std::unique_ptr<icu::SimpleDateFormat> date_format(
|
||||
new icu::SimpleDateFormat(pattern, icu_locale, status));
|
||||
if (U_FAILURE(status)) return std::unique_ptr<icu::SimpleDateFormat>();
|
||||
|
||||
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<icu::SimpleDateFormat*> DateFormat::InitializeDateTimeFormat(
|
||||
Isolate* isolate, Handle<String> locale, Handle<JSObject> options,
|
||||
Handle<JSObject> 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<const char*> empty_values = {};
|
||||
std::unique_ptr<char[]> timezone = nullptr;
|
||||
Maybe<bool> maybe_timezone =
|
||||
Intl::GetStringOption(isolate, options, "timeZone", empty_values,
|
||||
"Intl.DateTimeFormat", &timezone);
|
||||
MAYBE_RETURN(maybe_timezone, Nothing<icu::SimpleDateFormat*>());
|
||||
|
||||
if (!date_format) {
|
||||
Maybe<std::string> maybe_skeleton =
|
||||
JSDateTimeFormat::OptionsToSkeleton(isolate, options);
|
||||
MAYBE_RETURN(maybe_skeleton, Nothing<icu::SimpleDateFormat*>());
|
||||
CHECK(!maybe_skeleton.FromJust().empty());
|
||||
std::string skeleton = maybe_skeleton.FromJust();
|
||||
|
||||
std::unique_ptr<icu::Calendar> 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<icu::SimpleDateFormat*>());
|
||||
}
|
||||
std::unique_ptr<icu::SimpleDateFormat> 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<JSObject> obj) {
|
||||
|
@ -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<icu::SimpleDateFormat*> InitializeDateTimeFormat(
|
||||
Isolate* isolate, Handle<String> locale, Handle<JSObject> options,
|
||||
Handle<JSObject> resolved);
|
||||
|
||||
// Unpacks date format object from corresponding JavaScript object.
|
||||
static icu::SimpleDateFormat* UnpackDateFormat(Handle<JSObject> 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<void>& data);
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -95,19 +95,76 @@ static const std::vector<PatternItem>& GetPatternItems() {
|
||||
return kPatternItems;
|
||||
}
|
||||
|
||||
class PatternData {
|
||||
public:
|
||||
PatternData(const std::string property, std::vector<PatternMap> pairs,
|
||||
std::vector<const char*>* 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<const std::string, const std::string> map;
|
||||
std::vector<const char*>* allowed_values;
|
||||
};
|
||||
|
||||
enum HourOption {
|
||||
H_UNKNOWN,
|
||||
H_12,
|
||||
H_24,
|
||||
};
|
||||
|
||||
static const std::vector<PatternData> CreateCommonData() {
|
||||
std::vector<PatternData> 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<PatternData> CreateData(const char* digit2,
|
||||
const char* numeric) {
|
||||
static std::vector<const char*> k2DigitNumeric = {"2-digit", "numeric"};
|
||||
static const std::vector<PatternData> common = CreateCommonData();
|
||||
std::vector<PatternData> build(common);
|
||||
build.push_back(PatternData(
|
||||
"hour", {{digit2, "2-digit"}, {numeric, "numeric"}}, &k2DigitNumeric));
|
||||
return build;
|
||||
}
|
||||
|
||||
static const std::vector<PatternData>& GetPatternData(HourOption option) {
|
||||
static const std::vector<PatternData> data = CreateData("jj", "j");
|
||||
static const std::vector<PatternData> data_h12 = CreateData("hh", "h");
|
||||
static const std::vector<PatternData> 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<JSObject> options) {
|
||||
Factory* factory = isolate->factory();
|
||||
const std::vector<PatternItem>& 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<JSObject> JSDateTimeFormat::ResolvedOptions(
|
||||
Isolate* isolate, Handle<JSReceiver> format_holder) {
|
||||
Factory* factory = isolate->factory();
|
||||
@ -270,5 +422,31 @@ MaybeHandle<JSObject> JSDateTimeFormat::ResolvedOptions(
|
||||
return options;
|
||||
}
|
||||
|
||||
Maybe<std::string> JSDateTimeFormat::OptionsToSkeleton(
|
||||
Isolate* isolate, Handle<JSReceiver> options) {
|
||||
std::string result;
|
||||
bool hour12;
|
||||
Maybe<bool> maybe_get_hour12 = Intl::GetBoolOption(
|
||||
isolate, options, "hour12", "Intl.DateTimeFormat", &hour12);
|
||||
MAYBE_RETURN(maybe_get_hour12, Nothing<std::string>());
|
||||
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<char[]> input;
|
||||
Maybe<bool> maybe_get_option = Intl::GetStringOption(
|
||||
isolate, options, item.property.c_str(), *(item.allowed_values),
|
||||
"Intl.DateTimeFormat", &input);
|
||||
MAYBE_RETURN(maybe_get_option, Nothing<std::string>());
|
||||
if (maybe_get_option.FromJust()) {
|
||||
DCHECK_NOT_NULL(input.get());
|
||||
result += item.map.find(input.get())->second;
|
||||
}
|
||||
}
|
||||
return Just(result);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
} // namespace v8
|
||||
|
@ -26,6 +26,15 @@ class JSDateTimeFormat : public JSObject {
|
||||
V8_WARN_UNUSED_RESULT static MaybeHandle<JSObject> ResolvedOptions(
|
||||
Isolate* isolate, Handle<JSReceiver> date_time_holder);
|
||||
|
||||
// Convert the options to ICU DateTimePatternGenerator skeleton.
|
||||
static Maybe<std::string> OptionsToSkeleton(Isolate* isolate,
|
||||
Handle<JSReceiver> 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.
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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<icu::SimpleDateFormat*> 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<Smi*>(date_format));
|
||||
|
17
test/intl/date-format/timezone-conversion.js
Normal file
17
test/intl/date-format/timezone-conversion.js
Normal file
@ -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"); }}));
|
Loading…
Reference in New Issue
Block a user