Skip to content

Instantly share code, notes, and snippets.

@jesusalber1
Last active November 23, 2016 12:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jesusalber1/82ebdf6ad1ec0ce357298097052e93e9 to your computer and use it in GitHub Desktop.
Save jesusalber1/82ebdf6ad1ec0ce357298097052e93e9 to your computer and use it in GitHub Desktop.
Calendar synchronization: EDT (Telecom Bretagne) and GCal
/* Source (but there are some changes): https://cdn.rawgit.com/mozilla-comm/ical.js/892e969cc0e7d814aa3d0d10c0411b73a08be770/build/ical.js */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/* istanbul ignore next */
/* jshint ignore:start */
if (typeof module === 'object') {
// CommonJS, where exports may be different each time.
ICAL = module.exports;
} else if (typeof ICAL !== 'object') {/* istanbul ignore next */
/** @ignore */
this.ICAL = {};
}
/* jshint ignore:end */
/**
* The number of characters before iCalendar line folding should occur
* @type {Number}
* @default 75
*/
ICAL.foldLength = 75;
/**
* The character(s) to be used for a newline. The default value is provided by
* rfc5545.
* @type {String}
* @default "\r\n"
*/
ICAL.newLineChar = '\r\n';
/**
* Helper functions used in various places within ical.js
* @namespace
*/
ICAL.helpers = {
/**
* Checks if the given type is of the number type and also NaN.
*
* @param {Number} number The number to check
* @return {Boolean} True, if the number is strictly NaN
*/
isStrictlyNaN: function(number) {
return typeof(number) === 'number' && isNaN(number);
},
/**
* Parses a string value that is expected to be an integer, when the valid is
* not an integer throws a decoration error.
*
* @param {String} string Raw string input
* @return {Number} Parsed integer
*/
strictParseInt: function(string) {
var result = parseInt(string, 10);
if (ICAL.helpers.isStrictlyNaN(result)) {
throw new Error(
'Could not extract integer from "' + string + '"'
);
}
return result;
},
/**
* Creates or returns a class instance of a given type with the initialization
* data if the data is not already an instance of the given type.
*
* @example
* var time = new ICAL.Time(...);
* var result = ICAL.helpers.formatClassType(time, ICAL.Time);
*
* (result instanceof ICAL.Time)
* // => true
*
* result = ICAL.helpers.formatClassType({}, ICAL.Time);
* (result isntanceof ICAL.Time)
* // => true
*
*
* @param {Object} data object initialization data
* @param {Object} type object type (like ICAL.Time)
* @return {?} An instance of the found type.
*/
formatClassType: function formatClassType(data, type) {
if (typeof(data) === 'undefined') {
return undefined;
}
if (data instanceof type) {
return data;
}
return new type(data);
},
/**
* Identical to indexOf but will only match values when they are not preceded
* by a backslash character.
*
* @param {String} buffer String to search
* @param {String} search Value to look for
* @param {Number} pos Start position
* @return {Number} The position, or -1 if not found
*/
unescapedIndexOf: function(buffer, search, pos) {
while ((pos = buffer.indexOf(search, pos)) !== -1) {
if (pos > 0 && buffer[pos - 1] === '\\') {
pos += 1;
} else {
return pos;
}
}
return -1;
},
/**
* Find the index for insertion using binary search.
*
* @param {Array} list The list to search
* @param {?} seekVal The value to insert
* @param {function(?,?)} cmpfunc The comparison func, that can
* compare two seekVals
* @return {Number} The insert position
*/
binsearchInsert: function(list, seekVal, cmpfunc) {
if (!list.length)
return 0;
var low = 0, high = list.length - 1,
mid, cmpval;
while (low <= high) {
mid = low + Math.floor((high - low) / 2);
cmpval = cmpfunc(seekVal, list[mid]);
if (cmpval < 0)
high = mid - 1;
else if (cmpval > 0)
low = mid + 1;
else
break;
}
if (cmpval < 0)
return mid; // insertion is displacing, so use mid outright.
else if (cmpval > 0)
return mid + 1;
else
return mid;
},
/**
* Convenience function for debug output
* @private
*/
dumpn: /* istanbul ignore next */ function() {
if (!ICAL.debug) {
return;
}
if (typeof (console) !== 'undefined' && 'log' in console) {
ICAL.helpers.dumpn = function consoleDumpn(input) {
console.log(input);
};
} else {
ICAL.helpers.dumpn = function geckoDumpn(input) {
dump(input + '\n');
};
}
ICAL.helpers.dumpn(arguments[0]);
},
/**
* Clone the passed object or primitive. By default a shallow clone will be
* executed.
*
* @param {*} aSrc The thing to clone
* @param {Boolean=} aDeep If true, a deep clone will be performed
* @return {*} The copy of the thing
*/
clone: function(aSrc, aDeep) {
if (!aSrc || typeof aSrc != "object") {
return aSrc;
} else if (aSrc instanceof Date) {
return new Date(aSrc.getTime());
} else if ("clone" in aSrc) {
return aSrc.clone();
} else if (Array.isArray(aSrc)) {
var arr = [];
for (var i = 0; i < aSrc.length; i++) {
arr.push(aDeep ? ICAL.helpers.clone(aSrc[i], true) : aSrc[i]);
}
return arr;
} else {
var obj = {};
for (var name in aSrc) {
// uses prototype method to allow use of Object.create(null);
/* istanbul ignore else */
if (Object.prototype.hasOwnProperty.call(aSrc, name)) {
if (aDeep) {
obj[name] = ICAL.helpers.clone(aSrc[name], true);
} else {
obj[name] = aSrc[name];
}
}
}
return obj;
}
},
/**
* Performs iCalendar line folding. A line ending character is inserted and
* the next line begins with a whitespace.
*
* @example
* SUMMARY:This line will be fold
* ed right in the middle of a word.
*
* @param {String} aLine The line to fold
* @return {String} The folded line
*/
foldline: function foldline(aLine) {
var result = "";
var line = aLine || "";
while (line.length) {
result += ICAL.newLineChar + " " + line.substr(0, ICAL.foldLength);
line = line.substr(ICAL.foldLength);
}
return result.substr(ICAL.newLineChar.length + 1);
},
/**
* Pads the given string or number with zeros so it will have at least two
* characters.
*
* @param {String|Number} data The string or number to pad
* @return {String} The number padded as a string
*/
pad2: function pad(data) {
if (typeof(data) !== 'string') {
// handle fractions.
if (typeof(data) === 'number') {
data = parseInt(data);
}
data = String(data);
}
var len = data.length;
switch (len) {
case 0:
return '00';
case 1:
return '0' + data;
default:
return data;
}
},
/**
* Truncates the given number, correctly handling negative numbers.
*
* @param {Number} number The number to truncate
* @return {Number} The truncated number
*/
trunc: function trunc(number) {
return (number < 0 ? Math.ceil(number) : Math.floor(number));
},
/**
* Poor-man's cross-browser inheritance for JavaScript. Doesn't support all
* the features, but enough for our usage.
*
* @param {Function} base The base class constructor function.
* @param {Function} child The child class constructor function.
* @param {Object} extra Extends the prototype with extra properties
* and methods
*/
inherits: function(base, child, extra) {
function F() {}
F.prototype = base.prototype;
child.prototype = new F();
if (extra) {
ICAL.helpers.extend(extra, child.prototype);
}
},
/**
* Poor-man's cross-browser object extension. Doesn't support all the
* features, but enough for our usage. Note that the target's properties are
* not overwritten with the source properties.
*
* @example
* var child = ICAL.helpers.extend(parent, {
* "bar": 123
* });
*
* @param {Object} source The object to extend
* @param {Object} target The object to extend with
* @return {Object} Returns the target.
*/
extend: function(source, target) {
for (var key in source) {
var descr = Object.getOwnPropertyDescriptor(source, key);
if (descr && !Object.getOwnPropertyDescriptor(target, key)) {
Object.defineProperty(target, key, descr);
}
}
return target;
}
};
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/** @namespace ICAL */
/**
* This symbol is further described later on
* @ignore
*/
ICAL.design = (function() {
'use strict';
var FROM_ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g;
var TO_ICAL_NEWLINE = /\\|;|,|\n/g;
var FROM_VCARD_NEWLINE = /\\\\|\\,|\\[Nn]/g;
var TO_VCARD_NEWLINE = /\\|,|\n/g;
function createTextType(fromNewline, toNewline) {
var result = {
matches: /.*/,
fromICAL: function(aValue, structuredEscape) {
return replaceNewline(aValue, fromNewline, structuredEscape);
},
toICAL: function(aValue, structuredEscape) {
var regEx = toNewline;
if (structuredEscape)
regEx = new RegExp(regEx.source + '|' + structuredEscape);
return aValue.replace(regEx, function(str) {
switch (str) {
case "\\":
return "\\\\";
case ";":
return "\\;";
case ",":
return "\\,";
case "\n":
return "\\n";
/* istanbul ignore next */
default:
return str;
}
});
}
};
return result;
}
// default types used multiple times
var DEFAULT_TYPE_TEXT = { defaultType: "text" };
var DEFAULT_TYPE_TEXT_MULTI = { defaultType: "text", multiValue: "," };
var DEFAULT_TYPE_TEXT_STRUCTURED = { defaultType: "text", structuredValue: ";" };
var DEFAULT_TYPE_INTEGER = { defaultType: "integer" };
var DEFAULT_TYPE_DATETIME_DATE = { defaultType: "date-time", allowedTypes: ["date-time", "date"] };
var DEFAULT_TYPE_DATETIME = { defaultType: "date-time" };
var DEFAULT_TYPE_URI = { defaultType: "uri" };
var DEFAULT_TYPE_UTCOFFSET = { defaultType: "utc-offset" };
var DEFAULT_TYPE_RECUR = { defaultType: "recur" };
var DEFAULT_TYPE_DATE_ANDOR_TIME = { defaultType: "date-and-or-time", allowedTypes: ["date-time", "date", "text"] };
function replaceNewlineReplace(string) {
switch (string) {
case "\\\\":
return "\\";
case "\\;":
return ";";
case "\\,":
return ",";
case "\\n":
case "\\N":
return "\n";
/* istanbul ignore next */
default:
return string;
}
}
function replaceNewline(value, newline, structuredEscape) {
// avoid regex when possible.
if (value.indexOf('\\') === -1) {
return value;
}
if (structuredEscape)
newline = new RegExp(newline.source + '|\\\\' + structuredEscape);
return value.replace(newline, replaceNewlineReplace);
}
var commonProperties = {
"categories": DEFAULT_TYPE_TEXT_MULTI,
"url": DEFAULT_TYPE_URI,
"version": DEFAULT_TYPE_TEXT,
"uid": DEFAULT_TYPE_TEXT
};
var commonValues = {
"boolean": {
values: ["TRUE", "FALSE"],
fromICAL: function(aValue) {
switch (aValue) {
case 'TRUE':
return true;
case 'FALSE':
return false;
default:
//TODO: parser warning
return false;
}
},
toICAL: function(aValue) {
if (aValue) {
return 'TRUE';
}
return 'FALSE';
}
},
float: {
matches: /^[+-]?\d+\.\d+$/,
fromICAL: function(aValue) {
var parsed = parseFloat(aValue);
if (ICAL.helpers.isStrictlyNaN(parsed)) {
// TODO: parser warning
return 0.0;
}
return parsed;
},
toICAL: function(aValue) {
return String(aValue);
}
},
integer: {
fromICAL: function(aValue) {
var parsed = parseInt(aValue);
if (ICAL.helpers.isStrictlyNaN(parsed)) {
return 0;
}
return parsed;
},
toICAL: function(aValue) {
return String(aValue);
}
},
"utc-offset": {
toICAL: function(aValue) {
if (aValue.length < 7) {
// no seconds
// -0500
return aValue.substr(0, 3) +
aValue.substr(4, 2);
} else {
// seconds
// -050000
return aValue.substr(0, 3) +
aValue.substr(4, 2) +
aValue.substr(7, 2);
}
},
fromICAL: function(aValue) {
if (aValue.length < 6) {
// no seconds
// -05:00
return aValue.substr(0, 3) + ':' +
aValue.substr(3, 2);
} else {
// seconds
// -05:00:00
return aValue.substr(0, 3) + ':' +
aValue.substr(3, 2) + ':' +
aValue.substr(5, 2);
}
},
decorate: function(aValue) {
return ICAL.UtcOffset.fromString(aValue);
},
undecorate: function(aValue) {
return aValue.toString();
}
}
};
var icalParams = {
// Although the syntax is DQUOTE uri DQUOTE, I don't think we should
// enfoce anything aside from it being a valid content line.
//
// At least some params require - if multi values are used - DQUOTEs
// for each of its values - e.g. delegated-from="uri1","uri2"
// To indicate this, I introduced the new k/v pair
// multiValueSeparateDQuote: true
//
// "ALTREP": { ... },
// CN just wants a param-value
// "CN": { ... }
"cutype": {
values: ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"],
allowXName: true,
allowIanaToken: true
},
"delegated-from": {
valueType: "cal-address",
multiValue: ",",
multiValueSeparateDQuote: true
},
"delegated-to": {
valueType: "cal-address",
multiValue: ",",
multiValueSeparateDQuote: true
},
// "DIR": { ... }, // See ALTREP
"encoding": {
values: ["8BIT", "BASE64"]
},
// "FMTTYPE": { ... }, // See ALTREP
"fbtype": {
values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"],
allowXName: true,
allowIanaToken: true
},
// "LANGUAGE": { ... }, // See ALTREP
"member": {
valueType: "cal-address",
multiValue: ",",
multiValueSeparateDQuote: true
},
"partstat": {
// TODO These values are actually different per-component
values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE",
"DELEGATED", "COMPLETED", "IN-PROCESS"],
allowXName: true,
allowIanaToken: true
},
"range": {
values: ["THISLANDFUTURE"]
},
"related": {
values: ["START", "END"]
},
"reltype": {
values: ["PARENT", "CHILD", "SIBLING"],
allowXName: true,
allowIanaToken: true
},
"role": {
values: ["REQ-PARTICIPANT", "CHAIR",
"OPT-PARTICIPANT", "NON-PARTICIPANT"],
allowXName: true,
allowIanaToken: true
},
"rsvp": {
values: ["TRUE", "FALSE"]
},
"sent-by": {
valueType: "cal-address"
},
"tzid": {
matches: /^\//
},
"value": {
// since the value here is a 'type' lowercase is used.
values: ["binary", "boolean", "cal-address", "date", "date-time",
"duration", "float", "integer", "period", "recur", "text",
"time", "uri", "utc-offset"],
allowXName: true,
allowIanaToken: true
}
};
// When adding a value here, be sure to add it to the parameter types!
var icalValues = ICAL.helpers.extend(commonValues, {
text: createTextType(FROM_ICAL_NEWLINE, TO_ICAL_NEWLINE),
uri: {
// TODO
/* ... */
},
"binary": {
decorate: function(aString) {
return ICAL.Binary.fromString(aString);
},
undecorate: function(aBinary) {
return aBinary.toString();
}
},
"cal-address": {
// needs to be an uri
},
"date": {
decorate: function(aValue, aProp) {
return ICAL.Time.fromDateString(aValue, aProp);
},
/**
* undecorates a time object.
*/
undecorate: function(aValue) {
return aValue.toString();
},
fromICAL: function(aValue) {
// from: 20120901
// to: 2012-09-01
return aValue.substr(0, 4) + '-' +
aValue.substr(4, 2) + '-' +
aValue.substr(6, 2);
},
toICAL: function(aValue) {
// from: 2012-09-01
// to: 20120901
if (aValue.length > 11) {
//TODO: serialize warning?
return aValue;
}
return aValue.substr(0, 4) +
aValue.substr(5, 2) +
aValue.substr(8, 2);
}
},
"date-time": {
fromICAL: function(aValue) {
// from: 20120901T130000
// to: 2012-09-01T13:00:00
var result = aValue.substr(0, 4) + '-' +
aValue.substr(4, 2) + '-' +
aValue.substr(6, 2) + 'T' +
aValue.substr(9, 2) + ':' +
aValue.substr(11, 2) + ':' +
aValue.substr(13, 2);
if (aValue[15] && aValue[15] === 'Z') {
result += 'Z';
}
return result;
},
toICAL: function(aValue) {
// from: 2012-09-01T13:00:00
// to: 20120901T130000
if (aValue.length < 19) {
// TODO: error
return aValue;
}
var result = aValue.substr(0, 4) +
aValue.substr(5, 2) +
// grab the (DDTHH) segment
aValue.substr(8, 5) +
// MM
aValue.substr(14, 2) +
// SS
aValue.substr(17, 2);
if (aValue[19] && aValue[19] === 'Z') {
result += 'Z';
}
return result;
},
decorate: function(aValue, aProp) {
return ICAL.Time.fromDateTimeString(aValue, aProp);
},
undecorate: function(aValue) {
return aValue.toString();
}
},
duration: {
decorate: function(aValue) {
return ICAL.Duration.fromString(aValue);
},
undecorate: function(aValue) {
return aValue.toString();
}
},
period: {
fromICAL: function(string) {
var parts = string.split('/');
parts[0] = icalValues['date-time'].fromICAL(parts[0]);
if (!ICAL.Duration.isValueString(parts[1])) {
parts[1] = icalValues['date-time'].fromICAL(parts[1]);
}
return parts;
},
toICAL: function(parts) {
parts[0] = icalValues['date-time'].toICAL(parts[0]);
if (!ICAL.Duration.isValueString(parts[1])) {
parts[1] = icalValues['date-time'].toICAL(parts[1]);
}
return parts.join("/");
},
decorate: function(aValue, aProp) {
return ICAL.Period.fromJSON(aValue, aProp);
},
undecorate: function(aValue) {
return aValue.toJSON();
}
},
recur: {
fromICAL: function(string) {
return ICAL.Recur._stringToData(string, true);
},
toICAL: function(data) {
var str = "";
for (var k in data) {
/* istanbul ignore if */
if (!Object.prototype.hasOwnProperty.call(data, k)) {
continue;
}
var val = data[k];
if (k == "until") {
if (val.length > 10) {
val = icalValues['date-time'].toICAL(val);
} else {
val = icalValues.date.toICAL(val);
}
} else if (k == "wkst") {
if (typeof val === 'number') {
val = ICAL.Recur.numericDayToIcalDay(val);
}
} else if (Array.isArray(val)) {
val = val.join(",");
}
str += k.toUpperCase() + "=" + val + ";";
}
return str.substr(0, str.length - 1);
},
decorate: function decorate(aValue) {
return ICAL.Recur.fromData(aValue);
},
undecorate: function(aRecur) {
return aRecur.toJSON();
}
},
time: {
fromICAL: function(aValue) {
// from: MMHHSS(Z)?
// to: HH:MM:SS(Z)?
if (aValue.length < 6) {
// TODO: parser exception?
return aValue;
}
// HH::MM::SSZ?
var result = aValue.substr(0, 2) + ':' +
aValue.substr(2, 2) + ':' +
aValue.substr(4, 2);
if (aValue[6] === 'Z') {
result += 'Z';
}
return result;
},
toICAL: function(aValue) {
// from: HH:MM:SS(Z)?
// to: MMHHSS(Z)?
if (aValue.length < 8) {
//TODO: error
return aValue;
}
var result = aValue.substr(0, 2) +
aValue.substr(3, 2) +
aValue.substr(6, 2);
if (aValue[8] === 'Z') {
result += 'Z';
}
return result;
}
}
});
var icalProperties = ICAL.helpers.extend(commonProperties, {
"action": DEFAULT_TYPE_TEXT,
"attach": { defaultType: "uri" },
"attendee": { defaultType: "cal-address" },
"calscale": DEFAULT_TYPE_TEXT,
"class": DEFAULT_TYPE_TEXT,
"comment": DEFAULT_TYPE_TEXT,
"completed": DEFAULT_TYPE_DATETIME,
"contact": DEFAULT_TYPE_TEXT,
"created": DEFAULT_TYPE_DATETIME,
"description": DEFAULT_TYPE_TEXT,
"dtend": DEFAULT_TYPE_DATETIME_DATE,
"dtstamp": DEFAULT_TYPE_DATETIME,
"dtstart": DEFAULT_TYPE_DATETIME_DATE,
"due": DEFAULT_TYPE_DATETIME_DATE,
"duration": { defaultType: "duration" },
"exdate": {
defaultType: "date-time",
allowedTypes: ["date-time", "date"],
multiValue: ','
},
"exrule": DEFAULT_TYPE_RECUR,
"freebusy": { defaultType: "period", multiValue: "," },
"geo": { defaultType: "float", structuredValue: ";" },
"last-modified": DEFAULT_TYPE_DATETIME,
"location": DEFAULT_TYPE_TEXT,
"method": DEFAULT_TYPE_TEXT,
"organizer": { defaultType: "cal-address" },
"percent-complete": DEFAULT_TYPE_INTEGER,
"priority": DEFAULT_TYPE_INTEGER,
"prodid": DEFAULT_TYPE_TEXT,
"related-to": DEFAULT_TYPE_TEXT,
"repeat": DEFAULT_TYPE_INTEGER,
"rdate": {
defaultType: "date-time",
allowedTypes: ["date-time", "date", "period"],
multiValue: ',',
detectType: function(string) {
if (string.indexOf('/') !== -1) {
return 'period';
}
return (string.indexOf('T') === -1) ? 'date' : 'date-time';
}
},
"recurrence-id": DEFAULT_TYPE_DATETIME_DATE,
"resources": DEFAULT_TYPE_TEXT_MULTI,
"request-status": DEFAULT_TYPE_TEXT_STRUCTURED,
"rrule": DEFAULT_TYPE_RECUR,
"sequence": DEFAULT_TYPE_INTEGER,
"status": DEFAULT_TYPE_TEXT,
"summary": DEFAULT_TYPE_TEXT,
"transp": DEFAULT_TYPE_TEXT,
"trigger": { defaultType: "duration", allowedTypes: ["duration", "date-time"] },
"tzoffsetfrom": DEFAULT_TYPE_UTCOFFSET,
"tzoffsetto": DEFAULT_TYPE_UTCOFFSET,
"tzurl": DEFAULT_TYPE_URI,
"tzid": DEFAULT_TYPE_TEXT,
"tzname": DEFAULT_TYPE_TEXT
});
// When adding a value here, be sure to add it to the parameter types!
var vcardValues = ICAL.helpers.extend(commonValues, {
text: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE),
uri: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE),
date: {
decorate: function(aValue) {
return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date");
},
undecorate: function(aValue) {
return aValue.toString();
},
fromICAL: function(aValue) {
if (aValue.length == 8) {
return icalValues.date.fromICAL(aValue);
} else if (aValue[0] == '-' && aValue.length == 6) {
return aValue.substr(0, 4) + '-' + aValue.substr(4);
} else {
return aValue;
}
},
toICAL: function(aValue) {
if (aValue.length == 10) {
return icalValues.date.toICAL(aValue);
} else if (aValue[0] == '-' && aValue.length == 7) {
return aValue.substr(0, 4) + aValue.substr(5);
} else {
return aValue;
}
}
},
time: {
decorate: function(aValue) {
return ICAL.VCardTime.fromDateAndOrTimeString("T" + aValue, "time");
},
undecorate: function(aValue) {
return aValue.toString();
},
fromICAL: function(aValue) {
var splitzone = vcardValues.time._splitZone(aValue, true);
var zone = splitzone[0], value = splitzone[1];
//console.log("SPLIT: ",splitzone);
if (value.length == 6) {
value = value.substr(0, 2) + ':' +
value.substr(2, 2) + ':' +
value.substr(4, 2);
} else if (value.length == 4 && value[0] != '-') {
value = value.substr(0, 2) + ':' + value.substr(2, 2);
} else if (value.length == 5) {
value = value.substr(0, 3) + ':' + value.substr(3, 2);
}
if (zone.length == 5 && (zone[0] == '-' || zone[0] == '+')) {
zone = zone.substr(0, 3) + ':' + zone.substr(3);
}
return value + zone;
},
toICAL: function(aValue) {
var splitzone = vcardValues.time._splitZone(aValue);
var zone = splitzone[0], value = splitzone[1];
if (value.length == 8) {
value = value.substr(0, 2) +
value.substr(3, 2) +
value.substr(6, 2);
} else if (value.length == 5 && value[0] != '-') {
value = value.substr(0, 2) + value.substr(3, 2);
} else if (value.length == 6) {
value = value.substr(0, 3) + value.substr(4, 2);
}
if (zone.length == 6 && (zone[0] == '-' || zone[0] == '+')) {
zone = zone.substr(0, 3) + zone.substr(4);
}
return value + zone;
},
_splitZone: function(aValue, isFromIcal) {
var lastChar = aValue.length - 1;
var signChar = aValue.length - (isFromIcal ? 5 : 6);
var sign = aValue[signChar];
var zone, value;
if (aValue[lastChar] == 'Z') {
zone = aValue[lastChar];
value = aValue.substr(0, lastChar);
} else if (aValue.length > 6 && (sign == '-' || sign == '+')) {
zone = aValue.substr(signChar);
value = aValue.substr(0, signChar);
} else {
zone = "";
value = aValue;
}
return [zone, value];
}
},
"date-time": {
decorate: function(aValue) {
return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-time");
},
undecorate: function(aValue) {
return aValue.toString();
},
fromICAL: function(aValue) {
return vcardValues['date-and-or-time'].fromICAL(aValue);
},
toICAL: function(aValue) {
return vcardValues['date-and-or-time'].toICAL(aValue);
}
},
"date-and-or-time": {
decorate: function(aValue) {
return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-and-or-time");
},
undecorate: function(aValue) {
return aValue.toString();
},
fromICAL: function(aValue) {
var parts = aValue.split('T');
return (parts[0] ? vcardValues.date.fromICAL(parts[0]) : '') +
(parts[1] ? 'T' + vcardValues.time.fromICAL(parts[1]) : '');
},
toICAL: function(aValue) {
var parts = aValue.split('T');
return vcardValues.date.toICAL(parts[0]) +
(parts[1] ? 'T' + vcardValues.time.toICAL(parts[1]) : '');
}
},
timestamp: icalValues['date-time'],
"language-tag": {
matches: /^[a-zA-Z0-9\-]+$/ // Could go with a more strict regex here
}
});
var vcardParams = {
"type": {
valueType: "text",
multiValue: ","
},
"value": {
// since the value here is a 'type' lowercase is used.
values: ["text", "uri", "date", "time", "date-time", "date-and-or-time",
"timestamp", "boolean", "integer", "float", "utc-offset",
"language-tag"],
allowXName: true,
allowIanaToken: true
}
};
var vcardProperties = ICAL.helpers.extend(commonProperties, {
"adr": { defaultType: "text", structuredValue: ";", multiValue: "," },
"anniversary": DEFAULT_TYPE_DATE_ANDOR_TIME,
"bday": DEFAULT_TYPE_DATE_ANDOR_TIME,
"caladruri": DEFAULT_TYPE_URI,
"caluri": DEFAULT_TYPE_URI,
"clientpidmap": DEFAULT_TYPE_TEXT_STRUCTURED,
"email": DEFAULT_TYPE_TEXT,
"fburl": DEFAULT_TYPE_URI,
"fn": DEFAULT_TYPE_TEXT,
"gender": DEFAULT_TYPE_TEXT_STRUCTURED,
"geo": DEFAULT_TYPE_URI,
"impp": DEFAULT_TYPE_URI,
"key": DEFAULT_TYPE_URI,
"kind": DEFAULT_TYPE_TEXT,
"lang": { defaultType: "language-tag" },
"logo": DEFAULT_TYPE_URI,
"member": DEFAULT_TYPE_URI,
"n": { defaultType: "text", structuredValue: ";", multiValue: "," },
"nickname": DEFAULT_TYPE_TEXT_MULTI,
"note": DEFAULT_TYPE_TEXT,
"org": { defaultType: "text", structuredValue: ";" },
"photo": DEFAULT_TYPE_URI,
"related": DEFAULT_TYPE_URI,
"rev": { defaultType: "timestamp" },
"role": DEFAULT_TYPE_TEXT,
"sound": DEFAULT_TYPE_URI,
"source": DEFAULT_TYPE_URI,
"tel": { defaultType: "uri", allowedTypes: ["uri", "text"] },
"title": DEFAULT_TYPE_TEXT,
"tz": { defaultType: "text", allowedTypes: ["text", "utc-offset", "uri"] },
"xml": DEFAULT_TYPE_TEXT
});
var vcard3Values = ICAL.helpers.extend(commonValues, {
binary: icalValues.binary,
date: vcardValues.date,
"date-time": vcardValues["date-time"],
"phone-number": {
// TODO
/* ... */
},
uri: icalValues.uri,
text: icalValues.text,
time: icalValues.time,
vcard: icalValues.text,
"utc-offset": {
toICAL: function(aValue) {
return aValue.substr(0, 7);
},
fromICAL: function(aValue) {
return aValue.substr(0, 7);
},
decorate: function(aValue) {
return ICAL.UtcOffset.fromString(aValue);
},
undecorate: function(aValue) {
return aValue.toString();
}
}
});
var vcard3Params = {
"type": {
valueType: "text",
multiValue: ","
},
"value": {
// since the value here is a 'type' lowercase is used.
values: ["text", "uri", "date", "date-time", "phone-number", "time",
"boolean", "integer", "float", "utc-offset", "vcard", "binary"],
allowXName: true,
allowIanaToken: true
}
};
var vcard3Properties = ICAL.helpers.extend(commonProperties, {
fn: DEFAULT_TYPE_TEXT,
n: { defaultType: "text", structuredValue: ";", multiValue: "," },
nickname: DEFAULT_TYPE_TEXT_MULTI,
photo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
bday: {
defaultType: "date-time",
allowedTypes: ["date-time", "date"],
detectType: function(string) {
return (string.indexOf('T') === -1) ? 'date' : 'date-time';
}
},
adr: { defaultType: "text", structuredValue: ";", multiValue: "," },
label: DEFAULT_TYPE_TEXT,
tel: { defaultType: "phone-number" },
email: DEFAULT_TYPE_TEXT,
mailer: DEFAULT_TYPE_TEXT,
tz: { defaultType: "utc-offset", allowedTypes: ["utc-offset", "text"] },
geo: { defaultType: "float", structuredValue: ";" },
title: DEFAULT_TYPE_TEXT,
role: DEFAULT_TYPE_TEXT,
logo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
agent: { defaultType: "vcard", allowedTypes: ["vcard", "text", "uri"] },
org: DEFAULT_TYPE_TEXT_STRUCTURED,
note: DEFAULT_TYPE_TEXT_MULTI,
prodid: DEFAULT_TYPE_TEXT,
rev: {
defaultType: "date-time",
allowedTypes: ["date-time", "date"],
detectType: function(string) {
return (string.indexOf('T') === -1) ? 'date' : 'date-time';
}
},
"sort-string": DEFAULT_TYPE_TEXT,
sound: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
class: DEFAULT_TYPE_TEXT,
key: { defaultType: "binary", allowedTypes: ["binary", "text"] }
});
/**
* iCalendar design set
* @type {ICAL.design.designSet}
*/
var icalSet = {
value: icalValues,
param: icalParams,
property: icalProperties
};
/**
* vCard 4.0 design set
* @type {ICAL.design.designSet}
*/
var vcardSet = {
value: vcardValues,
param: vcardParams,
property: vcardProperties
};
/**
* vCard 3.0 design set
* @type {ICAL.design.designSet}
*/
var vcard3Set = {
value: vcard3Values,
param: vcard3Params,
property: vcard3Properties
};
/**
* The design data, used by the parser to determine types for properties and
* other metadata needed to produce correct jCard/jCal data.
*
* @alias ICAL.design
* @namespace
*/
var design = {
/**
* A designSet describes value, parameter and property data. It is used by
* ther parser and stringifier in components and properties to determine they
* should be represented.
*
* @typedef {Object} designSet
* @memberOf ICAL.design
* @property {Object} value Definitions for value types, keys are type names
* @property {Object} param Definitions for params, keys are param names
* @property {Object} property Defintions for properties, keys are property names
*/
/**
* The default set for new properties and components if none is specified.
* @type {ICAL.design.designSet}
*/
defaultSet: icalSet,
/**
* The default type for unknown properties
* @type {String}
*/
defaultType: 'unknown',
/**
* Holds the design set for known top-level components
*
* @type {Object}
* @property {ICAL.design.designSet} vcard vCard VCARD
* @property {ICAL.design.designSet} vevent iCalendar VEVENT
* @property {ICAL.design.designSet} vtodo iCalendar VTODO
* @property {ICAL.design.designSet} vjournal iCalendar VJOURNAL
* @property {ICAL.design.designSet} valarm iCalendar VALARM
* @property {ICAL.design.designSet} vtimezone iCalendar VTIMEZONE
* @property {ICAL.design.designSet} daylight iCalendar DAYLIGHT
* @property {ICAL.design.designSet} standard iCalendar STANDARD
*
* @example
* var propertyName = 'fn';
* var componentDesign = ICAL.design.components.vcard;
* var propertyDetails = componentDesign.property[propertyName];
* if (propertyDetails.defaultType == 'text') {
* // Yep, sure is...
* }
*/
components: {
vcard: vcardSet,
vcard3: vcard3Set,
vevent: icalSet,
vtodo: icalSet,
vjournal: icalSet,
valarm: icalSet,
vtimezone: icalSet,
daylight: icalSet,
standard: icalSet
},
/**
* The design set for iCalendar (rfc5545/rfc7265) components.
* @type {ICAL.design.designSet}
*/
icalendar: icalSet,
/**
* The design set for vCard (rfc6350/rfc7095) components.
* @type {ICAL.design.designSet}
*/
vcard: vcardSet,
/**
* The design set for vCard (rfc2425/rfc2426/rfc7095) components.
* @type {ICAL.design.designSet}
*/
vcard3: vcard3Set,
/**
* Gets the design set for the given component name.
*
* @param {String} componentName The name of the component
* @return {ICAL.design.designSet} The design set for the component
*/
getDesignSet: function(componentName) {
var isInDesign = componentName && componentName in design.components;
return isInDesign ? design.components[componentName] : design.defaultSet;
}
};
return design;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* Contains various functions to convert jCal and jCard data back into
* iCalendar and vCard.
* @namespace
*/
ICAL.stringify = (function() {
'use strict';
var LINE_ENDING = '\r\n';
var DEFAULT_VALUE_TYPE = 'unknown';
var design = ICAL.design;
var helpers = ICAL.helpers;
/**
* Convert a full jCal/jCard array into a iCalendar/vCard string.
*
* @function ICAL.stringify
* @variation function
* @param {Array} jCal The jCal/jCard document
* @return {String} The stringified iCalendar/vCard document
*/
function stringify(jCal) {
if (typeof jCal[0] == "string") {
// This is a single component
jCal = [jCal];
}
var i = 0;
var len = jCal.length;
var result = '';
for (; i < len; i++) {
result += stringify.component(jCal[i]) + LINE_ENDING;
}
return result;
}
/**
* Converts an jCal component array into a ICAL string.
* Recursive will resolve sub-components.
*
* Exact component/property order is not saved all
* properties will come before subcomponents.
*
* @function ICAL.stringify.component
* @param {Array} component
* jCal/jCard fragment of a component
* @param {ICAL.design.designSet} designSet
* The design data to use for this component
* @return {String} The iCalendar/vCard string
*/
stringify.component = function(component, designSet) {
var name = component[0].toUpperCase();
var result = 'BEGIN:' + name + LINE_ENDING;
var props = component[1];
var propIdx = 0;
var propLen = props.length;
var designSetName = component[0];
// rfc6350 requires that in vCard 4.0 the first component is the VERSION
// component with as value 4.0, note that 3.0 does not have this requirement.
if (designSetName === 'vcard' && component[1].length > 0 &&
!(component[1][0][0] === "version" && component[1][0][3] === "4.0")) {
designSetName = "vcard3";
}
designSet = designSet || design.getDesignSet(designSetName);
for (; propIdx < propLen; propIdx++) {
result += stringify.property(props[propIdx], designSet) + LINE_ENDING;
}
var comps = component[2];
var compIdx = 0;
var compLen = comps.length;
for (; compIdx < compLen; compIdx++) {
result += stringify.component(comps[compIdx], designSet) + LINE_ENDING;
}
result += 'END:' + name;
return result;
};
/**
* Converts a single jCal/jCard property to a iCalendar/vCard string.
*
* @function ICAL.stringify.property
* @param {Array} property
* jCal/jCard property array
* @param {ICAL.design.designSet} designSet
* The design data to use for this property
* @param {Boolean} noFold
* If true, the line is not folded
* @return {String} The iCalendar/vCard string
*/
stringify.property = function(property, designSet, noFold) {
var name = property[0].toUpperCase();
var jsName = property[0];
var params = property[1];
var line = name;
var paramName;
for (paramName in params) {
var value = params[paramName];
/* istanbul ignore else */
if (params.hasOwnProperty(paramName)) {
var multiValue = (paramName in designSet.param) && designSet.param[paramName].multiValue;
if (multiValue && Array.isArray(value)) {
if (designSet.param[paramName].multiValueSeparateDQuote) {
multiValue = '"' + multiValue + '"';
}
value = value.map(stringify._rfc6868Unescape);
value = stringify.multiValue(value, multiValue, "unknown", null, designSet);
} else {
value = stringify._rfc6868Unescape(value);
}
line += ';' + paramName.toUpperCase();
line += '=' + stringify.propertyValue(value);
}
}
if (property.length === 3) {
// If there are no values, we must assume a blank value
return line + ':';
}
var valueType = property[2];
if (!designSet) {
designSet = design.defaultSet;
}
var propDetails;
var multiValue = false;
var structuredValue = false;
var isDefault = false;
if (jsName in designSet.property) {
propDetails = designSet.property[jsName];
if ('multiValue' in propDetails) {
multiValue = propDetails.multiValue;
}
if (('structuredValue' in propDetails) && Array.isArray(property[3])) {
structuredValue = propDetails.structuredValue;
}
if ('defaultType' in propDetails) {
if (valueType === propDetails.defaultType) {
isDefault = true;
}
} else {
if (valueType === DEFAULT_VALUE_TYPE) {
isDefault = true;
}
}
} else {
if (valueType === DEFAULT_VALUE_TYPE) {
isDefault = true;
}
}
// push the VALUE property if type is not the default
// for the current property.
if (!isDefault) {
// value will never contain ;/:/, so we don't escape it here.
line += ';VALUE=' + valueType.toUpperCase();
}
line += ':';
if (multiValue && structuredValue) {
line += stringify.multiValue(
property[3], structuredValue, valueType, multiValue, designSet, structuredValue
);
} else if (multiValue) {
line += stringify.multiValue(
property.slice(3), multiValue, valueType, null, designSet, false
);
} else if (structuredValue) {
line += stringify.multiValue(
property[3], structuredValue, valueType, null, designSet, structuredValue
);
} else {
line += stringify.value(property[3], valueType, designSet, false);
}
return noFold ? line : ICAL.helpers.foldline(line);
};
/**
* Handles escaping of property values that may contain:
*
* COLON (:), SEMICOLON (;), or COMMA (,)
*
* If any of the above are present the result is wrapped
* in double quotes.
*
* @function ICAL.stringify.propertyValue
* @param {String} value Raw property value
* @return {String} Given or escaped value when needed
*/
stringify.propertyValue = function(value) {
if ((helpers.unescapedIndexOf(value, ',') === -1) &&
(helpers.unescapedIndexOf(value, ':') === -1) &&
(helpers.unescapedIndexOf(value, ';') === -1)) {
return value;
}
return '"' + value + '"';
};
/**
* Converts an array of ical values into a single
* string based on a type and a delimiter value (like ",").
*
* @function ICAL.stringify.multiValue
* @param {Array} values List of values to convert
* @param {String} delim Used to join the values (",", ";", ":")
* @param {String} type Lowecase ical value type
* (like boolean, date-time, etc..)
* @param {?String} innerMulti If set, each value will again be processed
* Used for structured values
* @param {ICAL.design.designSet} designSet
* The design data to use for this property
*
* @return {String} iCalendar/vCard string for value
*/
stringify.multiValue = function(values, delim, type, innerMulti, designSet, structuredValue) {
var result = '';
var len = values.length;
var i = 0;
for (; i < len; i++) {
if (innerMulti && Array.isArray(values[i])) {
result += stringify.multiValue(values[i], innerMulti, type, null, designSet, structuredValue);
} else {
result += stringify.value(values[i], type, designSet, structuredValue);
}
if (i !== (len - 1)) {
result += delim;
}
}
return result;
};
/**
* Processes a single ical value runs the associated "toICAL" method from the
* design value type if available to convert the value.
*
* @function ICAL.stringify.value
* @param {String|Number} value A formatted value
* @param {String} type Lowercase iCalendar/vCard value type
* (like boolean, date-time, etc..)
* @return {String} iCalendar/vCard value for single value
*/
stringify.value = function(value, type, designSet, structuredValue) {
if (type in designSet.value && 'toICAL' in designSet.value[type]) {
return designSet.value[type].toICAL(value, structuredValue);
}
return value;
};
/**
* Internal helper for rfc6868. Exposing this on ICAL.stringify so that
* hackers can disable the rfc6868 parsing if the really need to.
*
* @param {String} val The value to unescape
* @return {String} The escaped value
*/
stringify._rfc6868Unescape = function(val) {
return val.replace(/[\n^"]/g, function(x) {
return RFC6868_REPLACE_MAP[x];
});
};
var RFC6868_REPLACE_MAP = { '"': "^'", "\n": "^n", "^": "^^" };
return stringify;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* Contains various functions to parse iCalendar and vCard data.
* @namespace
*/
ICAL.parse = (function() {
'use strict';
var CHAR = /[^ \t]/;
var MULTIVALUE_DELIMITER = ',';
var VALUE_DELIMITER = ':';
var PARAM_DELIMITER = ';';
var PARAM_NAME_DELIMITER = '=';
var DEFAULT_VALUE_TYPE = 'unknown';
var DEFAULT_PARAM_TYPE = 'text';
var design = ICAL.design;
var helpers = ICAL.helpers;
/**
* An error that occurred during parsing.
*
* @param {String} message The error message
* @memberof ICAL.parse
* @extends {Error}
* @class
*/
function ParserError(message) {
this.message = message;
this.name = 'ParserError';
try {
throw new Error();
} catch (e) {
if (e.stack) {
var split = e.stack.split('\n');
split.shift();
this.stack = split.join('\n');
}
}
}
ParserError.prototype = Error.prototype;
/**
* Parses iCalendar or vCard data into a raw jCal object. Consult
* documentation on the {@tutorial layers|layers of parsing} for more
* details.
*
* @function ICAL.parse
* @variation function
* @todo Fix the API to be more clear on the return type
* @param {String} input The string data to parse
* @return {Object|Object[]} A single jCal object, or an array thereof
*/
function parser(input) {
var state = {};
var root = state.component = [];
state.stack = [root];
parser._eachLine(input, function(err, line) {
parser._handleContentLine(line, state);
});
// when there are still items on the stack
// throw a fatal error, a component was not closed
// correctly in that case.
if (state.stack.length > 1) {
throw new ParserError(
'invalid ical body. component began but did not end'
);
}
state = null;
return (root.length == 1 ? root[0] : root);
}
/**
* Parse an iCalendar property value into the jCal for a single property
*
* @function ICAL.parse.property
* @param {String} str
* The iCalendar property string to parse
* @param {ICAL.design.designSet=} designSet
* The design data to use for this property
* @return {Object}
* The jCal Object containing the property
*/
parser.property = function(str, designSet) {
var state = {
component: [[], []],
designSet: designSet || design.defaultSet
};
parser._handleContentLine(str, state);
return state.component[1][0];
};
/**
* Convenience method to parse a component. You can use ICAL.parse() directly
* instead.
*
* @function ICAL.parse.component
* @see ICAL.parse(function)
* @param {String} str The iCalendar component string to parse
* @return {Object} The jCal Object containing the component
*/
parser.component = function(str) {
return parser(str);
};
// classes & constants
parser.ParserError = ParserError;
/**
* The state for parsing content lines from an iCalendar/vCard string.
*
* @private
* @memberof ICAL.parse
* @typedef {Object} parserState
* @property {ICAL.design.designSet} designSet The design set to use for parsing
* @property {ICAL.Component[]} stack The stack of components being processed
* @property {ICAL.Component} component The currently active component
*/
/**
* Handles a single line of iCalendar/vCard, updating the state.
*
* @private
* @function ICAL.parse._handleContentLine
* @param {String} line The content line to process
* @param {ICAL.parse.parserState} The current state of the line parsing
*/
parser._handleContentLine = function(line, state) {
// break up the parts of the line
var valuePos = line.indexOf(VALUE_DELIMITER);
var paramPos = line.indexOf(PARAM_DELIMITER);
var lastParamIndex;
var lastValuePos;
// name of property or begin/end
var name;
var value;
// params is only overridden if paramPos !== -1.
// we can't do params = params || {} later on
// because it sacrifices ops.
var params = {};
/**
* Different property cases
*
*
* 1. RRULE:FREQ=foo
* // FREQ= is not a param but the value
*
* 2. ATTENDEE;ROLE=REQ-PARTICIPANT;
* // ROLE= is a param because : has not happened yet
*/
// when the parameter delimiter is after the
// value delimiter then its not a parameter.
if ((paramPos !== -1 && valuePos !== -1)) {
// when the parameter delimiter is after the
// value delimiter then its not a parameter.
if (paramPos > valuePos) {
paramPos = -1;
}
}
var parsedParams;
if (paramPos !== -1) {
name = line.substring(0, paramPos).toLowerCase();
parsedParams = parser._parseParameters(line.substring(paramPos), 0, state.designSet);
if (parsedParams[2] == -1) {
throw new ParserError("Invalid parameters in '" + line + "'");
}
params = parsedParams[0];
lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos;
if ((lastValuePos =
line.substring(lastParamIndex).indexOf(VALUE_DELIMITER)) !== -1) {
value = line.substring(lastParamIndex + lastValuePos + 1);
} else {
throw new ParserError("Missing parameter value in '" + line + "'");
}
} else if (valuePos !== -1) {
// without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC)
name = line.substring(0, valuePos).toLowerCase();
value = line.substring(valuePos + 1);
if (name === 'begin') {
var newComponent = [value.toLowerCase(), [], []];
if (state.stack.length === 1) {
state.component.push(newComponent);
} else {
state.component[2].push(newComponent);
}
state.stack.push(state.component);
state.component = newComponent;
if (!state.designSet) {
state.designSet = design.getDesignSet(state.component[0]);
}
return;
} else if (name === 'end') {
state.component = state.stack.pop();
return;
}
// If its not begin/end, then this is a property with an empty value,
// which should be considered valid.
} else {
/**
* Invalid line.
* The rational to throw an error is we will
* never be certain that the rest of the file
* is sane and its unlikely that we can serialize
* the result correctly either.
*/
throw new ParserError(
'invalid line (no token ";" or ":") "' + line + '"'
);
}
var valueType;
var multiValue = false;
var structuredValue = false;
var propertyDetails;
if (name in state.designSet.property) {
propertyDetails = state.designSet.property[name];
if ('multiValue' in propertyDetails) {
multiValue = propertyDetails.multiValue;
}
if ('structuredValue' in propertyDetails) {
structuredValue = propertyDetails.structuredValue;
}
if (value && 'detectType' in propertyDetails) {
valueType = propertyDetails.detectType(value);
}
}
// attempt to determine value
if (!valueType) {
if (!('value' in params)) {
if (propertyDetails) {
valueType = propertyDetails.defaultType;
} else {
valueType = DEFAULT_VALUE_TYPE;
}
} else {
// possible to avoid this?
valueType = params.value.toLowerCase();
}
}
delete params.value;
/**
* Note on `var result` juggling:
*
* I observed that building the array in pieces has adverse
* effects on performance, so where possible we inline the creation.
* Its a little ugly but resulted in ~2000 additional ops/sec.
*/
var result;
if (multiValue && structuredValue) {
value = parser._parseMultiValue(value, structuredValue, valueType, [], multiValue, state.designSet, structuredValue);
result = [name, params, valueType, value];
} else if (multiValue) {
result = [name, params, valueType];
parser._parseMultiValue(value, multiValue, valueType, result, null, state.designSet, false);
} else if (structuredValue) {
value = parser._parseMultiValue(value, structuredValue, valueType, [], null, state.designSet, structuredValue);
result = [name, params, valueType, value];
} else {
value = parser._parseValue(value, valueType, state.designSet, false);
result = [name, params, valueType, value];
}
// rfc6350 requires that in vCard 4.0 the first component is the VERSION
// component with as value 4.0, note that 3.0 does not have this requirement.
if (state.component[0] === 'vcard' && state.component[1].length === 0 &&
!(name === 'version' && value === '4.0')) {
state.designSet = design.getDesignSet("vcard3");
}
state.component[1].push(result);
};
/**
* Parse a value from the raw value into the jCard/jCal value.
*
* @private
* @function ICAL.parse._parseValue
* @param {String} value Original value
* @param {String} type Type of value
* @param {Object} designSet The design data to use for this value
* @return {Object} varies on type
*/
parser._parseValue = function(value, type, designSet, structuredValue) {
if (type in designSet.value && 'fromICAL' in designSet.value[type]) {
return designSet.value[type].fromICAL(value, structuredValue);
}
return value;
};
/**
* Parse parameters from a string to object.
*
* @function ICAL.parse._parseParameters
* @private
* @param {String} line A single unfolded line
* @param {Numeric} start Position to start looking for properties
* @param {Object} designSet The design data to use for this property
* @return {Object} key/value pairs
*/
parser._parseParameters = function(line, start, designSet) {
var lastParam = start;
var pos = 0;
var delim = PARAM_NAME_DELIMITER;
var result = {};
var name, lcname;
var value, valuePos = -1;
var type, multiValue, mvdelim;
// find the next '=' sign
// use lastParam and pos to find name
// check if " is used if so get value from "->"
// then increment pos to find next ;
while ((pos !== false) &&
(pos = helpers.unescapedIndexOf(line, delim, pos + 1)) !== -1) {
name = line.substr(lastParam + 1, pos - lastParam - 1);
if (name.length == 0) {
throw new ParserError("Empty parameter name in '" + line + "'");
}
lcname = name.toLowerCase();
if (lcname in designSet.param && designSet.param[lcname].valueType) {
type = designSet.param[lcname].valueType;
} else {
type = DEFAULT_PARAM_TYPE;
}
if (lcname in designSet.param) {
multiValue = designSet.param[lcname].multiValue;
if (designSet.param[lcname].multiValueSeparateDQuote) {
mvdelim = parser._rfc6868Escape('"' + multiValue + '"');
}
}
var nextChar = line[pos + 1];
if (nextChar === '"') {
valuePos = pos + 2;
pos = helpers.unescapedIndexOf(line, '"', valuePos);
if (multiValue && pos != -1) {
var extendedValue = true;
while (extendedValue) {
if (line[pos + 1] == multiValue && line[pos + 2] == '"') {
pos = helpers.unescapedIndexOf(line, '"', pos + 3);
} else {
extendedValue = false;
}
}
}
if (pos === -1) {
throw new ParserError(
'invalid line (no matching double quote) "' + line + '"'
);
}
value = line.substr(valuePos, pos - valuePos);
lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos);
if (lastParam === -1) {
pos = false;
}
} else {
valuePos = pos + 1;
// move to next ";"
var nextPos = helpers.unescapedIndexOf(line, PARAM_DELIMITER, valuePos);
var propValuePos = helpers.unescapedIndexOf(line, VALUE_DELIMITER, valuePos);
if (propValuePos !== -1 && nextPos > propValuePos) {
// this is a delimiter in the property value, let's stop here
nextPos = propValuePos;
pos = false;
} else if (nextPos === -1) {
// no ";"
if (propValuePos === -1) {
nextPos = line.length;
} else {
nextPos = propValuePos;
}
pos = false;
} else {
lastParam = nextPos;
pos = nextPos;
}
value = line.substr(valuePos, nextPos - valuePos);
}
value = parser._rfc6868Escape(value);
if (multiValue) {
var delimiter = mvdelim || multiValue;
result[lcname] = parser._parseMultiValue(value, delimiter, type, [], null, designSet);
} else {
result[lcname] = parser._parseValue(value, type, designSet);
}
}
return [result, value, valuePos];
};
/**
* Internal helper for rfc6868. Exposing this on ICAL.parse so that
* hackers can disable the rfc6868 parsing if the really need to.
*
* @function ICAL.parse._rfc6868Escape
* @param {String} val The value to escape
* @return {String} The escaped value
*/
parser._rfc6868Escape = function(val) {
return val.replace(/\^['n^]/g, function(x) {
return RFC6868_REPLACE_MAP[x];
});
};
var RFC6868_REPLACE_MAP = { "^'": '"', "^n": "\n", "^^": "^" };
/**
* Parse a multi value string. This function is used either for parsing
* actual multi-value property's values, or for handling parameter values. It
* can be used for both multi-value properties and structured value properties.
*
* @private
* @function ICAL.parse._parseMultiValue
* @param {String} buffer The buffer containing the full value
* @param {String} delim The multi-value delimiter
* @param {String} type The value type to be parsed
* @param {Array.<?>} result The array to append results to, varies on value type
* @param {String} innerMulti The inner delimiter to split each value with
* @param {ICAL.design.designSet} designSet The design data for this value
* @return {?|Array.<?>} Either an array of results, or the first result
*/
parser._parseMultiValue = function(buffer, delim, type, result, innerMulti, designSet, structuredValue) {
var pos = 0;
var lastPos = 0;
var value;
if (delim.length === 0) {
return buffer;
}
// split each piece
while ((pos = helpers.unescapedIndexOf(buffer, delim, lastPos)) !== -1) {
value = buffer.substr(lastPos, pos - lastPos);
if (innerMulti) {
value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
} else {
value = parser._parseValue(value, type, designSet, structuredValue);
}
result.push(value);
lastPos = pos + delim.length;
}
// on the last piece take the rest of string
value = buffer.substr(lastPos);
if (innerMulti) {
value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
} else {
value = parser._parseValue(value, type, designSet, structuredValue);
}
result.push(value);
return result.length == 1 ? result[0] : result;
};
/**
* Process a complete buffer of iCalendar/vCard data line by line, correctly
* unfolding content. Each line will be processed with the given callback
*
* @private
* @function ICAL.parse._eachLine
* @param {String} buffer The buffer to process
* @param {function(?String, String)} callback The callback for each line
*/
parser._eachLine = function(buffer, callback) {
var len = buffer.length;
var lastPos = buffer.search(CHAR);
var pos = lastPos;
var line;
var firstChar;
var newlineOffset;
do {
pos = buffer.indexOf('\n', lastPos) + 1;
if (pos > 1 && buffer[pos - 2] === '\r') {
newlineOffset = 2;
} else {
newlineOffset = 1;
}
if (pos === 0) {
pos = len;
newlineOffset = 0;
}
firstChar = buffer[lastPos];
if (firstChar === ' ' || firstChar === '\t') {
// add to line
line += buffer.substr(
lastPos + 1,
pos - lastPos - (newlineOffset + 1)
);
} else {
if (line)
callback(null, line);
// push line
line = buffer.substr(
lastPos,
pos - lastPos - newlineOffset
);
}
lastPos = pos;
} while (pos !== len);
// extra ending line
line = line.trim();
if (line.length)
callback(null, line);
};
return parser;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* This symbol is further described later on
* @ignore
*/
ICAL.Component = (function() {
'use strict';
var PROPERTY_INDEX = 1;
var COMPONENT_INDEX = 2;
var NAME_INDEX = 0;
/**
* @classdesc
* Wraps a jCal component, adding convenience methods to add, remove and
* update subcomponents and properties.
*
* @class
* @alias ICAL.Component
* @param {Array|String} jCal Raw jCal component data OR name of new
* component
* @param {ICAL.Component} parent Parent component to associate
*/
function Component(jCal, parent) {
if (typeof(jCal) === 'string') {
// jCal spec (name, properties, components)
jCal = [jCal, [], []];
}
// mostly for legacy reasons.
this.jCal = jCal;
this.parent = parent || null;
}
Component.prototype = {
/**
* Hydrated properties are inserted into the _properties array at the same
* position as in the jCal array, so its possible the array contains
* undefined values for unhydrdated properties. To avoid iterating the
* array when checking if all properties have been hydrated, we save the
* count here.
*
* @type {Number}
* @private
*/
_hydratedPropertyCount: 0,
/**
* The same count as for _hydratedPropertyCount, but for subcomponents
*
* @type {Number}
* @private
*/
_hydratedComponentCount: 0,
/**
* The name of this component
* @readonly
*/
get name() {
return this.jCal[NAME_INDEX];
},
/**
* The design set for this component, e.g. icalendar vs vcard
*
* @type {ICAL.design.designSet}
* @private
*/
get _designSet() {
var parentDesign = this.parent && this.parent._designSet;
return parentDesign || ICAL.design.getDesignSet(this.name);
},
_hydrateComponent: function(index) {
if (!this._components) {
this._components = [];
this._hydratedComponentCount = 0;
}
if (this._components[index]) {
return this._components[index];
}
var comp = new Component(
this.jCal[COMPONENT_INDEX][index],
this
);
this._hydratedComponentCount++;
return (this._components[index] = comp);
},
_hydrateProperty: function(index) {
if (!this._properties) {
this._properties = [];
this._hydratedPropertyCount = 0;
}
if (this._properties[index]) {
return this._properties[index];
}
var prop = new ICAL.Property(
this.jCal[PROPERTY_INDEX][index],
this
);
this._hydratedPropertyCount++;
return (this._properties[index] = prop);
},
/**
* Finds first sub component, optionally filtered by name.
*
* @param {String=} name Optional name to filter by
* @return {?ICAL.Component} The found subcomponent
*/
getFirstSubcomponent: function(name) {
if (name) {
var i = 0;
var comps = this.jCal[COMPONENT_INDEX];
var len = comps.length;
for (; i < len; i++) {
if (comps[i][NAME_INDEX] === name) {
var result = this._hydrateComponent(i);
return result;
}
}
} else {
if (this.jCal[COMPONENT_INDEX].length) {
return this._hydrateComponent(0);
}
}
// ensure we return a value (strict mode)
return null;
},
/**
* Finds all sub components, optionally filtering by name.
*
* @param {String=} name Optional name to filter by
* @return {ICAL.Component[]} The found sub components
*/
getAllSubcomponents: function(name) {
var jCalLen = this.jCal[COMPONENT_INDEX].length;
var i = 0;
if (name) {
var comps = this.jCal[COMPONENT_INDEX];
var result = [];
for (; i < jCalLen; i++) {
if (name === comps[i][NAME_INDEX]) {
result.push(
this._hydrateComponent(i)
);
}
}
return result;
} else {
if (!this._components ||
(this._hydratedComponentCount !== jCalLen)) {
for (; i < jCalLen; i++) {
this._hydrateComponent(i);
}
}
return this._components || [];
}
},
/**
* Returns true when a named property exists.
*
* @param {String} name The property name
* @return {Boolean} True, when property is found
*/
hasProperty: function(name) {
var props = this.jCal[PROPERTY_INDEX];
var len = props.length;
var i = 0;
for (; i < len; i++) {
// 0 is property name
if (props[i][NAME_INDEX] === name) {
return true;
}
}
return false;
},
/**
* Finds the first property, optionally with the given name.
*
* @param {String=} name Lowercase property name
* @return {?ICAL.Property} The found property
*/
getFirstProperty: function(name) {
if (name) {
var i = 0;
var props = this.jCal[PROPERTY_INDEX];
var len = props.length;
for (; i < len; i++) {
if (props[i][NAME_INDEX] === name) {
var result = this._hydrateProperty(i);
return result;
}
}
} else {
if (this.jCal[PROPERTY_INDEX].length) {
return this._hydrateProperty(0);
}
}
return null;
},
/**
* Returns first property's value, if available.
*
* @param {String=} name Lowercase property name
* @return {?String} The found property value.
*/
getFirstPropertyValue: function(name) {
var prop = this.getFirstProperty(name);
if (prop) {
return prop.getFirstValue();
}
return null;
},
/**
* Get all properties in the component, optionally filtered by name.
*
* @param {String=} name Lowercase property name
* @return {ICAL.Property[]} List of properties
*/
getAllProperties: function(name) {
var jCalLen = this.jCal[PROPERTY_INDEX].length;
var i = 0;
if (name) {
var props = this.jCal[PROPERTY_INDEX];
var result = [];
for (; i < jCalLen; i++) {
if (name === props[i][NAME_INDEX]) {
result.push(
this._hydrateProperty(i)
);
}
}
return result;
} else {
if (!this._properties ||
(this._hydratedPropertyCount !== jCalLen)) {
for (; i < jCalLen; i++) {
this._hydrateProperty(i);
}
}
return this._properties || [];
}
},
_removeObjectByIndex: function(jCalIndex, cache, index) {
cache = cache || [];
// remove cached version
if (cache[index]) {
var obj = cache[index];
if ("parent" in obj) {
obj.parent = null;
}
}
cache.splice(index, 1);
// remove it from the jCal
this.jCal[jCalIndex].splice(index, 1);
},
_removeObject: function(jCalIndex, cache, nameOrObject) {
var i = 0;
var objects = this.jCal[jCalIndex];
var len = objects.length;
var cached = this[cache];
if (typeof(nameOrObject) === 'string') {
for (; i < len; i++) {
if (objects[i][NAME_INDEX] === nameOrObject) {
this._removeObjectByIndex(jCalIndex, cached, i);
return true;
}
}
} else if (cached) {
for (; i < len; i++) {
if (cached[i] && cached[i] === nameOrObject) {
this._removeObjectByIndex(jCalIndex, cached, i);
return true;
}
}
}
return false;
},
_removeAllObjects: function(jCalIndex, cache, name) {
var cached = this[cache];
// Unfortunately we have to run through all children to reset their
// parent property.
var objects = this.jCal[jCalIndex];
var i = objects.length - 1;
// descending search required because splice
// is used and will effect the indices.
for (; i >= 0; i--) {
if (!name || objects[i][NAME_INDEX] === name) {
this._removeObjectByIndex(jCalIndex, cached, i);
}
}
},
/**
* Adds a single sub component.
*
* @param {ICAL.Component} component The component to add
* @return {ICAL.Component} The passed in component
*/
addSubcomponent: function(component) {
if (!this._components) {
this._components = [];
this._hydratedComponentCount = 0;
}
if (component.parent) {
component.parent.removeSubcomponent(component);
}
var idx = this.jCal[COMPONENT_INDEX].push(component.jCal);
this._components[idx - 1] = component;
this._hydratedComponentCount++;
component.parent = this;
return component;
},
/**
* Removes a single component by name or the instance of a specific
* component.
*
* @param {ICAL.Component|String} nameOrComp Name of component, or component
* @return {Boolean} True when comp is removed
*/
removeSubcomponent: function(nameOrComp) {
var removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp);
if (removed) {
this._hydratedComponentCount--;
}
return removed;
},
/**
* Removes all components or (if given) all components by a particular
* name.
*
* @param {String=} name Lowercase component name
*/
removeAllSubcomponents: function(name) {
var removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name);
this._hydratedComponentCount = 0;
return removed;
},
/**
* Adds an {@link ICAL.Property} to the component.
*
* @param {ICAL.Property} property The property to add
* @return {ICAL.Property} The passed in property
*/
addProperty: function(property) {
if (!(property instanceof ICAL.Property)) {
throw new TypeError('must instance of ICAL.Property');
}
if (!this._properties) {
this._properties = [];
this._hydratedPropertyCount = 0;
}
if (property.parent) {
property.parent.removeProperty(property);
}
var idx = this.jCal[PROPERTY_INDEX].push(property.jCal);
this._properties[idx - 1] = property;
this._hydratedPropertyCount++;
property.parent = this;
return property;
},
/**
* Helper method to add a property with a value to the component.
*
* @param {String} name Property name to add
* @param {String|Number|Object} value Property value
* @return {ICAL.Property} The created property
*/
addPropertyWithValue: function(name, value) {
var prop = new ICAL.Property(name);
prop.setValue(value);
this.addProperty(prop);
return prop;
},
/**
* Helper method that will update or create a property of the given name
* and sets its value. If multiple properties with the given name exist,
* only the first is updated.
*
* @param {String} name Property name to update
* @param {String|Number|Object} value Property value
* @return {ICAL.Property} The created property
*/
updatePropertyWithValue: function(name, value) {
var prop = this.getFirstProperty(name);
if (prop) {
prop.setValue(value);
} else {
prop = this.addPropertyWithValue(name, value);
}
return prop;
},
/**
* Removes a single property by name or the instance of the specific
* property.
*
* @param {String|ICAL.Property} nameOrProp Property name or instance to remove
* @return {Boolean} True, when deleted
*/
removeProperty: function(nameOrProp) {
var removed = this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp);
if (removed) {
this._hydratedPropertyCount--;
}
return removed;
},
/**
* Removes all properties associated with this component, optionally
* filtered by name.
*
* @param {String=} name Lowercase property name
* @return {Boolean} True, when deleted
*/
removeAllProperties: function(name) {
var removed = this._removeAllObjects(PROPERTY_INDEX, '_properties', name);
this._hydratedPropertyCount = 0;
return removed;
},
/**
* Returns the Object representation of this component. The returned object
* is a live jCal object and should be cloned if modified.
* @return {Object}
*/
toJSON: function() {
return this.jCal;
},
/**
* The string representation of this component.
* @return {String}
*/
toString: function() {
return ICAL.stringify.component(
this.jCal, this._designSet
);
}
};
/**
* Create an {@link ICAL.Component} by parsing the passed iCalendar string.
*
* @param {String} str The iCalendar string to parse
*/
Component.fromString = function(str) {
return new Component(ICAL.parse.component(str));
};
return Component;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* This symbol is further described later on
* @ignore
*/
ICAL.Property = (function() {
'use strict';
var NAME_INDEX = 0;
var PROP_INDEX = 1;
var TYPE_INDEX = 2;
var VALUE_INDEX = 3;
var design = ICAL.design;
/**
* @classdesc
* Provides a layer on top of the raw jCal object for manipulating a single
* property, with its parameters and value.
*
* @description
* Its important to note that mutations done in the wrapper
* directly mutate the jCal object used to initialize.
*
* Can also be used to create new properties by passing
* the name of the property (as a String).
*
* @class
* @alias ICAL.Property
* @param {Array|String} jCal Raw jCal representation OR
* the new name of the property
*
* @param {ICAL.Component=} parent Parent component
*/
function Property(jCal, parent) {
this._parent = parent || null;
if (typeof(jCal) === 'string') {
// We are creating the property by name and need to detect the type
this.jCal = [jCal, {}, design.defaultType];
this.jCal[TYPE_INDEX] = this.getDefaultType();
} else {
this.jCal = jCal;
}
this._updateType();
}
Property.prototype = {
/**
* The value type for this property
* @readonly
* @type {String}
*/
get type() {
return this.jCal[TYPE_INDEX];
},
/**
* The name of this property, in lowercase.
* @readonly
* @type {String}
*/
get name() {
return this.jCal[NAME_INDEX];
},
/**
* The parent component for this property.
* @type {ICAL.Component}
*//*
get parent() {
return this._parent;
},*/
set parent(p) {
// Before setting the parent, check if the design set has changed. If it
// has, we later need to update the type if it was unknown before.
var designSetChanged = !this._parent || (p && p._designSet != this._parent._designSet);
this._parent = p;
if (this.type == design.defaultType && designSetChanged) {
this.jCal[TYPE_INDEX] = this.getDefaultType();
this._updateType();
}
return p;
},
/**
* The design set for this property, e.g. icalendar vs vcard
*
* @type {ICAL.design.designSet}
* @private
*/
get _designSet() {
return this.parent ? this.parent._designSet : design.defaultSet;
},
/**
* Updates the type metadata from the current jCal type and design set.
*
* @private
*/
_updateType: function() {
var designSet = this._designSet;
if (this.type in designSet.value) {
var designType = designSet.value[this.type];
if ('decorate' in designSet.value[this.type]) {
this.isDecorated = true;
} else {
this.isDecorated = false;
}
if (this.name in designSet.property) {
this.isMultiValue = ('multiValue' in designSet.property[this.name]);
this.isStructuredValue = ('structuredValue' in designSet.property[this.name]);
}
}
},
/**
* Hydrate a single value. The act of hydrating means turning the raw jCal
* value into a potentially wrapped object, for example {@link ICAL.Time}.
*
* @private
* @param {Number} index The index of the value to hydrate
* @return {Object} The decorated value.
*/
_hydrateValue: function(index) {
if (this._values && this._values[index]) {
return this._values[index];
}
// for the case where there is no value.
if (this.jCal.length <= (VALUE_INDEX + index)) {
return null;
}
if (this.isDecorated) {
if (!this._values) {
this._values = [];
}
return (this._values[index] = this._decorate(
this.jCal[VALUE_INDEX + index]
));
} else {
return this.jCal[VALUE_INDEX + index];
}
},
/**
* Decorate a single value, returning its wrapped object. This is used by
* the hydrate function to actually wrap the value.
*
* @private
* @param {?} value The value to decorate
* @return {Object} The decorated value
*/
_decorate: function(value) {
return this._designSet.value[this.type].decorate(value, this);
},
/**
* Undecorate a single value, returning its raw jCal data.
*
* @private
* @param {Object} value The value to undecorate
* @return {?} The undecorated value
*/
_undecorate: function(value) {
return this._designSet.value[this.type].undecorate(value, this);
},
/**
* Sets the value at the given index while also hydrating it. The passed
* value can either be a decorated or undecorated value.
*
* @private
* @param {?} value The value to set
* @param {Number} index The index to set it at
*/
_setDecoratedValue: function(value, index) {
if (!this._values) {
this._values = [];
}
if (typeof(value) === 'object' && 'icaltype' in value) {
// decorated value
this.jCal[VALUE_INDEX + index] = this._undecorate(value);
this._values[index] = value;
} else {
// undecorated value
this.jCal[VALUE_INDEX + index] = value;
this._values[index] = this._decorate(value);
}
},
/**
* Gets a parameter on the property.
*
* @param {String} name Property name (lowercase)
* @return {Array|String} Property value
*/
getParameter: function(name) {
if (name in this.jCal[PROP_INDEX]) {
return this.jCal[PROP_INDEX][name];
} else {
return undefined;
}
},
/**
* Sets a parameter on the property.
*
* @param {String} name The parameter name
* @param {Array|String} value The parameter value
*/
setParameter: function(name, value) {
var lcname = name.toLowerCase();
if (typeof value === "string" &&
lcname in this._designSet.param &&
'multiValue' in this._designSet.param[lcname]) {
value = [value];
}
this.jCal[PROP_INDEX][name] = value;
},
/**
* Removes a parameter
*
* @param {String} name The parameter name
*/
removeParameter: function(name) {
delete this.jCal[PROP_INDEX][name];
},
/**
* Get the default type based on this property's name.
*
* @return {String} The default type for this property
*/
getDefaultType: function() {
var name = this.jCal[NAME_INDEX];
var designSet = this._designSet;
if (name in designSet.property) {
var details = designSet.property[name];
if ('defaultType' in details) {
return details.defaultType;
}
}
return design.defaultType;
},
/**
* Sets type of property and clears out any existing values of the current
* type.
*
* @param {String} type New iCAL type (see design.*.values)
*/
resetType: function(type) {
this.removeAllValues();
this.jCal[TYPE_INDEX] = type;
this._updateType();
},
/**
* Finds the first property value.
*
* @return {String} First property value
*/
getFirstValue: function() {
return this._hydrateValue(0);
},
/**
* Gets all values on the property.
*
* NOTE: this creates an array during each call.
*
* @return {Array} List of values
*/
getValues: function() {
var len = this.jCal.length - VALUE_INDEX;
if (len < 1) {
// its possible for a property to have no value.
return [];
}
var i = 0;
var result = [];
for (; i < len; i++) {
result[i] = this._hydrateValue(i);
}
return result;
},
/**
* Removes all values from this property
*/
removeAllValues: function() {
if (this._values) {
this._values.length = 0;
}
this.jCal.length = 3;
},
/**
* Sets the values of the property. Will overwrite the existing values.
* This can only be used for multi-value properties.
*
* @param {Array} values An array of values
*/
setValues: function(values) {
if (!this.isMultiValue) {
throw new Error(
this.name + ': does not not support mulitValue.\n' +
'override isMultiValue'
);
}
var len = values.length;
var i = 0;
this.removeAllValues();
if (len > 0 &&
typeof(values[0]) === 'object' &&
'icaltype' in values[0]) {
this.resetType(values[0].icaltype);
}
if (this.isDecorated) {
for (; i < len; i++) {
this._setDecoratedValue(values[i], i);
}
} else {
for (; i < len; i++) {
this.jCal[VALUE_INDEX + i] = values[i];
}
}
},
/**
* Sets the current value of the property. If this is a multi-value
* property, all other values will be removed.
*
* @param {String|Object} value New property value.
*/
setValue: function(value) {
this.removeAllValues();
if (typeof(value) === 'object' && 'icaltype' in value) {
this.resetType(value.icaltype);
}
if (this.isDecorated) {
this._setDecoratedValue(value, 0);
} else {
this.jCal[VALUE_INDEX] = value;
}
},
/**
* Returns the Object representation of this component. The returned object
* is a live jCal object and should be cloned if modified.
* @return {Object}
*/
toJSON: function() {
return this.jCal;
},
/**
* The string representation of this component.
* @return {String}
*/
toICALString: function() {
return ICAL.stringify.property(
this.jCal, this._designSet, true
);
}
};
/**
* Create an {@link ICAL.Property} by parsing the passed iCalendar string.
*
* @param {String} str The iCalendar string to parse
* @param {ICAL.design.designSet=} designSet The design data to use for this property
* @return {ICAL.Property} The created iCalendar property
*/
Property.fromString = function(str, designSet) {
return new Property(ICAL.parse.property(str, designSet));
};
return Property;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* This symbol is further described later on
* @ignore
*/
ICAL.UtcOffset = (function() {
/**
* @classdesc
* This class represents the "duration" value type, with various calculation
* and manipulation methods.
*
* @class
* @alias ICAL.UtcOffset
* @param {Object} aData An object with members of the utc offset
* @param {Number=} aData.hours The hours for the utc offset
* @param {Number=} aData.minutes The minutes in the utc offset
* @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1
*/
function UtcOffset(aData) {
this.fromData(aData);
}
UtcOffset.prototype = {
/**
* The hours in the utc-offset
* @type {Number}
*/
hours: 0,
/**
* The minutes in the utc-offset
* @type {Number}
*/
minutes: 0,
/**
* The sign of the utc offset, 1 for positive offset, -1 for negative
* offsets.
* @type {Number}
*/
factor: 1,
/**
* The type name, to be used in the jCal object.
* @constant
* @type {String}
* @default "utc-offset"
*/
icaltype: "utc-offset",
/**
* Returns a clone of the utc offset object.
*
* @return {ICAL.UtcOffset} The cloned object
*/
clone: function() {
return ICAL.UtcOffset.fromSeconds(this.toSeconds());
},
/**
* Sets up the current instance using members from the passed data object.
*
* @param {Object} aData An object with members of the utc offset
* @param {Number=} aData.hours The hours for the utc offset
* @param {Number=} aData.minutes The minutes in the utc offset
* @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1
*/
fromData: function(aData) {
if (aData) {
for (var key in aData) {
/* istanbul ignore else */
if (aData.hasOwnProperty(key)) {
this[key] = aData[key];
}
}
}
this._normalize();
},
/**
* Sets up the current instance from the given seconds value. The seconds
* value is truncated to the minute. Offsets are wrapped when the world
* ends, the hour after UTC+14:00 is UTC-12:00.
*
* @param {Number} aSeconds The seconds to convert into an offset
*/
fromSeconds: function(aSeconds) {
var secs = Math.abs(aSeconds);
this.factor = aSeconds < 0 ? -1 : 1;
this.hours = ICAL.helpers.trunc(secs / 3600);
secs -= (this.hours * 3600);
this.minutes = ICAL.helpers.trunc(secs / 60);
return this;
},
/**
* Convert the current offset to a value in seconds
*
* @return {Number} The offset in seconds
*/
toSeconds: function() {
return this.factor * (60 * this.minutes + 3600 * this.hours);
},
/**
* Compare this utc offset with another one.
*
* @param {ICAL.UtcOffset} other The other offset to compare with
* @return {Number} -1, 0 or 1 for less/equal/greater
*/
compare: function icaltime_compare(other) {
var a = this.toSeconds();
var b = other.toSeconds();
return (a > b) - (b > a);
},
_normalize: function() {
// Range: 97200 seconds (with 1 hour inbetween)
var secs = this.toSeconds();
var factor = this.factor;
while (secs < -43200) { // = UTC-12:00
secs += 97200;
}
while (secs > 50400) { // = UTC+14:00
secs -= 97200;
}
this.fromSeconds(secs);
// Avoid changing the factor when on zero seconds
if (secs == 0) {
this.factor = factor;
}
},
/**
* The iCalendar string representation of this utc-offset.
* @return {String}
*/
toICALString: function() {
return ICAL.design.icalendar.value['utc-offset'].toICAL(this.toString());
},
/**
* The string representation of this utc-offset.
* @return {String}
*/
toString: function toString() {
return (this.factor == 1 ? "+" : "-") +
ICAL.helpers.pad2(this.hours) + ':' +
ICAL.helpers.pad2(this.minutes);
}
};
/**
* Creates a new {@link ICAL.UtcOffset} instance from the passed string.
*
* @param {String} aString The string to parse
* @return {ICAL.Duration} The created utc-offset instance
*/
UtcOffset.fromString = function(aString) {
// -05:00
var options = {};
//TODO: support seconds per rfc5545 ?
options.factor = (aString[0] === '+') ? 1 : -1;
options.hours = ICAL.helpers.strictParseInt(aString.substr(1, 2));
options.minutes = ICAL.helpers.strictParseInt(aString.substr(4, 2));
return new ICAL.UtcOffset(options);
};
/**
* Creates a new {@link ICAL.UtcOffset} instance from the passed seconds
* value.
*
* @param {Number} aSeconds The number of seconds to convert
*/
UtcOffset.fromSeconds = function(aSeconds) {
var instance = new UtcOffset();
instance.fromSeconds(aSeconds);
return instance;
};
return UtcOffset;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* This symbol is further described later on
* @ignore
*/
ICAL.Binary = (function() {
/**
* @classdesc
* Represents the BINARY value type, which contains extra methods for
* encoding and decoding.
*
* @class
* @alias ICAL.Binary
* @param {String} aValue The binary data for this value
*/
function Binary(aValue) {
this.value = aValue;
}
Binary.prototype = {
/**
* The type name, to be used in the jCal object.
* @default "binary"
* @constant
*/
icaltype: "binary",
/**
* Base64 decode the current value
*
* @return {String} The base64-decoded value
*/
decodeValue: function decodeValue() {
return this._b64_decode(this.value);
},
/**
* Encodes the passed parameter with base64 and sets the internal
* value to the result.
*
* @param {String} aValue The raw binary value to encode
*/
setEncodedValue: function setEncodedValue(aValue) {
this.value = this._b64_encode(aValue);
},
_b64_encode: function base64_encode(data) {
// http://kevin.vanzonneveld.net
// + original by: Tyler Akins (http://rumkin.com)
// + improved by: Bayron Guevara
// + improved by: Thunder.m
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + bugfixed by: Pellentesque Malesuada
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + improved by: Rafał Kukawski (http://kukawski.pl)
// * example 1: base64_encode('Kevin van Zonneveld');
// * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
// mozilla has this native
// - but breaks in 2.0.0.12!
//if (typeof this.window['atob'] == 'function') {
// return atob(data);
//}
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"abcdefghijklmnopqrstuvwxyz0123456789+/=";
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
ac = 0,
enc = "",
tmp_arr = [];
if (!data) {
return data;
}
do { // pack three octets into four hexets
o1 = data.charCodeAt(i++);
o2 = data.charCodeAt(i++);
o3 = data.charCodeAt(i++);
bits = o1 << 16 | o2 << 8 | o3;
h1 = bits >> 18 & 0x3f;
h2 = bits >> 12 & 0x3f;
h3 = bits >> 6 & 0x3f;
h4 = bits & 0x3f;
// use hexets to index into b64, and append result to encoded string
tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
} while (i < data.length);
enc = tmp_arr.join('');
var r = data.length % 3;
return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3);
},
_b64_decode: function base64_decode(data) {
// http://kevin.vanzonneveld.net
// + original by: Tyler Akins (http://rumkin.com)
// + improved by: Thunder.m
// + input by: Aman Gupta
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + bugfixed by: Onno Marsman
// + bugfixed by: Pellentesque Malesuada
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + input by: Brett Zamir (http://brett-zamir.me)
// + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==');
// * returns 1: 'Kevin van Zonneveld'
// mozilla has this native
// - but breaks in 2.0.0.12!
//if (typeof this.window['btoa'] == 'function') {
// return btoa(data);
//}
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"abcdefghijklmnopqrstuvwxyz0123456789+/=";
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
ac = 0,
dec = "",
tmp_arr = [];
if (!data) {
return data;
}
data += '';
do { // unpack four hexets into three octets using index points in b64
h1 = b64.indexOf(data.charAt(i++));
h2 = b64.indexOf(data.charAt(i++));
h3 = b64.indexOf(data.charAt(i++));
h4 = b64.indexOf(data.charAt(i++));
bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
o1 = bits >> 16 & 0xff;
o2 = bits >> 8 & 0xff;
o3 = bits & 0xff;
if (h3 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1);
} else if (h4 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1, o2);
} else {
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
}
} while (i < data.length);
dec = tmp_arr.join('');
return dec;
},
/**
* The string representation of this value
* @return {String}
*/
toString: function() {
return this.value;
}
};
/**
* Creates a binary value from the given string.
*
* @param {String} aString The binary value string
* @return {ICAL.Binary} The binary value instance
*/
Binary.fromString = function(aString) {
return new Binary(aString);
};
return Binary;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
(function() {
/**
* @classdesc
* This class represents the "period" value type, with various calculation
* and manipulation methods.
*
* @description
* The passed data object cannot contain both and end date and a duration.
*
* @class
* @param {Object} aData An object with members of the period
* @param {ICAL.Time=} aData.start The start of the period
* @param {ICAL.Time=} aData.end The end of the period
* @param {ICAL.Duration=} aData.duration The duration of the period
*/
ICAL.Period = function icalperiod(aData) {
this.wrappedJSObject = this;
if (aData && 'start' in aData) {
if (aData.start && !(aData.start instanceof ICAL.Time)) {
throw new TypeError('.start must be an instance of ICAL.Time');
}
this.start = aData.start;
}
if (aData && aData.end && aData.duration) {
throw new Error('cannot accept both end and duration');
}
if (aData && 'end' in aData) {
if (aData.end && !(aData.end instanceof ICAL.Time)) {
throw new TypeError('.end must be an instance of ICAL.Time');
}
this.end = aData.end;
}
if (aData && 'duration' in aData) {
if (aData.duration && !(aData.duration instanceof ICAL.Duration)) {
throw new TypeError('.duration must be an instance of ICAL.Duration');
}
this.duration = aData.duration;
}
};
ICAL.Period.prototype = {
/**
* The start of the period
* @type {ICAL.Time}
*/
start: null,
/**
* The end of the period
* @type {ICAL.Time}
*/
end: null,
/**
* The duration of the period
* @type {ICAL.Duration}
*/
duration: null,
/**
* The class identifier.
* @constant
* @type {String}
* @default "icalperiod"
*/
icalclass: "icalperiod",
/**
* The type name, to be used in the jCal object.
* @constant
* @type {String}
* @default "period"
*/
icaltype: "period",
/**
* Returns a clone of the duration object.
*
* @return {ICAL.Period} The cloned object
*/
clone: function() {
return ICAL.Period.fromData({
start: this.start ? this.start.clone() : null,
end: this.end ? this.end.clone() : null,
duration: this.duration ? this.duration.clone() : null
});
},
/**
* Calculates the duration of the period, either directly or by subtracting
* start from end date.
*
* @return {ICAL.Duration} The calculated duration
*/
getDuration: function duration() {
if (this.duration) {
return this.duration;
} else {
return this.end.subtractDate(this.start);
}
},
/**
* Calculates the end date of the period, either directly or by adding
* duration to start date.
*
* @return {ICAL.Time} The calculated end date
*/
getEnd: function() {
if (this.end) {
return this.end;
} else {
var end = this.start.clone();
end.addDuration(this.duration);
return end;
}
},
/**
* The string representation of this period.
* @return {String}
*/
toString: function toString() {
return this.start + "/" + (this.end || this.duration);
},
/**
* The jCal representation of this period type.
* @return {Object}
*/
toJSON: function() {
return [this.start.toString(), (this.end || this.duration).toString()];
},
/**
* The iCalendar string representation of this period.
* @return {String}
*/
toICALString: function() {
return this.start.toICALString() + "/" +
(this.end || this.duration).toICALString();
}
};
/**
* Creates a new {@link ICAL.Period} instance from the passed string.
*
* @param {String} str The string to parse
* @param {ICAL.Property} prop The property this period will be on
* @return {ICAL.Period} The created period instance
*/
ICAL.Period.fromString = function fromString(str, prop) {
var parts = str.split('/');
if (parts.length !== 2) {
throw new Error(
'Invalid string value: "' + str + '" must contain a "/" char.'
);
}
var options = {
start: ICAL.Time.fromDateTimeString(parts[0], prop)
};
var end = parts[1];
if (ICAL.Duration.isValueString(end)) {
options.duration = ICAL.Duration.fromString(end);
} else {
options.end = ICAL.Time.fromDateTimeString(end, prop);
}
return new ICAL.Period(options);
};
/**
* Creates a new {@link ICAL.Period} instance from the given data object.
* The passed data object cannot contain both and end date and a duration.
*
* @param {Object} aData An object with members of the period
* @param {ICAL.Time=} aData.start The start of the period
* @param {ICAL.Time=} aData.end The end of the period
* @param {ICAL.Duration=} aData.duration The duration of the period
* @return {ICAL.Period} The period instance
*/
ICAL.Period.fromData = function fromData(aData) {
return new ICAL.Period(aData);
};
/**
* Returns a new period instance from the given jCal data array. The first
* member is always the start date string, the second member is either a
* duration or end date string.
*
* @param {Array<String,String>} aData The jCal data array
* @param {ICAL.Property} aProp The property this jCal data is on
* @return {ICAL.Period} The period instance
*/
ICAL.Period.fromJSON = function(aData, aProp) {
if (ICAL.Duration.isValueString(aData[1])) {
return ICAL.Period.fromData({
start: ICAL.Time.fromDateTimeString(aData[0], aProp),
duration: ICAL.Duration.fromString(aData[1])
});
} else {
return ICAL.Period.fromData({
start: ICAL.Time.fromDateTimeString(aData[0], aProp),
end: ICAL.Time.fromDateTimeString(aData[1], aProp)
});
}
};
})();
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
(function() {
var DURATION_LETTERS = /([PDWHMTS]{1,1})/;
/**
* @classdesc
* This class represents the "duration" value type, with various calculation
* and manipulation methods.
*
* @class
* @alias ICAL.Duration
* @param {Object} data An object with members of the duration
* @param {Number} data.weeks Duration in weeks
* @param {Number} data.days Duration in days
* @param {Number} data.hours Duration in hours
* @param {Number} data.minutes Duration in minutes
* @param {Number} data.seconds Duration in seconds
* @param {Boolean} data.isNegative If true, the duration is negative
*/
ICAL.Duration = function icalduration(data) {
this.wrappedJSObject = this;
this.fromData(data);
};
ICAL.Duration.prototype = {
/**
* The weeks in this duration
* @type {Number}
* @default 0
*/
weeks: 0,
/**
* The days in this duration
* @type {Number}
* @default 0
*/
days: 0,
/**
* The days in this duration
* @type {Number}
* @default 0
*/
hours: 0,
/**
* The minutes in this duration
* @type {Number}
* @default 0
*/
minutes: 0,
/**
* The seconds in this duration
* @type {Number}
* @default 0
*/
seconds: 0,
/**
* The seconds in this duration
* @type {Boolean}
* @default false
*/
isNegative: false,
/**
* The class identifier.
* @constant
* @type {String}
* @default "icalduration"
*/
icalclass: "icalduration",
/**
* The type name, to be used in the jCal object.
* @constant
* @type {String}
* @default "duration"
*/
icaltype: "duration",
/**
* Returns a clone of the duration object.
*
* @return {ICAL.Duration} The cloned object
*/
clone: function clone() {
return ICAL.Duration.fromData(this);
},
/**
* The duration value expressed as a number of seconds.
*
* @return {Number} The duration value in seconds
*/
toSeconds: function toSeconds() {
var seconds = this.seconds + 60 * this.minutes + 3600 * this.hours +
86400 * this.days + 7 * 86400 * this.weeks;
return (this.isNegative ? -seconds : seconds);
},
/**
* Reads the passed seconds value into this duration object. Afterwards,
* members like {@link ICAL.Duration#days days} and {@link ICAL.Duration#weeks weeks} will be set up
* accordingly.
*
* @param {Number} aSeconds The duration value in seconds
* @return {ICAL.Duration} Returns this instance
*/
fromSeconds: function fromSeconds(aSeconds) {
var secs = Math.abs(aSeconds);
this.isNegative = (aSeconds < 0);
this.days = ICAL.helpers.trunc(secs / 86400);
// If we have a flat number of weeks, use them.
if (this.days % 7 == 0) {
this.weeks = this.days / 7;
this.days = 0;
} else {
this.weeks = 0;
}
secs -= (this.days + 7 * this.weeks) * 86400;
this.hours = ICAL.helpers.trunc(secs / 3600);
secs -= this.hours * 3600;
this.minutes = ICAL.helpers.trunc(secs / 60);
secs -= this.minutes * 60;
this.seconds = secs;
return this;
},
/**
* Sets up the current instance using members from the passed data object.
*
* @param {Object} aData An object with members of the duration
* @param {Number} aData.weeks Duration in weeks
* @param {Number} aData.days Duration in days
* @param {Number} aData.hours Duration in hours
* @param {Number} aData.minutes Duration in minutes
* @param {Number} aData.seconds Duration in seconds
* @param {Boolean} aData.isNegative If true, the duration is negative
*/
fromData: function fromData(aData) {
var propsToCopy = ["weeks", "days", "hours",
"minutes", "seconds", "isNegative"];
for (var key in propsToCopy) {
/* istanbul ignore if */
if (!propsToCopy.hasOwnProperty(key)) {
continue;
}
var prop = propsToCopy[key];
if (aData && prop in aData) {
this[prop] = aData[prop];
} else {
this[prop] = 0;
}
}
},
/**
* Resets the duration instance to the default values, i.e. PT0S
*/
reset: function reset() {
this.isNegative = false;
this.weeks = 0;
this.days = 0;
this.hours = 0;
this.minutes = 0;
this.seconds = 0;
},
/**
* Compares the duration instance with another one.
*
* @param {ICAL.Duration} aOther The instance to compare with
* @return {Number} -1, 0 or 1 for less/equal/greater
*/
compare: function compare(aOther) {
var thisSeconds = this.toSeconds();
var otherSeconds = aOther.toSeconds();
return (thisSeconds > otherSeconds) - (thisSeconds < otherSeconds);
},
/**
* Normalizes the duration instance. For example, a duration with a value
* of 61 seconds will be normalized to 1 minute and 1 second.
*/
normalize: function normalize() {
this.fromSeconds(this.toSeconds());
},
/**
* The string representation of this duration.
* @return {String}
*/
toString: function toString() {
if (this.toSeconds() == 0) {
return "PT0S";
} else {
var str = "";
if (this.isNegative) str += "-";
str += "P";
if (this.weeks) str += this.weeks + "W";
if (this.days) str += this.days + "D";
if (this.hours || this.minutes || this.seconds) {
str += "T";
if (this.hours) str += this.hours + "H";
if (this.minutes) str += this.minutes + "M";
if (this.seconds) str += this.seconds + "S";
}
return str;
}
},
/**
* The iCalendar string representation of this duration.
* @return {String}
*/
toICALString: function() {
return this.toString();
}
};
/**
* Returns a new ICAL.Duration instance from the passed seconds value.
*
* @param {Number} aSeconds The seconds to create the instance from
* @return {ICAL.Duration} The newly created duration instance
*/
ICAL.Duration.fromSeconds = function icalduration_from_seconds(aSeconds) {
return (new ICAL.Duration()).fromSeconds(aSeconds);
};
/**
* Internal helper function to handle a chunk of a duration.
*
* @param {String} letter type of duration chunk
* @param {String} number numeric value or -/+
* @param {Object} dict target to assign values to
*/
function parseDurationChunk(letter, number, object) {
var type;
switch (letter) {
case 'P':
if (number && number === '-') {
object.isNegative = true;
} else {
object.isNegative = false;
}
// period
break;
case 'D':
type = 'days';
break;
case 'W':
type = 'weeks';
break;
case 'H':
type = 'hours';
break;
case 'M':
type = 'minutes';
break;
case 'S':
type = 'seconds';
break;
default:
// Not a valid chunk
return 0;
}
if (type) {
if (!number && number !== 0) {
throw new Error(
'invalid duration value: Missing number before "' + letter + '"'
);
}
var num = parseInt(number, 10);
if (ICAL.helpers.isStrictlyNaN(num)) {
throw new Error(
'invalid duration value: Invalid number "' + number + '" before "' + letter + '"'
);
}
object[type] = num;
}
return 1;
}
/**
* Checks if the given string is an iCalendar duration value.
*
* @param {String} value The raw ical value
* @return {Boolean} True, if the given value is of the
* duration ical type
*/
ICAL.Duration.isValueString = function(string) {
return (string[0] === 'P' || string[1] === 'P');
};
/**
* Creates a new {@link ICAL.Duration} instance from the passed string.
*
* @param {String} aStr The string to parse
* @return {ICAL.Duration} The created duration instance
*/
ICAL.Duration.fromString = function icalduration_from_string(aStr) {
var pos = 0;
var dict = Object.create(null);
var chunks = 0;
while ((pos = aStr.search(DURATION_LETTERS)) !== -1) {
var type = aStr[pos];
var numeric = aStr.substr(0, pos);
aStr = aStr.substr(pos + 1);
chunks += parseDurationChunk(type, numeric, dict);
}
if (chunks < 2) {
// There must be at least a chunk with "P" and some unit chunk
throw new Error(
'invalid duration value: Not enough duration components in "' + aStr + '"'
);
}
return new ICAL.Duration(dict);
};
/**
* Creates a new ICAL.Duration instance from the given data object.
*
* @param {Object} aData An object with members of the duration
* @param {Number} aData.weeks Duration in weeks
* @param {Number} aData.days Duration in days
* @param {Number} aData.hours Duration in hours
* @param {Number} aData.minutes Duration in minutes
* @param {Number} aData.seconds Duration in seconds
* @param {Boolean} aData.isNegative If true, the duration is negative
* @return {ICAL.Duration} The createad duration instance
*/
ICAL.Duration.fromData = function icalduration_from_data(aData) {
return new ICAL.Duration(aData);
};
})();
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2012 */
(function() {
var OPTIONS = ["tzid", "location", "tznames",
"latitude", "longitude"];
/**
* @classdesc
* Timezone representation, created by passing in a tzid and component.
*
* @example
* var vcalendar;
* var timezoneComp = vcalendar.getFirstSubcomponent('vtimezone');
* var tzid = timezoneComp.getFirstPropertyValue('tzid');
*
* var timezone = new ICAL.Timezone({
* component: timezoneComp,
* tzid
* });
*
* @class
* @param {ICAL.Component|Object} data options for class
* @param {String|ICAL.Component} data.component
* If data is a simple object, then this member can be set to either a
* string containing the component data, or an already parsed
* ICAL.Component
* @param {String} data.tzid The timezone identifier
* @param {String} data.location The timezone locationw
* @param {String} data.tznames An alternative string representation of the
* timezone
* @param {Number} data.latitude The latitude of the timezone
* @param {Number} data.longitude The longitude of the timezone
*/
ICAL.Timezone = function icaltimezone(data) {
this.wrappedJSObject = this;
this.fromData(data);
};
ICAL.Timezone.prototype = {
/**
* Timezone identifier
* @type {String}
*/
tzid: "",
/**
* Timezone location
* @type {String}
*/
location: "",
/**
* Alternative timezone name, for the string representation
* @type {String}
*/
tznames: "",
/**
* The primary latitude for the timezone.
* @type {Number}
*/
latitude: 0.0,
/**
* The primary longitude for the timezone.
* @type {Number}
*/
longitude: 0.0,
/**
* The vtimezone component for this timezone.
* @type {ICAL.Component}
*/
component: null,
/**
* The year this timezone has been expanded to. All timezone transition
* dates until this year are known and can be used for calculation
*
* @private
* @type {Number}
*/
expandedUntilYear: 0,
/**
* The class identifier.
* @constant
* @type {String}
* @default "icaltimezone"
*/
icalclass: "icaltimezone",
/**
* Sets up the current instance using members from the passed data object.
*
* @param {ICAL.Component|Object} aData options for class
* @param {String|ICAL.Component} aData.component
* If aData is a simple object, then this member can be set to either a
* string containing the component data, or an already parsed
* ICAL.Component
* @param {String} aData.tzid The timezone identifier
* @param {String} aData.location The timezone locationw
* @param {String} aData.tznames An alternative string representation of the
* timezone
* @param {Number} aData.latitude The latitude of the timezone
* @param {Number} aData.longitude The longitude of the timezone
*/
fromData: function fromData(aData) {
this.expandedUntilYear = 0;
this.changes = [];
if (aData instanceof ICAL.Component) {
// Either a component is passed directly
this.component = aData;
} else {
// Otherwise the component may be in the data object
if (aData && "component" in aData) {
if (typeof aData.component == "string") {
// If a string was passed, parse it as a component
var jCal = ICAL.parse(aData.component);
this.component = new ICAL.Component(jCal);
} else if (aData.component instanceof ICAL.Component) {
// If it was a component already, then just set it
this.component = aData.component;
} else {
// Otherwise just null out the component
this.component = null;
}
}
// Copy remaining passed properties
for (var key in OPTIONS) {
/* istanbul ignore else */
if (OPTIONS.hasOwnProperty(key)) {
var prop = OPTIONS[key];
if (aData && prop in aData) {
this[prop] = aData[prop];
}
}
}
}
// If we have a component but no TZID, attempt to get it from the
// component's properties.
if (this.component instanceof ICAL.Component && !this.tzid) {
this.tzid = this.component.getFirstPropertyValue('tzid');
}
return this;
},
/**
* Finds the utcOffset the given time would occur in this timezone.
*
* @param {ICAL.Time} tt The time to check for
* @return {Number} utc offset in seconds
*/
utcOffset: function utcOffset(tt) {
if (this == ICAL.Timezone.utcTimezone || this == ICAL.Timezone.localTimezone) {
return 0;
}
this._ensureCoverage(tt.year);
if (!this.changes.length) {
return 0;
}
var tt_change = {
year: tt.year,
month: tt.month,
day: tt.day,
hour: tt.hour,
minute: tt.minute,
second: tt.second
};
var change_num = this._findNearbyChange(tt_change);
var change_num_to_use = -1;
var step = 1;
// TODO: replace with bin search?
for (;;) {
var change = ICAL.helpers.clone(this.changes[change_num], true);
if (change.utcOffset < change.prevUtcOffset) {
ICAL.Timezone.adjust_change(change, 0, 0, 0, change.utcOffset);
} else {
ICAL.Timezone.adjust_change(change, 0, 0, 0,
change.prevUtcOffset);
}
var cmp = ICAL.Timezone._compare_change_fn(tt_change, change);
if (cmp >= 0) {
change_num_to_use = change_num;
} else {
step = -1;
}
if (step == -1 && change_num_to_use != -1) {
break;
}
change_num += step;
if (change_num < 0) {
return 0;
}
if (change_num >= this.changes.length) {
break;
}
}
var zone_change = this.changes[change_num_to_use];
var utcOffset_change = zone_change.utcOffset - zone_change.prevUtcOffset;
if (utcOffset_change < 0 && change_num_to_use > 0) {
var tmp_change = ICAL.helpers.clone(zone_change, true);
ICAL.Timezone.adjust_change(tmp_change, 0, 0, 0,
tmp_change.prevUtcOffset);
if (ICAL.Timezone._compare_change_fn(tt_change, tmp_change) < 0) {
var prev_zone_change = this.changes[change_num_to_use - 1];
var want_daylight = false; // TODO
if (zone_change.is_daylight != want_daylight &&
prev_zone_change.is_daylight == want_daylight) {
zone_change = prev_zone_change;
}
}
}
// TODO return is_daylight?
return zone_change.utcOffset;
},
_findNearbyChange: function icaltimezone_find_nearby_change(change) {
// find the closest match
var idx = ICAL.helpers.binsearchInsert(
this.changes,
change,
ICAL.Timezone._compare_change_fn
);
if (idx >= this.changes.length) {
return this.changes.length - 1;
}
return idx;
},
_ensureCoverage: function(aYear) {
if (ICAL.Timezone._minimumExpansionYear == -1) {
var today = ICAL.Time.now();
ICAL.Timezone._minimumExpansionYear = today.year;
}
var changesEndYear = aYear;
if (changesEndYear < ICAL.Timezone._minimumExpansionYear) {
changesEndYear = ICAL.Timezone._minimumExpansionYear;
}
changesEndYear += ICAL.Timezone.EXTRA_COVERAGE;
if (changesEndYear > ICAL.Timezone.MAX_YEAR) {
changesEndYear = ICAL.Timezone.MAX_YEAR;
}
if (!this.changes.length || this.expandedUntilYear < aYear) {
var subcomps = this.component.getAllSubcomponents();
var compLen = subcomps.length;
var compIdx = 0;
for (; compIdx < compLen; compIdx++) {
this._expandComponent(
subcomps[compIdx], changesEndYear, this.changes
);
}
this.changes.sort(ICAL.Timezone._compare_change_fn);
this.expandedUntilYear = changesEndYear;
}
},
_expandComponent: function(aComponent, aYear, changes) {
if (!aComponent.hasProperty("dtstart") ||
!aComponent.hasProperty("tzoffsetto") ||
!aComponent.hasProperty("tzoffsetfrom")) {
return null;
}
var dtstart = aComponent.getFirstProperty("dtstart").getFirstValue();
var change;
function convert_tzoffset(offset) {
return offset.factor * (offset.hours * 3600 + offset.minutes * 60);
}
function init_changes() {
var changebase = {};
changebase.is_daylight = (aComponent.name == "daylight");
changebase.utcOffset = convert_tzoffset(
aComponent.getFirstProperty("tzoffsetto").getFirstValue()
);
changebase.prevUtcOffset = convert_tzoffset(
aComponent.getFirstProperty("tzoffsetfrom").getFirstValue()
);
return changebase;
}
if (!aComponent.hasProperty("rrule") && !aComponent.hasProperty("rdate")) {
change = init_changes();
change.year = dtstart.year;
change.month = dtstart.month;
change.day = dtstart.day;
change.hour = dtstart.hour;
change.minute = dtstart.minute;
change.second = dtstart.second;
ICAL.Timezone.adjust_change(change, 0, 0, 0,
-change.prevUtcOffset);
changes.push(change);
} else {
var props = aComponent.getAllProperties("rdate");
for (var rdatekey in props) {
/* istanbul ignore if */
if (!props.hasOwnProperty(rdatekey)) {
continue;
}
var rdate = props[rdatekey];
var time = rdate.getFirstValue();
change = init_changes();
change.year = time.year;
change.month = time.month;
change.day = time.day;
if (time.isDate) {
change.hour = dtstart.hour;
change.minute = dtstart.minute;
change.second = dtstart.second;
if (dtstart.zone != ICAL.Timezone.utcTimezone) {
ICAL.Timezone.adjust_change(change, 0, 0, 0,
-change.prevUtcOffset);
}
} else {
change.hour = time.hour;
change.minute = time.minute;
change.second = time.second;
if (time.zone != ICAL.Timezone.utcTimezone) {
ICAL.Timezone.adjust_change(change, 0, 0, 0,
-change.prevUtcOffset);
}
}
changes.push(change);
}
var rrule = aComponent.getFirstProperty("rrule");
if (rrule) {
rrule = rrule.getFirstValue();
change = init_changes();
if (rrule.until && rrule.until.zone == ICAL.Timezone.utcTimezone) {
rrule.until.adjust(0, 0, 0, change.prevUtcOffset);
rrule.until.zone = ICAL.Timezone.localTimezone;
}
var iterator = rrule.iterator(dtstart);
var occ;
while ((occ = iterator.next())) {
change = init_changes();
if (occ.year > aYear || !occ) {
break;
}
change.year = occ.year;
change.month = occ.month;
change.day = occ.day;
change.hour = occ.hour;
change.minute = occ.minute;
change.second = occ.second;
change.isDate = occ.isDate;
ICAL.Timezone.adjust_change(change, 0, 0, 0,
-change.prevUtcOffset);
changes.push(change);
}
}
}
return changes;
},
/**
* The string representation of this timezone.
* @return {String}
*/
toString: function toString() {
return (this.tznames ? this.tznames : this.tzid);
}
};
ICAL.Timezone._compare_change_fn = function icaltimezone_compare_change_fn(a, b) {
if (a.year < b.year) return -1;
else if (a.year > b.year) return 1;
if (a.month < b.month) return -1;
else if (a.month > b.month) return 1;
if (a.day < b.day) return -1;
else if (a.day > b.day) return 1;
if (a.hour < b.hour) return -1;
else if (a.hour > b.hour) return 1;
if (a.minute < b.minute) return -1;
else if (a.minute > b.minute) return 1;
if (a.second < b.second) return -1;
else if (a.second > b.second) return 1;
return 0;
};
/**
* Convert the date/time from one zone to the next.
*
* @param {ICAL.Time} tt The time to convert
* @param {ICAL.Timezone} from_zone The source zone to convert from
* @param {ICAL.Timezone} to_zone The target zone to conver to
* @return {ICAL.Time} The converted date/time object
*/
ICAL.Timezone.convert_time = function icaltimezone_convert_time(tt, from_zone, to_zone) {
if (tt.isDate ||
from_zone.tzid == to_zone.tzid ||
from_zone == ICAL.Timezone.localTimezone ||
to_zone == ICAL.Timezone.localTimezone) {
tt.zone = to_zone;
return tt;
}
var utcOffset = from_zone.utcOffset(tt);
tt.adjust(0, 0, 0, - utcOffset);
utcOffset = to_zone.utcOffset(tt);
tt.adjust(0, 0, 0, utcOffset);
return null;
};
/**
* Creates a new ICAL.Timezone instance from the passed data object.
*
* @param {ICAL.Component|Object} aData options for class
* @param {String|ICAL.Component} aData.component
* If aData is a simple object, then this member can be set to either a
* string containing the component data, or an already parsed
* ICAL.Component
* @param {String} aData.tzid The timezone identifier
* @param {String} aData.location The timezone locationw
* @param {String} aData.tznames An alternative string representation of the
* timezone
* @param {Number} aData.latitude The latitude of the timezone
* @param {Number} aData.longitude The longitude of the timezone
*/
ICAL.Timezone.fromData = function icaltimezone_fromData(aData) {
var tt = new ICAL.Timezone();
return tt.fromData(aData);
};
/**
* The instance describing the UTC timezone
* @type {ICAL.Timezone}
* @constant
* @instance
*/
ICAL.Timezone.utcTimezone = ICAL.Timezone.fromData({
tzid: "UTC"
});
/**
* The instance describing the local timezone
* @type {ICAL.Timezone}
* @constant
* @instance
*/
ICAL.Timezone.localTimezone = ICAL.Timezone.fromData({
tzid: "floating"
});
/**
* Adjust a timezone change object.
* @private
* @param {Object} change The timezone change object
* @param {Number} days The extra amount of days
* @param {Number} hours The extra amount of hours
* @param {Number} minutes The extra amount of minutes
* @param {Number} seconds The extra amount of seconds
*/
ICAL.Timezone.adjust_change = function icaltimezone_adjust_change(change, days, hours, minutes, seconds) {
return ICAL.Time.prototype.adjust.call(
change,
days,
hours,
minutes,
seconds,
change
);
};
ICAL.Timezone._minimumExpansionYear = -1;
ICAL.Timezone.MAX_YEAR = 2035; // TODO this is because of time_t, which we don't need. Still usefull?
ICAL.Timezone.EXTRA_COVERAGE = 5;
})();
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* This symbol is further described later on
* @ignore
*/
ICAL.TimezoneService = (function() {
var zones;
/**
* @classdesc
* Singleton class to contain timezones. Right now its all manual registry in
* the future we may use this class to download timezone information or handle
* loading pre-expanded timezones.
*
* @namespace
* @alias ICAL.TimezoneService
*/
var TimezoneService = {
reset: function() {
zones = Object.create(null);
var utc = ICAL.Timezone.utcTimezone;
zones.Z = utc;
zones.UTC = utc;
zones.GMT = utc;
},
/**
* Checks if timezone id has been registered.
*
* @param {String} tzid Timezone identifier (e.g. America/Los_Angeles)
* @return {Boolean} False, when not present
*/
has: function(tzid) {
return !!zones[tzid];
},
/**
* Returns a timezone by its tzid if present.
*
* @param {String} tzid Timezone identifier (e.g. America/Los_Angeles)
* @return {?ICAL.Timezone} The timezone, or null if not found
*/
get: function(tzid) {
return zones[tzid];
},
/**
* Registers a timezone object or component.
*
* @param {String=} name
* The name of the timezone. Defaults to the component's TZID if not
* passed.
* @param {ICAL.Component|ICAL.Timezone} zone
* The initialized zone or vtimezone.
*/
register: function(name, timezone) {
if (name instanceof ICAL.Component) {
if (name.name === 'vtimezone') {
timezone = new ICAL.Timezone(name);
name = timezone.tzid;
}
}
if (timezone instanceof ICAL.Timezone) {
zones[name] = timezone;
} else {
throw new TypeError('timezone must be ICAL.Timezone or ICAL.Component');
}
},
/**
* Removes a timezone by its tzid from the list.
*
* @param {String} tzid Timezone identifier (e.g. America/Los_Angeles)
* @return {?ICAL.Timezone} The removed timezone, or null if not registered
*/
remove: function(tzid) {
return (delete zones[tzid]);
}
};
// initialize defaults
TimezoneService.reset();
return TimezoneService;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
(function() {
/**
* @classdesc
* iCalendar Time representation (similar to JS Date object). Fully
* independent of system (OS) timezone / time. Unlike JS Date, the month
* January is 1, not zero.
*
* @example
* var time = new ICAL.Time({
* year: 2012,
* month: 10,
* day: 11
* minute: 0,
* second: 0,
* isDate: false
* });
*
*
* @alias ICAL.Time
* @class
* @param {Object} data Time initialization
* @param {Number=} data.year The year for this date
* @param {Number=} data.month The month for this date
* @param {Number=} data.day The day for this date
* @param {Number=} data.hour The hour for this date
* @param {Number=} data.minute The minute for this date
* @param {Number=} data.second The second for this date
* @param {Boolean=} data.isDate If true, the instance represents a date (as
* opposed to a date-time)
* @param {ICAL.Timezone} zone timezone this position occurs in
*/
ICAL.Time = function icaltime(data, zone) {
this.wrappedJSObject = this;
var time = this._time = Object.create(null);
/* time defaults */
time.year = 0;
time.month = 1;
time.day = 1;
time.hour = 0;
time.minute = 0;
time.second = 0;
time.isDate = false;
this.fromData(data, zone);
};
ICAL.Time._dowCache = {};
ICAL.Time._wnCache = {};
ICAL.Time.prototype = {
/**
* The class identifier.
* @constant
* @type {String}
* @default "icaltime"
*/
icalclass: "icaltime",
_cachedUnixTime: null,
/**
* The type name, to be used in the jCal object. This value may change and
* is strictly defined by the {@link ICAL.Time#isDate isDate} member.
* @readonly
* @type {String}
* @default "date-time"
*/
get icaltype() {
return this.isDate ? 'date' : 'date-time';
},
/**
* The timezone for this time.
* @type {ICAL.Timezone}
*/
zone: null,
/**
* Internal uses to indicate that a change has been made and the next read
* operation must attempt to normalize the value (for example changing the
* day to 33).
*
* @type {Boolean}
* @private
*/
_pendingNormalization: false,
/**
* Returns a clone of the time object.
*
* @return {ICAL.Time} The cloned object
*/
clone: function() {
return new ICAL.Time(this._time, this.zone);
},
/**
* Reset the time instance to epoch time
*/
reset: function icaltime_reset() {
this.fromData(ICAL.Time.epochTime);
this.zone = ICAL.Timezone.utcTimezone;
},
/**
* Reset the time instance to the given date/time values.
*
* @param {Number} year The year to set
* @param {Number} month The month to set
* @param {Number} day The day to set
* @param {Number} hour The hour to set
* @param {Number} minute The minute to set
* @param {Number} second The second to set
* @param {ICAL.Timezone} timezone The timezone to set
*/
resetTo: function icaltime_resetTo(year, month, day,
hour, minute, second, timezone) {
this.fromData({
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
zone: timezone
});
},
/**
* Set up the current instance from the Javascript date value.
*
* @param {?Date} aDate The Javascript Date to read, or null to reset
* @param {Boolean} useUTC If true, the UTC values of the date will be used
*/
fromJSDate: function icaltime_fromJSDate(aDate, useUTC) {
if (!aDate) {
this.reset();
} else {
if (useUTC) {
this.zone = ICAL.Timezone.utcTimezone;
this.year = aDate.getUTCFullYear();
this.month = aDate.getUTCMonth() + 1;
this.day = aDate.getUTCDate();
this.hour = aDate.getUTCHours();
this.minute = aDate.getUTCMinutes();
this.second = aDate.getUTCSeconds();
} else {
this.zone = ICAL.Timezone.localTimezone;
this.year = aDate.getFullYear();
this.month = aDate.getMonth() + 1;
this.day = aDate.getDate();
this.hour = aDate.getHours();
this.minute = aDate.getMinutes();
this.second = aDate.getSeconds();
}
}
this._cachedUnixTime = null;
return this;
},
/**
* Sets up the current instance using members from the passed data object.
*
* @param {Object} aData Time initialization
* @param {Number=} aData.year The year for this date
* @param {Number=} aData.month The month for this date
* @param {Number=} aData.day The day for this date
* @param {Number=} aData.hour The hour for this date
* @param {Number=} aData.minute The minute for this date
* @param {Number=} aData.second The second for this date
* @param {Boolean=} aData.isDate If true, the instance represents a date
* (as opposed to a date-time)
* @param {ICAL.Timezone=} aZone Timezone this position occurs in
*/
fromData: function fromData(aData, aZone) {
if (aData) {
for (var key in aData) {
/* istanbul ignore else */
if (Object.prototype.hasOwnProperty.call(aData, key)) {
// ical type cannot be set
if (key === 'icaltype') continue;
this[key] = aData[key];
}
}
}
if (aZone) {
this.zone = aZone;
}
if (aData && !("isDate" in aData)) {
this.isDate = !("hour" in aData);
} else if (aData && ("isDate" in aData)) {
this.isDate = aData.isDate;
}
if (aData && "timezone" in aData) {
var zone = ICAL.TimezoneService.get(
aData.timezone
);
this.zone = zone || ICAL.Timezone.localTimezone;
}
if (aData && "zone" in aData) {
this.zone = aData.zone;
}
if (!this.zone) {
this.zone = ICAL.Timezone.localTimezone;
}
this._cachedUnixTime = null;
return this;
},
/**
* Calculate the day of week.
* @return {ICAL.Time.weekDay}
*/
dayOfWeek: function icaltime_dayOfWeek() {
var dowCacheKey = (this.year << 9) + (this.month << 5) + this.day;
if (dowCacheKey in ICAL.Time._dowCache) {
return ICAL.Time._dowCache[dowCacheKey];
}
// Using Zeller's algorithm
var q = this.day;
var m = this.month + (this.month < 3 ? 12 : 0);
var Y = this.year - (this.month < 3 ? 1 : 0);
var h = (q + Y + ICAL.helpers.trunc(((m + 1) * 26) / 10) + ICAL.helpers.trunc(Y / 4));
/* istanbul ignore else */
if (true /* gregorian */) {
h += ICAL.helpers.trunc(Y / 100) * 6 + ICAL.helpers.trunc(Y / 400);
} else {
h += 5;
}
// Normalize to 1 = sunday
h = ((h + 6) % 7) + 1;
ICAL.Time._dowCache[dowCacheKey] = h;
return h;
},
/**
* Calculate the day of year.
* @return {Number}
*/
dayOfYear: function dayOfYear() {
var is_leap = (ICAL.Time.isLeapYear(this.year) ? 1 : 0);
var diypm = ICAL.Time.daysInYearPassedMonth;
return diypm[is_leap][this.month - 1] + this.day;
},
/**
* Returns a copy of the current date/time, rewound to the start of the
* week. The resulting ICAL.Time instance is of icaltype date, even if this
* is a date-time.
*
* @param {ICAL.Time.weekDay=} aWeekStart
* The week start weekday, defaults to SUNDAY
* @return {ICAL.Time} The start of the week (cloned)
*/
startOfWeek: function startOfWeek(aWeekStart) {
var firstDow = aWeekStart || ICAL.Time.SUNDAY;
var result = this.clone();
result.day -= ((this.dayOfWeek() + 7 - firstDow) % 7);
result.isDate = true;
result.hour = 0;
result.minute = 0;
result.second = 0;
return result;
},
/**
* Returns a copy of the current date/time, shifted to the end of the week.
* The resulting ICAL.Time instance is of icaltype date, even if this is a
* date-time.
*
* @param {ICAL.Time.weekDay=} aWeekStart
* The week start weekday, defaults to SUNDAY
* @return {ICAL.Time} The end of the week (cloned)
*/
endOfWeek: function endOfWeek(aWeekStart) {
var firstDow = aWeekStart || ICAL.Time.SUNDAY;
var result = this.clone();
result.day += (7 - this.dayOfWeek() + firstDow - ICAL.Time.SUNDAY) % 7;
result.isDate = true;
result.hour = 0;
result.minute = 0;
result.second = 0;
return result;
},
/**
* Returns a copy of the current date/time, rewound to the start of the
* month. The resulting ICAL.Time instance is of icaltype date, even if
* this is a date-time.
*
* @return {ICAL.Time} The start of the month (cloned)
*/
startOfMonth: function startOfMonth() {
var result = this.clone();
result.day = 1;
result.isDate = true;
result.hour = 0;
result.minute = 0;
result.second = 0;
return result;
},
/**
* Returns a copy of the current date/time, shifted to the end of the
* month. The resulting ICAL.Time instance is of icaltype date, even if
* this is a date-time.
*
* @return {ICAL.Time} The end of the month (cloned)
*/
endOfMonth: function endOfMonth() {
var result = this.clone();
result.day = ICAL.Time.daysInMonth(result.month, result.year);
result.isDate = true;
result.hour = 0;
result.minute = 0;
result.second = 0;
return result;
},
/**
* Returns a copy of the current date/time, rewound to the start of the
* year. The resulting ICAL.Time instance is of icaltype date, even if
* this is a date-time.
*
* @return {ICAL.Time} The start of the year (cloned)
*/
startOfYear: function startOfYear() {
var result = this.clone();
result.day = 1;
result.month = 1;
result.isDate = true;
result.hour = 0;
result.minute = 0;
result.second = 0;
return result;
},
/**
* Returns a copy of the current date/time, shifted to the end of the
* year. The resulting ICAL.Time instance is of icaltype date, even if
* this is a date-time.
*
* @return {ICAL.Time} The end of the year (cloned)
*/
endOfYear: function endOfYear() {
var result = this.clone();
result.day = 31;
result.month = 12;
result.isDate = true;
result.hour = 0;
result.minute = 0;
result.second = 0;
return result;
},
/**
* First calculates the start of the week, then returns the day of year for
* this date. If the day falls into the previous year, the day is zero or negative.
*
* @param {ICAL.Time.weekDay=} aFirstDayOfWeek
* The week start weekday, defaults to SUNDAY
* @return {Number} The calculated day of year
*/
startDoyWeek: function startDoyWeek(aFirstDayOfWeek) {
var firstDow = aFirstDayOfWeek || ICAL.Time.SUNDAY;
var delta = this.dayOfWeek() - firstDow;
if (delta < 0) delta += 7;
return this.dayOfYear() - delta;
},
/**
* Get the dominical letter for the current year. Letters range from A - G
* for common years, and AG to GF for leap years.
*
* @param {Number} yr The year to retrieve the letter for
* @return {String} The dominical letter.
*/
getDominicalLetter: function() {
return ICAL.Time.getDominicalLetter(this.year);
},
/**
* Finds the nthWeekDay relative to the current month (not day). The
* returned value is a day relative the month that this month belongs to so
* 1 would indicate the first of the month and 40 would indicate a day in
* the following month.
*
* @param {Number} aDayOfWeek Day of the week see the day name constants
* @param {Number} aPos Nth occurrence of a given week day values
* of 1 and 0 both indicate the first weekday of that type. aPos may
* be either positive or negative
*
* @return {Number} numeric value indicating a day relative
* to the current month of this time object
*/
nthWeekDay: function icaltime_nthWeekDay(aDayOfWeek, aPos) {
var daysInMonth = ICAL.Time.daysInMonth(this.month, this.year);
var weekday;
var pos = aPos;
var start = 0;
var otherDay = this.clone();
if (pos >= 0) {
otherDay.day = 1;
// because 0 means no position has been given
// 1 and 0 indicate the same day.
if (pos != 0) {
// remove the extra numeric value
pos--;
}
// set current start offset to current day.
start = otherDay.day;
// find the current day of week
var startDow = otherDay.dayOfWeek();
// calculate the difference between current
// day of the week and desired day of the week
var offset = aDayOfWeek - startDow;
// if the offset goes into the past
// week we add 7 so its goes into the next
// week. We only want to go forward in time here.
if (offset < 0)
// this is really important otherwise we would
// end up with dates from in the past.
offset += 7;
// add offset to start so start is the same
// day of the week as the desired day of week.
start += offset;
// because we are going to add (and multiply)
// the numeric value of the day we subtract it
// from the start position so not to add it twice.
start -= aDayOfWeek;
// set week day
weekday = aDayOfWeek;
} else {
// then we set it to the last day in the current month
otherDay.day = daysInMonth;
// find the ends weekday
var endDow = otherDay.dayOfWeek();
pos++;
weekday = (endDow - aDayOfWeek);
if (weekday < 0) {
weekday += 7;
}
weekday = daysInMonth - weekday;
}
weekday += pos * 7;
return start + weekday;
},
/**
* Checks if current time is the nth weekday, relative to the current
* month. Will always return false when rule resolves outside of current
* month.
*
* @param {ICAL.Time.weekDay} aDayOfWeek Day of week to check
* @param {Number} aPos Relative position
* @return {Boolean} True, if its the nth weekday
*/
isNthWeekDay: function(aDayOfWeek, aPos) {
var dow = this.dayOfWeek();
if (aPos === 0 && dow === aDayOfWeek) {
return true;
}
// get pos
var day = this.nthWeekDay(aDayOfWeek, aPos);
if (day === this.day) {
return true;
}
return false;
},
/**
* Calculates the ISO 8601 week number. The first week of a year is the
* week that contains the first Thursday. The year can have 53 weeks, if
* January 1st is a Friday.
*
* Note there are regions where the first week of the year is the one that
* starts on January 1st, which may offset the week number. Also, if a
* different week start is specified, this will also affect the week
* number.
*
* @see ICAL.Time.weekOneStarts
* @param {ICAL.Time.weekDay} aWeekStart The weekday the week starts with
* @return {Number} The ISO week number
*/
weekNumber: function weekNumber(aWeekStart) {
var wnCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + aWeekStart;
if (wnCacheKey in ICAL.Time._wnCache) {
return ICAL.Time._wnCache[wnCacheKey];
}
// This function courtesty of Julian Bucknall, published under the MIT license
// http://www.boyet.com/articles/publishedarticles/calculatingtheisoweeknumb.html
// plus some fixes to be able to use different week starts.
var week1;
var dt = this.clone();
dt.isDate = true;
var isoyear = this.year;
if (dt.month == 12 && dt.day > 25) {
week1 = ICAL.Time.weekOneStarts(isoyear + 1, aWeekStart);
if (dt.compare(week1) < 0) {
week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart);
} else {
isoyear++;
}
} else {
week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart);
if (dt.compare(week1) < 0) {
week1 = ICAL.Time.weekOneStarts(--isoyear, aWeekStart);
}
}
var daysBetween = (dt.subtractDate(week1).toSeconds() / 86400);
var answer = ICAL.helpers.trunc(daysBetween / 7) + 1;
ICAL.Time._wnCache[wnCacheKey] = answer;
return answer;
},
/**
* Adds the duration to the current time. The instance is modified in
* place.
*
* @param {ICAL.Duration} aDuration The duration to add
*/
addDuration: function icaltime_add(aDuration) {
var mult = (aDuration.isNegative ? -1 : 1);
// because of the duration optimizations it is much
// more efficient to grab all the values up front
// then set them directly (which will avoid a normalization call).
// So we don't actually normalize until we need it.
var second = this.second;
var minute = this.minute;
var hour = this.hour;
var day = this.day;
second += mult * aDuration.seconds;
minute += mult * aDuration.minutes;
hour += mult * aDuration.hours;
day += mult * aDuration.days;
day += mult * 7 * aDuration.weeks;
this.second = second;
this.minute = minute;
this.hour = hour;
this.day = day;
this._cachedUnixTime = null;
},
/**
* Subtract the date details (_excluding_ timezone). Useful for finding
* the relative difference between two time objects excluding their
* timezone differences.
*
* @param {ICAL.Time} aDate The date to substract
* @return {ICAL.Duration} The difference as a duration
*/
subtractDate: function icaltime_subtract(aDate) {
var unixTime = this.toUnixTime() + this.utcOffset();
var other = aDate.toUnixTime() + aDate.utcOffset();
return ICAL.Duration.fromSeconds(unixTime - other);
},
/**
* Subtract the date details, taking timezones into account.
*
* @param {ICAL.Time} aDate The date to subtract
* @return {ICAL.Duration} The difference in duration
*/
subtractDateTz: function icaltime_subtract_abs(aDate) {
var unixTime = this.toUnixTime();
var other = aDate.toUnixTime();
return ICAL.Duration.fromSeconds(unixTime - other);
},
/**
* Compares the ICAL.Time instance with another one.
*
* @param {ICAL.Duration} aOther The instance to compare with
* @return {Number} -1, 0 or 1 for less/equal/greater
*/
compare: function icaltime_compare(other) {
var a = this.toUnixTime();
var b = other.toUnixTime();
if (a > b) return 1;
if (b > a) return -1;
return 0;
},
/**
* Compares only the date part of this instance with another one.
*
* @param {ICAL.Duration} other The instance to compare with
* @param {ICAL.Timezone} tz The timezone to compare in
* @return {Number} -1, 0 or 1 for less/equal/greater
*/
compareDateOnlyTz: function icaltime_compareDateOnlyTz(other, tz) {
function cmp(attr) {
return ICAL.Time._cmp_attr(a, b, attr);
}
var a = this.convertToZone(tz);
var b = other.convertToZone(tz);
var rc = 0;
if ((rc = cmp("year")) != 0) return rc;
if ((rc = cmp("month")) != 0) return rc;
if ((rc = cmp("day")) != 0) return rc;
return rc;
},
/**
* Convert the instance into another timzone. The returned ICAL.Time
* instance is always a copy.
*
* @param {ICAL.Timezone} zone The zone to convert to
* @return {ICAL.Time} The copy, converted to the zone
*/
convertToZone: function convertToZone(zone) {
var copy = this.clone();
var zone_equals = (this.zone.tzid == zone.tzid);
if (!this.isDate && !zone_equals) {
ICAL.Timezone.convert_time(copy, this.zone, zone);
}
copy.zone = zone;
return copy;
},
/**
* Calculates the UTC offset of the current date/time in the timezone it is
* in.
*
* @return {Number} UTC offset in seconds
*/
utcOffset: function utc_offset() {
if (this.zone == ICAL.Timezone.localTimezone ||
this.zone == ICAL.Timezone.utcTimezone) {
return 0;
} else {
return this.zone.utcOffset(this);
}
},
/**
* Returns an RFC 5545 compliant ical representation of this object.
*
* @return {String} ical date/date-time
*/
toICALString: function() {
var string = this.toString();
if (string.length > 10) {
return ICAL.design.icalendar.value['date-time'].toICAL(string);
} else {
return ICAL.design.icalendar.value.date.toICAL(string);
}
},
/**
* The string representation of this date/time, in jCal form
* (including : and - separators).
* @return {String}
*/
toString: function toString() {
var result = this.year + '-' +
ICAL.helpers.pad2(this.month) + '-' +
ICAL.helpers.pad2(this.day);
if (!this.isDate) {
result += 'T' + ICAL.helpers.pad2(this.hour) + ':' +
ICAL.helpers.pad2(this.minute) + ':' +
ICAL.helpers.pad2(this.second);
if (this.zone === ICAL.Timezone.utcTimezone) {
result += 'Z';
}
}
return result;
},
/**
* Converts the current instance to a Javascript date
* @return {Date}
*/
toJSDate: function toJSDate() {
if (this.zone == ICAL.Timezone.localTimezone) {
if (this.isDate) {
return new Date(this.year, this.month - 1, this.day);
} else {
return new Date(this.year, this.month - 1, this.day,
this.hour, this.minute, this.second, 0);
}
} else {
return new Date(this.toUnixTime() * 1000);
}
},
_normalize: function icaltime_normalize() {
var isDate = this._time.isDate;
if (this._time.isDate) {
this._time.hour = 0;
this._time.minute = 0;
this._time.second = 0;
}
this.adjust(0, 0, 0, 0);
return this;
},
/**
* Adjust the date/time by the given offset
*
* @param {Number} aExtraDays The extra amount of days
* @param {Number} aExtraHours The extra amount of hours
* @param {Number} aExtraMinutes The extra amount of minutes
* @param {Number} aExtraSeconds The extra amount of seconds
* @param {Number=} aTime The time to adjust, defaults to the
* current instance.
*/
adjust: function icaltime_adjust(aExtraDays, aExtraHours,
aExtraMinutes, aExtraSeconds, aTime) {
var minutesOverflow, hoursOverflow,
daysOverflow = 0, yearsOverflow = 0;
var second, minute, hour, day;
var daysInMonth;
var time = aTime || this._time;
if (!time.isDate) {
second = time.second + aExtraSeconds;
time.second = second % 60;
minutesOverflow = ICAL.helpers.trunc(second / 60);
if (time.second < 0) {
time.second += 60;
minutesOverflow--;
}
minute = time.minute + aExtraMinutes + minutesOverflow;
time.minute = minute % 60;
hoursOverflow = ICAL.helpers.trunc(minute / 60);
if (time.minute < 0) {
time.minute += 60;
hoursOverflow--;
}
hour = time.hour + aExtraHours + hoursOverflow;
time.hour = hour % 24;
daysOverflow = ICAL.helpers.trunc(hour / 24);
if (time.hour < 0) {
time.hour += 24;
daysOverflow--;
}
}
// Adjust month and year first, because we need to know what month the day
// is in before adjusting it.
if (time.month > 12) {
yearsOverflow = ICAL.helpers.trunc((time.month - 1) / 12);
} else if (time.month < 1) {
yearsOverflow = ICAL.helpers.trunc(time.month / 12) - 1;
}
time.year += yearsOverflow;
time.month -= 12 * yearsOverflow;
// Now take care of the days (and adjust month if needed)
day = time.day + aExtraDays + daysOverflow;
if (day > 0) {
for (;;) {
daysInMonth = ICAL.Time.daysInMonth(time.month, time.year);
if (day <= daysInMonth) {
break;
}
time.month++;
if (time.month > 12) {
time.year++;
time.month = 1;
}
day -= daysInMonth;
}
} else {
while (day <= 0) {
if (time.month == 1) {
time.year--;
time.month = 12;
} else {
time.month--;
}
day += ICAL.Time.daysInMonth(time.month, time.year);
}
}
time.day = day;
this._cachedUnixTime = null;
return this;
},
/**
* Sets up the current instance from unix time, the number of seconds since
* January 1st, 1970.
*
* @param {Number} seconds The seconds to set up with
*/
fromUnixTime: function fromUnixTime(seconds) {
this.zone = ICAL.Timezone.utcTimezone;
var epoch = ICAL.Time.epochTime.clone();
epoch.adjust(0, 0, 0, seconds);
this.year = epoch.year;
this.month = epoch.month;
this.day = epoch.day;
this.hour = epoch.hour;
this.minute = epoch.minute;
this.second = Math.floor(epoch.second);
this._cachedUnixTime = null;
},
/**
* Converts the current instance to seconds since January 1st 1970.
*
* @return {Number} Seconds since 1970
*/
toUnixTime: function toUnixTime() {
if (this._cachedUnixTime !== null) {
return this._cachedUnixTime;
}
var offset = this.utcOffset();
// we use the offset trick to ensure
// that we are getting the actual UTC time
var ms = Date.UTC(
this.year,
this.month - 1,
this.day,
this.hour,
this.minute,
this.second - offset
);
// seconds
this._cachedUnixTime = ms / 1000;
return this._cachedUnixTime;
},
/**
* Converts time to into Object which can be serialized then re-created
* using the constructor.
*
* @example
* // toJSON will automatically be called
* var json = JSON.stringify(mytime);
*
* var deserialized = JSON.parse(json);
*
* var time = new ICAL.Time(deserialized);
*
* @return {Object}
*/
toJSON: function() {
var copy = [
'year',
'month',
'day',
'hour',
'minute',
'second',
'isDate'
];
var result = Object.create(null);
var i = 0;
var len = copy.length;
var prop;
for (; i < len; i++) {
prop = copy[i];
result[prop] = this[prop];
}
if (this.zone) {
result.timezone = this.zone.tzid;
}
return result;
}
};
(function setupNormalizeAttributes() {
// This needs to run before any instances are created!
function defineAttr(attr) {
Object.defineProperty(ICAL.Time.prototype, attr, {
get: function getTimeAttr() {
if (this._pendingNormalization) {
this._normalize();
this._pendingNormalization = false;
}
return this._time[attr];
},
set: function setTimeAttr(val) {
this._cachedUnixTime = null;
this._pendingNormalization = true;
this._time[attr] = val;
return val;
}
});
}
/* istanbul ignore else */
if ("defineProperty" in Object) {
defineAttr("year");
defineAttr("month");
defineAttr("day");
defineAttr("hour");
defineAttr("minute");
defineAttr("second");
defineAttr("isDate");
}
})();
/**
* Returns the days in the given month
*
* @param {Number} month The month to check
* @param {Number} year The year to check
* @return {Number} The number of days in the month
*/
ICAL.Time.daysInMonth = function icaltime_daysInMonth(month, year) {
var _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
var days = 30;
if (month < 1 || month > 12) return days;
days = _daysInMonth[month];
if (month == 2) {
days += ICAL.Time.isLeapYear(year);
}
return days;
};
/**
* Checks if the year is a leap year
*
* @param {Number} year The year to check
* @return {Boolean} True, if the year is a leap year
*/
ICAL.Time.isLeapYear = function isLeapYear(year) {
if (year <= 1752) {
return ((year % 4) == 0);
} else {
return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0));
}
};
/**
* Create a new ICAL.Time from the day of year and year. The date is returned
* in floating timezone.
*
* @param {Number} aDayOfYear The day of year
* @param {Number} aYear The year to create the instance in
* @return {ICAL.Time} The created instance with the calculated date
*/
ICAL.Time.fromDayOfYear = function icaltime_fromDayOfYear(aDayOfYear, aYear) {
var year = aYear;
var doy = aDayOfYear;
var tt = new ICAL.Time();
tt.auto_normalize = false;
var is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0);
if (doy < 1) {
year--;
is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0);
doy += ICAL.Time.daysInYearPassedMonth[is_leap][12];
return ICAL.Time.fromDayOfYear(doy, year);
} else if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][12]) {
is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0);
doy -= ICAL.Time.daysInYearPassedMonth[is_leap][12];
year++;
return ICAL.Time.fromDayOfYear(doy, year);
}
tt.year = year;
tt.isDate = true;
for (var month = 11; month >= 0; month--) {
if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][month]) {
tt.month = month + 1;
tt.day = doy - ICAL.Time.daysInYearPassedMonth[is_leap][month];
break;
}
}
tt.auto_normalize = true;
return tt;
};
/**
* Returns a new ICAL.Time instance from a date string, e.g 2015-01-02.
*
* @deprecated Use {@link ICAL.Time.fromDateString} instead
* @param {String} str The string to create from
* @return {ICAL.Time} The date/time instance
*/
ICAL.Time.fromStringv2 = function fromString(str) {
return new ICAL.Time({
year: parseInt(str.substr(0, 4), 10),
month: parseInt(str.substr(5, 2), 10),
day: parseInt(str.substr(8, 2), 10),
isDate: true
});
};
/**
* Returns a new ICAL.Time instance from a date string, e.g 2015-01-02.
*
* @param {String} aValue The string to create from
* @return {ICAL.Time} The date/time instance
*/
ICAL.Time.fromDateString = function(aValue) {
// Dates should have no timezone.
// Google likes to sometimes specify Z on dates
// we specifically ignore that to avoid issues.
// YYYY-MM-DD
// 2012-10-10
return new ICAL.Time({
year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)),
month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)),
day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)),
isDate: true
});
};
/**
* Returns a new ICAL.Time instance from a date-time string, e.g
* 2015-01-02T03:04:05. If a property is specified, the timezone is set up
* from the property's TZID parameter.
*
* @param {String} aValue The string to create from
* @param {ICAL.Property=} prop The property the date belongs to
* @return {ICAL.Time} The date/time instance
*/
ICAL.Time.fromDateTimeString = function(aValue, prop) {
if (aValue.length < 19) {
throw new Error(
'invalid date-time value: "' + aValue + '"'
);
}
var zone;
if (aValue[19] && aValue[19] === 'Z') {
zone = 'Z';
} else if (prop) {
zone = prop.getParameter('tzid');
}
// 2012-10-10T10:10:10(Z)?
var time = new ICAL.Time({
year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)),
month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)),
day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)),
hour: ICAL.helpers.strictParseInt(aValue.substr(11, 2)),
minute: ICAL.helpers.strictParseInt(aValue.substr(14, 2)),
second: ICAL.helpers.strictParseInt(aValue.substr(17, 2)),
timezone: zone
});
return time;
};
/**
* Returns a new ICAL.Time instance from a date or date-time string,
*
* @param {String} aValue The string to create from
* @return {ICAL.Time} The date/time instance
*/
ICAL.Time.fromString = function fromString(aValue) {
if (aValue.length > 10) {
return ICAL.Time.fromDateTimeString(aValue);
} else {
return ICAL.Time.fromDateString(aValue);
}
};
/**
* Creates a new ICAL.Time instance from the given Javascript Date.
*
* @param {?Date} aDate The Javascript Date to read, or null to reset
* @param {Boolean} useUTC If true, the UTC values of the date will be used
*/
ICAL.Time.fromJSDate = function fromJSDate(aDate, useUTC) {
var tt = new ICAL.Time();
return tt.fromJSDate(aDate, useUTC);
};
/**
* Creates a new ICAL.Time instance from the the passed data object.
*
* @param {Object} aData Time initialization
* @param {Number=} aData.year The year for this date
* @param {Number=} aData.month The month for this date
* @param {Number=} aData.day The day for this date
* @param {Number=} aData.hour The hour for this date
* @param {Number=} aData.minute The minute for this date
* @param {Number=} aData.second The second for this date
* @param {Boolean=} aData.isDate If true, the instance represents a date
* (as opposed to a date-time)
* @param {ICAL.Timezone=} aZone Timezone this position occurs in
*/
ICAL.Time.fromData = function fromData(aData, aZone) {
var t = new ICAL.Time();
return t.fromData(aData, aZone);
};
/**
* Creates a new ICAL.Time instance from the current moment.
* @return {ICAL.Time}
*/
ICAL.Time.now = function icaltime_now() {
return ICAL.Time.fromJSDate(new Date(), false);
};
/**
* Returns the date on which ISO week number 1 starts.
*
* @see ICAL.Time#weekNumber
* @param {Number} aYear The year to search in
* @param {ICAL.Time.weekDay=} aWeekStart The week start weekday, used for calculation.
* @return {ICAL.Time} The date on which week number 1 starts
*/
ICAL.Time.weekOneStarts = function weekOneStarts(aYear, aWeekStart) {
var t = ICAL.Time.fromData({
year: aYear,
month: 1,
day: 1,
isDate: true
});
var dow = t.dayOfWeek();
var wkst = aWeekStart || ICAL.Time.DEFAULT_WEEK_START;
if (dow > ICAL.Time.THURSDAY) {
t.day += 7;
}
if (wkst > ICAL.Time.THURSDAY) {
t.day -= 7;
}
t.day -= dow - wkst;
return t;
};
/**
* Get the dominical letter for the given year. Letters range from A - G for
* common years, and AG to GF for leap years.
*
* @param {Number} yr The year to retrieve the letter for
* @return {String} The dominical letter.
*/
ICAL.Time.getDominicalLetter = function(yr) {
var LTRS = "GFEDCBA";
var dom = (yr + (yr / 4 | 0) + (yr / 400 | 0) - (yr / 100 | 0) - 1) % 7;
var isLeap = ICAL.Time.isLeapYear(yr);
if (isLeap) {
return LTRS[(dom + 6) % 7] + LTRS[dom];
} else {
return LTRS[dom];
}
};
/**
* January 1st, 1970 as an ICAL.Time.
* @type {ICAL.Time}
* @constant
* @instance
*/
ICAL.Time.epochTime = ICAL.Time.fromData({
year: 1970,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
isDate: false,
timezone: "Z"
});
ICAL.Time._cmp_attr = function _cmp_attr(a, b, attr) {
if (a[attr] > b[attr]) return 1;
if (a[attr] < b[attr]) return -1;
return 0;
};
/**
* The days that have passed in the year after a given month. The array has
* two members, one being an array of passed days for non-leap years, the
* other analog for leap years.
* @example
* var isLeapYear = ICAL.Time.isLeapYear(year);
* var passedDays = ICAL.Time.daysInYearPassedMonth[isLeapYear][month];
* @type {Array.<Array.<Number>>}
*/
ICAL.Time.daysInYearPassedMonth = [
[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365],
[0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
];
/**
* The weekday, 1 = SUNDAY, 7 = SATURDAY. Access via
* ICAL.Time.MONDAY, ICAL.Time.TUESDAY, ...
*
* @typedef {Number} weekDay
* @memberof ICAL.Time
*/
ICAL.Time.SUNDAY = 1;
ICAL.Time.MONDAY = 2;
ICAL.Time.TUESDAY = 3;
ICAL.Time.WEDNESDAY = 4;
ICAL.Time.THURSDAY = 5;
ICAL.Time.FRIDAY = 6;
ICAL.Time.SATURDAY = 7;
/**
* The default weekday for the WKST part.
* @constant
* @default ICAL.Time.MONDAY
*/
ICAL.Time.DEFAULT_WEEK_START = ICAL.Time.MONDAY;
})();
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2015 */
(function() {
/**
* Describes a vCard time, which has slight differences to the ICAL.Time.
* Properties can be null if not specified, for example for dates with
* reduced accuracy or truncation.
*
* Note that currently not all methods are correctly re-implemented for
* VCardTime. For example, comparison will have undefined results when some
* members are null.
*
* Also, normalization is not yet implemented for this class!
*
* @alias ICAL.VCardTime
* @class
* @extends {ICAL.Time}
* @param {Object} data The data for the time instance
* @param {Number=} data.year The year for this date
* @param {Number=} data.month The month for this date
* @param {Number=} data.day The day for this date
* @param {Number=} data.hour The hour for this date
* @param {Number=} data.minute The minute for this date
* @param {Number=} data.second The second for this date
* @param {ICAL.Timezone|ICAL.UtcOffset} zone The timezone to use
* @param {String} icaltype The type for this date/time object
*/
ICAL.VCardTime = function(data, zone, icaltype) {
this.wrappedJSObject = this;
var time = this._time = Object.create(null);
time.year = null;
time.month = null;
time.day = null;
time.hour = null;
time.minute = null;
time.second = null;
this.icaltype = icaltype || "date-and-or-time";
this.fromData(data, zone);
};
ICAL.helpers.inherits(ICAL.Time, ICAL.VCardTime, /** @lends ICAL.VCardTime */ {
/**
* The class identifier.
* @constant
* @type {String}
* @default "vcardtime"
*/
icalclass: "vcardtime",
/**
* The type name, to be used in the jCal object.
* @type {String}
* @default "date-and-or-time"
*/
icaltype: "date-and-or-time",
/**
* The timezone. This can either be floating, UTC, or an instance of
* ICAL.UtcOffset.
* @type {ICAL.Timezone|ICAL.UtcOFfset}
*/
zone: null,
/**
* Returns a clone of the vcard date/time object.
*
* @return {ICAL.VCardTime} The cloned object
*/
clone: function() {
return new ICAL.VCardTime(this._time, this.zone, this.icaltype);
},
_normalize: function() {
return this;
},
/**
* @inheritdoc
*/
utcOffset: function() {
if (this.zone instanceof ICAL.UtcOffset) {
return this.zone.toSeconds();
} else {
return ICAL.Time.prototype.utcOffset.apply(this, arguments);
}
},
/**
* Returns an RFC 6350 compliant representation of this object.
*
* @return {String} vcard date/time string
*/
toICALString: function() {
return ICAL.design.vcard.value[this.icaltype].toICAL(this.toString());
},
/**
* The string representation of this date/time, in jCard form
* (including : and - separators).
* @return {String}
*/
toString: function toString() {
var p2 = ICAL.helpers.pad2;
var y = this.year, m = this.month, d = this.day;
var h = this.hour, mm = this.minute, s = this.second;
var hasYear = y !== null, hasMonth = m !== null, hasDay = d !== null;
var hasHour = h !== null, hasMinute = mm !== null, hasSecond = s !== null;
var datepart = (hasYear ? p2(y) + (hasMonth || hasDay ? '-' : '') : (hasMonth || hasDay ? '--' : '')) +
(hasMonth ? p2(m) : '') +
(hasDay ? '-' + p2(d) : '');
var timepart = (hasHour ? p2(h) : '-') + (hasHour && hasMinute ? ':' : '') +
(hasMinute ? p2(mm) : '') + (!hasHour && !hasMinute ? '-' : '') +
(hasMinute && hasSecond ? ':' : '') +
(hasSecond ? p2(s) : '');
var zone;
if (this.zone === ICAL.Timezone.utcTimezone) {
zone = 'Z';
} else if (this.zone instanceof ICAL.UtcOffset) {
zone = this.zone.toString();
} else if (this.zone === ICAL.Timezone.localTimezone) {
zone = '';
} else if (this.zone instanceof ICAL.Timezone) {
var offset = ICAL.UtcOffset.fromSeconds(this.zone.utcOffset(this));
zone = offset.toString();
} else {
zone = '';
}
switch (this.icaltype) {
case "time":
return timepart + zone;
case "date-and-or-time":
case "date-time":
return datepart + (timepart == '--' ? '' : 'T' + timepart + zone);
case "date":
return datepart;
}
return null;
}
});
/**
* Returns a new ICAL.VCardTime instance from a date and/or time string.
*
* @param {String} aValue The string to create from
* @param {String} aIcalType The type for this instance, e.g. date-and-or-time
* @return {ICAL.VCardTime} The date/time instance
*/
ICAL.VCardTime.fromDateAndOrTimeString = function(aValue, aIcalType) {
function part(v, s, e) {
return v ? ICAL.helpers.strictParseInt(v.substr(s, e)) : null;
}
var parts = aValue.split('T');
var dt = parts[0], tmz = parts[1];
var splitzone = tmz ? ICAL.design.vcard.value.time._splitZone(tmz) : [];
var zone = splitzone[0], tm = splitzone[1];
var stoi = ICAL.helpers.strictParseInt;
var dtlen = dt ? dt.length : 0;
var tmlen = tm ? tm.length : 0;
var hasDashDate = dt && dt[0] == '-' && dt[1] == '-';
var hasDashTime = tm && tm[0] == '-';
var o = {
year: hasDashDate ? null : part(dt, 0, 4),
month: hasDashDate && (dtlen == 4 || dtlen == 7) ? part(dt, 2, 2) : dtlen == 7 ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 5, 2) : null,
day: dtlen == 5 ? part(dt, 3, 2) : dtlen == 7 && hasDashDate ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 8, 2) : null,
hour: hasDashTime ? null : part(tm, 0, 2),
minute: hasDashTime && tmlen == 3 ? part(tm, 1, 2) : tmlen > 4 ? hasDashTime ? part(tm, 1, 2) : part(tm, 3, 2) : null,
second: tmlen == 4 ? part(tm, 2, 2) : tmlen == 6 ? part(tm, 4, 2) : tmlen == 8 ? part(tm, 6, 2) : null
};
if (zone == 'Z') {
zone = ICAL.Timezone.utcTimezone;
} else if (zone && zone[3] == ':') {
zone = ICAL.UtcOffset.fromString(zone);
} else {
zone = null;
}
return new ICAL.VCardTime(o, zone, aIcalType);
};
})();
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
(function() {
var DOW_MAP = {
SU: ICAL.Time.SUNDAY,
MO: ICAL.Time.MONDAY,
TU: ICAL.Time.TUESDAY,
WE: ICAL.Time.WEDNESDAY,
TH: ICAL.Time.THURSDAY,
FR: ICAL.Time.FRIDAY,
SA: ICAL.Time.SATURDAY
};
var REVERSE_DOW_MAP = {};
for (var key in DOW_MAP) {
/* istanbul ignore else */
if (DOW_MAP.hasOwnProperty(key)) {
REVERSE_DOW_MAP[DOW_MAP[key]] = key;
}
}
var COPY_PARTS = ["BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY",
"BYMONTHDAY", "BYYEARDAY", "BYWEEKNO",
"BYMONTH", "BYSETPOS"];
/**
* @classdesc
* This class represents the "recur" value type, with various calculation
* and manipulation methods.
*
* @class
* @alias ICAL.Recur
* @param {Object} data An object with members of the recurrence
* @param {ICAL.Recur.frequencyValues} freq The frequency value
* @param {Number=} data.interval The INTERVAL value
* @param {ICAL.Time.weekDay=} data.wkst The week start value
* @param {ICAL.Time=} data.until The end of the recurrence set
* @param {Number=} data.count The number of occurrences
* @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part
* @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part
* @param {Array.<Number>=} data.byhour The hours for the BYHOUR part
* @param {Array.<String>=} data.byday The BYDAY values
* @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part
* @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part
* @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part
* @param {Array.<Number>=} data.bymonth The month for the BYMONTH part
* @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part
*/
ICAL.Recur = function icalrecur(data) {
this.wrappedJSObject = this;
this.parts = {};
if (data && typeof(data) === 'object') {
this.fromData(data);
}
};
ICAL.Recur.prototype = {
/**
* An object holding the BY-parts of the recurrence rule
* @type {Object}
*/
parts: null,
/**
* The interval value for the recurrence rule.
* @type {Number}
*/
interval: 1,
/**
* The week start day
*
* @type {ICAL.Time.weekDay}
* @default ICAL.Time.MONDAY
*/
wkst: ICAL.Time.MONDAY,
/**
* The end of the recurrence
* @type {?ICAL.Time}
*/
until: null,
/**
* The maximum number of occurrences
* @type {?Number}
*/
count: null,
/**
* The frequency value.
* @type {ICAL.Recur.frequencyValues}
*/
freq: null,
/**
* The class identifier.
* @constant
* @type {String}
* @default "icalrecur"
*/
icalclass: "icalrecur",
/**
* The type name, to be used in the jCal object.
* @constant
* @type {String}
* @default "recur"
*/
icaltype: "recur",
/**
* Create a new iterator for this recurrence rule. The passed start date
* must be the start date of the event, not the start of the range to
* search in.
*
* @example
* var recur = comp.getFirstPropertyValue('rrule');
* var dtstart = comp.getFirstPropertyValue('dtstart');
* var iter = recur.iterator(dtstart);
* for (var next = iter.next(); next; next = iter.next()) {
* if (next.compare(rangeStart) < 0) {
* continue;
* }
* console.log(next.toString());
* }
*
* @param {ICAL.Time} aStart The item's start date
* @return {ICAL.RecurIterator} The recurrence iterator
*/
iterator: function(aStart) {
return new ICAL.RecurIterator({
rule: this,
dtstart: aStart
});
},
/**
* Returns a clone of the recurrence object.
*
* @return {ICAL.Recur} The cloned object
*/
clone: function clone() {
return new ICAL.Recur(this.toJSON());
},
/**
* Checks if the current rule is finite, i.e. has a count or until part.
*
* @return {Boolean} True, if the rule is finite
*/
isFinite: function isfinite() {
return !!(this.count || this.until);
},
/**
* Checks if the current rule has a count part, and not limited by an until
* part.
*
* @return {Boolean} True, if the rule is by count
*/
isByCount: function isbycount() {
return !!(this.count && !this.until);
},
/**
* Adds a component (part) to the recurrence rule. This is not a component
* in the sense of {@link ICAL.Component}, but a part of the recurrence
* rule, i.e. BYMONTH.
*
* @param {String} aType The name of the component part
* @param {Array|String} aValue The component value
*/
addComponent: function addPart(aType, aValue) {
var ucname = aType.toUpperCase();
if (ucname in this.parts) {
this.parts[ucname].push(aValue);
} else {
this.parts[ucname] = [aValue];
}
},
/**
* Sets the component value for the given by-part.
*
* @param {String} aType The component part name
* @param {Array} aValues The component values
*/
setComponent: function setComponent(aType, aValues) {
this.parts[aType.toUpperCase()] = aValues.slice();
},
/**
* Gets (a copy) of the requested component value.
*
* @param {String} aType The component part name
* @return {Array} The component part value
*/
getComponent: function getComponent(aType) {
var ucname = aType.toUpperCase();
return (ucname in this.parts ? this.parts[ucname].slice() : []);
},
/**
* Retrieves the next occurrence after the given recurrence id. See the
* guide on {@tutorial terminology} for more details.
*
* NOTE: Currently, this method iterates all occurrences from the start
* date. It should not be called in a loop for performance reasons. If you
* would like to get more than one occurrence, you can iterate the
* occurrences manually, see the example on the
* {@link ICAL.Recur#iterator iterator} method.
*
* @param {ICAL.Time} aStartTime The start of the event series
* @param {ICAL.Time} aRecurrenceId The date of the last occurrence
* @return {ICAL.Time} The next occurrence after
*/
getNextOccurrence: function getNextOccurrence(aStartTime, aRecurrenceId) {
var iter = this.iterator(aStartTime);
var next, cdt;
do {
next = iter.next();
} while (next && next.compare(aRecurrenceId) <= 0);
if (next && aRecurrenceId.zone) {
next.zone = aRecurrenceId.zone;
}
return next;
},
/**
* Sets up the current instance using members from the passed data object.
*
* @param {Object} data An object with members of the recurrence
* @param {ICAL.Recur.frequencyValues} freq The frequency value
* @param {Number=} data.interval The INTERVAL value
* @param {ICAL.Time.weekDay=} data.wkst The week start value
* @param {ICAL.Time=} data.until The end of the recurrence set
* @param {Number=} data.count The number of occurrences
* @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part
* @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part
* @param {Array.<Number>=} data.byhour The hours for the BYHOUR part
* @param {Array.<String>=} data.byday The BYDAY values
* @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part
* @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part
* @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part
* @param {Array.<Number>=} data.bymonth The month for the BYMONTH part
* @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part
*/
fromData: function(data) {
for (var key in data) {
var uckey = key.toUpperCase();
if (uckey in partDesign) {
if (Array.isArray(data[key])) {
this.parts[uckey] = data[key];
} else {
this.parts[uckey] = [data[key]];
}
} else {
this[key] = data[key];
}
}
if (this.wkst && typeof this.wkst != "number") {
this.wkst = ICAL.Recur.icalDayToNumericDay(this.wkst);
}
if (this.until && !(this.until instanceof ICAL.Time)) {
this.until = ICAL.Time.fromString(this.until);
}
},
/**
* The jCal representation of this recurrence type.
* @return {Object}
*/
toJSON: function() {
var res = Object.create(null);
res.freq = this.freq;
if (this.count) {
res.count = this.count;
}
if (this.interval > 1) {
res.interval = this.interval;
}
for (var k in this.parts) {
/* istanbul ignore if */
if (!this.parts.hasOwnProperty(k)) {
continue;
}
var kparts = this.parts[k];
if (Array.isArray(kparts) && kparts.length == 1) {
res[k.toLowerCase()] = kparts[0];
} else {
res[k.toLowerCase()] = ICAL.helpers.clone(this.parts[k]);
}
}
if (this.until) {
res.until = this.until.toString();
}
if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) {
res.wkst = ICAL.Recur.numericDayToIcalDay(this.wkst);
}
return res;
},
/**
* The string representation of this recurrence rule.
* @return {String}
*/
toString: function icalrecur_toString() {
// TODO retain order
var str = "FREQ=" + this.freq;
if (this.count) {
str += ";COUNT=" + this.count;
}
if (this.interval > 1) {
str += ";INTERVAL=" + this.interval;
}
for (var k in this.parts) {
/* istanbul ignore else */
if (this.parts.hasOwnProperty(k)) {
str += ";" + k + "=" + this.parts[k];
}
}
if (this.until) {
str += ';UNTIL=' + this.until.toString();
}
if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) {
str += ';WKST=' + ICAL.Recur.numericDayToIcalDay(this.wkst);
}
return str;
}
};
function parseNumericValue(type, min, max, value) {
var result = value;
if (value[0] === '+') {
result = value.substr(1);
}
result = ICAL.helpers.strictParseInt(result);
if (min !== undefined && value < min) {
throw new Error(
type + ': invalid value "' + value + '" must be > ' + min
);
}
if (max !== undefined && value > max) {
throw new Error(
type + ': invalid value "' + value + '" must be < ' + min
);
}
return result;
}
/**
* Convert an ical representation of a day (SU, MO, etc..)
* into a numeric value of that day.
*
* @param {String} string The iCalendar day name
* @return {Number} Numeric value of given day
*/
ICAL.Recur.icalDayToNumericDay = function toNumericDay(string) {
//XXX: this is here so we can deal
// with possibly invalid string values.
return DOW_MAP[string];
};
/**
* Convert a numeric day value into its ical representation (SU, MO, etc..)
*
* @param {Number} num Numeric value of given day
* @return {String} The ICAL day value, e.g SU,MO,...
*/
ICAL.Recur.numericDayToIcalDay = function toIcalDay(num) {
//XXX: this is here so we can deal with possibly invalid number values.
// Also, this allows consistent mapping between day numbers and day
// names for external users.
return REVERSE_DOW_MAP[num];
};
var VALID_DAY_NAMES = /^(SU|MO|TU|WE|TH|FR|SA)$/;
var VALID_BYDAY_PART = /^([+-])?(5[0-3]|[1-4][0-9]|[1-9])?(SU|MO|TU|WE|TH|FR|SA)$/;
/**
* Possible frequency values for the FREQ part
* (YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY)
*
* @typedef {String} frequencyValues
* @memberof ICAL.Recur
*/
var ALLOWED_FREQ = ['SECONDLY', 'MINUTELY', 'HOURLY',
'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'];
var optionDesign = {
FREQ: function(value, dict, fmtIcal) {
// yes this is actually equal or faster then regex.
// upside here is we can enumerate the valid values.
if (ALLOWED_FREQ.indexOf(value) !== -1) {
dict.freq = value;
} else {
throw new Error(
'invalid frequency "' + value + '" expected: "' +
ALLOWED_FREQ.join(', ') + '"'
);
}
},
COUNT: function(value, dict, fmtIcal) {
dict.count = ICAL.helpers.strictParseInt(value);
},
INTERVAL: function(value, dict, fmtIcal) {
dict.interval = ICAL.helpers.strictParseInt(value);
if (dict.interval < 1) {
// 0 or negative values are not allowed, some engines seem to generate
// it though. Assume 1 instead.
dict.interval = 1;
}
},
UNTIL: function(value, dict, fmtIcal) {
if (fmtIcal) {
if (value.length > 10) {
dict.until = ICAL.design.icalendar.value['date-time'].fromICAL(value);
} else {
dict.until = ICAL.design.icalendar.value.date.fromICAL(value);
}
} else {
dict.until = ICAL.Time.fromString(value);
}
},
WKST: function(value, dict, fmtIcal) {
if (VALID_DAY_NAMES.test(value)) {
dict.wkst = ICAL.Recur.icalDayToNumericDay(value);
} else {
throw new Error('invalid WKST value "' + value + '"');
}
}
};
var partDesign = {
BYSECOND: parseNumericValue.bind(this, 'BYSECOND', 0, 60),
BYMINUTE: parseNumericValue.bind(this, 'BYMINUTE', 0, 59),
BYHOUR: parseNumericValue.bind(this, 'BYHOUR', 0, 23),
BYDAY: function(value) {
if (VALID_BYDAY_PART.test(value)) {
return value;
} else {
throw new Error('invalid BYDAY value "' + value + '"');
}
},
BYMONTHDAY: parseNumericValue.bind(this, 'BYMONTHDAY', -31, 31),
BYYEARDAY: parseNumericValue.bind(this, 'BYYEARDAY', -366, 366),
BYWEEKNO: parseNumericValue.bind(this, 'BYWEEKNO', -53, 53),
BYMONTH: parseNumericValue.bind(this, 'BYMONTH', 0, 12),
BYSETPOS: parseNumericValue.bind(this, 'BYSETPOS', -366, 366)
};
/**
* Creates a new {@link ICAL.Recur} instance from the passed string.
*
* @param {String} string The string to parse
* @return {ICAL.Recur} The created recurrence instance
*/
ICAL.Recur.fromString = function(string) {
var data = ICAL.Recur._stringToData(string, false);
return new ICAL.Recur(data);
};
/**
* Creates a new {@link ICAL.Recur} instance using members from the passed
* data object.
*
* @param {Object} aData An object with members of the recurrence
* @param {ICAL.Recur.frequencyValues} freq The frequency value
* @param {Number=} aData.interval The INTERVAL value
* @param {ICAL.Time.weekDay=} aData.wkst The week start value
* @param {ICAL.Time=} aData.until The end of the recurrence set
* @param {Number=} aData.count The number of occurrences
* @param {Array.<Number>=} aData.bysecond The seconds for the BYSECOND part
* @param {Array.<Number>=} aData.byminute The minutes for the BYMINUTE part
* @param {Array.<Number>=} aData.byhour The hours for the BYHOUR part
* @param {Array.<String>=} aData.byday The BYDAY values
* @param {Array.<Number>=} aData.bymonthday The days for the BYMONTHDAY part
* @param {Array.<Number>=} aData.byyearday The days for the BYYEARDAY part
* @param {Array.<Number>=} aData.byweekno The weeks for the BYWEEKNO part
* @param {Array.<Number>=} aData.bymonth The month for the BYMONTH part
* @param {Array.<Number>=} aData.bysetpos The positionals for the BYSETPOS part
*/
ICAL.Recur.fromData = function(aData) {
return new ICAL.Recur(aData);
};
/**
* Converts a recurrence string to a data object, suitable for the fromData
* method.
*
* @param {String} string The string to parse
* @param {Boolean} fmtIcal If true, the string is considered to be an
* iCalendar string
* @return {ICAL.Recur} The recurrence instance
*/
ICAL.Recur._stringToData = function(string, fmtIcal) {
var dict = Object.create(null);
// split is slower in FF but fast enough.
// v8 however this is faster then manual split?
var values = string.split(';');
var len = values.length;
for (var i = 0; i < len; i++) {
var parts = values[i].split('=');
var ucname = parts[0].toUpperCase();
var lcname = parts[0].toLowerCase();
var name = (fmtIcal ? lcname : ucname);
var value = parts[1];
if (ucname in partDesign) {
var partArr = value.split(',');
var partArrIdx = 0;
var partArrLen = partArr.length;
for (; partArrIdx < partArrLen; partArrIdx++) {
partArr[partArrIdx] = partDesign[ucname](partArr[partArrIdx]);
}
dict[name] = (partArr.length == 1 ? partArr[0] : partArr);
} else if (ucname in optionDesign) {
optionDesign[ucname](value, dict, fmtIcal);
} else {
// Don't swallow unknown values. Just set them as they are.
dict[lcname] = value;
}
}
return dict;
};
})();
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* This symbol is further described later on
* @ignore
*/
ICAL.RecurIterator = (function() {
/**
* @classdesc
* An iterator for a single recurrence rule. This class usually doesn't have
* to be instanciated directly, the convenience method
* {@link ICAL.Recur#iterator} can be used.
*
* @description
* The options object may contain additional members when resuming iteration from a previous run
*
* @description
* The options object may contain additional members when resuming iteration
* from a previous run.
*
* @class
* @alias ICAL.RecurIterator
* @param {Object} options The iterator options
* @param {ICAL.Recur} options.rule The rule to iterate.
* @param {ICAL.Time} options.dtstart The start date of the event.
* @param {Boolean=} options.initialized When true, assume that options are
* from a previously constructed iterator. Initialization will not be
* repeated.
*/
function icalrecur_iterator(options) {
this.fromData(options);
}
icalrecur_iterator.prototype = {
/**
* True when iteration is finished.
* @type {Boolean}
*/
completed: false,
/**
* The rule that is being iterated
* @type {ICAL.Recur}
*/
rule: null,
/**
* The start date of the event being iterated.
* @type {ICAL.Time}
*/
dtstart: null,
/**
* The last occurrence that was returned from the
* {@link ICAL.RecurIterator#next} method.
* @type {ICAL.Time}
*/
last: null,
/**
* The sequence number from the occurrence
* @type {Number}
*/
occurrence_number: 0,
/**
* The indices used for the {@link ICAL.RecurIterator#by_data} object.
* @type {Object}
* @private
*/
by_indices: null,
/**
* If true, the iterator has already been initialized
* @type {Boolean}
* @private
*/
initialized: false,
/**
* The initializd by-data.
* @type {Object}
* @private
*/
by_data: null,
/**
* The expanded yeardays
* @type {Array}
* @private
*/
days: null,
/**
* The index in the {@link ICAL.RecurIterator#days} array.
* @type {Number}
* @private
*/
days_index: 0,
/**
* Initialize the recurrence iterator from the passed data object. This
* method is usually not called directly, you can initialize the iterator
* through the constructor.
*
* @param {Object} options The iterator options
* @param {ICAL.Recur} options.rule The rule to iterate.
* @param {ICAL.Time} options.dtstart The start date of the event.
* @param {Boolean=} options.initialized When true, assume that options are
* from a previously constructed iterator. Initialization will not be
* repeated.
*/
fromData: function(options) {
this.rule = ICAL.helpers.formatClassType(options.rule, ICAL.Recur);
if (!this.rule) {
throw new Error('iterator requires a (ICAL.Recur) rule');
}
this.dtstart = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time);
if (!this.dtstart) {
throw new Error('iterator requires a (ICAL.Time) dtstart');
}
if (options.by_data) {
this.by_data = options.by_data;
} else {
this.by_data = ICAL.helpers.clone(this.rule.parts, true);
}
if (options.occurrence_number)
this.occurrence_number = options.occurrence_number;
this.days = options.days || [];
if (options.last) {
this.last = ICAL.helpers.formatClassType(options.last, ICAL.Time);
}
this.by_indices = options.by_indices;
if (!this.by_indices) {
this.by_indices = {
"BYSECOND": 0,
"BYMINUTE": 0,
"BYHOUR": 0,
"BYDAY": 0,
"BYMONTH": 0,
"BYWEEKNO": 0,
"BYMONTHDAY": 0
};
}
this.initialized = options.initialized || false;
if (!this.initialized) {
this.init();
}
},
/**
* Intialize the iterator
* @private
*/
init: function icalrecur_iterator_init() {
this.initialized = true;
this.last = this.dtstart.clone();
var parts = this.by_data;
if ("BYDAY" in parts) {
// libical does this earlier when the rule is loaded, but we postpone to
// now so we can preserve the original order.
this.sort_byday_rules(parts.BYDAY, this.rule.wkst);
}
// If the BYYEARDAY appares, no other date rule part may appear
if ("BYYEARDAY" in parts) {
if ("BYMONTH" in parts || "BYWEEKNO" in parts ||
"BYMONTHDAY" in parts || "BYDAY" in parts) {
throw new Error("Invalid BYYEARDAY rule");
}
}
// BYWEEKNO and BYMONTHDAY rule parts may not both appear
if ("BYWEEKNO" in parts && "BYMONTHDAY" in parts) {
throw new Error("BYWEEKNO does not fit to BYMONTHDAY");
}
// For MONTHLY recurrences (FREQ=MONTHLY) neither BYYEARDAY nor
// BYWEEKNO may appear.
if (this.rule.freq == "MONTHLY" &&
("BYYEARDAY" in parts || "BYWEEKNO" in parts)) {
throw new Error("For MONTHLY recurrences neither BYYEARDAY nor BYWEEKNO may appear");
}
// For WEEKLY recurrences (FREQ=WEEKLY) neither BYMONTHDAY nor
// BYYEARDAY may appear.
if (this.rule.freq == "WEEKLY" &&
("BYYEARDAY" in parts || "BYMONTHDAY" in parts)) {
throw new Error("For WEEKLY recurrences neither BYMONTHDAY nor BYYEARDAY may appear");
}
// BYYEARDAY may only appear in YEARLY rules
if (this.rule.freq != "YEARLY" && "BYYEARDAY" in parts) {
throw new Error("BYYEARDAY may only appear in YEARLY rules");
}
this.last.second = this.setup_defaults("BYSECOND", "SECONDLY", this.dtstart.second);
this.last.minute = this.setup_defaults("BYMINUTE", "MINUTELY", this.dtstart.minute);
this.last.hour = this.setup_defaults("BYHOUR", "HOURLY", this.dtstart.hour);
this.last.day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day);
this.last.month = this.setup_defaults("BYMONTH", "MONTHLY", this.dtstart.month);
if (this.rule.freq == "WEEKLY") {
if ("BYDAY" in parts) {
var bydayParts = this.ruleDayOfWeek(parts.BYDAY[0]);
var pos = bydayParts[0];
var dow = bydayParts[1];
var wkdy = dow - this.last.dayOfWeek();
if ((this.last.dayOfWeek() < dow && wkdy >= 0) || wkdy < 0) {
// Initial time is after first day of BYDAY data
this.last.day += wkdy;
}
} else {
var dayName = ICAL.Recur.numericDayToIcalDay(this.dtstart.dayOfWeek());
parts.BYDAY = [dayName];
}
}
if (this.rule.freq == "YEARLY") {
for (;;) {
this.expand_year_days(this.last.year);
if (this.days.length > 0) {
break;
}
this.increment_year(this.rule.interval);
}
this._nextByYearDay();
}
if (this.rule.freq == "MONTHLY" && this.has_by_data("BYDAY")) {
var tempLast = null;
var initLast = this.last.clone();
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
// Check every weekday in BYDAY with relative dow and pos.
for (var i in this.by_data.BYDAY) {
/* istanbul ignore if */
if (!this.by_data.BYDAY.hasOwnProperty(i)) {
continue;
}
this.last = initLast.clone();
var bydayParts = this.ruleDayOfWeek(this.by_data.BYDAY[i]);
var pos = bydayParts[0];
var dow = bydayParts[1];
var dayOfMonth = this.last.nthWeekDay(dow, pos);
// If |pos| >= 6, the byday is invalid for a monthly rule.
if (pos >= 6 || pos <= -6) {
throw new Error("Malformed values in BYDAY part");
}
// If a Byday with pos=+/-5 is not in the current month it
// must be searched in the next months.
if (dayOfMonth > daysInMonth || dayOfMonth <= 0) {
// Skip if we have already found a "last" in this month.
if (tempLast && tempLast.month == initLast.month) {
continue;
}
while (dayOfMonth > daysInMonth || dayOfMonth <= 0) {
this.increment_month();
daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
dayOfMonth = this.last.nthWeekDay(dow, pos);
}
}
this.last.day = dayOfMonth;
if (!tempLast || this.last.compare(tempLast) < 0) {
tempLast = this.last.clone();
}
}
this.last = tempLast.clone();
//XXX: This feels like a hack, but we need to initialize
// the BYMONTHDAY case correctly and byDayAndMonthDay handles
// this case. It accepts a special flag which will avoid incrementing
// the initial value without the flag days that match the start time
// would be missed.
if (this.has_by_data('BYMONTHDAY')) {
this._byDayAndMonthDay(true);
}
if (this.last.day > daysInMonth || this.last.day == 0) {
throw new Error("Malformed values in BYDAY part");
}
} else if (this.has_by_data("BYMONTHDAY")) {
if (this.last.day < 0) {
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
this.last.day = daysInMonth + this.last.day + 1;
}
}
},
/**
* Retrieve the next occurrence from the iterator.
* @return {ICAL.Time}
*/
next: function icalrecur_iterator_next() {
var before = (this.last ? this.last.clone() : null);
if ((this.rule.count && this.occurrence_number >= this.rule.count) ||
(this.rule.until && this.last.compare(this.rule.until) > 0)) {
//XXX: right now this is just a flag and has no impact
// we can simplify the above case to check for completed later.
this.completed = true;
return null;
}
if (this.occurrence_number == 0 && this.last.compare(this.dtstart) >= 0) {
// First of all, give the instance that was initialized
this.occurrence_number++;
return this.last;
}
var valid;
do {
valid = 1;
switch (this.rule.freq) {
case "SECONDLY":
this.next_second();
break;
case "MINUTELY":
this.next_minute();
break;
case "HOURLY":
this.next_hour();
break;
case "DAILY":
this.next_day();
break;
case "WEEKLY":
this.next_week();
break;
case "MONTHLY":
valid = this.next_month();
break;
case "YEARLY":
this.next_year();
break;
default:
return null;
}
} while (!this.check_contracting_rules() ||
this.last.compare(this.dtstart) < 0 ||
!valid);
// TODO is this valid?
if (this.last.compare(before) == 0) {
throw new Error("Same occurrence found twice, protecting " +
"you from death by recursion");
}
if (this.rule.until && this.last.compare(this.rule.until) > 0) {
this.completed = true;
return null;
} else {
this.occurrence_number++;
return this.last;
}
},
next_second: function next_second() {
return this.next_generic("BYSECOND", "SECONDLY", "second", "minute");
},
increment_second: function increment_second(inc) {
return this.increment_generic(inc, "second", 60, "minute");
},
next_minute: function next_minute() {
return this.next_generic("BYMINUTE", "MINUTELY",
"minute", "hour", "next_second");
},
increment_minute: function increment_minute(inc) {
return this.increment_generic(inc, "minute", 60, "hour");
},
next_hour: function next_hour() {
return this.next_generic("BYHOUR", "HOURLY", "hour",
"monthday", "next_minute");
},
increment_hour: function increment_hour(inc) {
this.increment_generic(inc, "hour", 24, "monthday");
},
next_day: function next_day() {
var has_by_day = ("BYDAY" in this.by_data);
var this_freq = (this.rule.freq == "DAILY");
if (this.next_hour() == 0) {
return 0;
}
if (this_freq) {
this.increment_monthday(this.rule.interval);
} else {
this.increment_monthday(1);
}
return 0;
},
next_week: function next_week() {
var end_of_data = 0;
if (this.next_weekday_by_week() == 0) {
return end_of_data;
}
if (this.has_by_data("BYWEEKNO")) {
var idx = ++this.by_indices.BYWEEKNO;
if (this.by_indices.BYWEEKNO == this.by_data.BYWEEKNO.length) {
this.by_indices.BYWEEKNO = 0;
end_of_data = 1;
}
// HACK should be first month of the year
this.last.month = 1;
this.last.day = 1;
var week_no = this.by_data.BYWEEKNO[this.by_indices.BYWEEKNO];
this.last.day += 7 * week_no;
if (end_of_data) {
this.increment_year(1);
}
} else {
// Jump to the next week
this.increment_monthday(7 * this.rule.interval);
}
return end_of_data;
},
/**
* Normalize each by day rule for a given year/month.
* Takes into account ordering and negative rules
*
* @private
* @param {Number} year Current year.
* @param {Number} month Current month.
* @param {Array} rules Array of rules.
*
* @return {Array} sorted and normalized rules.
* Negative rules will be expanded to their
* correct positive values for easier processing.
*/
normalizeByMonthDayRules: function(year, month, rules) {
var daysInMonth = ICAL.Time.daysInMonth(month, year);
// XXX: This is probably bad for performance to allocate
// a new array for each month we scan, if possible
// we should try to optimize this...
var newRules = [];
var ruleIdx = 0;
var len = rules.length;
var rule;
for (; ruleIdx < len; ruleIdx++) {
rule = rules[ruleIdx];
// if this rule falls outside of given
// month discard it.
if (Math.abs(rule) > daysInMonth) {
continue;
}
// negative case
if (rule < 0) {
// we add (not subtract its a negative number)
// one from the rule because 1 === last day of month
rule = daysInMonth + (rule + 1);
} else if (rule === 0) {
// skip zero its invalid.
continue;
}
// only add unique items...
if (newRules.indexOf(rule) === -1) {
newRules.push(rule);
}
}
// unique and sort
return newRules.sort(function(a, b) { return a - b; });
},
/**
* NOTES:
* We are given a list of dates in the month (BYMONTHDAY) (23, etc..)
* Also we are given a list of days (BYDAY) (MO, 2SU, etc..) when
* both conditions match a given date (this.last.day) iteration stops.
*
* @private
* @param {Boolean=} isInit When given true will not increment the
* current day (this.last).
*/
_byDayAndMonthDay: function(isInit) {
var byMonthDay; // setup in initMonth
var byDay = this.by_data.BYDAY;
var date;
var dateIdx = 0;
var dateLen; // setup in initMonth
var dayLen = byDay.length;
// we are not valid by default
var dataIsValid = 0;
var daysInMonth;
var self = this;
// we need a copy of this, because a DateTime gets normalized
// automatically if the day is out of range. At some points we
// set the last day to 0 to start counting.
var lastDay = this.last.day;
function initMonth() {
daysInMonth = ICAL.Time.daysInMonth(
self.last.month, self.last.year
);
byMonthDay = self.normalizeByMonthDayRules(
self.last.year,
self.last.month,
self.by_data.BYMONTHDAY
);
dateLen = byMonthDay.length;
// For the case of more than one occurrence in one month
// we have to be sure to start searching after the last
// found date or at the last BYMONTHDAY, unless we are
// initializing the iterator because in this case we have
// to consider the last found date too.
while (byMonthDay[dateIdx] <= lastDay &&
!(isInit && byMonthDay[dateIdx] == lastDay) &&
dateIdx < dateLen - 1) {
dateIdx++;
}
}
function nextMonth() {
// since the day is incremented at the start
// of the loop below, we need to start at 0
lastDay = 0;
self.increment_month();
dateIdx = 0;
initMonth();
}
initMonth();
// should come after initMonth
if (isInit) {
lastDay -= 1;
}
// Use a counter to avoid an infinite loop with malformed rules.
// Stop checking after 4 years so we consider also a leap year.
var monthsCounter = 48;
while (!dataIsValid && monthsCounter) {
monthsCounter--;
// increment the current date. This is really
// important otherwise we may fall into the infinite
// loop trap. The initial date takes care of the case
// where the current date is the date we are looking
// for.
date = lastDay + 1;
if (date > daysInMonth) {
nextMonth();
continue;
}
// find next date
var next = byMonthDay[dateIdx++];
// this logic is dependant on the BYMONTHDAYS
// being in order (which is done by #normalizeByMonthDayRules)
if (next >= date) {
// if the next month day is in the future jump to it.
lastDay = next;
} else {
// in this case the 'next' monthday has past
// we must move to the month.
nextMonth();
continue;
}
// Now we can loop through the day rules to see
// if one matches the current month date.
for (var dayIdx = 0; dayIdx < dayLen; dayIdx++) {
var parts = this.ruleDayOfWeek(byDay[dayIdx]);
var pos = parts[0];
var dow = parts[1];
this.last.day = lastDay;
if (this.last.isNthWeekDay(dow, pos)) {
// when we find the valid one we can mark
// the conditions as met and break the loop.
// (Because we have this condition above
// it will also break the parent loop).
dataIsValid = 1;
break;
}
}
// Its completely possible that the combination
// cannot be matched in the current month.
// When we reach the end of possible combinations
// in the current month we iterate to the next one.
// since dateIdx is incremented right after getting
// "next", we don't need dateLen -1 here.
if (!dataIsValid && dateIdx === dateLen) {
nextMonth();
continue;
}
}
if (monthsCounter <= 0) {
// Checked 4 years without finding a Byday that matches
// a Bymonthday. Maybe the rule is not correct.
throw new Error("Malformed values in BYDAY combined with BYMONTHDAY parts");
}
return dataIsValid;
},
next_month: function next_month() {
var this_freq = (this.rule.freq == "MONTHLY");
var data_valid = 1;
if (this.next_hour() == 0) {
return data_valid;
}
if (this.has_by_data("BYDAY") && this.has_by_data("BYMONTHDAY")) {
data_valid = this._byDayAndMonthDay();
} else if (this.has_by_data("BYDAY")) {
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
var setpos = 0;
var setpos_total = 0;
if (this.has_by_data("BYSETPOS")) {
var last_day = this.last.day;
for (var day = 1; day <= daysInMonth; day++) {
this.last.day = day;
if (this.is_day_in_byday(this.last)) {
setpos_total++;
if (day <= last_day) {
setpos++;
}
}
}
this.last.day = last_day;
}
data_valid = 0;
for (var day = this.last.day + 1; day <= daysInMonth; day++) {
this.last.day = day;
if (this.is_day_in_byday(this.last)) {
if (!this.has_by_data("BYSETPOS") ||
this.check_set_position(++setpos) ||
this.check_set_position(setpos - setpos_total - 1)) {
data_valid = 1;
break;
}
}
}
if (day > daysInMonth) {
this.last.day = 1;
this.increment_month();
if (this.is_day_in_byday(this.last)) {
if (!this.has_by_data("BYSETPOS") || this.check_set_position(1)) {
data_valid = 1;
}
} else {
data_valid = 0;
}
}
} else if (this.has_by_data("BYMONTHDAY")) {
this.by_indices.BYMONTHDAY++;
if (this.by_indices.BYMONTHDAY >= this.by_data.BYMONTHDAY.length) {
this.by_indices.BYMONTHDAY = 0;
this.increment_month();
}
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
var day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY];
if (day < 0) {
day = daysInMonth + day + 1;
}
if (day > daysInMonth) {
this.last.day = 1;
data_valid = this.is_day_in_byday(this.last);
} else {
this.last.day = day;
}
} else {
this.increment_month();
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
if (this.by_data.BYMONTHDAY[0] > daysInMonth) {
data_valid = 0;
} else {
this.last.day = this.by_data.BYMONTHDAY[0];
}
}
return data_valid;
},
next_weekday_by_week: function next_weekday_by_week() {
var end_of_data = 0;
if (this.next_hour() == 0) {
return end_of_data;
}
if (!this.has_by_data("BYDAY")) {
return 1;
}
for (;;) {
var tt = new ICAL.Time();
this.by_indices.BYDAY++;
if (this.by_indices.BYDAY == Object.keys(this.by_data.BYDAY).length) {
this.by_indices.BYDAY = 0;
end_of_data = 1;
}
var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY];
var parts = this.ruleDayOfWeek(coded_day);
var dow = parts[1];
dow -= this.rule.wkst;
if (dow < 0) {
dow += 7;
}
tt.year = this.last.year;
tt.month = this.last.month;
tt.day = this.last.day;
var startOfWeek = tt.startDoyWeek(this.rule.wkst);
if (dow + startOfWeek < 1) {
// The selected date is in the previous year
if (!end_of_data) {
continue;
}
}
var next = ICAL.Time.fromDayOfYear(startOfWeek + dow,
this.last.year);
/**
* The normalization horrors below are due to
* the fact that when the year/month/day changes
* it can effect the other operations that come after.
*/
this.last.year = next.year;
this.last.month = next.month;
this.last.day = next.day;
return end_of_data;
}
},
next_year: function next_year() {
if (this.next_hour() == 0) {
return 0;
}
if (++this.days_index == this.days.length) {
this.days_index = 0;
do {
this.increment_year(this.rule.interval);
this.expand_year_days(this.last.year);
} while (this.days.length == 0);
}
this._nextByYearDay();
return 1;
},
_nextByYearDay: function _nextByYearDay() {
var doy = this.days[this.days_index];
var year = this.last.year;
if (doy < 1) {
// Time.fromDayOfYear(doy, year) indexes relative to the
// start of the given year. That is different from the
// semantics of BYYEARDAY where negative indexes are an
// offset from the end of the given year.
doy += 1;
year += 1;
}
var next = ICAL.Time.fromDayOfYear(doy, year);
this.last.day = next.day;
this.last.month = next.month;
},
ruleDayOfWeek: function ruleDayOfWeek(dow) {
var matches = dow.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/);
if (matches) {
var pos = parseInt(matches[1] || 0, 10);
dow = ICAL.Recur.icalDayToNumericDay(matches[2]);
return [pos, dow];
} else {
return [0, 0];
}
},
next_generic: function next_generic(aRuleType, aInterval, aDateAttr,
aFollowingAttr, aPreviousIncr) {
var has_by_rule = (aRuleType in this.by_data);
var this_freq = (this.rule.freq == aInterval);
var end_of_data = 0;
if (aPreviousIncr && this[aPreviousIncr]() == 0) {
return end_of_data;
}
if (has_by_rule) {
this.by_indices[aRuleType]++;
var idx = this.by_indices[aRuleType];
var dta = this.by_data[aRuleType];
if (this.by_indices[aRuleType] == dta.length) {
this.by_indices[aRuleType] = 0;
end_of_data = 1;
}
this.last[aDateAttr] = dta[this.by_indices[aRuleType]];
} else if (this_freq) {
this["increment_" + aDateAttr](this.rule.interval);
}
if (has_by_rule && end_of_data && this_freq) {
this["increment_" + aFollowingAttr](1);
}
return end_of_data;
},
increment_monthday: function increment_monthday(inc) {
for (var i = 0; i < inc; i++) {
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
this.last.day++;
if (this.last.day > daysInMonth) {
this.last.day -= daysInMonth;
this.increment_month();
}
}
},
increment_month: function increment_month() {
this.last.day = 1;
if (this.has_by_data("BYMONTH")) {
this.by_indices.BYMONTH++;
if (this.by_indices.BYMONTH == this.by_data.BYMONTH.length) {
this.by_indices.BYMONTH = 0;
this.increment_year(1);
}
this.last.month = this.by_data.BYMONTH[this.by_indices.BYMONTH];
} else {
if (this.rule.freq == "MONTHLY") {
this.last.month += this.rule.interval;
} else {
this.last.month++;
}
this.last.month--;
var years = ICAL.helpers.trunc(this.last.month / 12);
this.last.month %= 12;
this.last.month++;
if (years != 0) {
this.increment_year(years);
}
}
},
increment_year: function increment_year(inc) {
this.last.year += inc;
},
increment_generic: function increment_generic(inc, aDateAttr,
aFactor, aNextIncrement) {
this.last[aDateAttr] += inc;
var nextunit = ICAL.helpers.trunc(this.last[aDateAttr] / aFactor);
this.last[aDateAttr] %= aFactor;
if (nextunit != 0) {
this["increment_" + aNextIncrement](nextunit);
}
},
has_by_data: function has_by_data(aRuleType) {
return (aRuleType in this.rule.parts);
},
expand_year_days: function expand_year_days(aYear) {
var t = new ICAL.Time();
this.days = [];
// We need our own copy with a few keys set
var parts = {};
var rules = ["BYDAY", "BYWEEKNO", "BYMONTHDAY", "BYMONTH", "BYYEARDAY"];
for (var p in rules) {
/* istanbul ignore else */
if (rules.hasOwnProperty(p)) {
var part = rules[p];
if (part in this.rule.parts) {
parts[part] = this.rule.parts[part];
}
}
}
if ("BYMONTH" in parts && "BYWEEKNO" in parts) {
var valid = 1;
var validWeeks = {};
t.year = aYear;
t.isDate = true;
for (var monthIdx = 0; monthIdx < this.by_data.BYMONTH.length; monthIdx++) {
var month = this.by_data.BYMONTH[monthIdx];
t.month = month;
t.day = 1;
var first_week = t.weekNumber(this.rule.wkst);
t.day = ICAL.Time.daysInMonth(month, aYear);
var last_week = t.weekNumber(this.rule.wkst);
for (monthIdx = first_week; monthIdx < last_week; monthIdx++) {
validWeeks[monthIdx] = 1;
}
}
for (var weekIdx = 0; weekIdx < this.by_data.BYWEEKNO.length && valid; weekIdx++) {
var weekno = this.by_data.BYWEEKNO[weekIdx];
if (weekno < 52) {
valid &= validWeeks[weekIdx];
} else {
valid = 0;
}
}
if (valid) {
delete parts.BYMONTH;
} else {
delete parts.BYWEEKNO;
}
}
var partCount = Object.keys(parts).length;
if (partCount == 0) {
var t1 = this.dtstart.clone();
t1.year = this.last.year;
this.days.push(t1.dayOfYear());
} else if (partCount == 1 && "BYMONTH" in parts) {
for (var monthkey in this.by_data.BYMONTH) {
/* istanbul ignore if */
if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) {
continue;
}
var t2 = this.dtstart.clone();
t2.year = aYear;
t2.month = this.by_data.BYMONTH[monthkey];
t2.isDate = true;
this.days.push(t2.dayOfYear());
}
} else if (partCount == 1 && "BYMONTHDAY" in parts) {
for (var monthdaykey in this.by_data.BYMONTHDAY) {
/* istanbul ignore if */
if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) {
continue;
}
var t3 = this.dtstart.clone();
var day_ = this.by_data.BYMONTHDAY[monthdaykey];
if (day_ < 0) {
var daysInMonth = ICAL.Time.daysInMonth(t3.month, aYear);
day_ = day_ + daysInMonth + 1;
}
t3.day = day_;
t3.year = aYear;
t3.isDate = true;
this.days.push(t3.dayOfYear());
}
} else if (partCount == 2 &&
"BYMONTHDAY" in parts &&
"BYMONTH" in parts) {
for (var monthkey in this.by_data.BYMONTH) {
/* istanbul ignore if */
if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) {
continue;
}
var month_ = this.by_data.BYMONTH[monthkey];
var daysInMonth = ICAL.Time.daysInMonth(month_, aYear);
for (var monthdaykey in this.by_data.BYMONTHDAY) {
/* istanbul ignore if */
if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) {
continue;
}
var day_ = this.by_data.BYMONTHDAY[monthdaykey];
if (day_ < 0) {
day_ = day_ + daysInMonth + 1;
}
t.day = day_;
t.month = month_;
t.year = aYear;
t.isDate = true;
this.days.push(t.dayOfYear());
}
}
} else if (partCount == 1 && "BYWEEKNO" in parts) {
// TODO unimplemented in libical
} else if (partCount == 2 &&
"BYWEEKNO" in parts &&
"BYMONTHDAY" in parts) {
// TODO unimplemented in libical
} else if (partCount == 1 && "BYDAY" in parts) {
this.days = this.days.concat(this.expand_by_day(aYear));
} else if (partCount == 2 && "BYDAY" in parts && "BYMONTH" in parts) {
for (var monthkey in this.by_data.BYMONTH) {
/* istanbul ignore if */
if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) {
continue;
}
var month = this.by_data.BYMONTH[monthkey];
var daysInMonth = ICAL.Time.daysInMonth(month, aYear);
t.year = aYear;
t.month = this.by_data.BYMONTH[monthkey];
t.day = 1;
t.isDate = true;
var first_dow = t.dayOfWeek();
var doy_offset = t.dayOfYear() - 1;
t.day = daysInMonth;
var last_dow = t.dayOfWeek();
if (this.has_by_data("BYSETPOS")) {
var set_pos_counter = 0;
var by_month_day = [];
for (var day = 1; day <= daysInMonth; day++) {
t.day = day;
if (this.is_day_in_byday(t)) {
by_month_day.push(day);
}
}
for (var spIndex = 0; spIndex < by_month_day.length; spIndex++) {
if (this.check_set_position(spIndex + 1) ||
this.check_set_position(spIndex - by_month_day.length)) {
this.days.push(doy_offset + by_month_day[spIndex]);
}
}
} else {
for (var daycodedkey in this.by_data.BYDAY) {
/* istanbul ignore if */
if (!this.by_data.BYDAY.hasOwnProperty(daycodedkey)) {
continue;
}
var coded_day = this.by_data.BYDAY[daycodedkey];
var bydayParts = this.ruleDayOfWeek(coded_day);
var pos = bydayParts[0];
var dow = bydayParts[1];
var month_day;
var first_matching_day = ((dow + 7 - first_dow) % 7) + 1;
var last_matching_day = daysInMonth - ((last_dow + 7 - dow) % 7);
if (pos == 0) {
for (var day = first_matching_day; day <= daysInMonth; day += 7) {
this.days.push(doy_offset + day);
}
} else if (pos > 0) {
month_day = first_matching_day + (pos - 1) * 7;
if (month_day <= daysInMonth) {
this.days.push(doy_offset + month_day);
}
} else {
month_day = last_matching_day + (pos + 1) * 7;
if (month_day > 0) {
this.days.push(doy_offset + month_day);
}
}
}
}
}
// Return dates in order of occurrence (1,2,3,...) instead
// of by groups of weekdays (1,8,15,...,2,9,16,...).
this.days.sort(function(a, b) { return a - b; }); // Comparator function allows to sort numbers.
} else if (partCount == 2 && "BYDAY" in parts && "BYMONTHDAY" in parts) {
var expandedDays = this.expand_by_day(aYear);
for (var daykey in expandedDays) {
/* istanbul ignore if */
if (!expandedDays.hasOwnProperty(daykey)) {
continue;
}
var day = expandedDays[daykey];
var tt = ICAL.Time.fromDayOfYear(day, aYear);
if (this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) {
this.days.push(day);
}
}
} else if (partCount == 3 &&
"BYDAY" in parts &&
"BYMONTHDAY" in parts &&
"BYMONTH" in parts) {
var expandedDays = this.expand_by_day(aYear);
for (var daykey in expandedDays) {
/* istanbul ignore if */
if (!expandedDays.hasOwnProperty(daykey)) {
continue;
}
var day = expandedDays[daykey];
var tt = ICAL.Time.fromDayOfYear(day, aYear);
if (this.by_data.BYMONTH.indexOf(tt.month) >= 0 &&
this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) {
this.days.push(day);
}
}
} else if (partCount == 2 && "BYDAY" in parts && "BYWEEKNO" in parts) {
var expandedDays = this.expand_by_day(aYear);
for (var daykey in expandedDays) {
/* istanbul ignore if */
if (!expandedDays.hasOwnProperty(daykey)) {
continue;
}
var day = expandedDays[daykey];
var tt = ICAL.Time.fromDayOfYear(day, aYear);
var weekno = tt.weekNumber(this.rule.wkst);
if (this.by_data.BYWEEKNO.indexOf(weekno)) {
this.days.push(day);
}
}
} else if (partCount == 3 &&
"BYDAY" in parts &&
"BYWEEKNO" in parts &&
"BYMONTHDAY" in parts) {
// TODO unimplemted in libical
} else if (partCount == 1 && "BYYEARDAY" in parts) {
this.days = this.days.concat(this.by_data.BYYEARDAY);
} else {
this.days = [];
}
return 0;
},
expand_by_day: function expand_by_day(aYear) {
var days_list = [];
var tmp = this.last.clone();
tmp.year = aYear;
tmp.month = 1;
tmp.day = 1;
tmp.isDate = true;
var start_dow = tmp.dayOfWeek();
tmp.month = 12;
tmp.day = 31;
tmp.isDate = true;
var end_dow = tmp.dayOfWeek();
var end_year_day = tmp.dayOfYear();
for (var daykey in this.by_data.BYDAY) {
/* istanbul ignore if */
if (!this.by_data.BYDAY.hasOwnProperty(daykey)) {
continue;
}
var day = this.by_data.BYDAY[daykey];
var parts = this.ruleDayOfWeek(day);
var pos = parts[0];
var dow = parts[1];
if (pos == 0) {
var tmp_start_doy = ((dow + 7 - start_dow) % 7) + 1;
for (var doy = tmp_start_doy; doy <= end_year_day; doy += 7) {
days_list.push(doy);
}
} else if (pos > 0) {
var first;
if (dow >= start_dow) {
first = dow - start_dow + 1;
} else {
first = dow - start_dow + 8;
}
days_list.push(first + (pos - 1) * 7);
} else {
var last;
pos = -pos;
if (dow <= end_dow) {
last = end_year_day - end_dow + dow;
} else {
last = end_year_day - end_dow + dow - 7;
}
days_list.push(last - (pos - 1) * 7);
}
}
return days_list;
},
is_day_in_byday: function is_day_in_byday(tt) {
for (var daykey in this.by_data.BYDAY) {
/* istanbul ignore if */
if (!this.by_data.BYDAY.hasOwnProperty(daykey)) {
continue;
}
var day = this.by_data.BYDAY[daykey];
var parts = this.ruleDayOfWeek(day);
var pos = parts[0];
var dow = parts[1];
var this_dow = tt.dayOfWeek();
if ((pos == 0 && dow == this_dow) ||
(tt.nthWeekDay(dow, pos) == tt.day)) {
return 1;
}
}
return 0;
},
/**
* Checks if given value is in BYSETPOS.
*
* @private
* @param {Numeric} aPos position to check for.
* @return {Boolean} false unless BYSETPOS rules exist
* and the given value is present in rules.
*/
check_set_position: function check_set_position(aPos) {
if (this.has_by_data('BYSETPOS')) {
var idx = this.by_data.BYSETPOS.indexOf(aPos);
// negative numbers are not false-y
return idx !== -1;
}
return false;
},
sort_byday_rules: function icalrecur_sort_byday_rules(aRules, aWeekStart) {
for (var i = 0; i < aRules.length; i++) {
for (var j = 0; j < i; j++) {
var one = this.ruleDayOfWeek(aRules[j])[1];
var two = this.ruleDayOfWeek(aRules[i])[1];
one -= aWeekStart;
two -= aWeekStart;
if (one < 0) one += 7;
if (two < 0) two += 7;
if (one > two) {
var tmp = aRules[i];
aRules[i] = aRules[j];
aRules[j] = tmp;
}
}
}
},
check_contract_restriction: function check_contract_restriction(aRuleType, v) {
var indexMapValue = icalrecur_iterator._indexMap[aRuleType];
var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue];
var pass = false;
if (aRuleType in this.by_data &&
ruleMapValue == icalrecur_iterator.CONTRACT) {
var ruleType = this.by_data[aRuleType];
for (var bydatakey in ruleType) {
/* istanbul ignore else */
if (ruleType.hasOwnProperty(bydatakey)) {
if (ruleType[bydatakey] == v) {
pass = true;
break;
}
}
}
} else {
// Not a contracting byrule or has no data, test passes
pass = true;
}
return pass;
},
check_contracting_rules: function check_contracting_rules() {
var dow = this.last.dayOfWeek();
var weekNo = this.last.weekNumber(this.rule.wkst);
var doy = this.last.dayOfYear();
return (this.check_contract_restriction("BYSECOND", this.last.second) &&
this.check_contract_restriction("BYMINUTE", this.last.minute) &&
this.check_contract_restriction("BYHOUR", this.last.hour) &&
this.check_contract_restriction("BYDAY", ICAL.Recur.numericDayToIcalDay(dow)) &&
this.check_contract_restriction("BYWEEKNO", weekNo) &&
this.check_contract_restriction("BYMONTHDAY", this.last.day) &&
this.check_contract_restriction("BYMONTH", this.last.month) &&
this.check_contract_restriction("BYYEARDAY", doy));
},
setup_defaults: function setup_defaults(aRuleType, req, deftime) {
var indexMapValue = icalrecur_iterator._indexMap[aRuleType];
var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue];
if (ruleMapValue != icalrecur_iterator.CONTRACT) {
if (!(aRuleType in this.by_data)) {
this.by_data[aRuleType] = [deftime];
}
if (this.rule.freq != req) {
return this.by_data[aRuleType][0];
}
}
return deftime;
},
/**
* Convert iterator into a serialize-able object. Will preserve current
* iteration sequence to ensure the seamless continuation of the recurrence
* rule.
* @return {Object}
*/
toJSON: function() {
var result = Object.create(null);
result.initialized = this.initialized;
result.rule = this.rule.toJSON();
result.dtstart = this.dtstart.toJSON();
result.by_data = this.by_data;
result.days = this.days;
result.last = this.last.toJSON();
result.by_indices = this.by_indices;
result.occurrence_number = this.occurrence_number;
return result;
}
};
icalrecur_iterator._indexMap = {
"BYSECOND": 0,
"BYMINUTE": 1,
"BYHOUR": 2,
"BYDAY": 3,
"BYMONTHDAY": 4,
"BYYEARDAY": 5,
"BYWEEKNO": 6,
"BYMONTH": 7,
"BYSETPOS": 8
};
icalrecur_iterator._expandMap = {
"SECONDLY": [1, 1, 1, 1, 1, 1, 1, 1],
"MINUTELY": [2, 1, 1, 1, 1, 1, 1, 1],
"HOURLY": [2, 2, 1, 1, 1, 1, 1, 1],
"DAILY": [2, 2, 2, 1, 1, 1, 1, 1],
"WEEKLY": [2, 2, 2, 2, 3, 3, 1, 1],
"MONTHLY": [2, 2, 2, 2, 2, 3, 3, 1],
"YEARLY": [2, 2, 2, 2, 2, 2, 2, 2]
};
icalrecur_iterator.UNKNOWN = 0;
icalrecur_iterator.CONTRACT = 1;
icalrecur_iterator.EXPAND = 2;
icalrecur_iterator.ILLEGAL = 3;
return icalrecur_iterator;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* This symbol is further described later on
* @ignore
*/
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');
}
/**
* @classdesc
* 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.
*
* @example
* // assuming event is a parsed ical component
* var event;
*
* var expand = new ICAL.RecurExpansion({
* component: event,
* dtstart: 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));
*
* @description
* The options object can be filled with the specified initial values. It can
* also contain additional members, as a result of serializing a previous
* expansion state, as shown in the example.
*
* @class
* @alias ICAL.RecurExpansion
* @param {Object} options
* Recurrence expansion options
* @param {ICAL.Time} options.dtstart
* Start time of the event
* @param {ICAL.Component=} options.component
* Component for expansion, required if not resuming.
*/
function RecurExpansion(options) {
this.ruleDates = [];
this.exDates = [];
this.fromData(options);
}
RecurExpansion.prototype = {
/**
* True when iteration is fully completed.
* @type {Boolean}
*/
complete: false,
/**
* Array of rrule iterators.
*
* @type {ICAL.RecurIterator[]}
* @private
*/
ruleIterators: null,
/**
* Array of rdate instances.
*
* @type {ICAL.Time[]}
* @private
*/
ruleDates: null,
/**
* Array of exdate instances.
*
* @type {ICAL.Time[]}
* @private
*/
exDates: null,
/**
* Current position in ruleDates array.
* @type {Number}
* @private
*/
ruleDateInc: 0,
/**
* Current position in exDates array
* @type {Number}
* @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,
/**
* Initialize the recurrence expansion from the data object. The options
* object may also contain additional members, see the
* {@link ICAL.RecurExpansion constructor} for more details.
*
* @param {Object} options
* Recurrence expansion options
* @param {ICAL.Time} options.dtstart
* Start time of the event
* @param {ICAL.Component=} options.component
* Component for expansion, required if not resuming.
*/
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) || start.clone();
if (!options.ruleIterators) {
throw new Error('.ruleIterators or .component must be given');
}
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;
}
}
},
/**
* Retrieve the next occurrence in the series.
* @return {ICAL.Time}
*/
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. This format can be passed
* back into the expansion to resume iteration.
* @return {Object}
*/
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;
},
/**
* Extract all dates from the properties in the given component. The
* properties will be filtered by the property name.
*
* @private
* @param {ICAL.Component} component The component to search in
* @param {String} propertyName The property name to search for
* @return {ICAL.Time[]} The extracted dates.
*/
_extractDates: function(component, propertyName) {
function handleProp(prop) {
idx = ICAL.helpers.binsearchInsert(
result,
prop,
compareTime
);
// ordered insert
result.splice(idx, 0, prop);
}
var result = [];
var props = component.getAllProperties(propertyName);
var len = props.length;
var i = 0;
var prop;
var idx;
for (; i < len; i++) {
props[i].getValues().forEach(handleProp);
}
return result;
},
/**
* Initialize the recurrence expansion.
*
* @private
* @param {ICAL.Component} component The component to initialize from.
*/
_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];
}
},
/**
* Advance to the next exdate
* @private
*/
_nextExDay: function() {
this.exDate = this.exDates[++this.exDateInc];
},
/**
* Advance to the next rule date
* @private
*/
_nextRuleDay: function() {
this.ruleDate = this.ruleDates[++this.ruleDateInc];
},
/**
* Find and return the recurrence rule with the most recent event and
* return it.
*
* @private
* @return {?ICAL.RecurIterator} Found 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;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* This symbol is further described later on
* @ignore
*/
ICAL.Event = (function() {
/**
* @classdesc
* ICAL.js is organized into multiple layers. The bottom layer is a raw jCal
* object, followed by the component/property layer. The highest level is the
* event representation, which this class is part of. See the
* {@tutorial layers} guide for more details.
*
* @class
* @alias ICAL.Event
* @param {ICAL.Component=} component The ICAL.Component to base this event on
* @param {Object} options Options for this event
* @param {Boolean} options.strictExceptions
* When true, will verify exceptions are related by their UUID
* @param {Array<ICAL.Component|ICAL.Event>} options.exceptions
* Exceptions to this event, either as components or events
*/
function Event(component, options) {
if (!(component instanceof ICAL.Component)) {
options = component;
component = null;
}
if (component) {
this.component = component;
} else {
this.component = new ICAL.Component('vevent');
}
this._rangeExceptionCache = Object.create(null);
this.exceptions = Object.create(null);
this.rangeExceptions = [];
if (options && options.strictExceptions) {
this.strictExceptions = options.strictExceptions;
}
if (options && options.exceptions) {
options.exceptions.forEach(this.relateException, this);
}
}
Event.prototype = {
THISANDFUTURE: 'THISANDFUTURE',
/**
* List of related event exceptions.
*
* @type {ICAL.Event[]}
*/
exceptions: null,
/**
* When true, will verify exceptions are related by their UUID.
*
* @type {Boolean}
*/
strictExceptions: false,
/**
* Relates a given event exception to this object. If the given component
* does not share the UID of this event it cannot be related and will throw
* an exception.
*
* If this component is an exception it cannot have other exceptions
* related to it.
*
* @param {ICAL.Component|ICAL.Event} obj Component or event
*/
relateException: function(obj) {
if (this.isRecurrenceException()) {
throw new Error('cannot relate exception to exceptions');
}
if (obj instanceof ICAL.Component) {
obj = new ICAL.Event(obj);
}
if (this.strictExceptions && obj.uid !== this.uid) {
throw new Error('attempted to relate unrelated exception');
}
var id = obj.recurrenceId.toString();
// we don't sort or manage exceptions directly
// here the recurrence expander handles that.
this.exceptions[id] = obj;
// index RANGE=THISANDFUTURE exceptions so we can
// look them up later in getOccurrenceDetails.
if (obj.modifiesFuture()) {
var item = [
obj.recurrenceId.toUnixTime(), id
];
// we keep them sorted so we can find the nearest
// value later on...
var idx = ICAL.helpers.binsearchInsert(
this.rangeExceptions,
item,
compareRangeException
);
this.rangeExceptions.splice(idx, 0, item);
}
},
/**
* Checks if this record is an exception and has the RANGE=THISANDFUTURE
* value.
*
* @return {Boolean} True, when exception is within range
*/
modifiesFuture: function() {
var range = this.component.getFirstPropertyValue('range');
return range === this.THISANDFUTURE;
},
/**
* Finds the range exception nearest to the given date.
*
* @param {ICAL.Time} time usually an occurrence time of an event
* @return {?ICAL.Event} the related event/exception or null
*/
findRangeException: function(time) {
if (!this.rangeExceptions.length) {
return null;
}
var utc = time.toUnixTime();
var idx = ICAL.helpers.binsearchInsert(
this.rangeExceptions,
[utc],
compareRangeException
);
idx -= 1;
// occurs before
if (idx < 0) {
return null;
}
var rangeItem = this.rangeExceptions[idx];
/* istanbul ignore next: sanity check only */
if (utc < rangeItem[0]) {
return null;
}
return rangeItem[1];
},
/**
* This object is returned by {@link ICAL.Event#getOccurrenceDetails getOccurrenceDetails}
*
* @typedef {Object} occurrenceDetails
* @memberof ICAL.Event
* @property {ICAL.Time} recurrenceId The passed in recurrence id
* @property {ICAL.Event} item The occurrence
* @property {ICAL.Time} startDate The start of the occurrence
* @property {ICAL.Time} endDate The end of the occurrence
*/
/**
* Returns the occurrence details based on its start time. If the
* occurrence has an exception will return the details for that exception.
*
* NOTE: this method is intend to be used in conjunction
* with the {@link ICAL.Event#iterator iterator} method.
*
* @param {ICAL.Time} occurrence time occurrence
* @return {ICAL.Event.occurrenceDetails} Information about the occurrence
*/
getOccurrenceDetails: function(occurrence) {
var id = occurrence.toString();
var utcId = occurrence.convertToZone(ICAL.Timezone.utcTimezone).toString();
var item;
var result = {
//XXX: Clone?
recurrenceId: occurrence
};
if (id in this.exceptions) {
item = result.item = this.exceptions[id];
result.startDate = item.startDate;
result.endDate = item.endDate;
result.item = item;
} else if (utcId in this.exceptions) {
item = this.exceptions[utcId];
result.startDate = item.startDate;
result.endDate = item.endDate;
result.item = item;
} else {
// range exceptions (RANGE=THISANDFUTURE) have a
// lower priority then direct exceptions but
// must be accounted for first. Their item is
// always the first exception with the range prop.
var rangeExceptionId = this.findRangeException(
occurrence
);
var end;
if (rangeExceptionId) {
var exception = this.exceptions[rangeExceptionId];
// range exception must modify standard time
// by the difference (if any) in start/end times.
result.item = exception;
var startDiff = this._rangeExceptionCache[rangeExceptionId];
if (!startDiff) {
var original = exception.recurrenceId.clone();
var newStart = exception.startDate.clone();
// zones must be same otherwise subtract may be incorrect.
original.zone = newStart.zone;
startDiff = newStart.subtractDate(original);
this._rangeExceptionCache[rangeExceptionId] = startDiff;
}
var start = occurrence.clone();
start.zone = exception.startDate.zone;
start.addDuration(startDiff);
end = start.clone();
end.addDuration(exception.duration);
result.startDate = start;
result.endDate = end;
} else {
// no range exception standard expansion
end = occurrence.clone();
end.addDuration(this.duration);
result.endDate = end;
result.startDate = occurrence;
result.item = this;
}
}
return result;
},
/**
* Builds a recur expansion instance for a specific point in time (defaults
* to startDate).
*
* @param {ICAL.Time} startTime Starting point for expansion
* @return {ICAL.RecurExpansion} Expansion object
*/
iterator: function(startTime) {
return new ICAL.RecurExpansion({
component: this.component,
dtstart: startTime || this.startDate
});
},
/**
* Checks if the event is recurring
*
* @return {Boolean} True, if event is recurring
*/
isRecurring: function() {
var comp = this.component;
return comp.hasProperty('rrule') || comp.hasProperty('rdate');
},
/**
* Checks if the event describes a recurrence exception. See
* {@tutorial terminology} for details.
*
* @return {Boolean} True, if the even describes a recurrence exception
*/
isRecurrenceException: function() {
return this.component.hasProperty('recurrence-id');
},
/**
* Returns the types of recurrences this event may have.
*
* Returned as an object with the following possible keys:
*
* - YEARLY
* - MONTHLY
* - WEEKLY
* - DAILY
* - MINUTELY
* - SECONDLY
*
* @return {Object.<ICAL.Recur.frequencyValues, Boolean>}
* Object of recurrence flags
*/
getRecurrenceTypes: function() {
var rules = this.component.getAllProperties('rrule');
var i = 0;
var len = rules.length;
var result = Object.create(null);
for (; i < len; i++) {
var value = rules[i].getFirstValue();
result[value.freq] = true;
}
return result;
},
/**
* The uid of this event
* @type {String}
*/
get uid() {
return this._firstProp('uid');
},
set uid(value) {
this._setProp('uid', value);
},
/**
* The start date
* @type {ICAL.Time}
*/
get startDate() {
return this._firstProp('dtstart');
},
set startDate(value) {
this._setTime('dtstart', value);
},
/**
* The end date. This can be the result directly from the property, or the
* end date calculated from start date and duration.
* @type {ICAL.Time}
*/
get endDate() {
var endDate = this._firstProp('dtend');
if (!endDate) {
var duration = this._firstProp('duration');
endDate = this.startDate.clone();
if (duration) {
endDate.addDuration(duration);
} else if (endDate.isDate) {
endDate.day += 1;
}
}
return endDate;
},
set endDate(value) {
this._setTime('dtend', value);
},
/**
* The duration. This can be the result directly from the property, or the
* duration calculated from start date and end date.
* @type {ICAL.Duration}
* @readonly
*/
get duration() {
var duration = this._firstProp('duration');
if (!duration) {
return this.endDate.subtractDate(this.startDate);
}
return duration;
},
/**
* The location of the event.
* @type {String}
*/
get location() {
return this._firstProp('location');
},
set location(value) {
return this._setProp('location', value);
},
/**
* The attendees in the event
* @type {ICAL.Property[]}
* @readonly
*/
get attendees() {
//XXX: This is way lame we should have a better
// data structure for this later.
return this.component.getAllProperties('attendee');
},
/**
* The event summary
* @type {String}
*/
get summary() {
return this._firstProp('summary');
},
set summary(value) {
this._setProp('summary', value);
},
/**
* The event description.
* @type {String}
*/
get description() {
return this._firstProp('description');
},
set description(value) {
this._setProp('description', value);
},
/**
* The organizer value as an uri. In most cases this is a mailto: uri, but
* it can also be something else, like urn:uuid:...
* @type {String}
*/
get organizer() {
return this._firstProp('organizer');
},
set organizer(value) {
this._setProp('organizer', value);
},
/**
* The sequence value for this event. Used for scheduling
* see {@tutorial terminology}.
* @type {Number}
*/
get sequence() {
return this._firstProp('sequence');
},
set sequence(value) {
this._setProp('sequence', value);
},
/**
* The recurrence id for this event. See {@tutorial terminology} for details.
* @type {ICAL.Time}
*/
get recurrenceId() {
return this._firstProp('recurrence-id');
},
set recurrenceId(value) {
this._setProp('recurrence-id', value);
},
/**
* Set/update a time property's value.
* This will also update the TZID of the property.
*
* TODO: this method handles the case where we are switching
* from a known timezone to an implied timezone (one without TZID).
* This does _not_ handle the case of moving between a known
* (by TimezoneService) timezone to an unknown timezone...
*
* We will not add/remove/update the VTIMEZONE subcomponents
* leading to invalid ICAL data...
* @private
* @param {String} propName The property name
* @param {ICAL.Time} time The time to set
*/
_setTime: function(propName, time) {
var prop = this.component.getFirstProperty(propName);
if (!prop) {
prop = new ICAL.Property(propName);
this.component.addProperty(prop);
}
// utc and local don't get a tzid
if (
time.zone === ICAL.Timezone.localTimezone ||
time.zone === ICAL.Timezone.utcTimezone
) {
// remove the tzid
prop.removeParameter('tzid');
} else {
prop.setParameter('tzid', time.zone.tzid);
}
prop.setValue(time);
},
_setProp: function(name, value) {
this.component.updatePropertyWithValue(name, value);
},
_firstProp: function(name) {
return this.component.getFirstPropertyValue(name);
},
/**
* The string representation of this event.
* @return {String}
*/
toString: function() {
return this.component.toString();
}
};
function compareRangeException(a, b) {
if (a[0] > b[0]) return 1;
if (b[0] > a[0]) return -1;
return 0;
}
return Event;
}());
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2011-2015 */
/**
* This symbol is further described later on
* @ignore
*/
ICAL.ComponentParser = (function() {
/**
* @classdesc
* The ComponentParser is used to process a String or jCal Object,
* firing callbacks for various found components, as well as completion.
*
* @example
* var options = {
* // when false no events will be emitted for type
* parseEvent: true,
* parseTimezone: true
* };
*
* var parser = new ICAL.ComponentParser(options);
*
* parser.onevent(eventComponent) {
* //...
* }
*
* // ontimezone, etc...
*
* parser.oncomplete = function() {
*
* };
*
* parser.process(stringOrComponent);
*
* @class
* @alias ICAL.ComponentParser
* @param {Object=} options Component parser options
* @param {Boolean} options.parseEvent Whether events should be parsed
* @param {Boolean} options.parseTimezeone Whether timezones should be parsed
*/
function ComponentParser(options) {
if (typeof(options) === 'undefined') {
options = {};
}
var key;
for (key in options) {
/* istanbul ignore else */
if (options.hasOwnProperty(key)) {
this[key] = options[key];
}
}
}
ComponentParser.prototype = {
/**
* When true, parse events
*
* @type {Boolean}
*/
parseEvent: true,
/**
* When true, parse timezones
*
* @type {Boolean}
*/
parseTimezone: true,
/* SAX like events here for reference */
/**
* Fired when parsing is complete
* @callback
*/
oncomplete: /* istanbul ignore next */ function() {},
/**
* Fired if an error occurs during parsing.
*
* @callback
* @param {Error} err details of error
*/
onerror: /* istanbul ignore next */ function(err) {},
/**
* Fired when a top level component (VTIMEZONE) is found
*
* @callback
* @param {ICAL.Timezone} component Timezone object
*/
ontimezone: /* istanbul ignore next */ function(component) {},
/**
* Fired when a top level component (VEVENT) is found.
*
* @callback
* @param {ICAL.Event} component Top level component
*/
onevent: /* istanbul ignore next */ function(component) {},
/**
* Process a string or parse ical object. This function itself will return
* nothing but will start the parsing process.
*
* Events must be registered prior to calling this method.
*
* @param {ICAL.Component|String|Object} ical The component to process,
* either in its final form, as a jCal Object, or string representation
*/
process: function(ical) {
//TODO: this is sync now in the future we will have a incremental parser.
if (typeof(ical) === 'string') {
ical = ICAL.parse(ical);
}
if (!(ical instanceof ICAL.Component)) {
ical = new ICAL.Component(ical);
}
var components = ical.getAllSubcomponents();
var i = 0;
var len = components.length;
var component;
for (; i < len; i++) {
component = components[i];
switch (component.name) {
case 'vtimezone':
if (this.parseTimezone) {
var tzid = component.getFirstPropertyValue('tzid');
if (tzid) {
this.ontimezone(new ICAL.Timezone({
tzid: tzid,
component: component
}));
}
}
break;
case 'vevent':
if (this.parseEvent) {
this.onevent(new ICAL.Event(component));
}
break;
default:
continue;
}
}
//XXX: ideally we should do a "nextTick" here
// so in all cases this is actually async.
this.oncomplete();
}
};
return ComponentParser;
}());
/* Constants */
var URL_EDT = 'http://edt.telecom-bretagne.eu/xxx'; /* Student's iCal source (EDT) */
var GCAL_ID = 'xxx@group.calendar.google.com'; /* Google Calendar ID */
/* ---Helpers--- */
/* Custom object to handle events between EDT and GCal */
function CustomEvent(title, startTime, endTime, description, location) {
this.title = title;
this.startTime = startTime;
this.endTime = endTime;
this.description = description;
this.location = location;
}
/* Function to retrieve the desired ICS file from source (EDT) and get its content (text) */
function getICSText(URL) {
var response = null;
try {
response = UrlFetchApp.fetch(URL).getContentText();
} catch(error) {}
return response;
}
/* Based on: http://stackoverflow.com/a/8381494 */
/* Date proptotype extension: get range of the week */
Date.prototype.getWeek = function() {
/* Get current date and then shift it */
var today = new Date(this.setHours(0, 0, 0, 0)); /* Current date */
var dayOfWeek = today.getDay() - 1; /* It starts on Monday (Sunday by default) */
var dayOfMonth = today.getDate() - dayOfWeek; /* ([1-31]) Beginning of the week */
/* Get range */
var start = new Date(today.setDate(dayOfMonth)); /* Monday */
today.setHours(23, 59, 59, 59); /* Until the end of the day */
var end = new Date(today.setDate(dayOfMonth + 6)); /* Sunday */
/* Result as an object */
return {
start: start,
end: end
};
}
/* Process ICS data */
function parseICScalendar(iCSData) {
/* Using ical.js library (API: http://mozilla-comm.github.io/ical.js/api/) */
var iCScalendar = new ICAL.Component(ICAL.parse(iCSData));
var iCSevents = iCScalendar.getAllSubcomponents('vevent');
var parsedEvents = [];
for (var i = 0; i < iCSevents.length; i++) {
var iCSevent = new ICAL.Event(iCSevents[i]);
/* Remove last line: 'Exporté...' because it's always different and also remove first line (empty) */
iCSevent.description = iCSevent.description.split('\n').slice(1, -1).join('\n');
var event = new CustomEvent(iCSevent.summary, iCSevent.startDate.toJSDate().getTime(), iCSevent.endDate.toJSDate().getTime(),
iCSevent.description, iCSevent.location);
parsedEvents.push(event);
}
return parsedEvents;
}
/* Important: https://developers.google.com/apps-script/reference/calendar/calendar-app#createeventtitle-starttime-endtime-options */
/* Process GC data */
function processGCcalendar(GCcalendar) {
var week = (new Date).getWeek();
var GCevents = GCcalendar.getEvents(week.start, week.end);
var parsedEvents = [];
GCevents.map(function(GCevent){
var event = new CustomEvent(GCevent.getTitle(), GCevent.getStartTime().getTime(), GCevent.getEndTime().getTime(),
GCevent.getDescription(), GCevent.getLocation());
parsedEvents.push(event);
});
return parsedEvents;
}
/* ---Main function -> Run!--- */
function main() {
//Logger.log('Running script');
/* Google Calendar (retrieve data) */
var GCcalendar = CalendarApp.getCalendarById(GCAL_ID);
var GCevents = processGCcalendar(GCcalendar);
/* EDT (retrieve data) */
var week = (new Date).getWeek();
var urlStart = '&firstDate=' + week.start.getFullYear() + '-' + (week.start.getMonth() + 1) + '-' + week.start.getDate(); /* Months [0-11] */
var urlEnd = '&lastDate=' + week.end.getFullYear() + '-' + (week.end.getMonth() + 1) + '-' + week.end.getDate();
var iCSdata = getICSText(URL_EDT + urlStart + urlEnd);
if (iCSdata !== null) {
var iCSevents = parseICScalendar(iCSdata);
} else {
//Logger.log('Error retrieving ICS calendar from EDT');
}
/* Combine the two calendars */
/* It's better to start with ICS because we're creating a copy of it on GCal (for-loops for indexes) */
for (var i = 0; i < iCSevents.length; i++) {
var iCSevent = iCSevents[i];
var created = false;
for(var j = 0; j < GCevents.length; j++) {
var GCevent = GCevents[j];
/* Check if the title exists (but it's possible to find different events with the same name) */
if (iCSevent.title == GCevent.title) {
/* Same time and duration (same event) */
if ((iCSevent.startTime == GCevent.startTime) && (iCSevent.endTime == GCevent.endTime)){
/* Event already created */
created = true;
/* Removing elements from GCal temporal array (GC must be equal to iCS) */
GCevents.splice(j, 1);
break;
}
}
}
/* This event isn't in GC -> Create it */
if (!created) {
GCcalendar.createEvent(iCSevent.title, new Date(iCSevent.startTime), new Date(iCSevent.endTime),
{ description: iCSevent.description, location: iCSevent.location });
//Logger.log('Created event: ' + iCSevent.title + ' [' + new Date(iCSevent.startTime) + ']');
}
}
/* If there are more events in Google Calendar, we remove them (something changed) */
GCevents.map(function(GCevent){
var GCeventRes = GCcalendar.getEvents(new Date(GCevent.startTime), new Date(GCevent.endTime), { search: GCevent.title });
/* If an event is duplicated, GCevents contain all copies so GCeventRes[0] is our target */
GCeventRes[0].deleteEvent();
//Logger.log('Deleted event: ' + GCevent.title + ' [' + new Date(GCevent.startTime) + ']');
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment