UNPKG

ical.js-one.com

Version:

[![Build Status](https://secure.travis-ci.org/mozilla-comm/ical.js.png?branch=master)](http://travis-ci.org/mozilla-comm/ical.js)

438 lines (366 loc) 11 kB
ICAL.RecurExpansion = (function() { function formatTime(item) { return ICAL.helpers.formatClassType(item, ICAL.Time); } function compareTime(a, b) { return a.compare(b); } function isRecurringComponent(comp) { return comp.hasProperty('rdate') || comp.hasProperty('rrule') || comp.hasProperty('recurrence-id'); } /** * Primary class for expanding recurring rules. * Can take multiple rrules, rdates, exdate(s) * and iterate (in order) over each next occurrence. * * Once initialized this class can also be serialized * saved and continue iteration from the last point. * * NOTE: it is intended that this class is to be used * with ICAL.Event which handles recurrence exceptions. * * Options: * - dtstart: (ICAL.Time) start time of event (required) * - component: (ICAL.Component) component (required unless resuming) * * Examples: * * // assuming event is a parsed ical component * var event; * * var expand = new ICAL.RecurExpansion({ * component: event, * start: event.getFirstPropertyValue('DTSTART') * }); * * // remember there are infinite rules * // so its a good idea to limit the scope * // of the iterations then resume later on. * * // next is always an ICAL.Time or null * var next; * * while(someCondition && (next = expand.next())) { * // do something with next * } * * // save instance for later * var json = JSON.stringify(expand); * * //... * * // NOTE: if the component's properties have * // changed you will need to rebuild the * // class and start over. This only works * // when the component's recurrence info is the same. * var expand = new ICAL.RecurExpansion(JSON.parse(json)); * * * @param {Object} options see options block. */ function RecurExpansion(options) { this.ruleDates = []; this.exDates = []; this.fromData(options); } RecurExpansion.prototype = { /** * True when iteration is fully completed. */ complete: false, /** * Array of rrule iterators. * * @type Array[ICAL.RecurIterator] * @private */ ruleIterators: null, /** * Array of rdate instances. * * @type Array[ICAL.Time] * @private */ ruleDates: null, /** * Array of exdate instances. * * @type Array[ICAL.Time] * @private */ exDates: null, /** * Current position in ruleDates array. * @type Numeric * @private */ ruleDateInc: 0, /** * Current position in exDates array * @type Numeric * @private */ exDateInc: 0, /** * Current negative date. * * @type ICAL.Time * @private */ exDate: null, /** * Current additional date. * * @type ICAL.Time * @private */ ruleDate: null, /** * Start date of recurring rules. * * @type ICAL.Time */ dtstart: null, /** * Last expanded time * * @type ICAL.Time */ last: null, fromData: function(options) { var start = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time); if (!start) { throw new Error('.dtstart (ICAL.Time) must be given'); } else { this.dtstart = start; } if (options.component) { this._init(options.component); } else { this.last = formatTime(options.last); this.ruleIterators = options.ruleIterators.map(function(item) { return ICAL.helpers.formatClassType(item, ICAL.RecurIterator); }); this.ruleDateInc = options.ruleDateInc; this.exDateInc = options.exDateInc; if (options.ruleDates) { this.ruleDates = options.ruleDates.map(formatTime); this.ruleDate = this.ruleDates[this.ruleDateInc]; } if (options.exDates) { this.exDates = options.exDates.map(formatTime); this.exDate = this.exDates[this.exDateInc]; } if (typeof(options.complete) !== 'undefined') { this.complete = options.complete; } } }, next: function() { var iter; var ruleOfDay; var next; var compare; var maxTries = 500; var currentTry = 0; while (true) { if (currentTry++ > maxTries) { throw new Error( 'max tries have occured, rule may be impossible to forfill.' ); } next = this.ruleDate; iter = this._nextRecurrenceIter(this.last); // no more matches // because we increment the rule day or rule // _after_ we choose a value this should be // the only spot where we need to worry about the // end of events. if (!next && !iter) { // there are no more iterators or rdates this.complete = true; break; } // no next rule day or recurrence rule is first. if (!next || (iter && next.compare(iter.last) > 0)) { // must be cloned, recur will reuse the time element. next = iter.last.clone(); // move to next so we can continue iter.next(); } // if the ruleDate is still next increment it. if (this.ruleDate === next) { this._nextRuleDay(); } this.last = next; // check the negative rules if (this.exDate) { compare = this.exDate.compare(this.last); if (compare < 0) { this._nextExDay(); } // if the current rule is excluded skip it. if (compare === 0) { this._nextExDay(); continue; } } //XXX: The spec states that after we resolve the final // list of dates we execute exdate this seems somewhat counter // intuitive to what I have seen most servers do so for now // I exclude based on the original date not the one that may // have been modified by the exception. return this.last; } }, /** * Converts object into a serialize-able format. */ toJSON: function() { function toJSON(item) { return item.toJSON(); } var result = Object.create(null); result.ruleIterators = this.ruleIterators.map(toJSON); if (this.ruleDates) { result.ruleDates = this.ruleDates.map(toJSON); } if (this.exDates) { result.exDates = this.exDates.map(toJSON); } result.ruleDateInc = this.ruleDateInc; result.exDateInc = this.exDateInc; result.last = this.last.toJSON(); result.dtstart = this.dtstart.toJSON(); result.complete = this.complete; return result; }, _extractDates: function(component, property) { var result = []; var props = component.getAllProperties(property); var len = props.length; var i = 0; var prop; var idx; for (; i < len; i++) { props[i].getValues().forEach(function(prop) { idx = ICAL.helpers.binsearchInsert( result, prop, compareTime ); // ordered insert result.splice(idx, 0, prop); }); } return result; }, _init: function(component) { this.ruleIterators = []; this.last = this.dtstart.clone(); // to provide api consistency non-recurring // events can also use the iterator though it will // only return a single time. if (!isRecurringComponent(component)) { this.ruleDate = this.last.clone(); this.complete = true; return; } if (component.hasProperty('rdate')) { this.ruleDates = this._extractDates(component, 'rdate'); // special hack for cases where first rdate is prior // to the start date. We only check for the first rdate. // This is mostly for google's crazy recurring date logic // (contacts birthdays). if ((this.ruleDates[0]) && (this.ruleDates[0].compare(this.dtstart) < 0)) { this.ruleDateInc = 0; this.last = this.ruleDates[0].clone(); } else { this.ruleDateInc = ICAL.helpers.binsearchInsert( this.ruleDates, this.last, compareTime ); } this.ruleDate = this.ruleDates[this.ruleDateInc]; } if (component.hasProperty('rrule')) { var rules = component.getAllProperties('rrule'); var i = 0; var len = rules.length; var rule; var iter; for (; i < len; i++) { rule = rules[i].getFirstValue(); iter = rule.iterator(this.dtstart); this.ruleIterators.push(iter); // increment to the next occurrence so future // calls to next return times beyond the initial iteration. // XXX: I find this suspicious might be a bug? iter.next(); } } if (component.hasProperty('exdate')) { this.exDates = this._extractDates(component, 'exdate'); // if we have a .last day we increment the index to beyond it. this.exDateInc = ICAL.helpers.binsearchInsert( this.exDates, this.last, compareTime ); this.exDate = this.exDates[this.exDateInc]; } }, _nextExDay: function() { this.exDate = this.exDates[++this.exDateInc]; }, _nextRuleDay: function() { this.ruleDate = this.ruleDates[++this.ruleDateInc]; }, /** * Find and return the recurrence rule with the most * recent event and return it. * * @return {Object} iterator. */ _nextRecurrenceIter: function() { var iters = this.ruleIterators; if (iters.length === 0) { return null; } var len = iters.length; var iter; var iterTime; var iterIdx = 0; var chosenIter; // loop through each iterator for (; iterIdx < len; iterIdx++) { iter = iters[iterIdx]; iterTime = iter.last; // if iteration is complete // then we must exclude it from // the search and remove it. if (iter.completed) { len--; if (iterIdx !== 0) { iterIdx--; } iters.splice(iterIdx, 1); continue; } // find the most recent possible choice if (!chosenIter || chosenIter.last.compare(iterTime) > 0) { // that iterator is saved chosenIter = iter; } } // the chosen iterator is returned but not mutated // this iterator contains the most recent event. return chosenIter; } }; return RecurExpansion; }());