jquery-ui/external/globalize/globalize.js
2015-01-29 17:47:48 -05:00

1789 lines
42 KiB
JavaScript

/*!
* Globalize v1.0.0pre
*
* http://github.com/jquery/globalize
*
* Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2013-12-01T12:08Z
*/
(function( root, factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD.
define( factory );
} else if ( typeof module === "object" && typeof module.exports === "object" ) {
// Node. CommonJS.
module.exports = factory();
} else {
// Global
root.Globalize = factory();
}
}( this, function() {
/**
* CLDR JavaScript Library v0.2.4-pre
* http://jquery.com/
*
* Copyright 2013 Rafael Xavier de Souza
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2013-11-30T11:30Z
*/
/*!
* CLDR JavaScript Library v0.2.4-pre 2013-11-30T11:30Z MIT license © Rafael Xavier
* http://git.io/h4lmVg
*/
var Cldr = (function() {
var alwaysArray = function( stringOrArray ) {
return typeof stringOrArray === "string" ? [ stringOrArray ] : stringOrArray;
};
var common = function( Cldr ) {
Cldr.prototype.main = function( path ) {
path = alwaysArray( path );
return this.get( [ "main/{languageId}" ].concat( path ) );
};
};
var arrayIsArray = Array.isArray || function( obj ) {
return Object.prototype.toString.call( obj ) === "[object Array]";
};
var pathNormalize = function( path, attributes ) {
if ( arrayIsArray( path ) ) {
path = path.join( "/" );
}
if ( typeof path !== "string" ) {
throw new Error( "invalid path \"" + path + "\"" );
}
// 1: Ignore leading slash `/`
// 2: Ignore leading `cldr/`
path = path
.replace( /^\// , "" ) /* 1 */
.replace( /^cldr\// , "" ); /* 2 */
// Replace {attribute}'s
path = path.replace( /{[a-zA-Z]+}/g, function( name ) {
name = name.replace( /^{([^}]*)}$/, "$1" );
return attributes[ name ];
});
return path.split( "/" );
};
var arraySome = function( array, callback ) {
var i, length;
if ( array.some ) {
return array.some( callback );
}
for ( i = 0, length = array.length; i < length; i++ ) {
if ( callback( array[ i ], i, array ) ) {
return true;
}
}
return false;
};
// Return the maximized language id as defined in
// http://www.unicode.org/reports/tr35/#Likely_Subtags
// 1. Canonicalize.
// 1.1 Make sure the input locale is in canonical form: uses the right separator, and has the right casing.
// TODO Right casing? What df? It seems languages are lowercase, scripts are Capitalized, territory is uppercase. I am leaving this as an exercise to the user.
// 1.2 Replace any deprecated subtags with their canonical values using the <alias> data in supplemental metadata. Use the first value in the replacement list, if it exists. Language tag replacements may have multiple parts, such as "sh" ➞ "sr_Latn" or mo" ➞ "ro_MD". In such a case, the original script and/or region are retained if there is one. Thus "sh_Arab_AQ" ➞ "sr_Arab_AQ", not "sr_Latn_AQ".
// TODO What <alias> data?
// 1.3 If the tag is grandfathered (see <variable id="$grandfathered" type="choice"> in the supplemental data), then return it.
// TODO grandfathered?
// 1.4 Remove the script code 'Zzzz' and the region code 'ZZ' if they occur.
// 1.5 Get the components of the cleaned-up source tag (languages, scripts, and regions), plus any variants and extensions.
// 2. Lookup. Lookup each of the following in order, and stop on the first match:
// 2.1 languages_scripts_regions
// 2.2 languages_regions
// 2.3 languages_scripts
// 2.4 languages
// 2.5 und_scripts
// 3. Return
// 3.1 If there is no match, either return an error value, or the match for "und" (in APIs where a valid language tag is required).
// 3.2 Otherwise there is a match = languagem_scriptm_regionm
// 3.3 Let xr = xs if xs is not empty, and xm otherwise.
// 3.4 Return the language tag composed of languager _ scriptr _ regionr + variants + extensions .
//
// @subtags [Array] normalized language id subtags tuple (see init.js).
var likelySubtags = function( cldr, subtags, options ) {
var match, matchFound,
language = subtags[ 0 ],
script = subtags[ 1 ],
territory = subtags[ 2 ];
options = options || {};
// Skip if (language, script, territory) is not empty [3.3]
if ( language !== "und" && script !== "Zzzz" && territory !== "ZZ" ) {
return [ language, script, territory ];
}
// Skip if no supplemental likelySubtags data is present
if ( typeof cldr.get( "supplemental/likelySubtags" ) === "undefined" ) {
return;
}
// [2]
matchFound = arraySome([
[ language, script, territory ],
[ language, territory ],
[ language, script ],
[ language ],
[ "und", script ]
], function( test ) {
return match = !(/\b(Zzzz|ZZ)\b/).test( test.join( "_" ) ) /* [1.4] */ && cldr.get( [ "supplemental/likelySubtags", test.join( "_" ) ] );
});
// [3]
if ( matchFound ) {
// [3.2 .. 3.4]
match = match.split( "_" );
return [
language !== "und" ? language : match[ 0 ],
script !== "Zzzz" ? script : match[ 1 ],
territory !== "ZZ" ? territory : match[ 2 ]
];
} else if ( options.force ) {
// [3.1.2]
return cldr.get( "supplemental/likelySubtags/und" ).split( "_" );
} else {
// [3.1.1]
return;
}
};
// Given a locale, remove any fields that Add Likely Subtags would add.
// http://www.unicode.org/reports/tr35/#Likely_Subtags
// 1. First get max = AddLikelySubtags(inputLocale). If an error is signaled, return it.
// 2. Remove the variants from max.
// 3. Then for trial in {language, language _ region, language _ script}. If AddLikelySubtags(trial) = max, then return trial + variants.
// 4. If you do not get a match, return max + variants.
//
// @maxLanguageId [Array] maxLanguageId tuple (see init.js).
var removeLikelySubtags = function( cldr, maxLanguageId ) {
var match, matchFound,
language = maxLanguageId[ 0 ],
script = maxLanguageId[ 1 ],
territory = maxLanguageId[ 2 ];
// [3]
matchFound = arraySome([
[ [ language, "Zzzz", "ZZ" ], [ language ] ],
[ [ language, "Zzzz", territory ], [ language, territory ] ],
[ [ language, script, "ZZ" ], [ language, script ] ]
], function( test ) {
var result = likelySubtags( cldr, test[ 0 ] );
match = test[ 1 ];
return result && result[ 0 ] === maxLanguageId[ 0 ] &&
result[ 1 ] === maxLanguageId[ 1 ] &&
result[ 2 ] === maxLanguageId[ 2 ];
});
// [4]
return matchFound ? match : maxLanguageId;
};
var supplemental = function( cldr ) {
var prepend, supplemental;
prepend = function( prepend ) {
return function( path ) {
path = alwaysArray( path );
return cldr.get( [ prepend ].concat( path ) );
};
};
supplemental = prepend( "supplemental" );
// Week Data
// http://www.unicode.org/reports/tr35/tr35-dates.html#Week_Data
supplemental.weekData = prepend( "supplemental/weekData" );
supplemental.weekData.firstDay = function() {
return cldr.get( "supplemental/weekData/firstDay/{territory}" ) ||
cldr.get( "supplemental/weekData/firstDay/001" );
};
supplemental.weekData.minDays = function() {
var minDays = cldr.get( "supplemental/weekData/minDays/{territory}" ) ||
cldr.get( "supplemental/weekData/minDays/001" );
return parseInt( minDays, 10 );
};
// Time Data
// http://www.unicode.org/reports/tr35/tr35-dates.html#Time_Data
supplemental.timeData = prepend( "supplemental/timeData" );
supplemental.timeData.allowed = function() {
return cldr.get( "supplemental/timeData/{territory}/_allowed" ) ||
cldr.get( "supplemental/timeData/001/_allowed" );
};
supplemental.timeData.preferred = function() {
return cldr.get( "supplemental/timeData/{territory}/_preferred" ) ||
cldr.get( "supplemental/timeData/001/_preferred" );
};
return supplemental;
};
var init = function( locale ) {
var language, languageId, maxLanguageId, script, territory, unicodeLanguageId, variant;
if ( typeof locale !== "string" ) {
throw new Error( "invalid locale type: \"" + JSON.stringify( locale ) + "\"" );
}
// Normalize locale code.
// Get (or deduce) the "triple subtags": language, territory (also aliased as region), and script subtags.
// Get the variant subtags (calendar, collation, currency, etc).
// refs:
// - http://www.unicode.org/reports/tr35/#Field_Definitions
// - http://www.unicode.org/reports/tr35/#Language_and_Locale_IDs
// - http://www.unicode.org/reports/tr35/#Unicode_locale_identifier
locale = locale.replace( /-/, "_" );
// TODO normalize unicode locale extensions. Currently, skipped.
// unicodeLocaleExtensions = locale.split( "_u_" )[ 1 ];
locale = locale.split( "_u_" )[ 0 ];
// TODO normalize transformed extensions. Currently, skipped.
// transformedExtensions = locale.split( "_t_" )[ 1 ];
locale = locale.split( "_t_" )[ 0 ];
unicodeLanguageId = locale;
// unicodeLanguageId = ...
switch ( true ) {
// language_script_territory..
case /^[a-z]{2}_[A-Z][a-z]{3}_[A-Z0-9]{2}(\b|_)/.test( unicodeLanguageId ):
language = unicodeLanguageId.split( "_" )[ 0 ];
script = unicodeLanguageId.split( "_" )[ 1 ];
territory = unicodeLanguageId.split( "_" )[ 2 ];
variant = unicodeLanguageId.split( "_" )[ 3 ];
break;
// language_script..
case /^[a-z]{2}_[A-Z][a-z]{3}(\b|_)/.test( unicodeLanguageId ):
language = unicodeLanguageId.split( "_" )[ 0 ];
script = unicodeLanguageId.split( "_" )[ 1 ];
territory = "ZZ";
variant = unicodeLanguageId.split( "_" )[ 2 ];
break;
// language_territory..
case /^[a-z]{2}_[A-Z0-9]{2}(\b|_)/.test( unicodeLanguageId ):
language = unicodeLanguageId.split( "_" )[ 0 ];
script = "Zzzz";
territory = unicodeLanguageId.split( "_" )[ 1 ];
variant = unicodeLanguageId.split( "_" )[ 2 ];
break;
// language.., or root
case /^([a-z]{2}|root)(\b|_)/.test( unicodeLanguageId ):
language = unicodeLanguageId.split( "_" )[ 0 ];
script = "Zzzz";
territory = "ZZ";
variant = unicodeLanguageId.split( "_" )[ 1 ];
break;
default:
language = "und";
break;
}
// When a locale id does not specify a language, or territory (region), or script, they are obtained by Likely Subtags.
maxLanguageId = likelySubtags( this, [ language, script, territory ], { force: true } ) || unicodeLanguageId.split( "_" );
language = maxLanguageId[ 0 ];
script = maxLanguageId[ 1 ];
territory = maxLanguageId[ 2 ];
// TODO json content distributed on zip file use languageId with `-` on main.<lang>. Why `-` vs. `_` ?
languageId = removeLikelySubtags( this, maxLanguageId ).join( "_" );
// Set attributes
this.attributes = {
// Unicode Language Id
languageId: languageId,
maxLanguageId: maxLanguageId.join( "_" ),
// Unicode Language Id Subtabs
language: language,
script: script,
territory: territory,
region: territory, /* alias */
variant: variant
};
this.locale = variant ? [ languageId, variant ].join( "_" ) : languageId;
// Inlcude supplemental helper
this.supplemental = supplemental( this );
};
// @path: normalized path
var resourceGet = function( data, path ) {
var i,
node = data,
length = path.length;
for ( i = 0; i < length - 1; i++ ) {
node = node[ path[ i ] ];
if ( !node ) {
return undefined;
}
}
return node[ path[ i ] ];
};
var bundleParentLookup = function( Cldr, locale ) {
var parent;
if ( locale === "root" ) {
return;
}
// First, try to find parent on supplemental data.
parent = resourceGet( Cldr._resolved, pathNormalize( [ "supplemental/parentLocales/parentLocale", locale ] ) );
if ( parent ) {
return parent;
}
// Or truncate locale.
parent = locale.substr( 0, locale.lastIndexOf( "_" ) );
if ( !parent ) {
return "root";
}
return parent;
};
// @path: normalized path
var resourceSet = function( data, path, value ) {
var i,
node = data,
length = path.length;
for ( i = 0; i < length - 1; i++ ) {
if ( !node[ path[ i ] ] ) {
node[ path[ i ] ] = {};
}
node = node[ path[ i ] ];
}
node[ path[ i ] ] = value;
};
var arrayForEach = function( array, callback ) {
var i, length;
if ( array.forEach ) {
return array.forEach( callback );
}
for ( i = 0, length = array.length; i < length; i++ ) {
callback( array[ i ], i, array );
}
};
var jsonMerge = (function() {
// Returns new deeply merged JSON.
//
// Eg.
// merge( { a: { b: 1, c: 2 } }, { a: { b: 3, d: 4 } } )
// -> { a: { b: 3, c: 2, d: 4 } }
//
// @arguments JSON's
//
var merge = function() {
var destination = {},
sources = [].slice.call( arguments, 0 );
arrayForEach( sources, function( source ) {
var prop;
for ( prop in source ) {
if ( prop in destination && arrayIsArray( destination[ prop ] ) ) {
// Concat Arrays
destination[ prop ] = destination[ prop ].concat( source[ prop ] );
} else if ( prop in destination && typeof destination[ prop ] === "object" ) {
// Merge Objects
destination[ prop ] = merge( destination[ prop ], source[ prop ] );
} else {
// Set new values
destination[ prop ] = source[ prop ];
}
}
});
return destination;
};
return merge;
}());
var itemLookup = (function() {
var lookup;
lookup = function( Cldr, locale, path, attributes, childLocale ) {
var normalizedPath, parent, value;
// 1: Finish recursion
// 2: Avoid infinite loop
if ( typeof locale === "undefined" /* 1 */ || locale === childLocale /* 2 */ ) {
return;
}
// Resolve path
normalizedPath = pathNormalize( path, attributes );
// Check resolved (cached) data first
value = resourceGet( Cldr._resolved, normalizedPath );
if ( value ) {
return value;
}
// Check raw data
value = resourceGet( Cldr._raw, normalizedPath );
if ( !value ) {
// Or, lookup at parent locale
parent = bundleParentLookup( Cldr, locale );
value = lookup( Cldr, parent, path, jsonMerge( attributes, { languageId: parent }), locale );
}
// Set resolved (cached)
resourceSet( Cldr._resolved, normalizedPath, value );
return value;
};
return lookup;
}());
var itemGetResolved = function( Cldr, path, attributes ) {
// Resolve path
var normalizedPath = pathNormalize( path, attributes );
return resourceGet( Cldr._resolved, normalizedPath );
};
var Cldr = function() {
init.apply( this, arguments );
};
Cldr._resolved = {};
Cldr._raw = {};
// Load resolved or unresolved cldr data
// @json [JSON]
Cldr.load = function( json ) {
if ( typeof json !== "object" ) {
throw new Error( "invalid json" );
}
Cldr._raw = jsonMerge( Cldr._raw, json );
};
Cldr.prototype.get = function( path ) {
// Simplify locale using languageId (there are no other resource bundles)
// 1: during init(), get is called, but languageId is not defined. Use "" as a workaround in this very specific scenario.
var locale = this.attributes && this.attributes.languageId || "" /* 1 */;
return itemGetResolved( Cldr, path, this.attributes ) ||
itemLookup( Cldr, locale, path, this.attributes );
};
common( Cldr );
return Cldr;
}());
var arrayMap = function( array, callback ) {
var clone, i, length;
if ( array.map ) {
return array.map( callback );
}
for ( clone = [], i = 0, length = array.length; i < length; i++ ) {
clone[ i ] = callback( array[ i ], i, array );
}
return clone;
};
var objectValues = function( object ) {
var i,
result = [];
for ( i in object ) {
result.push( object[ i ] );
}
return result;
};
/**
* allPreset()
*
* @cldr [Cldr instance].
*
* Return an Array with all (skeleton, date, time, datetime) presets.
*/
var datetimeAllPresets = function( cldr ) {
var result = [];
// Skeleton
result = objectValues( cldr.main( "dates/calendars/gregorian/dateTimeFormats/availableFormats" ) );
// Time
result = result.concat( objectValues( cldr.main( "dates/calendars/gregorian/timeFormats" ) ) );
// Date
result = result.concat( objectValues( cldr.main( "dates/calendars/gregorian/dateFormats" ) ) );
// Datetime
result = result.concat( arrayMap( objectValues( cldr.main( "dates/calendars/gregorian/dateTimeFormats" ) ), function( datetimeFormat, key ) {
if ( typeof datetimeFormat !== "string" ) {
return datetimeFormat;
}
return datetimeFormat
.replace( /\{0\}/, cldr.main([
"dates/calendars/gregorian/timeFormats",
key
]))
.replace( /\{1\}/, cldr.main([
"dates/calendars/gregorian/dateFormats",
key
]));
}));
return arrayMap( result, function( pattern ) {
return { pattern: pattern };
});
};
/**
* expandPattern( pattern, cldr )
*
* @pattern [String or Object] if String, it's considered a skeleton. Object accepts:
* - skeleton: [String] lookup availableFormat;
* - date: [String] ( "full" | "long" | "medium" | "short" );
* - time: [String] ( "full" | "long" | "medium" | "short" );
* - datetime: [String] ( "full" | "long" | "medium" | "short" );
* - pattern: [String] For more info see datetime/format.js.
*
* @cldr [Cldr instance].
*
* Return the corresponding pattern.
* Eg for "en":
* - "GyMMMd" returns "MMM d, y G";
* - { skeleton: "GyMMMd" } returns "MMM d, y G";
* - { date: "full" } returns "EEEE, MMMM d, y";
* - { time: "full" } returns "h:mm:ss a zzzz";
* - { datetime: "full" } returns "EEEE, MMMM d, y 'at' h:mm:ss a zzzz";
* - { pattern: "dd/mm" } returns "dd/mm";
*/
var datetimeExpandPattern = function( pattern, cldr ) {
var result;
if ( typeof pattern === "string" ) {
pattern = { skeleton: pattern };
}
if ( typeof pattern === "object" ) {
switch ( true ) {
case "skeleton" in pattern:
result = cldr.main([
"dates/calendars/gregorian/dateTimeFormats/availableFormats",
pattern.skeleton
]);
break;
case "date" in pattern:
case "time" in pattern:
result = cldr.main([
"dates/calendars/gregorian",
"date" in pattern ? "dateFormats" : "timeFormats",
( pattern.date || pattern.time )
]);
break;
case "datetime" in pattern:
result = cldr.main([
"dates/calendars/gregorian/dateTimeFormats",
pattern.datetime
]);
if ( result ) {
result = result
.replace( /\{0\}/, cldr.main([
"dates/calendars/gregorian/timeFormats",
pattern.datetime
]))
.replace( /\{1\}/, cldr.main([
"dates/calendars/gregorian/dateFormats",
pattern.datetime
]));
}
break;
case "pattern" in pattern:
result = pattern.pattern;
break;
default:
throw new Error( "Invalid pattern" );
}
} else {
throw new Error( "Invalid pattern" );
}
if ( !result ) {
throw new Error( "Pattern not found" );
}
return result;
};
var datetimeWeekDays = [ "sun", "mon", "tue", "wed", "thu", "fri", "sat" ];
var arrayIndexOf = function( array, item ) {
if ( array.indexOf ) {
return array.indexOf( item );
}
for ( var i = 0, length = array.length; i < length; i++ ) {
if ( array[i] === item ) {
return i;
}
}
return -1;
};
/**
* firstDayOfWeek
*/
var datetimeFirstDayOfWeek = function( cldr ) {
return arrayIndexOf( datetimeWeekDays, cldr.supplemental.weekData.firstDay() );
};
/**
* dayOfWeek
*
* Return the day of the week normalized by the territory's firstDay [0-6].
* Eg for "mon":
* - return 0 if territory is GB, or BR, or DE, or FR (week starts on "mon");
* - return 1 if territory is US (week starts on "sun");
* - return 2 if territory is EG (week starts on "sat");
*/
var datetimeDayOfWeek = function( date, cldr ) {
return ( date.getDay() - datetimeFirstDayOfWeek( cldr ) + 7 ) % 7;
};
/**
* distanceInDays( from, to )
*
* Return the distance in days between from and to Dates.
*/
var datetimeDistanceInDays = function( from, to ) {
var inDays = 864e5;
return ( to.getTime() - from.getTime() ) / inDays;
};
/**
* startOf
*
* Return the
*/
var datetimeStartOf = function( date, unit ) {
date = new Date( date.getTime() );
switch( unit ) {
case "year":
date.setMonth( 0 );
/* falls through */
case "month":
date.setDate( 1 );
/* falls through */
case "day":
date.setHours( 0 );
/* falls through */
case "hour":
date.setMinutes( 0 );
/* falls through */
case "minute":
date.setSeconds( 0 );
/* falls through */
case "second":
date.setMilliseconds( 0 );
}
return date;
};
/**
* dayOfYear
*
* Return the distance in days of the date to the begin of the year [0-d].
*/
var datetimeDayOfYear = function( date ) {
return Math.floor( datetimeDistanceInDays( datetimeStartOf( date, "year" ), date ) );
};
/**
* millisecondsInDay
*/
var datetimeMillisecondsInDay = function( date ) {
// TODO Handle daylight savings discontinuities
return date - datetimeStartOf( date, "day" );
};
var datetimePatternRe = (/([a-z])\1*|'[^']+'|''|./ig);
var stringPad = function( str, count, right ) {
var length;
if ( typeof str !== "string" ) {
str = String( str );
}
for ( length = str.length; length < count; length += 1 ) {
str = ( right ? ( str + "0" ) : ( "0" + str ) );
}
return str;
};
/**
* format( date, pattern, cldr )
*
* @date [Date instance].
*
* @pattern [String] raw pattern.
* ref: http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
*
* @cldr [Cldr instance].
*
* TODO Support other calendar types.
*
* Disclosure: this function borrows excerpts of dojo/date/locale.
*/
var datetimeFormat = function( date, pattern, cldr ) {
var widths = [ "abbreviated", "wide", "narrow" ];
return pattern.replace( datetimePatternRe, function( current ) {
var pad, ret,
chr = current.charAt( 0 ),
length = current.length;
if ( chr === "j" ) {
// Locale preferred hHKk.
// http://www.unicode.org/reports/tr35/tr35-dates.html#Time_Data
chr = cldr.supplemental.timeData.preferred();
}
switch ( chr ) {
// Era
case "G":
ret = cldr.main([
"dates/calendars/gregorian/eras",
length <= 3 ? "eraAbbr" : ( length === 4 ? "eraNames" : "eraNarrow" ),
date.getFullYear() < 0 ? 0 : 1
]);
break;
// Year
case "y":
// Plain year.
// The length specifies the padding, but for two letters it also specifies the maximum length.
ret = String( date.getFullYear() );
pad = true;
if ( length === 2 ) {
ret = ret.substr( ret.length - 2 );
}
break;
case "Y":
// Year in "Week of Year"
// The length specifies the padding, but for two letters it also specifies the maximum length.
// yearInWeekofYear = date + DaysInAWeek - (dayOfWeek - firstDay) - minDays
ret = new Date( date.getTime() );
ret.setDate( ret.getDate() + 7 - ( datetimeDayOfWeek( date, cldr ) - datetimeFirstDayOfWeek( cldr ) ) - cldr.supplemental.weekData.minDays() );
ret = String( ret.getFullYear() );
pad = true;
if ( length === 2 ) {
ret = ret.substr( ret.length - 2 );
}
break;
case "u": // Extended year. Need to be implemented.
case "U": // Cyclic year name. Need to be implemented.
throw new Error( "Not implemented" );
// Quarter
case "Q":
case "q":
ret = Math.ceil( ( date.getMonth() + 1 ) / 3 );
if ( length <= 2 ) {
pad = true;
} else {
// http://unicode.org/cldr/trac/ticket/6788
ret = cldr.main([
"dates/calendars/gregorian/quarters",
chr === "Q" ? "format" : "stand-alone",
widths[ length - 3 ],
ret
]);
}
break;
// Month
case "M":
case "L":
ret = date.getMonth() + 1;
if ( length <= 2 ) {
pad = true;
} else {
ret = cldr.main([
"dates/calendars/gregorian/months",
chr === "M" ? "format" : "stand-alone",
widths[ length - 3 ],
ret
]);
}
break;
// Week
case "w":
// Week of Year.
// woy = ceil( ( doy + dow of 1/1 ) / 7 ) - minDaysStuff ? 1 : 0.
// TODO should pad on ww? Not documented, but I guess so.
ret = datetimeDayOfWeek( datetimeStartOf( date, "year" ), cldr );
ret = Math.ceil( ( datetimeDayOfYear( date ) + ret ) / 7 ) - ( 7 - ret >= cldr.supplemental.weekData.minDays() ? 0 : 1 );
pad = true;
break;
case "W":
// Week of Month.
// wom = ceil( ( dom + dow of `1/month` ) / 7 ) - minDaysStuff ? 1 : 0.
ret = datetimeDayOfWeek( datetimeStartOf( date, "month" ), cldr );
ret = Math.ceil( ( date.getDate() + ret ) / 7 ) - ( 7 - ret >= cldr.supplemental.weekData.minDays() ? 0 : 1 );
break;
// Day
case "d":
ret = date.getDate();
pad = true;
break;
case "D":
ret = datetimeDayOfYear( date ) + 1;
pad = true;
break;
case "F":
// Day of Week in month. eg. 2nd Wed in July.
ret = Math.floor( date.getDate() / 7 ) + 1;
break;
case "g+":
// Modified Julian day. Need to be implemented.
throw new Error( "Not implemented" );
// Week day
case "e":
case "c":
if ( length <= 2 ) {
// Range is [1-7] (deduced by example provided on documentation)
// TODO Should pad with zeros (not specified in the docs)?
ret = datetimeDayOfWeek( date, cldr ) + 1;
pad = true;
break;
}
/* falls through */
case "E":
ret = datetimeWeekDays[ date.getDay() ];
if ( length === 6 ) {
// If short day names are not explicitly specified, abbreviated day names are used instead.
// http://www.unicode.org/reports/tr35/tr35-dates.html#months_days_quarters_eras
// http://unicode.org/cldr/trac/ticket/6790
ret = cldr.main([
"dates/calendars/gregorian/days",
[ chr === "c" ? "stand-alone" : "format" ],
"short",
ret
]) || cldr.main([
"dates/calendars/gregorian/days",
[ chr === "c" ? "stand-alone" : "format" ],
"abbreviated",
ret
]);
} else {
ret = cldr.main([
"dates/calendars/gregorian/days",
[ chr === "c" ? "stand-alone" : "format" ],
widths[ length < 3 ? 0 : length - 3 ],
ret
]);
}
break;
// Period (AM or PM)
case "a":
ret = cldr.main([
"dates/calendars/gregorian/dayPeriods/format/wide",
date.getHours() < 12 ? "am" : "pm"
]);
break;
// Hour
case "h": // 1-12
ret = ( date.getHours() % 12 ) || 12;
pad = true;
break;
case "H": // 0-23
ret = date.getHours();
pad = true;
break;
case "K": // 0-11
ret = date.getHours() % 12;
pad = true;
break;
case "k": // 1-24
ret = date.getHours() || 24;
pad = true;
break;
// Minute
case "m":
ret = date.getMinutes();
pad = true;
break;
// Second
case "s":
ret = date.getSeconds();
pad = true;
break;
case "S":
ret = Math.round( date.getMilliseconds() * Math.pow( 10, length - 3 ) );
pad = true;
break;
case "A":
ret = Math.round( datetimeMillisecondsInDay( date ) * Math.pow( 10, length - 3 ) );
pad = true;
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":
case "X":
case "x":
throw new Error( "Not implemented" );
// Anything else is considered a literal, including [ ,:/.'@#], chinese, japonese, and arabic characters.
default:
return current;
}
if ( pad ) {
ret = stringPad( ret, length );
}
return ret;
});
};
var arrayEvery = function( array, callback ) {
var i, length;
if ( array.every ) {
return array.every( callback );
}
for ( i = 0, length = array.length; i < length; i++ ) {
if ( !callback( array[ i ], i, array ) ) {
return false;
}
}
return true;
};
/**
* tokenizer( value, pattern )
*
* Returns an Array of tokens, eg. value "5 o'clock PM", pattern "h 'o''clock' a":
* [{
* type: "h",
* lexeme: "5"
* }, {
* type: "literal",
* lexeme: " "
* }, {
* type: "literal",
* lexeme: "o'clock"
* }, {
* type: "literal",
* lexeme: " "
* }, {
* type: "a",
* lexeme: "PM",
* value: "pm"
* }]
*
* OBS: lexeme's are always String and may return invalid ranges depending of the token type. Eg. "99" for month number.
*
* Return an empty Array when not successfully parsed.
*/
var datetimeTokenizer = function( value, pattern, cldr ) {
var valid,
tokens = [],
widths = [ "abbreviated", "wide", "narrow" ];
valid = arrayEvery( pattern.match( datetimePatternRe ), function( current ) {
var chr, length, tokenRe,
token = {};
function oneDigitIfLengthOne() {
if ( length === 1 ) {
return tokenRe = /\d/;
}
}
function oneOrTwoDigitsIfLengthOne() {
if ( length === 1 ) {
return tokenRe = /\d\d?/;
}
}
function twoDigitsIfLengthTwo() {
if ( length === 2 ) {
return tokenRe = /\d\d/;
}
}
// Brute-force test every locale entry in an attempt to match the given value.
// Return the first found one (and set token accordingly), or null.
function lookup( path ) {
var i, re,
data = cldr.main( path );
for ( i in data ) {
re = new RegExp( "^" + data[ i ] );
if ( re.test( value ) ) {
token.value = i;
return tokenRe = new RegExp( data[ i ] );
}
}
return null;
}
token.type = current;
chr = current.charAt( 0 ),
length = current.length;
switch ( chr ) {
// Era
case "G":
lookup([
"dates/calendars/gregorian/eras",
length <= 3 ? "eraAbbr" : ( length === 4 ? "eraNames" : "eraNarrow" )
]);
break;
// Year
case "y":
case "Y":
// number l=1:+, l=2:{2}, l=3:{3,}, l=4:{4,}, ...
if ( length === 1 ) {
tokenRe = /\d+/;
} else if ( length === 2 ) {
tokenRe = /\d\d/;
} else {
tokenRe = new RegExp( "\\d{" + length + ",}" );
}
break;
case "u": // Extended year. Need to be implemented.
case "U": // Cyclic year name. Need to be implemented.
throw new Error( "Not implemented" );
// Quarter
case "Q":
case "q":
// number l=1:{1}, l=2:{2}.
// lookup l=3...
oneDigitIfLengthOne() || twoDigitsIfLengthTwo() || lookup([
"dates/calendars/gregorian/quarters",
chr === "Q" ? "format" : "stand-alone",
widths[ length - 3 ]
]);
break;
// Month
case "M":
case "L":
// number l=1:{1,2}, l=2:{2}.
// lookup l=3...
oneOrTwoDigitsIfLengthOne() || twoDigitsIfLengthTwo() || lookup([
"dates/calendars/gregorian/months",
chr === "M" ? "format" : "stand-alone",
widths[ length - 3 ]
]);
break;
// Day (see d below)
case "D":
// number {l,3}.
if ( length <= 3 ) {
tokenRe = new RegExp( "\\d{" + length + ",3}" );
}
break;
case "W":
case "F":
// number l=1:{1}.
oneDigitIfLengthOne();
break;
case "g+":
// Modified Julian day. Need to be implemented.
throw new Error( "Not implemented" );
// Week day
case "e":
case "c":
// number l=1:{1}, l=2:{2}.
// lookup for length >=3.
if( length <= 2 ) {
oneDigitIfLengthOne() || twoDigitsIfLengthTwo();
break;
}
/* falls through */
case "E":
if ( length === 6 ) {
// Note: if short day names are not explicitly specified, abbreviated day names are used instead http://www.unicode.org/reports/tr35/tr35-dates.html#months_days_quarters_eras
lookup([
"dates/calendars/gregorian/days",
[ chr === "c" ? "stand-alone" : "format" ],
"short"
]) || lookup([
"dates/calendars/gregorian/days",
[ chr === "c" ? "stand-alone" : "format" ],
"abbreviated"
]);
} else {
lookup([
"dates/calendars/gregorian/days",
[ chr === "c" ? "stand-alone" : "format" ],
widths[ length < 3 ? 0 : length - 3 ]
]);
}
break;
// Period (AM or PM)
case "a":
lookup([
"dates/calendars/gregorian/dayPeriods/format/wide"
]);
break;
// Week, Day, Hour, Minute, or Second
case "w":
case "d":
case "h":
case "H":
case "K":
case "k":
case "j":
case "m":
case "s":
// number l1:{1,2}, l2:{2}.
oneOrTwoDigitsIfLengthOne() || twoDigitsIfLengthTwo();
break;
case "S":
// number {l}.
tokenRe = new RegExp( "\\d{" + length + "}" );
break;
case "A":
// number {l+5}.
tokenRe = new RegExp( "\\d{" + ( length + 5 ) + "}" );
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":
case "X":
case "x":
throw new Error( "Not implemented" );
case "'":
token.type = "literal";
if ( current.charAt( 1 ) === "'" ) {
tokenRe = /'/;
} else {
tokenRe = /'[^']+'/;
}
break;
default:
token.type = "literal";
tokenRe = /./;
}
if ( !tokenRe ) {
return false;
}
// Get lexeme and consume it.
value = value.replace( new RegExp( "^" + tokenRe.source ), function( lexeme ) {
token.lexeme = lexeme;
return "";
});
if ( !token.lexeme ) {
return false;
}
tokens.push( token );
return true;
});
return valid ? tokens : [];
};
var datetimeParse = (function() {
function outOfRange( value, low, high ) {
return value < low || value > high;
}
/**
* parse
*
* ref: http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
*/
return function( value, pattern, cldr ) {
var amPm, era, hour24, valid,
YEAR = 0,
MONTH = 1,
DAY = 2,
HOUR = 3,
MINUTE = 4,
SECOND = 5,
MILLISECONDS = 6,
date = new Date(),
tokens = datetimeTokenizer( value, pattern, cldr ),
truncateAt = [],
units = [ "year", "month", "day", "hour", "minute", "second", "milliseconds" ];
if ( !tokens.length ) {
return null;
}
valid = arrayEvery( tokens, function( token ) {
var century, chr, value, length;
if ( token.type === "literal" ) {
// continue
return true;
}
chr = token.type.charAt( 0 );
length = token.type.length;
if ( chr === "j" ) {
// Locale preferred hHKk.
// http://www.unicode.org/reports/tr35/tr35-dates.html#Time_Data
chr = cldr.supplemental.timeData.preferred();
}
switch ( chr ) {
// Era
case "G":
truncateAt.push( YEAR );
era = +token.value;
break;
// Year
case "y":
value = +token.lexeme;
if ( length === 2 ) {
if ( outOfRange( value, 0, 99 ) ) {
return false;
}
// mimic dojo/date/locale: choose century to apply, according to a sliding window of 80 years before and 20 years after present year.
century = Math.floor( date.getFullYear() / 100 ) * 100;
value += century;
if ( value > date.getFullYear() + 20 ) {
value -= 100;
}
}
date.setFullYear( value );
truncateAt.push( YEAR );
break;
case "Y": // Year in "Week of Year"
case "u": // Extended year. Need to be implemented.
case "U": // Cyclic year name. Need to be implemented.
throw new Error( "Not implemented" );
// Quarter (skip)
case "Q":
case "q":
break;
// Month
case "M":
case "L":
if ( length <= 2 ) {
value = +token.lexeme;
} else {
value = +token.value;
}
if( outOfRange( value, 1, 12 ) ) {
return false;
}
date.setMonth( value - 1 );
truncateAt.push( MONTH );
break;
// Week (skip)
case "w": // Week of Year.
case "W": // Week of Month.
break;
// Day
case "d":
value = +token.lexeme;
if( outOfRange( value, 1, 31 ) ) {
return false;
}
date.setDate( value );
truncateAt.push( DAY );
break;
case "D":
value = +token.lexeme;
if( outOfRange( value, 1, 366 ) ) {
return false;
}
date.setMonth(0);
date.setDate( value );
truncateAt.push( DAY );
break;
case "F":
// Day of Week in month. eg. 2nd Wed in July.
// Skip
break;
case "g+":
// Modified Julian day. Need to be implemented.
throw new Error( "Not implemented" );
// Week day
case "e":
case "c":
case "E":
// Skip.
// value = arrayIndexOf( datetimeWeekDays, token.value );
break;
// Period (AM or PM)
case "a":
amPm = token.value;
break;
// Hour
case "K": // 0-11
value = +token.lexeme + 1;
/* falls through */
case "h": // 1-12
value = value || +token.lexeme;
if( outOfRange( value, 1, 12 ) ) {
return false;
}
date.setHours( value );
truncateAt.push( HOUR );
break;
case "H": // 0-23
value = +token.lexeme + 1;
/* falls through */
case "k": // 1-24
hour24 = true;
value = value || +token.lexeme;
if( outOfRange( value, 1, 24 ) ) {
return false;
}
date.setHours( value );
truncateAt.push( HOUR );
break;
// Minute
case "m":
value = +token.lexeme;
if( outOfRange( value, 0, 59 ) ) {
return false;
}
date.setMinutes( value );
truncateAt.push( MINUTE );
break;
// Second
case "s":
value = +token.lexeme;
if( outOfRange( value, 0, 59 ) ) {
return false;
}
date.setSeconds( value );
truncateAt.push( SECOND );
break;
case "A":
date.setHours( 0 );
date.setMinutes( 0 );
date.setSeconds( 0 );
/* falls through */
case "S":
value = Math.round( +token.lexeme * Math.pow( 10, 3 - length ) );
date.setMilliseconds( value );
truncateAt.push( MILLISECONDS );
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":
case "X":
case "x":
throw new Error( "Not implemented" );
}
return true;
});
if ( !valid || amPm && hour24 ) {
return null;
}
if ( era === 0 ) {
// 1 BC = year 0
date.setFullYear( date.getFullYear() * -1 + 1 );
}
if ( amPm === "pm" && date.getHours() !== 12 ) {
date.setHours( date.getHours() + 12 );
}
// 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 );
truncateAt = Math.max.apply( null, truncateAt );
date = datetimeStartOf( date, units[ truncateAt ] );
return date;
};
}());
var arrayIsArray = Array.isArray || function( obj ) {
return Object.prototype.toString.call( obj ) === "[object Array]";
};
var alwaysArray = function( stringOrArray ) {
return arrayIsArray( stringOrArray ) ? stringOrArray : [ stringOrArray ];
};
var arraySome = function( array, callback ) {
var i, length;
if ( array.some ) {
return array.some( callback );
}
for ( i = 0, length = array.length; i < length; i++ ) {
if ( callback( array[ i ], i, array ) ) {
return true;
}
}
return false;
};
var defaultLocale;
function getLocale( locale ) {
return locale ? new Cldr( locale ) : defaultLocale;
}
var Globalize = {};
/**
* Globalize.load( json )
*
* @json [JSON]
*
* Load resolved or unresolved cldr data.
* Somewhat equivalent to previous Globalize.addCultureInfo(...).
*/
Globalize.load = function( json ) {
Cldr.load( json );
};
/**
* Globalize.loadTranslations( locale, json )
*
* @locale [String]
*
* @json [JSON]
*
* Load translation data per locale.
*/
Globalize.loadTranslations = function( locale, json ) {
var customData = {
"globalize-translation": {}
};
locale = new Cldr( locale );
customData[ "globalize-translation" ][ locale.attributes.languageId ] = json;
Cldr.load( customData );
};
/**
* Globalize.locale( locale )
*
* @locale [String]
*
* Set default locale.
* Somewhat equivalent to previous culture( selector ).
*/
Globalize.locale = function( locale ) {
if ( arguments.length ) {
defaultLocale = new Cldr( locale );
}
return defaultLocale;
};
/**
* Globalize.format( value, pattern, locale )
*
* @value [Date or Number]
*
* @pattern [String or Object] see datetime/expand_pattern for more info.
*
* @locale [String]
*
* Formats a date or number according to the given pattern string and the given locale (or the default locale if not specified).
*/
Globalize.format = function( value, pattern, locale ) {
locale = getLocale( locale );
if ( value instanceof Date ) {
if ( !pattern ) {
throw new Error( "Missing pattern" );
}
pattern = datetimeExpandPattern( pattern, locale );
value = datetimeFormat( value, pattern, locale );
} else if ( typeof value === "number" ) {
// TODO value = numberFormat( value, pattern, locale );
throw new Error( "Number Format not implemented yet" );
}
return value;
};
/**
* Globalize.parseDate( value, patterns, locale )
*
* @value [Date]
*
* @patterns [Array] Optional. See datetime/expand_pattern for more info about each pattern. Defaults to the list of all presets defined in the locale (see datetime/all_presets for more info).
*
* @locale [String]
*
* Return a Date instance or null.
*/
Globalize.parseDate = function( value, patterns, locale ) {
var date;
locale = getLocale( locale );
if ( typeof value !== "string" ) {
throw new Error( "invalid value (" + value + "), string expected" );
}
if ( !patterns ) {
patterns = datetimeAllPresets( locale );
} else {
patterns = alwaysArray( patterns );
}
arraySome( patterns, function( pattern ) {
pattern = datetimeExpandPattern( pattern, locale );
date = datetimeParse( value, pattern, locale );
return !!date;
});
return date || null;
};
/**
* Globalize.translate( path, locale )
*
* @path [String or Array]
*
* @locale [String]
*
* Translate item given its path.
*/
Globalize.translate = function( path , locale ) {
locale = getLocale( locale );
path = alwaysArray( path );
return locale.get( [ "globalize-translation/{languageId}" ].concat( path ) );
};
return Globalize;
}));