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 ) {
380 zeroPad = function( str, count, left ) {
382 for ( l = str.length; l < count; l += 1 ) {
383 str = ( left ? ("0" + str) : (str + "0") );
389 // private Globalization utility functions
392 appendPreOrPostMatch = function( preMatch, strings ) {
393 // appends pre- and post- token match strings while removing escaped characters.
394 // Returns a single quote count which is used to determine if the token occurs
395 // in a string literal.
398 for ( var i = 0, il = preMatch.length; i < il; i++ ) {
399 var c = preMatch.charAt( i );
403 strings.push( "\'" );
412 strings.push( "\\" );
425 expandFormat = function( cal, format ) {
426 // expands unspecified or single character date formats into the full pattern.
427 format = format || "F";
429 patterns = cal.patterns,
432 pattern = patterns[ format ];
434 throw "Invalid date format string \'" + format + "\'.";
438 else if ( len === 2 && format.charAt(0) === "%" ) {
439 // %X escape format -- intended as a custom format string that is only one character, not a built-in format.
440 format = format.charAt( 1 );
445 formatDate = function( value, format, culture ) {
446 var cal = culture.calendar,
447 convert = cal.convert;
449 if ( !format || !format.length || format === "i" ) {
451 if ( culture && culture.name.length ) {
453 // non-gregorian calendar, so we cannot use built-in toLocaleString()
454 ret = formatDate( value, cal.patterns.F, culture );
457 var eraDate = new Date( value.getTime() ),
458 era = getEra( value, cal.eras );
459 eraDate.setFullYear( getEraYear(value, cal, era) );
460 ret = eraDate.toLocaleString();
464 ret = value.toString();
470 sortable = format === "s";
471 format = expandFormat( cal, format );
473 // Start with an empty string
476 zeros = [ "0", "00", "000" ],
479 dayPartRegExp = /([^d]|^)(d|dd)([^d]|$)/g,
481 tokenRegExp = getTokenRegExp(),
484 function padZeros( num, c ) {
486 if ( c > 1 && s.length < c ) {
487 r = ( zeros[c - 2] + s);
488 return r.substr( r.length - c, c );
497 if ( foundDay || checkedDay ) {
500 foundDay = dayPartRegExp.test( format );
505 function getPart( date, part ) {
507 return converted[ part ];
510 case 0: return date.getFullYear();
511 case 1: return date.getMonth();
512 case 2: return date.getDate();
516 if ( !sortable && convert ) {
517 converted = convert.fromGregorian( value );
521 // Save the current index
522 var index = tokenRegExp.lastIndex,
523 // Look for the next pattern
524 ar = tokenRegExp.exec( format );
526 // Append the text before the pattern (or the end of the string if not found)
527 var preMatch = format.slice( index, ar ? ar.index : format.length );
528 quoteCount += appendPreOrPostMatch( preMatch, ret );
534 // do not replace any matches that occur inside a string literal.
535 if ( quoteCount % 2 ) {
540 var current = ar[ 0 ],
541 clength = current.length;
545 //Day of the week, as a three-letter abbreviation
547 // Day of the week, using the full name
548 var names = ( clength === 3 ) ? cal.days.namesAbbr : cal.days.names;
549 ret.push( names[value.getDay()] );
552 // Day of month, without leading zero for single-digit days
554 // Day of month, with leading zero for single-digit days
557 padZeros( getPart(value, 2), clength )
561 // Month, as a three-letter abbreviation
563 // Month, using the full name
564 var part = getPart( value, 1 );
566 ( cal.monthsGenitive && hasDay() )
568 cal.monthsGenitive[ clength === 3 ? "namesAbbr" : "names" ][ part ]
570 cal.months[ clength === 3 ? "namesAbbr" : "names" ][ part ]
574 // Month, as digits, with no leading zero for single-digit months
576 // Month, as digits, with leading zero for single-digit months
578 padZeros( getPart(value, 1) + 1, clength )
582 // Year, as two digits, but with no leading zero for years less than 10
584 // Year, as two digits, with leading zero for years less than 10
586 // Year represented by four full digits
587 part = converted ? converted[ 0 ] : getEraYear( value, cal, getEra(value, eras), sortable );
592 padZeros( part, clength )
596 // Hours with no leading zero for single-digit hours, using 12-hour clock
598 // Hours with leading zero for single-digit hours, using 12-hour clock
599 hour = value.getHours() % 12;
600 if ( hour === 0 ) hour = 12;
602 padZeros( hour, clength )
606 // Hours with no leading zero for single-digit hours, using 24-hour clock
608 // Hours with leading zero for single-digit hours, using 24-hour clock
610 padZeros( value.getHours(), clength )
614 // Minutes with no leading zero for single-digit minutes
616 // Minutes with leading zero for single-digit minutes
618 padZeros( value.getMinutes(), clength )
622 // Seconds with no leading zero for single-digit seconds
624 // Seconds with leading zero for single-digit seconds
626 padZeros( value.getSeconds(), clength )
630 // One character am/pm indicator ("a" or "p")
632 // Multicharacter am/pm indicator
633 part = value.getHours() < 12 ? ( cal.AM ? cal.AM[0] : " " ) : ( cal.PM ? cal.PM[0] : " " );
634 ret.push( clength === 1 ? part.charAt(0) : part );
643 padZeros( value.getMilliseconds(), 3 ).substr( 0, clength )
647 // Time zone offset, no leading zero
649 // Time zone offset with leading zero
650 hour = value.getTimezoneOffset() / 60;
652 ( hour <= 0 ? "+" : "-" ) + padZeros( Math.floor(Math.abs(hour)), clength )
656 // Time zone offset with leading zero
657 hour = value.getTimezoneOffset() / 60;
659 ( hour <= 0 ? "+" : "-" ) + padZeros( Math.floor(Math.abs(hour)), 2 )
660 // Hard coded ":" separator, rather than using cal.TimeSeparator
661 // Repeated here for consistency, plus ":" was already assumed in date parsing.
662 + ":" + padZeros( Math.abs(value.getTimezoneOffset() % 60), 2 )
669 cal.eras[ getEra(value, eras) ].name
674 ret.push( cal["/"] );
677 throw "Invalid date format pattern \'" + current + "\'.";
681 return ret.join( "" );
688 expandNumber = function( number, precision, formatInfo ) {
689 var groupSizes = formatInfo.groupSizes,
690 curSize = groupSizes[ 0 ],
692 factor = Math.pow( 10, precision ),
693 rounded = Math.round( number * factor ) / factor;
695 if ( !isFinite(rounded) ) {
700 var numberString = number+"",
702 split = numberString.split( /e/i ),
703 exponent = split.length > 1 ? parseInt( split[1], 10 ) : 0;
704 numberString = split[ 0 ];
705 split = numberString.split( "." );
706 numberString = split[ 0 ];
707 right = split.length > 1 ? split[ 1 ] : "";
710 if ( exponent > 0 ) {
711 right = zeroPad( right, exponent, false );
712 numberString += right.slice( 0, exponent );
713 right = right.substr( exponent );
715 else if ( exponent < 0 ) {
716 exponent = -exponent;
717 numberString = zeroPad( numberString, exponent + 1 );
718 right = numberString.slice( -exponent, numberString.length ) + right;
719 numberString = numberString.slice( 0, -exponent );
722 if ( precision > 0 ) {
723 right = formatInfo[ "." ] +
724 ( (right.length > precision) ? right.slice(0, precision) : zeroPad(right, precision) );
730 var stringIndex = numberString.length - 1,
731 sep = formatInfo[ "," ],
734 while ( stringIndex >= 0 ) {
735 if ( curSize === 0 || curSize > stringIndex ) {
736 return numberString.slice( 0, stringIndex + 1 ) + ( ret.length ? (sep + ret + right) : right );
738 ret = numberString.slice( stringIndex - curSize + 1, stringIndex + 1 ) + ( ret.length ? (sep + ret) : "" );
740 stringIndex -= curSize;
742 if ( curGroupIndex < groupSizes.length ) {
743 curSize = groupSizes[ curGroupIndex ];
748 return numberString.slice( 0, stringIndex + 1 ) + sep + ret + right;
751 formatNumber = function( value, format, culture ) {
752 if ( !isFinite(value) ) {
753 if ( value === Infinity ) {
754 return culture.numberFormat.positiveInfinity;
756 if ( value === -Infinity ) {
757 return culture.numberFormat.negativeInfinity;
759 return culture.numberFormat.NaN;
761 if ( !format || format === "i" ) {
762 return culture.name.length ? value.toLocaleString() : value.toString();
764 format = format || "D";
766 var nf = culture.numberFormat,
767 number = Math.abs( value ),
770 if ( format.length > 1 ) precision = parseInt( format.slice(1), 10 );
772 var current = format.charAt( 0 ).toUpperCase(),
778 number = truncate( number );
779 if ( precision !== -1 ) {
780 number = zeroPad( "" + number, precision, true );
782 if ( value < 0 ) number = "-" + number;
788 formatInfo = formatInfo || nf.currency;
791 formatInfo = formatInfo || nf.percent;
792 pattern = value < 0 ? formatInfo.pattern[ 0 ] : ( formatInfo.pattern[1] || "n" );
793 if ( precision === -1 ) precision = formatInfo.decimals;
794 number = expandNumber( number * (current === "P" ? 100 : 1), precision, formatInfo );
797 throw "Bad number format specifier: " + current;
800 var patternParts = /n|\$|-|%/g,
803 var index = patternParts.lastIndex,
804 ar = patternParts.exec( pattern );
806 ret += pattern.slice( index, ar ? ar.index : pattern.length );
817 ret += nf.currency.symbol;
820 // don't make 0 negative
821 if ( /[1-9]/.test(number) ) {
826 ret += nf.percent.symbol;
836 getTokenRegExp = function() {
837 // regular expression for matching date and time tokens in format strings.
838 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;
841 getEra = function( date, eras ) {
842 if ( !eras ) return 0;
843 var start, ticks = date.getTime();
844 for ( var i = 0, l = eras.length; i < l; i++ ) {
845 start = eras[ i ].start;
846 if ( start === null || ticks >= start ) {
853 getEraYear = function( date, cal, era, sortable ) {
854 var year = date.getFullYear();
855 if ( !sortable && cal.eras ) {
856 // convert normal gregorian year to era-shifted gregorian
857 // year by subtracting the era offset
858 year -= cal.eras[ era ].offset;
873 expandYear = function( cal, year ) {
874 // expands 2-digit year into 4 digits.
875 var now = new Date(),
878 var twoDigitYearMax = cal.twoDigitYearMax;
879 twoDigitYearMax = typeof twoDigitYearMax === "string" ? new Date().getFullYear() % 100 + parseInt( twoDigitYearMax, 10 ) : twoDigitYearMax;
880 var curr = getEraYear( now, cal, era );
881 year += curr - ( curr % 100 );
882 if ( year > twoDigitYearMax ) {
889 getDayIndex = function ( cal, value, abbr ) {
892 upperDays = cal._upperDays;
894 cal._upperDays = upperDays = [
895 toUpperArray( days.names ),
896 toUpperArray( days.namesAbbr ),
897 toUpperArray( days.namesShort )
900 value = toUpper( value );
902 ret = arrayIndexOf( upperDays[1], value );
904 ret = arrayIndexOf( upperDays[2], value );
908 ret = arrayIndexOf( upperDays[0], value );
913 getMonthIndex = function( cal, value, abbr ) {
914 var months = cal.months,
915 monthsGen = cal.monthsGenitive || cal.months,
916 upperMonths = cal._upperMonths,
917 upperMonthsGen = cal._upperMonthsGen;
918 if ( !upperMonths ) {
919 cal._upperMonths = upperMonths = [
920 toUpperArray( months.names ),
921 toUpperArray( months.namesAbbr )
923 cal._upperMonthsGen = upperMonthsGen = [
924 toUpperArray( monthsGen.names ),
925 toUpperArray( monthsGen.namesAbbr )
928 value = toUpper( value );
929 var i = arrayIndexOf( abbr ? upperMonths[1] : upperMonths[0], value );
931 i = arrayIndexOf( abbr ? upperMonthsGen[1] : upperMonthsGen[0], value );
936 getParseRegExp = function( cal, format ) {
937 // converts a format string into a regular expression with groups that
938 // can be used to extract date fields from a date string.
939 // check for a cached parse regex.
940 var re = cal._parseRegExp;
942 cal._parseRegExp = re = {};
945 var reFormat = re[ format ];
951 // expand single digit formats, then escape regular expression characters.
952 var expFormat = expandFormat( cal, format ).replace( /([\^\$\.\*\+\?\|\[\]\(\)\{\}])/g, "\\\\$1" ),
957 tokenRegExp = getTokenRegExp(),
960 // iterate through each date token found.
961 while ( (match = tokenRegExp.exec(expFormat)) !== null ) {
962 var preMatch = expFormat.slice( index, match.index );
963 index = tokenRegExp.lastIndex;
965 // don't replace any matches that occur inside a string literal.
966 quoteCount += appendPreOrPostMatch( preMatch, regexp );
967 if ( quoteCount % 2 ) {
968 regexp.push( match[0] );
972 // add a regex group for the token.
977 case "dddd": case "ddd":
978 case "MMMM": case "MMM":
989 add = "(\\d{" + len + "})";
1001 add = "([+-]?\\d\\d?:\\d{2})";
1003 case "zz": case "z":
1004 add = "([+-]?\\d\\d?)";
1007 add = "(\\" + cal[ "/" ] + ")";
1010 throw "Invalid date format pattern \'" + m + "\'.";
1016 groups.push( match[0] );
1018 appendPreOrPostMatch( expFormat.slice(index), regexp );
1021 // allow whitespace to differ when matching formats.
1022 var regexpStr = regexp.join( "" ).replace( /\s+/g, "\\s+" ),
1023 parseRegExp = { "regExp": regexpStr, "groups": groups };
1025 // cache the regex for this format.
1026 return re[ format ] = parseRegExp;
1029 outOfRange = function( value, low, high ) {
1030 return value < low || value > high;
1033 toUpper = function( value ) {
1034 // "he-IL" has non-breaking space in weekday names.
1035 return value.split( "\u00A0" ).join( " " ).toUpperCase();
1038 toUpperArray = function( arr ) {
1040 for ( var i = 0, l = arr.length; i < l; i++ ) {
1041 results[ i ] = toUpper( arr[i] );
1046 parseExact = function( value, format, culture ) {
1047 // try to parse the date string by matching against the format string
1048 // while using the specified culture for date field names.
1049 value = trim( value );
1050 var cal = culture.calendar,
1051 // convert date formats into regular expressions with groupings.
1052 // use the regexp to determine the input format and extract the date fields.
1053 parseInfo = getParseRegExp( cal, format ),
1054 match = new RegExp( parseInfo.regExp ).exec( value );
1055 if ( match === null ) {
1058 // found a date format that matches the input.
1059 var groups = parseInfo.groups,
1060 era = null, year = null, month = null, date = null, weekDay = null,
1061 hour = 0, hourOffset, min = 0, sec = 0, msec = 0, tzMinOffset = null,
1063 // iterate the format groups to extract and set the date fields.
1064 for ( var j = 0, jl = groups.length; j < jl; j++ ) {
1065 var matchGroup = match[ j + 1 ];
1067 var current = groups[ j ],
1068 clength = current.length,
1069 matchInt = parseInt( matchGroup, 10 );
1070 switch ( current ) {
1071 case "dd": case "d":
1074 // check that date is generally in valid range, also checking overflow below.
1075 if ( outOfRange(date, 1, 31) ) return null;
1077 case "MMM": case "MMMM":
1078 month = getMonthIndex( cal, matchGroup, clength === 3 );
1079 if ( outOfRange(month, 0, 11) ) return null;
1081 case "M": case "MM":
1083 month = matchInt - 1;
1084 if ( outOfRange(month, 0, 11) ) return null;
1086 case "y": case "yy":
1088 year = clength < 4 ? expandYear( cal, matchInt ) : matchInt;
1089 if ( outOfRange(year, 0, 9999) ) return null;
1091 case "h": case "hh":
1092 // Hours (12-hour clock).
1094 if ( hour === 12 ) hour = 0;
1095 if ( outOfRange(hour, 0, 11) ) return null;
1097 case "H": case "HH":
1098 // Hours (24-hour clock).
1100 if ( outOfRange(hour, 0, 23) ) return null;
1102 case "m": case "mm":
1105 if ( outOfRange(min, 0, 59) ) return null;
1107 case "s": case "ss":
1110 if ( outOfRange(sec, 0, 59) ) return null;
1112 case "tt": case "t":
1113 // AM/PM designator.
1114 // see if it is standard, upper, or lower case PM. If not, ensure it is at least one of
1115 // the AM tokens. If not, fail the parse for this format.
1116 pmHour = cal.PM && ( matchGroup === cal.PM[0] || matchGroup === cal.PM[1] || matchGroup === cal.PM[2] );
1119 !cal.AM || ( matchGroup !== cal.AM[0] && matchGroup !== cal.AM[1] && matchGroup !== cal.AM[2] )
1129 msec = matchInt * Math.pow( 10, 3 - clength );
1130 if ( outOfRange(msec, 0, 999) ) return null;
1136 weekDay = getDayIndex( cal, matchGroup, clength === 3 );
1137 if ( outOfRange(weekDay, 0, 6) ) return null;
1140 // Time zone offset in +/- hours:min.
1141 var offsets = matchGroup.split( /:/ );
1142 if ( offsets.length !== 2 ) return null;
1143 hourOffset = parseInt( offsets[0], 10 );
1144 if ( outOfRange(hourOffset, -12, 13) ) return null;
1145 var minOffset = parseInt( offsets[1], 10 );
1146 if ( outOfRange(minOffset, 0, 59) ) return null;
1147 tzMinOffset = ( hourOffset * 60 ) + ( startsWith(matchGroup, "-") ? -minOffset : minOffset );
1149 case "z": case "zz":
1150 // Time zone offset in +/- hours.
1151 hourOffset = matchInt;
1152 if ( outOfRange(hourOffset, -12, 13) ) return null;
1153 tzMinOffset = hourOffset * 60;
1155 case "g": case "gg":
1156 var eraName = matchGroup;
1157 if ( !eraName || !cal.eras ) return null;
1158 eraName = trim( eraName.toLowerCase() );
1159 for ( var i = 0, l = cal.eras.length; i < l; i++ ) {
1160 if ( eraName === cal.eras[i].name.toLowerCase() ) {
1165 // could not find an era with that name
1166 if ( era === null ) return null;
1171 var result = new Date(), defaultYear, convert = cal.convert;
1172 defaultYear = convert ? convert.fromGregorian( result )[ 0 ] : result.getFullYear();
1173 if ( year === null ) {
1176 else if ( cal.eras ) {
1177 // year must be shifted to normal gregorian year
1178 // but not if year was not specified, its already normal gregorian
1179 // per the main if clause above.
1180 year += cal.eras[( era || 0 )].offset;
1182 // set default day and month to 1 and January, so if unspecified, these are the defaults
1183 // instead of the current day/month.
1184 if ( month === null ) {
1187 if ( date === null ) {
1190 // now have year, month, and date, but in the culture's calendar.
1191 // convert to gregorian if necessary
1193 result = convert.toGregorian( year, month, date );
1194 // conversion failed, must be an invalid match
1195 if ( result === null ) return null;
1198 // have to set year, month and date together to avoid overflow based on current date.
1199 result.setFullYear( year, month, date );
1200 // check to see if date overflowed for specified month (only checked 1-31 above).
1201 if ( result.getDate() !== date ) return null;
1202 // invalid day of week.
1203 if ( weekDay !== null && result.getDay() !== weekDay ) {
1207 // if pm designator token was found make sure the hours fit the 24-hour clock.
1208 if ( pmHour && hour < 12 ) {
1211 result.setHours( hour, min, sec, msec );
1212 if ( tzMinOffset !== null ) {
1213 // adjust timezone to utc before applying local offset.
1214 var adjustedMin = result.getMinutes() - ( tzMinOffset + result.getTimezoneOffset() );
1215 // Safari limits hours and minutes to the range of -127 to 127. We need to use setHours
1216 // to ensure both these fields will not exceed this range. adjustedMin will range
1217 // somewhere between -1440 and 1500, so we only need to split this into hours.
1218 result.setHours( result.getHours() + parseInt(adjustedMin / 60, 10), adjustedMin % 60 );
1224 parseNegativePattern = function( value, nf, negativePattern ) {
1225 var neg = nf[ "-" ],
1228 switch ( negativePattern ) {
1234 if ( endsWith(value, neg) ) {
1235 ret = [ "-", value.substr(0, value.length - neg.length) ];
1237 else if ( endsWith(value, pos) ) {
1238 ret = [ "+", value.substr(0, value.length - pos.length) ];
1246 if ( startsWith(value, neg) ) {
1247 ret = [ "-", value.substr(neg.length) ];
1249 else if ( startsWith(value, pos) ) {
1250 ret = [ "+", value.substr(pos.length) ];
1254 if ( startsWith(value, "(") && endsWith(value, ")") ) {
1255 ret = [ "-", value.substr(1, value.length - 2) ];
1259 return ret || [ "", value ];
1263 // public instance functions
1266 Globalize.prototype.findClosestCulture = function( cultureSelector ) {
1267 return Globalize.findClosestCulture.call( this, cultureSelector );
1270 Globalize.prototype.format = function( value, format, cultureSelector ) {
1271 return Globalize.format.call( this, value, format, cultureSelector );
1274 Globalize.prototype.localize = function( key, cultureSelector ) {
1275 return Globalize.localize.call( this, key, cultureSelector );
1278 Globalize.prototype.parseInt = function( value, radix, cultureSelector ) {
1279 return Globalize.parseInt.call( this, value, radix, cultureSelector );
1282 Globalize.prototype.parseFloat = function( value, radix, cultureSelector ) {
1283 return Globalize.parseFloat.call( this, value, radix, cultureSelector );
1286 Globalize.prototype.culture = function( cultureSelector ) {
1287 return Globalize.culture.call( this, cultureSelector );
1291 // public singleton functions
1294 Globalize.addCultureInfo = function( cultureName, baseCultureName, info ) {
1299 if ( typeof cultureName !== "string" ) {
1300 // cultureName argument is optional string. If not specified, assume info is first
1301 // and only argument. Specified info deep-extends current culture.
1303 cultureName = this.culture().name;
1304 base = this.cultures[ cultureName ];
1305 } else if ( typeof baseCultureName !== "string" ) {
1306 // baseCultureName argument is optional string. If not specified, assume info is second
1307 // argument. Specified info deep-extends specified culture.
1308 // If specified culture does not exist, create by deep-extending default
1309 info = baseCultureName;
1310 isNew = ( this.cultures[ cultureName ] == null );
1311 base = this.cultures[ cultureName ] || this.cultures[ "default" ];
1313 // cultureName and baseCultureName specified. Assume a new culture is being created
1314 // by deep-extending an specified base culture
1316 base = this.cultures[ baseCultureName ];
1319 this.cultures[ cultureName ] = extend(true, {},
1323 // Make the standard calendar the current culture if it's a new culture
1325 this.cultures[ cultureName ].calendar = this.cultures[ cultureName ].calendars.standard;
1329 Globalize.findClosestCulture = function( name ) {
1332 return this.cultures[ this.cultureSelector ] || this.cultures[ "default" ];
1334 if ( typeof name === "string" ) {
1335 name = name.split( "," );
1337 if ( isArray(name) ) {
1339 cultures = this.cultures,
1343 for ( i = 0; i < l; i++ ) {
1344 name = trim( list[i] );
1345 var pri, parts = name.split( ";" );
1346 lang = trim( parts[0] );
1347 if ( parts.length === 1 ) {
1351 name = trim( parts[1] );
1352 if ( name.indexOf("q=") === 0 ) {
1353 name = name.substr( 2 );
1354 pri = parseFloat( name );
1355 pri = isNaN( pri ) ? 0 : pri;
1361 prioritized.push({ lang: lang, pri: pri });
1363 prioritized.sort(function( a, b ) {
1364 return a.pri < b.pri ? 1 : -1;
1368 for ( i = 0; i < l; i++ ) {
1369 lang = prioritized[ i ].lang;
1370 match = cultures[ lang ];
1376 // neutral language match
1377 for ( i = 0; i < l; i++ ) {
1378 lang = prioritized[ i ].lang;
1380 var index = lang.lastIndexOf( "-" );
1381 if ( index === -1 ) {
1384 // strip off the last part. e.g. en-US => en
1385 lang = lang.substr( 0, index );
1386 match = cultures[ lang ];
1394 // last resort: match first culture using that language
1395 for ( i = 0; i < l; i++ ) {
1396 lang = prioritized[ i ].lang;
1397 for ( var cultureKey in cultures ) {
1398 var culture = cultures[ cultureKey ];
1399 if ( culture.language == lang ) {
1405 else if ( typeof name === "object" ) {
1408 return match || null;
1411 Globalize.format = function( value, format, cultureSelector ) {
1412 culture = this.findClosestCulture( cultureSelector );
1413 if ( value instanceof Date ) {
1414 value = formatDate( value, format, culture );
1416 else if ( typeof value === "number" ) {
1417 value = formatNumber( value, format, culture );
1422 Globalize.localize = function( key, cultureSelector ) {
1423 return this.findClosestCulture( cultureSelector ).messages[ key ] ||
1424 this.cultures[ "default" ].messages[ key ];
1427 Globalize.parseDate = function( value, formats, culture ) {
1428 culture = this.findClosestCulture( culture );
1430 var date, prop, patterns;
1432 if ( typeof formats === "string" ) {
1433 formats = [ formats ];
1435 if ( formats.length ) {
1436 for ( var i = 0, l = formats.length; i < l; i++ ) {
1437 var format = formats[ i ];
1439 date = parseExact( value, format, culture );
1447 patterns = culture.calendar.patterns;
1448 for ( prop in patterns ) {
1449 date = parseExact( value, patterns[prop], culture );
1456 return date || null;
1459 Globalize.parseInt = function( value, radix, cultureSelector ) {
1460 return truncate( Globalize.parseFloat(value, radix, cultureSelector) );
1463 Globalize.parseFloat = function( value, radix, cultureSelector ) {
1464 // radix argument is optional
1465 if ( typeof radix !== "number" ) {
1466 cultureSelector = radix;
1470 var culture = this.findClosestCulture( cultureSelector );
1472 nf = culture.numberFormat;
1474 if ( value.indexOf(culture.numberFormat.currency.symbol) > -1 ) {
1475 // remove currency symbol
1476 value = value.replace( culture.numberFormat.currency.symbol, "" );
1477 // replace decimal seperator
1478 value = value.replace( culture.numberFormat.currency["."], culture.numberFormat["."] );
1481 // trim leading and trailing whitespace
1482 value = trim( value );
1484 // allow infinity or hexidecimal
1485 if ( regexInfinity.test(value) ) {
1486 ret = parseFloat( value );
1488 else if ( !radix && regexHex.test(value) ) {
1489 ret = parseInt( value, 16 );
1493 // determine sign and number
1494 var signInfo = parseNegativePattern( value, nf, nf.pattern[0] ),
1495 sign = signInfo[ 0 ],
1496 num = signInfo[ 1 ];
1498 // #44 - try parsing as "(n)"
1499 if ( sign === "" && nf.pattern[0] !== "(n)" ) {
1500 signInfo = parseNegativePattern( value, nf, "(n)" );
1501 sign = signInfo[ 0 ];
1502 num = signInfo[ 1 ];
1505 // try parsing as "-n"
1506 if ( sign === "" && nf.pattern[0] !== "-n" ) {
1507 signInfo = parseNegativePattern( value, nf, "-n" );
1508 sign = signInfo[ 0 ];
1509 num = signInfo[ 1 ];
1514 // determine exponent and number
1517 exponentPos = num.indexOf( "e" );
1518 if ( exponentPos < 0 ) exponentPos = num.indexOf( "E" );
1519 if ( exponentPos < 0 ) {
1520 intAndFraction = num;
1524 intAndFraction = num.substr( 0, exponentPos );
1525 exponent = num.substr( exponentPos + 1 );
1527 // determine decimal position
1531 decimalPos = intAndFraction.indexOf( decSep );
1532 if ( decimalPos < 0 ) {
1533 integer = intAndFraction;
1537 integer = intAndFraction.substr( 0, decimalPos );
1538 fraction = intAndFraction.substr( decimalPos + decSep.length );
1540 // handle groups (e.g. 1,000,000)
1541 var groupSep = nf[ "," ];
1542 integer = integer.split( groupSep ).join( "" );
1543 var altGroupSep = groupSep.replace( /\u00A0/g, " " );
1544 if ( groupSep !== altGroupSep ) {
1545 integer = integer.split( altGroupSep ).join( "" );
1547 // build a natively parsable number string
1548 var p = sign + integer;
1549 if ( fraction !== null ) {
1550 p += "." + fraction;
1552 if ( exponent !== null ) {
1553 // exponent itself may have a number patternd
1554 var expSignInfo = parseNegativePattern( exponent, nf, "-n" );
1555 p += "e" + ( expSignInfo[0] || "+" ) + expSignInfo[ 1 ];
1557 if ( regexParseFloat.test(p) ) {
1558 ret = parseFloat( p );
1564 Globalize.culture = function( cultureSelector ) {
1566 if ( typeof cultureSelector !== "undefined" ) {
1567 this.cultureSelector = cultureSelector;
1570 return this.findClosestCulture( cultureSelector ) || this.culture[ "default" ];