UNPKG

dojo

Version:

Dojo core is a powerful, lightweight library that makes common tasks quicker and easier. Animate elements, manipulate the DOM, and query with easy CSS syntax, all without sacrificing performance.

715 lines (660 loc) 22.4 kB
define([ "../_base/lang", "../_base/array", "../date", /*===== "../_base/declare", =====*/ "../cldr/supplemental", "../i18n", "../regexp", "../string", "../i18n!../cldr/nls/gregorian", "module" ], function(lang, array, date, /*===== declare, =====*/ supplemental, i18n, regexp, string, gregorian, module){ // module: // dojo/date/locale var exports = { // summary: // This modules defines dojo/date/locale, localization methods for Date. }; lang.setObject(module.id.replace(/\//g, "."), exports); // Localization methods for Date. Honor local customs using locale-dependent dojo.cldr data. // Load the bundles containing localization information for // names and formats //NOTE: Everything in this module assumes Gregorian calendars. // Other calendars will be implemented in separate modules. // Format a pattern without literals function formatPattern(dateObject, bundle, options, pattern){ return pattern.replace(/([a-z])\1*/ig, function(match){ var s, pad, c = match.charAt(0), l = match.length, widthList = ["abbr", "wide", "narrow"]; switch(c){ case 'G': s = bundle[(l < 4) ? "eraAbbr" : "eraNames"][dateObject.getFullYear() < 0 ? 0 : 1]; break; case 'y': s = dateObject.getFullYear(); switch(l){ case 1: break; case 2: if(!options.fullYear){ s = String(s); s = s.substr(s.length - 2); break; } // fallthrough default: pad = true; } break; case 'Q': case 'q': s = Math.ceil((dateObject.getMonth()+1)/3); // switch(l){ // case 1: case 2: pad = true; // break; // case 3: case 4: // unimplemented // } break; case 'M': case 'L': var m = dateObject.getMonth(); if(l<3){ s = m+1; pad = true; }else{ var propM = [ "months", c == 'L' ? "standAlone" : "format", widthList[l-3] ].join("-"); s = bundle[propM][m]; } break; case 'w': var firstDay = 0; s = exports._getWeekOfYear(dateObject, firstDay); pad = true; break; case 'd': s = dateObject.getDate(); pad = true; break; case 'D': s = exports._getDayOfYear(dateObject); pad = true; break; case 'e': case 'c': var d = dateObject.getDay(); if(l<2){ s = (d - supplemental.getFirstDayOfWeek(options.locale) + 8) % 7 break; } // fallthrough case 'E': d = dateObject.getDay(); if(l<3){ s = d+1; pad = true; }else{ var propD = [ "days", c == 'c' ? "standAlone" : "format", widthList[l-3] ].join("-"); s = bundle[propD][d]; } break; case 'a': var timePeriod = dateObject.getHours() < 12 ? 'am' : 'pm'; s = options[timePeriod] || bundle['dayPeriods-format-wide-' + timePeriod]; break; case 'h': case 'H': case 'K': case 'k': var h = dateObject.getHours(); // strange choices in the date format make it impossible to write this succinctly switch (c){ case 'h': // 1-12 s = (h % 12) || 12; break; case 'H': // 0-23 s = h; break; case 'K': // 0-11 s = (h % 12); break; case 'k': // 1-24 s = h || 24; break; } pad = true; break; case 'm': s = dateObject.getMinutes(); pad = true; break; case 's': s = dateObject.getSeconds(); pad = true; break; case 'S': s = Math.round(dateObject.getMilliseconds() * Math.pow(10, l-3)); pad = true; break; case 'v': // FIXME: don't know what this is. seems to be same as z? case 'z': // We only have one timezone to offer; the one from the browser s = exports._getZone(dateObject, true, options); if(s){break;} l=4; // fallthrough... use GMT if tz not available case 'Z': var offset = exports._getZone(dateObject, false, options); var tz = [ (offset<=0 ? "+" : "-"), string.pad(Math.floor(Math.abs(offset)/60), 2), string.pad(Math.abs(offset)% 60, 2) ]; if(l==4){ tz.splice(0, 0, "GMT"); tz.splice(3, 0, ":"); } s = tz.join(""); break; // case 'Y': case 'u': case 'W': case 'F': case 'g': case 'A': // console.log(match+" modifier unimplemented"); default: throw new Error("dojo.date.locale.format: invalid pattern char: "+pattern); } if(pad){ s = string.pad(s, l); } return s; }); } /*===== var __FormatOptions = exports.__FormatOptions = declare(null, { // selector: String // choice of 'time','date' (default: date and time) // formatLength: String // choice of long, short, medium or full (plus any custom additions). Defaults to 'short' // datePattern:String // override pattern with this string // timePattern:String // override pattern with this string // am: String // override strings for am in times // pm: String // override strings for pm in times // locale: String // override the locale used to determine formatting rules // fullYear: Boolean // (format only) use 4 digit years whenever 2 digit years are called for // strict: Boolean // (parse only) strict parsing, off by default }); =====*/ exports._getZone = function(/*Date*/ dateObject, /*boolean*/ getName, /*__FormatOptions?*/ options){ // summary: // Returns the zone (or offset) for the given date and options. This // is broken out into a separate function so that it can be overridden // by timezone-aware code. // // dateObject: // the date and/or time being formatted. // // getName: // Whether to return the timezone string (if true), or the offset (if false) // // options: // The options being used for formatting if(getName){ return date.getTimezoneName(dateObject); }else{ return dateObject.getTimezoneOffset(); } }; exports.format = function(/*Date*/ dateObject, /*__FormatOptions?*/ options){ // summary: // Format a Date object as a String, using locale-specific settings. // // description: // Create a string from a Date object using a known localized pattern. // By default, this method formats both date and time from dateObject. // Formatting patterns are chosen appropriate to the locale. Different // formatting lengths may be chosen, with "full" used by default. // Custom patterns may be used or registered with translations using // the dojo/date/locale.addCustomFormats() method. // Formatting patterns are implemented using [the syntax described at // unicode.org](http://www.unicode.org/reports/tr35/tr35-4.html#Date_Format_Patterns) // // dateObject: // the date and/or time to be formatted. If a time only is formatted, // the values in the year, month, and day fields are irrelevant. The // opposite is true when formatting only dates. options = options || {}; var locale = i18n.normalizeLocale(options.locale), formatLength = options.formatLength || 'short', bundle = exports._getGregorianBundle(locale), str = [], sauce = lang.hitch(this, formatPattern, dateObject, bundle, options); if(options.selector == "year"){ return _processPattern(bundle["dateFormatItem-yyyy"] || "yyyy", sauce); } var pattern; if(options.selector != "date"){ pattern = options.timePattern || bundle["timeFormat-"+formatLength]; if(pattern){str.push(_processPattern(pattern, sauce));} } if(options.selector != "time"){ pattern = options.datePattern || bundle["dateFormat-"+formatLength]; if(pattern){str.push(_processPattern(pattern, sauce));} } return str.length == 1 ? str[0] : bundle["dateTimeFormat-"+formatLength].replace(/\'/g,'').replace(/\{(\d+)\}/g, function(match, key){ return str[key]; }); // String }; exports.regexp = function(/*__FormatOptions?*/ options){ // summary: // Builds the regular needed to parse a localized date return exports._parseInfo(options).regexp; // String }; exports._parseInfo = function(/*__FormatOptions?*/ options){ options = options || {}; var locale = i18n.normalizeLocale(options.locale), bundle = exports._getGregorianBundle(locale), formatLength = options.formatLength || 'short', datePattern = options.datePattern || bundle["dateFormat-" + formatLength], timePattern = options.timePattern || bundle["timeFormat-" + formatLength], pattern; if(options.selector == 'date'){ pattern = datePattern; }else if(options.selector == 'time'){ pattern = timePattern; }else{ pattern = bundle["dateTimeFormat-"+formatLength].replace(/\{(\d+)\}/g, function(match, key){ return [timePattern, datePattern][key]; }); } var tokens = [], re = _processPattern(pattern, lang.hitch(this, _buildDateTimeRE, tokens, bundle, options)); return {regexp: re, tokens: tokens, bundle: bundle}; }; exports.parse = function(/*String*/ value, /*__FormatOptions?*/ options){ // summary: // Convert a properly formatted string to a primitive Date object, // using locale-specific settings. // // description: // Create a Date object from a string using a known localized pattern. // By default, this method parses looking for both date and time in the string. // Formatting patterns are chosen appropriate to the locale. Different // formatting lengths may be chosen, with "full" used by default. // Custom patterns may be used or registered with translations using // the dojo/date/locale.addCustomFormats() method. // // Formatting patterns are implemented using [the syntax described at // unicode.org](http://www.unicode.org/reports/tr35/tr35-4.html#Date_Format_Patterns) // When two digit years are used, a century is chosen according to a sliding // window of 80 years before and 20 years after present year, for both `yy` and `yyyy` patterns. // year < 100CE requires strict mode. // // value: // A string representation of a date // remove non-printing bidi control chars from input and pattern var controlChars = /[\u200E\u200F\u202A\u202E]/g, info = exports._parseInfo(options), tokens = info.tokens, bundle = info.bundle, re = new RegExp("^" + info.regexp.replace(controlChars, "") + "$", info.strict ? "" : "i"), match = re.exec(value && value.replace(controlChars, "")); if(!match){ return null; } // null var widthList = ['abbr', 'wide', 'narrow'], result = [1970,0,1,0,0,0,0], // will get converted to a Date at the end amPm = "", valid = array.every(match, function(v, i){ if(!i){return true;} var token = tokens[i-1], l = token.length, c = token.charAt(0); switch(c){ case 'y': if(l != 2 && options.strict){ //interpret year literally, so '5' would be 5 A.D. result[0] = v; }else{ if(v<100){ v = Number(v); //choose century to apply, according to a sliding window //of 80 years before and 20 years after present year var year = '' + new Date().getFullYear(), century = year.substring(0, 2) * 100, cutoff = Math.min(Number(year.substring(2, 4)) + 20, 99); result[0] = (v < cutoff) ? century + v : century - 100 + v; }else{ //we expected 2 digits and got more... if(options.strict){ return false; } //interpret literally, so '150' would be 150 A.D. //also tolerate '1950', if 'yyyy' input passed to 'yy' format result[0] = v; } } break; case 'M': case 'L': if(l>2){ var months = bundle['months-' + (c == 'L' ? 'standAlone' : 'format') + '-' + widthList[l-3]].concat(); if(!options.strict){ //Tolerate abbreviating period in month part //Case-insensitive comparison v = v.replace(".","").toLowerCase(); months = array.map(months, function(s){ return s.replace(".","").toLowerCase(); } ); } v = array.indexOf(months, v); if(v == -1){ // console.log("dojo/date/locale.parse: Could not parse month name: '" + v + "'."); return false; } }else{ v--; } result[1] = v; break; case 'E': case 'e': case 'c': var days = bundle['days-' + (c == 'c' ? 'standAlone' : 'format') + '-' + widthList[l-3]].concat(); if(!options.strict){ //Case-insensitive comparison v = v.toLowerCase(); days = array.map(days, function(d){return d.toLowerCase();}); } v = array.indexOf(days, v); if(v == -1){ // console.log("dojo/date/locale.parse: Could not parse weekday name: '" + v + "'."); return false; } //TODO: not sure what to actually do with this input, //in terms of setting something on the Date obj...? //without more context, can't affect the actual date //TODO: just validate? break; case 'D': result[1] = 0; // fallthrough... case 'd': result[2] = v; break; case 'a': //am/pm var am = options.am || bundle['dayPeriods-format-wide-am'], pm = options.pm || bundle['dayPeriods-format-wide-pm']; if(!options.strict){ var period = /\./g; v = v.replace(period,'').toLowerCase(); am = am.replace(period,'').toLowerCase(); pm = pm.replace(period,'').toLowerCase(); } if(options.strict && v != am && v != pm){ // console.log("dojo/date/locale.parse: Could not parse am/pm part."); return false; } // we might not have seen the hours field yet, so store the state and apply hour change later amPm = (v == pm) ? 'p' : (v == am) ? 'a' : ''; break; case 'K': //hour (1-24) if(v == 24){ v = 0; } // fallthrough... case 'h': //hour (1-12) case 'H': //hour (0-23) case 'k': //hour (0-11) //TODO: strict bounds checking, padding if(v > 23){ // console.log("dojo/date/locale.parse: Illegal hours value"); return false; } //in the 12-hour case, adjusting for am/pm requires the 'a' part //which could come before or after the hour, so we will adjust later result[3] = v; break; case 'm': //minutes result[4] = v; break; case 's': //seconds result[5] = v; break; case 'S': //milliseconds result[6] = v; // break; // case 'w': //TODO var firstDay = 0; // default: //TODO: throw? // console.log("dojo/date/locale.parse: unsupported pattern char=" + token.charAt(0)); } return true; }); var hours = +result[3]; if(amPm === 'p' && hours < 12){ result[3] = hours + 12; //e.g., 3pm -> 15 }else if(amPm === 'a' && hours == 12){ result[3] = 0; //12am -> 0 } //TODO: implement a getWeekday() method in order to test //validity of input strings containing 'EEE' or 'EEEE'... var dateObject = new Date(result[0], result[1], result[2], result[3], result[4], result[5], result[6]); // Date if(options.strict){ dateObject.setFullYear(result[0]); } // Check for overflow. The Date() constructor normalizes things like April 32nd... //TODO: why isn't this done for times as well? var allTokens = tokens.join(""), dateToken = allTokens.indexOf('d') != -1, monthToken = allTokens.indexOf('M') != -1; if(!valid || (monthToken && dateObject.getMonth() > result[1]) || (dateToken && dateObject.getDate() > result[2])){ return null; } // Check for underflow, due to DST shifts. See #9366 // This assumes a 1 hour dst shift correction at midnight // We could compare the timezone offset after the shift and add the difference instead. if((monthToken && dateObject.getMonth() < result[1]) || (dateToken && dateObject.getDate() < result[2])){ dateObject = date.add(dateObject, "hour", 1); } return dateObject; // Date }; function _processPattern(pattern, applyPattern, applyLiteral, applyAll){ //summary: Process a pattern with literals in it // Break up on single quotes, treat every other one as a literal, except '' which becomes ' var identity = function(x){return x;}; applyPattern = applyPattern || identity; applyLiteral = applyLiteral || identity; applyAll = applyAll || identity; //split on single quotes (which escape literals in date format strings) //but preserve escaped single quotes (e.g., o''clock) var chunks = pattern.match(/(''|[^'])+/g), literal = pattern.charAt(0) == "'"; array.forEach(chunks, function(chunk, i){ if(!chunk){ chunks[i]=''; }else{ chunks[i]=(literal ? applyLiteral : applyPattern)(chunk.replace(/''/g, "'")); literal = !literal; } }); return applyAll(chunks.join('')); } var widthList = ['abbr', 'wide', 'narrow']; function _buildDateTimeRE(tokens, bundle, options, pattern){ pattern = regexp.escapeString(pattern); if(!options.strict){ pattern = pattern.replace(" a", " ?a"); } // kludge to tolerate no space before am/pm return pattern.replace(/([a-z])\1*/ig, function(match){ // Build a simple regexp. Avoid captures, which would ruin the tokens list var s, c = match.charAt(0), l = match.length, p2 = '', p3 = ''; if(options.strict){ if(l > 1){ p2 = '0' + '{'+(l-1)+'}'; } if(l > 2){ p3 = '0' + '{'+(l-2)+'}'; } }else{ p2 = '0?'; p3 = '0{0,2}'; } switch(c){ case 'y': s = '\\d{2,4}'; break; case 'M': case 'L': if(l>2){ var months = bundle[ 'months-' + (c == 'L' ? 'standAlone' : 'format') + '-' + widthList[l-3] ].slice(0); s = months.join('|'); if(!options.strict){ s = s.replace(/\./g, ''); //Tolerate abbreviating period in month part s = '(?:' + s + ')\\.?'; } }else{ s = '1[0-2]|'+p2+'[1-9]'; } break; case 'D': s = '[12][0-9][0-9]|3[0-5][0-9]|36[0-6]|'+p2+'[1-9][0-9]|'+p3+'[1-9]'; break; case 'd': s = '3[01]|[12]\\d|'+p2+'[1-9]'; break; case 'w': s = '[1-4][0-9]|5[0-3]|'+p2+'[1-9]'; break; case 'E': case 'e': case 'c': s = '.+?'; // match anything including spaces until the first pattern delimiter is found such as a comma or space break; case 'h': //hour (1-12) s = '1[0-2]|'+p2+'[1-9]'; break; case 'k': //hour (0-11) s = '1[01]|'+p2+'\\d'; break; case 'H': //hour (0-23) s = '1\\d|2[0-3]|'+p2+'\\d'; break; case 'K': //hour (1-24) s = '1\\d|2[0-4]|'+p2+'[1-9]'; break; case 'm': case 's': s = '[0-5]\\d'; break; case 'S': s = '\\d{'+l+'}'; break; case 'a': var am = options.am || bundle['dayPeriods-format-wide-am'], pm = options.pm || bundle['dayPeriods-format-wide-pm']; s = am + '|' + pm; if(!options.strict){ if(am != am.toLowerCase()){ s += '|' + am.toLowerCase(); } if(pm != pm.toLowerCase()){ s += '|' + pm.toLowerCase(); } if(s.indexOf('.') != -1){ s += '|' + s.replace(/\./g, ""); } } s = s.replace(/\./g, "\\."); break; default: // case 'v': // case 'z': // case 'Z': s = ".*"; // console.log("parse of date format, pattern=" + pattern); } if(tokens){ tokens.push(match); } return "(" + s + ")"; // add capture }).replace(/[\xa0 ]/g, "[\\s\\xa0]"); // normalize whitespace. Need explicit handling of \xa0 for IE. } var _customFormats = []; var _cachedGregorianBundles = {}; exports.addCustomFormats = function(/*String*/ packageName, /*String*/ bundleName){ // summary: // Add a reference to a bundle containing localized custom formats to be // used by date/time formatting and parsing routines. // // description: // The user may add custom localized formats where the bundle has properties following the // same naming convention used by dojo.cldr: `dateFormat-xxxx` / `timeFormat-xxxx` // The pattern string should match the format used by the CLDR. // See dojo/date/locale.format() for details. // The resources must be loaded by dojo.requireLocalization() prior to use _customFormats.push({pkg:packageName,name:bundleName}); _cachedGregorianBundles = {}; }; exports._getGregorianBundle = function(/*String*/ locale){ if(_cachedGregorianBundles[locale]){ return _cachedGregorianBundles[locale]; } var gregorian = {}; array.forEach(_customFormats, function(desc){ var bundle = i18n.getLocalization(desc.pkg, desc.name, locale); gregorian = lang.mixin(gregorian, bundle); }, this); return _cachedGregorianBundles[locale] = gregorian; /*Object*/ }; exports.addCustomFormats(module.id.replace(/\/date\/locale$/, ".cldr"),"gregorian"); exports.getNames = function(/*String*/ item, /*String*/ type, /*String?*/ context, /*String?*/ locale){ // summary: // Used to get localized strings from dojo.cldr for day or month names. // // item: // 'months' || 'days' // type: // 'wide' || 'abbr' || 'narrow' (e.g. "Monday", "Mon", or "M" respectively, in English) // context: // 'standAlone' || 'format' (default) // locale: // override locale used to find the names var label, lookup = exports._getGregorianBundle(locale), props = [item, context, type]; if(context == 'standAlone'){ var key = props.join('-'); label = lookup[key]; // Fall back to 'format' flavor of name if(label[0] == 1){ label = undefined; } // kludge, in the absence of real aliasing support in dojo.cldr } props[1] = 'format'; // return by copy so changes won't be made accidentally to the in-memory model return (label || lookup[props.join('-')]).concat(); /*Array*/ }; exports.isWeekend = function(/*Date?*/ dateObject, /*String?*/ locale){ // summary: // Determines if the date falls on a weekend, according to local custom. var weekend = supplemental.getWeekend(locale), day = (dateObject || new Date()).getDay(); if(weekend.end < weekend.start){ weekend.end += 7; if(day < weekend.start){ day += 7; } } return day >= weekend.start && day <= weekend.end; // Boolean }; // These are used only by format and strftime. Do they need to be public? Which module should they go in? exports._getDayOfYear = function(/*Date*/ dateObject){ // summary: // gets the day of the year as represented by dateObject return date.difference(new Date(dateObject.getFullYear(), 0, 1, dateObject.getHours()), dateObject) + 1; // Number }; exports._getWeekOfYear = function(/*Date*/ dateObject, /*Number*/ firstDayOfWeek){ if(arguments.length == 1){ firstDayOfWeek = 0; } // Sunday var firstDayOfYear = new Date(dateObject.getFullYear(), 0, 1).getDay(), adj = (firstDayOfYear - firstDayOfWeek + 7) % 7, week = Math.floor((exports._getDayOfYear(dateObject) + adj - 1) / 7); // if year starts on the specified day, start counting weeks at 1 if(firstDayOfYear == firstDayOfWeek){ week++; } return week; // Number }; return exports; });