@@ -170,12 +170,25 @@ var TIMEZONE_NAME_CHECK_RE = UNDEFINED;
170170
171171function GetTimezoneNameCheckRE ( ) {
172172 if ( IS_UNDEFINED ( TIMEZONE_NAME_CHECK_RE ) ) {
173- TIMEZONE_NAME_CHECK_RE =
174- new GlobalRegExp ( '^([A-Za-z]+)/([A-Za-z ]+)(?:_( [A-Za-z ]+))*$' ) ;
173+ TIMEZONE_NAME_CHECK_RE = new GlobalRegExp (
174+ '^([A-Za-z]+)/([A-Za-z_- ]+)((?:\/ [A-Za-z_- ]+)+ )*$' )
175175 }
176176 return TIMEZONE_NAME_CHECK_RE ;
177177}
178178
179+ /**
180+ * Matches valid location parts of IANA time zone names.
181+ */
182+ var TIMEZONE_NAME_LOCATION_PART_RE = UNDEFINED ;
183+
184+ function GetTimezoneNameLocationPartRE ( ) {
185+ if ( IS_UNDEFINED ( TIMEZONE_NAME_LOCATION_PART_RE ) ) {
186+ TIMEZONE_NAME_LOCATION_PART_RE =
187+ new GlobalRegExp ( '^([A-Za-z]+)((?:[_-][A-Za-z]+)+)*$' ) ;
188+ }
189+ return TIMEZONE_NAME_LOCATION_PART_RE ;
190+ }
191+
179192/**
180193 * Adds bound method to the prototype of the given object.
181194 */
@@ -672,6 +685,34 @@ function toTitleCaseWord(word) {
672685 % StringToLowerCase ( % _CallFunction ( word , 1 , StringSubstr ) ) ;
673686}
674687
688+ /**
689+ * Returns titlecased location, bueNos_airES -> Buenos_Aires
690+ * or ho_cHi_minH -> Ho_Chi_Minh. It is locale-agnostic and only
691+ * deals with ASCII only characters.
692+ * 'of', 'au' and 'es' are special-cased and lowercased.
693+ */
694+ function toTitleCaseTimezoneLocation ( location ) {
695+ var match = % _CallFunction ( location , GetTimezoneNameLocationPartRE ( ) , StringMatch ) ;
696+ if ( IS_NULL ( match ) ) throw MakeRangeError ( kExpectedLocation , location ) ;
697+
698+ var result = toTitleCaseWord ( match [ 1 ] ) ;
699+ if ( ! IS_UNDEFINED ( match [ 2 ] ) && 2 < match . length ) {
700+ // The first character is a separator, '_' or '-'.
701+ // None of IANA zone names has both '_' and '-'.
702+ var separator = % _CallFunction ( match [ 2 ] , 0 , 1 , StringSubstring ) ;
703+ var parts = % _CallFunction ( match [ 2 ] , separator , StringSplit ) ;
704+ for ( var i = 1 ; i < parts . length ; i ++ ) {
705+ var part = parts [ i ]
706+ var lowercasedPart = % StringToLowerCase ( part ) ;
707+ result = result + separator +
708+ ( ( lowercasedPart !== 'es' &&
709+ lowercasedPart !== 'of' && lowercasedPart !== 'au' ) ?
710+ toTitleCaseWord ( part ) : lowercasedPart ) ;
711+ }
712+ }
713+ return result ;
714+ }
715+
675716/**
676717 * Canonicalizes the language tag, or throws in case the tag is invalid.
677718 */
@@ -1723,8 +1764,8 @@ addBoundMethod(Intl.DateTimeFormat, 'v8Parse', parseDate, 1);
17231764
17241765
17251766/**
1726- * Returns canonical Area/Location name, or throws an exception if the zone
1727- * name is invalid IANA name.
1767+ * Returns canonical Area/Location(/Location) name, or throws an exception
1768+ * if the zone name is invalid IANA name.
17281769 */
17291770function canonicalizeTimeZoneID ( tzID ) {
17301771 // Skip undefined zones.
@@ -1739,16 +1780,22 @@ function canonicalizeTimeZoneID(tzID) {
17391780 return 'UTC' ;
17401781 }
17411782
1742- // We expect only _ and / beside ASCII letters.
1743- // All inputs should conform to Area/Location from now on.
1783+ // TODO(jshin): Add support for Etc/GMT[+-]([1-9]|1[0-2])
1784+
1785+ // We expect only _, '-' and / beside ASCII letters.
1786+ // All inputs should conform to Area/Location(/Location)* from now on.
17441787 var match = % _CallFunction ( tzID , GetTimezoneNameCheckRE ( ) , StringMatch ) ;
1745- if ( IS_NULL ( match ) ) throw MakeRangeError ( kExpectedLocation , tzID ) ;
1788+ if ( IS_NULL ( match ) ) throw MakeRangeError ( kExpectedTimezoneID , tzID ) ;
1789+
1790+ var result = toTitleCaseTimezoneLocation ( match [ 1 ] ) + '/' +
1791+ toTitleCaseTimezoneLocation ( match [ 2 ] ) ;
17461792
1747- var result = toTitleCaseWord ( match [ 1 ] ) + '/' + toTitleCaseWord ( match [ 2 ] ) ;
1748- var i = 3 ;
1749- while ( ! IS_UNDEFINED ( match [ i ] ) && i < match . length ) {
1750- result = result + '_' + toTitleCaseWord ( match [ i ] ) ;
1751- i ++ ;
1793+ if ( ! IS_UNDEFINED ( match [ 3 ] ) && 3 < match . length ) {
1794+ var locations = % _CallFunction ( match [ 3 ] , '/' , StringSplit ) ;
1795+ // The 1st element is empty. Starts with i=1.
1796+ for ( var i = 1 ; i < locations . length ; i ++ ) {
1797+ result = result + '/' + toTitleCaseTimezoneLocation ( locations [ i ] ) ;
1798+ }
17521799 }
17531800
17541801 return result ;
0 commit comments