devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
621 lines (620 loc) • 22 kB
JavaScript
/**
* DevExtreme (ui/scheduler/utils.recurrence.js)
* Version: 18.1.3
* Build date: Tue May 15 2018
*
* Copyright (c) 2012 - 2018 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
;
var errors = require("../../core/errors"),
extend = require("../../core/utils/extend").extend,
each = require("../../core/utils/iterator").each,
inArray = require("../../core/utils/array").inArray,
dateUtils = require("../../core/utils/date");
var toMs = dateUtils.dateToMilliseconds;
var leastDaysInWeek = 4;
var intervalMap = {
secondly: "seconds",
minutely: "minutes",
hourly: "hours",
daily: "days",
weekly: "weeks",
monthly: "months",
yearly: "years"
};
var dateSetterMap = {
bysecond: function(date, value) {
date.setSeconds(value)
},
byminute: function(date, value) {
date.setMinutes(value)
},
byhour: function(date, value) {
date.setHours(value)
},
bymonth: function(date, value) {
date.setMonth(value)
},
bymonthday: function(date, value) {
if (value < 0) {
var initialDate = new Date(date);
setDateByNegativeValue(initialDate, 1, -1);
var daysInMonth = initialDate.getDate();
if (daysInMonth >= Math.abs(value)) {
setDateByNegativeValue(date, 1, value)
} else {
setDateByNegativeValue(date, 2, value)
}
} else {
date.setDate(value);
correctDate(date, value)
}
},
byday: function(date, byDay, weekStart, frequency) {
var dayOfWeek = byDay;
if ("DAILY" === frequency && 0 === byDay) {
dayOfWeek = 7
}
byDay += days[weekStart] > dayOfWeek ? 7 : 0;
date.setDate(date.getDate() - date.getDay() + byDay)
},
byweekno: function(date, weekNumber, weekStart) {
var initialDate = new Date(date),
firstYearDate = new Date(initialDate.setMonth(0, 1)),
dayShift = firstYearDate.getDay() - days[weekStart],
firstDayOfYear = firstYearDate.getTime() - dayShift * toMs("day"),
newFirstYearDate = dayShift + 1;
if (newFirstYearDate > leastDaysInWeek) {
date.setTime(firstDayOfYear + 7 * weekNumber * toMs("day"))
} else {
date.setTime(firstDayOfYear + 7 * (weekNumber - 1) * toMs("day"))
}
var timezoneDiff = (date.getTimezoneOffset() - firstYearDate.getTimezoneOffset()) * toMs("minute");
timezoneDiff && date.setTime(date.getTime() + timezoneDiff)
},
byyearday: function(date, dayOfYear) {
date.setMonth(0, 1);
date.setDate(dayOfYear)
}
};
var setDateByNegativeValue = function(date, month, value) {
var initialDate = new Date(date);
date.setMonth(date.getMonth() + month);
if (date.getMonth() - initialDate.getMonth() > month) {
date.setDate(value + 1)
}
date.setDate(value + 1)
};
var dateGetterMap = {
bysecond: function(date) {
return date.getSeconds()
},
byminute: function(date) {
return date.getMinutes()
},
byhour: function(date) {
return date.getHours()
},
bymonth: function(date) {
return date.getMonth()
},
bymonthday: function(date) {
return date.getDate()
},
byday: function(date) {
return date.getDay()
},
byweekno: function(date, weekStart) {
var daysFromYearStart, current = new Date(date),
diff = leastDaysInWeek - current.getDay() + days[weekStart] - 1,
dayInMilliseconds = toMs("day");
if (date.getDay() < days[weekStart]) {
diff -= 7
}
current.setHours(0, 0, 0);
current.setDate(current.getDate() + diff);
daysFromYearStart = 1 + (current - new Date(current.getFullYear(), 0, 1)) / dayInMilliseconds;
return Math.ceil(daysFromYearStart / 7)
},
byyearday: function(date) {
var yearStart = new Date(date.getFullYear(), 0, 0),
timezoneDiff = date.getTimezoneOffset() - yearStart.getTimezoneOffset(),
diff = date - yearStart - timezoneDiff * toMs("minute"),
dayLength = toMs("day");
return Math.floor(diff / dayLength)
}
};
var ruleNames = ["freq", "interval", "byday", "byweekno", "byyearday", "bymonth", "bymonthday", "count", "until", "byhour", "byminute", "bysecond", "bysetpos", "wkst"],
freqNames = ["DAILY", "WEEKLY", "MONTHLY", "YEARLY", "SECONDLY", "MINUTELY", "HOURLY"],
days = {
SU: 0,
MO: 1,
TU: 2,
WE: 3,
TH: 4,
FR: 5,
SA: 6
};
var getTimeZoneOffset = function() {
return (new Date).getTimezoneOffset()
};
var dateInRecurrenceRange = function(options) {
var result = [];
if (options.rule) {
result = getDatesByRecurrence(options)
}
return !!result.length
};
var normalizeInterval = function(rule) {
var interval = rule.interval,
freq = rule.freq,
intervalObject = {},
intervalField = intervalMap[freq.toLowerCase()];
if ("MONTHLY" === freq && rule.byday) {
intervalField = intervalMap.daily
}
intervalObject[intervalField] = interval;
return intervalObject
};
var getDatesByRecurrenceException = function(ruleValues) {
var result = [];
for (var i = 0, len = ruleValues.length; i < len; i++) {
result[i] = getDateByAsciiString(ruleValues[i])
}
return result
};
var dateIsRecurrenceException = function(date, recurrenceException) {
var result = false;
if (!recurrenceException) {
return result
}
var splitDates = recurrenceException.split(","),
exceptDates = getDatesByRecurrenceException(splitDates),
shortFormat = /\d{8}$/;
for (var i = 0, len = exceptDates.length; i < len; i++) {
if (splitDates[i].match(shortFormat)) {
var diffs = getDatePartDiffs(date, exceptDates[i]);
if (0 === diffs.years && 0 === diffs.months && 0 === diffs.days) {
result = true
}
} else {
if (date.getTime() === exceptDates[i].getTime()) {
result = true
}
}
}
return result
};
var doNextIteration = function(date, startIntervalDate, endIntervalDate, recurrenceRule, iterationCount) {
var dateInInterval, matchCountIsCorrect = true;
endIntervalDate = endIntervalDate.getTime();
if (recurrenceRule.until) {
if (recurrenceRule.until.getTime() < endIntervalDate) {
endIntervalDate = recurrenceRule.until.getTime()
}
}
if (recurrenceRule.count) {
if (iterationCount === recurrenceRule.count) {
matchCountIsCorrect = false
}
}
dateInInterval = date.getTime() <= endIntervalDate;
return dateInInterval && matchCountIsCorrect
};
var getDatesByRecurrence = function(options) {
var dateRules, result = [],
recurrenceRule = getRecurrenceRule(options.rule),
iterationResult = {},
rule = recurrenceRule.rule,
recurrenceStartDate = options.start;
if (!recurrenceRule.isValid || !rule.freq) {
return result
}
rule.interval = normalizeInterval(rule);
dateRules = splitDateRules(rule);
var duration = options.end ? options.end.getTime() - options.start.getTime() : toMs("day");
var config = {
exception: options.exception,
min: options.min,
dateRules: dateRules,
rule: rule,
recurrenceStartDate: recurrenceStartDate,
recurrenceEndDate: options.end,
duration: duration
};
if (dateRules.length && rule.count) {
var iteration = 0;
getDatesByCount(dateRules, new Date(recurrenceStartDate), new Date(recurrenceStartDate), rule).forEach(function(currentDate, i) {
if (currentDate < options.max) {
iteration++;
iterationResult = pushToResult(iteration, iterationResult, currentDate, i, config, true)
}
})
} else {
getDatesByRules(dateRules, new Date(recurrenceStartDate), rule).forEach(function(currentDate, i) {
var iteration = 0;
while (doNextIteration(currentDate, recurrenceStartDate, options.max, rule, iteration)) {
iteration++;
iterationResult = pushToResult(iteration, iterationResult, currentDate, i, config);
currentDate = incrementDate(currentDate, recurrenceStartDate, rule, i)
}
})
}
if (rule.bysetpos) {
each(iterationResult, function(iterationIndex, iterationDates) {
iterationResult[iterationIndex] = filterDatesBySetPos(iterationDates, rule.bysetpos)
})
}
each(iterationResult, function(_, iterationDates) {
result = result.concat(iterationDates)
});
result.sort(function(a, b) {
return a - b
});
return result
};
var pushToResult = function(iteration, iterationResult, currentDate, i, config, verifiedField) {
if (!iterationResult[iteration]) {
iterationResult[iteration] = []
}
if (checkDate(currentDate, i, config, verifiedField)) {
iterationResult[iteration].push(currentDate)
}
return iterationResult
};
var checkDate = function(currentDate, i, config, verifiedField) {
if (!dateIsRecurrenceException(currentDate, config.exception)) {
var duration = dateUtils.sameDate(currentDate, config.recurrenceEndDate) ? config.recurrenceEndDate.getTime() - currentDate.getTime() : config.duration;
if (currentDate.getTime() >= config.recurrenceStartDate.getTime() && currentDate.getTime() + duration > config.min.getTime()) {
return verifiedField || checkDateByRule(currentDate, [config.dateRules[i]], config.rule.wkst)
}
}
return false
};
var filterDatesBySetPos = function(dates, bySetPos) {
var resultArray = [];
bySetPos.split(",").forEach(function(index) {
index = Number(index);
var dateIndex = index > 0 ? index - 1 : dates.length + index;
if (dates[dateIndex]) {
resultArray.push(dates[dateIndex])
}
});
return resultArray
};
var correctDate = function(originalDate, date) {
if (originalDate.getDate() !== date) {
originalDate.setDate(date)
}
};
var incrementDate = function(date, originalStartDate, rule, iterationStep) {
var initialDate = new Date(date),
needCorrect = true;
date = dateUtils.addInterval(date, rule.interval);
if ("MONTHLY" === rule.freq && !rule.byday) {
var expectedDate = originalStartDate.getDate();
if (rule.bymonthday) {
expectedDate = Number(rule.bymonthday.split(",")[iterationStep]);
if (expectedDate < 0) {
initialDate.setMonth(initialDate.getMonth() + 1, 1);
dateSetterMap.bymonthday(initialDate, expectedDate);
date = initialDate;
needCorrect = false
}
}
needCorrect && correctDate(date, expectedDate)
}
if ("YEARLY" === rule.freq) {
if (rule.byyearday) {
var dayNumber = Number(rule.byyearday.split(",")[iterationStep]);
dateSetterMap.byyearday(date, dayNumber)
}
var dateRules = splitDateRules(rule);
for (var field in dateRules[iterationStep]) {
dateSetterMap[field] && dateSetterMap[field](date, dateRules[iterationStep][field], rule.wkst)
}
}
return date
};
var getDatePartDiffs = function(date1, date2) {
return {
years: date1.getFullYear() - date2.getFullYear(),
months: date1.getMonth() - date2.getMonth(),
days: date1.getDate() - date2.getDate(),
hours: date1.getHours() - date2.getHours(),
minutes: date1.getMinutes() - date2.getMinutes(),
seconds: date1.getSeconds() - date2.getSeconds()
}
};
var getRecurrenceRule = function(recurrence) {
var result = {
rule: {},
isValid: false
};
if (recurrence) {
result.rule = parseRecurrenceRule(recurrence);
result.isValid = validateRRule(result.rule, recurrence)
}
return result
};
var loggedWarnings = [];
var validateRRule = function(rule, recurrence) {
if (brokenRuleNameExists(rule) || inArray(rule.freq, freqNames) === -1 || wrongCountRule(rule) || wrongIntervalRule(rule) || wrongDayOfWeek(rule) || wrongByMonthDayRule(rule) || wrongByMonth(rule) || wrongUntilRule(rule)) {
logBrokenRule(recurrence);
return false
}
return true
};
var wrongUntilRule = function(rule) {
var wrongUntil = false,
until = rule.until;
if (void 0 !== until && !(until instanceof Date)) {
wrongUntil = true
}
return wrongUntil
};
var wrongCountRule = function(rule) {
var wrongCount = false,
count = rule.count;
if (count && "string" === typeof count) {
wrongCount = true
}
return wrongCount
};
var wrongByMonthDayRule = function(rule) {
var wrongByMonthDay = false,
byMonthDay = rule.bymonthday;
if (byMonthDay && isNaN(parseInt(byMonthDay))) {
wrongByMonthDay = true
}
return wrongByMonthDay
};
var wrongByMonth = function wrongByMonth(rule) {
var wrongByMonth = false,
byMonth = rule.bymonth;
if (byMonth && isNaN(parseInt(byMonth))) {
wrongByMonth = true
}
return wrongByMonth
};
var wrongIntervalRule = function(rule) {
var wrongInterval = false,
interval = rule.interval;
if (interval && "string" === typeof interval) {
wrongInterval = true
}
return wrongInterval
};
var wrongDayOfWeek = function(rule) {
var daysByRule = daysFromByDayRule(rule),
brokenDaysExist = false;
each(daysByRule, function(_, day) {
if (!days.hasOwnProperty(day)) {
brokenDaysExist = true;
return false
}
});
return brokenDaysExist
};
var brokenRuleNameExists = function(rule) {
var brokenRuleExists = false;
each(rule, function(ruleName) {
if (inArray(ruleName, ruleNames) === -1) {
brokenRuleExists = true;
return false
}
});
return brokenRuleExists
};
var logBrokenRule = function(recurrence) {
if (inArray(recurrence, loggedWarnings) === -1) {
errors.log("W0006", recurrence);
loggedWarnings.push(recurrence)
}
};
var parseRecurrenceRule = function(recurrence) {
var ruleObject = {},
ruleParts = recurrence.split(";");
for (var i = 0, len = ruleParts.length; i < len; i++) {
var rule = ruleParts[i].split("="),
ruleName = rule[0].toLowerCase(),
ruleValue = rule[1];
ruleObject[ruleName] = ruleValue
}
var count = parseInt(ruleObject.count);
if (!isNaN(count)) {
ruleObject.count = count
}
if (ruleObject.interval) {
var interval = parseInt(ruleObject.interval);
if (!isNaN(interval)) {
ruleObject.interval = interval
}
} else {
ruleObject.interval = 1
}
if (ruleObject.freq && ruleObject.until) {
ruleObject.until = getDateByAsciiString(ruleObject.until)
}
return ruleObject
};
var getDateByAsciiString = function(string) {
if ("string" !== typeof string) {
return string
}
var arrayDate = string.match(/(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2}))?(Z)?/);
if (!arrayDate) {
return null
}
var isUTC = void 0 !== arrayDate[8],
currentOffset = 6e4 * resultUtils.getTimeZoneOffset(),
date = new(Function.prototype.bind.apply(Date, prepareDateArrayToParse(arrayDate)));
if (isUTC) {
date = new Date(date.getTime() - currentOffset)
}
return date
};
var prepareDateArrayToParse = function(arrayDate) {
arrayDate.shift();
if (void 0 === arrayDate[3]) {
arrayDate.splice(3)
} else {
arrayDate.splice(3, 1);
arrayDate.splice(6)
}
arrayDate[1]--;
arrayDate.unshift(null);
return arrayDate
};
var daysFromByDayRule = function(rule) {
var result = [];
if (rule.byday) {
result = rule.byday.split(",")
}
return result
};
var getAsciiStringByDate = function(date) {
var currentOffset = 6e4 * resultUtils.getTimeZoneOffset();
date = new Date(date.getTime() + currentOffset);
return date.getFullYear() + ("0" + (date.getMonth() + 1)).slice(-2) + ("0" + date.getDate()).slice(-2) + "T" + ("0" + date.getHours()).slice(-2) + ("0" + date.getMinutes()).slice(-2) + ("0" + date.getSeconds()).slice(-2) + "Z"
};
var splitDateRules = function(rule) {
var result = [];
if (!rule.wkst) {
rule.wkst = "MO"
}
if (rule.byweekno && !rule.byday) {
var dayNames = Object.keys(days);
for (var i = 0; i < days[rule.wkst]; i++) {
dayNames.push(dayNames.shift())
}
rule.byday = dayNames.join(",")
}
for (var field in dateSetterMap) {
if (!rule[field]) {
continue
}
var ruleFieldValues = rule[field].split(","),
ruleArray = getDateRuleArray(field, ruleFieldValues);
result = result.length ? extendObjectArray(ruleArray, result) : ruleArray
}
return result
};
var getDateRuleArray = function(field, values) {
var result = [];
for (var i = 0, length = values.length; i < length; i++) {
var dateRule = {};
dateRule[field] = handleRuleFieldValue(field, values[i]);
result.push(dateRule)
}
return result
};
var handleRuleFieldValue = function(field, value) {
var result = parseInt(value);
if ("bymonth" === field) {
result -= 1
}
if ("byday" === field) {
result = days[value]
}
return result
};
var extendObjectArray = function(firstArray, secondArray) {
var result = [];
for (var i = 0, firstArrayLength = firstArray.length; i < firstArrayLength; i++) {
for (var j = 0, secondArrayLength = secondArray.length; j < secondArrayLength; j++) {
result.push(extend({}, firstArray[i], secondArray[j]))
}
}
return result
};
var getDatesByRules = function(dateRules, startDate, rule) {
var result = [];
for (var i = 0, len = dateRules.length; i < len; i++) {
var current = dateRules[i],
updatedDate = new Date(startDate);
for (var field in current) {
dateSetterMap[field] && dateSetterMap[field](updatedDate, current[field], rule.wkst, rule.freq)
}
if (Array.isArray(updatedDate)) {
result = result.concat(updatedDate)
} else {
result.push(new Date(updatedDate))
}
}
if (!result.length) {
result.push(startDate)
}
return result
};
var getDatesByCount = function(dateRules, startDate, recurrenceStartDate, rule) {
var result = [],
count = rule.count,
counter = 0,
date = new Date(startDate.setDate(1));
while (counter < count) {
var dates = getDatesByRules(dateRules, date, rule);
var checkedDates = [];
for (var i = 0; i < dates.length; i++) {
if (dates[i].getTime() >= recurrenceStartDate.getTime()) {
checkedDates.push(dates[i])
}
}
var length = checkedDates.length;
counter += length;
var delCount = counter - count;
if (counter > count) {
checkedDates.splice(length - delCount, delCount)
}
for (i = 0; i < checkedDates.length; i++) {
result.push(checkedDates[i])
}
date = dateUtils.addInterval(date, rule.interval)
}
return result
};
var checkDateByRule = function(date, rules, weekStart) {
var result = false;
for (var i = 0; i < rules.length; i++) {
var current = rules[i],
currentRuleResult = true;
for (var field in current) {
var processNegative = "bymonthday" === field && current[field] < 0;
if (dateGetterMap[field] && !processNegative && current[field] !== dateGetterMap[field](date, weekStart)) {
currentRuleResult = false
}
}
result = result || currentRuleResult
}
return result || !rules.length
};
var getRecurrenceString = function(object) {
if (!object || !object.freq) {
return
}
var result = "";
for (var field in object) {
var value = object[field];
if ("interval" === field && value < 2) {
continue
}
if ("until" === field) {
value = getAsciiStringByDate(value)
}
result += field + "=" + value + ";"
}
result = result.substring(0, result.length - 1);
return result.toUpperCase()
};
var resultUtils = {
getRecurrenceString: getRecurrenceString,
getRecurrenceRule: getRecurrenceRule,
getAsciiStringByDate: getAsciiStringByDate,
getDatesByRecurrence: getDatesByRecurrence,
dateInRecurrenceRange: dateInRecurrenceRange,
getDateByAsciiString: getDateByAsciiString,
daysFromByDayRule: daysFromByDayRule,
getTimeZoneOffset: getTimeZoneOffset
};
module.exports = resultUtils;