sugar
Version:
A Javascript library for working with native objects.
1,516 lines (1,375 loc) • 77.1 kB
JavaScript
/***
* @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 = '\\d{1,2}' + 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;
},
error: 0.919
},
{
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 setDefault(name, value) {
loc[name] = loc[name] || value;
}
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);
setDefault('code', localeCode);
setDefault('date', getDigit(1,2, loc['digitDate']));
setDefault('year', "'\\d{2}|" + getDigit(4,4));
setDefault('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.replace(/'/, '');
}
num = parseFloat(value.replace(/'/, '').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)) {
// If the source here is already a date object, then the operation
// is the same as cloning the date, which preserves the UTC flag.
d.utc(f.isUTC()).setTime(f.getTime());
} else if(isNumber(f)) {
d.setTime(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);
if(match) {
format = dif;
loc = format.locale;
set = getFormatMatch(match, format.to, loc);
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.
if(f !== 'now') {
d = new date(f);
}
if(forceUTC) {
// Falling back to system date here which cannot be parsed as UTC,
// so if we're forcing UTC then simply add the offset.
d.addMinutes(d.getTimezoneOffset());
}
} 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();
}
// A date created by parsing a string presumes that the format *itself* is UTC, but
// not that the date, once created, should be manipulated as such. In other words,
// if you are creating a date object from a server time "2012-11-15T12:00:00Z",
// in the majority of cases you are using it to create a date that will, after creation,
// be manipulated as local, so reset the utc flag here.
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, t, min, max, minOffset, maxOffset, override, capitalized, accuracy = 0, loBuffer = 0, hiBuffer = 0;
p = getExtendedDate(find, null, null, forceUTC);
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;
}
}
t = d.getTime();
min = p.date.getTime();
max = max || (min + accuracy);
max = compensateForTimezoneTraversal(d, min, max);
return t >= (min - loBuffer) && t <= (max + hiBuffer);
}
function compensateForTimezoneTraversal(d, min, max) {
var dMin, dMax, minOffset, maxOffset;
dMin = new Date(min);
dMax = new Date(max).utc(d.isUTC());
if(callDateGet(dMax, 'Hours') !== 23) {
minOffset = dMin.getTimezoneOffset();
maxOffset = dMax.getTimezoneOffset();
if(minOffset !== maxOffset) {
max += (maxOffset - minOffset).minutes();
}
}
return max;
}
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(isDefined(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 = targetMonth % 12 + 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.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/until now" only count "past" an integer, i.e. "2 days ago" is
// anything between 2 - 2.999 days. The default margin of error is 0.999,
// but "months" have an inherently larger margin, as the number of days
// in a given month may be significantly less than the number of days in
// the average month, so for example "30 days" before March 15 may in fact
// be 1 month ago. Years also have a margin of error due to leap years,
// but this is roughly 0.999 anyway (365 / 365.25). Other units do not
// technically need the error margin applied to them but this accounts
// for discrepancies like (15).hoursAgo() which technically creates the
// current date first, then creates a date 15 hours before and compares
// them, the discrepancy between the creation of the 2 dates means that
// they may actually be 15.0001 hours apart. Milliseconds don't have
// fractions, so they won't be subject to this error margin.
function applyErrorMargin(ms) {
var num = ms / multiplier,
fraction = num % 1,
error = u.error || 0.999;
if(fraction && math.abs(fraction % 1) > error) {
num = round(num);
}
return parseInt(num);
}
since = function(f, localeCode) {
return applyErrorMargin(this.getTime() - date.create(f, localeCode).getTime());
};
until = function(f, localeCode) {
return applyErrorMargin(date.create(f, localeCode).getTime() - this.getTime());
};
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'] = callDateGet(this, 'FullYear'); break;
case 'month': set['month'] = callDateGet(this, 'Month'); break;
case 'day': set['day'] = callDateGet(this, 'Date'); 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(utc) {
return this.is(name, 0, utc);
};
});
}
function buildUTCAliases() {
date.extend({
'utc': {
'create': function() {
return createDate(arguments, 0, true);
},
'past': function() {
return createDate(arguments, -1, true);
},
'future': function() {
return createDate(arguments, 1, true);
}
}
}, false, false);
}
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);
}
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. UTC-based dates can be created through the %utc% object. For more see @date_format.
* @set
* Date.utc.create
*
* @example
*
* Date.create('July') -> July of this year
* Date.create('1776') -> 1776
* Date.create('today') -> today
* Date.create('wednesday') -> This wednesday
* D