mirror of
https://github.com/pretix/pretix.git
synced 2026-05-10 16:04:02 +00:00
* copy-from things * Some frontend * rrule UI * . * Fixes * UI improvements * First test * Tests
2288 lines
67 KiB
JavaScript
Vendored
2288 lines
67 KiB
JavaScript
Vendored
/*!
|
|
* Changes by Raphael Michel starting line 112!!
|
|
* See also: https://github.com/jakubroztocil/rrule/issues/157
|
|
*
|
|
* rrule.js - Library for working with recurrence rules for calendar dates.
|
|
* https://github.com/jakubroztocil/rrule
|
|
*
|
|
* Copyright 2010, Jakub Roztocil and Lars Schoning
|
|
* Licenced under the BSD licence.
|
|
* https://github.com/jakubroztocil/rrule/blob/master/LICENCE
|
|
*
|
|
* Based on:
|
|
* python-dateutil - Extensions to the standard Python datetime module.
|
|
* Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
|
* Copyright (c) 2012 - Tomi Pieviläinen <tomi.pievilainen@iki.fi>
|
|
* https://github.com/jakubroztocil/rrule/blob/master/LICENCE
|
|
*/
|
|
/* global module, define */
|
|
|
|
;(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory()
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
define([], factory)
|
|
} else {
|
|
root.RRule = factory(root)
|
|
root.RRuleSet = root.RRule.RRuleSet
|
|
root.rrulestr = root.RRule.rrulestr
|
|
}
|
|
}(typeof window === 'object' ? window : this, function (root) {
|
|
// =============================================================================
|
|
// Date utilities
|
|
// =============================================================================
|
|
|
|
/**
|
|
* General date-related utilities.
|
|
* Also handles several incompatibilities between JavaScript and Python
|
|
*
|
|
*/
|
|
var dateutil = {
|
|
MONTH_DAYS: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
|
|
|
|
/**
|
|
* Number of milliseconds of one day
|
|
*/
|
|
ONE_DAY: 1000 * 60 * 60 * 24,
|
|
|
|
/**
|
|
* @see: <http://docs.python.org/library/datetime.html#datetime.MAXYEAR>
|
|
*/
|
|
MAXYEAR: 9999,
|
|
|
|
/**
|
|
* Python uses 1-Jan-1 as the base for calculating ordinals but we don't
|
|
* want to confuse the JS engine with milliseconds > Number.MAX_NUMBER,
|
|
* therefore we use 1-Jan-1900 instead
|
|
*/
|
|
ORDINAL_BASE: new Date(1900, 0, 1),
|
|
|
|
/**
|
|
* Python: MO-SU: 0 - 6
|
|
* JS: SU-SAT 0 - 6
|
|
*/
|
|
PY_WEEKDAYS: [6, 0, 1, 2, 3, 4, 5],
|
|
|
|
/**
|
|
* py_date.timetuple()[7]
|
|
*/
|
|
getYearDay: function (date) {
|
|
var dateNoTime = new Date(
|
|
date.getFullYear(), date.getMonth(), date.getDate())
|
|
return Math.ceil(
|
|
(dateNoTime - new Date(date.getFullYear(), 0, 1)) / dateutil.ONE_DAY) + 1
|
|
},
|
|
|
|
isLeapYear: function (year) {
|
|
return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)
|
|
},
|
|
|
|
/**
|
|
* @return {Number} the date's timezone offset in ms
|
|
*/
|
|
tzOffset: function (date) {
|
|
return date.getTimezoneOffset() * 60 * 1000
|
|
},
|
|
|
|
/**
|
|
* @see: <http://www.mcfedries.com/JavaScript/DaysBetween.asp>
|
|
*/
|
|
daysBetween: function (date1, date2) {
|
|
// The number of milliseconds in one day
|
|
// Convert both dates to milliseconds
|
|
var date1ms = date1.getTime() - dateutil.tzOffset(date1)
|
|
var date2ms = date2.getTime() - dateutil.tzOffset(date2)
|
|
// Calculate the difference in milliseconds
|
|
var differencems = date1ms - date2ms
|
|
// Convert back to days and return
|
|
return Math.round(differencems / dateutil.ONE_DAY)
|
|
},
|
|
|
|
/**
|
|
* @see: <http://docs.python.org/library/datetime.html#datetime.date.toordinal>
|
|
*/
|
|
toOrdinal: function (date) {
|
|
if (date < dateutil.ORDINAL_BASE) {
|
|
throw new Error('dates lower than ' + dateutil.ORDINAL_BASE + ' are not supported')
|
|
}
|
|
return dateutil.daysBetween(date, dateutil.ORDINAL_BASE)
|
|
},
|
|
|
|
/**
|
|
* @see - <http://docs.python.org/library/datetime.html#datetime.date.fromordinal>
|
|
*/
|
|
fromOrdinal: function (ordinal) {
|
|
var millisecsFromBase = ordinal * dateutil.ONE_DAY
|
|
return new Date(dateutil.ORDINAL_BASE.getTime() +
|
|
millisecsFromBase);
|
|
},
|
|
|
|
/**
|
|
* @see: <http://docs.python.org/library/calendar.html#calendar.monthrange>
|
|
*/
|
|
monthRange: function (year, month) {
|
|
var date = new Date(year, month, 1)
|
|
return [dateutil.getWeekday(date), dateutil.getMonthDays(date)]
|
|
},
|
|
|
|
getMonthDays: function (date) {
|
|
var month = date.getMonth()
|
|
return month === 1 && dateutil.isLeapYear(date.getFullYear())
|
|
? 29 : dateutil.MONTH_DAYS[month]
|
|
},
|
|
|
|
/**
|
|
* @return {Number} python-like weekday
|
|
*/
|
|
getWeekday: function (date) {
|
|
return dateutil.PY_WEEKDAYS[date.getDay()]
|
|
},
|
|
|
|
/**
|
|
* @see: <http://docs.python.org/library/datetime.html#datetime.datetime.combine>
|
|
*/
|
|
combine: function (date, time) {
|
|
time = time || date
|
|
return new Date(
|
|
date.getFullYear(), date.getMonth(), date.getDate(),
|
|
time.getHours(), time.getMinutes(), time.getSeconds(),
|
|
time.getMilliseconds())
|
|
},
|
|
|
|
clone: function (date) {
|
|
var dolly = new Date(date.getTime())
|
|
return dolly
|
|
},
|
|
|
|
cloneDates: function (dates) {
|
|
var clones = []
|
|
for (var i = 0; i < dates.length; i++) {
|
|
clones.push(dateutil.clone(dates[i]))
|
|
}
|
|
return clones
|
|
},
|
|
|
|
/**
|
|
* Sorts an array of Date or dateutil.Time objects
|
|
*/
|
|
sort: function (dates) {
|
|
dates.sort(function (a, b) {
|
|
return a.getTime() - b.getTime()
|
|
})
|
|
},
|
|
|
|
timeToUntilString: function (time) {
|
|
var comp
|
|
var date = new Date(time)
|
|
var comps = [
|
|
date.getUTCFullYear(),
|
|
date.getUTCMonth() + 1,
|
|
date.getUTCDate(),
|
|
'T',
|
|
date.getUTCHours(),
|
|
date.getUTCMinutes(),
|
|
date.getUTCSeconds(),
|
|
'Z'
|
|
]
|
|
|
|
for (var i = 0; i < comps.length; i++) {
|
|
comp = comps[i]
|
|
if (!/[TZ]/.test(comp) && comp < 10) comps[i] = '0' + String(comp)
|
|
}
|
|
return comps.join('')
|
|
},
|
|
|
|
untilStringToDate: function (until) {
|
|
var re = /^(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2})Z?)?$/
|
|
var bits = re.exec(until)
|
|
if (!bits) throw new Error('Invalid UNTIL value: ' + until)
|
|
return new Date(Date.UTC(
|
|
bits[1],
|
|
bits[2] - 1,
|
|
bits[3],
|
|
bits[5] || 0,
|
|
bits[6] || 0,
|
|
bits[7] || 0))
|
|
}
|
|
}
|
|
|
|
dateutil.Time = function (hour, minute, second, millisecond) {
|
|
this.hour = hour
|
|
this.minute = minute
|
|
this.second = second
|
|
this.millisecond = millisecond || 0
|
|
}
|
|
|
|
dateutil.Time.prototype = {
|
|
constructor: dateutil.Time,
|
|
getHours: function () {
|
|
return this.hour
|
|
},
|
|
getMinutes: function () {
|
|
return this.minute
|
|
},
|
|
getSeconds: function () {
|
|
return this.second
|
|
},
|
|
getMilliseconds: function () {
|
|
return this.millisecond
|
|
},
|
|
getTime: function () {
|
|
return ((this.hour * 60 * 60) + (this.minute * 60) + this.second) * 1000 +
|
|
this.millisecond
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helper functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Simplified version of python's range()
|
|
*/
|
|
var range = function (start, end) {
|
|
if (arguments.length === 1) {
|
|
end = start
|
|
start = 0
|
|
}
|
|
var rang = []
|
|
for (var i = start; i < end; i++) rang.push(i)
|
|
return rang
|
|
}
|
|
|
|
var repeat = function (value, times) {
|
|
var i = 0
|
|
var array = []
|
|
|
|
if (value instanceof Array) {
|
|
for (; i < times; i++) array[i] = [].concat(value)
|
|
} else {
|
|
for (; i < times; i++) array[i] = value
|
|
}
|
|
return array
|
|
}
|
|
|
|
/**
|
|
* Python like split
|
|
*/
|
|
var split = function (str, sep, num) {
|
|
var splits = str.split(sep)
|
|
return num
|
|
? splits.slice(0, num).concat([splits.slice(num).join(sep)]) : splits
|
|
}
|
|
|
|
/**
|
|
* closure/goog/math/math.js:modulo
|
|
* Copyright 2006 The Closure Library Authors.
|
|
* The % operator in JavaScript returns the remainder of a / b, but differs from
|
|
* some other languages in that the result will have the same sign as the
|
|
* dividend. For example, -1 % 8 == -1, whereas in some other languages
|
|
* (such as Python) the result would be 7. This function emulates the more
|
|
* correct modulo behavior, which is useful for certain applications such as
|
|
* calculating an offset index in a circular list.
|
|
*
|
|
* @param {number} a The dividend.
|
|
* @param {number} b The divisor.
|
|
* @return {number} a % b where the result is between 0 and b (either 0 <= x < b
|
|
* or b < x <= 0, depending on the sign of b).
|
|
*/
|
|
var pymod = function (a, b) {
|
|
var r = a % b
|
|
// If r and b differ in sign, add b to wrap the result to the correct sign.
|
|
return (r * b < 0) ? r + b : r
|
|
}
|
|
|
|
/**
|
|
* @see: <http://docs.python.org/library/functions.html#divmod>
|
|
*/
|
|
var divmod = function (a, b) {
|
|
return {div: Math.floor(a / b), mod: pymod(a, b)}
|
|
}
|
|
|
|
/**
|
|
* Python-like boolean
|
|
* @return {Boolean} value of an object/primitive, taking into account
|
|
* the fact that in Python an empty list's/tuple's
|
|
* boolean value is False, whereas in JS it's true
|
|
*/
|
|
var plb = function (obj) {
|
|
return (obj instanceof Array && obj.length === 0)
|
|
? false : Boolean(obj)
|
|
}
|
|
|
|
/**
|
|
* Return true if a value is in an array
|
|
*/
|
|
var contains = function (arr, val) {
|
|
return arr.indexOf(val) !== -1
|
|
}
|
|
|
|
// =============================================================================
|
|
// Date masks
|
|
// =============================================================================
|
|
|
|
// Every mask is 7 days longer to handle cross-year weekly periods.
|
|
|
|
var M365MASK = [].concat(
|
|
repeat(1, 31), repeat(2, 28), repeat(3, 31),
|
|
repeat(4, 30), repeat(5, 31), repeat(6, 30),
|
|
repeat(7, 31), repeat(8, 31), repeat(9, 30),
|
|
repeat(10, 31), repeat(11, 30), repeat(12, 31),
|
|
repeat(1, 7))
|
|
|
|
var M366MASK = [].concat(
|
|
repeat(1, 31), repeat(2, 29), repeat(3, 31),
|
|
repeat(4, 30), repeat(5, 31), repeat(6, 30),
|
|
repeat(7, 31), repeat(8, 31), repeat(9, 30),
|
|
repeat(10, 31), repeat(11, 30), repeat(12, 31),
|
|
repeat(1, 7))
|
|
|
|
var M28 = range(1, 29)
|
|
var M29 = range(1, 30)
|
|
var M30 = range(1, 31)
|
|
var M31 = range(1, 32)
|
|
|
|
var MDAY366MASK = [].concat(
|
|
M31, M29, M31,
|
|
M30, M31, M30,
|
|
M31, M31, M30,
|
|
M31, M30, M31,
|
|
M31.slice(0, 7))
|
|
|
|
var MDAY365MASK = [].concat(
|
|
M31, M28, M31,
|
|
M30, M31, M30,
|
|
M31, M31, M30,
|
|
M31, M30, M31,
|
|
M31.slice(0, 7))
|
|
|
|
M28 = range(-28, 0)
|
|
M29 = range(-29, 0)
|
|
M30 = range(-30, 0)
|
|
M31 = range(-31, 0)
|
|
|
|
var NMDAY366MASK = [].concat(
|
|
M31, M29, M31,
|
|
M30, M31, M30,
|
|
M31, M31, M30,
|
|
M31, M30, M31,
|
|
M31.slice(0, 7))
|
|
|
|
var NMDAY365MASK = [].concat(
|
|
M31, M28, M31,
|
|
M30, M31, M30,
|
|
M31, M31, M30,
|
|
M31, M30, M31,
|
|
M31.slice(0, 7))
|
|
|
|
var M366RANGE = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
|
|
var M365RANGE = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
|
|
|
|
var WDAYMASK = (function () {
|
|
for (var wdaymask = [], i = 0; i < 55; i++) wdaymask = wdaymask.concat(range(7))
|
|
return wdaymask
|
|
}())
|
|
|
|
var WDAYS = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']
|
|
|
|
// =============================================================================
|
|
// Weekday
|
|
// =============================================================================
|
|
|
|
var Weekday = function (weekday, n) {
|
|
if (n === 0) throw new Error("Can't create weekday with n == 0")
|
|
this.weekday = weekday
|
|
this.n = n
|
|
}
|
|
|
|
Weekday.prototype = {
|
|
constructor: Weekday,
|
|
// __call__ - Cannot call the object directly, do it through
|
|
// e.g. RRule.TH.nth(-1) instead,
|
|
nth: function (n) {
|
|
return this.n === n ? this : new Weekday(this.weekday, n)
|
|
},
|
|
|
|
// __eq__
|
|
equals: function (other) {
|
|
return this.weekday === other.weekday && this.n === other.n
|
|
},
|
|
|
|
// __repr__
|
|
toString: function () {
|
|
var s = WDAYS[this.weekday]
|
|
if (this.n) s = (this.n > 0 ? '+' : '') + String(this.n) + s
|
|
return s
|
|
},
|
|
|
|
getJsWeekday: function () {
|
|
return this.weekday === 6 ? 0 : this.weekday + 1
|
|
}
|
|
|
|
}
|
|
|
|
// =============================================================================
|
|
// RRule
|
|
// =============================================================================
|
|
|
|
/**
|
|
*
|
|
* @param {Object?} options - see <http://labix.org/python-dateutil/#head-cf004ee9a75592797e076752b2a889c10f445418>
|
|
* The only required option is `freq`, one of RRule.YEARLY, RRule.MONTHLY, ...
|
|
* @constructor
|
|
*/
|
|
var RRule = function (options, noCache) {
|
|
options = options || {}
|
|
// RFC string
|
|
this._string = null
|
|
this._cache = noCache ? null : {
|
|
all: false,
|
|
before: [],
|
|
after: [],
|
|
between: []
|
|
}
|
|
|
|
// used by toString()
|
|
this.origOptions = {}
|
|
|
|
this.options = {}
|
|
|
|
var invalid = []
|
|
var keys = Object.keys(options)
|
|
var defaultKeys = Object.keys(RRule.DEFAULT_OPTIONS)
|
|
|
|
// Shallow copy for options and origOptions and check for invalid
|
|
keys.forEach(function (key) {
|
|
this.origOptions[key] = options[key]
|
|
this.options[key] = options[key]
|
|
if (!contains(defaultKeys, key)) invalid.push(key)
|
|
}, this)
|
|
|
|
if (invalid.length) throw new Error('Invalid options: ' + invalid.join(', '))
|
|
|
|
if (!RRule.FREQUENCIES[options.freq] && options.byeaster === null) {
|
|
throw new Error('Invalid frequency: ' + String(options.freq))
|
|
}
|
|
|
|
// Merge in default options
|
|
defaultKeys.forEach(function (key) {
|
|
if (!contains(keys, key)) this.options[key] = RRule.DEFAULT_OPTIONS[key]
|
|
}, this)
|
|
|
|
var opts = this.options
|
|
|
|
if (opts.byeaster !== null) opts.freq = RRule.YEARLY
|
|
if (!opts.dtstart) opts.dtstart = new Date(new Date().setMilliseconds(0))
|
|
|
|
var millisecondModulo = opts.dtstart.getTime() % 1000
|
|
if (opts.wkst === null) {
|
|
opts.wkst = RRule.MO.weekday
|
|
} else if (typeof opts.wkst === 'number') {
|
|
// cool, just keep it like that
|
|
} else {
|
|
opts.wkst = opts.wkst.weekday
|
|
}
|
|
|
|
if (opts.bysetpos !== null) {
|
|
if (typeof opts.bysetpos === 'number') opts.bysetpos = [opts.bysetpos]
|
|
|
|
for (var i = 0; i < opts.bysetpos.length; i++) {
|
|
var v = opts.bysetpos[i]
|
|
if (v === 0 || !(v >= -366 && v <= 366)) {
|
|
throw new Error('bysetpos must be between 1 and 366,' +
|
|
' or between -366 and -1')
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!(plb(opts.byweekno) || plb(opts.byyearday) || plb(opts.bymonthday) ||
|
|
opts.byweekday !== null || opts.byeaster !== null)) {
|
|
switch (opts.freq) {
|
|
case RRule.YEARLY:
|
|
if (!opts.bymonth) opts.bymonth = opts.dtstart.getMonth() + 1
|
|
opts.bymonthday = opts.dtstart.getDate()
|
|
break
|
|
case RRule.MONTHLY:
|
|
opts.bymonthday = opts.dtstart.getDate()
|
|
break
|
|
case RRule.WEEKLY:
|
|
opts.byweekday = dateutil.getWeekday(opts.dtstart)
|
|
break
|
|
}
|
|
}
|
|
|
|
// bymonth
|
|
if (opts.bymonth !== null && !(opts.bymonth instanceof Array)) {
|
|
opts.bymonth = [opts.bymonth]
|
|
}
|
|
// byyearday
|
|
if (opts.byyearday !== null && !(opts.byyearday instanceof Array)) {
|
|
opts.byyearday = [opts.byyearday]
|
|
}
|
|
|
|
// bymonthday
|
|
if (opts.bymonthday === null) {
|
|
opts.bymonthday = []
|
|
opts.bynmonthday = []
|
|
} else if (opts.bymonthday instanceof Array) {
|
|
var bymonthday = []
|
|
var bynmonthday = []
|
|
|
|
for (i = 0; i < opts.bymonthday.length; i++) {
|
|
v = opts.bymonthday[i]
|
|
if (v > 0) {
|
|
bymonthday.push(v)
|
|
} else if (v < 0) {
|
|
bynmonthday.push(v)
|
|
}
|
|
}
|
|
opts.bymonthday = bymonthday
|
|
opts.bynmonthday = bynmonthday
|
|
} else {
|
|
if (opts.bymonthday < 0) {
|
|
opts.bynmonthday = [opts.bymonthday]
|
|
opts.bymonthday = []
|
|
} else {
|
|
opts.bynmonthday = []
|
|
opts.bymonthday = [opts.bymonthday]
|
|
}
|
|
}
|
|
|
|
// byweekno
|
|
if (opts.byweekno !== null && !(opts.byweekno instanceof Array)) {
|
|
opts.byweekno = [opts.byweekno]
|
|
}
|
|
|
|
// byweekday / bynweekday
|
|
if (opts.byweekday === null) {
|
|
opts.bynweekday = null
|
|
} else if (typeof opts.byweekday === 'number') {
|
|
opts.byweekday = [opts.byweekday]
|
|
opts.bynweekday = null
|
|
} else if (opts.byweekday instanceof Weekday) {
|
|
if (!opts.byweekday.n || opts.freq > RRule.MONTHLY) {
|
|
opts.byweekday = [opts.byweekday.weekday]
|
|
opts.bynweekday = null
|
|
} else {
|
|
opts.bynweekday = [
|
|
[opts.byweekday.weekday, opts.byweekday.n]
|
|
]
|
|
opts.byweekday = null
|
|
}
|
|
} else {
|
|
var byweekday = []
|
|
var bynweekday = []
|
|
|
|
for (i = 0; i < opts.byweekday.length; i++) {
|
|
var wday = opts.byweekday[i]
|
|
|
|
if (typeof wday === 'number') {
|
|
byweekday.push(wday)
|
|
} else if (!wday.n || opts.freq > RRule.MONTHLY) {
|
|
byweekday.push(wday.weekday)
|
|
} else {
|
|
bynweekday.push([wday.weekday, wday.n])
|
|
}
|
|
}
|
|
opts.byweekday = plb(byweekday) ? byweekday : null
|
|
opts.bynweekday = plb(bynweekday) ? bynweekday : null
|
|
}
|
|
|
|
// byhour
|
|
if (opts.byhour === null) {
|
|
opts.byhour = (opts.freq < RRule.HOURLY) ? [opts.dtstart.getHours()] : null
|
|
} else if (typeof opts.byhour === 'number') {
|
|
opts.byhour = [opts.byhour]
|
|
}
|
|
|
|
// byminute
|
|
if (opts.byminute === null) {
|
|
opts.byminute = (opts.freq < RRule.MINUTELY)
|
|
? [opts.dtstart.getMinutes()] : null
|
|
} else if (typeof opts.byminute === 'number') {
|
|
opts.byminute = [opts.byminute]
|
|
}
|
|
|
|
// bysecond
|
|
if (opts.bysecond === null) {
|
|
opts.bysecond = (opts.freq < RRule.SECONDLY)
|
|
? [opts.dtstart.getSeconds()] : null
|
|
} else if (typeof opts.bysecond === 'number') {
|
|
opts.bysecond = [opts.bysecond]
|
|
}
|
|
|
|
if (opts.freq >= RRule.HOURLY) {
|
|
this.timeset = null
|
|
} else {
|
|
this.timeset = []
|
|
for (i = 0; i < opts.byhour.length; i++) {
|
|
var hour = opts.byhour[i]
|
|
for (var j = 0; j < opts.byminute.length; j++) {
|
|
var minute = opts.byminute[j]
|
|
for (var k = 0; k < opts.bysecond.length; k++) {
|
|
var second = opts.bysecond[k]
|
|
// python:
|
|
// datetime.time(hour, minute, second,
|
|
// tzinfo=self._tzinfo))
|
|
this.timeset.push(new dateutil.Time(hour, minute, second, millisecondModulo))
|
|
}
|
|
}
|
|
}
|
|
dateutil.sort(this.timeset)
|
|
}
|
|
}
|
|
|
|
// RRule class 'constants'
|
|
|
|
RRule.FREQUENCIES = [
|
|
'YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY',
|
|
'HOURLY', 'MINUTELY', 'SECONDLY'
|
|
]
|
|
|
|
RRule.YEARLY = 0
|
|
RRule.MONTHLY = 1
|
|
RRule.WEEKLY = 2
|
|
RRule.DAILY = 3
|
|
RRule.HOURLY = 4
|
|
RRule.MINUTELY = 5
|
|
RRule.SECONDLY = 6
|
|
|
|
RRule.MO = new Weekday(0)
|
|
RRule.TU = new Weekday(1)
|
|
RRule.WE = new Weekday(2)
|
|
RRule.TH = new Weekday(3)
|
|
RRule.FR = new Weekday(4)
|
|
RRule.SA = new Weekday(5)
|
|
RRule.SU = new Weekday(6)
|
|
|
|
RRule.DEFAULT_OPTIONS = {
|
|
freq: null,
|
|
dtstart: null,
|
|
interval: 1,
|
|
wkst: RRule.MO,
|
|
count: null,
|
|
until: null,
|
|
bysetpos: null,
|
|
bymonth: null,
|
|
bymonthday: null,
|
|
bynmonthday: null,
|
|
byyearday: null,
|
|
byweekno: null,
|
|
byweekday: null,
|
|
bynweekday: null,
|
|
byhour: null,
|
|
byminute: null,
|
|
bysecond: null,
|
|
byeaster: null
|
|
}
|
|
|
|
RRule.parseText = function (text, language) {
|
|
return getnlp().parseText(text, language)
|
|
}
|
|
|
|
RRule.fromText = function (text, language) {
|
|
return getnlp().fromText(text, language)
|
|
}
|
|
|
|
RRule.optionsToString = function (options) {
|
|
var key, value, strValues
|
|
var pairs = []
|
|
var keys = Object.keys(options)
|
|
var defaultKeys = Object.keys(RRule.DEFAULT_OPTIONS)
|
|
|
|
for (var i = 0; i < keys.length; i++) {
|
|
if (!contains(defaultKeys, keys[i])) continue
|
|
|
|
key = keys[i].toUpperCase()
|
|
value = options[keys[i]]
|
|
strValues = []
|
|
|
|
if (value === null || value instanceof Array && !value.length) continue
|
|
|
|
switch (key) {
|
|
case 'FREQ':
|
|
value = RRule.FREQUENCIES[options.freq]
|
|
break
|
|
case 'WKST':
|
|
if (!(value instanceof Weekday)) {
|
|
value = new Weekday(value)
|
|
}
|
|
break
|
|
case 'BYWEEKDAY':
|
|
/*
|
|
NOTE: BYWEEKDAY is a special case.
|
|
RRule() deconstructs the rule.options.byweekday array
|
|
into an array of Weekday arguments.
|
|
On the other hand, rule.origOptions is an array of Weekdays.
|
|
We need to handle both cases here.
|
|
It might be worth change RRule to keep the Weekdays.
|
|
|
|
Also, BYWEEKDAY (used by RRule) vs. BYDAY (RFC)
|
|
|
|
*/
|
|
key = 'BYDAY'
|
|
if (!(value instanceof Array)) value = [value]
|
|
|
|
for (var wday, j = 0; j < value.length; j++) {
|
|
wday = value[j]
|
|
if (wday instanceof Weekday) {
|
|
// good
|
|
} else if (wday instanceof Array) {
|
|
wday = new Weekday(wday[0], wday[1])
|
|
} else {
|
|
wday = new Weekday(wday)
|
|
}
|
|
strValues[j] = wday.toString()
|
|
}
|
|
value = strValues
|
|
break
|
|
case 'DTSTART':
|
|
case 'UNTIL':
|
|
value = dateutil.timeToUntilString(value)
|
|
break
|
|
default:
|
|
if (value instanceof Array) {
|
|
for (j = 0; j < value.length; j++) strValues[j] = String(value[j])
|
|
value = strValues
|
|
} else {
|
|
value = String(value)
|
|
}
|
|
|
|
}
|
|
pairs.push([key, value])
|
|
}
|
|
|
|
var strings = []
|
|
for (i = 0; i < pairs.length; i++) {
|
|
var attr = pairs[i]
|
|
strings.push(attr[0] + '=' + attr[1].toString())
|
|
}
|
|
return strings.join(';')
|
|
}
|
|
|
|
RRule.prototype = {
|
|
constructor: RRule,
|
|
|
|
/**
|
|
* @param {Function} iterator - optional function that will be called
|
|
* on each date that is added. It can return false
|
|
* to stop the iteration.
|
|
* @return Array containing all recurrences.
|
|
*/
|
|
all: function (iterator) {
|
|
if (iterator) {
|
|
return this._iter(new CallbackIterResult('all', {}, iterator))
|
|
} else {
|
|
var result = this._cacheGet('all')
|
|
if (result === false) {
|
|
result = this._iter(new IterResult('all', {}))
|
|
this._cacheAdd('all', result)
|
|
}
|
|
return result
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns all the occurrences of the rrule between after and before.
|
|
* The inc keyword defines what happens if after and/or before are
|
|
* themselves occurrences. With inc == True, they will be included in the
|
|
* list, if they are found in the recurrence set.
|
|
* @return Array
|
|
*/
|
|
between: function (after, before, inc, iterator) {
|
|
var args = {
|
|
before: before,
|
|
after: after,
|
|
inc: inc
|
|
}
|
|
|
|
if (iterator) {
|
|
return this._iter(new CallbackIterResult('between', args, iterator))
|
|
}
|
|
var result = this._cacheGet('between', args)
|
|
if (result === false) {
|
|
result = this._iter(new IterResult('between', args))
|
|
this._cacheAdd('between', result, args)
|
|
}
|
|
return result
|
|
},
|
|
|
|
/**
|
|
* Returns the last recurrence before the given datetime instance.
|
|
* The inc keyword defines what happens if dt is an occurrence.
|
|
* With inc == True, if dt itself is an occurrence, it will be returned.
|
|
* @return Date or null
|
|
*/
|
|
before: function (dt, inc) {
|
|
var args = {dt: dt, inc: inc}
|
|
var result = this._cacheGet('before', args)
|
|
if (result === false) {
|
|
result = this._iter(new IterResult('before', args))
|
|
this._cacheAdd('before', result, args)
|
|
}
|
|
return result
|
|
},
|
|
|
|
/**
|
|
* Returns the first recurrence after the given datetime instance.
|
|
* The inc keyword defines what happens if dt is an occurrence.
|
|
* With inc == True, if dt itself is an occurrence, it will be returned.
|
|
* @return Date or null
|
|
*/
|
|
after: function (dt, inc) {
|
|
var args = {dt: dt, inc: inc}
|
|
var result = this._cacheGet('after', args)
|
|
if (result === false) {
|
|
result = this._iter(new IterResult('after', args))
|
|
this._cacheAdd('after', result, args)
|
|
}
|
|
return result
|
|
},
|
|
|
|
/**
|
|
* Returns the number of recurrences in this set. It will have go trough
|
|
* the whole recurrence, if this hasn't been done before.
|
|
*/
|
|
count: function () {
|
|
return this.all().length
|
|
},
|
|
|
|
/**
|
|
* Converts the rrule into its string representation
|
|
* @see <http://www.ietf.org/rfc/rfc2445.txt>
|
|
* @return String
|
|
*/
|
|
toString: function () {
|
|
return RRule.optionsToString(this.origOptions)
|
|
},
|
|
|
|
/**
|
|
* Will convert all rules described in nlp:ToText
|
|
* to text.
|
|
*/
|
|
toText: function (gettext, language) {
|
|
return getnlp().toText(this, gettext, language)
|
|
},
|
|
|
|
isFullyConvertibleToText: function () {
|
|
return getnlp().isFullyConvertible(this)
|
|
},
|
|
|
|
/**
|
|
* @param {String} what - all/before/after/between
|
|
* @param {Array,Date} value - an array of dates, one date, or null
|
|
* @param {Object?} args - _iter arguments
|
|
*/
|
|
_cacheAdd: function (what, value, args) {
|
|
if (!this._cache) return
|
|
|
|
if (value) {
|
|
value = (value instanceof Date)
|
|
? dateutil.clone(value) : dateutil.cloneDates(value)
|
|
}
|
|
|
|
if (what === 'all') {
|
|
this._cache.all = value
|
|
} else {
|
|
args._value = value
|
|
this._cache[what].push(args)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @return false - not in the cache
|
|
* null - cached, but zero occurrences (before/after)
|
|
* Date - cached (before/after)
|
|
* [] - cached, but zero occurrences (all/between)
|
|
* [Date1, DateN] - cached (all/between)
|
|
*/
|
|
_cacheGet: function (what, args) {
|
|
if (!this._cache) return false
|
|
|
|
var cached = false
|
|
var argsKeys = args ? Object.keys(args) : []
|
|
var findCacheDiff = function (item) {
|
|
for (var key, i = 0; i < argsKeys.length; i++) {
|
|
key = argsKeys[i]
|
|
if (String(args[key]) !== String(item[key])) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
if (what === 'all') {
|
|
cached = this._cache.all
|
|
} else {
|
|
// Let's see whether we've already called the
|
|
// 'what' method with the same 'args'
|
|
for (var item, i = 0; i < this._cache[what].length; i++) {
|
|
item = this._cache[what][i]
|
|
if (argsKeys.length && findCacheDiff(item)) continue
|
|
cached = item._value
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!cached && this._cache.all) {
|
|
// Not in the cache, but we already know all the occurrences,
|
|
// so we can find the correct dates from the cached ones.
|
|
var iterResult = new IterResult(what, args)
|
|
for (i = 0; i < this._cache.all.length; i++) {
|
|
if (!iterResult.accept(this._cache.all[i])) break
|
|
}
|
|
cached = iterResult.getValue()
|
|
this._cacheAdd(what, cached, args)
|
|
}
|
|
|
|
return cached instanceof Array
|
|
? dateutil.cloneDates(cached)
|
|
: (cached instanceof Date ? dateutil.clone(cached) : cached)
|
|
},
|
|
|
|
/**
|
|
* @return a RRule instance with the same freq and options
|
|
* as this one (cache is not cloned)
|
|
*/
|
|
clone: function () {
|
|
return new RRule(this.origOptions)
|
|
},
|
|
|
|
_iter: function (iterResult) {
|
|
/* Since JavaScript doesn't have the python's yield operator (<1.7),
|
|
we use the IterResult object that tells us when to stop iterating.
|
|
|
|
*/
|
|
|
|
var dtstart = this.options.dtstart
|
|
var dtstartMillisecondModulo = this.options.dtstart % 1000
|
|
|
|
var year = dtstart.getFullYear()
|
|
var month = dtstart.getMonth() + 1
|
|
var day = dtstart.getDate()
|
|
var hour = dtstart.getHours()
|
|
var minute = dtstart.getMinutes()
|
|
var second = dtstart.getSeconds()
|
|
var weekday = dateutil.getWeekday(dtstart)
|
|
|
|
// Some local variables to speed things up a bit
|
|
var freq = this.options.freq
|
|
var interval = this.options.interval
|
|
var wkst = this.options.wkst
|
|
var until = this.options.until
|
|
var bymonth = this.options.bymonth
|
|
var byweekno = this.options.byweekno
|
|
var byyearday = this.options.byyearday
|
|
var byweekday = this.options.byweekday
|
|
var byeaster = this.options.byeaster
|
|
var bymonthday = this.options.bymonthday
|
|
var bynmonthday = this.options.bynmonthday
|
|
var bysetpos = this.options.bysetpos
|
|
var byhour = this.options.byhour
|
|
var byminute = this.options.byminute
|
|
var bysecond = this.options.bysecond
|
|
|
|
var ii = new Iterinfo(this)
|
|
ii.rebuild(year, month)
|
|
|
|
var getdayset = {}
|
|
getdayset[RRule.YEARLY] = ii.ydayset
|
|
getdayset[RRule.MONTHLY] = ii.mdayset
|
|
getdayset[RRule.WEEKLY] = ii.wdayset
|
|
getdayset[RRule.DAILY] = ii.ddayset
|
|
getdayset[RRule.HOURLY] = ii.ddayset
|
|
getdayset[RRule.MINUTELY] = ii.ddayset
|
|
getdayset[RRule.SECONDLY] = ii.ddayset
|
|
|
|
getdayset = getdayset[freq]
|
|
|
|
var timeset
|
|
if (freq < RRule.HOURLY) {
|
|
timeset = this.timeset
|
|
} else {
|
|
var gettimeset = {}
|
|
gettimeset[RRule.HOURLY] = ii.htimeset
|
|
gettimeset[RRule.MINUTELY] = ii.mtimeset
|
|
gettimeset[RRule.SECONDLY] = ii.stimeset
|
|
gettimeset = gettimeset[freq]
|
|
if ((freq >= RRule.HOURLY && plb(byhour) && !contains(byhour, hour)) ||
|
|
(freq >= RRule.MINUTELY && plb(byminute) && !contains(byminute, minute)) ||
|
|
(freq >= RRule.SECONDLY && plb(bysecond) && !contains(bysecond, second))) {
|
|
timeset = []
|
|
} else {
|
|
timeset = gettimeset.call(ii, hour, minute, second, dtstartMillisecondModulo)
|
|
}
|
|
}
|
|
|
|
var total = 0
|
|
var count = this.options.count
|
|
var i, j, k, dm, div, mod, tmp, pos, dayset, start, end, fixday, filtered
|
|
|
|
while (true) {
|
|
// Get dayset with the right frequency
|
|
tmp = getdayset.call(ii, year, month, day)
|
|
dayset = tmp[0]
|
|
start = tmp[1]
|
|
end = tmp[2]
|
|
|
|
// Do the "hard" work ;-)
|
|
filtered = false
|
|
for (j = start; j < end; j++) {
|
|
i = dayset[j]
|
|
|
|
filtered = (plb(bymonth) && !contains(bymonth, ii.mmask[i])) ||
|
|
(plb(byweekno) && !ii.wnomask[i]) ||
|
|
(plb(byweekday) && !contains(byweekday, ii.wdaymask[i])) ||
|
|
(plb(ii.nwdaymask) && !ii.nwdaymask[i]) ||
|
|
(byeaster !== null && !contains(ii.eastermask, i)) ||
|
|
((plb(bymonthday) || plb(bynmonthday)) &&
|
|
!contains(bymonthday, ii.mdaymask[i]) &&
|
|
!contains(bynmonthday, ii.nmdaymask[i])) ||
|
|
(plb(byyearday) &&
|
|
((i < ii.yearlen &&
|
|
!contains(byyearday, i + 1) &&
|
|
!contains(byyearday, -ii.yearlen + i)) ||
|
|
(i >= ii.yearlen &&
|
|
!contains(byyearday, i + 1 - ii.yearlen) &&
|
|
!contains(byyearday, -ii.nextyearlen + i - ii.yearlen))))
|
|
|
|
if (filtered) dayset[i] = null
|
|
}
|
|
|
|
// Output results
|
|
if (plb(bysetpos) && plb(timeset)) {
|
|
var daypos, timepos
|
|
var poslist = []
|
|
|
|
for (i, j = 0; j < bysetpos.length; j++) {
|
|
pos = bysetpos[j]
|
|
|
|
if (pos < 0) {
|
|
daypos = Math.floor(pos / timeset.length)
|
|
timepos = pymod(pos, timeset.length)
|
|
} else {
|
|
daypos = Math.floor((pos - 1) / timeset.length)
|
|
timepos = pymod((pos - 1), timeset.length)
|
|
}
|
|
|
|
try {
|
|
tmp = []
|
|
for (k = start; k < end; k++) {
|
|
var val = dayset[k]
|
|
if (val === null) continue
|
|
tmp.push(val)
|
|
}
|
|
if (daypos < 0) {
|
|
// we're trying to emulate python's aList[-n]
|
|
i = tmp.slice(daypos)[0]
|
|
} else {
|
|
i = tmp[daypos]
|
|
}
|
|
|
|
var time = timeset[timepos]
|
|
var date = dateutil.fromOrdinal(ii.yearordinal + i)
|
|
var res = dateutil.combine(date, time)
|
|
// XXX: can this ever be in the array?
|
|
// - compare the actual date instead?
|
|
if (!contains(poslist, res)) poslist.push(res)
|
|
} catch (e) {}
|
|
}
|
|
|
|
dateutil.sort(poslist)
|
|
for (j = 0; j < poslist.length; j++) {
|
|
res = poslist[j]
|
|
if (until && res > until) {
|
|
this._len = total
|
|
return iterResult.getValue()
|
|
} else if (res >= dtstart) {
|
|
++total
|
|
if (!iterResult.accept(res)) return iterResult.getValue()
|
|
if (count) {
|
|
--count
|
|
if (!count) {
|
|
this._len = total
|
|
return iterResult.getValue()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for (j = start; j < end; j++) {
|
|
i = dayset[j]
|
|
if (i !== null) {
|
|
date = dateutil.fromOrdinal(ii.yearordinal + i)
|
|
for (k = 0; k < timeset.length; k++) {
|
|
time = timeset[k]
|
|
res = dateutil.combine(date, time)
|
|
if (until && res > until) {
|
|
this._len = total
|
|
return iterResult.getValue()
|
|
} else if (res >= dtstart) {
|
|
++total
|
|
if (!iterResult.accept(res)) return iterResult.getValue()
|
|
if (count) {
|
|
--count
|
|
if (!count) {
|
|
this._len = total
|
|
return iterResult.getValue()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle frequency and interval
|
|
fixday = false
|
|
if (freq === RRule.YEARLY) {
|
|
year += interval
|
|
if (year > dateutil.MAXYEAR) {
|
|
this._len = total
|
|
return iterResult.getValue()
|
|
}
|
|
ii.rebuild(year, month)
|
|
} else if (freq === RRule.MONTHLY) {
|
|
month += interval
|
|
if (month > 12) {
|
|
div = Math.floor(month / 12)
|
|
mod = pymod(month, 12)
|
|
month = mod
|
|
year += div
|
|
if (month === 0) {
|
|
month = 12
|
|
--year
|
|
}
|
|
if (year > dateutil.MAXYEAR) {
|
|
this._len = total
|
|
return iterResult.getValue()
|
|
}
|
|
}
|
|
ii.rebuild(year, month)
|
|
} else if (freq === RRule.WEEKLY) {
|
|
if (wkst > weekday) {
|
|
day += -(weekday + 1 + (6 - wkst)) + interval * 7
|
|
} else {
|
|
day += -(weekday - wkst) + interval * 7
|
|
}
|
|
weekday = wkst
|
|
fixday = true
|
|
} else if (freq === RRule.DAILY) {
|
|
day += interval
|
|
fixday = true
|
|
} else if (freq === RRule.HOURLY) {
|
|
if (filtered) {
|
|
// Jump to one iteration before next day
|
|
hour += Math.floor((23 - hour) / interval) * interval
|
|
}
|
|
while (true) {
|
|
hour += interval
|
|
dm = divmod(hour, 24)
|
|
div = dm.div
|
|
mod = dm.mod
|
|
if (div) {
|
|
hour = mod
|
|
day += div
|
|
fixday = true
|
|
}
|
|
if (!plb(byhour) || contains(byhour, hour)) break
|
|
}
|
|
timeset = gettimeset.call(ii, hour, minute, second)
|
|
} else if (freq === RRule.MINUTELY) {
|
|
if (filtered) {
|
|
// Jump to one iteration before next day
|
|
minute += Math.floor(
|
|
(1439 - (hour * 60 + minute)) / interval) * interval
|
|
}
|
|
|
|
while (true) {
|
|
minute += interval
|
|
dm = divmod(minute, 60)
|
|
div = dm.div
|
|
mod = dm.mod
|
|
if (div) {
|
|
minute = mod
|
|
hour += div
|
|
dm = divmod(hour, 24)
|
|
div = dm.div
|
|
mod = dm.mod
|
|
if (div) {
|
|
hour = mod
|
|
day += div
|
|
fixday = true
|
|
filtered = false
|
|
}
|
|
}
|
|
if ((!plb(byhour) || contains(byhour, hour)) &&
|
|
(!plb(byminute) || contains(byminute, minute))) {
|
|
break
|
|
}
|
|
}
|
|
timeset = gettimeset.call(ii, hour, minute, second)
|
|
} else if (freq === RRule.SECONDLY) {
|
|
if (filtered) {
|
|
// Jump to one iteration before next day
|
|
second += Math.floor(
|
|
(86399 - (hour * 3600 + minute * 60 + second)) / interval) * interval
|
|
}
|
|
while (true) {
|
|
second += interval
|
|
dm = divmod(second, 60)
|
|
div = dm.div
|
|
mod = dm.mod
|
|
if (div) {
|
|
second = mod
|
|
minute += div
|
|
dm = divmod(minute, 60)
|
|
div = dm.div
|
|
mod = dm.mod
|
|
if (div) {
|
|
minute = mod
|
|
hour += div
|
|
dm = divmod(hour, 24)
|
|
div = dm.div
|
|
mod = dm.mod
|
|
if (div) {
|
|
hour = mod
|
|
day += div
|
|
fixday = true
|
|
}
|
|
}
|
|
}
|
|
if ((!plb(byhour) || contains(byhour, hour)) &&
|
|
(!plb(byminute) || contains(byminute, minute)) &&
|
|
(!plb(bysecond) || contains(bysecond, second))) {
|
|
break
|
|
}
|
|
}
|
|
timeset = gettimeset.call(ii, hour, minute, second)
|
|
}
|
|
|
|
if (fixday && day > 28) {
|
|
var daysinmonth = dateutil.monthRange(year, month - 1)[1]
|
|
if (day > daysinmonth) {
|
|
while (day > daysinmonth) {
|
|
day -= daysinmonth
|
|
++month
|
|
if (month === 13) {
|
|
month = 1
|
|
++year
|
|
if (year > dateutil.MAXYEAR) {
|
|
this._len = total
|
|
return iterResult.getValue()
|
|
}
|
|
}
|
|
daysinmonth = dateutil.monthRange(year, month - 1)[1]
|
|
}
|
|
ii.rebuild(year, month)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
RRule.parseString = function (rfcString) {
|
|
rfcString = rfcString.replace(/^\s+|\s+$/, '')
|
|
if (!rfcString.length) return null
|
|
|
|
var i, j, key, value, attr
|
|
var attrs = rfcString.split(';')
|
|
var options = {}
|
|
|
|
for (i = 0; i < attrs.length; i++) {
|
|
attr = attrs[i].split('=')
|
|
key = attr[0]
|
|
value = attr[1]
|
|
switch (key) {
|
|
case 'FREQ':
|
|
options.freq = RRule[value]
|
|
break
|
|
case 'WKST':
|
|
options.wkst = RRule[value]
|
|
break
|
|
case 'COUNT':
|
|
case 'INTERVAL':
|
|
case 'BYSETPOS':
|
|
case 'BYMONTH':
|
|
case 'BYMONTHDAY':
|
|
case 'BYYEARDAY':
|
|
case 'BYWEEKNO':
|
|
case 'BYHOUR':
|
|
case 'BYMINUTE':
|
|
case 'BYSECOND':
|
|
if (value.indexOf(',') !== -1) {
|
|
value = value.split(',')
|
|
for (j = 0; j < value.length; j++) {
|
|
if (/^[+-]?\d+$/.test(value[j])) value[j] = Number(value[j])
|
|
}
|
|
} else if (/^[+-]?\d+$/.test(value)) {
|
|
value = Number(value)
|
|
}
|
|
key = key.toLowerCase()
|
|
options[key] = value
|
|
break
|
|
case 'BYDAY': // => byweekday
|
|
var n, wday, day
|
|
var days = value.split(',')
|
|
|
|
options.byweekday = []
|
|
for (j = 0; j < days.length; j++) {
|
|
day = days[j]
|
|
if (day.length === 2) { // MO, TU, ...
|
|
wday = RRule[day] // wday instanceof Weekday
|
|
options.byweekday.push(wday)
|
|
} else { // -1MO, +3FR, 1SO, ...
|
|
day = day.match(/^([+-]?\d)([A-Z]{2})$/)
|
|
n = Number(day[1])
|
|
wday = day[2]
|
|
wday = RRule[wday].weekday
|
|
options.byweekday.push(new Weekday(wday, n))
|
|
}
|
|
}
|
|
break
|
|
case 'DTSTART':
|
|
options.dtstart = dateutil.untilStringToDate(value)
|
|
break
|
|
case 'UNTIL':
|
|
options.until = dateutil.untilStringToDate(value)
|
|
break
|
|
case 'BYEASTER':
|
|
options.byeaster = Number(value)
|
|
break
|
|
default:
|
|
throw new Error("Unknown RRULE property '" + key + "'")
|
|
}
|
|
}
|
|
return options
|
|
}
|
|
|
|
RRule.fromString = function (string) {
|
|
return new RRule(RRule.parseString(string))
|
|
}
|
|
|
|
// =============================================================================
|
|
// Iterinfo
|
|
// =============================================================================
|
|
|
|
var Iterinfo = function (rrule) {
|
|
this.rrule = rrule
|
|
this.lastyear = null
|
|
this.lastmonth = null
|
|
this.yearlen = null
|
|
this.nextyearlen = null
|
|
this.yearordinal = null
|
|
this.yearweekday = null
|
|
this.mmask = null
|
|
this.mrange = null
|
|
this.mdaymask = null
|
|
this.nmdaymask = null
|
|
this.wdaymask = null
|
|
this.wnomask = null
|
|
this.nwdaymask = null
|
|
this.eastermask = null
|
|
}
|
|
|
|
Iterinfo.prototype.easter = function (y, offset) {
|
|
offset = offset || 0
|
|
|
|
var a = y % 19
|
|
var b = Math.floor(y / 100)
|
|
var c = y % 100
|
|
var d = Math.floor(b / 4)
|
|
var e = b % 4
|
|
var f = Math.floor((b + 8) / 25)
|
|
var g = Math.floor((b - f + 1) / 3)
|
|
var h = Math.floor(19 * a + b - d - g + 15) % 30
|
|
var i = Math.floor(c / 4)
|
|
var k = c % 4
|
|
var l = Math.floor(32 + 2 * e + 2 * i - h - k) % 7
|
|
var m = Math.floor((a + 11 * h + 22 * l) / 451)
|
|
var month = Math.floor((h + l - 7 * m + 114) / 31)
|
|
var day = (h + l - 7 * m + 114) % 31 + 1
|
|
var date = Date.UTC(y, month - 1, day + offset)
|
|
var yearStart = Date.UTC(y, 0, 1)
|
|
|
|
return [Math.ceil((date - yearStart) / (1000 * 60 * 60 * 24))]
|
|
}
|
|
|
|
Iterinfo.prototype.rebuild = function (year, month) {
|
|
var rr = this.rrule
|
|
|
|
if (year !== this.lastyear) {
|
|
this.yearlen = dateutil.isLeapYear(year) ? 366 : 365
|
|
this.nextyearlen = dateutil.isLeapYear(year + 1) ? 366 : 365
|
|
var firstyday = new Date(year, 0, 1)
|
|
|
|
this.yearordinal = dateutil.toOrdinal(firstyday)
|
|
this.yearweekday = dateutil.getWeekday(firstyday)
|
|
|
|
var wday = dateutil.getWeekday(new Date(year, 0, 1))
|
|
|
|
if (this.yearlen === 365) {
|
|
this.mmask = [].concat(M365MASK)
|
|
this.mdaymask = [].concat(MDAY365MASK)
|
|
this.nmdaymask = [].concat(NMDAY365MASK)
|
|
this.wdaymask = WDAYMASK.slice(wday)
|
|
this.mrange = [].concat(M365RANGE)
|
|
} else {
|
|
this.mmask = [].concat(M366MASK)
|
|
this.mdaymask = [].concat(MDAY366MASK)
|
|
this.nmdaymask = [].concat(NMDAY366MASK)
|
|
this.wdaymask = WDAYMASK.slice(wday)
|
|
this.mrange = [].concat(M366RANGE)
|
|
}
|
|
|
|
if (!plb(rr.options.byweekno)) {
|
|
this.wnomask = null
|
|
} else {
|
|
this.wnomask = repeat(0, this.yearlen + 7)
|
|
var no1wkst, firstwkst, wyearlen
|
|
no1wkst = firstwkst = pymod(7 - this.yearweekday + rr.options.wkst, 7)
|
|
if (no1wkst >= 4) {
|
|
no1wkst = 0
|
|
// Number of days in the year, plus the days we got
|
|
// from last year.
|
|
wyearlen = this.yearlen + pymod(this.yearweekday - rr.options.wkst, 7)
|
|
} else {
|
|
// Number of days in the year, minus the days we
|
|
// left in last year.
|
|
wyearlen = this.yearlen - no1wkst
|
|
}
|
|
var div = Math.floor(wyearlen / 7)
|
|
var mod = pymod(wyearlen, 7)
|
|
var numweeks = Math.floor(div + (mod / 4))
|
|
for (var n, i, j = 0; j < rr.options.byweekno.length; j++) {
|
|
n = rr.options.byweekno[j]
|
|
if (n < 0) {
|
|
n += numweeks + 1
|
|
} if (!(n > 0 && n <= numweeks)) {
|
|
continue
|
|
} if (n > 1) {
|
|
i = no1wkst + (n - 1) * 7
|
|
if (no1wkst !== firstwkst) {
|
|
i -= 7 - firstwkst
|
|
}
|
|
} else {
|
|
i = no1wkst
|
|
}
|
|
for (var k = 0; k < 7; k++) {
|
|
this.wnomask[i] = 1
|
|
i++
|
|
if (this.wdaymask[i] === rr.options.wkst) break
|
|
}
|
|
}
|
|
|
|
if (contains(rr.options.byweekno, 1)) {
|
|
// Check week number 1 of next year as well
|
|
// orig-TODO : Check -numweeks for next year.
|
|
i = no1wkst + numweeks * 7
|
|
if (no1wkst !== firstwkst) i -= 7 - firstwkst
|
|
if (i < this.yearlen) {
|
|
// If week starts in next year, we
|
|
// don't care about it.
|
|
for (j = 0; j < 7; j++) {
|
|
this.wnomask[i] = 1
|
|
i += 1
|
|
if (this.wdaymask[i] === rr.options.wkst) break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (no1wkst) {
|
|
// Check last week number of last year as
|
|
// well. If no1wkst is 0, either the year
|
|
// started on week start, or week number 1
|
|
// got days from last year, so there are no
|
|
// days from last year's last week number in
|
|
// this year.
|
|
var lnumweeks
|
|
if (!contains(rr.options.byweekno, -1)) {
|
|
var lyearweekday = dateutil.getWeekday(new Date(year - 1, 0, 1))
|
|
var lno1wkst = pymod(7 - lyearweekday + rr.options.wkst, 7)
|
|
var lyearlen = dateutil.isLeapYear(year - 1) ? 366 : 365
|
|
if (lno1wkst >= 4) {
|
|
lno1wkst = 0
|
|
lnumweeks = Math.floor(52 +
|
|
pymod(lyearlen + pymod(lyearweekday - rr.options.wkst, 7), 7) / 4)
|
|
} else {
|
|
lnumweeks = Math.floor(52 + pymod(this.yearlen - no1wkst, 7) / 4)
|
|
}
|
|
} else {
|
|
lnumweeks = -1
|
|
}
|
|
if (contains(rr.options.byweekno, lnumweeks)) {
|
|
for (i = 0; i < no1wkst; i++) this.wnomask[i] = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (plb(rr.options.bynweekday) && (month !== this.lastmonth || year !== this.lastyear)) {
|
|
var ranges = []
|
|
if (rr.options.freq === RRule.YEARLY) {
|
|
if (plb(rr.options.bymonth)) {
|
|
for (j = 0; j < rr.options.bymonth.length; j++) {
|
|
month = rr.options.bymonth[j]
|
|
ranges.push(this.mrange.slice(month - 1, month + 1))
|
|
}
|
|
} else {
|
|
ranges = [[0, this.yearlen]]
|
|
}
|
|
} else if (rr.options.freq === RRule.MONTHLY) {
|
|
ranges = [this.mrange.slice(month - 1, month + 1)]
|
|
}
|
|
if (plb(ranges)) {
|
|
// Weekly frequency won't get here, so we may not
|
|
// care about cross-year weekly periods.
|
|
this.nwdaymask = repeat(0, this.yearlen)
|
|
|
|
for (j = 0; j < ranges.length; j++) {
|
|
var rang = ranges[j]
|
|
var first = rang[0]
|
|
var last = rang[1]
|
|
last -= 1
|
|
for (k = 0; k < rr.options.bynweekday.length; k++) {
|
|
wday = rr.options.bynweekday[k][0]
|
|
n = rr.options.bynweekday[k][1]
|
|
if (n < 0) {
|
|
i = last + (n + 1) * 7
|
|
i -= pymod(this.wdaymask[i] - wday, 7)
|
|
} else {
|
|
i = first + (n - 1) * 7
|
|
i += pymod(7 - this.wdaymask[i] + wday, 7)
|
|
}
|
|
if (first <= i && i <= last) this.nwdaymask[i] = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
this.lastyear = year
|
|
this.lastmonth = month
|
|
}
|
|
|
|
if (rr.options.byeaster !== null) {
|
|
this.eastermask = this.easter(year, rr.options.byeaster)
|
|
}
|
|
}
|
|
|
|
Iterinfo.prototype.ydayset = function (year, month, day) {
|
|
return [range(this.yearlen), 0, this.yearlen]
|
|
}
|
|
|
|
Iterinfo.prototype.mdayset = function (year, month, day) {
|
|
var set = repeat(null, this.yearlen)
|
|
var start = this.mrange[month - 1]
|
|
var end = this.mrange[month]
|
|
for (var i = start; i < end; i++) set[i] = i
|
|
return [set, start, end]
|
|
}
|
|
|
|
Iterinfo.prototype.wdayset = function (year, month, day) {
|
|
// We need to handle cross-year weeks here.
|
|
var set = repeat(null, this.yearlen + 7)
|
|
var i = dateutil.toOrdinal(new Date(year, month - 1, day)) - this.yearordinal
|
|
var start = i
|
|
for (var j = 0; j < 7; j++) {
|
|
set[i] = i
|
|
++i
|
|
if (this.wdaymask[i] === this.rrule.options.wkst) break
|
|
}
|
|
return [set, start, i]
|
|
}
|
|
|
|
Iterinfo.prototype.ddayset = function (year, month, day) {
|
|
var set = repeat(null, this.yearlen)
|
|
var i = dateutil.toOrdinal(new Date(year, month - 1, day)) - this.yearordinal
|
|
set[i] = i
|
|
return [set, i, i + 1]
|
|
}
|
|
|
|
Iterinfo.prototype.htimeset = function (hour, minute, second, millisecond) {
|
|
var set = []
|
|
var rr = this.rrule
|
|
for (var i = 0; i < rr.options.byminute.length; i++) {
|
|
minute = rr.options.byminute[i]
|
|
for (var j = 0; j < rr.options.bysecond.length; j++) {
|
|
second = rr.options.bysecond[j]
|
|
set.push(new dateutil.Time(hour, minute, second, millisecond))
|
|
}
|
|
}
|
|
dateutil.sort(set)
|
|
return set
|
|
}
|
|
|
|
Iterinfo.prototype.mtimeset = function (hour, minute, second, millisecond) {
|
|
var set = []
|
|
var rr = this.rrule
|
|
for (var j = 0; j < rr.options.bysecond.length; j++) {
|
|
second = rr.options.bysecond[j]
|
|
set.push(new dateutil.Time(hour, minute, second, millisecond))
|
|
}
|
|
dateutil.sort(set)
|
|
return set
|
|
}
|
|
|
|
Iterinfo.prototype.stimeset = function (hour, minute, second, millisecond) {
|
|
return [new dateutil.Time(hour, minute, second, millisecond)]
|
|
}
|
|
|
|
// =============================================================================
|
|
// Results
|
|
// =============================================================================
|
|
|
|
/**
|
|
* This class helps us to emulate python's generators, sorta.
|
|
*/
|
|
var IterResult = function (method, args) {
|
|
this.init(method, args)
|
|
}
|
|
|
|
IterResult.prototype = {
|
|
constructor: IterResult,
|
|
init: function (method, args) {
|
|
this.method = method
|
|
this.args = args
|
|
this.minDate = null
|
|
this.maxDate = null
|
|
this._result = []
|
|
|
|
if (method === 'between') {
|
|
this.maxDate = args.inc
|
|
? args.before : new Date(args.before.getTime() - 1)
|
|
this.minDate = args.inc
|
|
? args.after : new Date(args.after.getTime() + 1)
|
|
} else if (method === 'before') {
|
|
this.maxDate = args.inc ? args.dt : new Date(args.dt.getTime() - 1)
|
|
} else if (method === 'after') {
|
|
this.minDate = args.inc ? args.dt : new Date(args.dt.getTime() + 1)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Possibly adds a date into the result.
|
|
*
|
|
* @param {Date} date - the date isn't necessarly added to the result
|
|
* list (if it is too late/too early)
|
|
* @return {Boolean} true if it makes sense to continue the iteration
|
|
* false if we're done.
|
|
*/
|
|
accept: function (date) {
|
|
var tooEarly = this.minDate && date < this.minDate
|
|
var tooLate = this.maxDate && date > this.maxDate
|
|
|
|
if (this.method === 'between') {
|
|
if (tooEarly) return true
|
|
if (tooLate) return false
|
|
} else if (this.method === 'before') {
|
|
if (tooLate) return false
|
|
} else if (this.method === 'after') {
|
|
if (tooEarly) return true
|
|
this.add(date)
|
|
return false
|
|
}
|
|
|
|
return this.add(date)
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @param {Date} date that is part of the result.
|
|
* @return {Boolean} whether we are interested in more values.
|
|
*/
|
|
add: function (date) {
|
|
this._result.push(date)
|
|
return true
|
|
},
|
|
|
|
/**
|
|
* 'before' and 'after' return only one date, whereas 'all'
|
|
* and 'between' an array.
|
|
* @return {Date,Array?}
|
|
*/
|
|
getValue: function () {
|
|
var res = this._result
|
|
switch (this.method) {
|
|
case 'all':
|
|
case 'between':
|
|
return res
|
|
case 'before':
|
|
case 'after':
|
|
return res.length ? res[res.length - 1] : null
|
|
}
|
|
},
|
|
|
|
clone: function () {
|
|
return new IterResult(this.method, this.args)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* IterResult subclass that calls a callback function on each add,
|
|
* and stops iterating when the callback returns false.
|
|
*/
|
|
var CallbackIterResult = function (method, args, iterator) {
|
|
var allowedMethods = ['all', 'between']
|
|
if (!contains(allowedMethods, method)) {
|
|
throw new Error('Invalid method "' + method +
|
|
'". Only all and between works with iterator.')
|
|
}
|
|
this.add = function (date) {
|
|
if (iterator(date, this._result.length)) {
|
|
this._result.push(date)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
this.init(method, args)
|
|
}
|
|
CallbackIterResult.prototype = IterResult.prototype
|
|
|
|
/**
|
|
*
|
|
* @param {Boolean?} noCache
|
|
* The same stratagy as RRule on cache, default to false
|
|
* @constructor
|
|
*/
|
|
|
|
var RRuleSet = function (noCache) {
|
|
// Let RRuleSet cacheable
|
|
this._cache = noCache ? null : {
|
|
all: false,
|
|
before: [],
|
|
after: [],
|
|
between: []
|
|
}
|
|
this._rrule = []
|
|
this._rdate = []
|
|
this._exrule = []
|
|
this._exdate = []
|
|
}
|
|
|
|
RRuleSet.prototype = {
|
|
constructor: RRuleSet,
|
|
|
|
/**
|
|
* @param {RRule}
|
|
*/
|
|
rrule: function (rrule) {
|
|
if (!(rrule instanceof RRule)) {
|
|
throw new TypeError(String(rrule) + ' is not RRule instance')
|
|
}
|
|
if (!contains(this._rrule.map(String), String(rrule))) {
|
|
this._rrule.push(rrule)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {Date}
|
|
*/
|
|
rdate: function (date) {
|
|
if (!(date instanceof Date)) {
|
|
throw new TypeError(String(date) + ' is not Date instance')
|
|
}
|
|
if (!contains(this._rdate.map(Number), Number(date))) {
|
|
this._rdate.push(date)
|
|
dateutil.sort(this._rdate)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {RRule}
|
|
*/
|
|
exrule: function (rrule) {
|
|
if (!(rrule instanceof RRule)) {
|
|
throw new TypeError(String(rrule) + ' is not RRule instance')
|
|
}
|
|
if (!contains(this._exrule.map(String), String(rrule))) {
|
|
this._exrule.push(rrule)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {Date}
|
|
*/
|
|
exdate: function (date) {
|
|
if (!(date instanceof Date)) {
|
|
throw new TypeError(String(date) + ' is not Date instance')
|
|
}
|
|
if (!contains(this._exdate.map(Number), Number(date))) {
|
|
this._exdate.push(date)
|
|
dateutil.sort(this._exdate)
|
|
}
|
|
},
|
|
|
|
valueOf: function () {
|
|
var result = []
|
|
if (this._rrule.length) {
|
|
this._rrule.forEach(function (rrule) {
|
|
result.push('RRULE:' + rrule)
|
|
})
|
|
}
|
|
if (this._rdate.length) {
|
|
result.push('RDATE:' + this._rdate.map(function (rdate) {
|
|
return dateutil.timeToUntilString(rdate)
|
|
}).join(','))
|
|
}
|
|
if (this._exrule.length) {
|
|
this._exrule.forEach(function (exrule) {
|
|
result.push('EXRULE:' + exrule)
|
|
})
|
|
}
|
|
if (this._exdate.length) {
|
|
result.push('EXDATE:' + this._exdate.map(function (exdate) {
|
|
return dateutil.timeToUntilString(exdate)
|
|
}).join(','))
|
|
}
|
|
return result
|
|
},
|
|
|
|
/**
|
|
* to generate recurrence field sush as:
|
|
* ["RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU;DTSTART=19970902T010000Z","RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH;DTSTART=19970902T010000Z"]
|
|
*/
|
|
toString: function () {
|
|
return JSON.stringify(this.valueOf())
|
|
},
|
|
|
|
_iter: function (iterResult) {
|
|
var _exdateHash = {}
|
|
var _exrule = this._exrule
|
|
var _accept = iterResult.accept
|
|
|
|
function evalExdate (after, before) {
|
|
_exrule.forEach(function (rrule) {
|
|
rrule.between(after, before, true).forEach(function (date) {
|
|
_exdateHash[Number(date)] = true
|
|
})
|
|
})
|
|
}
|
|
|
|
this._exdate.forEach(function (date) {
|
|
_exdateHash[Number(date)] = true
|
|
})
|
|
|
|
iterResult.accept = function (date) {
|
|
var dt = Number(date)
|
|
if (!_exdateHash[dt]) {
|
|
evalExdate(new Date(dt - 1), new Date(dt + 1))
|
|
if (!_exdateHash[dt]) {
|
|
_exdateHash[dt] = true
|
|
return _accept.call(this, date)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
if (iterResult.method === 'between') {
|
|
evalExdate(iterResult.args.after, iterResult.args.before)
|
|
iterResult.accept = function (date) {
|
|
var dt = Number(date)
|
|
if (!_exdateHash[dt]) {
|
|
_exdateHash[dt] = true
|
|
return _accept.call(this, date)
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < this._rdate.length; i++) {
|
|
if (!iterResult.accept(new Date(this._rdate[i]))) break
|
|
}
|
|
|
|
this._rrule.forEach(function (rrule) {
|
|
rrule._iter(iterResult)
|
|
})
|
|
|
|
var res = iterResult._result
|
|
dateutil.sort(res)
|
|
switch (iterResult.method) {
|
|
case 'all':
|
|
case 'between':
|
|
return res
|
|
case 'before':
|
|
return (res.length && res[res.length - 1]) || null
|
|
case 'after':
|
|
return (res.length && res[0]) || null
|
|
default:
|
|
return null
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create a new RRuleSet Object completely base on current instance
|
|
*/
|
|
clone: function () {
|
|
var rrs = new RRuleSet(!!this._cache)
|
|
var i
|
|
for (i = 0; i < this._rrule.length; i++) {
|
|
rrs.rrule(this._rrule[i].clone())
|
|
}
|
|
for (i = 0; i < this._rdate.length; i++) {
|
|
rrs.rdate(new Date(this._rdate[i]))
|
|
}
|
|
for (i = 0; i < this._exrule.length; i++) {
|
|
rrs.exrule(this._exrule[i].clone())
|
|
}
|
|
for (i = 0; i < this._exdate.length; i++) {
|
|
rrs.exdate(new Date(this._exdate[i]))
|
|
}
|
|
return rrs
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inherts method from RRule
|
|
* add Read interface and set RRuleSet cacheable
|
|
*/
|
|
var RRuleSetMethods = ['all', 'between', 'before', 'after', 'count', '_cacheAdd', '_cacheGet']
|
|
RRuleSetMethods.forEach(function (method) {
|
|
RRuleSet.prototype[method] = RRule.prototype[method]
|
|
})
|
|
|
|
/**
|
|
* RRuleStr
|
|
* To parse a set of rrule strings
|
|
*/
|
|
|
|
var RRuleStr = function () {}
|
|
|
|
RRuleStr.DEFAULT_OPTIONS = {
|
|
dtstart: null,
|
|
cache: false,
|
|
unfold: false,
|
|
forceset: false,
|
|
compatible: false,
|
|
ignoretz: false,
|
|
tzinfos: null
|
|
}
|
|
|
|
RRuleStr._freq_map = {
|
|
'YEARLY': RRule.YEARLY,
|
|
'MONTHLY': RRule.MONTHLY,
|
|
'WEEKLY': RRule.WEEKLY,
|
|
'DAILY': RRule.DAILY,
|
|
'HOURLY': RRule.HOURLY,
|
|
'MINUTELY': RRule.MINUTELY,
|
|
'SECONDLY': RRule.SECONDLY
|
|
}
|
|
|
|
RRuleStr._weekday_map = {
|
|
'MO': 0,
|
|
'TU': 1,
|
|
'WE': 2,
|
|
'TH': 3,
|
|
'FR': 4,
|
|
'SA': 5,
|
|
'SU': 6
|
|
}
|
|
|
|
RRuleStr.prototype = {
|
|
constructor: RRuleStr,
|
|
|
|
_handle_int: function (rrkwargs, name, value, options) {
|
|
rrkwargs[name.toLowerCase()] = parseInt(value, 10)
|
|
},
|
|
|
|
_handle_int_list: function (rrkwargs, name, value, options) {
|
|
rrkwargs[name.toLowerCase()] = value.split(',').map(function (x) {
|
|
return parseInt(x, 10)
|
|
})
|
|
},
|
|
|
|
_handle_FREQ: function (rrkwargs, name, value, options) {
|
|
rrkwargs['freq'] = RRuleStr._freq_map[value]
|
|
},
|
|
|
|
_handle_UNTIL: function (rrkwargs, name, value, options) {
|
|
try {
|
|
rrkwargs['until'] = dateutil.untilStringToDate(value)
|
|
} catch (error) {
|
|
throw new Error('invalid until date')
|
|
}
|
|
},
|
|
|
|
_handle_WKST: function (rrkwargs, name, value, options) {
|
|
rrkwargs['wkst'] = RRuleStr._weekday_map[value]
|
|
},
|
|
|
|
_handle_BYWEEKDAY: function (rrkwargs, name, value, options) {
|
|
// Two ways to specify this: +1MO or MO(+1)
|
|
var splt, i, j, n, w, wday
|
|
var l = []
|
|
var wdays = value.split(',')
|
|
|
|
for (i = 0; i < wdays.length; i++) {
|
|
wday = wdays[i]
|
|
if (wday.indexOf('(') > -1) {
|
|
// If it's of the form TH(+1), etc.
|
|
splt = wday.split('(')
|
|
w = splt[0]
|
|
n = parseInt(splt.slice(1, -1), 10)
|
|
} else {
|
|
// # If it's of the form +1MO
|
|
for (j = 0; j < wday.length; j++) {
|
|
if ('+-0123456789'.indexOf(wday[j]) === -1) break
|
|
}
|
|
n = wday.slice(0, j) || null
|
|
w = wday.slice(j)
|
|
|
|
if (n) n = parseInt(n, 10)
|
|
}
|
|
|
|
var weekday = new Weekday(RRuleStr._weekday_map[w], n)
|
|
l.push(weekday)
|
|
}
|
|
rrkwargs['byweekday'] = l
|
|
},
|
|
|
|
_parseRfcRRule: function (line, options) {
|
|
options = options || {}
|
|
options.dtstart = options.dtstart || null
|
|
options.cache = options.cache || false
|
|
options.ignoretz = options.ignoretz || false
|
|
options.tzinfos = options.tzinfos || null
|
|
|
|
var name, value, parts
|
|
if (line.indexOf(':') !== -1) {
|
|
parts = line.split(':')
|
|
name = parts[0]
|
|
value = parts[1]
|
|
|
|
if (name !== 'RRULE') throw new Error('unknown parameter name')
|
|
} else {
|
|
value = line
|
|
}
|
|
|
|
var i
|
|
var rrkwargs = {}
|
|
var pairs = value.split(';')
|
|
|
|
for (i = 0; i < pairs.length; i++) {
|
|
parts = pairs[i].split('=')
|
|
name = parts[0].toUpperCase()
|
|
value = parts[1].toUpperCase()
|
|
|
|
try {
|
|
this['_handle_' + name](rrkwargs, name, value, {
|
|
ignoretz: options.ignoretz,
|
|
tzinfos: options.tzinfos
|
|
})
|
|
} catch (error) {
|
|
throw new Error("unknown parameter '" + name + "':" + value)
|
|
}
|
|
}
|
|
rrkwargs.dtstart = rrkwargs.dtstart || options.dtstart
|
|
return new RRule(rrkwargs, !options.cache)
|
|
},
|
|
|
|
_parseRfc: function (s, options) {
|
|
if (options.compatible) {
|
|
options.forceset = true
|
|
options.unfold = true
|
|
}
|
|
|
|
s = s && s.toUpperCase().trim()
|
|
if (!s) throw new Error('Invalid empty string')
|
|
|
|
var i = 0
|
|
var line, lines
|
|
|
|
// More info about 'unfold' option
|
|
// Go head to http://www.ietf.org/rfc/rfc2445.txt
|
|
if (options.unfold) {
|
|
lines = s.split('\n')
|
|
while (i < lines.length) {
|
|
// TODO
|
|
line = lines[i] = lines[i].replace(/\s+$/g, '')
|
|
if (!line) {
|
|
lines.splice(i, 1)
|
|
} else if (i > 0 && line[0] === ' ') {
|
|
lines[i - 1] += line.slice(1)
|
|
lines.splice(i, 1)
|
|
} else {
|
|
i += 1
|
|
}
|
|
}
|
|
} else {
|
|
lines = s.split(/\s/)
|
|
}
|
|
|
|
var rrulevals = []
|
|
var rdatevals = []
|
|
var exrulevals = []
|
|
var exdatevals = []
|
|
var name, value, parts, parms, parm, dtstart, rset, j, k, datestrs, datestr
|
|
|
|
if (!options.forceset && lines.length === 1 && (s.indexOf(':') === -1 ||
|
|
s.indexOf('RRULE:') === 0)) {
|
|
return this._parseRfcRRule(lines[0], {
|
|
cache: options.cache,
|
|
dtstart: options.dtstart,
|
|
ignoretz: options.ignoretz,
|
|
tzinfos: options.tzinfos
|
|
})
|
|
} else {
|
|
for (i = 0; i < lines.length; i++) {
|
|
line = lines[i]
|
|
if (!line) continue
|
|
if (line.indexOf(':') === -1) {
|
|
name = 'RRULE'
|
|
value = line
|
|
} else {
|
|
parts = split(line, ':', 1)
|
|
name = parts[0]
|
|
value = parts[1]
|
|
}
|
|
parms = name.split(';')
|
|
if (!parms) throw new Error('empty property name')
|
|
name = parms[0]
|
|
parms = parms.slice(1)
|
|
|
|
if (name === 'RRULE') {
|
|
for (j = 0; j < parms.length; j++) {
|
|
parm = parms[j]
|
|
throw new Error('unsupported RRULE parm: ' + parm)
|
|
}
|
|
rrulevals.push(value)
|
|
} else if (name === 'RDATE') {
|
|
for (j = 0; j < parms.length; j++) {
|
|
parm = parms[j]
|
|
if (parm !== 'VALUE=DATE-TIME' && parm !== 'VALUE=DATE') {
|
|
throw new Error('unsupported RDATE parm: ' + parm)
|
|
}
|
|
}
|
|
rdatevals.push(value)
|
|
} else if (name === 'EXRULE') {
|
|
for (j = 0; j < parms.length; j++) {
|
|
parm = parms[j]
|
|
throw new Error('unsupported EXRULE parm: ' + parm)
|
|
}
|
|
exrulevals.push(value)
|
|
} else if (name === 'EXDATE') {
|
|
for (j = 0; j < parms.length; j++) {
|
|
parm = parms[j]
|
|
if (parm !== 'VALUE=DATE-TIME' && parm !== 'VALUE=DATE') {
|
|
throw new Error('unsupported RDATE parm: ' + parm)
|
|
}
|
|
}
|
|
exdatevals.push(value)
|
|
} else if (name === 'DTSTART') {
|
|
dtstart = dateutil.untilStringToDate(value)
|
|
} else {
|
|
throw new Error('unsupported property: ' + name)
|
|
}
|
|
}
|
|
|
|
if (options.forceset || rrulevals.length > 1 || rdatevals.length ||
|
|
exrulevals.length || exdatevals.length) {
|
|
rset = new RRuleSet(!options.cache)
|
|
for (j = 0; j < rrulevals.length; j++) {
|
|
rset.rrule(this._parseRfcRRule(rrulevals[j], {
|
|
dtstart: options.dtstart || dtstart,
|
|
ignoretz: options.ignoretz,
|
|
tzinfos: options.tzinfos
|
|
}))
|
|
}
|
|
for (j = 0; j < rdatevals.length; j++) {
|
|
datestrs = rdatevals[j].split(',')
|
|
for (k = 0; k < datestrs.length; k++) {
|
|
datestr = datestrs[k]
|
|
rset.rdate(dateutil.untilStringToDate(datestr))
|
|
}
|
|
}
|
|
for (j = 0; j < exrulevals.length; j++) {
|
|
rset.exrule(this._parseRfcRRule(exrulevals[j], {
|
|
dtstart: options.dtstart || dtstart,
|
|
ignoretz: options.ignoretz,
|
|
tzinfos: options.tzinfos
|
|
}))
|
|
}
|
|
for (j = 0; j < exdatevals.length; j++) {
|
|
datestrs = exdatevals[j].split(',')
|
|
for (k = 0; k < datestrs.length; k++) {
|
|
datestr = datestrs[k]
|
|
rset.exdate(dateutil.untilStringToDate(datestr))
|
|
}
|
|
}
|
|
|
|
if (options.campatiable && options.dtstart) rset.rdate(dtstart)
|
|
return rset
|
|
} else {
|
|
return this._parseRfcRRule(rrulevals[0], {
|
|
dtstart: options.dtstart || dtstart,
|
|
cache: options.cache,
|
|
ignoretz: options.ignoretz,
|
|
tzinfos: options.tzinfos
|
|
})
|
|
}
|
|
}
|
|
},
|
|
|
|
parse: function (s, options) {
|
|
options = options || {}
|
|
|
|
var invalid = []
|
|
var keys = Object.keys(options)
|
|
var defaultKeys = Object.keys(RRuleStr.DEFAULT_OPTIONS)
|
|
|
|
keys.forEach(function (key) {
|
|
if (!contains(defaultKeys, key)) invalid.push(key)
|
|
}, this)
|
|
|
|
if (invalid.length) throw new Error('Invalid options: ' + invalid.join(', '))
|
|
|
|
// Merge in default options
|
|
defaultKeys.forEach(function (key) {
|
|
if (!contains(keys, key)) options[key] = RRuleStr.DEFAULT_OPTIONS[key]
|
|
})
|
|
|
|
return this._parseRfc(s, options)
|
|
}
|
|
}
|
|
|
|
RRuleStr.prototype._handle_DTSTART = function (rrkwargs, name, value, options) {
|
|
rrkwargs[name.toLowerCase()] = dateutil.untilStringToDate(value)
|
|
}
|
|
|
|
RRuleStr.prototype._handle_BYDAY = RRuleStr.prototype._handle_BYWEEKDAY
|
|
RRuleStr.prototype._handle_INTERVAL = RRuleStr.prototype._handle_int
|
|
RRuleStr.prototype._handle_COUNT = RRuleStr.prototype._handle_int
|
|
|
|
;[
|
|
'_handle_BYSETPOS', '_handle_BYMONTH', '_handle_BYMONTHDAY',
|
|
'_handle_BYYEARDAY', '_handle_BYEASTER', '_handle_BYWEEKNO',
|
|
'_handle_BYHOUR', '_handle_BYMINUTE', '_handle_BYSECOND'
|
|
].forEach(function (method) {
|
|
RRuleStr.prototype[method] = RRuleStr.prototype._handle_int_list
|
|
})
|
|
|
|
// =============================================================================
|
|
// Export
|
|
// =============================================================================
|
|
|
|
// Only one RRuleStr instance for all rrule string parsing work.
|
|
var rruleStr = new RRuleStr()
|
|
var rrulestr = function () {
|
|
return rruleStr.parse.apply(rruleStr, arguments)
|
|
}
|
|
|
|
RRule.RRule = RRule
|
|
RRule.RRuleSet = RRuleSet
|
|
RRule.rrulestr = rrulestr
|
|
return RRule
|
|
|
|
function getnlp () {
|
|
// Lazy, runtime import to avoid circular refs.
|
|
if (!getnlp._nlp) {
|
|
if (root && root._getRRuleNLP) {
|
|
getnlp._nlp = root._getRRuleNLP(RRule)
|
|
} else if (typeof require === 'function') {
|
|
getnlp._nlp = require('./nlp')(RRule)
|
|
} else {
|
|
throw new Error('You need to include rrule/nlp.js for fromText/toText to work.')
|
|
}
|
|
}
|
|
return getnlp._nlp
|
|
}
|
|
})); // eslint-disable-line
|