UNPKG

sugar

Version:

A Javascript library for working with native objects.

1,486 lines (1,356 loc) 74.3 kB
/*** * @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 FloatReg = '\\d{1,2}(?:[,.]\\d+)?'; var RequiredTime = '({t})?\\s*('+FloatReg+')(?:{h}('+FloatReg+')?{m}(?::?('+FloatReg+'){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 d.getMilliseconds(); } }, { token: 'ss?|seconds', format: function(d, len) { return d.getSeconds(); } }, { token: 'mm?|minutes', format: function(d, len) { return d.getMinutes(); } }, { token: 'hh?|hours|12hr', format: function(d) { return getShortHour(d); } }, { token: 'HH?|24hr', format: function(d) { return d.getHours(); } }, { token: 'dd?|date|day', format: function(d) { return d.getDate(); } }, { token: 'dow|weekday', word: true, format: function(d, loc, n, t) { return loc['weekdays'][d.getDay() + (n - 1) * 7]; } }, { token: 'MM?', format: function(d) { return d.getMonth() + 1; } }, { token: 'mon|month', word: true, format: function(d, loc, n, len) { return loc['months'][d.getMonth() + (n - 1) * 12]; } }, { token: 'y{2,4}|year', format: function(d) { return d.getFullYear(); } }, { token: '[Tt]{1,2}', format: function(d, loc, n, format) { var str = loc['ampm'][floor(d.getHours() / 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 d = d.getDate(); return d + getOrdinalizedSuffix(d); } } ]; var DateUnits = [ { unit: 'year', method: 'FullYear', 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]; }, relative: 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']; }, matchPM: function(str) { return str === this['ampm'][1]; }, convertAdjustedToFormat: function(adu, format) { var num = adu[0], u = adu[1], ms = adu[2], sign, unit, last, mult; if(this['code'] == 'ru') { last = num.toString().slice(-1); switch(true) { case last == 1: mult = 1; break; case last >= 2 && last <= 4: mult = 2; break; default: mult = 3; } } else { 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 this[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 opt = k.match(/\?$/), slice = k.match(/(\d)(?:-(\d))?/), nc = k.match(/^\d+$/), key = k.replace(/[^a-z]+$/, ''), value, arr; if(nc) { value = loc['optionals'][nc[0]]; } 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) { return '(?:' + value + ')?'; } else { if(!match) { to.push(key); } return '(' + value + ')' + (opt ? '?' : ''); } }); 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; 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 = []; if(!loc[name]) return; loc[name].forEach(function(el, i) { eachAlternate(el, function(str, j) { arr[j * multiple + i] = str.toLowerCase(); }); }); if(abbreviate) arr = arr.concat(loc[name].map(function(str) { return str.slice(0,3).toLowerCase(); })); return 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,optionals,timeMarker,ampm,timeSuffixes,dateParse,timeParse'.split(',').forEach(initializeField); setArray('months', true, 12); setArray('weekdays', true, 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(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) { var d = new date(), relative = false, baseLocalization, loc, format, set, unit, weekday, num, tmp, after; if(isDate(f)) { d = f.clone(); } else if(isNumber(f)) { d = new date(f); } else if(isObject(f)) { d = new date().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); if(match) { format = dif; loc = format.locale; set = getFormatMatch(match, format.to, loc); 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() { updateDate(d, { 'weekday': weekday + (7 * (set['num'] - 1)) }, false, false, false, 1); } 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; } // Adjust for timezone offset if('offset_hours' in set || 'offset_minutes' in set) { set['utc'] = true; 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(set['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, set['utc'], 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(); } } return { date: d, set: set } } // If the year is two digits, add the most appropriate century prefix. function getYearFromAbbreviation(year) { return round(new date().getFullYear() / 100) * 100 - round(year / 100) * 100 + year; } function getShortHour(d, utc) { var hours = callDateMethod(d, 'get', utc, '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) { var dow = date.getDay() || 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.relative(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) { var p = getExtendedDate(find), 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); // Offset any shift that may occur as a result of DST traversal. return t >= (min - loBuffer) && t <= (max + hiBuffer); } function updateDate(d, params, reset, utc, advance, prefer) { var weekday; function getParam(key) { return isDefined(params[key]) ? params[key] : params[key + 's']; } function paramExists(key) { return isDefined(getParam(key)); } function canDisambiguate(u, higherUnit) { return prefer && u.ambiguous && !paramExists(higherUnit.unit); } 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(paramExists(u.unit) || (isDay && paramExists('weekday'))) { params.specificity = u.unit; 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. callDateMethod(d, 'set', utc, 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(canDisambiguate(u, higherUnit)) { // Formats like "June" have an ambiguous year. If no preference is stated, this // is fine as "June of this year", however in a future context, this would mean // "the next June", which may be either this year or next year. If we have an // ambiguity *and* a preference for resolving it, then advance or rewind the // higher order as necessary. Note that weekdays are handled differently below. var current = callDateMethod(new date, 'get', utc, u.method); if(current >= value === (prefer === 1)) { d[higherUnit.addMethod](prefer); } } if(advance) { if(unit === 'week') { value = (params['day'] || 0) + (value * 7); method = 'Date'; } value = (value * advance) + callDateMethod(d, 'get', false, 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. // d.setDate(15); } callDateMethod(d, 'set', utc, 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; if(isDefined(prefer)) { // If there is a preference as to whether this weekday is in the future, // then add an offset as needed. NOTE: Was previously doing something much // more one-liner-hipster here, but it made Opera choke (order of operations // bug??) ... better to be more explicit here anyway. isAhead = callDateMethod(d, 'get', utc, 'Day') - (weekday % 7) >= 0; futurePreferred = prefer === 1; if(isAhead === futurePreferred) { weekday += prefer * 7; } } callDateMethod(d, 'set', utc, 'Weekday', weekday) } return d; } function callDateMethod(d, prefix, utc, method, value) { return d[prefix + (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 != date.getMonth()) { date.setDate(0); } } function createDate(args, prefer) { var f; 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]; } return getExtendedDate(f, args[1], prefer).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.create().endOfYear() -> the end of the year * ***/ function buildDateMethods() { extendSimilar(date, true, false, DateUnits, function(methods, u, i) { var unit = u.unit, caps = simpleCapitalize(unit), multiplier = u.multiplier(), since, until; u.addMethod = 'add' + caps + 's'; since = function(f, localeCode) { return round((this.getTime() - date.create(f, localeCode).getTime()) / multiplier); }; until = function(f, localeCode) { return round((date.create(f, localeCode).getTime() - this.getTime()) / multiplier); }; methods[unit+'sAgo'] = until; methods[unit+'sUntil'] = until; methods[unit+'sSince'] = since; methods[unit+'sFromNow'] = since; methods[u.addMethod] = function(num, reset) { var set = {}; set[unit] = num; return this.advance(set, reset); }; buildNumberToDateAlias(u, multiplier); if(i < 3) { ['Last','This','Next'].forEach(function(shift) { methods['is' + shift + caps] = function() { return this.is(shift + ' ' + unit); }; }); } if(i < 4) { methods['beginningOf' + caps] = function() { var set = {}; switch(unit) { case 'year': set['year'] = this.getFullYear(); break; case 'month': set['month'] = this.getMonth(); break; case 'day': set['day'] = this.getDate(); break; case 'week': set['weekday'] = 0; break; } return this.set(set, true); }; methods['endOf' + caps] = function() { var set = { 'hours': 23, 'minutes': 59, 'seconds': 59, 'milliseconds': 999 }; switch(unit) { case 'year': set['month'] = 11; set['day'] = 31; break; case 'month': set['day'] = this.daysInMonth(); break; case 'week': set['weekday'] = 6; break; } return this.set(set, true); }; } }); } function buildCoreInputFormats() { English.addFormat('([+-])?(\\d{4,4})[-.]?{full_month}[-.]?(\\d{1,2})?', true, ['year_sign','year','month','date'], false, true); English.addFormat('(\\d{1,2})[-.\\/]{full_month}(?:[-.\\/](\\d{2,4}))?', true, ['date','month','year'], true); English.addFormat('{full_month}[-.](\\d{4,4})', false, ['month','year']); English.addFormat('\\/Date\\((\\d+(?:\\+\\d{4,4})?)\\)\\/', false, ['timestamp']) English.addFormat(prepareTime(RequiredTime, English), false, TimeFormat) // When a new locale is initialized it will have the CoreDateFormats initialized by default. // From there, adding new formats will push them in front of the previous ones, so the core // formats will be the last to be reached. However, the core formats themselves have English // months in them, which means that English needs to first be initialized and creates a race // condition. I'm getting around this here by adding these generalized formats in the order // specific -> general, which will mean they will be added to the English localization in // general -> specific order, then chopping them off the front and reversing to get the correct // order. Note that there are 7 formats as 2 have times which adds a front and a back format. CoreDateFormats = English.compiledFormats.slice(0,7).reverse(); English.compiledFormats = English.compiledFormats.slice(7).concat(CoreDateFormats); } function buildDateOutputShortcuts() { extendSimilar(date, true, false, 'short,long,full', function(methods, name) { methods[name] = function(localeCode) { return formatDate(this, name, false, localeCode); } }); } function buildAsianDigits() { KanjiDigits.split('').forEach(function(digit, value) { var holder; if(value > 9) { value = math.pow(10, value - 9); } AsianDigitMap[digit] = value; }); FullWidthDigits.split('').forEach(function(digit, value) { AsianDigitMap[digit] = value; }); // Kanji numerals may also be included in phrases which are text-based rather // than actual numbers such as Chinese weekdays (上周三), and "the day before // yesterday" (一昨日) in Japanese, so don't match these. AsianDigitReg = regexp('([期週周])?([' + KanjiDigits + FullWidthDigits + ']+)(?!昨)', 'g'); } /*** * @method is[Day]() * @returns Boolean * @short Returns true if the date falls on that day. * @extra Also available: %isYesterday%, %isToday%, %isTomorrow%, %isWeekday%, and %isWeekend%. * * @set * isToday * isYesterday * isTomorrow * isWeekday * isWeekend * isSunday * isMonday * isTuesday * isWednesday * isThursday * isFriday * isSaturday * * @example * * Date.create('tomorrow').isToday() -> false * Date.create('thursday').isTomorrow() -> ? * Date.create('yesterday').isWednesday() -> ? * Date.create('today').isWeekend() -> ? * *** * @method isFuture() * @returns Boolean * @short Returns true if the date is in the future. * @example * * Date.create('next week').isFuture() -> true * Date.create('last week').isFuture() -> false * *** * @method isPast() * @returns Boolean * @short Returns true if the date is in the past. * @example * * Date.create('last week').isPast() -> true * Date.create('next week').isPast() -> false * ***/ function buildRelativeAliases() { var special = 'today,yesterday,tomorrow,weekday,weekend,future,past'.split(','); var weekdays = English['weekdays'].slice(0,7); var months = English['months'].slice(0,12); extendSimilar(date, true, false, special.concat(weekdays).concat(months), function(methods, name) { methods['is'+ simpleCapitalize(name)] = function() { return this.is(name); }; }); } function setDateProperties() { date.extend({ 'RFC1123': '{Dow}, {dd} {Mon} {yyyy} {HH}:{mm}:{ss} {tz}', 'RFC1036': '{Weekday}, {dd}-{Mon}-{yy} {HH}:{mm}:{ss} {tz}', 'ISO8601_DATE': '{yyyy}-{MM}-{dd}', 'ISO8601_DATETIME': '{yyyy}-{MM}-{dd}T{HH}:{mm}:{ss}.{fff}{isotz}' }, false, false); } function buildDate() { buildDateUnits(); buildDateMethods(); buildCoreInputFormats(); buildDateOutputShortcuts(); buildAsianDigits(); buildRelativeAliases(); setDateProperties(); } date.extend({ /*** * @method Date.create(<d>, [locale] = currentLocale) * @returns Date * @short Alternate Date constructor which understands many different text formats, a timestamp, or another date. * @extra If no argument is given, date is assumed to be now. %Date.create% additionally can accept enumerated parameters as with the standard date constructor. [locale] can be passed to specify the locale that the date is in. When unspecified, the current locale (default is English) is assumed. For more information, see @date_format. * @example * * Date.create('July') -> July of this year * Date.create('1776') -> 1776 * Date.create('today') -> today * Date.create('wednesday') -> This wednesday * Date.create('next friday') -> Next friday * Date.create('July 4, 1776') -> July 4, 1776 * Date.create(-446806800000) -> November 5, 1955 * Date.create(1776, 6, 4) -> July 4, 1776 * Date.create('1776年07月04日', 'ja') -> July 4, 1776 * ***/ 'create': function() { return createDate(arguments); }, /*** * @method Date.past(<d>, [locale] = currentLocale) * @returns Date * @short Alternate form of %Date.create% with any ambiguity assumed to be the past. * @extra For example %"Sunday"% can be either "the Sunday coming up" or "the Sunday last" depending on context. Note that dates explicitly in the future ("next Sunday") will remain in the future. This method simply provides a hint when ambiguity exists. * @example * * Date.past('July') -> July of this year or last depending on the current month * Date.past('Wednesday') -> This wednesday or last depending on the current weekday * ***/ 'past': function() { return createDate(arguments, -1); }, /*** * @method Date.future(<d>, [locale] = currentLocale) * @returns Date * @short Alternate form of %Date.create% with any ambiguity assumed to be the future. * @extra For example %"Sunday"% can be either "the Sunday coming up" or "the Sunday last" depending on context. Note that dates explicitly in the past ("last Sunday") will remain in the past. This method simply provides a hint when ambiguity exists. * @example * * Date.future('July') -> July of this year or next depending on the current month * Date.future('Wednesday') -> This wednesday or next depending on the current weekday * ***/ 'future': function() { return createDate(arguments, 1); }, /*** * @method Date.addLocale(<code>, <set>) * @returns Locale * @short Adds a locale <set> to the locales understood by Sugar. * @extra For more see @date_format. * ***/ 'addLocale': function(localeCode, set) { return setLocalization(localeCode, set); }, /*** * @method Date.setLocale(<code>) * @returns Locale * @short Sets the current locale to be used with dates. * @extra Sugar has support for 13 locales that are available through the "Date Locales" package. In addition you can define a new locale with %Date.addLocale%. For more see @date_format. * ***/ 'setLocale': function(localeCode, set) { var loc = getLocalization(localeCode, false); CurrentLocalization = loc; // The code is allowed to be more specific than the codes which are required: // i.e. zh-CN or en-US. Currently this only affects US date variants such as 8/10/2000. if(localeCode && localeCode != loc['code']) { loc['code'] = localeCode; } return loc; }, /*** * @method Date.getLocale([code] = current) * @returns Locale * @short Gets the locale for the given code, or the current locale. * @extra The resulting locale object can be manipulated to provide more control over date localizations. For more about locales, see @date_format. * ***/ 'getLocale': function(localeCode) { return !localeCode ? CurrentLocaliz