sugar
Version:
A Javascript utility library for working with native objects.
460 lines (402 loc) • 14.4 kB
JavaScript
;
var MINUTES = require('../var/MINUTES'),
ABBREVIATED_YEAR_REG = require('../var/ABBREVIATED_YEAR_REG'),
LocaleHelpers = require('../var/LocaleHelpers'),
DateUnitIndexes = require('../var/DateUnitIndexes'),
_utc = require('../../common/var/_utc'),
trunc = require('../../common/var/trunc'),
forEach = require('../../common/internal/forEach'),
tzOffset = require('./tzOffset'),
isDefined = require('../../common/internal/isDefined'),
resetTime = require('./resetTime'),
getNewDate = require('./getNewDate'),
updateDate = require('./updateDate'),
setWeekday = require('./setWeekday'),
simpleMerge = require('../../common/internal/simpleMerge'),
advanceDate = require('./advanceDate'),
isUndefined = require('../../common/internal/isUndefined'),
classChecks = require('../../common/var/classChecks'),
dateIsValid = require('./dateIsValid'),
simpleClone = require('../../common/internal/simpleClone'),
isObjectType = require('../../common/internal/isObjectType'),
moveToEndOfUnit = require('./moveToEndOfUnit'),
deleteDateParam = require('./deleteDateParam'),
coreUtilityAliases = require('../../common/var/coreUtilityAliases'),
moveToBeginningOfUnit = require('./moveToBeginningOfUnit'),
iterateOverDateParams = require('./iterateOverDateParams'),
getYearFromAbbreviation = require('./getYearFromAbbreviation'),
iterateOverHigherDateParams = require('./iterateOverHigherDateParams');
var isNumber = classChecks.isNumber,
isString = classChecks.isString,
isDate = classChecks.isDate,
getOwn = coreUtilityAliases.getOwn,
English = LocaleHelpers.English,
localeManager = LocaleHelpers.localeManager,
DAY_INDEX = DateUnitIndexes.DAY_INDEX,
WEEK_INDEX = DateUnitIndexes.WEEK_INDEX,
MONTH_INDEX = DateUnitIndexes.MONTH_INDEX,
YEAR_INDEX = DateUnitIndexes.YEAR_INDEX;
function getExtendedDate(contextDate, d, opt, forceClone) {
// Locals
var date, set, loc, afterCallbacks, relative, weekdayDir;
// Options
var optPrefer, optLocale, optFromUTC, optSetUTC, optParams, optClone;
afterCallbacks = [];
setupOptions(opt);
function setupOptions(opt) {
opt = isString(opt) ? { locale: opt } : opt || {};
optPrefer = +!!getOwn(opt, 'future') - +!!getOwn(opt, 'past');
optLocale = getOwn(opt, 'locale');
optFromUTC = getOwn(opt, 'fromUTC');
optSetUTC = getOwn(opt, 'setUTC');
optParams = getOwn(opt, 'params');
optClone = getOwn(opt, 'clone');
}
function parseFormatValues(match, dif) {
var set = optParams || {};
forEach(dif.to, function(param, i) {
var str = match[i + 1], val;
if (!str) return;
val = parseIrregular(str, param);
if (isUndefined(val)) {
val = loc.parseValue(str, param);
}
set[param] = val;
});
return set;
}
function parseIrregular(str, param) {
if (param === 'utc') {
return 1;
} else if (param === 'year') {
var match = str.match(ABBREVIATED_YEAR_REG);
if (match) {
return getYearFromAbbreviation(match[1], date, optPrefer);
}
}
}
// Force the UTC flags to be true if the source date
// date is UTC, as they will be overwritten later.
function cloneDateByFlag(d, clone) {
if (_utc(d) && !isDefined(optFromUTC)) {
optFromUTC = true;
}
if (_utc(d) && !isDefined(optSetUTC)) {
optSetUTC = true;
}
if (clone) {
d = new Date(d.getTime());
}
return d;
}
function afterDateSet(fn) {
afterCallbacks.push(fn);
}
function fireCallbacks() {
forEach(afterCallbacks, function(fn) {
fn.call();
});
}
function parseStringDate(str) {
str = str.toLowerCase();
// The act of getting the locale will initialize
// if it is missing and add the required formats.
loc = localeManager.get(optLocale);
for (var i = 0, dif, match; dif = loc.compiledFormats[i]; i++) {
match = str.match(dif.reg);
if (match) {
// Note that caching the format will modify the compiledFormats array
// which is not a good idea to do inside its for loop, however we
// know at this point that we have a matched format and that we will
// break out below, so simpler to do it here.
loc.cacheFormat(dif, i);
set = parseFormatValues(match, dif);
if (isDefined(set.timestamp)) {
date.setTime(set.timestamp);
break;
}
if (isDefined(set.ampm)) {
handleAmpm(set.ampm);
}
if (set.utc || isDefined(set.tzHour)) {
handleTimezoneOffset(set.tzHour, set.tzMinute);
}
if (isDefined(set.shift) && isUndefined(set.unit)) {
// "next january", "next monday", etc
handleUnitlessShift();
}
if (isDefined(set.num) && isUndefined(set.unit)) {
// "the second of January", etc
handleUnitlessNum(set.num);
}
if (set.midday) {
// "noon" and "midnight"
handleMidday(set.midday);
}
if (isDefined(set.day)) {
// Relative day localizations such as "today" and "tomorrow".
handleRelativeDay(set.day);
}
if (isDefined(set.unit)) {
// "3 days ago", etc
handleRelativeUnit(set.unit);
}
if (set.edge) {
// "the end of January", etc
handleEdge(set.edge, set);
}
break;
}
}
if (!set) {
// TODO: remove in next major version
// Fall back to native parsing
date = new Date(str);
if (optFromUTC && dateIsValid(date)) {
// Falling back to system date here which cannot be parsed as UTC,
// so if we're forcing UTC then simply add the offset.
date.setTime(date.getTime() + (tzOffset(date) * MINUTES));
}
} else if (relative) {
updateDate(date, set, false, 1);
} else {
updateDate(date, set, true, 0, optPrefer, weekdayDir, contextDate);
}
fireCallbacks();
return date;
}
function handleAmpm(ampm) {
if (ampm === 1 && set.hour < 12) {
// If the time is 1pm-11pm advance the time by 12 hours.
set.hour += 12;
} else if (ampm === 0 && set.hour === 12) {
// If it is 12:00am then set the hour to 0.
set.hour = 0;
}
}
function handleTimezoneOffset(tzHour, tzMinute) {
// Adjust for timezone offset
_utc(date, true);
// Sign is parsed as part of the hour, so flip
// the minutes if it's negative.
if (tzHour < 0) {
tzMinute *= -1;
}
var offset = tzHour * 60 + (tzMinute || 0);
if (offset) {
set.minute = (set.minute || 0) - offset;
}
}
function handleUnitlessShift() {
if (isDefined(set.month)) {
// "next January"
set.unit = YEAR_INDEX;
} else if (isDefined(set.weekday)) {
// "next Monday"
set.unit = WEEK_INDEX;
}
}
function handleUnitlessNum(num) {
if (isDefined(set.weekday)) {
// "The second Tuesday of March"
setOrdinalWeekday(num);
} else if (isDefined(set.month)) {
// "The second of March"
set.date = set.num;
}
}
function handleMidday(hour) {
set.hour = hour % 24;
if (hour > 23) {
// If the date has hours past 24, we need to prevent it from traversing
// into a new day as that would make it being part of a new week in
// ambiguous dates such as "Monday".
afterDateSet(function() {
advanceDate(date, 'date', trunc(hour / 24));
});
}
}
function handleRelativeDay() {
resetTime(date);
if (isUndefined(set.unit)) {
set.unit = DAY_INDEX;
set.num = set.day;
delete set.day;
}
}
function handleRelativeUnit(unitIndex) {
var num;
if (isDefined(set.num)) {
num = set.num;
} else if (isDefined(set.edge) && isUndefined(set.shift)) {
num = 0;
} else {
num = 1;
}
// If a weekday is defined, there are 3 possible formats being applied:
//
// 1. "the day after monday": unit is days
// 2. "next monday": short for "next week monday", unit is weeks
// 3. "the 2nd monday of next month": unit is months
//
// In the first case, we need to set the weekday up front, as the day is
// relative to it. The second case also needs to be handled up front for
// formats like "next monday at midnight" which will have its weekday reset
// if not set up front. The last case will set up the params necessary to
// shift the weekday and allow separateAbsoluteUnits below to handle setting
// it after the date has been shifted.
if(isDefined(set.weekday)) {
if(unitIndex === MONTH_INDEX) {
setOrdinalWeekday(num);
num = 1;
} else {
updateDate(date, { weekday: set.weekday }, true);
delete set.weekday;
}
}
if (set.half) {
// Allow localized "half" as a standalone colloquialism. Purposely avoiding
// the locale number system to reduce complexity. The units "month" and
// "week" are purposely excluded in the English date formats below, as
// "half a week" and "half a month" are meaningless as exact dates.
num *= set.half;
}
if (isDefined(set.shift)) {
// Shift and unit, ie "next month", "last week", etc.
num *= set.shift;
} else if (set.sign) {
// Unit and sign, ie "months ago", "weeks from now", etc.
num *= set.sign;
}
if (isDefined(set.day)) {
// "the day after tomorrow"
num += set.day;
delete set.day;
}
// Formats like "the 15th of last month" or "6:30pm of next week"
// contain absolute units in addition to relative ones, so separate
// them here, remove them from the params, and set up a callback to
// set them after the relative ones have been set.
separateAbsoluteUnits(unitIndex);
// Finally shift the unit.
set[English.units[unitIndex]] = num;
relative = true;
}
function handleEdge(edge, params) {
var edgeIndex = params.unit, weekdayOfMonth;
if (!edgeIndex) {
// If we have "the end of January", then we need to find the unit index.
iterateOverHigherDateParams(params, function(unitName, val, unit, i) {
if (unitName === 'weekday' && isDefined(params.month)) {
// If both a month and weekday exist, then we have a format like
// "the last tuesday in November, 2012", where the "last" is still
// relative to the end of the month, so prevent the unit "weekday"
// from taking over.
return;
}
edgeIndex = i;
});
}
if (edgeIndex === MONTH_INDEX && isDefined(params.weekday)) {
// If a weekday in a month exists (as described above),
// then set it up to be set after the date has been shifted.
weekdayOfMonth = params.weekday;
delete params.weekday;
}
afterDateSet(function() {
var stopIndex;
// "edge" values that are at the very edge are "2" so the beginning of the
// year is -2 and the end of the year is 2. Conversely, the "last day" is
// actually 00:00am so it is 1. -1 is reserved but unused for now.
if (edge < 0) {
moveToBeginningOfUnit(date, edgeIndex, optLocale);
} else if (edge > 0) {
if (edge === 1) {
stopIndex = DAY_INDEX;
moveToBeginningOfUnit(date, DAY_INDEX);
}
moveToEndOfUnit(date, edgeIndex, optLocale, stopIndex);
}
if (isDefined(weekdayOfMonth)) {
setWeekday(date, weekdayOfMonth, -edge);
resetTime(date);
}
});
if (edgeIndex === MONTH_INDEX) {
params.specificity = DAY_INDEX;
} else {
params.specificity = edgeIndex - 1;
}
}
function setOrdinalWeekday(num) {
// If we have "the 2nd Tuesday of June", then pass the "weekdayDir"
// flag along to updateDate so that the date does not accidentally traverse
// into the previous month. This needs to be independent of the "prefer"
// flag because we are only ensuring that the weekday is in the future, not
// the entire date.
set.weekday = 7 * (num - 1) + set.weekday;
set.date = 1;
weekdayDir = 1;
}
function separateAbsoluteUnits(unitIndex) {
var params;
iterateOverDateParams(set, function(name, val, unit, i) {
// If there is a time unit set that is more specific than
// the matched unit we have a string like "5:30am in 2 minutes",
// which is meaningless, so invalidate the date...
if (i >= unitIndex) {
date.setTime(NaN);
return false;
} else if (i < unitIndex) {
// ...otherwise set the params to set the absolute date
// as a callback after the relative date has been set.
params = params || {};
params[name] = val;
deleteDateParam(set, name);
}
});
if (params) {
afterDateSet(function() {
updateDate(date, params, true, 0, false, weekdayDir);
if (optParams) {
simpleMerge(optParams, params);
}
});
if (set.edge) {
// "the end of March of next year"
handleEdge(set.edge, params);
delete set.edge;
}
}
}
if (contextDate && d) {
// If a context date is passed ("get" and "unitsFromNow"),
// then use it as the starting point.
date = cloneDateByFlag(contextDate, true);
} else {
date = getNewDate();
}
_utc(date, optFromUTC);
if (isString(d)) {
date = parseStringDate(d);
} else if (isDate(d)) {
date = cloneDateByFlag(d, optClone || forceClone);
} else if (isObjectType(d)) {
set = simpleClone(d);
updateDate(date, set, true);
} else if (isNumber(d) || d === null) {
date.setTime(d);
}
// 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 unless "setUTC" is also set.
_utc(date, !!optSetUTC);
return {
set: set,
date: date
};
}
module.exports = getExtendedDate;