Skip to content

Commit

Permalink
Date: Parse timezone (z, Z, O, x, X)
Browse files Browse the repository at this point in the history
Fixes #196
Closes #202
  • Loading branch information
rxaviers committed Oct 10, 2014
1 parent 03dd112 commit 9dd3cd0
Show file tree
Hide file tree
Showing 5 changed files with 434 additions and 17 deletions.
16 changes: 9 additions & 7 deletions src/date/parse.js
Expand Up @@ -21,7 +21,7 @@ function outOfRange( value, low, high ) {
* ref: http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
*/
return function( value, tokens, properties ) {
var amPm, era, hour, hour12, valid,
var amPm, era, hour, hour12, timezoneOffset, valid,
YEAR = 0,
MONTH = 1,
DAY = 2,
Expand Down Expand Up @@ -227,16 +227,14 @@ return function( value, tokens, properties ) {
break;

// Zone
// see http://www.unicode.org/reports/tr35/tr35-dates.html#Using_Time_Zone_Names ?
// Need to be implemented.
case "z":
case "Z":
case "z":
case "O":
case "v":
case "V":
case "X":
case "x":
throw new Error( "Not implemented" );
timezoneOffset = token.value - date.getTimezoneOffset();
break;

}

return true;
Expand All @@ -261,6 +259,10 @@ return function( value, tokens, properties ) {
date.setHours( date.getHours() + 12 );
}

if ( timezoneOffset ) {
date.setMinutes( date.getMinutes() + timezoneOffset );
}

// Truncate date at the most precise unit defined. Eg.
// If value is "12/31", and pattern is "MM/dd":
// => new Date( <current Year>, 12, 31, 0, 0, 0, 0 );
Expand Down
23 changes: 22 additions & 1 deletion src/date/tokenizer-properties.js
Expand Up @@ -18,7 +18,9 @@ return function( pattern, cldr ) {
widths = [ "abbreviated", "wide", "narrow" ];

function populateProperties( path, value ) {
properties[ path.replace( /^.*calendars\//, "" ) ] = value;

// The `dates` and `calendars` trim's purpose is to reduce properties' key size only.
properties[ path.replace( /^.*\/dates\//, "" ).replace( /calendars\//, "" ) ] = value;
}

cldr.on( "get", populateProperties );
Expand All @@ -29,6 +31,11 @@ return function( pattern, cldr ) {
chr = current.charAt( 0 ),
length = current.length;

if ( chr === "Z" && length < 5 ) {
chr = "O";
length = 4;
}

switch ( chr ) {

// Era
Expand Down Expand Up @@ -102,6 +109,20 @@ return function( pattern, cldr ) {
"dates/calendars/gregorian/dayPeriods/format/wide"
]);
break;

// Zone
case "z":
case "O":
cldr.main( "dates/timeZoneNames/gmtFormat" );
cldr.main( "dates/timeZoneNames/gmtZeroFormat" );
cldr.main( "dates/timeZoneNames/hourFormat" );
break;

case "v":
case "V":
throw createErrorUnsupportedFeature({
feature: "timezone pattern `" + chr + "`"
});
}
});

Expand Down
105 changes: 99 additions & 6 deletions src/date/tokenizer.js
Expand Up @@ -42,6 +42,53 @@ return function( value, properties ) {
var chr, length, tokenRe,
token = {};

function hourFormatParse( tokenRe ) {
var aux = value.match( tokenRe );

if ( !aux ) {
return false;
}

// hourFormat containing H only, e.g., `+H;-H`
if ( aux.length < 4 ) {
token.value = ( aux[ 1 ] ? -aux[ 1 ] : +aux[ 2 ] ) * 60;

// hourFormat containing H and m, e.g., `+HHmm;-HHmm`
} else {
token.value = ( aux[ 1 ] ? -aux[ 1 ] : +aux[ 3 ] ) * 60 +
( aux[ 1 ] ? -aux[ 2 ] : +aux[ 4 ] );
}

return true;
}

// Transform:
// - "+H;-H" -> /\+(\d\d?)|-(\d\d?)/
// - "+HH;-HH" -> /\+(\d\d)|-(\d\d)/
// - "+HHmm;-HHmm" -> /\+(\d\d)(\d\d)|-(\d\d)(\d\d)/
// - "+HH:mm;-HH:mm" -> /\+(\d\d):(\d\d)|-(\d\d):(\d\d)/
//
// If gmtFormat is GMT{0}, the regexp must fill {0} in each side, e.g.:
// - "+H;-H" -> /GMT\+(\d\d?)|GMT-(\d\d?)/
function hourFormatRe( hourFormat, gmtFormat ) {
var re;

if ( !gmtFormat ) {
gmtFormat = "{0}";
}

re = hourFormat
.replace( "+", "\\+" )
.replace( /HH|mm/g, "(\\d\\d)" )
.replace( /H|m/g, "(\\d\\d?)" );

re = re.split( ";" ).map(function( part ) {
return gmtFormat.replace( "{0}", part );
}).join( "|" );

return new RegExp( re );
}

function oneDigitIfLengthOne() {
if ( length === 1 ) {
return tokenRe = /\d/;
Expand Down Expand Up @@ -80,6 +127,24 @@ return function( value, properties ) {
chr = current.charAt( 0 ),
length = current.length;

if ( chr === "Z" ) {
// Z..ZZZ: same as "xxxx".
if ( length < 4 ) {
chr = "x";
length = 4;

// ZZZZ: same as "OOOO".
} else if ( length < 5 ) {
chr = "O";
length = 4;

// ZZZZZ: same as "XXXXX"
} else {
chr = "X";
length = 5;
}
}

switch ( chr ) {

// Era
Expand Down Expand Up @@ -214,16 +279,44 @@ return function( value, properties ) {
break;

// Zone
// see http://www.unicode.org/reports/tr35/tr35-dates.html#Using_Time_Zone_Names ?
// Need to be implemented.
case "z":
case "Z":
case "O":
case "v":
case "V":
// O: "{gmtFormat}+H;{gmtFormat}-H" or "{gmtZeroFormat}", eg. "GMT-8" or "GMT".
// OOOO: "{gmtFormat}{hourFormat}" or "{gmtZeroFormat}", eg. "GMT-08:00" or "GMT".
if ( value === properties[ "timeZoneNames/gmtZeroFormat" ] ) {
token.value = 0;
tokenRe = new RegExp( properties[ "timeZoneNames/gmtZeroFormat" ] );
} else {
tokenRe = hourFormatRe(
length < 4 ? "+H;-H" : properties[ "timeZoneNames/hourFormat" ],
properties[ "timeZoneNames/gmtFormat" ]
);
if ( !hourFormatParse( tokenRe ) ) {
return null;
}
}
break;

case "X":
// Same as x*, except it uses "Z" for zero offset.
if ( value === "Z" ) {
token.value = 0;
tokenRe = /Z/;
break;
}

/* falls through */
case "x":
throw new Error( "Not implemented" );
// x: hourFormat("+HH;-HH")
// xx or xxxx: hourFormat("+HHmm;-HHmm")
// xxx or xxxxx: hourFormat("+HH:mm;-HH:mm")
tokenRe = hourFormatRe(
length === 1 ? "+HH;-HH" : ( length % 2 ? "+HH:mm;-HH:mm" : "+HHmm;-HHmm" )
);
if ( !hourFormatParse( tokenRe ) ) {
return null;
}
break;

case "'":
token.type = "literal";
Expand Down
113 changes: 111 additions & 2 deletions test/unit/date/parse.js
Expand Up @@ -13,20 +13,34 @@ define([
], function( Cldr, parse, parseProperties, startOf, tokenizer, tokenizerProperties, enCaGregorian,
likelySubtags, timeData, weekData ) {

var cldr, date1, date2;
var cldr, date1, date2, midnight;

function assertParse( assert, stringDate, pattern, cldr, date ) {
var tokens = tokenizer( stringDate, tokenizerProperties( pattern, cldr ) );
assert.deepEqual( parse( stringDate, tokens, parseProperties( cldr ) ), date );
}

function assertParseTimezone( assert, stringDate, pattern, cldr, timezoneOffset ) {
var parsedTimezoneOffset, parsedDate, tokens,
testPattern = "HH:mm " + pattern,
testStringDate = "00:00 " + stringDate;
tokens = tokenizer( testStringDate, tokenizerProperties( testPattern, cldr ) );
parsedDate = parse( testStringDate, tokens, parseProperties( cldr ) );
parsedTimezoneOffset = ( parsedDate - midnight ) / 1000 / 60 + midnight.getTimezoneOffset();
assert.equal( parsedTimezoneOffset, timezoneOffset, "stringDate `" + stringDate +
"` pattern `" + pattern + "`" );
}

Cldr.load( enCaGregorian );
Cldr.load( likelySubtags );
Cldr.load( timeData );
Cldr.load( weekData );

cldr = new Cldr( "en" );

midnight = new Date();
midnight = startOf( midnight, "day" );

QUnit.module( "Date Parse" );

/**
Expand Down Expand Up @@ -375,6 +389,101 @@ QUnit.test( "should parse milliseconds in a day (A+)", function( assert ) {
* Zone
*/

// TODO all
QUnit.test( "should format timezone (z)", function( assert ) {
[ "z", "zz", "zzz", "zzzz" ].forEach(function( z ) {
assertParseTimezone( assert, "GMT", z, cldr, 0 );
});

assertParseTimezone( assert, "GMT-3", "z", cldr, 180 );
assertParseTimezone( assert, "GMT-3", "zz", cldr, 180 );
assertParseTimezone( assert, "GMT-3", "zzz", cldr, 180 );
assertParseTimezone( assert, "GMT-03:00", "zzzz", cldr, 180 );

assertParseTimezone( assert, "GMT+11", "z", cldr, -660 );
assertParseTimezone( assert, "GMT+11", "zz", cldr, -660 );
assertParseTimezone( assert, "GMT+11", "zzz", cldr, -660 );
assertParseTimezone( assert, "GMT+11:00", "zzzz", cldr, -660 );
});

QUnit.test( "should format timezone (Z)", function( assert ) {
assertParseTimezone( assert, "+0000", "Z", cldr, 0 );
assertParseTimezone( assert, "+0000", "ZZ", cldr, 0 );
assertParseTimezone( assert, "+0000", "ZZZ", cldr, 0 );
assertParseTimezone( assert, "GMT", "ZZZZ", cldr, 0 );
assertParseTimezone( assert, "Z", "ZZZZZ", cldr, 0 );

assertParseTimezone( assert, "-0300", "Z", cldr, 180 );
assertParseTimezone( assert, "-0300", "ZZ", cldr, 180 );
assertParseTimezone( assert, "-0300", "ZZZ", cldr, 180 );
assertParseTimezone( assert, "GMT-03:00","ZZZZ" , cldr, 180 );
assertParseTimezone( assert, "-03:00", "ZZZZZ", cldr, 180 );

assertParseTimezone( assert, "+1100", "Z", cldr, -660 );
assertParseTimezone( assert, "+1100", "ZZ", cldr, -660 );
assertParseTimezone( assert, "+1100", "ZZZ", cldr, -660 );
assertParseTimezone( assert, "GMT+11:00","ZZZZ" , cldr, -660 );
assertParseTimezone( assert, "+11:00", "ZZZZZ", cldr, -660 );
});

QUnit.test( "should format timezone (O)", function( assert ) {
assertParseTimezone( assert, "GMT", "O", cldr, 0 );
assertParseTimezone( assert, "GMT", "OOOO", cldr, 0 );

assertParseTimezone( assert, "GMT-3", "O", cldr, 180 );
assertParseTimezone( assert, "GMT-03:00","OOOO" , cldr, 180 );

assertParseTimezone( assert, "GMT+11", "O", cldr, -660 );
assertParseTimezone( assert, "GMT+11:00","OOOO" , cldr, -660 );
});

QUnit.test( "should format timezone (X)", function( assert ) {
assertParseTimezone( assert, "Z", "X", cldr, 0 );
assertParseTimezone( assert, "Z", "XX", cldr, 0 );
assertParseTimezone( assert, "Z", "XXX", cldr, 0 );
assertParseTimezone( assert, "Z", "XXXX", cldr, 0 );
assertParseTimezone( assert, "Z", "XXXXX", cldr, 0 );

assertParseTimezone( assert, "-03", "X", cldr, 180 );
assertParseTimezone( assert, "-0300", "XX", cldr, 180 );
assertParseTimezone( assert, "-03:00", "XXX", cldr, 180 );
assertParseTimezone( assert, "-0300", "XXXX", cldr, 180 );
assertParseTimezone( assert, "-03:00", "XXXXX", cldr, 180 );

assertParseTimezone( assert, "+0530", "XX", cldr, -330 );
assertParseTimezone( assert, "+05:30", "XXX", cldr, -330 );
assertParseTimezone( assert, "+0530", "XXXX", cldr, -330 );
assertParseTimezone( assert, "+05:30", "XXXXX", cldr, -330 );

assertParseTimezone( assert, "+11", "X", cldr, -660 );
assertParseTimezone( assert, "+1100", "XX", cldr, -660 );
assertParseTimezone( assert, "+11:00", "XXX", cldr, -660 );
assertParseTimezone( assert, "+1100", "XXXX", cldr, -660 );
assertParseTimezone( assert, "+11:00", "XXXXX", cldr, -660 );
});

QUnit.test( "should format timezone (x)", function( assert ) {
assertParseTimezone( assert, "+00", "x", cldr, 0 );
assertParseTimezone( assert, "+0000", "xx", cldr, 0 );
assertParseTimezone( assert, "+00:00", "xxx", cldr, 0 );
assertParseTimezone( assert, "+0000", "xxxx", cldr, 0 );
assertParseTimezone( assert, "+00:00", "xxxxx", cldr, 0 );

assertParseTimezone( assert, "-03", "x", cldr, 180 );
assertParseTimezone( assert, "-0300", "xx", cldr, 180 );
assertParseTimezone( assert, "-03:00", "xxx", cldr, 180 );
assertParseTimezone( assert, "-0300", "xxxx", cldr, 180 );
assertParseTimezone( assert, "-03:00", "xxxxx", cldr, 180 );

assertParseTimezone( assert, "+0530", "xx", cldr, -330 );
assertParseTimezone( assert, "+05:30", "xxx", cldr, -330 );
assertParseTimezone( assert, "+0530", "xxxx", cldr, -330 );
assertParseTimezone( assert, "+05:30", "xxxxx", cldr, -330 );

assertParseTimezone( assert, "+11", "x", cldr, -660 );
assertParseTimezone( assert, "+1100", "xx", cldr, -660 );
assertParseTimezone( assert, "+11:00", "xxx", cldr, -660 );
assertParseTimezone( assert, "+1100", "xxxx", cldr, -660 );
assertParseTimezone( assert, "+11:00", "xxxxx", cldr, -660 );
});

});

0 comments on commit 9dd3cd0

Please sign in to comment.