var isNodeJS = !(typeof exports === 'undefined');
(function(exports, moment) {
var beginningOfMonth = exports.beginningOfMonth = function(date) {
var r = beginningOfDay(date);
r.setDate(1);
return r;
};
/** Warning: only safe to use if day of month is less than 28...
*/
var addMonths = function(date, months) {
var d = new Date(date);
d.setMonth(date.getMonth() + months);
return d;
};
var dateFromArg = function(stringOrDate) {
if ((typeof stringOrDate) == 'string') return new Date(stringOrDate);
return stringOrDate;
}
var beginningOfDay = exports.beginningOfDay = function(date) {
var r = new Date(date);
r.setHours(0);
r.setMinutes(0);
r.setSeconds(0);
r.setMilliseconds(0);
return r;
}
var Once = function Once(when) {
this.when = dateFromArg(when);
};
Once.prototype.nextOccurrence = function(onOrAfter) {
if (this.when >= onOrAfter) {
return this.when;
} else {
return null;
}
};
Once.prototype.isOccurring = function(aDate) {
return beginningOfDay(aDate) == beginningOfDay(this.when);
};
/**
* Specifies an expression that matches one particular day.
* @param {Date} when = the day matched
*/
exports.once = function(when) {
return new Once(when);
}
var OnSpecificDates = function OnSpecificDates(dates) {
this.datesSorted = [];
for (var i = 0; i < dates.length; i++) {
this.datesSorted.push(beginningOfDay(dates[i]));
}
this.datesSorted.sort(function(a, b) { return a.getTime() > b.getTime() });
}
OnSpecificDates.prototype.isOccurring = function(aDate) {
var day = beginningOfDay(aDate);
for (var i = 0; i < this.datesSorted.length; i++) {
if (day.getTime() == this.datesSorted[i].getTime()) {
return true;
}
if (day < this.datesSorted[i]) {
return false;
}
}
return false;
}
OnSpecificDates.prototype.nextOccurrence = function(onOrAfter, butNotLaterThan) {
for (var i = 0; i < this.datesSorted.length; i++) {
if (this.datesSorted[i] > butNotLaterThan) {
return null;
}
if (this.datesSorted[i] >= onOrAfter) {
return this.datesSorted[i];
}
}
return null;
}
exports.onSpecificDates = function(dates) {
return new OnSpecificDates(dates);
}
var OnOrAfter = function OnOrAfter(firstDay) {
this.firstDay = dateFromArg(firstDay);
};
OnOrAfter.prototype.isOccurring = function(aDate) {
return this.firstDay <= beginningOfDay(aDate);
};
OnOrAfter.prototype.nextOccurrence = function(onOrAfter) {
if (this.firstDay >= onOrAfter) {
return this.firstDay;
} else {
return onOrAfter;
}
};
/**
* Specifies an expression that matches a day and all days after that day.
* @param {Date} firstDay - The first day to match
*/
exports.onOrAfter = function(firstDay) {
return new OnOrAfter(firstDay);
}
var NegationOf = function NegationOf(expr) {
this.expr = expr;
}
NegationOf.prototype.isOccurring = function(aDate) {
var result = !this.expr.isOccurring(aDate);
return result;
}
NegationOf.prototype.nextOccurrence = function(onOrAfter, butNotLaterThan) {
var when = onOrAfter;
while (when <= butNotLaterThan) {
var next = this.expr.nextOccurrence(when, butNotLaterThan);
if (!next || next > when) {
return when;
}
when = addDays(next, 1);
}
return null;
}
/**
* Specifies an expression that matches all days that do not match the
* expression given.
* @param {Object} expr - The expression, created by one
* of the functions exported by TempEx.
*/
exports.negate = function(expression) {
return new NegationOf(expression);
}
/**
* Specifies an expression that matches all days up to and including lastDay.
* @param {Date} lastDay - last day to be matched.
*/
exports.onOrBefore = function(lastDay) {
return new NegationOf(new OnOrAfter(addDays(lastDay, 1)));
}
/**
* Specifies an expression that matches all days that are not in a list of
* given days.
* @param {Array} dates - array of Date instances
*/
exports.notOnSpecificDates = function(dates) {
return new NegationOf(new OnSpecificDates(dates));
}
var OnWeekdays = function OnWeekdays( /* e.g. [ 0, 2, 3 ] for Sun,Tue,Wed */ days) {
this.days = days;
}
OnWeekdays.prototype.isOccurring = function(aDate) {
var day = aDate.getDay();
for (var i = 0; i < this.days.length; i++) {
if (this.days[i] == day) {
return true;
}
}
return false;
};
OnWeekdays.prototype.nextOccurrence = function(onOrAfter) {
var onOrAfterDay = onOrAfter.getDay();
var daysToAdd = null;
for (var i = 0; i < this.days.length; i++) {
var deltaUntilDay = this.days[i] - onOrAfterDay;
while (deltaUntilDay < 0) {
deltaUntilDay += 7;
}
if (daysToAdd === null) {
daysToAdd = deltaUntilDay;
} else {
daysToAdd = Math.min(daysToAdd, deltaUntilDay);
}
}
return addDays(onOrAfter, daysToAdd);
};
/**
* Specifies an expression that matches days in the week, e.g. Mondays and
* Wednesday, but not all other week days.
* @param {Array} days - Array of integers describing the days of the week,
* e.g. [0, 2, 3] will represent Sundays, Tuesdays and Wednesdays
*/
exports.onWeekdays = function(days /* e.g. [ 0, 2, 3 ] for Sun,Tue,Wed */) {
return new OnWeekdays(days);
}
/**
* 'rolling over' means to add 12 to each month in months that's less than month...
*/
var getMonthsRolledOver = function(months, month) {
var result = [];
for (var i = 0; i < months.length; i++) {
var rolledOverMonth = months[i] < month ? months[i] + 12 : months[i];
result.push(rolledOverMonth);
}
return result;
}
InMonths = function InMonths(months) {
this.months = months;
}
InMonths.prototype.nextOccurrence = function(onOrAfter) {
var month = onOrAfter.getMonth();
var firstOfMonth = beginningOfMonth(onOrAfter);
var monthsRolledOver = getMonthsRolledOver(this.months, month);
var monthsToAdd = Math.min.apply(null, monthsRolledOver) - month;
if (monthsToAdd > 0) {
return addMonths(firstOfMonth, monthsToAdd);
} else {
return onOrAfter;
}
};
InMonths.prototype.isOccurring = function(aDate) {
var next = this.nextOccurrence(aDate);
return next.getTime() == aDate.getTime();
};
/** Match one or multiple months.
* @param {Array} months - array of integers specifying months to match.
* 0=January, 1=February, etc.
*/
exports.months = function(months) {
return new InMonths(months);
};
var DayInMonth = function DayInMonth(day) {
this.day = day;
}
DayInMonth.prototype.nextOccurrence = function(onOrAfter) {
if (onOrAfter.getDate() <= this.day) {
return addDays(onOrAfter, this.day - onOrAfter.getDate());
} else {
var next = addMonths(beginningOfMonth(onOrAfter), 1);
next.setDate(this.day);
return next;
}
};
/** Matches based on the calendar day in the month
* @param {int} day - the Day in the month, e.g. 11 for the 11th day of the month
*/
exports.dayInMonth = function(day) {
return new DayInMonth(day);
}
DayOfWeekInMonth = function DayOfWeekInMonth(day, nth) {
this.day = day;
this.nth = nth;
};
DayOfWeekInMonth.prototype.nextOccurrence = function(onOrAfter) {
var startOfThisMonth = beginningOfMonth(onOrAfter);
var daysToSkip = this.day < startOfThisMonth.getDay() ? (7 - startOfThisMonth.getDay()) : 0;
var daysToAdd = this.day - addDays(startOfThisMonth, daysToSkip).getDay();
daysToAdd += (this.nth - 1) * 7;
var targetDateThisMonth = addDays(startOfThisMonth, daysToSkip + daysToAdd);
if (targetDateThisMonth < onOrAfter) {
return this.nextOccurrence(addMonths(startOfThisMonth, 1));
} else {
return targetDateThisMonth;
}
}
/** Matches based on the day of the week in the month.
* @param {int} day - the day of the week, 0=Sunday, 1=Monday, etc.
* @param {int} nth - If 1, it will match the first day (e.g. first Sunday)
* of the month, if 2 it will be the second, etc.
*/
exports.dayOfWeekInMonth = function(day, nth) {
return new DayOfWeekInMonth(day, nth);
};
function EveryNthWeekFrom(n, day) {
this.n = n;
this.day = moment(day);
}
EveryNthWeekFrom.prototype.nextOccurrence = function(onOrAfter) {
var until = moment(onOrAfter);
var daysDiff = until.diff(this.day, 'days');
var weeksDiff = Math.floor(daysDiff / 7);
if ((weeksDiff % this.n) == 0) {
return moment(until.toDate()).toDate();
} else {
var nextWeekFromDay = weeksDiff - (weeksDiff % this.n) + this.n;
var firstDayOfNextWeekFromDay = moment(this.day).add(nextWeekFromDay, 'week');
return firstDayOfNextWeekFromDay.toDate();
}
}
exports.everyNthWeekFrom = function(n, day) {
return new EveryNthWeekFrom(n, day);
};
var maxNextOccurrenceOf = function(expressions, onOrAfter, butNotLaterThan) {
if (onOrAfter === null) {
throw "onOrAfter cannot be null";
}
var maxNext = null;
for (var i = 0; i < expressions.length; i++) {
var next = expressions[i].nextOccurrence(onOrAfter, butNotLaterThan);
if (next === null) {
return null; // no next occurrence for this expression, so no maxNext
}
if (next < onOrAfter) {
throw("nextOccurrence cannot be smaller than 'onOrAfter'")
}
if (maxNext === null || maxNext < next) {
maxNext = next;
}
}
return maxNext;
};
var allAreOccurringOn = function(expressions, when) {
if (when === null) {
return false;
}
for (var i = 0; i < expressions.length; i++) {
if (!expressions[i].isOccurring(when)) {
return false;
}
}
return true;
};
var Union = function Union(expressions) {
this.unionOf = expressions;
}
Union.prototype.isOccurring = function(aDate) {
var i;
for (i = 0; i < this.unionOf.length; i++) {
if (this.unionOf[i].isOccurring(aDate)) {
return true;
}
}
return false;
}
Union.prototype.nextOccurrence = function(onOrAfter, butNotLaterThan) {
var theNext;
var expressions = this.unionOf;
for (var i = 0; i < expressions.length; i++) {
var occurrence = expressions[i].nextOccurrence(onOrAfter, butNotLaterThan);
if (occurrence && occurrence < onOrAfter) {
throw new Error("invalid next occurrence: " + occurrence + " is less than " + onOrAfter);
}
if (!theNext || occurrence && occurrence < theNext) {
theNext = occurrence;
}
}
return theNext;
}
/**
* Matches the union of the expressions given: If any of the expressions
* provided matches, then it is a match.
* @param {Array} expressions - array of expressions, as created by one of
* the factory functions of TempEx.
*/
exports.union = function(expressions) {
return new Union(expressions);
};
var IntersectionOf = function IntersectionOf(expr1, expr2) {
this.expressions = [ expr1, expr2 ];
};
IntersectionOf.prototype.isOccurring = function(aDate) {
var i;
for (i = 0; i < this.expressions.length; i++) {
if (!this.expressions[i].isOccurring(aDate)) {
return false;
}
}
return true;
};
IntersectionOf.prototype.nextOccurrence = function(onOrAfter, butNotLaterThan) {
while (onOrAfter <= butNotLaterThan) {
var nextOfAll = maxNextOccurrenceOf(this.expressions, onOrAfter, butNotLaterThan);
if (nextOfAll === null) {
return null;
}
if (allAreOccurringOn(this.expressions, nextOfAll)) {
return nextOfAll;
}
onOrAfter = addDays(nextOfAll, 1);
}
return null;
};
/** Specifies an intersection of two expression. Only those days will match
* this expression that match both (sub) expressions.
* @param {Object} expr1 - first expression
* @param {Object} expr2 - second expression
*/
exports.intersectionOf = function(expr1, expr2) {
return new IntersectionOf(expr1, expr2);
};
var nextOccurrence = exports.nextOccurrence = function(expression, onOrAfter, butNotLaterThan) {
var occurrence = expression.nextOccurrence(onOrAfter, butNotLaterThan);
if (occurrence && occurrence < onOrAfter) {
throw "invalid next occurrence: " + occurrence + " is less than " + onOrAfter;
}
return occurrence;
}
/** Adds (or subtracts) days from a date given.
* @param {Date} aDate - the date to add days to
* @param {int} howMany - the amount of days to add
*/
var addDays;
exports.addDays = addDays = function(date, days) {
var r = new Date(date);
r.setDate(r.getDate() + days);
return r;
}
/**
* Given a time interval (date range), determines all occurrences (days)
* within the interval for the given expression.
* @param {Object} expression - An expression created by one of the factory functions
* @param {Date} from - start of time interval
* @param {Date} to - end of time interval
*/
exports.occurrences = function(expression, from, to) {
if (!expression || !from || !(from instanceof Date) || !to || !(to instanceof Date)) {
throw new Error("invalid arguments");
}
var result = [];
while(from <= to) {
occurrence = nextOccurrence(expression, from, to);
if(!occurrence || occurrence > to) {
break;
}
result.push(occurrence);
from = beginningOfDay(addDays(occurrence, 1));
}
return result;
};
})(isNodeJS ? exports : this['TempEx']={}, isNodeJS ? require('moment') : moment);