UNPKG

repeating-interval

Version:

IS0 8601 repeating interval parsing and manipulation

440 lines 17.1 kB
"use strict"; var moment = require('moment'); var millisPerSecond = 1000; var millisPerMinute = millisPerSecond * 60; var millisPerHour = millisPerMinute * 60; var millisPerDay = millisPerHour * 24; var millisPerWeek = millisPerDay * 7; var millisPerYear = millisPerDay * 365; var recurrenceRegex = /^R(\d*)/; var recurrenceIndex = { repetitions: 1 }; // From https://gist.github.com/philipashlock/8830168 var dateRegex = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; var durationRegex = /^(R(\d*)?\/)?P(?:(\d+(?:\.\d+)?)?Y)?(?:(\d+(?:\.\d+)?)?M)?(?:(\d+(?:\.\d+)?)?W)?(?:(\d+(?:\.\d+)?)?D)?(?:T(?:(\d+(?:\.\d+)?)?H)?(?:(\d+(?:\.\d+)?)?M)?(?:(\d+(?:\.\d+)?)?S)?)?$/; var durationIndex = { hasRecurrence: 0, repetitionCount: 1, year: 2, month: 3, week: 4, day: 5, hour: 6, minute: 7, second: 8 }; var rangeDateDateRegex = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?(\/)([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; var rangeDateDurationRegex = /^(R\d*\/)?([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\4([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\18[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?(\/)P(?:\d+(?:\.\d+)?Y)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?W)?(?:\d+(?:\.\d+)?D)?(?:T(?:\d+(?:\.\d+)?H)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?S)?)?$/; var rangeDurationDateRegex = /^(R\d*\/)?P(?:\d+(?:\.\d+)?Y)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?W)?(?:\d+(?:\.\d+)?D)?(?:T(?:\d+(?:\.\d+)?H)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?S)?)?\/([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\4([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\18[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; function isInfinite(s) { return s.length === 0; } function isDuration(s) { return s[0] === 'P'; } function isRepeating(s) { return s[0] === 'R'; } function isTime(s) { return moment(s).isValid(); } var String8601Type; (function (String8601Type) { String8601Type[String8601Type["repeating"] = 0] = "repeating"; String8601Type[String8601Type["time"] = 1] = "time"; String8601Type[String8601Type["duration"] = 2] = "duration"; String8601Type[String8601Type["infinite"] = 3] = "infinite"; })(String8601Type || (String8601Type = {})); function parse8601String(s) { if (isInfinite(s)) { return { type: String8601Type.infinite, value: null }; } else if (isDuration(s)) { return { type: String8601Type.duration, value: moment.duration(s) }; } else if (isRepeating(s)) { var result = recurrenceRegex.exec(s); return { type: String8601Type.repeating, value: typeof result[recurrenceIndex.repetitions] === 'string' ? parseInt(result[recurrenceIndex.repetitions]) : Number.POSITIVE_INFINITY }; } else if (isTime(s)) { return { type: String8601Type.time, value: moment(s).valueOf() }; } else { throw new Error("[" + s + "] Is not a valid part of an ISO8601 time string"); } } /** * Class to describe ISO 8601 time intervals, including the repeating functionality. * * The class is primarily backed by moment.js and contains the start, end, duration, * and number of repetitions found in the ISO8601 string or overridden in the constructor. * Typical usage of the class is to construct one with a ISO8601 string in the constructor. */ var Interval = (function () { /** * @param interval either ISO8601 string or an instance to copy * @param repetitions the number of times to repeat, null for infinite */ function Interval(interval, repetitions) { this._recurs = false; this._repetitions = 0; this._infiniteSpan = false; if (typeof interval === 'string') { // Parse out the ISO 8601 string var split = interval.split('/'); if (split.length > 3) { throw new Error("Invalid ISO 8601 string[" + interval + "]"); } for (var i = 0; i < split.length; i++) { var fragment = split[i]; var parsed = parse8601String(fragment); switch (parsed.type) { case String8601Type.repeating: if (i !== 0) { throw new Error("Incorrect placement of recurrence"); } if (parsed.value !== 0) { this._recurs = true; this._repetitions = parsed.value; } break; case String8601Type.time: if (this._duration || this._start || this._infiniteSpan) { this._end = parsed.value; } else if (!this._start) { this._start = parsed.value; } else { throw new Error("Invalid interval[" + interval + "] end time must come after a duration, start time or be an infinite span"); } break; case String8601Type.duration: if (this._duration) { throw new Error("Invalid interval[" + interval + "] two durations found"); } this._duration = parsed.value; break; case String8601Type.infinite: if (this._duration || this._recurs) { throw new Error("Invalid interval[" + interval + "] single span infinite not compatible with durations or recurrence"); } this._infiniteSpan = true; break; default: throw new Error("Experienced unhandled type"); } } } else { this._start = interval._start; this._end = interval._end; this._duration = moment.duration(interval._duration); this._recurs = interval._recurs; this._repetitions = interval._repetitions; this._infiniteSpan = interval._infiniteSpan; } if (typeof repetitions === 'number') { if (repetitions === 0) { this._recurs = false; } this._repetitions = repetitions; } else if (repetitions === null) { this._recurs = true; this._repetitions = repetitions; } // Throw an error if this this is just bunk if (this._start && this._end && this._duration) { throw new Error("Error creating interval with args[" + arguments + "]"); } // Determine the first and last occurrence indexes if (!this._recurs) { // Non recurring case this._first = 0; if (this._end || this._duration) { this._last = 1; } else { this._last = 0; } } else if (this._start && (this._end || this._duration)) { // Forward progressing case this._first = 0; this._last = this._repetitions; } else { // Backward progressing case this._last = 0; this._first = -this._repetitions; } } Object.defineProperty(Interval.prototype, "start", { /** * The start of all intervals, if this is a repeating interval * then this is the start of the first repitition. * Returns an invalid moment if there are infinite negative repetitions */ get: function () { if (this.isInfiniteNegative) { return moment.invalid(); } else { return this.occurrence(this.first); } }, enumerable: true, configurable: true }); Object.defineProperty(Interval.prototype, "end", { /** * The end of all intervals, if this is a repeating interval * then this is then end of the last repitition. * Returns an invalid moment if there are infinite positive repetitions */ get: function () { if (this.isInfinitePositive) { return moment.invalid(); } else { return this.occurrence(this.last); } }, enumerable: true, configurable: true }); Object.defineProperty(Interval.prototype, "duration", { /** * The duration of a single repition of the interval */ get: function () { if (this._duration) { return this._duration; } if (this._start && this._end) { return moment.duration(this._end - this._start); } else { return moment.duration(0); } }, enumerable: true, configurable: true }); Object.defineProperty(Interval.prototype, "isInfinite", { /** * True if the schedule is infintely long * @returns {boolean} */ get: function () { return !isFinite(this._repetitions) || this._infiniteSpan; }, enumerable: true, configurable: true }); Object.defineProperty(Interval.prototype, "isInfinitePositive", { /** * True if the schedule progresses infinitely in the positive * @returns {boolean} */ get: function () { return !isFinite(this.last) || (this._infiniteSpan && !this._end); }, enumerable: true, configurable: true }); Object.defineProperty(Interval.prototype, "isInfiniteNegative", { /** * True if the schedule progresses infinitely in the negative * @returns {boolean} */ get: function () { return !isFinite(this.first) || (this._infiniteSpan && !this._start); }, enumerable: true, configurable: true }); Object.defineProperty(Interval.prototype, "isRepeating", { /** * True if the schedule repeats (has more than one occurrence) * @returns {boolean} */ get: function () { return this._recurs; }, enumerable: true, configurable: true }); Object.defineProperty(Interval.prototype, "repetitions", { /** * The number of repetitions * @returns {number} */ get: function () { return this._repetitions; }, enumerable: true, configurable: true }); Object.defineProperty(Interval.prototype, "first", { /** * Index of the first occurrence, * Number.NEGATIVE_INFINITY if the interval is reverse repeating indefinitely * @returns {number} */ get: function () { return this._first; }, enumerable: true, configurable: true }); Object.defineProperty(Interval.prototype, "last", { /** * Index of the last occurrence, * Number.POSITIVE_INFINITY if the interval is forward repeating indefinitely * @returns {number} */ get: function () { return this._last; }, enumerable: true, configurable: true }); Interval.prototype.occurrence = function (idx) { if (idx > this.last || idx < this.first) { return moment.invalid(); } if (this.isInfiniteNegative) { return moment(this._end).subtract(this.durationBetween(idx, this.last)); } else { return moment(this._start).add(this.durationBetween(this.first, idx)); } }; /** * Get a list of the occurrences for an interval, * if no parameters are supplied then all occurrences are returned. * * @param {number} [from] the starting occurrence index * @param {number} [to] the ending occurrence index * @returns {moment.Moment[]} */ Interval.prototype.slice = function (from, to) { // Check for being out of bounds if (from > this.last || to < this.first) { return []; } if (from == null) { if (this.isInfiniteNegative) { throw new Error("Tried to get all occurrences with no lower bound[" + this.toISOString() + "]"); } from = this.first; } else if (from < this.first) { from = this.first; } if (to == null) { if (this.isInfinitePositive) { throw new Error("Tried to get all occurrences with no upper bound[" + this.toISOString() + "]"); } to = this.last; } else if (to > this.last) { to = this.last; } var count = to - from + 1; var initialDuration = this.durationBetween(from, to); var currentOccurrence; if (this.isInfiniteNegative) { currentOccurrence = moment(this._end).subtract(initialDuration); } else { currentOccurrence = moment(this._start).add(initialDuration); } var occurrences = []; for (var i = 0; i < count; i++) { occurrences.push(currentOccurrence); currentOccurrence.add(this.duration); } return occurrences; }; /** * Get the recurrence (the index of the occurrence) after the supplied time * @param after Moment or something that it parses (if a number, then in epoc ms) */ Interval.prototype.indexAfter = function (after) { var afterMs = moment(after).valueOf(); var deltaMs; if (this._start) { deltaMs = afterMs - this._start; } else { deltaMs = afterMs - this._end; } var durationMs = this.duration.asMilliseconds(); return Math.ceil(deltaMs / durationMs); }; Interval.prototype.indexBefore = function (before) { return this.indexAfter(before) - 1; }; /** * Get the occurrence happening after the supplied date. * Throws Error if there is no occurrence after the supplied date */ Interval.prototype.occurrenceAfter = function (after) { var afterMoment = moment(after); var recurrence = this.indexAfter(afterMoment); return this.occurrence(recurrence); }; Interval.prototype.durationBetween = function (from, to) { return moment.duration(this.duration.asMilliseconds() * (to - from)); }; Interval.extents = function (intervalList) { var start = moment.min(intervalList.map(function (s) { return s.start; })); var end = moment.max(intervalList.map(function (s) { return s.end; })); return new Interval((start.isValid() ? start.toISOString() : '') + "/" + (end.isValid() ? end.toISOString() : '')); }; Interval.prototype.toISOString = function () { var parts = []; if (this._recurs) { parts.push("R" + (isFinite(this._repetitions) ? this._repetitions.toString() : '')); } if (this._start) { parts.push(moment(this._start).toISOString()); if (this._infiniteSpan) { parts.push(''); } } if (this._duration) { parts.push(this._duration.toISOString()); } if (this._end) { parts.push(moment(this._end).toISOString()); if (this._infiniteSpan) { parts.push(''); } } return parts.join('/'); }; Interval.prototype.toString = function () { return this.toISOString(); }; return Interval; }()); exports.Interval = Interval; //# sourceMappingURL=interval.js.map