/*! * Globalize * * http://github.com/jquery/globalize * * Copyright Software Freedom Conservancy, Inc. * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license */ (function (window, undefined) { var Globalize, // private variables regexHex, regexInfinity, regexParseFloat, regexTrim, // private JavaScript utility functions arrayIndexOf, endsWith, extend, isArray, isFunction, isObject, startsWith, trim, truncate, zeroPad, // private Globalization utility functions appendPreOrPostMatch, expandFormat, formatDate, formatNumber, getTokenRegExp, getEra, getEraYear, parseExact, parseNegativePattern; // Global variable (Globalize) or CommonJS module (globalize) Globalize = function (cultureSelector) { return new Globalize.prototype.init(cultureSelector); }; if (typeof require !== "undefined" && typeof exports !== "undefined" && typeof module !== "undefined") { // Assume CommonJS module.exports = Globalize; } else { // Export as global variable window.Globalize = Globalize; } Globalize.cultures = {}; Globalize.prototype = { constructor: Globalize, init: function (cultureSelector) { this.cultures = Globalize.cultures; this.cultureSelector = cultureSelector; return this; } }; Globalize.prototype.init.prototype = Globalize.prototype; // 1. When defining a culture, all fields are required except the ones stated as optional. // 2. Each culture should have a ".calendars" object with at least one calendar named "standard" // which serves as the default calendar in use by that culture. // 3. Each culture should have a ".calendar" object which is the current calendar being used, // it may be dynamically changed at any time to one of the calendars in ".calendars". Globalize.cultures[ "default" ] = { // A unique name for the culture in the form - name: "en", // the name of the culture in the english language englishName: "English", // the name of the culture in its own language nativeName: "English", // whether the culture uses right-to-left text isRTL: false, // "language" is used for so-called "specific" cultures. // For example, the culture "es-CL" means "Spanish, in Chili". // It represents the Spanish-speaking culture as it is in Chili, // which might have different formatting rules or even translations // than Spanish in Spain. A "neutral" culture is one that is not // specific to a region. For example, the culture "es" is the generic // Spanish culture, which may be a more generalized version of the language // that may or may not be what a specific culture expects. // For a specific culture like "es-CL", the "language" field refers to the // neutral, generic culture information for the language it is using. // This is not always a simple matter of the string before the dash. // For example, the "zh-Hans" culture is netural (Simplified Chinese). // And the "zh-SG" culture is Simplified Chinese in Singapore, whose lanugage // field is "zh-CHS", not "zh". // This field should be used to navigate from a specific culture to it's // more general, neutral culture. If a culture is already as general as it // can get, the language may refer to itself. language: "en", // numberFormat defines general number formatting rules, like the digits in // each grouping, the group separator, and how negative numbers are displayed. numberFormat: { // [negativePattern] // Note, numberFormat.pattern has no "positivePattern" unlike percent and currency, // but is still defined as an array for consistency with them. // negativePattern: one of "(n)|-n|- n|n-|n -" pattern: [ "-n" ], // number of decimal places normally shown decimals: 2, // string that separates number groups, as in 1,000,000 ",": ",", // string that separates a number from the fractional portion, as in 1.99 ".": ".", // array of numbers indicating the size of each number group. // TODO: more detailed description and example groupSizes: [ 3 ], // symbol used for positive numbers "+": "+", // symbol used for negative numbers "-": "-", // symbol used for NaN (Not-A-Number) "NaN": "NaN", // symbol used for Negative Infinity negativeInfinity: "-Infinity", // symbol used for Positive Infinity positiveInfinity: "Infinity", percent: { // [negativePattern, positivePattern] // negativePattern: one of "-n %|-n%|-%n|%-n|%n-|n-%|n%-|-% n|n %-|% n-|% -n|n- %" // positivePattern: one of "n %|n%|%n|% n" pattern: [ "-n %", "n %" ], // number of decimal places normally shown decimals: 2, // array of numbers indicating the size of each number group. // TODO: more detailed description and example groupSizes: [ 3 ], // string that separates number groups, as in 1,000,000 ",": ",", // string that separates a number from the fractional portion, as in 1.99 ".": ".", // symbol used to represent a percentage symbol: "%" }, currency: { // [negativePattern, positivePattern] // negativePattern: one of "($n)|-$n|$-n|$n-|(n$)|-n$|n-$|n$-|-n $|-$ n|n $-|$ n-|$ -n|n- $|($ n)|(n $)" // positivePattern: one of "$n|n$|$ n|n $" pattern: [ "($n)", "$n" ], // number of decimal places normally shown decimals: 2, // array of numbers indicating the size of each number group. // TODO: more detailed description and example groupSizes: [ 3 ], // string that separates number groups, as in 1,000,000 ",": ",", // string that separates a number from the fractional portion, as in 1.99 ".": ".", // symbol used to represent currency symbol: "$" } }, // calendars defines all the possible calendars used by this culture. // There should be at least one defined with name "standard", and is the default // calendar used by the culture. // A calendar contains information about how dates are formatted, information about // the calendar's eras, a standard set of the date formats, // translations for day and month names, and if the calendar is not based on the Gregorian // calendar, conversion functions to and from the Gregorian calendar. calendars: { standard: { // name that identifies the type of calendar this is name: "Gregorian_USEnglish", // separator of parts of a date (e.g. "/" in 11/05/1955) "/": "/", // separator of parts of a time (e.g. ":" in 05:44 PM) ":": ":", // the first day of the week (0 = Sunday, 1 = Monday, etc) firstDay: 0, days: { // full day names names: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], // abbreviated day names namesAbbr: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], // shortest day names namesShort: [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ] }, months: { // full month names (13 months for lunar calendards -- 13th month should be "" if not lunar) names: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "" ], // abbreviated month names namesAbbr: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "" ] }, // AM and PM designators in one of these forms: // The usual view, and the upper and lower case versions // [ standard, lowercase, uppercase ] // The culture does not use AM or PM (likely all standard date formats use 24 hour time) // null AM: [ "AM", "am", "AM" ], PM: [ "PM", "pm", "PM" ], eras: [ // eras in reverse chronological order. // name: the name of the era in this culture (e.g. A.D., C.E.) // start: when the era starts in ticks (gregorian, gmt), null if it is the earliest supported era. // offset: offset in years from gregorian calendar { "name": "A.D.", "start": null, "offset": 0 } ], // when a two digit year is given, it will never be parsed as a four digit // year greater than this year (in the appropriate era for the culture) // Set it as a full year (e.g. 2029) or use an offset format starting from // the current year: "+19" would correspond to 2029 if the current year 2010. twoDigitYearMax: 2029, // set of predefined date and time patterns used by the culture // these represent the format someone in this culture would expect // to see given the portions of the date that are shown. patterns: { // short date pattern d: "M/d/yyyy", // long date pattern D: "dddd, MMMM dd, yyyy", // short time pattern t: "h:mm tt", // long time pattern T: "h:mm:ss tt", // long date, short time pattern f: "dddd, MMMM dd, yyyy h:mm tt", // long date, long time pattern F: "dddd, MMMM dd, yyyy h:mm:ss tt", // month/day pattern M: "MMMM dd", // month/year pattern Y: "yyyy MMMM", // S is a sortable format that does not vary by culture S: "yyyy\u0027-\u0027MM\u0027-\u0027dd\u0027T\u0027HH\u0027:\u0027mm\u0027:\u0027ss" } // optional fields for each calendar: /* monthsGenitive: Same as months but used when the day preceeds the month. Omit if the culture has no genitive distinction in month names. For an explaination of genitive months, see http://blogs.msdn.com/michkap/archive/2004/12/25/332259.aspx convert: Allows for the support of non-gregorian based calendars. This convert object is used to to convert a date to and from a gregorian calendar date to handle parsing and formatting. The two functions: fromGregorian( date ) Given the date as a parameter, return an array with parts [ year, month, day ] corresponding to the non-gregorian based year, month, and day for the calendar. toGregorian( year, month, day ) Given the non-gregorian year, month, and day, return a new Date() object set to the corresponding date in the gregorian calendar. */ } }, // For localized strings messages: {} }; Globalize.cultures[ "default" ].calendar = Globalize.cultures[ "default" ].calendars.standard; Globalize.cultures.en = Globalize.cultures[ "default" ]; Globalize.cultureSelector = "en"; // // private variables // regexHex = /^0x[a-f0-9]+$/i; regexInfinity = /^[+\-]?infinity$/i; regexParseFloat = /^[+\-]?\d*\.?\d*(e[+\-]?\d+)?$/; regexTrim = /^\s+|\s+$/g; // // private JavaScript utility functions // 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; }; endsWith = function (value, pattern) { return value.substr(value.length - pattern.length) === pattern; }; extend = function () { var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation if (typeof target === "boolean") { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if (typeof target !== "object" && !isFunction(target)) { target = {}; } for (; i < length; i++) { // Only deal with non-null/undefined values if ((options = arguments[ i ]) != null) { // Extend the base object for (name in options) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if (target === copy) { continue; } // Recurse if we're merging plain objects or arrays if (deep && copy && ( isObject(copy) || (copyIsArray = isArray(copy)) )) { if (copyIsArray) { copyIsArray = false; clone = src && isArray(src) ? src : []; } else { clone = src && isObject(src) ? src : {}; } // Never move original objects, clone them target[ name ] = extend(deep, clone, copy); // Don't bring in undefined values } else if (copy !== undefined) { target[ name ] = copy; } } } } // Return the modified object return target; }; isArray = Array.isArray || function (obj) { return Object.prototype.toString.call(obj) === "[object Array]"; }; isFunction = function (obj) { return Object.prototype.toString.call(obj) === "[object Function]"; }; isObject = function (obj) { return Object.prototype.toString.call(obj) === "[object Object]"; }; startsWith = function (value, pattern) { return value.indexOf(pattern) === 0; }; trim = function (value) { return ( value + "" ).replace(regexTrim, ""); }; truncate = function (value) { if (isNaN(value)) { return NaN; } return Math[ value < 0 ? "ceil" : "floor" ](value); }; zeroPad = function (str, count, left) { var l; for (l = str.length; l < count; l += 1) { str = ( left ? ("0" + str) : (str + "0") ); } return str; }; // // private Globalization utility functions // appendPreOrPostMatch = function (preMatch, strings) { // appends pre- and post- token match strings while removing escaped characters. // Returns a single quote count which is used to determine if the token occurs // in a string literal. var quoteCount = 0, escaped = false; for (var i = 0, il = preMatch.length; i < il; i++) { var c = preMatch.charAt(i); switch (c) { case "\'": if (escaped) { strings.push("\'"); } else { quoteCount++; } escaped = false; break; case "\\": if (escaped) { strings.push("\\"); } escaped = !escaped; break; default: strings.push(c); escaped = false; break; } } return quoteCount; }; expandFormat = function (cal, format) { // expands unspecified or single character date formats into the full pattern. format = format || "F"; var pattern, patterns = cal.patterns, len = format.length; if (len === 1) { pattern = patterns[ format ]; if (!pattern) { throw "Invalid date format string \'" + format + "\'."; } format = pattern; } else if (len === 2 && format.charAt(0) === "%") { // %X escape format -- intended as a custom format string that is only one character, not a built-in format. format = format.charAt(1); } return format; }; formatDate = function (value, format, culture) { var cal = culture.calendar, convert = cal.convert, ret; if (!format || !format.length || format === "i") { if (culture && culture.name.length) { if (convert) { // non-gregorian calendar, so we cannot use built-in toLocaleString() ret = formatDate(value, cal.patterns.F, culture); } else { var eraDate = new Date(value.getTime()), era = getEra(value, cal.eras); eraDate.setFullYear(getEraYear(value, cal, era)); ret = eraDate.toLocaleString(); } } else { ret = value.toString(); } return ret; } var eras = cal.eras, sortable = format === "s"; format = expandFormat(cal, format); // Start with an empty string ret = []; var hour, zeros = [ "0", "00", "000" ], foundDay, checkedDay, dayPartRegExp = /([^d]|^)(d|dd)([^d]|$)/g, quoteCount = 0, tokenRegExp = getTokenRegExp(), converted; function padZeros(num, c) { var r, s = num + ""; if (c > 1 && s.length < c) { r = ( zeros[c - 2] + s); return r.substr(r.length - c, c); } else { r = s; } return r; } function hasDay() { if (foundDay || checkedDay) { return foundDay; } foundDay = dayPartRegExp.test(format); checkedDay = true; return foundDay; } function getPart(date, part) { if (converted) { return converted[ part ]; } switch (part) { case 0: return date.getFullYear(); case 1: return date.getMonth(); case 2: return date.getDate(); default: throw "Invalid part value " + part; } } if (!sortable && convert) { converted = convert.fromGregorian(value); } for (; ;) { // Save the current index var index = tokenRegExp.lastIndex, // Look for the next pattern ar = tokenRegExp.exec(format); // Append the text before the pattern (or the end of the string if not found) var preMatch = format.slice(index, ar ? ar.index : format.length); quoteCount += appendPreOrPostMatch(preMatch, ret); if (!ar) { break; } // do not replace any matches that occur inside a string literal. if (quoteCount % 2) { ret.push(ar[0]); continue; } var current = ar[ 0 ], clength = current.length; switch (current) { case "ddd": //Day of the week, as a three-letter abbreviation case "dddd": // Day of the week, using the full name var names = ( clength === 3 ) ? cal.days.namesAbbr : cal.days.names; ret.push(names[value.getDay()]); break; case "d": // Day of month, without leading zero for single-digit days case "dd": // Day of month, with leading zero for single-digit days foundDay = true; ret.push( padZeros(getPart(value, 2), clength) ); break; case "MMM": // Month, as a three-letter abbreviation case "MMMM": // Month, using the full name var part = getPart(value, 1); ret.push( ( cal.monthsGenitive && hasDay() ) ? ( cal.monthsGenitive[ clength === 3 ? "namesAbbr" : "names" ][ part ] ) : ( cal.months[ clength === 3 ? "namesAbbr" : "names" ][ part ] ) ); break; case "M": // Month, as digits, with no leading zero for single-digit months case "MM": // Month, as digits, with leading zero for single-digit months ret.push( padZeros(getPart(value, 1) + 1, clength) ); break; case "y": // Year, as two digits, but with no leading zero for years less than 10 case "yy": // Year, as two digits, with leading zero for years less than 10 case "yyyy": // Year represented by four full digits part = converted ? converted[ 0 ] : getEraYear(value, cal, getEra(value, eras), sortable); if (clength < 4) { part = part % 100; } ret.push( padZeros(part, clength) ); break; case "h": // Hours with no leading zero for single-digit hours, using 12-hour clock case "hh": // Hours with leading zero for single-digit hours, using 12-hour clock hour = value.getHours() % 12; if (hour === 0) hour = 12; ret.push( padZeros(hour, clength) ); break; case "H": // Hours with no leading zero for single-digit hours, using 24-hour clock case "HH": // Hours with leading zero for single-digit hours, using 24-hour clock ret.push( padZeros(value.getHours(), clength) ); break; case "m": // Minutes with no leading zero for single-digit minutes case "mm": // Minutes with leading zero for single-digit minutes ret.push( padZeros(value.getMinutes(), clength) ); break; case "s": // Seconds with no leading zero for single-digit seconds case "ss": // Seconds with leading zero for single-digit seconds ret.push( padZeros(value.getSeconds(), clength) ); break; case "t": // One character am/pm indicator ("a" or "p") case "tt": // Multicharacter am/pm indicator part = value.getHours() < 12 ? ( cal.AM ? cal.AM[0] : " " ) : ( cal.PM ? cal.PM[0] : " " ); ret.push(clength === 1 ? part.charAt(0) : part); break; case "f": // Deciseconds case "ff": // Centiseconds case "fff": // Milliseconds ret.push( padZeros(value.getMilliseconds(), 3).substr(0, clength) ); break; case "z": // Time zone offset, no leading zero case "zz": // Time zone offset with leading zero hour = value.getTimezoneOffset() / 60; ret.push( ( hour <= 0 ? "+" : "-" ) + padZeros(Math.floor(Math.abs(hour)), clength) ); break; case "zzz": // Time zone offset with leading zero hour = value.getTimezoneOffset() / 60; ret.push( ( hour <= 0 ? "+" : "-" ) + padZeros(Math.floor(Math.abs(hour)), 2) + // Hard coded ":" separator, rather than using cal.TimeSeparator // Repeated here for consistency, plus ":" was already assumed in date parsing. ":" + padZeros(Math.abs(value.getTimezoneOffset() % 60), 2) ); break; case "g": case "gg": if (cal.eras) { ret.push( cal.eras[ getEra(value, eras) ].name ); } break; case "/": ret.push(cal["/"]); break; default: throw "Invalid date format pattern \'" + current + "\'."; } } return ret.join(""); }; // formatNumber (function () { var expandNumber; expandNumber = function (number, precision, formatInfo) { var groupSizes = formatInfo.groupSizes, curSize = groupSizes[ 0 ], curGroupIndex = 1, factor = Math.pow(10, precision), rounded = Math.round(number * factor) / factor; if (!isFinite(rounded)) { rounded = number; } number = rounded; var numberString = number + "", right = "", split = numberString.split(/e/i), exponent = split.length > 1 ? parseInt(split[1], 10) : 0; numberString = split[ 0 ]; split = numberString.split("."); numberString = split[ 0 ]; right = split.length > 1 ? split[ 1 ] : ""; var l; if (exponent > 0) { right = zeroPad(right, exponent, false); numberString += right.slice(0, exponent); right = right.substr(exponent); } else if (exponent < 0) { exponent = -exponent; numberString = zeroPad(numberString, exponent + 1, true); right = numberString.slice(-exponent, numberString.length) + right; numberString = numberString.slice(0, -exponent); } if (precision > 0) { right = formatInfo[ "." ] + ( (right.length > precision) ? right.slice(0, precision) : zeroPad(right, precision) ); } else { right = ""; } var stringIndex = numberString.length - 1, sep = formatInfo[ "," ], ret = ""; while (stringIndex >= 0) { if (curSize === 0 || curSize > stringIndex) { return numberString.slice(0, stringIndex + 1) + ( ret.length ? (sep + ret + right) : right ); } ret = numberString.slice(stringIndex - curSize + 1, stringIndex + 1) + ( ret.length ? (sep + ret) : "" ); stringIndex -= curSize; if (curGroupIndex < groupSizes.length) { curSize = groupSizes[ curGroupIndex ]; curGroupIndex++; } } return numberString.slice(0, stringIndex + 1) + sep + ret + right; }; formatNumber = function (value, format, culture) { if (!isFinite(value)) { if (value === Infinity) { return culture.numberFormat.positiveInfinity; } if (value === -Infinity) { return culture.numberFormat.negativeInfinity; } return culture.numberFormat.NaN; } if (!format || format === "i") { return culture.name.length ? value.toLocaleString() : value.toString(); } format = format || "D"; var nf = culture.numberFormat, number = Math.abs(value), precision = -1, pattern; if (format.length > 1) precision = parseInt(format.slice(1), 10); var current = format.charAt(0).toUpperCase(), formatInfo; switch (current) { case "D": pattern = "n"; number = truncate(number); if (precision !== -1) { number = zeroPad("" + number, precision, true); } if (value < 0) number = "-" + number; break; case "N": formatInfo = nf; /* falls through */ case "C": formatInfo = formatInfo || nf.currency; /* falls through */ case "P": formatInfo = formatInfo || nf.percent; pattern = value < 0 ? formatInfo.pattern[ 0 ] : ( formatInfo.pattern[1] || "n" ); if (precision === -1) precision = formatInfo.decimals; number = expandNumber(number * (current === "P" ? 100 : 1), precision, formatInfo); break; default: throw "Bad number format specifier: " + current; } var patternParts = /n|\$|-|%/g, ret = ""; for (; ;) { var index = patternParts.lastIndex, ar = patternParts.exec(pattern); ret += pattern.slice(index, ar ? ar.index : pattern.length); if (!ar) { break; } switch (ar[0]) { case "n": ret += number; break; case "$": ret += nf.currency.symbol; break; case "-": // don't make 0 negative if (/[1-9]/.test(number)) { ret += nf[ "-" ]; } break; case "%": ret += nf.percent.symbol; break; } } return ret; }; }()); getTokenRegExp = function () { // regular expression for matching date and time tokens in format strings. 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); }; getEra = function (date, eras) { if (!eras) return 0; var start, ticks = date.getTime(); for (var i = 0, l = eras.length; i < l; i++) { start = eras[ i ].start; if (start === null || ticks >= start) { return i; } } return 0; }; getEraYear = function (date, cal, era, sortable) { var year = date.getFullYear(); if (!sortable && cal.eras) { // convert normal gregorian year to era-shifted gregorian // year by subtracting the era offset year -= cal.eras[ era ].offset; } return year; }; // parseExact (function () { var expandYear, getDayIndex, getMonthIndex, getParseRegExp, outOfRange, toUpper, toUpperArray; expandYear = function (cal, year) { // expands 2-digit year into 4 digits. if (year < 100) { var now = new Date(), era = getEra(now), curr = getEraYear(now, cal, era), twoDigitYearMax = cal.twoDigitYearMax; twoDigitYearMax = typeof twoDigitYearMax === "string" ? new Date().getFullYear() % 100 + parseInt(twoDigitYearMax, 10) : twoDigitYearMax; year += curr - ( curr % 100 ); if (year > twoDigitYearMax) { year -= 100; } } return year; }; getDayIndex = function (cal, value, abbr) { var ret, days = cal.days, upperDays = cal._upperDays; if (!upperDays) { cal._upperDays = upperDays = [ toUpperArray(days.names), toUpperArray(days.namesAbbr), toUpperArray(days.namesShort) ]; } value = toUpper(value); if (abbr) { ret = arrayIndexOf(upperDays[1], value); if (ret === -1) { ret = arrayIndexOf(upperDays[2], value); } } else { ret = arrayIndexOf(upperDays[0], value); } return ret; }; getMonthIndex = function (cal, value, abbr) { var months = cal.months, monthsGen = cal.monthsGenitive || cal.months, upperMonths = cal._upperMonths, upperMonthsGen = cal._upperMonthsGen; if (!upperMonths) { cal._upperMonths = upperMonths = [ toUpperArray(months.names), toUpperArray(months.namesAbbr) ]; cal._upperMonthsGen = upperMonthsGen = [ toUpperArray(monthsGen.names), toUpperArray(monthsGen.namesAbbr) ]; } value = toUpper(value); var i = arrayIndexOf(abbr ? upperMonths[1] : upperMonths[0], value); if (i < 0) { i = arrayIndexOf(abbr ? upperMonthsGen[1] : upperMonthsGen[0], value); } return i; }; getParseRegExp = function (cal, format) { // converts a format string into a regular expression with groups that // can be used to extract date fields from a date string. // check for a cached parse regex. var re = cal._parseRegExp; if (!re) { cal._parseRegExp = re = {}; } else { var reFormat = re[ format ]; if (reFormat) { return reFormat; } } // expand single digit formats, then escape regular expression characters. var expFormat = expandFormat(cal, format).replace(/([\^\$\.\*\+\?\|\[\]\(\)\{\}])/g, "\\\\$1"), regexp = [ "^" ], groups = [], index = 0, quoteCount = 0, tokenRegExp = getTokenRegExp(), match; // iterate through each date token found. while ((match = tokenRegExp.exec(expFormat)) !== null) { var preMatch = expFormat.slice(index, match.index); index = tokenRegExp.lastIndex; // don't replace any matches that occur inside a string literal. quoteCount += appendPreOrPostMatch(preMatch, regexp); if (quoteCount % 2) { regexp.push(match[0]); continue; } // add a regex group for the token. var m = match[ 0 ], len = m.length, add; switch (m) { case "dddd": case "ddd": case "MMMM": case "MMM": case "gg": case "g": add = "(\\D+)"; break; case "tt": case "t": add = "(\\D*)"; break; case "yyyy": case "fff": case "ff": case "f": add = "(\\d{" + len + "})"; break; case "dd": case "d": case "MM": case "M": case "yy": case "y": case "HH": case "H": case "hh": case "h": case "mm": case "m": case "ss": case "s": add = "(\\d\\d?)"; break; case "zzz": add = "([+-]?\\d\\d?:\\d{2})"; break; case "zz": case "z": add = "([+-]?\\d\\d?)"; break; case "/": add = "(\\/)"; break; default: throw "Invalid date format pattern \'" + m + "\'."; } if (add) { regexp.push(add); } groups.push(match[0]); } appendPreOrPostMatch(expFormat.slice(index), regexp); regexp.push("$"); // allow whitespace to differ when matching formats. var regexpStr = regexp.join("").replace(/\s+/g, "\\s+"), parseRegExp = { "regExp": regexpStr, "groups": groups }; // cache the regex for this format. return re[ format ] = parseRegExp; }; outOfRange = function (value, low, high) { return value < low || value > high; }; toUpper = function (value) { // "he-IL" has non-breaking space in weekday names. return value.split("\u00A0").join(" ").toUpperCase(); }; toUpperArray = function (arr) { var results = []; for (var i = 0, l = arr.length; i < l; i++) { results[ i ] = toUpper(arr[i]); } return results; }; parseExact = function (value, format, culture) { // try to parse the date string by matching against the format string // while using the specified culture for date field names. value = trim(value); var cal = culture.calendar, // convert date formats into regular expressions with groupings. // use the regexp to determine the input format and extract the date fields. parseInfo = getParseRegExp(cal, format), match = new RegExp(parseInfo.regExp).exec(value); if (match === null) { return null; } // found a date format that matches the input. var groups = parseInfo.groups, era = null, year = null, month = null, date = null, weekDay = null, hour = 0, hourOffset, min = 0, sec = 0, msec = 0, tzMinOffset = null, pmHour = false; // iterate the format groups to extract and set the date fields. for (var j = 0, jl = groups.length; j < jl; j++) { var matchGroup = match[ j + 1 ]; if (matchGroup) { var current = groups[ j ], clength = current.length, matchInt = parseInt(matchGroup, 10); switch (current) { case "dd": case "d": // Day of month. date = matchInt; // check that date is generally in valid range, also checking overflow below. if (outOfRange(date, 1, 31)) return null; break; case "MMM": case "MMMM": month = getMonthIndex(cal, matchGroup, clength === 3); if (outOfRange(month, 0, 11)) return null; break; case "M": case "MM": // Month. month = matchInt - 1; if (outOfRange(month, 0, 11)) return null; break; case "y": case "yy": case "yyyy": year = clength < 4 ? expandYear(cal, matchInt) : matchInt; if (outOfRange(year, 0, 9999)) return null; break; case "h": case "hh": // Hours (12-hour clock). hour = matchInt; if (hour === 12) hour = 0; if (outOfRange(hour, 0, 11)) return null; break; case "H": case "HH": // Hours (24-hour clock). hour = matchInt; if (outOfRange(hour, 0, 23)) return null; break; case "m": case "mm": // Minutes. min = matchInt; if (outOfRange(min, 0, 59)) return null; break; case "s": case "ss": // Seconds. sec = matchInt; if (outOfRange(sec, 0, 59)) return null; break; case "tt": case "t": // AM/PM designator. // see if it is standard, upper, or lower case PM. If not, ensure it is at least one of // the AM tokens. If not, fail the parse for this format. pmHour = cal.PM && ( matchGroup === cal.PM[0] || matchGroup === cal.PM[1] || matchGroup === cal.PM[2] ); if ( !pmHour && ( !cal.AM || ( matchGroup !== cal.AM[0] && matchGroup !== cal.AM[1] && matchGroup !== cal.AM[2] ) ) ) return null; break; case "f": // Deciseconds. case "ff": // Centiseconds. case "fff": // Milliseconds. msec = matchInt * Math.pow(10, 3 - clength); if (outOfRange(msec, 0, 999)) return null; break; case "ddd": // Day of week. case "dddd": // Day of week. weekDay = getDayIndex(cal, matchGroup, clength === 3); if (outOfRange(weekDay, 0, 6)) return null; break; case "zzz": // Time zone offset in +/- hours:min. var offsets = matchGroup.split(/:/); if (offsets.length !== 2) return null; hourOffset = parseInt(offsets[0], 10); if (outOfRange(hourOffset, -12, 13)) return null; var minOffset = parseInt(offsets[1], 10); if (outOfRange(minOffset, 0, 59)) return null; tzMinOffset = ( hourOffset * 60 ) + ( startsWith(matchGroup, "-") ? -minOffset : minOffset ); break; case "z": case "zz": // Time zone offset in +/- hours. hourOffset = matchInt; if (outOfRange(hourOffset, -12, 13)) return null; tzMinOffset = hourOffset * 60; break; case "g": case "gg": var eraName = matchGroup; if (!eraName || !cal.eras) return null; eraName = trim(eraName.toLowerCase()); for (var i = 0, l = cal.eras.length; i < l; i++) { if (eraName === cal.eras[i].name.toLowerCase()) { era = i; break; } } // could not find an era with that name if (era === null) return null; break; } } } var result = new Date(), defaultYear, convert = cal.convert; defaultYear = convert ? convert.fromGregorian(result)[ 0 ] : result.getFullYear(); if (year === null) { year = defaultYear; } else if (cal.eras) { // year must be shifted to normal gregorian year // but not if year was not specified, its already normal gregorian // per the main if clause above. year += cal.eras[( era || 0 )].offset; } // set default day and month to 1 and January, so if unspecified, these are the defaults // instead of the current day/month. if (month === null) { month = 0; } if (date === null) { date = 1; } // now have year, month, and date, but in the culture's calendar. // convert to gregorian if necessary if (convert) { result = convert.toGregorian(year, month, date); // conversion failed, must be an invalid match if (result === null) return null; } else { // have to set year, month and date together to avoid overflow based on current date. result.setFullYear(year, month, date); // check to see if date overflowed for specified month (only checked 1-31 above). if (result.getDate() !== date) return null; // invalid day of week. if (weekDay !== null && result.getDay() !== weekDay) { return null; } } // if pm designator token was found make sure the hours fit the 24-hour clock. if (pmHour && hour < 12) { hour += 12; } result.setHours(hour, min, sec, msec); if (tzMinOffset !== null) { // adjust timezone to utc before applying local offset. var adjustedMin = result.getMinutes() - ( tzMinOffset + result.getTimezoneOffset() ); // Safari limits hours and minutes to the range of -127 to 127. We need to use setHours // to ensure both these fields will not exceed this range. adjustedMin will range // somewhere between -1440 and 1500, so we only need to split this into hours. result.setHours(result.getHours() + parseInt(adjustedMin / 60, 10), adjustedMin % 60); } return result; }; }()); parseNegativePattern = function (value, nf, negativePattern) { var neg = nf[ "-" ], pos = nf[ "+" ], ret; switch (negativePattern) { case "n -": neg = " " + neg; pos = " " + pos; /* falls through */ case "n-": if (endsWith(value, neg)) { ret = [ "-", value.substr(0, value.length - neg.length) ]; } else if (endsWith(value, pos)) { ret = [ "+", value.substr(0, value.length - pos.length) ]; } break; case "- n": neg += " "; pos += " "; /* falls through */ case "-n": if (startsWith(value, neg)) { ret = [ "-", value.substr(neg.length) ]; } else if (startsWith(value, pos)) { ret = [ "+", value.substr(pos.length) ]; } break; case "(n)": if (startsWith(value, "(") && endsWith(value, ")")) { ret = [ "-", value.substr(1, value.length - 2) ]; } break; } return ret || [ "", value ]; }; // // public instance functions // Globalize.prototype.findClosestCulture = function (cultureSelector) { return Globalize.findClosestCulture.call(this, cultureSelector); }; Globalize.prototype.format = function (value, format, cultureSelector) { return Globalize.format.call(this, value, format, cultureSelector); }; Globalize.prototype.localize = function (key, cultureSelector) { return Globalize.localize.call(this, key, cultureSelector); }; Globalize.prototype.parseInt = function (value, radix, cultureSelector) { return Globalize.parseInt.call(this, value, radix, cultureSelector); }; Globalize.prototype.parseFloat = function (value, radix, cultureSelector) { return Globalize.parseFloat.call(this, value, radix, cultureSelector); }; Globalize.prototype.culture = function (cultureSelector) { return Globalize.culture.call(this, cultureSelector); }; // // public singleton functions // Globalize.addCultureInfo = function (cultureName, baseCultureName, info) { var base = {}, isNew = false; if (typeof cultureName !== "string") { // cultureName argument is optional string. If not specified, assume info is first // and only argument. Specified info deep-extends current culture. info = cultureName; cultureName = this.culture().name; base = this.cultures[ cultureName ]; } else if (typeof baseCultureName !== "string") { // baseCultureName argument is optional string. If not specified, assume info is second // argument. Specified info deep-extends specified culture. // If specified culture does not exist, create by deep-extending default info = baseCultureName; isNew = ( this.cultures[ cultureName ] == null ); base = this.cultures[ cultureName ] || this.cultures[ "default" ]; } else { // cultureName and baseCultureName specified. Assume a new culture is being created // by deep-extending an specified base culture isNew = true; base = this.cultures[ baseCultureName ]; } this.cultures[ cultureName ] = extend(true, {}, base, info ); // Make the standard calendar the current culture if it's a new culture if (isNew) { this.cultures[ cultureName ].calendar = this.cultures[ cultureName ].calendars.standard; } }; Globalize.findClosestCulture = function (name) { var match; if (!name) { return this.findClosestCulture(this.cultureSelector) || this.cultures[ "default" ]; } if (typeof name === "string") { name = name.split(","); } if (isArray(name)) { var lang, cultures = this.cultures, list = name, i, l = list.length, prioritized = []; for (i = 0; i < l; i++) { name = trim(list[i]); var pri, parts = name.split(";"); lang = trim(parts[0]); if (parts.length === 1) { pri = 1; } else { name = trim(parts[1]); if (name.indexOf("q=") === 0) { name = name.substr(2); pri = parseFloat(name); pri = isNaN(pri) ? 0 : pri; } else { pri = 1; } } prioritized.push({ lang: lang, pri: pri }); } prioritized.sort(function (a, b) { if (a.pri < b.pri) { return 1; } else if (a.pri > b.pri) { return -1; } return 0; }); // exact match for (i = 0; i < l; i++) { lang = prioritized[ i ].lang; match = cultures[ lang ]; if (match) { return match; } } // neutral language match for (i = 0; i < l; i++) { lang = prioritized[ i ].lang; do { var index = lang.lastIndexOf("-"); if (index === -1) { break; } // strip off the last part. e.g. en-US => en lang = lang.substr(0, index); match = cultures[ lang ]; if (match) { return match; } } while (1); } // last resort: match first culture using that language for (i = 0; i < l; i++) { lang = prioritized[ i ].lang; for (var cultureKey in cultures) { var culture = cultures[ cultureKey ]; if (culture.language == lang) { return culture; } } } } else if (typeof name === "object") { return name; } return match || null; }; Globalize.format = function (value, format, cultureSelector) { var culture = this.findClosestCulture(cultureSelector); if (value instanceof Date) { value = formatDate(value, format, culture); } else if (typeof value === "number") { value = formatNumber(value, format, culture); } return value; }; Globalize.localize = function (key, cultureSelector) { return this.findClosestCulture(cultureSelector).messages[ key ] || this.cultures[ "default" ].messages[ key ]; }; Globalize.parseDate = function (value, formats, culture) { culture = this.findClosestCulture(culture); var date, prop, patterns; if (formats) { if (typeof formats === "string") { formats = [ formats ]; } if (formats.length) { for (var i = 0, l = formats.length; i < l; i++) { var format = formats[ i ]; if (format) { date = parseExact(value, format, culture); if (date) { break; } } } } } else { patterns = culture.calendar.patterns; for (prop in patterns) { date = parseExact(value, patterns[prop], culture); if (date) { break; } } } return date || null; }; Globalize.parseInt = function (value, radix, cultureSelector) { return truncate(Globalize.parseFloat(value, radix, cultureSelector)); }; Globalize.parseFloat = function (value, radix, cultureSelector) { // radix argument is optional if (typeof radix !== "number") { cultureSelector = radix; radix = 10; } var culture = this.findClosestCulture(cultureSelector); var ret = NaN, nf = culture.numberFormat; if (value.indexOf(culture.numberFormat.currency.symbol) > -1) { // remove currency symbol value = value.replace(culture.numberFormat.currency.symbol, ""); // replace decimal seperator value = value.replace(culture.numberFormat.currency["."], culture.numberFormat["."]); } //Remove percentage character from number string before parsing if (value.indexOf(culture.numberFormat.percent.symbol) > -1) { value = value.replace(culture.numberFormat.percent.symbol, ""); } // remove spaces: leading, trailing and between - and number. Used for negative currency pt-BR value = value.replace(/ /g, ""); // allow infinity or hexidecimal if (regexInfinity.test(value)) { ret = parseFloat(value); } else if (!radix && regexHex.test(value)) { ret = parseInt(value, 16); } else { // determine sign and number var signInfo = parseNegativePattern(value, nf, nf.pattern[0]), sign = signInfo[ 0 ], num = signInfo[ 1 ]; // #44 - try parsing as "(n)" if (sign === "" && nf.pattern[0] !== "(n)") { signInfo = parseNegativePattern(value, nf, "(n)"); sign = signInfo[ 0 ]; num = signInfo[ 1 ]; } // try parsing as "-n" if (sign === "" && nf.pattern[0] !== "-n") { signInfo = parseNegativePattern(value, nf, "-n"); sign = signInfo[ 0 ]; num = signInfo[ 1 ]; } sign = sign || "+"; // determine exponent and number var exponent, intAndFraction, exponentPos = num.indexOf("e"); if (exponentPos < 0) exponentPos = num.indexOf("E"); if (exponentPos < 0) { intAndFraction = num; exponent = null; } else { intAndFraction = num.substr(0, exponentPos); exponent = num.substr(exponentPos + 1); } // determine decimal position var integer, fraction, decSep = nf[ "." ], decimalPos = intAndFraction.indexOf(decSep); if (decimalPos < 0) { integer = intAndFraction; fraction = null; } else { integer = intAndFraction.substr(0, decimalPos); fraction = intAndFraction.substr(decimalPos + decSep.length); } // handle groups (e.g. 1,000,000) var groupSep = nf[ "," ]; integer = integer.split(groupSep).join(""); var altGroupSep = groupSep.replace(/\u00A0/g, " "); if (groupSep !== altGroupSep) { integer = integer.split(altGroupSep).join(""); } // build a natively parsable number string var p = sign + integer; if (fraction !== null) { p += "." + fraction; } if (exponent !== null) { // exponent itself may have a number patternd var expSignInfo = parseNegativePattern(exponent, nf, "-n"); p += "e" + ( expSignInfo[0] || "+" ) + expSignInfo[ 1 ]; } if (regexParseFloat.test(p)) { ret = parseFloat(p); } } return ret; }; Globalize.culture = function (cultureSelector) { // setter if (typeof cultureSelector !== "undefined") { this.cultureSelector = cultureSelector; } // getter return this.findClosestCulture(cultureSelector) || this.cultures[ "default" ]; }; }(this));