UNPKG

sugar

Version:

A Javascript library for working with native objects.

1,615 lines (1,421 loc) 84.7 kB
/*** * @package Core * @description Internal utility and common methods. ***/ // A few optimizations for Google Closure Compiler will save us a couple kb in the release script. var object = Object, array = Array, regexp = RegExp, date = Date, string = String, number = Number, math = Math, Undefined; // The global context var globalContext = typeof global !== 'undefined' ? global : this; // defineProperty exists in IE8 but will error when trying to define a property on // native objects. IE8 does not have defineProperies, however, so this check saves a try/catch block. var definePropertySupport = object.defineProperty && object.defineProperties; // Class initializers and class helpers var ClassNames = 'Array,Boolean,Date,Function,Number,String,RegExp'.split(','); var isArray = buildClassCheck(ClassNames[0]); var isBoolean = buildClassCheck(ClassNames[1]); var isDate = buildClassCheck(ClassNames[2]); var isFunction = buildClassCheck(ClassNames[3]); var isNumber = buildClassCheck(ClassNames[4]); var isString = buildClassCheck(ClassNames[5]); var isRegExp = buildClassCheck(ClassNames[6]); function buildClassCheck(type) { return function(obj) { return className(obj) === '[object '+type+']'; } } function className(obj) { return object.prototype.toString.call(obj); } function initializeClasses() { initializeClass(object); iterateOverObject(ClassNames, function(i,name) { initializeClass(globalContext[name]); }); } function initializeClass(klass) { if(klass['SugarMethods']) return; defineProperty(klass, 'SugarMethods', {}); extend(klass, false, false, { 'restore': function() { var all = arguments.length === 0, methods = multiArgs(arguments); iterateOverObject(klass['SugarMethods'], function(name, m) { if(all || methods.indexOf(name) > -1) { defineProperty(m.instance ? klass.prototype : klass, name, m.method); } }); }, 'extend': function(methods, override, instance) { extend(klass, instance !== false, override, methods); } }); } // Class extending methods function extend(klass, instance, override, methods) { var extendee = instance ? klass.prototype : klass, original; initializeClass(klass); iterateOverObject(methods, function(name, method) { original = extendee[name]; if(typeof override === 'function') { method = wrapNative(extendee[name], method, override); } if(override !== false || !extendee[name]) { defineProperty(extendee, name, method); } // If the method is internal to Sugar, then store a reference so it can be restored later. klass['SugarMethods'][name] = { instance: instance, method: method, original: original }; }); } function extendSimilar(klass, instance, override, set, fn) { var methods = {}; set = isString(set) ? set.split(',') : set; set.forEach(function(name, i) { fn(methods, name, i); }); extend(klass, instance, override, methods); } function wrapNative(nativeFn, extendedFn, condition) { return function() { if(nativeFn && (condition === true || !condition.apply(this, arguments))) { return nativeFn.apply(this, arguments); } else { return extendedFn.apply(this, arguments); } } } function defineProperty(target, name, method) { if(definePropertySupport) { object.defineProperty(target, name, { 'value': method, 'configurable': true, 'enumerable': false, 'writable': true }); } else { target[name] = method; } } // Argument helpers function multiArgs(args, fn) { var result = [], i; for(i = 0; i < args.length; i++) { result.push(args[i]); if(fn) fn.call(args, args[i], i); } return result; } function checkCallback(fn) { if(!fn || !fn.call) { throw new TypeError('Callback is not callable'); } } // General helpers function isDefined(o) { return o !== Undefined; } function isUndefined(o) { return o === Undefined; } // Object helpers function isObjectPrimitive(obj) { // Check for null return obj && typeof obj === 'object'; } function isObject(obj) { // === on the constructor is not safe across iframes // 'hasOwnProperty' ensures that the object also inherits // from Object, which is false for DOMElements in IE. return !!obj && className(obj) === '[object Object]' && 'hasOwnProperty' in obj; } function hasOwnProperty(obj, key) { return object['hasOwnProperty'].call(obj, key); } function iterateOverObject(obj, fn) { var key; for(key in obj) { if(!hasOwnProperty(obj, key)) continue; if(fn.call(obj, key, obj[key], obj) === false) break; } } function simpleMerge(target, source) { iterateOverObject(source, function(key) { target[key] = source[key]; }); return target; } // Hash definition function Hash(obj) { simpleMerge(this, obj); }; Hash.prototype.constructor = object; // Number helpers function getRange(start, stop, fn, step) { var arr = [], i = parseInt(start), down = step < 0; while((!down && i <= stop) || (down && i >= stop)) { arr.push(i); if(fn) fn.call(this, i); i += step || 1; } return arr; } function round(val, precision, method) { var fn = math[method || 'round']; var multiplier = math.pow(10, math.abs(precision || 0)); if(precision < 0) multiplier = 1 / multiplier; return fn(val * multiplier) / multiplier; } function ceil(val, precision) { return round(val, precision, 'ceil'); } function floor(val, precision) { return round(val, precision, 'floor'); } function padNumber(num, place, sign, base) { var str = math.abs(num).toString(base || 10); str = repeatString(place - str.replace(/\.\d+/, '').length, '0') + str; if(sign || num < 0) { str = (num < 0 ? '-' : '+') + str; } return str; } function getOrdinalizedSuffix(num) { if(num >= 11 && num <= 13) { return 'th'; } else { switch(num % 10) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; default: return 'th'; } } } // String helpers // WhiteSpace/LineTerminator as defined in ES5.1 plus Unicode characters in the Space, Separator category. function getTrimmableCharacters() { return '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u2028\u2029\u3000\uFEFF'; } function repeatString(times, str) { return array(math.max(0, isDefined(times) ? times : 1) + 1).join(str || ''); } // RegExp helpers function getRegExpFlags(reg, add) { var flags = reg.toString().match(/[^/]*$/)[0]; if(add) { flags = (flags + add).split('').sort().join('').replace(/([gimy])\1+/g, '$1'); } return flags; } function escapeRegExp(str) { if(!isString(str)) str = string(str); return str.replace(/([\\/'*+?|()\[\]{}.^$])/g,'\\$1'); } // Specialized helpers // Used by Array#unique and Object.equal function stringify(thing, stack) { var type = typeof thing, thingIsObject, thingIsArray, klass, value, arr, key, i; // Return quickly if string to save cycles if(type === 'string') return thing; klass = object.prototype.toString.call(thing) thingIsObject = isObject(thing); thingIsArray = klass === '[object Array]'; if(thing != null && thingIsObject || thingIsArray) { // This method for checking for cyclic structures was egregiously stolen from // the ingenious method by @kitcambridge from the Underscore script: // https://github.com/documentcloud/underscore/issues/240 if(!stack) stack = []; // Allowing a step into the structure before triggering this // script to save cycles on standard JSON structures and also to // try as hard as possible to catch basic properties that may have // been modified. if(stack.length > 1) { i = stack.length; while (i--) { if (stack[i] === thing) { return 'CYC'; } } } stack.push(thing); value = string(thing.constructor); arr = thingIsArray ? thing : object.keys(thing).sort(); for(i = 0; i < arr.length; i++) { key = thingIsArray ? i : arr[i]; value += key + stringify(thing[key], stack); } stack.pop(); } else if(1 / thing === -Infinity) { value = '-0'; } else { value = string(thing && thing.valueOf ? thing.valueOf() : thing); } return type + klass + value; } function isEqual(a, b) { if(objectIsMatchedByValue(a) && objectIsMatchedByValue(b)) { return stringify(a) === stringify(b); } else { return a === b; } } function objectIsMatchedByValue(obj) { var klass = className(obj); return klass === '[object Date]' || klass === '[object Array]' || klass === '[object String]' || klass === '[object Number]' || klass === '[object RegExp]' || klass === '[object Boolean]' || klass === '[object Arguments]' || isObject(obj); } // Used by Array#at and String#at function entryAtIndex(arr, args, str) { var result = [], length = arr.length, loop = args[args.length - 1] !== false, r; multiArgs(args, function(index) { if(isBoolean(index)) return false; if(loop) { index = index % length; if(index < 0) index = length + index; } r = str ? arr.charAt(index) || '' : arr[index]; result.push(r); }); return result.length < 2 ? result[0] : result; } // Object class methods implemented as instance methods function buildObjectInstanceMethods(set, target) { extendSimilar(target, true, false, set, function(methods, name) { methods[name + (name === 'equal' ? 's' : '')] = function() { return object[name].apply(null, [this].concat(multiArgs(arguments))); } }); } initializeClasses(); /*** * @package Date * @dependency core * @description Date parsing and formatting, relative formats like "1 minute ago", Number methods like "daysAgo", localization support with default English locale definition. * ***/ var English; var CurrentLocalization; var TimeFormat = ['ampm','hour','minute','second','ampm','utc','offset_sign','offset_hours','offset_minutes','ampm'] var DecimalReg = '(?:[,.]\\d+)?'; var HoursReg = '2[0-4]|[01]?\\d' + DecimalReg; var SixtyReg = '[0-5]\\d' + DecimalReg; var RequiredTime = '({t})?\\s*('+HoursReg+')(?:{h}('+SixtyReg+')?{m}(?::?('+SixtyReg+'){s})?\\s*(?:({t})|(Z)|(?:([+-])(\\d{2,2})(?::?(\\d{2,2}))?)?)?|\\s*({t}))'; var KanjiDigits = '〇一二三四五六七八九十百千万'; var FullWidthDigits = '0123456789'; var AsianDigitMap = {}; var AsianDigitReg; var DateArgumentUnits; var DateUnitsReversed; var CoreDateFormats = []; var DateOutputFormats = [ { token: 'f{1,4}|ms|milliseconds', format: function(d) { return callDateGet(d, 'Milliseconds'); } }, { token: 'ss?|seconds', format: function(d, len) { return callDateGet(d, 'Seconds'); } }, { token: 'mm?|minutes', format: function(d, len) { return callDateGet(d, 'Minutes'); } }, { token: 'hh?|hours|12hr', format: function(d) { return getShortHour(d); } }, { token: 'HH?|24hr', format: function(d) { return callDateGet(d, 'Hours'); } }, { token: 'dd?|date|day', format: function(d) { return callDateGet(d, 'Date'); } }, { token: 'dow|weekday', word: true, format: function(d, loc, n, t) { var dow = callDateGet(d, 'Day'); return loc['weekdays'][dow + (n - 1) * 7]; } }, { token: 'MM?', format: function(d) { return callDateGet(d, 'Month') + 1; } }, { token: 'mon|month', word: true, format: function(d, loc, n, len) { var month = callDateGet(d, 'Month'); return loc['months'][month + (n - 1) * 12]; } }, { token: 'y{2,4}|year', format: function(d) { return callDateGet(d, 'FullYear'); } }, { token: '[Tt]{1,2}', format: function(d, loc, n, format) { if(loc['ampm'].length == 0) return ''; var hours = callDateGet(d, 'Hours'); var str = loc['ampm'][floor(hours / 12)]; if(format.length === 1) str = str.slice(0,1); if(format.slice(0,1) === 'T') str = str.toUpperCase(); return str; } }, { token: 'z{1,4}|tz|timezone', text: true, format: function(d, loc, n, format) { var tz = d.getUTCOffset(); if(format == 'z' || format == 'zz') { tz = tz.replace(/(\d{2})(\d{2})/, function(f,h,m) { return padNumber(h, format.length); }); } return tz; } }, { token: 'iso(tz|timezone)', format: function(d) { return d.getUTCOffset(true); } }, { token: 'ord', format: function(d) { var date = callDateGet(d, 'Date'); return date + getOrdinalizedSuffix(date); } } ]; var DateUnits = [ { unit: 'year', method: 'FullYear', ambiguous: true, multiplier: function(d) { var adjust = d ? (d.isLeapYear() ? 1 : 0) : 0.25; return (365 + adjust) * 24 * 60 * 60 * 1000; } }, { unit: 'month', method: 'Month', ambiguous: true, multiplier: function(d, ms) { var days = 30.4375, inMonth; if(d) { inMonth = d.daysInMonth(); if(ms <= inMonth.days()) { days = inMonth; } } return days * 24 * 60 * 60 * 1000; } }, { unit: 'week', method: 'Week', multiplier: function() { return 7 * 24 * 60 * 60 * 1000; } }, { unit: 'day', method: 'Date', ambiguous: true, multiplier: function() { return 24 * 60 * 60 * 1000; } }, { unit: 'hour', method: 'Hours', multiplier: function() { return 60 * 60 * 1000; } }, { unit: 'minute', method: 'Minutes', multiplier: function() { return 60 * 1000; } }, { unit: 'second', method: 'Seconds', multiplier: function() { return 1000; } }, { unit: 'millisecond', method: 'Milliseconds', multiplier: function() { return 1; } } ]; // Date Localization var Localizations = {}; // Localization object function Localization(l) { simpleMerge(this, l); this.compiledFormats = CoreDateFormats.concat(); } Localization.prototype = { getMonth: function(n) { if(isNumber(n)) { return n - 1; } else { return this['months'].indexOf(n) % 12; } }, getWeekday: function(n) { return this['weekdays'].indexOf(n) % 7; }, getNumber: function(n) { var i; if(isNumber(n)) { return n; } else if(n && (i = this['numbers'].indexOf(n)) !== -1) { return (i + 1) % 10; } else { return 1; } }, getNumericDate: function(n) { var self = this; return n.replace(regexp(this['num'], 'g'), function(d) { var num = self.getNumber(d); return num || ''; }); }, getEnglishUnit: function(n) { return English['units'][this['units'].indexOf(n) % 8]; }, getRelativeFormat: function(adu) { return this.convertAdjustedToFormat(adu, adu[2] > 0 ? 'future' : 'past'); }, getDuration: function(ms) { return this.convertAdjustedToFormat(getAdjustedUnit(ms), 'duration'); }, hasVariant: function(code) { code = code || this.code; return code === 'en' || code === 'en-US' ? true : this['variant']; }, matchAM: function(str) { return str === this['ampm'][0]; }, matchPM: function(str) { return str && str === this['ampm'][1]; }, convertAdjustedToFormat: function(adu, mode) { var sign, unit, mult, num = adu[0], u = adu[1], ms = adu[2], format = this[mode] || this['relative']; if(isFunction(format)) { return format.call(this, num, u, ms, mode); } mult = this['plural'] && num > 1 ? 1 : 0; unit = this['units'][mult * 8 + u] || this['units'][u]; if(this['capitalizeUnit']) unit = simpleCapitalize(unit); sign = this['modifiers'].filter(function(m) { return m.name == 'sign' && m.value == (ms > 0 ? 1 : -1); })[0]; return format.replace(/\{(.*?)\}/g, function(full, match) { switch(match) { case 'num': return num; case 'unit': return unit; case 'sign': return sign.src; } }); }, getFormats: function() { return this.cachedFormat ? [this.cachedFormat].concat(this.compiledFormats) : this.compiledFormats; }, addFormat: function(src, allowsTime, match, variant, iso) { var to = match || [], loc = this, time, timeMarkers, lastIsNumeral; src = src.replace(/\s+/g, '[-,. ]*'); src = src.replace(/\{([^,]+?)\}/g, function(all, k) { var value, arr, result, opt = k.match(/\?$/), nc = k.match(/^(\d+)\??$/), slice = k.match(/(\d)(?:-(\d))?/), key = k.replace(/[^a-z]+$/, ''); if(nc) { value = loc['tokens'][nc[1]]; } else if(loc[key]) { value = loc[key]; } else if(loc[key + 's']) { value = loc[key + 's']; if(slice) { // Can't use filter here as Prototype hijacks the method and doesn't // pass an index, so use a simple loop instead! arr = []; value.forEach(function(m, i) { var mod = i % (loc['units'] ? 8 : value.length); if(mod >= slice[1] && mod <= (slice[2] || slice[1])) { arr.push(m); } }); value = arr; } value = arrayToAlternates(value); } if(nc) { result = '(?:' + value + ')'; } else { if(!match) { to.push(key); } result = '(' + value + ')'; } if(opt) { result += '?'; } return result; }); if(allowsTime) { time = prepareTime(RequiredTime, loc, iso); timeMarkers = ['t','[\\s\\u3000]'].concat(loc['timeMarker']); lastIsNumeral = src.match(/\\d\{\d,\d\}\)+\??$/); addDateInputFormat(loc, '(?:' + time + ')[,\\s\\u3000]+?' + src, TimeFormat.concat(to), variant); addDateInputFormat(loc, src + '(?:[,\\s]*(?:' + timeMarkers.join('|') + (lastIsNumeral ? '+' : '*') +')' + time + ')?', to.concat(TimeFormat), variant); } else { addDateInputFormat(loc, src, to, variant); } } }; // Localization helpers function getLocalization(localeCode, fallback) { var loc; if(!isString(localeCode)) localeCode = ''; loc = Localizations[localeCode] || Localizations[localeCode.slice(0,2)]; if(fallback === false && !loc) { throw new Error('Invalid locale.'); } return loc || CurrentLocalization; } function setLocalization(localeCode, set) { var loc, canAbbreviate; function initializeField(name) { var val = loc[name]; if(isString(val)) { loc[name] = val.split(','); } else if(!val) { loc[name] = []; } } function eachAlternate(str, fn) { str = str.split('+').map(function(split) { return split.replace(/(.+):(.+)$/, function(full, base, suffixes) { return suffixes.split('|').map(function(suffix) { return base + suffix; }).join('|'); }); }).join('|'); return str.split('|').forEach(fn); } function setArray(name, abbreviate, multiple) { var arr = []; loc[name].forEach(function(full, i) { if(abbreviate) { full += '+' + full.slice(0,3); } eachAlternate(full, function(day, j) { arr[j * multiple + i] = day.toLowerCase(); }); }); loc[name] = arr; } function getDigit(start, stop, allowNumbers) { var str = '\\d{' + start + ',' + stop + '}'; if(allowNumbers) str += '|(?:' + arrayToAlternates(loc['numbers']) + ')+'; return str; } function getNum() { var arr = ['\\d+'].concat(loc['articles']); if(loc['numbers']) arr = arr.concat(loc['numbers']); return arrayToAlternates(arr); } function setModifiers() { var arr = []; loc.modifiersByName = {}; loc['modifiers'].forEach(function(modifier) { var name = modifier.name; eachAlternate(modifier.src, function(t) { var locEntry = loc[name]; loc.modifiersByName[t] = modifier; arr.push({ name: name, src: t, value: modifier.value }); loc[name] = locEntry ? locEntry + '|' + t : t; }); }); loc['day'] += '|' + arrayToAlternates(loc['weekdays']); loc['modifiers'] = arr; } // Initialize the locale loc = new Localization(set); initializeField('modifiers'); 'months,weekdays,units,numbers,articles,tokens,timeMarker,ampm,timeSuffixes,dateParse,timeParse'.split(',').forEach(initializeField); canAbbreviate = !loc['monthSuffix']; setArray('months', canAbbreviate, 12); setArray('weekdays', canAbbreviate, 7); setArray('units', false, 8); setArray('numbers', false, 10); loc['code'] = localeCode; loc['date'] = getDigit(1,2, loc['digitDate']); loc['year'] = getDigit(4,4); loc['num'] = getNum(); setModifiers(); if(loc['monthSuffix']) { loc['month'] = getDigit(1,2); loc['months'] = getRange(1, 12).map(function(n) { return n + loc['monthSuffix']; }); } loc['full_month'] = getDigit(1,2) + '|' + arrayToAlternates(loc['months']); // The order of these formats is very important. Order is reversed so formats that come // later will take precedence over formats that come before. This generally means that // more specific formats should come later, however, the {year} format should come before // {day}, as 2011 needs to be parsed as a year (2011) and not date (20) + hours (11) // If the locale has time suffixes then add a time only format for that locale // that is separate from the core English-based one. if(loc['timeSuffixes'].length > 0) { loc.addFormat(prepareTime(RequiredTime, loc), false, TimeFormat) } loc.addFormat('{day}', true); loc.addFormat('{month}' + (loc['monthSuffix'] || '')); loc.addFormat('{year}' + (loc['yearSuffix'] || '')); loc['timeParse'].forEach(function(src) { loc.addFormat(src, true); }); loc['dateParse'].forEach(function(src) { loc.addFormat(src); }); return Localizations[localeCode] = loc; } // General helpers function addDateInputFormat(locale, format, match, variant) { locale.compiledFormats.unshift({ variant: variant, locale: locale, reg: regexp('^' + format + '$', 'i'), to: match }); } function simpleCapitalize(str) { return str.slice(0,1).toUpperCase() + str.slice(1); } function arrayToAlternates(arr) { return arr.filter(function(el) { return !!el; }).join('|'); } // Date argument helpers function collectDateArguments(args, allowDuration) { var obj, arr; if(isObject(args[0])) { return args; } else if (isNumber(args[0]) && !isNumber(args[1])) { return [args[0]]; } else if (isString(args[0]) && allowDuration) { return [getDateParamsFromString(args[0]), args[1]]; } obj = {}; DateArgumentUnits.forEach(function(u,i) { obj[u.unit] = args[i]; }); return [obj]; } function getDateParamsFromString(str, num) { var params = {}; match = str.match(/^(\d+)?\s?(\w+?)s?$/i); if(match) { if(isUndefined(num)) { num = parseInt(match[1]) || 1; } params[match[2].toLowerCase()] = num; } return params; } // Date parsing helpers function getFormatMatch(match, arr) { var obj = {}, value, num; arr.forEach(function(key, i) { value = match[i + 1]; if(isUndefined(value) || value === '') return; if(key === 'year') obj.yearAsString = value; num = parseFloat(value.replace(/,/, '.')); obj[key] = !isNaN(num) ? num : value.toLowerCase(); }); return obj; } function cleanDateInput(str) { str = str.trim().replace(/^(just )?now|\.+$/i, ''); return convertAsianDigits(str); } function convertAsianDigits(str) { return str.replace(AsianDigitReg, function(full, disallowed, match) { var sum = 0, place = 1, lastWasHolder, lastHolder; if(disallowed) return full; match.split('').reverse().forEach(function(letter) { var value = AsianDigitMap[letter], holder = value > 9; if(holder) { if(lastWasHolder) sum += place; place *= value / (lastHolder || 1); lastHolder = value; } else { if(lastWasHolder === false) { place *= 10; } sum += place * value; } lastWasHolder = holder; }); if(lastWasHolder) sum += place; return sum; }); } function getExtendedDate(f, localeCode, prefer, forceUTC) { var d = new date(), relative = false, baseLocalization, loc, format, set, unit, weekday, num, tmp, after; d.utc(forceUTC); if(isDate(f)) { d = new date(f.getTime()); } else if(isNumber(f)) { d = new date(f); } else if(isObject(f)) { d.set(f, true); set = f; } else if(isString(f)) { // The act of getting the localization will pre-initialize // if it is missing and add the required formats. baseLocalization = getLocalization(localeCode); // Clean the input and convert Kanji based numerals if they exist. f = cleanDateInput(f); if(baseLocalization) { iterateOverObject(baseLocalization.getFormats(), function(i, dif) { var match = f.match(dif.reg); console.info(match, dif.reg); if(match) { format = dif; loc = format.locale; set = getFormatMatch(match, format.to, loc); console.info('yar', set, dif.reg); if(set['utc']) { d.utc(); } loc.cachedFormat = format; if(set.timestamp) { set = set.timestamp; return false; } // If there's a variant (crazy Endian American format), swap the month and day. if(format.variant && !isString(set['month']) && (isString(set['date']) || baseLocalization.hasVariant(localeCode))) { tmp = set['month']; set['month'] = set['date']; set['date'] = tmp; } // If the year is 2 digits then get the implied century. if(set['year'] && set.yearAsString.length === 2) { set['year'] = getYearFromAbbreviation(set['year']); } // Set the month which may be localized. if(set['month']) { set['month'] = loc.getMonth(set['month']); if(set['shift'] && !set['unit']) set['unit'] = loc['units'][7]; } // If there is both a weekday and a date, the date takes precedence. if(set['weekday'] && set['date']) { delete set['weekday']; // Otherwise set a localized weekday. } else if(set['weekday']) { set['weekday'] = loc.getWeekday(set['weekday']); if(set['shift'] && !set['unit']) set['unit'] = loc['units'][5]; } // Relative day localizations such as "today" and "tomorrow". if(set['day'] && (tmp = loc.modifiersByName[set['day']])) { set['day'] = tmp.value; d.reset(); relative = true; // If the day is a weekday, then set that instead. } else if(set['day'] && (weekday = loc.getWeekday(set['day'])) > -1) { delete set['day']; if(set['num'] && set['month']) { // If we have "the 2nd tuesday of June", set the day to the beginning of the month, then // look ahead to set the weekday after all other properties have been set. The weekday needs // to be set after the actual set because it requires overriding the "prefer" argument which // could unintentionally send the year into the future, past, etc. after = function() { var w = d.getWeekday(); d.setWeekday((7 * (set['num'] - 1)) + (w > weekday ? weekday + 7 : weekday)); } set['day'] = 1; } else { set['weekday'] = weekday; } } if(set['date'] && !isNumber(set['date'])) { set['date'] = loc.getNumericDate(set['date']); } // If the time is 1pm-11pm advance the time by 12 hours. if(loc.matchPM(set['ampm']) && set['hour'] < 12) { set['hour'] += 12; } else if(loc.matchAM(set['ampm']) && set['hour'] === 12) { set['hour'] = 0; } // Adjust for timezone offset if('offset_hours' in set || 'offset_minutes' in set) { d.utc(); set['offset_minutes'] = set['offset_minutes'] || 0; set['offset_minutes'] += set['offset_hours'] * 60; if(set['offset_sign'] === '-') { set['offset_minutes'] *= -1; } set['minute'] -= set['offset_minutes']; } // Date has a unit like "days", "months", etc. are all relative to the current date. if(set['unit']) { relative = true; num = loc.getNumber(set['num']); unit = loc.getEnglishUnit(set['unit']); // Shift and unit, ie "next month", "last week", etc. if(set['shift'] || set['edge']) { num *= (tmp = loc.modifiersByName[set['shift']]) ? tmp.value : 0; // Relative month and static date: "the 15th of last month" if(unit === 'month' && isDefined(set['date'])) { d.set({ 'day': set['date'] }, true); delete set['date']; } // Relative year and static month/date: "June 15th of last year" if(unit === 'year' && isDefined(set['month'])) { d.set({ 'month': set['month'], 'day': set['date'] }, true); delete set['month']; delete set['date']; } } // Unit and sign, ie "months ago", "weeks from now", etc. if(set['sign'] && (tmp = loc.modifiersByName[set['sign']])) { num *= tmp.value; } // Units can be with non-relative dates, set here. ie "the day after monday" if(isDefined(set['weekday'])) { d.set({'weekday': set['weekday'] }, true); delete set['weekday']; } // Finally shift the unit. set[unit] = (set[unit] || 0) + num; } if(set['year_sign'] === '-') { set['year'] *= -1; } DateUnitsReversed.slice(1,4).forEach(function(u, i) { var value = set[u.unit], fraction = value % 1; if(fraction) { set[DateUnitsReversed[i].unit] = round(fraction * (u.unit === 'second' ? 1000 : 60)); set[u.unit] = floor(value); } }); return false; } }); } if(!format) { // The Date constructor does something tricky like checking the number // of arguments so simply passing in undefined won't work. d = f ? new date(f) : new date(); } else if(relative) { d.advance(set); } else { if(d._utc) { // UTC times can traverse into other days or even months, // so preemtively reset the time here to prevent this. d.reset(); } updateDate(d, set, true, false, prefer); } // If there is an "edge" it needs to be set after the // other fields are set. ie "the end of February" if(set && set['edge']) { tmp = loc.modifiersByName[set['edge']]; iterateOverObject(DateUnitsReversed.slice(4), function(i, u) { if(isDefined(set[u.unit])) { unit = u.unit; return false; } }); if(unit === 'year') set.specificity = 'month'; else if(unit === 'month' || unit === 'week') set.specificity = 'day'; d[(tmp.value < 0 ? 'endOf' : 'beginningOf') + simpleCapitalize(unit)](); // This value of -2 is arbitrary but it's a nice clean way to hook into this system. if(tmp.value === -2) d.reset(); } if(after) { after(); } } d.utc(false); return { date: d, set: set } } // If the year is two digits, add the most appropriate century prefix. function getYearFromAbbreviation(year) { return round(callDateGet(new date(), 'FullYear') / 100) * 100 - round(year / 100) * 100 + year; } function getShortHour(d) { var hours = callDateGet(d, 'Hours'); return hours === 0 ? 12 : hours - (floor(hours / 13) * 12); } // weeksSince won't work here as the result needs to be floored, not rounded. function getWeekNumber(date) { date = date.clone(); var dow = callDateGet(date, 'Day') || 7; date.addDays(4 - dow).reset(); return 1 + floor(date.daysSince(date.clone().beginningOfYear()) / 7); } function getAdjustedUnit(ms) { var next, ams = math.abs(ms), value = ams, unit = 0; DateUnitsReversed.slice(1).forEach(function(u, i) { next = floor(round(ams / u.multiplier() * 10) / 10); if(next >= 1) { value = next; unit = i + 1; } }); return [value, unit, ms]; } // Date formatting helpers function formatDate(date, format, relative, localeCode) { var adu, loc = getLocalization(localeCode), caps = regexp(/^[A-Z]/), value, shortcut; if(!date.isValid()) { return 'Invalid Date'; } else if(Date[format]) { format = Date[format]; } else if(isFunction(format)) { adu = getAdjustedUnit(date.millisecondsFromNow()); format = format.apply(date, adu.concat(loc)); } if(!format && relative) { adu = adu || getAdjustedUnit(date.millisecondsFromNow()); // Adjust up if time is in ms, as this doesn't // look very good for a standard relative date. if(adu[1] === 0) { adu[1] = 1; adu[0] = 1; } return loc.getRelativeFormat(adu); } format = format || 'long'; format = loc[format] || format; DateOutputFormats.forEach(function(dof) { format = format.replace(regexp('\\{('+dof.token+')(\\d)?\\}', dof.word ? 'i' : ''), function(m,t,d) { var val = dof.format(date, loc, d || 1, t), l = t.length, one = t.match(/^(.)\1+$/); if(dof.word) { if(l === 3) val = val.slice(0,3); if(one || t.match(caps)) val = simpleCapitalize(val); } else if(one && !dof.text) { val = (isNumber(val) ? padNumber(val, l) : val.toString()).slice(-l); } return val; }); }); return format; } // Date comparison helpers function compareDate(d, find, buffer, forceUTC) { var p = getExtendedDate(find, null, null, forceUTC), accuracy = 0, loBuffer = 0, hiBuffer = 0, override, capitalized; if(buffer > 0) { loBuffer = hiBuffer = buffer; override = true; } if(!p.date.isValid()) return false; if(p.set && p.set.specificity) { DateUnits.forEach(function(u, i) { if(u.unit === p.set.specificity) { accuracy = u.multiplier(p.date, d - p.date) - 1; } }); capitalized = simpleCapitalize(p.set.specificity); if(p.set['edge'] || p.set['shift']) { p.date['beginningOf' + capitalized](); } if(p.set.specificity === 'month') { max = p.date.clone()['endOf' + capitalized]().getTime(); } if(!override && p.set['sign'] && p.set.specificity != 'millisecond') { // If the time is relative, there can occasionally be an disparity between the relative date // and "now", which it is being compared to, so set an extra buffer to account for this. loBuffer = 50; hiBuffer = -50; } } var t = d.getTime(); var min = p.date.getTime(); var max = max || (min + accuracy); return t >= (min - loBuffer) && t <= (max + hiBuffer); } function updateDate(d, params, reset, advance, prefer) { var weekday, specificityIndex; function getParam(key) { return isDefined(params[key]) ? params[key] : params[key + 's']; } function paramExists(key) { return isDefined(getParam(key)); } function uniqueParamExists(key, isDay) { return paramExists(key) || (isDay && paramExists('weekday')); } function canDisambiguate() { var now = new date; return (prefer === -1 && d > now) || (prefer === 1 && d < now); } if(isNumber(params) && advance) { // If param is a number and we're advancing, the number is presumed to be milliseconds. params = { 'milliseconds': params }; } else if(isNumber(params)) { // Otherwise just set the timestamp and return. d.setTime(params); return d; } // "date" can also be passed for the day if(params['date']) params['day'] = params['date']; // Reset any unit lower than the least specific unit set. Do not do this for weeks // or for years. This needs to be performed before the acutal setting of the date // because the order needs to be reversed in order to get the lowest specificity, // also because higher order units can be overwritten by lower order units, such // as setting hour: 3, minute: 345, etc. iterateOverObject(DateUnitsReversed, function(i,u) { var isDay = u.unit === 'day'; if(uniqueParamExists(u.unit, isDay)) { params.specificity = u.unit; specificityIndex = +i; return false; } else if(reset && u.unit !== 'week' && (!isDay || !paramExists('week'))) { // Days are relative to months, not weeks, so don't reset if a week exists. callDateSet(d, u.method, (isDay ? 1 : 0)); } }); // Now actually set or advance the date in order, higher units first. DateUnits.forEach(function(u,i) { var unit = u.unit, method = u.method, higherUnit = DateUnits[i - 1], value; value = getParam(unit) if(isUndefined(value)) return; if(advance) { if(unit === 'week') { value = (params['day'] || 0) + (value * 7); method = 'Date'; } value = (value * advance) + callDateGet(d, method); } else if(unit === 'month' && paramExists('day')) { // When setting the month, there is a chance that we will traverse into a new month. // This happens in DST shifts, for example June 1st DST jumping to January 1st // (non-DST) will have a shift of -1:00 which will traverse into the previous year. // Prevent this by proactively setting the day when we know it will be set again anyway. // It can also happen when there are not enough days in the target month. This second // situation is identical to checkMonthTraversal below, however when we are advancing // we want to reset the date to "the last date in the target month". In the case of // DST shifts, however, we want to avoid the "edges" of months as that is where this // unintended traversal can happen. This is the reason for the different handling of // two similar but slightly different situations. // // TL;DR This method avoids the edges of a month IF not advancing and the date is going // to be set anyway, while checkMonthTraversal resets the date to the last day if advancing. // callDateSet(d, 'Date', 15); } callDateSet(d, method, value); if(advance && unit === 'month') { checkMonthTraversal(d, value); } }); // If a weekday is included in the params, set it ahead of time and set the params // to reflect the updated date so that resetting works properly. if(!advance && !paramExists('day') && paramExists('weekday')) { var weekday = getParam('weekday'), isAhead, futurePreferred; d.setWeekday(weekday); } if(canDisambiguate()) { iterateOverObject(DateUnitsReversed.slice(specificityIndex + 1), function(i,u) { var ambiguous = u.ambiguous || (u.unit === 'week' && paramExists('weekday')); if(ambiguous && !uniqueParamExists(u.unit, u.unit === 'day')) { d[u.addMethod](prefer); return false; } }); } return d; } function callDateGet(d, method) { return d['get' + (d._utc ? 'UTC' : '') + method](); } function callDateSet(d, method, value) { return d['set' + (d._utc ? 'UTC' : '') + method](value); } // The ISO format allows times strung together without a demarcating ":", so make sure // that these markers are now optional. function prepareTime(format, loc, iso) { var timeSuffixMapping = {'h':0,'m':1,'s':2}, add; loc = loc || English; return format.replace(/{([a-z])}/g, function(full, token) { var separators = [], isHours = token === 'h', tokenIsRequired = isHours && !iso; if(token === 't') { return loc['ampm'].join('|'); } else { if(isHours) { separators.push(':'); } if(add = loc['timeSuffixes'][timeSuffixMapping[token]]) { separators.push(add + '\\s*'); } return separators.length === 0 ? '' : '(?:' + separators.join('|') + ')' + (tokenIsRequired ? '' : '?'); } }); } // If the month is being set, then we don't want to accidentally // traverse into a new month just because the target month doesn't have enough // days. In other words, "5 months ago" from July 30th is still February, even // though there is no February 30th, so it will of necessity be February 28th // (or 29th in the case of a leap year). function checkMonthTraversal(date, targetMonth) { if(targetMonth < 0) targetMonth += 12; if(targetMonth % 12 != callDateGet(date, 'Month')) { callDateSet(date, 'Date', 0); } } function createDate(args, prefer, forceUTC) { var f, localeCode; if(isNumber(args[1])) { // If the second argument is a number, then we have an enumerated constructor type as in "new Date(2003, 2, 12);" f = collectDateArguments(args)[0]; } else { f = args[0]; localeCode = args[1]; } return getExtendedDate(f, localeCode, prefer, forceUTC).date; } function buildDateUnits() { DateUnitsReversed = DateUnits.concat().reverse(); DateArgumentUnits = DateUnits.concat(); DateArgumentUnits.splice(2,1); } /*** * @method [units]Since([d], [locale] = currentLocale) * @returns Number * @short Returns the time since [d] in the appropriate unit. * @extra [d] will accept a date object, timestamp, or text format. If not specified, [d] is assumed to be now. [locale] can be passed to specify the locale that the date is in. %[unit]Ago% is provided as an alias to make this more readable when [d] is assumed to be the current date. For more see @date_format. * * @set * millisecondsSince * secondsSince * minutesSince * hoursSince * daysSince * weeksSince * monthsSince * yearsSince * * @example * * Date.create().millisecondsSince('1 hour ago') -> 3,600,000 * Date.create().daysSince('1 week ago') -> 7 * Date.create().yearsSince('15 years ago') -> 15 * Date.create('15 years ago').yearsAgo() -> 15 * *** * @method [units]Ago() * @returns Number * @short Returns the time ago in the appropriate unit. * * @set * millisecondsAgo * secondsAgo * minutesAgo * hoursAgo * daysAgo * weeksAgo * monthsAgo * yearsAgo * * @example * * Date.create('last year').millisecondsAgo() -> 3,600,000 * Date.create('last year').daysAgo() -> 7 * Date.create('last year').yearsAgo() -> 15 * *** * @method [units]Until([d], [locale] = currentLocale) * @returns Number * @short Returns the time until [d] in the appropriate unit. * @extra [d] will accept a date object, timestamp, or text format. If not specified, [d] is assumed to be now. [locale] can be passed to specify the locale that the date is in. %[unit]FromNow% is provided as an alias to make this more readable when [d] is assumed to be the current date. For more see @date_format. * * @set * millisecondsUntil * secondsUntil * minutesUntil * hoursUntil * daysUntil * weeksUntil * monthsUntil * yearsUntil * * @example * * Date.create().millisecondsUntil('1 hour from now') -> 3,600,000 * Date.create().daysUntil('1 week from now') -> 7 * Date.create().yearsUntil('15 years from now') -> 15 * Date.create('15 years from now').yearsFromNow() -> 15 * *** * @method [units]FromNow() * @returns Number * @short Returns the time from now in the appropriate unit. * * @set * millisecondsFromNow * secondsFromNow * minutesFromNow * hoursFromNow * daysFromNow * weeksFromNow * monthsFromNow * yearsFromNow * * @example * * Date.create('next year').millisecondsFromNow() -> 3,600,000 * Date.create('next year').daysFromNow() -> 7 * Date.create('next year').yearsFromNow() -> 15 * *** * @method add[Units](<num>, [reset] = false) * @returns Date * @short Adds <num> of the unit to the date. If [reset] is true, all lower units will be reset. * @extra Note that "months" is ambiguous as a unit of time. If the target date falls on a day that does not exist (ie. August 31 -> February 31), the date will be shifted to the last day of the month. Don't use %addMonths% if you need precision. * * @set * addMilliseconds * addSeconds * addMinutes * addHours * addDays * addWeeks * addMonths * addYears * * @example * * Date.create().addMilliseconds(5) -> current time + 5 milliseconds * Date.create().addDays(5) -> current time + 5 days * Date.create().addYears(5) -> current time + 5 years * *** * @method isLast[Unit]() * @returns Boolean * @short Returns true if the date is last week/month/year. * * @set * isLastWeek * isLastMonth * isLastYear * * @example * * Date.create('yesterday').isLastWeek() -> true or false? * Date.create('yesterday').isLastMonth() -> probably not... * Date.create('yesterday').isLastYear() -> even less likely... * *** * @method isThis[Unit]() * @returns Boolean * @short Returns true if the date is this week/month/year. * * @set * isThisWeek * isThisMonth * isThisYear * * @example * * Date.create('tomorrow').isThisWeek() -> true or false? * Date.create('tomorrow').isThisMonth() -> probably... * Date.create('tomorrow').isThisYear() -> signs point to yes... * *** * @method isNext[Unit]() * @returns Boolean * @short Returns true if the date is next week/month/year. * * @set * isNextWeek * isNextMonth * isNextYear * * @example * * Date.create('tomorrow').isNextWeek() -> true or false? * Date.create('tomorrow').isNextMonth() -> probably not... * Date.create('tomorrow').isNextYear() -> even less likely... * *** * @method beginningOf[Unit]() * @returns Date * @short Sets the date to the beginning of the appropriate unit. * * @set * beginningOfDay * beginningOfWeek * beginningOfMonth * beginningOfYear * * @example * * Date.create().beginningOfDay() -> the beginning of today (resets the time) * Date.create().beginningOfWeek() -> the beginning of the week * Date.create().beginningOfMonth() -> the beginning of the month * Date.create().beginningOfYear() -> the beginning of the year * *** * @method endOf[Unit]() * @returns Date * @short Sets the date to the end of the appropriate unit. * * @set * endOfDay * endOfWeek * endOfMonth * endOfYear * * @example * * Date.create().endOfDay() -> the end of today (sets the time to 23:59:59.999) * Date.create().endOfWeek() -> the end of the week * Date.create().endOfMonth() -> the end of the month * Date.cre