4 * http://github.com/jquery/globalize
6 * Copyright Software Freedom Conservancy, Inc.
7 * Dual licensed under the MIT or GPL Version 2 licenses.
8 * http://jquery.org/license
11 (function( window, undefined ) {
19 // private JavaScript utility functions
30 // private Globalization utility functions
41 // Global variable (Globalize) or CommonJS module (globalize)
42 Globalize = function( cultureSelector ) {
43 return new Globalize.prototype.init( cultureSelector );
46 if ( typeof require !== "undefined"
47 && typeof exports !== "undefined"
48 && typeof module !== "undefined" ) {
50 module.exports = Globalize;
52 // Export as global variable
53 window.Globalize = Globalize;
56 Globalize.cultures = {};
58 Globalize.prototype = {
59 constructor: Globalize,
60 init: function( cultureSelector ) {
61 this.cultures = Globalize.cultures;
62 this.cultureSelector = cultureSelector;
67 Globalize.prototype.init.prototype = Globalize.prototype;
69 // 1. When defining a culture, all fields are required except the ones stated as optional.
70 // 2. Each culture should have a ".calendars" object with at least one calendar named "standard"
71 // which serves as the default calendar in use by that culture.
72 // 3. Each culture should have a ".calendar" object which is the current calendar being used,
73 // it may be dynamically changed at any time to one of the calendars in ".calendars".
74 Globalize.cultures[ "default" ] = {
75 // A unique name for the culture in the form <language code>-<country/region code>
77 // the name of the culture in the english language
78 englishName: "English",
79 // the name of the culture in its own language
80 nativeName: "English",
81 // whether the culture uses right-to-left text
83 // "language" is used for so-called "specific" cultures.
84 // For example, the culture "es-CL" means "Spanish, in Chili".
85 // It represents the Spanish-speaking culture as it is in Chili,
86 // which might have different formatting rules or even translations
87 // than Spanish in Spain. A "neutral" culture is one that is not
88 // specific to a region. For example, the culture "es" is the generic
89 // Spanish culture, which may be a more generalized version of the language
90 // that may or may not be what a specific culture expects.
91 // For a specific culture like "es-CL", the "language" field refers to the
92 // neutral, generic culture information for the language it is using.
93 // This is not always a simple matter of the string before the dash.
94 // For example, the "zh-Hans" culture is netural (Simplified Chinese).
95 // And the "zh-SG" culture is Simplified Chinese in Singapore, whose lanugage
96 // field is "zh-CHS", not "zh".
97 // This field should be used to navigate from a specific culture to it's
98 // more general, neutral culture. If a culture is already as general as it
99 // can get, the language may refer to itself.
101 // numberFormat defines general number formatting rules, like the digits in
102 // each grouping, the group separator, and how negative numbers are displayed.
105 // Note, numberFormat.pattern has no "positivePattern" unlike percent and currency,
106 // but is still defined as an array for consistency with them.
107 // negativePattern: one of "(n)|-n|- n|n-|n -"
109 // number of decimal places normally shown
111 // string that separates number groups, as in 1,000,000
113 // string that separates a number from the fractional portion, as in 1.99
115 // array of numbers indicating the size of each number group.
116 // TODO: more detailed description and example
118 // symbol used for positive numbers
120 // symbol used for negative numbers
122 // symbol used for NaN (Not-A-Number)
124 // symbol used for Negative Infinity
125 negativeInfinity: "-Infinity",
126 // symbol used for Positive Infinity
127 positiveInfinity: "Infinity",
129 // [negativePattern, positivePattern]
130 // negativePattern: one of "-n %|-n%|-%n|%-n|%n-|n-%|n%-|-% n|n %-|% n-|% -n|n- %"
131 // positivePattern: one of "n %|n%|%n|% n"
132 pattern: [ "-n %", "n %" ],
133 // number of decimal places normally shown
135 // array of numbers indicating the size of each number group.
136 // TODO: more detailed description and example
138 // string that separates number groups, as in 1,000,000
140 // string that separates a number from the fractional portion, as in 1.99
142 // symbol used to represent a percentage
146 // [negativePattern, positivePattern]
147 // negativePattern: one of "($n)|-$n|$-n|$n-|(n$)|-n$|n-$|n$-|-n $|-$ n|n $-|$ n-|$ -n|n- $|($ n)|(n $)"
148 // positivePattern: one of "$n|n$|$ n|n $"
149 pattern: [ "($n)", "$n" ],
150 // number of decimal places normally shown
152 // array of numbers indicating the size of each number group.
153 // TODO: more detailed description and example
155 // string that separates number groups, as in 1,000,000
157 // string that separates a number from the fractional portion, as in 1.99
159 // symbol used to represent currency
163 // calendars defines all the possible calendars used by this culture.
164 // There should be at least one defined with name "standard", and is the default
165 // calendar used by the culture.
166 // A calendar contains information about how dates are formatted, information about
167 // the calendar's eras, a standard set of the date formats,
168 // translations for day and month names, and if the calendar is not based on the Gregorian
169 // calendar, conversion functions to and from the Gregorian calendar.
172 // name that identifies the type of calendar this is
173 name: "Gregorian_USEnglish",
174 // separator of parts of a date (e.g. "/" in 11/05/1955)
176 // separator of parts of a time (e.g. ":" in 05:44 PM)
178 // the first day of the week (0 = Sunday, 1 = Monday, etc)
182 names: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ],
183 // abbreviated day names
184 namesAbbr: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ],
185 // shortest day names
186 namesShort: [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ]
189 // full month names (13 months for lunar calendards -- 13th month should be "" if not lunar)
190 names: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "" ],
191 // abbreviated month names
192 namesAbbr: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "" ]
194 // AM and PM designators in one of these forms:
195 // The usual view, and the upper and lower case versions
196 // [ standard, lowercase, uppercase ]
197 // The culture does not use AM or PM (likely all standard date formats use 24 hour time)
199 AM: [ "AM", "am", "AM" ],
200 PM: [ "PM", "pm", "PM" ],
202 // eras in reverse chronological order.
203 // name: the name of the era in this culture (e.g. A.D., C.E.)
204 // start: when the era starts in ticks (gregorian, gmt), null if it is the earliest supported era.
205 // offset: offset in years from gregorian calendar
212 // when a two digit year is given, it will never be parsed as a four digit
213 // year greater than this year (in the appropriate era for the culture)
214 // Set it as a full year (e.g. 2029) or use an offset format starting from
215 // the current year: "+19" would correspond to 2029 if the current year 2010.
216 twoDigitYearMax: 2029,
217 // set of predefined date and time patterns used by the culture
218 // these represent the format someone in this culture would expect
219 // to see given the portions of the date that are shown.
221 // short date pattern
224 D: "dddd, MMMM dd, yyyy",
225 // short time pattern
229 // long date, short time pattern
230 f: "dddd, MMMM dd, yyyy h:mm tt",
231 // long date, long time pattern
232 F: "dddd, MMMM dd, yyyy h:mm:ss tt",
235 // month/year pattern
237 // S is a sortable format that does not vary by culture
238 S: "yyyy\u0027-\u0027MM\u0027-\u0027dd\u0027T\u0027HH\u0027:\u0027mm\u0027:\u0027ss"
240 // optional fields for each calendar:
243 Same as months but used when the day preceeds the month.
244 Omit if the culture has no genitive distinction in month names.
245 For an explaination of genitive months, see http://blogs.msdn.com/michkap/archive/2004/12/25/332259.aspx
247 Allows for the support of non-gregorian based calendars. This convert object is used to
248 to convert a date to and from a gregorian calendar date to handle parsing and formatting.
250 fromGregorian( date )
251 Given the date as a parameter, return an array with parts [ year, month, day ]
252 corresponding to the non-gregorian based year, month, and day for the calendar.
253 toGregorian( year, month, day )
254 Given the non-gregorian year, month, and day, return a new Date() object
255 set to the corresponding date in the gregorian calendar.
259 // For localized strings
263 Globalize.cultures[ "default" ].calendar = Globalize.cultures[ "default" ].calendars.standard;
265 Globalize.cultures[ "en" ] = Globalize.cultures[ "default" ];
267 Globalize.cultureSelector = "en";
273 regexHex = /^0x[a-f0-9]+$/i;
274 regexInfinity = /^[+-]?infinity$/i;
275 regexParseFloat = /^[+-]?\d*\.?\d*(e[+-]?\d+)?$/;
276 regexTrim = /^\s+|\s+$/g;
279 // private JavaScript utility functions
282 arrayIndexOf = function( array, item ) {
283 if ( array.indexOf ) {
284 return array.indexOf( item );
286 for ( var i = 0, length = array.length; i < length; i++ ) {
287 if ( array[i] === item ) {
294 endsWith = function( value, pattern ) {
295 return value.substr( value.length - pattern.length ) === pattern;
298 extend = function( deep ) {
299 var options, name, src, copy, copyIsArray, clone,
300 target = arguments[0] || {},
302 length = arguments.length,
305 // Handle a deep copy situation
306 if ( typeof target === "boolean" ) {
308 target = arguments[1] || {};
309 // skip the boolean and the target
313 // Handle case when target is a string or something (possible in deep copy)
314 if ( typeof target !== "object" && !isFunction(target) ) {
318 for ( ; i < length; i++ ) {
319 // Only deal with non-null/undefined values
320 if ( (options = arguments[ i ]) != null ) {
321 // Extend the base object
322 for ( name in options ) {
323 src = target[ name ];
324 copy = options[ name ];
326 // Prevent never-ending loop
327 if ( target === copy ) {
331 // Recurse if we're merging plain objects or arrays
332 if ( deep && copy && ( isObject(copy) || (copyIsArray = isArray(copy)) ) ) {
335 clone = src && isArray(src) ? src : [];
338 clone = src && isObject(src) ? src : {};
341 // Never move original objects, clone them
342 target[ name ] = extend( deep, clone, copy );
344 // Don't bring in undefined values
345 } else if ( copy !== undefined ) {
346 target[ name ] = copy;
352 // Return the modified object
356 isArray = Array.isArray || function( obj ) {
357 return Object.prototype.toString.call( obj ) === "[object Array]";
360 isFunction = function( obj ) {
361 return Object.prototype.toString.call( obj ) === "[object Function]";
364 isObject = function( obj ) {
365 return Object.prototype.toString.call( obj ) === "[object Object]";
368 startsWith = function( value, pattern ) {
369 return value.indexOf( pattern ) === 0;
372 trim = function( value ) {
373 return ( value + "" ).replace( regexTrim, "" );
376 truncate = function( value ) {
377 if ( isNaN( value ) ) {
380 return Math[ value < 0 ? "ceil" : "floor" ]( value );
383 zeroPad = function( str, count, left ) {
385 for ( l = str.length; l < count; l += 1 ) {
386 str = ( left ? ("0" + str) : (str + "0") );
392 // private Globalization utility functions
395 appendPreOrPostMatch = function( preMatch, strings ) {
396 // appends pre- and post- token match strings while removing escaped characters.
397 // Returns a single quote count which is used to determine if the token occurs
398 // in a string literal.
401 for ( var i = 0, il = preMatch.length; i < il; i++ ) {
402 var c = preMatch.charAt( i );
406 strings.push( "\'" );
415 strings.push( "\\" );
428 expandFormat = function( cal, format ) {
429 // expands unspecified or single character date formats into the full pattern.
430 format = format || "F";
432 patterns = cal.patterns,
435 pattern = patterns[ format ];
437 throw "Invalid date format string \'" + format + "\'.";
441 else if ( len === 2 && format.charAt(0) === "%" ) {
442 // %X escape format -- intended as a custom format string that is only one character, not a built-in format.
443 format = format.charAt( 1 );
448 formatDate = function( value, format, culture ) {
449 var cal = culture.calendar,
450 convert = cal.convert;
452 if ( !format || !format.length || format === "i" ) {
454 if ( culture && culture.name.length ) {
456 // non-gregorian calendar, so we cannot use built-in toLocaleString()
457 ret = formatDate( value, cal.patterns.F, culture );
460 var eraDate = new Date( value.getTime() ),
461 era = getEra( value, cal.eras );
462 eraDate.setFullYear( getEraYear(value, cal, era) );
463 ret = eraDate.toLocaleString();
467 ret = value.toString();
473 sortable = format === "s";
474 format = expandFormat( cal, format );
476 // Start with an empty string
479 zeros = [ "0", "00", "000" ],
482 dayPartRegExp = /([^d]|^)(d|dd)([^d]|$)/g,
484 tokenRegExp = getTokenRegExp(),
487 function padZeros( num, c ) {
489 if ( c > 1 && s.length < c ) {
490 r = ( zeros[c - 2] + s);
491 return r.substr( r.length - c, c );
500 if ( foundDay || checkedDay ) {
503 foundDay = dayPartRegExp.test( format );
508 function getPart( date, part ) {
510 return converted[ part ];
513 case 0: return date.getFullYear();
514 case 1: return date.getMonth();
515 case 2: return date.getDate();
519 if ( !sortable && convert ) {
520 converted = convert.fromGregorian( value );
524 // Save the current index
525 var index = tokenRegExp.lastIndex,
526 // Look for the next pattern
527 ar = tokenRegExp.exec( format );
529 // Append the text before the pattern (or the end of the string if not found)
530 var preMatch = format.slice( index, ar ? ar.index : format.length );
531 quoteCount += appendPreOrPostMatch( preMatch, ret );
537 // do not replace any matches that occur inside a string literal.
538 if ( quoteCount % 2 ) {
543 var current = ar[ 0 ],
544 clength = current.length;
548 //Day of the week, as a three-letter abbreviation
550 // Day of the week, using the full name
551 var names = ( clength === 3 ) ? cal.days.namesAbbr : cal.days.names;
552 ret.push( names[value.getDay()] );
555 // Day of month, without leading zero for single-digit days
557 // Day of month, with leading zero for single-digit days
560 padZeros( getPart(value, 2), clength )
564 // Month, as a three-letter abbreviation
566 // Month, using the full name
567 var part = getPart( value, 1 );
569 ( cal.monthsGenitive && hasDay() )
571 cal.monthsGenitive[ clength === 3 ? "namesAbbr" : "names" ][ part ]
573 cal.months[ clength === 3 ? "namesAbbr" : "names" ][ part ]
577 // Month, as digits, with no leading zero for single-digit months
579 // Month, as digits, with leading zero for single-digit months
581 padZeros( getPart(value, 1) + 1, clength )
585 // Year, as two digits, but with no leading zero for years less than 10
587 // Year, as two digits, with leading zero for years less than 10
589 // Year represented by four full digits
590 part = converted ? converted[ 0 ] : getEraYear( value, cal, getEra(value, eras), sortable );
595 padZeros( part, clength )
599 // Hours with no leading zero for single-digit hours, using 12-hour clock
601 // Hours with leading zero for single-digit hours, using 12-hour clock
602 hour = value.getHours() % 12;
603 if ( hour === 0 ) hour = 12;
605 padZeros( hour, clength )
609 // Hours with no leading zero for single-digit hours, using 24-hour clock
611 // Hours with leading zero for single-digit hours, using 24-hour clock
613 padZeros( value.getHours(), clength )
617 // Minutes with no leading zero for single-digit minutes
619 // Minutes with leading zero for single-digit minutes
621 padZeros( value.getMinutes(), clength )
625 // Seconds with no leading zero for single-digit seconds
627 // Seconds with leading zero for single-digit seconds
629 padZeros( value.getSeconds(), clength )
633 // One character am/pm indicator ("a" or "p")
635 // Multicharacter am/pm indicator
636 part = value.getHours() < 12 ? ( cal.AM ? cal.AM[0] : " " ) : ( cal.PM ? cal.PM[0] : " " );
637 ret.push( clength === 1 ? part.charAt(0) : part );
646 padZeros( value.getMilliseconds(), 3 ).substr( 0, clength )
650 // Time zone offset, no leading zero
652 // Time zone offset with leading zero
653 hour = value.getTimezoneOffset() / 60;
655 ( hour <= 0 ? "+" : "-" ) + padZeros( Math.floor(Math.abs(hour)), clength )
659 // Time zone offset with leading zero
660 hour = value.getTimezoneOffset() / 60;
662 ( hour <= 0 ? "+" : "-" ) + padZeros( Math.floor(Math.abs(hour)), 2 )
663 // Hard coded ":" separator, rather than using cal.TimeSeparator
664 // Repeated here for consistency, plus ":" was already assumed in date parsing.
665 + ":" + padZeros( Math.abs(value.getTimezoneOffset() % 60), 2 )
672 cal.eras[ getEra(value, eras) ].name
677 ret.push( cal["/"] );
680 throw "Invalid date format pattern \'" + current + "\'.";
684 return ret.join( "" );
691 expandNumber = function( number, precision, formatInfo ) {
692 var groupSizes = formatInfo.groupSizes,
693 curSize = groupSizes[ 0 ],
695 factor = Math.pow( 10, precision ),
696 rounded = Math.round( number * factor ) / factor;
698 if ( !isFinite(rounded) ) {
703 var numberString = number+"",
705 split = numberString.split( /e/i ),
706 exponent = split.length > 1 ? parseInt( split[1], 10 ) : 0;
707 numberString = split[ 0 ];
708 split = numberString.split( "." );
709 numberString = split[ 0 ];
710 right = split.length > 1 ? split[ 1 ] : "";
713 if ( exponent > 0 ) {
714 right = zeroPad( right, exponent, false );
715 numberString += right.slice( 0, exponent );
716 right = right.substr( exponent );
718 else if ( exponent < 0 ) {
719 exponent = -exponent;
720 numberString = zeroPad( numberString, exponent + 1 );
721 right = numberString.slice( -exponent, numberString.length ) + right;
722 numberString = numberString.slice( 0, -exponent );
725 if ( precision > 0 ) {
726 right = formatInfo[ "." ] +
727 ( (right.length > precision) ? right.slice(0, precision) : zeroPad(right, precision) );
733 var stringIndex = numberString.length - 1,
734 sep = formatInfo[ "," ],
737 while ( stringIndex >= 0 ) {
738 if ( curSize === 0 || curSize > stringIndex ) {
739 return numberString.slice( 0, stringIndex + 1 ) + ( ret.length ? (sep + ret + right) : right );
741 ret = numberString.slice( stringIndex - curSize + 1, stringIndex + 1 ) + ( ret.length ? (sep + ret) : "" );
743 stringIndex -= curSize;
745 if ( curGroupIndex < groupSizes.length ) {
746 curSize = groupSizes[ curGroupIndex ];
751 return numberString.slice( 0, stringIndex + 1 ) + sep + ret + right;
754 formatNumber = function( value, format, culture ) {
755 if ( !isFinite(value) ) {
756 if ( value === Infinity ) {
757 return culture.numberFormat.positiveInfinity;
759 if ( value === -Infinity ) {
760 return culture.numberFormat.negativeInfinity;
762 return culture.numberFormat.NaN;
764 if ( !format || format === "i" ) {
765 return culture.name.length ? value.toLocaleString() : value.toString();
767 format = format || "D";
769 var nf = culture.numberFormat,
770 number = Math.abs( value ),
773 if ( format.length > 1 ) precision = parseInt( format.slice(1), 10 );
775 var current = format.charAt( 0 ).toUpperCase(),
781 number = truncate( number );
782 if ( precision !== -1 ) {
783 number = zeroPad( "" + number, precision, true );
785 if ( value < 0 ) number = "-" + number;
791 formatInfo = formatInfo || nf.currency;
794 formatInfo = formatInfo || nf.percent;
795 pattern = value < 0 ? formatInfo.pattern[ 0 ] : ( formatInfo.pattern[1] || "n" );
796 if ( precision === -1 ) precision = formatInfo.decimals;
797 number = expandNumber( number * (current === "P" ? 100 : 1), precision, formatInfo );
800 throw "Bad number format specifier: " + current;
803 var patternParts = /n|\$|-|%/g,
806 var index = patternParts.lastIndex,
807 ar = patternParts.exec( pattern );
809 ret += pattern.slice( index, ar ? ar.index : pattern.length );
820 ret += nf.currency.symbol;
823 // don't make 0 negative
824 if ( /[1-9]/.test(number) ) {
829 ret += nf.percent.symbol;
839 getTokenRegExp = function() {
840 // regular expression for matching date and time tokens in format strings.
841 return /\/|dddd|ddd|dd|d|MMMM|MMM|MM|M|yyyy|yy|y|hh|h|HH|H|mm|m|ss|s|tt|t|fff|ff|f|zzz|zz|z|gg|g/g;
844 getEra = function( date, eras ) {
845 if ( !eras ) return 0;
846 var start, ticks = date.getTime();
847 for ( var i = 0, l = eras.length; i < l; i++ ) {
848 start = eras[ i ].start;
849 if ( start === null || ticks >= start ) {
856 getEraYear = function( date, cal, era, sortable ) {
857 var year = date.getFullYear();
858 if ( !sortable && cal.eras ) {
859 // convert normal gregorian year to era-shifted gregorian
860 // year by subtracting the era offset
861 year -= cal.eras[ era ].offset;
876 expandYear = function( cal, year ) {
877 // expands 2-digit year into 4 digits.
879 var now = new Date(),
881 curr = getEraYear( now, cal, era ),
882 twoDigitYearMax = cal.twoDigitYearMax;
883 twoDigitYearMax = typeof twoDigitYearMax === "string" ? new Date().getFullYear() % 100 + parseInt( twoDigitYearMax, 10 ) : twoDigitYearMax;
884 year += curr - ( curr % 100 );
885 if ( year > twoDigitYearMax ) {
892 getDayIndex = function ( cal, value, abbr ) {
895 upperDays = cal._upperDays;
897 cal._upperDays = upperDays = [
898 toUpperArray( days.names ),
899 toUpperArray( days.namesAbbr ),
900 toUpperArray( days.namesShort )
903 value = toUpper( value );
905 ret = arrayIndexOf( upperDays[1], value );
907 ret = arrayIndexOf( upperDays[2], value );
911 ret = arrayIndexOf( upperDays[0], value );
916 getMonthIndex = function( cal, value, abbr ) {
917 var months = cal.months,
918 monthsGen = cal.monthsGenitive || cal.months,
919 upperMonths = cal._upperMonths,
920 upperMonthsGen = cal._upperMonthsGen;
921 if ( !upperMonths ) {
922 cal._upperMonths = upperMonths = [
923 toUpperArray( months.names ),
924 toUpperArray( months.namesAbbr )
926 cal._upperMonthsGen = upperMonthsGen = [
927 toUpperArray( monthsGen.names ),
928 toUpperArray( monthsGen.namesAbbr )
931 value = toUpper( value );
932 var i = arrayIndexOf( abbr ? upperMonths[1] : upperMonths[0], value );
934 i = arrayIndexOf( abbr ? upperMonthsGen[1] : upperMonthsGen[0], value );
939 getParseRegExp = function( cal, format ) {
940 // converts a format string into a regular expression with groups that
941 // can be used to extract date fields from a date string.
942 // check for a cached parse regex.
943 var re = cal._parseRegExp;
945 cal._parseRegExp = re = {};
948 var reFormat = re[ format ];
954 // expand single digit formats, then escape regular expression characters.
955 var expFormat = expandFormat( cal, format ).replace( /([\^\$\.\*\+\?\|\[\]\(\)\{\}])/g, "\\\\$1" ),
960 tokenRegExp = getTokenRegExp(),
963 // iterate through each date token found.
964 while ( (match = tokenRegExp.exec(expFormat)) !== null ) {
965 var preMatch = expFormat.slice( index, match.index );
966 index = tokenRegExp.lastIndex;
968 // don't replace any matches that occur inside a string literal.
969 quoteCount += appendPreOrPostMatch( preMatch, regexp );
970 if ( quoteCount % 2 ) {
971 regexp.push( match[0] );
975 // add a regex group for the token.
980 case "dddd": case "ddd":
981 case "MMMM": case "MMM":
992 add = "(\\d{" + len + "})";
1000 case "ss": case "s":
1004 add = "([+-]?\\d\\d?:\\d{2})";
1006 case "zz": case "z":
1007 add = "([+-]?\\d\\d?)";
1010 add = "(\\" + cal[ "/" ] + ")";
1013 throw "Invalid date format pattern \'" + m + "\'.";
1019 groups.push( match[0] );
1021 appendPreOrPostMatch( expFormat.slice(index), regexp );
1024 // allow whitespace to differ when matching formats.
1025 var regexpStr = regexp.join( "" ).replace( /\s+/g, "\\s+" ),
1026 parseRegExp = { "regExp": regexpStr, "groups": groups };
1028 // cache the regex for this format.
1029 return re[ format ] = parseRegExp;
1032 outOfRange = function( value, low, high ) {
1033 return value < low || value > high;
1036 toUpper = function( value ) {
1037 // "he-IL" has non-breaking space in weekday names.
1038 return value.split( "\u00A0" ).join( " " ).toUpperCase();
1041 toUpperArray = function( arr ) {
1043 for ( var i = 0, l = arr.length; i < l; i++ ) {
1044 results[ i ] = toUpper( arr[i] );
1049 parseExact = function( value, format, culture ) {
1050 // try to parse the date string by matching against the format string
1051 // while using the specified culture for date field names.
1052 value = trim( value );
1053 var cal = culture.calendar,
1054 // convert date formats into regular expressions with groupings.
1055 // use the regexp to determine the input format and extract the date fields.
1056 parseInfo = getParseRegExp( cal, format ),
1057 match = new RegExp( parseInfo.regExp ).exec( value );
1058 if ( match === null ) {
1061 // found a date format that matches the input.
1062 var groups = parseInfo.groups,
1063 era = null, year = null, month = null, date = null, weekDay = null,
1064 hour = 0, hourOffset, min = 0, sec = 0, msec = 0, tzMinOffset = null,
1066 // iterate the format groups to extract and set the date fields.
1067 for ( var j = 0, jl = groups.length; j < jl; j++ ) {
1068 var matchGroup = match[ j + 1 ];
1070 var current = groups[ j ],
1071 clength = current.length,
1072 matchInt = parseInt( matchGroup, 10 );
1073 switch ( current ) {
1074 case "dd": case "d":
1077 // check that date is generally in valid range, also checking overflow below.
1078 if ( outOfRange(date, 1, 31) ) return null;
1080 case "MMM": case "MMMM":
1081 month = getMonthIndex( cal, matchGroup, clength === 3 );
1082 if ( outOfRange(month, 0, 11) ) return null;
1084 case "M": case "MM":
1086 month = matchInt - 1;
1087 if ( outOfRange(month, 0, 11) ) return null;
1089 case "y": case "yy":
1091 year = clength < 4 ? expandYear( cal, matchInt ) : matchInt;
1092 if ( outOfRange(year, 0, 9999) ) return null;
1094 case "h": case "hh":
1095 // Hours (12-hour clock).
1097 if ( hour === 12 ) hour = 0;
1098 if ( outOfRange(hour, 0, 11) ) return null;
1100 case "H": case "HH":
1101 // Hours (24-hour clock).
1103 if ( outOfRange(hour, 0, 23) ) return null;
1105 case "m": case "mm":
1108 if ( outOfRange(min, 0, 59) ) return null;
1110 case "s": case "ss":
1113 if ( outOfRange(sec, 0, 59) ) return null;
1115 case "tt": case "t":
1116 // AM/PM designator.
1117 // see if it is standard, upper, or lower case PM. If not, ensure it is at least one of
1118 // the AM tokens. If not, fail the parse for this format.
1119 pmHour = cal.PM && ( matchGroup === cal.PM[0] || matchGroup === cal.PM[1] || matchGroup === cal.PM[2] );
1122 !cal.AM || ( matchGroup !== cal.AM[0] && matchGroup !== cal.AM[1] && matchGroup !== cal.AM[2] )
1132 msec = matchInt * Math.pow( 10, 3 - clength );
1133 if ( outOfRange(msec, 0, 999) ) return null;
1139 weekDay = getDayIndex( cal, matchGroup, clength === 3 );
1140 if ( outOfRange(weekDay, 0, 6) ) return null;
1143 // Time zone offset in +/- hours:min.
1144 var offsets = matchGroup.split( /:/ );
1145 if ( offsets.length !== 2 ) return null;
1146 hourOffset = parseInt( offsets[0], 10 );
1147 if ( outOfRange(hourOffset, -12, 13) ) return null;
1148 var minOffset = parseInt( offsets[1], 10 );
1149 if ( outOfRange(minOffset, 0, 59) ) return null;
1150 tzMinOffset = ( hourOffset * 60 ) + ( startsWith(matchGroup, "-") ? -minOffset : minOffset );
1152 case "z": case "zz":
1153 // Time zone offset in +/- hours.
1154 hourOffset = matchInt;
1155 if ( outOfRange(hourOffset, -12, 13) ) return null;
1156 tzMinOffset = hourOffset * 60;
1158 case "g": case "gg":
1159 var eraName = matchGroup;
1160 if ( !eraName || !cal.eras ) return null;
1161 eraName = trim( eraName.toLowerCase() );
1162 for ( var i = 0, l = cal.eras.length; i < l; i++ ) {
1163 if ( eraName === cal.eras[i].name.toLowerCase() ) {
1168 // could not find an era with that name
1169 if ( era === null ) return null;
1174 var result = new Date(), defaultYear, convert = cal.convert;
1175 defaultYear = convert ? convert.fromGregorian( result )[ 0 ] : result.getFullYear();
1176 if ( year === null ) {
1179 else if ( cal.eras ) {
1180 // year must be shifted to normal gregorian year
1181 // but not if year was not specified, its already normal gregorian
1182 // per the main if clause above.
1183 year += cal.eras[( era || 0 )].offset;
1185 // set default day and month to 1 and January, so if unspecified, these are the defaults
1186 // instead of the current day/month.
1187 if ( month === null ) {
1190 if ( date === null ) {
1193 // now have year, month, and date, but in the culture's calendar.
1194 // convert to gregorian if necessary
1196 result = convert.toGregorian( year, month, date );
1197 // conversion failed, must be an invalid match
1198 if ( result === null ) return null;
1201 // have to set year, month and date together to avoid overflow based on current date.
1202 result.setFullYear( year, month, date );
1203 // check to see if date overflowed for specified month (only checked 1-31 above).
1204 if ( result.getDate() !== date ) return null;
1205 // invalid day of week.
1206 if ( weekDay !== null && result.getDay() !== weekDay ) {
1210 // if pm designator token was found make sure the hours fit the 24-hour clock.
1211 if ( pmHour && hour < 12 ) {
1214 result.setHours( hour, min, sec, msec );
1215 if ( tzMinOffset !== null ) {
1216 // adjust timezone to utc before applying local offset.
1217 var adjustedMin = result.getMinutes() - ( tzMinOffset + result.getTimezoneOffset() );
1218 // Safari limits hours and minutes to the range of -127 to 127. We need to use setHours
1219 // to ensure both these fields will not exceed this range. adjustedMin will range
1220 // somewhere between -1440 and 1500, so we only need to split this into hours.
1221 result.setHours( result.getHours() + parseInt(adjustedMin / 60, 10), adjustedMin % 60 );
1227 parseNegativePattern = function( value, nf, negativePattern ) {
1228 var neg = nf[ "-" ],
1231 switch ( negativePattern ) {
1237 if ( endsWith(value, neg) ) {
1238 ret = [ "-", value.substr(0, value.length - neg.length) ];
1240 else if ( endsWith(value, pos) ) {
1241 ret = [ "+", value.substr(0, value.length - pos.length) ];
1249 if ( startsWith(value, neg) ) {
1250 ret = [ "-", value.substr(neg.length) ];
1252 else if ( startsWith(value, pos) ) {
1253 ret = [ "+", value.substr(pos.length) ];
1257 if ( startsWith(value, "(") && endsWith(value, ")") ) {
1258 ret = [ "-", value.substr(1, value.length - 2) ];
1262 return ret || [ "", value ];
1266 // public instance functions
1269 Globalize.prototype.findClosestCulture = function( cultureSelector ) {
1270 return Globalize.findClosestCulture.call( this, cultureSelector );
1273 Globalize.prototype.format = function( value, format, cultureSelector ) {
1274 return Globalize.format.call( this, value, format, cultureSelector );
1277 Globalize.prototype.localize = function( key, cultureSelector ) {
1278 return Globalize.localize.call( this, key, cultureSelector );
1281 Globalize.prototype.parseInt = function( value, radix, cultureSelector ) {
1282 return Globalize.parseInt.call( this, value, radix, cultureSelector );
1285 Globalize.prototype.parseFloat = function( value, radix, cultureSelector ) {
1286 return Globalize.parseFloat.call( this, value, radix, cultureSelector );
1289 Globalize.prototype.culture = function( cultureSelector ) {
1290 return Globalize.culture.call( this, cultureSelector );
1294 // public singleton functions
1297 Globalize.addCultureInfo = function( cultureName, baseCultureName, info ) {
1302 if ( typeof cultureName !== "string" ) {
1303 // cultureName argument is optional string. If not specified, assume info is first
1304 // and only argument. Specified info deep-extends current culture.
1306 cultureName = this.culture().name;
1307 base = this.cultures[ cultureName ];
1308 } else if ( typeof baseCultureName !== "string" ) {
1309 // baseCultureName argument is optional string. If not specified, assume info is second
1310 // argument. Specified info deep-extends specified culture.
1311 // If specified culture does not exist, create by deep-extending default
1312 info = baseCultureName;
1313 isNew = ( this.cultures[ cultureName ] == null );
1314 base = this.cultures[ cultureName ] || this.cultures[ "default" ];
1316 // cultureName and baseCultureName specified. Assume a new culture is being created
1317 // by deep-extending an specified base culture
1319 base = this.cultures[ baseCultureName ];
1322 this.cultures[ cultureName ] = extend(true, {},
1326 // Make the standard calendar the current culture if it's a new culture
1328 this.cultures[ cultureName ].calendar = this.cultures[ cultureName ].calendars.standard;
1332 Globalize.findClosestCulture = function( name ) {
1335 return this.findClosestCulture( this.cultureSelector ) || this.cultures[ "default" ];
1337 if ( typeof name === "string" ) {
1338 name = name.split( "," );
1340 if ( isArray(name) ) {
1342 cultures = this.cultures,
1346 for ( i = 0; i < l; i++ ) {
1347 name = trim( list[i] );
1348 var pri, parts = name.split( ";" );
1349 lang = trim( parts[0] );
1350 if ( parts.length === 1 ) {
1354 name = trim( parts[1] );
1355 if ( name.indexOf("q=") === 0 ) {
1356 name = name.substr( 2 );
1357 pri = parseFloat( name );
1358 pri = isNaN( pri ) ? 0 : pri;
1364 prioritized.push({ lang: lang, pri: pri });
1366 prioritized.sort(function( a, b ) {
1367 return a.pri < b.pri ? 1 : -1;
1371 for ( i = 0; i < l; i++ ) {
1372 lang = prioritized[ i ].lang;
1373 match = cultures[ lang ];
1379 // neutral language match
1380 for ( i = 0; i < l; i++ ) {
1381 lang = prioritized[ i ].lang;
1383 var index = lang.lastIndexOf( "-" );
1384 if ( index === -1 ) {
1387 // strip off the last part. e.g. en-US => en
1388 lang = lang.substr( 0, index );
1389 match = cultures[ lang ];
1397 // last resort: match first culture using that language
1398 for ( i = 0; i < l; i++ ) {
1399 lang = prioritized[ i ].lang;
1400 for ( var cultureKey in cultures ) {
1401 var culture = cultures[ cultureKey ];
1402 if ( culture.language == lang ) {
1408 else if ( typeof name === "object" ) {
1411 return match || null;
1414 Globalize.format = function( value, format, cultureSelector ) {
1415 culture = this.findClosestCulture( cultureSelector );
1416 if ( value instanceof Date ) {
1417 value = formatDate( value, format, culture );
1419 else if ( typeof value === "number" ) {
1420 value = formatNumber( value, format, culture );
1425 Globalize.localize = function( key, cultureSelector ) {
1426 return this.findClosestCulture( cultureSelector ).messages[ key ] ||
1427 this.cultures[ "default" ].messages[ key ];
1430 Globalize.parseDate = function( value, formats, culture ) {
1431 culture = this.findClosestCulture( culture );
1433 var date, prop, patterns;
1435 if ( typeof formats === "string" ) {
1436 formats = [ formats ];
1438 if ( formats.length ) {
1439 for ( var i = 0, l = formats.length; i < l; i++ ) {
1440 var format = formats[ i ];
1442 date = parseExact( value, format, culture );
1450 patterns = culture.calendar.patterns;
1451 for ( prop in patterns ) {
1452 date = parseExact( value, patterns[prop], culture );
1459 return date || null;
1462 Globalize.parseInt = function( value, radix, cultureSelector ) {
1463 return truncate( Globalize.parseFloat(value, radix, cultureSelector) );
1466 Globalize.parseFloat = function( value, radix, cultureSelector ) {
1467 // radix argument is optional
1468 if ( typeof radix !== "number" ) {
1469 cultureSelector = radix;
1473 var culture = this.findClosestCulture( cultureSelector );
1475 nf = culture.numberFormat;
1477 if ( value.indexOf(culture.numberFormat.currency.symbol) > -1 ) {
1478 // remove currency symbol
1479 value = value.replace( culture.numberFormat.currency.symbol, "" );
1480 // replace decimal seperator
1481 value = value.replace( culture.numberFormat.currency["."], culture.numberFormat["."] );
1484 // trim leading and trailing whitespace
1485 value = trim( value );
1487 // allow infinity or hexidecimal
1488 if ( regexInfinity.test(value) ) {
1489 ret = parseFloat( value );
1491 else if ( !radix && regexHex.test(value) ) {
1492 ret = parseInt( value, 16 );
1496 // determine sign and number
1497 var signInfo = parseNegativePattern( value, nf, nf.pattern[0] ),
1498 sign = signInfo[ 0 ],
1499 num = signInfo[ 1 ];
1501 // #44 - try parsing as "(n)"
1502 if ( sign === "" && nf.pattern[0] !== "(n)" ) {
1503 signInfo = parseNegativePattern( value, nf, "(n)" );
1504 sign = signInfo[ 0 ];
1505 num = signInfo[ 1 ];
1508 // try parsing as "-n"
1509 if ( sign === "" && nf.pattern[0] !== "-n" ) {
1510 signInfo = parseNegativePattern( value, nf, "-n" );
1511 sign = signInfo[ 0 ];
1512 num = signInfo[ 1 ];
1517 // determine exponent and number
1520 exponentPos = num.indexOf( "e" );
1521 if ( exponentPos < 0 ) exponentPos = num.indexOf( "E" );
1522 if ( exponentPos < 0 ) {
1523 intAndFraction = num;
1527 intAndFraction = num.substr( 0, exponentPos );
1528 exponent = num.substr( exponentPos + 1 );
1530 // determine decimal position
1534 decimalPos = intAndFraction.indexOf( decSep );
1535 if ( decimalPos < 0 ) {
1536 integer = intAndFraction;
1540 integer = intAndFraction.substr( 0, decimalPos );
1541 fraction = intAndFraction.substr( decimalPos + decSep.length );
1543 // handle groups (e.g. 1,000,000)
1544 var groupSep = nf[ "," ];
1545 integer = integer.split( groupSep ).join( "" );
1546 var altGroupSep = groupSep.replace( /\u00A0/g, " " );
1547 if ( groupSep !== altGroupSep ) {
1548 integer = integer.split( altGroupSep ).join( "" );
1550 // build a natively parsable number string
1551 var p = sign + integer;
1552 if ( fraction !== null ) {
1553 p += "." + fraction;
1555 if ( exponent !== null ) {
1556 // exponent itself may have a number patternd
1557 var expSignInfo = parseNegativePattern( exponent, nf, "-n" );
1558 p += "e" + ( expSignInfo[0] || "+" ) + expSignInfo[ 1 ];
1560 if ( regexParseFloat.test(p) ) {
1561 ret = parseFloat( p );
1567 Globalize.culture = function( cultureSelector ) {
1569 if ( typeof cultureSelector !== "undefined" ) {
1570 this.cultureSelector = cultureSelector;
1573 return this.findClosestCulture( cultureSelector ) || this.culture[ "default" ];