1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/**
6 * This is ical.js from <https://github.com/mozilla-comm/ical.js>.
7 *
8 * If you would like to change anything in ical.js, it is required to do so
9 * upstream first.
10 *
11 * Current ical.js git revision: 7c99a434b38ad5d670a0e778ef055a2e17ccb3db (v1.4.0)
12 */
13
14var EXPORTED_SYMBOLS = ["ICAL", "unwrap", "unwrapSetter", "unwrapSingle", "wrapGetter"];
15
16function wrapGetter(type, val) {
17    return val ? new type(val) : null;
18}
19
20function unwrap(type, innerFunc) {
21    return function(val) { return unwrapSetter.call(this, type, val, innerFunc); };
22}
23
24function unwrapSetter(type, val, innerFunc, thisObj) {
25    return innerFunc.call(thisObj || this, unwrapSingle(type, val));
26}
27
28function unwrapSingle(type, val) {
29    if (!val || !val.wrappedJSObject) {
30        return null;
31    } else if (val.wrappedJSObject.innerObject instanceof type) {
32        return val.wrappedJSObject.innerObject;
33    } else {
34        var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
35        Cu.reportError("Unknown " + (type.icalclass || type) + " passed at " + cal.STACK(10));
36        return null;
37    }
38}
39
40// -- start ical.js --
41
42/* This Source Code Form is subject to the terms of the Mozilla Public
43 * License, v. 2.0. If a copy of the MPL was not distributed with this
44 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
45 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
46
47var ICAL;
48
49/* istanbul ignore next */
50/* jshint ignore:start */
51if (typeof module === 'object') {
52  // CommonJS, where exports may be different each time.
53  ICAL = module.exports;
54} else if (typeof ICAL !== 'object') {/* istanbul ignore next */
55  /** @ignore */
56  ICAL = {};
57}
58/* jshint ignore:end */
59
60
61/**
62 * The number of characters before iCalendar line folding should occur
63 * @type {Number}
64 * @default 75
65 */
66ICAL.foldLength = 75;
67
68
69/**
70 * The character(s) to be used for a newline. The default value is provided by
71 * rfc5545.
72 * @type {String}
73 * @default "\r\n"
74 */
75ICAL.newLineChar = '\r\n';
76
77
78/**
79 * Helper functions used in various places within ical.js
80 * @namespace
81 */
82ICAL.helpers = {
83  /**
84   * Compiles a list of all referenced TZIDs in all subcomponents and
85   * removes any extra VTIMEZONE subcomponents. In addition, if any TZIDs
86   * are referenced by a component, but a VTIMEZONE does not exist,
87   * an attempt will be made to generate a VTIMEZONE using ICAL.TimezoneService.
88   *
89   * @param {ICAL.Component} vcal     The top-level VCALENDAR component.
90   * @return {ICAL.Component}         The ICAL.Component that was passed in.
91   */
92  updateTimezones: function(vcal) {
93    var allsubs, properties, vtimezones, reqTzid, i, tzid;
94
95    if (!vcal || vcal.name !== "vcalendar") {
96      //not a top-level vcalendar component
97      return vcal;
98    }
99
100    //Store vtimezone subcomponents in an object reference by tzid.
101    //Store properties from everything else in another array
102    allsubs = vcal.getAllSubcomponents();
103    properties = [];
104    vtimezones = {};
105    for (i = 0; i < allsubs.length; i++) {
106      if (allsubs[i].name === "vtimezone") {
107        tzid = allsubs[i].getFirstProperty("tzid").getFirstValue();
108        vtimezones[tzid] = allsubs[i];
109      } else {
110        properties = properties.concat(allsubs[i].getAllProperties());
111      }
112    }
113
114    //create an object with one entry for each required tz
115    reqTzid = {};
116    for (i = 0; i < properties.length; i++) {
117      if ((tzid = properties[i].getParameter("tzid"))) {
118        reqTzid[tzid] = true;
119      }
120    }
121
122    //delete any vtimezones that are not on the reqTzid list.
123    for (i in vtimezones) {
124      if (vtimezones.hasOwnProperty(i) && !reqTzid[i]) {
125        vcal.removeSubcomponent(vtimezones[i]);
126      }
127    }
128
129    //create any missing, but registered timezones
130    for (i in reqTzid) {
131      if (
132        reqTzid.hasOwnProperty(i) &&
133        !vtimezones[i] &&
134        ICAL.TimezoneService.has(i)
135      ) {
136        vcal.addSubcomponent(ICAL.TimezoneService.get(i).component);
137      }
138    }
139
140    return vcal;
141  },
142
143  /**
144   * Checks if the given type is of the number type and also NaN.
145   *
146   * @param {Number} number     The number to check
147   * @return {Boolean}          True, if the number is strictly NaN
148   */
149  isStrictlyNaN: function(number) {
150    return typeof(number) === 'number' && isNaN(number);
151  },
152
153  /**
154   * Parses a string value that is expected to be an integer, when the valid is
155   * not an integer throws a decoration error.
156   *
157   * @param {String} string     Raw string input
158   * @return {Number}           Parsed integer
159   */
160  strictParseInt: function(string) {
161    var result = parseInt(string, 10);
162
163    if (ICAL.helpers.isStrictlyNaN(result)) {
164      throw new Error(
165        'Could not extract integer from "' + string + '"'
166      );
167    }
168
169    return result;
170  },
171
172  /**
173   * Creates or returns a class instance of a given type with the initialization
174   * data if the data is not already an instance of the given type.
175   *
176   * @example
177   * var time = new ICAL.Time(...);
178   * var result = ICAL.helpers.formatClassType(time, ICAL.Time);
179   *
180   * (result instanceof ICAL.Time)
181   * // => true
182   *
183   * result = ICAL.helpers.formatClassType({}, ICAL.Time);
184   * (result isntanceof ICAL.Time)
185   * // => true
186   *
187   *
188   * @param {Object} data       object initialization data
189   * @param {Object} type       object type (like ICAL.Time)
190   * @return {?}                An instance of the found type.
191   */
192  formatClassType: function formatClassType(data, type) {
193    if (typeof(data) === 'undefined') {
194      return undefined;
195    }
196
197    if (data instanceof type) {
198      return data;
199    }
200    return new type(data);
201  },
202
203  /**
204   * Identical to indexOf but will only match values when they are not preceded
205   * by a backslash character.
206   *
207   * @param {String} buffer         String to search
208   * @param {String} search         Value to look for
209   * @param {Number} pos            Start position
210   * @return {Number}               The position, or -1 if not found
211   */
212  unescapedIndexOf: function(buffer, search, pos) {
213    while ((pos = buffer.indexOf(search, pos)) !== -1) {
214      if (pos > 0 && buffer[pos - 1] === '\\') {
215        pos += 1;
216      } else {
217        return pos;
218      }
219    }
220    return -1;
221  },
222
223  /**
224   * Find the index for insertion using binary search.
225   *
226   * @param {Array} list            The list to search
227   * @param {?} seekVal             The value to insert
228   * @param {function(?,?)} cmpfunc The comparison func, that can
229   *                                  compare two seekVals
230   * @return {Number}               The insert position
231   */
232  binsearchInsert: function(list, seekVal, cmpfunc) {
233    if (!list.length)
234      return 0;
235
236    var low = 0, high = list.length - 1,
237        mid, cmpval;
238
239    while (low <= high) {
240      mid = low + Math.floor((high - low) / 2);
241      cmpval = cmpfunc(seekVal, list[mid]);
242
243      if (cmpval < 0)
244        high = mid - 1;
245      else if (cmpval > 0)
246        low = mid + 1;
247      else
248        break;
249    }
250
251    if (cmpval < 0)
252      return mid; // insertion is displacing, so use mid outright.
253    else if (cmpval > 0)
254      return mid + 1;
255    else
256      return mid;
257  },
258
259  /**
260   * Convenience function for debug output
261   * @private
262   */
263  dumpn: /* istanbul ignore next */ function() {
264    if (!ICAL.debug) {
265      return;
266    }
267
268    if (typeof (console) !== 'undefined' && 'log' in console) {
269      ICAL.helpers.dumpn = function consoleDumpn(input) {
270        console.log(input);
271      };
272    } else {
273      ICAL.helpers.dumpn = function geckoDumpn(input) {
274        dump(input + '\n');
275      };
276    }
277
278    ICAL.helpers.dumpn(arguments[0]);
279  },
280
281  /**
282   * Clone the passed object or primitive. By default a shallow clone will be
283   * executed.
284   *
285   * @param {*} aSrc            The thing to clone
286   * @param {Boolean=} aDeep    If true, a deep clone will be performed
287   * @return {*}                The copy of the thing
288   */
289  clone: function(aSrc, aDeep) {
290    if (!aSrc || typeof aSrc != "object") {
291      return aSrc;
292    } else if (aSrc instanceof Date) {
293      return new Date(aSrc.getTime());
294    } else if ("clone" in aSrc) {
295      return aSrc.clone();
296    } else if (Array.isArray(aSrc)) {
297      var arr = [];
298      for (var i = 0; i < aSrc.length; i++) {
299        arr.push(aDeep ? ICAL.helpers.clone(aSrc[i], true) : aSrc[i]);
300      }
301      return arr;
302    } else {
303      var obj = {};
304      for (var name in aSrc) {
305        // uses prototype method to allow use of Object.create(null);
306        /* istanbul ignore else */
307        if (Object.prototype.hasOwnProperty.call(aSrc, name)) {
308          if (aDeep) {
309            obj[name] = ICAL.helpers.clone(aSrc[name], true);
310          } else {
311            obj[name] = aSrc[name];
312          }
313        }
314      }
315      return obj;
316    }
317  },
318
319  /**
320   * Performs iCalendar line folding. A line ending character is inserted and
321   * the next line begins with a whitespace.
322   *
323   * @example
324   * SUMMARY:This line will be fold
325   *  ed right in the middle of a word.
326   *
327   * @param {String} aLine      The line to fold
328   * @return {String}           The folded line
329   */
330  foldline: function foldline(aLine) {
331    var result = "";
332    var line = aLine || "";
333
334    while (line.length) {
335      result += ICAL.newLineChar + " " + line.substr(0, ICAL.foldLength);
336      line = line.substr(ICAL.foldLength);
337    }
338    return result.substr(ICAL.newLineChar.length + 1);
339  },
340
341  /**
342   * Pads the given string or number with zeros so it will have at least two
343   * characters.
344   *
345   * @param {String|Number} data    The string or number to pad
346   * @return {String}               The number padded as a string
347   */
348  pad2: function pad(data) {
349    if (typeof(data) !== 'string') {
350      // handle fractions.
351      if (typeof(data) === 'number') {
352        data = parseInt(data);
353      }
354      data = String(data);
355    }
356
357    var len = data.length;
358
359    switch (len) {
360      case 0:
361        return '00';
362      case 1:
363        return '0' + data;
364      default:
365        return data;
366    }
367  },
368
369  /**
370   * Truncates the given number, correctly handling negative numbers.
371   *
372   * @param {Number} number     The number to truncate
373   * @return {Number}           The truncated number
374   */
375  trunc: function trunc(number) {
376    return (number < 0 ? Math.ceil(number) : Math.floor(number));
377  },
378
379  /**
380   * Poor-man's cross-browser inheritance for JavaScript. Doesn't support all
381   * the features, but enough for our usage.
382   *
383   * @param {Function} base     The base class constructor function.
384   * @param {Function} child    The child class constructor function.
385   * @param {Object} extra      Extends the prototype with extra properties
386   *                              and methods
387   */
388  inherits: function(base, child, extra) {
389    function F() {}
390    F.prototype = base.prototype;
391    child.prototype = new F();
392
393    if (extra) {
394      ICAL.helpers.extend(extra, child.prototype);
395    }
396  },
397
398  /**
399   * Poor-man's cross-browser object extension. Doesn't support all the
400   * features, but enough for our usage. Note that the target's properties are
401   * not overwritten with the source properties.
402   *
403   * @example
404   * var child = ICAL.helpers.extend(parent, {
405   *   "bar": 123
406   * });
407   *
408   * @param {Object} source     The object to extend
409   * @param {Object} target     The object to extend with
410   * @return {Object}           Returns the target.
411   */
412  extend: function(source, target) {
413    for (var key in source) {
414      var descr = Object.getOwnPropertyDescriptor(source, key);
415      if (descr && !Object.getOwnPropertyDescriptor(target, key)) {
416        Object.defineProperty(target, key, descr);
417      }
418    }
419    return target;
420  }
421};
422/* This Source Code Form is subject to the terms of the Mozilla Public
423 * License, v. 2.0. If a copy of the MPL was not distributed with this
424 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
425 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
426
427/** @namespace ICAL */
428
429
430/**
431 * This symbol is further described later on
432 * @ignore
433 */
434ICAL.design = (function() {
435  'use strict';
436
437  var FROM_ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g;
438  var TO_ICAL_NEWLINE = /\\|;|,|\n/g;
439  var FROM_VCARD_NEWLINE = /\\\\|\\,|\\[Nn]/g;
440  var TO_VCARD_NEWLINE = /\\|,|\n/g;
441
442  function createTextType(fromNewline, toNewline) {
443    var result = {
444      matches: /.*/,
445
446      fromICAL: function(aValue, structuredEscape) {
447        return replaceNewline(aValue, fromNewline, structuredEscape);
448      },
449
450      toICAL: function(aValue, structuredEscape) {
451        var regEx = toNewline;
452        if (structuredEscape)
453          regEx = new RegExp(regEx.source + '|' + structuredEscape);
454        return aValue.replace(regEx, function(str) {
455          switch (str) {
456          case "\\":
457            return "\\\\";
458          case ";":
459            return "\\;";
460          case ",":
461            return "\\,";
462          case "\n":
463            return "\\n";
464          /* istanbul ignore next */
465          default:
466            return str;
467          }
468        });
469      }
470    };
471    return result;
472  }
473
474  // default types used multiple times
475  var DEFAULT_TYPE_TEXT = { defaultType: "text" };
476  var DEFAULT_TYPE_TEXT_MULTI = { defaultType: "text", multiValue: "," };
477  var DEFAULT_TYPE_TEXT_STRUCTURED = { defaultType: "text", structuredValue: ";" };
478  var DEFAULT_TYPE_INTEGER = { defaultType: "integer" };
479  var DEFAULT_TYPE_DATETIME_DATE = { defaultType: "date-time", allowedTypes: ["date-time", "date"] };
480  var DEFAULT_TYPE_DATETIME = { defaultType: "date-time" };
481  var DEFAULT_TYPE_URI = { defaultType: "uri" };
482  var DEFAULT_TYPE_UTCOFFSET = { defaultType: "utc-offset" };
483  var DEFAULT_TYPE_RECUR = { defaultType: "recur" };
484  var DEFAULT_TYPE_DATE_ANDOR_TIME = { defaultType: "date-and-or-time", allowedTypes: ["date-time", "date", "text"] };
485
486  function replaceNewlineReplace(string) {
487    switch (string) {
488      case "\\\\":
489        return "\\";
490      case "\\;":
491        return ";";
492      case "\\,":
493        return ",";
494      case "\\n":
495      case "\\N":
496        return "\n";
497      /* istanbul ignore next */
498      default:
499        return string;
500    }
501  }
502
503  function replaceNewline(value, newline, structuredEscape) {
504    // avoid regex when possible.
505    if (value.indexOf('\\') === -1) {
506      return value;
507    }
508    if (structuredEscape)
509      newline = new RegExp(newline.source + '|\\\\' + structuredEscape);
510    return value.replace(newline, replaceNewlineReplace);
511  }
512
513  var commonProperties = {
514    "categories": DEFAULT_TYPE_TEXT_MULTI,
515    "url": DEFAULT_TYPE_URI,
516    "version": DEFAULT_TYPE_TEXT,
517    "uid": DEFAULT_TYPE_TEXT
518  };
519
520  var commonValues = {
521    "boolean": {
522      values: ["TRUE", "FALSE"],
523
524      fromICAL: function(aValue) {
525        switch (aValue) {
526          case 'TRUE':
527            return true;
528          case 'FALSE':
529            return false;
530          default:
531            //TODO: parser warning
532            return false;
533        }
534      },
535
536      toICAL: function(aValue) {
537        if (aValue) {
538          return 'TRUE';
539        }
540        return 'FALSE';
541      }
542
543    },
544    float: {
545      matches: /^[+-]?\d+\.\d+$/,
546
547      fromICAL: function(aValue) {
548        var parsed = parseFloat(aValue);
549        if (ICAL.helpers.isStrictlyNaN(parsed)) {
550          // TODO: parser warning
551          return 0.0;
552        }
553        return parsed;
554      },
555
556      toICAL: function(aValue) {
557        return String(aValue);
558      }
559    },
560    integer: {
561      fromICAL: function(aValue) {
562        var parsed = parseInt(aValue);
563        if (ICAL.helpers.isStrictlyNaN(parsed)) {
564          return 0;
565        }
566        return parsed;
567      },
568
569      toICAL: function(aValue) {
570        return String(aValue);
571      }
572    },
573    "utc-offset": {
574      toICAL: function(aValue) {
575        if (aValue.length < 7) {
576          // no seconds
577          // -0500
578          return aValue.substr(0, 3) +
579                 aValue.substr(4, 2);
580        } else {
581          // seconds
582          // -050000
583          return aValue.substr(0, 3) +
584                 aValue.substr(4, 2) +
585                 aValue.substr(7, 2);
586        }
587      },
588
589      fromICAL: function(aValue) {
590        if (aValue.length < 6) {
591          // no seconds
592          // -05:00
593          return aValue.substr(0, 3) + ':' +
594                 aValue.substr(3, 2);
595        } else {
596          // seconds
597          // -05:00:00
598          return aValue.substr(0, 3) + ':' +
599                 aValue.substr(3, 2) + ':' +
600                 aValue.substr(5, 2);
601        }
602      },
603
604      decorate: function(aValue) {
605        return ICAL.UtcOffset.fromString(aValue);
606      },
607
608      undecorate: function(aValue) {
609        return aValue.toString();
610      }
611    }
612  };
613
614  var icalParams = {
615    // Although the syntax is DQUOTE uri DQUOTE, I don't think we should
616    // enfoce anything aside from it being a valid content line.
617    //
618    // At least some params require - if multi values are used - DQUOTEs
619    // for each of its values - e.g. delegated-from="uri1","uri2"
620    // To indicate this, I introduced the new k/v pair
621    // multiValueSeparateDQuote: true
622    //
623    // "ALTREP": { ... },
624
625    // CN just wants a param-value
626    // "CN": { ... }
627
628    "cutype": {
629      values: ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"],
630      allowXName: true,
631      allowIanaToken: true
632    },
633
634    "delegated-from": {
635      valueType: "cal-address",
636      multiValue: ",",
637      multiValueSeparateDQuote: true
638    },
639    "delegated-to": {
640      valueType: "cal-address",
641      multiValue: ",",
642      multiValueSeparateDQuote: true
643    },
644    // "DIR": { ... }, // See ALTREP
645    "encoding": {
646      values: ["8BIT", "BASE64"]
647    },
648    // "FMTTYPE": { ... }, // See ALTREP
649    "fbtype": {
650      values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"],
651      allowXName: true,
652      allowIanaToken: true
653    },
654    // "LANGUAGE": { ... }, // See ALTREP
655    "member": {
656      valueType: "cal-address",
657      multiValue: ",",
658      multiValueSeparateDQuote: true
659    },
660    "partstat": {
661      // TODO These values are actually different per-component
662      values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE",
663               "DELEGATED", "COMPLETED", "IN-PROCESS"],
664      allowXName: true,
665      allowIanaToken: true
666    },
667    "range": {
668      values: ["THISLANDFUTURE"]
669    },
670    "related": {
671      values: ["START", "END"]
672    },
673    "reltype": {
674      values: ["PARENT", "CHILD", "SIBLING"],
675      allowXName: true,
676      allowIanaToken: true
677    },
678    "role": {
679      values: ["REQ-PARTICIPANT", "CHAIR",
680               "OPT-PARTICIPANT", "NON-PARTICIPANT"],
681      allowXName: true,
682      allowIanaToken: true
683    },
684    "rsvp": {
685      values: ["TRUE", "FALSE"]
686    },
687    "sent-by": {
688      valueType: "cal-address"
689    },
690    "tzid": {
691      matches: /^\//
692    },
693    "value": {
694      // since the value here is a 'type' lowercase is used.
695      values: ["binary", "boolean", "cal-address", "date", "date-time",
696               "duration", "float", "integer", "period", "recur", "text",
697               "time", "uri", "utc-offset"],
698      allowXName: true,
699      allowIanaToken: true
700    }
701  };
702
703  // When adding a value here, be sure to add it to the parameter types!
704  var icalValues = ICAL.helpers.extend(commonValues, {
705    text: createTextType(FROM_ICAL_NEWLINE, TO_ICAL_NEWLINE),
706
707    uri: {
708      // TODO
709      /* ... */
710    },
711
712    "binary": {
713      decorate: function(aString) {
714        return ICAL.Binary.fromString(aString);
715      },
716
717      undecorate: function(aBinary) {
718        return aBinary.toString();
719      }
720    },
721    "cal-address": {
722      // needs to be an uri
723    },
724    "date": {
725      decorate: function(aValue, aProp) {
726        if (design.strict) {
727          return ICAL.Time.fromDateString(aValue, aProp);
728        } else {
729          return ICAL.Time.fromString(aValue, aProp);
730        }
731      },
732
733      /**
734       * undecorates a time object.
735       */
736      undecorate: function(aValue) {
737        return aValue.toString();
738      },
739
740      fromICAL: function(aValue) {
741        // from: 20120901
742        // to: 2012-09-01
743        if (!design.strict && aValue.length >= 15) {
744          // This is probably a date-time, e.g. 20120901T130000Z
745          return icalValues["date-time"].fromICAL(aValue);
746        } else {
747          return aValue.substr(0, 4) + '-' +
748                 aValue.substr(4, 2) + '-' +
749                 aValue.substr(6, 2);
750        }
751      },
752
753      toICAL: function(aValue) {
754        // from: 2012-09-01
755        // to: 20120901
756        var len = aValue.length;
757
758        if (len == 10) {
759          return aValue.substr(0, 4) +
760                 aValue.substr(5, 2) +
761                 aValue.substr(8, 2);
762        } else if (len >= 19) {
763          return icalValues["date-time"].toICAL(aValue);
764        } else {
765          //TODO: serialize warning?
766          return aValue;
767        }
768
769      }
770    },
771    "date-time": {
772      fromICAL: function(aValue) {
773        // from: 20120901T130000
774        // to: 2012-09-01T13:00:00
775        if (!design.strict && aValue.length == 8) {
776          // This is probably a date, e.g. 20120901
777          return icalValues.date.fromICAL(aValue);
778        } else {
779          var result = aValue.substr(0, 4) + '-' +
780                       aValue.substr(4, 2) + '-' +
781                       aValue.substr(6, 2) + 'T' +
782                       aValue.substr(9, 2) + ':' +
783                       aValue.substr(11, 2) + ':' +
784                       aValue.substr(13, 2);
785
786          if (aValue[15] && aValue[15] === 'Z') {
787            result += 'Z';
788          }
789
790          return result;
791        }
792      },
793
794      toICAL: function(aValue) {
795        // from: 2012-09-01T13:00:00
796        // to: 20120901T130000
797        var len = aValue.length;
798
799        if (len == 10 && !design.strict) {
800          return icalValues.date.toICAL(aValue);
801        } else if (len >= 19) {
802          var result = aValue.substr(0, 4) +
803                       aValue.substr(5, 2) +
804                       // grab the (DDTHH) segment
805                       aValue.substr(8, 5) +
806                       // MM
807                       aValue.substr(14, 2) +
808                       // SS
809                       aValue.substr(17, 2);
810
811          if (aValue[19] && aValue[19] === 'Z') {
812            result += 'Z';
813          }
814          return result;
815        } else {
816          // TODO: error
817          return aValue;
818        }
819      },
820
821      decorate: function(aValue, aProp) {
822        if (design.strict) {
823          return ICAL.Time.fromDateTimeString(aValue, aProp);
824        } else {
825          return ICAL.Time.fromString(aValue, aProp);
826        }
827      },
828
829      undecorate: function(aValue) {
830        return aValue.toString();
831      }
832    },
833    duration: {
834      decorate: function(aValue) {
835        return ICAL.Duration.fromString(aValue);
836      },
837      undecorate: function(aValue) {
838        return aValue.toString();
839      }
840    },
841    period: {
842
843      fromICAL: function(string) {
844        var parts = string.split('/');
845        parts[0] = icalValues['date-time'].fromICAL(parts[0]);
846
847        if (!ICAL.Duration.isValueString(parts[1])) {
848          parts[1] = icalValues['date-time'].fromICAL(parts[1]);
849        }
850
851        return parts;
852      },
853
854      toICAL: function(parts) {
855        if (!design.strict && parts[0].length == 10) {
856          parts[0] = icalValues.date.toICAL(parts[0]);
857        } else {
858          parts[0] = icalValues['date-time'].toICAL(parts[0]);
859        }
860
861        if (!ICAL.Duration.isValueString(parts[1])) {
862          if (!design.strict && parts[1].length == 10) {
863            parts[1] = icalValues.date.toICAL(parts[1]);
864          } else {
865            parts[1] = icalValues['date-time'].toICAL(parts[1]);
866          }
867        }
868
869        return parts.join("/");
870      },
871
872      decorate: function(aValue, aProp) {
873        return ICAL.Period.fromJSON(aValue, aProp, !design.strict);
874      },
875
876      undecorate: function(aValue) {
877        return aValue.toJSON();
878      }
879    },
880    recur: {
881      fromICAL: function(string) {
882        return ICAL.Recur._stringToData(string, true);
883      },
884
885      toICAL: function(data) {
886        var str = "";
887        for (var k in data) {
888          /* istanbul ignore if */
889          if (!Object.prototype.hasOwnProperty.call(data, k)) {
890            continue;
891          }
892          var val = data[k];
893          if (k == "until") {
894            if (val.length > 10) {
895              val = icalValues['date-time'].toICAL(val);
896            } else {
897              val = icalValues.date.toICAL(val);
898            }
899          } else if (k == "wkst") {
900            if (typeof val === 'number') {
901              val = ICAL.Recur.numericDayToIcalDay(val);
902            }
903          } else if (Array.isArray(val)) {
904            val = val.join(",");
905          }
906          str += k.toUpperCase() + "=" + val + ";";
907        }
908        return str.substr(0, str.length - 1);
909      },
910
911      decorate: function decorate(aValue) {
912        return ICAL.Recur.fromData(aValue);
913      },
914
915      undecorate: function(aRecur) {
916        return aRecur.toJSON();
917      }
918    },
919
920    time: {
921      fromICAL: function(aValue) {
922        // from: MMHHSS(Z)?
923        // to: HH:MM:SS(Z)?
924        if (aValue.length < 6) {
925          // TODO: parser exception?
926          return aValue;
927        }
928
929        // HH::MM::SSZ?
930        var result = aValue.substr(0, 2) + ':' +
931                     aValue.substr(2, 2) + ':' +
932                     aValue.substr(4, 2);
933
934        if (aValue[6] === 'Z') {
935          result += 'Z';
936        }
937
938        return result;
939      },
940
941      toICAL: function(aValue) {
942        // from: HH:MM:SS(Z)?
943        // to: MMHHSS(Z)?
944        if (aValue.length < 8) {
945          //TODO: error
946          return aValue;
947        }
948
949        var result = aValue.substr(0, 2) +
950                     aValue.substr(3, 2) +
951                     aValue.substr(6, 2);
952
953        if (aValue[8] === 'Z') {
954          result += 'Z';
955        }
956
957        return result;
958      }
959    }
960  });
961
962  var icalProperties = ICAL.helpers.extend(commonProperties, {
963
964    "action": DEFAULT_TYPE_TEXT,
965    "attach": { defaultType: "uri" },
966    "attendee": { defaultType: "cal-address" },
967    "calscale": DEFAULT_TYPE_TEXT,
968    "class": DEFAULT_TYPE_TEXT,
969    "comment": DEFAULT_TYPE_TEXT,
970    "completed": DEFAULT_TYPE_DATETIME,
971    "contact": DEFAULT_TYPE_TEXT,
972    "created": DEFAULT_TYPE_DATETIME,
973    "description": DEFAULT_TYPE_TEXT,
974    "dtend": DEFAULT_TYPE_DATETIME_DATE,
975    "dtstamp": DEFAULT_TYPE_DATETIME,
976    "dtstart": DEFAULT_TYPE_DATETIME_DATE,
977    "due": DEFAULT_TYPE_DATETIME_DATE,
978    "duration": { defaultType: "duration" },
979    "exdate": {
980      defaultType: "date-time",
981      allowedTypes: ["date-time", "date"],
982      multiValue: ','
983    },
984    "exrule": DEFAULT_TYPE_RECUR,
985    "freebusy": { defaultType: "period", multiValue: "," },
986    "geo": { defaultType: "float", structuredValue: ";" },
987    "last-modified": DEFAULT_TYPE_DATETIME,
988    "location": DEFAULT_TYPE_TEXT,
989    "method": DEFAULT_TYPE_TEXT,
990    "organizer": { defaultType: "cal-address" },
991    "percent-complete": DEFAULT_TYPE_INTEGER,
992    "priority": DEFAULT_TYPE_INTEGER,
993    "prodid": DEFAULT_TYPE_TEXT,
994    "related-to": DEFAULT_TYPE_TEXT,
995    "repeat": DEFAULT_TYPE_INTEGER,
996    "rdate": {
997      defaultType: "date-time",
998      allowedTypes: ["date-time", "date", "period"],
999      multiValue: ',',
1000      detectType: function(string) {
1001        if (string.indexOf('/') !== -1) {
1002          return 'period';
1003        }
1004        return (string.indexOf('T') === -1) ? 'date' : 'date-time';
1005      }
1006    },
1007    "recurrence-id": DEFAULT_TYPE_DATETIME_DATE,
1008    "resources": DEFAULT_TYPE_TEXT_MULTI,
1009    "request-status": DEFAULT_TYPE_TEXT_STRUCTURED,
1010    "rrule": DEFAULT_TYPE_RECUR,
1011    "sequence": DEFAULT_TYPE_INTEGER,
1012    "status": DEFAULT_TYPE_TEXT,
1013    "summary": DEFAULT_TYPE_TEXT,
1014    "transp": DEFAULT_TYPE_TEXT,
1015    "trigger": { defaultType: "duration", allowedTypes: ["duration", "date-time"] },
1016    "tzoffsetfrom": DEFAULT_TYPE_UTCOFFSET,
1017    "tzoffsetto": DEFAULT_TYPE_UTCOFFSET,
1018    "tzurl": DEFAULT_TYPE_URI,
1019    "tzid": DEFAULT_TYPE_TEXT,
1020    "tzname": DEFAULT_TYPE_TEXT
1021  });
1022
1023  // When adding a value here, be sure to add it to the parameter types!
1024  var vcardValues = ICAL.helpers.extend(commonValues, {
1025    text: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE),
1026    uri: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE),
1027
1028    date: {
1029      decorate: function(aValue) {
1030        return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date");
1031      },
1032      undecorate: function(aValue) {
1033        return aValue.toString();
1034      },
1035      fromICAL: function(aValue) {
1036        if (aValue.length == 8) {
1037          return icalValues.date.fromICAL(aValue);
1038        } else if (aValue[0] == '-' && aValue.length == 6) {
1039          return aValue.substr(0, 4) + '-' + aValue.substr(4);
1040        } else {
1041          return aValue;
1042        }
1043      },
1044      toICAL: function(aValue) {
1045        if (aValue.length == 10) {
1046          return icalValues.date.toICAL(aValue);
1047        } else if (aValue[0] == '-' && aValue.length == 7) {
1048          return aValue.substr(0, 4) + aValue.substr(5);
1049        } else {
1050          return aValue;
1051        }
1052      }
1053    },
1054
1055    time: {
1056      decorate: function(aValue) {
1057        return ICAL.VCardTime.fromDateAndOrTimeString("T" + aValue, "time");
1058      },
1059      undecorate: function(aValue) {
1060        return aValue.toString();
1061      },
1062      fromICAL: function(aValue) {
1063        var splitzone = vcardValues.time._splitZone(aValue, true);
1064        var zone = splitzone[0], value = splitzone[1];
1065
1066        //console.log("SPLIT: ",splitzone);
1067
1068        if (value.length == 6) {
1069          value = value.substr(0, 2) + ':' +
1070                  value.substr(2, 2) + ':' +
1071                  value.substr(4, 2);
1072        } else if (value.length == 4 && value[0] != '-') {
1073          value = value.substr(0, 2) + ':' + value.substr(2, 2);
1074        } else if (value.length == 5) {
1075          value = value.substr(0, 3) + ':' + value.substr(3, 2);
1076        }
1077
1078        if (zone.length == 5 && (zone[0] == '-' || zone[0] == '+')) {
1079          zone = zone.substr(0, 3) + ':' + zone.substr(3);
1080        }
1081
1082        return value + zone;
1083      },
1084
1085      toICAL: function(aValue) {
1086        var splitzone = vcardValues.time._splitZone(aValue);
1087        var zone = splitzone[0], value = splitzone[1];
1088
1089        if (value.length == 8) {
1090          value = value.substr(0, 2) +
1091                  value.substr(3, 2) +
1092                  value.substr(6, 2);
1093        } else if (value.length == 5 && value[0] != '-') {
1094          value = value.substr(0, 2) + value.substr(3, 2);
1095        } else if (value.length == 6) {
1096          value = value.substr(0, 3) + value.substr(4, 2);
1097        }
1098
1099        if (zone.length == 6 && (zone[0] == '-' || zone[0] == '+')) {
1100          zone = zone.substr(0, 3) + zone.substr(4);
1101        }
1102
1103        return value + zone;
1104      },
1105
1106      _splitZone: function(aValue, isFromIcal) {
1107        var lastChar = aValue.length - 1;
1108        var signChar = aValue.length - (isFromIcal ? 5 : 6);
1109        var sign = aValue[signChar];
1110        var zone, value;
1111
1112        if (aValue[lastChar] == 'Z') {
1113          zone = aValue[lastChar];
1114          value = aValue.substr(0, lastChar);
1115        } else if (aValue.length > 6 && (sign == '-' || sign == '+')) {
1116          zone = aValue.substr(signChar);
1117          value = aValue.substr(0, signChar);
1118        } else {
1119          zone = "";
1120          value = aValue;
1121        }
1122
1123        return [zone, value];
1124      }
1125    },
1126
1127    "date-time": {
1128      decorate: function(aValue) {
1129        return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-time");
1130      },
1131
1132      undecorate: function(aValue) {
1133        return aValue.toString();
1134      },
1135
1136      fromICAL: function(aValue) {
1137        return vcardValues['date-and-or-time'].fromICAL(aValue);
1138      },
1139
1140      toICAL: function(aValue) {
1141        return vcardValues['date-and-or-time'].toICAL(aValue);
1142      }
1143    },
1144
1145    "date-and-or-time": {
1146      decorate: function(aValue) {
1147        return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-and-or-time");
1148      },
1149
1150      undecorate: function(aValue) {
1151        return aValue.toString();
1152      },
1153
1154      fromICAL: function(aValue) {
1155        var parts = aValue.split('T');
1156        return (parts[0] ? vcardValues.date.fromICAL(parts[0]) : '') +
1157               (parts[1] ? 'T' + vcardValues.time.fromICAL(parts[1]) : '');
1158      },
1159
1160      toICAL: function(aValue) {
1161        var parts = aValue.split('T');
1162        return vcardValues.date.toICAL(parts[0]) +
1163               (parts[1] ? 'T' + vcardValues.time.toICAL(parts[1]) : '');
1164
1165      }
1166    },
1167    timestamp: icalValues['date-time'],
1168    "language-tag": {
1169      matches: /^[a-zA-Z0-9-]+$/ // Could go with a more strict regex here
1170    }
1171  });
1172
1173  var vcardParams = {
1174    "type": {
1175      valueType: "text",
1176      multiValue: ","
1177    },
1178    "value": {
1179      // since the value here is a 'type' lowercase is used.
1180      values: ["text", "uri", "date", "time", "date-time", "date-and-or-time",
1181               "timestamp", "boolean", "integer", "float", "utc-offset",
1182               "language-tag"],
1183      allowXName: true,
1184      allowIanaToken: true
1185    }
1186  };
1187
1188  var vcardProperties = ICAL.helpers.extend(commonProperties, {
1189    "adr": { defaultType: "text", structuredValue: ";", multiValue: "," },
1190    "anniversary": DEFAULT_TYPE_DATE_ANDOR_TIME,
1191    "bday": DEFAULT_TYPE_DATE_ANDOR_TIME,
1192    "caladruri": DEFAULT_TYPE_URI,
1193    "caluri": DEFAULT_TYPE_URI,
1194    "clientpidmap": DEFAULT_TYPE_TEXT_STRUCTURED,
1195    "email": DEFAULT_TYPE_TEXT,
1196    "fburl": DEFAULT_TYPE_URI,
1197    "fn": DEFAULT_TYPE_TEXT,
1198    "gender": DEFAULT_TYPE_TEXT_STRUCTURED,
1199    "geo": DEFAULT_TYPE_URI,
1200    "impp": DEFAULT_TYPE_URI,
1201    "key": DEFAULT_TYPE_URI,
1202    "kind": DEFAULT_TYPE_TEXT,
1203    "lang": { defaultType: "language-tag" },
1204    "logo": DEFAULT_TYPE_URI,
1205    "member": DEFAULT_TYPE_URI,
1206    "n": { defaultType: "text", structuredValue: ";", multiValue: "," },
1207    "nickname": DEFAULT_TYPE_TEXT_MULTI,
1208    "note": DEFAULT_TYPE_TEXT,
1209    "org": { defaultType: "text", structuredValue: ";" },
1210    "photo": DEFAULT_TYPE_URI,
1211    "related": DEFAULT_TYPE_URI,
1212    "rev": { defaultType: "timestamp" },
1213    "role": DEFAULT_TYPE_TEXT,
1214    "sound": DEFAULT_TYPE_URI,
1215    "source": DEFAULT_TYPE_URI,
1216    "tel": { defaultType: "uri", allowedTypes: ["uri", "text"] },
1217    "title": DEFAULT_TYPE_TEXT,
1218    "tz": { defaultType: "text", allowedTypes: ["text", "utc-offset", "uri"] },
1219    "xml": DEFAULT_TYPE_TEXT
1220  });
1221
1222  var vcard3Values = ICAL.helpers.extend(commonValues, {
1223    binary: icalValues.binary,
1224    date: vcardValues.date,
1225    "date-time": vcardValues["date-time"],
1226    "phone-number": {
1227      // TODO
1228      /* ... */
1229    },
1230    uri: icalValues.uri,
1231    text: icalValues.text,
1232    time: icalValues.time,
1233    vcard: icalValues.text,
1234    "utc-offset": {
1235      toICAL: function(aValue) {
1236        return aValue.substr(0, 7);
1237      },
1238
1239      fromICAL: function(aValue) {
1240        return aValue.substr(0, 7);
1241      },
1242
1243      decorate: function(aValue) {
1244        return ICAL.UtcOffset.fromString(aValue);
1245      },
1246
1247      undecorate: function(aValue) {
1248        return aValue.toString();
1249      }
1250    }
1251  });
1252
1253  var vcard3Params = {
1254    "type": {
1255      valueType: "text",
1256      multiValue: ","
1257    },
1258    "value": {
1259      // since the value here is a 'type' lowercase is used.
1260      values: ["text", "uri", "date", "date-time", "phone-number", "time",
1261               "boolean", "integer", "float", "utc-offset", "vcard", "binary"],
1262      allowXName: true,
1263      allowIanaToken: true
1264    }
1265  };
1266
1267  var vcard3Properties = ICAL.helpers.extend(commonProperties, {
1268    fn: DEFAULT_TYPE_TEXT,
1269    n: { defaultType: "text", structuredValue: ";", multiValue: "," },
1270    nickname: DEFAULT_TYPE_TEXT_MULTI,
1271    photo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
1272    bday: {
1273      defaultType: "date-time",
1274      allowedTypes: ["date-time", "date"],
1275      detectType: function(string) {
1276        return (string.indexOf('T') === -1) ? 'date' : 'date-time';
1277      }
1278    },
1279
1280    adr: { defaultType: "text", structuredValue: ";", multiValue: "," },
1281    label: DEFAULT_TYPE_TEXT,
1282
1283    tel: { defaultType: "phone-number" },
1284    email: DEFAULT_TYPE_TEXT,
1285    mailer: DEFAULT_TYPE_TEXT,
1286
1287    tz: { defaultType: "utc-offset", allowedTypes: ["utc-offset", "text"] },
1288    geo: { defaultType: "float", structuredValue: ";" },
1289
1290    title: DEFAULT_TYPE_TEXT,
1291    role: DEFAULT_TYPE_TEXT,
1292    logo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
1293    agent: { defaultType: "vcard", allowedTypes: ["vcard", "text", "uri"] },
1294    org: DEFAULT_TYPE_TEXT_STRUCTURED,
1295
1296    note: DEFAULT_TYPE_TEXT_MULTI,
1297    prodid: DEFAULT_TYPE_TEXT,
1298    rev: {
1299      defaultType: "date-time",
1300      allowedTypes: ["date-time", "date"],
1301      detectType: function(string) {
1302        return (string.indexOf('T') === -1) ? 'date' : 'date-time';
1303      }
1304    },
1305    "sort-string": DEFAULT_TYPE_TEXT,
1306    sound: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
1307
1308    class: DEFAULT_TYPE_TEXT,
1309    key: { defaultType: "binary", allowedTypes: ["binary", "text"] }
1310  });
1311
1312  /**
1313   * iCalendar design set
1314   * @type {ICAL.design.designSet}
1315   */
1316  var icalSet = {
1317    value: icalValues,
1318    param: icalParams,
1319    property: icalProperties
1320  };
1321
1322  /**
1323   * vCard 4.0 design set
1324   * @type {ICAL.design.designSet}
1325   */
1326  var vcardSet = {
1327    value: vcardValues,
1328    param: vcardParams,
1329    property: vcardProperties
1330  };
1331
1332  /**
1333   * vCard 3.0 design set
1334   * @type {ICAL.design.designSet}
1335   */
1336  var vcard3Set = {
1337    value: vcard3Values,
1338    param: vcard3Params,
1339    property: vcard3Properties
1340  };
1341
1342  /**
1343   * The design data, used by the parser to determine types for properties and
1344   * other metadata needed to produce correct jCard/jCal data.
1345   *
1346   * @alias ICAL.design
1347   * @namespace
1348   */
1349  var design = {
1350    /**
1351     * A designSet describes value, parameter and property data. It is used by
1352     * ther parser and stringifier in components and properties to determine they
1353     * should be represented.
1354     *
1355     * @typedef {Object} designSet
1356     * @memberOf ICAL.design
1357     * @property {Object} value       Definitions for value types, keys are type names
1358     * @property {Object} param       Definitions for params, keys are param names
1359     * @property {Object} property    Defintions for properties, keys are property names
1360     */
1361
1362    /**
1363     * Can be set to false to make the parser more lenient.
1364     */
1365    strict: true,
1366
1367    /**
1368     * The default set for new properties and components if none is specified.
1369     * @type {ICAL.design.designSet}
1370     */
1371    defaultSet: icalSet,
1372
1373    /**
1374     * The default type for unknown properties
1375     * @type {String}
1376     */
1377    defaultType: 'unknown',
1378
1379    /**
1380     * Holds the design set for known top-level components
1381     *
1382     * @type {Object}
1383     * @property {ICAL.design.designSet} vcard       vCard VCARD
1384     * @property {ICAL.design.designSet} vevent      iCalendar VEVENT
1385     * @property {ICAL.design.designSet} vtodo       iCalendar VTODO
1386     * @property {ICAL.design.designSet} vjournal    iCalendar VJOURNAL
1387     * @property {ICAL.design.designSet} valarm      iCalendar VALARM
1388     * @property {ICAL.design.designSet} vtimezone   iCalendar VTIMEZONE
1389     * @property {ICAL.design.designSet} daylight    iCalendar DAYLIGHT
1390     * @property {ICAL.design.designSet} standard    iCalendar STANDARD
1391     *
1392     * @example
1393     * var propertyName = 'fn';
1394     * var componentDesign = ICAL.design.components.vcard;
1395     * var propertyDetails = componentDesign.property[propertyName];
1396     * if (propertyDetails.defaultType == 'text') {
1397     *   // Yep, sure is...
1398     * }
1399     */
1400    components: {
1401      vcard: vcardSet,
1402      vcard3: vcard3Set,
1403      vevent: icalSet,
1404      vtodo: icalSet,
1405      vjournal: icalSet,
1406      valarm: icalSet,
1407      vtimezone: icalSet,
1408      daylight: icalSet,
1409      standard: icalSet
1410    },
1411
1412
1413    /**
1414     * The design set for iCalendar (rfc5545/rfc7265) components.
1415     * @type {ICAL.design.designSet}
1416     */
1417    icalendar: icalSet,
1418
1419    /**
1420     * The design set for vCard (rfc6350/rfc7095) components.
1421     * @type {ICAL.design.designSet}
1422     */
1423    vcard: vcardSet,
1424
1425    /**
1426     * The design set for vCard (rfc2425/rfc2426/rfc7095) components.
1427     * @type {ICAL.design.designSet}
1428     */
1429    vcard3: vcard3Set,
1430
1431    /**
1432     * Gets the design set for the given component name.
1433     *
1434     * @param {String} componentName        The name of the component
1435     * @return {ICAL.design.designSet}      The design set for the component
1436     */
1437    getDesignSet: function(componentName) {
1438      var isInDesign = componentName && componentName in design.components;
1439      return isInDesign ? design.components[componentName] : design.defaultSet;
1440    }
1441  };
1442
1443  return design;
1444}());
1445/* This Source Code Form is subject to the terms of the Mozilla Public
1446 * License, v. 2.0. If a copy of the MPL was not distributed with this
1447 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
1448 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
1449
1450
1451/**
1452 * Contains various functions to convert jCal and jCard data back into
1453 * iCalendar and vCard.
1454 * @namespace
1455 */
1456ICAL.stringify = (function() {
1457  'use strict';
1458
1459  var LINE_ENDING = '\r\n';
1460  var DEFAULT_VALUE_TYPE = 'unknown';
1461
1462  var design = ICAL.design;
1463  var helpers = ICAL.helpers;
1464
1465  /**
1466   * Convert a full jCal/jCard array into a iCalendar/vCard string.
1467   *
1468   * @function ICAL.stringify
1469   * @variation function
1470   * @param {Array} jCal    The jCal/jCard document
1471   * @return {String}       The stringified iCalendar/vCard document
1472   */
1473  function stringify(jCal) {
1474    if (typeof jCal[0] == "string") {
1475      // This is a single component
1476      jCal = [jCal];
1477    }
1478
1479    var i = 0;
1480    var len = jCal.length;
1481    var result = '';
1482
1483    for (; i < len; i++) {
1484      result += stringify.component(jCal[i]) + LINE_ENDING;
1485    }
1486
1487    return result;
1488  }
1489
1490  /**
1491   * Converts an jCal component array into a ICAL string.
1492   * Recursive will resolve sub-components.
1493   *
1494   * Exact component/property order is not saved all
1495   * properties will come before subcomponents.
1496   *
1497   * @function ICAL.stringify.component
1498   * @param {Array} component
1499   *        jCal/jCard fragment of a component
1500   * @param {ICAL.design.designSet} designSet
1501   *        The design data to use for this component
1502   * @return {String}       The iCalendar/vCard string
1503   */
1504  stringify.component = function(component, designSet) {
1505    var name = component[0].toUpperCase();
1506    var result = 'BEGIN:' + name + LINE_ENDING;
1507
1508    var props = component[1];
1509    var propIdx = 0;
1510    var propLen = props.length;
1511
1512    var designSetName = component[0];
1513    // rfc6350 requires that in vCard 4.0 the first component is the VERSION
1514    // component with as value 4.0, note that 3.0 does not have this requirement.
1515    if (designSetName === 'vcard' && component[1].length > 0 &&
1516            !(component[1][0][0] === "version" && component[1][0][3] === "4.0")) {
1517      designSetName = "vcard3";
1518    }
1519    designSet = designSet || design.getDesignSet(designSetName);
1520
1521    for (; propIdx < propLen; propIdx++) {
1522      result += stringify.property(props[propIdx], designSet) + LINE_ENDING;
1523    }
1524
1525    // Ignore subcomponents if none exist, e.g. in vCard.
1526    var comps = component[2] || [];
1527    var compIdx = 0;
1528    var compLen = comps.length;
1529
1530    for (; compIdx < compLen; compIdx++) {
1531      result += stringify.component(comps[compIdx], designSet) + LINE_ENDING;
1532    }
1533
1534    result += 'END:' + name;
1535    return result;
1536  };
1537
1538  /**
1539   * Converts a single jCal/jCard property to a iCalendar/vCard string.
1540   *
1541   * @function ICAL.stringify.property
1542   * @param {Array} property
1543   *        jCal/jCard property array
1544   * @param {ICAL.design.designSet} designSet
1545   *        The design data to use for this property
1546   * @param {Boolean} noFold
1547   *        If true, the line is not folded
1548   * @return {String}       The iCalendar/vCard string
1549   */
1550  stringify.property = function(property, designSet, noFold) {
1551    var name = property[0].toUpperCase();
1552    var jsName = property[0];
1553    var params = property[1];
1554
1555    var line = name;
1556
1557    var paramName;
1558    for (paramName in params) {
1559      var value = params[paramName];
1560
1561      /* istanbul ignore else */
1562      if (params.hasOwnProperty(paramName)) {
1563        var multiValue = (paramName in designSet.param) && designSet.param[paramName].multiValue;
1564        if (multiValue && Array.isArray(value)) {
1565          if (designSet.param[paramName].multiValueSeparateDQuote) {
1566            multiValue = '"' + multiValue + '"';
1567          }
1568          value = value.map(stringify._rfc6868Unescape);
1569          value = stringify.multiValue(value, multiValue, "unknown", null, designSet);
1570        } else {
1571          value = stringify._rfc6868Unescape(value);
1572        }
1573
1574
1575        line += ';' + paramName.toUpperCase();
1576        line += '=' + stringify.propertyValue(value);
1577      }
1578    }
1579
1580    if (property.length === 3) {
1581      // If there are no values, we must assume a blank value
1582      return line + ':';
1583    }
1584
1585    var valueType = property[2];
1586
1587    if (!designSet) {
1588      designSet = design.defaultSet;
1589    }
1590
1591    var propDetails;
1592    var multiValue = false;
1593    var structuredValue = false;
1594    var isDefault = false;
1595
1596    if (jsName in designSet.property) {
1597      propDetails = designSet.property[jsName];
1598
1599      if ('multiValue' in propDetails) {
1600        multiValue = propDetails.multiValue;
1601      }
1602
1603      if (('structuredValue' in propDetails) && Array.isArray(property[3])) {
1604        structuredValue = propDetails.structuredValue;
1605      }
1606
1607      if ('defaultType' in propDetails) {
1608        if (valueType === propDetails.defaultType) {
1609          isDefault = true;
1610        }
1611      } else {
1612        if (valueType === DEFAULT_VALUE_TYPE) {
1613          isDefault = true;
1614        }
1615      }
1616    } else {
1617      if (valueType === DEFAULT_VALUE_TYPE) {
1618        isDefault = true;
1619      }
1620    }
1621
1622    // push the VALUE property if type is not the default
1623    // for the current property.
1624    if (!isDefault) {
1625      // value will never contain ;/:/, so we don't escape it here.
1626      line += ';VALUE=' + valueType.toUpperCase();
1627    }
1628
1629    line += ':';
1630
1631    if (multiValue && structuredValue) {
1632      line += stringify.multiValue(
1633        property[3], structuredValue, valueType, multiValue, designSet, structuredValue
1634      );
1635    } else if (multiValue) {
1636      line += stringify.multiValue(
1637        property.slice(3), multiValue, valueType, null, designSet, false
1638      );
1639    } else if (structuredValue) {
1640      line += stringify.multiValue(
1641        property[3], structuredValue, valueType, null, designSet, structuredValue
1642      );
1643    } else {
1644      line += stringify.value(property[3], valueType, designSet, false);
1645    }
1646
1647    return noFold ? line : ICAL.helpers.foldline(line);
1648  };
1649
1650  /**
1651   * Handles escaping of property values that may contain:
1652   *
1653   *    COLON (:), SEMICOLON (;), or COMMA (,)
1654   *
1655   * If any of the above are present the result is wrapped
1656   * in double quotes.
1657   *
1658   * @function ICAL.stringify.propertyValue
1659   * @param {String} value      Raw property value
1660   * @return {String}           Given or escaped value when needed
1661   */
1662  stringify.propertyValue = function(value) {
1663
1664    if ((helpers.unescapedIndexOf(value, ',') === -1) &&
1665        (helpers.unescapedIndexOf(value, ':') === -1) &&
1666        (helpers.unescapedIndexOf(value, ';') === -1)) {
1667
1668      return value;
1669    }
1670
1671    return '"' + value + '"';
1672  };
1673
1674  /**
1675   * Converts an array of ical values into a single
1676   * string based on a type and a delimiter value (like ",").
1677   *
1678   * @function ICAL.stringify.multiValue
1679   * @param {Array} values      List of values to convert
1680   * @param {String} delim      Used to join the values (",", ";", ":")
1681   * @param {String} type       Lowecase ical value type
1682   *        (like boolean, date-time, etc..)
1683   * @param {?String} innerMulti If set, each value will again be processed
1684   *        Used for structured values
1685   * @param {ICAL.design.designSet} designSet
1686   *        The design data to use for this property
1687   *
1688   * @return {String}           iCalendar/vCard string for value
1689   */
1690  stringify.multiValue = function(values, delim, type, innerMulti, designSet, structuredValue) {
1691    var result = '';
1692    var len = values.length;
1693    var i = 0;
1694
1695    for (; i < len; i++) {
1696      if (innerMulti && Array.isArray(values[i])) {
1697        result += stringify.multiValue(values[i], innerMulti, type, null, designSet, structuredValue);
1698      } else {
1699        result += stringify.value(values[i], type, designSet, structuredValue);
1700      }
1701
1702      if (i !== (len - 1)) {
1703        result += delim;
1704      }
1705    }
1706
1707    return result;
1708  };
1709
1710  /**
1711   * Processes a single ical value runs the associated "toICAL" method from the
1712   * design value type if available to convert the value.
1713   *
1714   * @function ICAL.stringify.value
1715   * @param {String|Number} value       A formatted value
1716   * @param {String} type               Lowercase iCalendar/vCard value type
1717   *  (like boolean, date-time, etc..)
1718   * @return {String}                   iCalendar/vCard value for single value
1719   */
1720  stringify.value = function(value, type, designSet, structuredValue) {
1721    if (type in designSet.value && 'toICAL' in designSet.value[type]) {
1722      return designSet.value[type].toICAL(value, structuredValue);
1723    }
1724    return value;
1725  };
1726
1727  /**
1728   * Internal helper for rfc6868. Exposing this on ICAL.stringify so that
1729   * hackers can disable the rfc6868 parsing if the really need to.
1730   *
1731   * @param {String} val        The value to unescape
1732   * @return {String}           The escaped value
1733   */
1734  stringify._rfc6868Unescape = function(val) {
1735    return val.replace(/[\n^"]/g, function(x) {
1736      return RFC6868_REPLACE_MAP[x];
1737    });
1738  };
1739  var RFC6868_REPLACE_MAP = { '"': "^'", "\n": "^n", "^": "^^" };
1740
1741  return stringify;
1742}());
1743/* This Source Code Form is subject to the terms of the Mozilla Public
1744 * License, v. 2.0. If a copy of the MPL was not distributed with this
1745 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
1746 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
1747
1748
1749/**
1750 * Contains various functions to parse iCalendar and vCard data.
1751 * @namespace
1752 */
1753ICAL.parse = (function() {
1754  'use strict';
1755
1756  var CHAR = /[^ \t]/;
1757  var MULTIVALUE_DELIMITER = ',';
1758  var VALUE_DELIMITER = ':';
1759  var PARAM_DELIMITER = ';';
1760  var PARAM_NAME_DELIMITER = '=';
1761  var DEFAULT_VALUE_TYPE = 'unknown';
1762  var DEFAULT_PARAM_TYPE = 'text';
1763
1764  var design = ICAL.design;
1765  var helpers = ICAL.helpers;
1766
1767  /**
1768   * An error that occurred during parsing.
1769   *
1770   * @param {String} message        The error message
1771   * @memberof ICAL.parse
1772   * @extends {Error}
1773   * @class
1774   */
1775  function ParserError(message) {
1776    this.message = message;
1777    this.name = 'ParserError';
1778
1779    try {
1780      throw new Error();
1781    } catch (e) {
1782      if (e.stack) {
1783        var split = e.stack.split('\n');
1784        split.shift();
1785        this.stack = split.join('\n');
1786      }
1787    }
1788  }
1789
1790  ParserError.prototype = Error.prototype;
1791
1792  /**
1793   * Parses iCalendar or vCard data into a raw jCal object. Consult
1794   * documentation on the {@tutorial layers|layers of parsing} for more
1795   * details.
1796   *
1797   * @function ICAL.parse
1798   * @variation function
1799   * @todo Fix the API to be more clear on the return type
1800   * @param {String} input      The string data to parse
1801   * @return {Object|Object[]}  A single jCal object, or an array thereof
1802   */
1803  function parser(input) {
1804    var state = {};
1805    var root = state.component = [];
1806
1807    state.stack = [root];
1808
1809    parser._eachLine(input, function(err, line) {
1810      parser._handleContentLine(line, state);
1811    });
1812
1813
1814    // when there are still items on the stack
1815    // throw a fatal error, a component was not closed
1816    // correctly in that case.
1817    if (state.stack.length > 1) {
1818      throw new ParserError(
1819        'invalid ical body. component began but did not end'
1820      );
1821    }
1822
1823    state = null;
1824
1825    return (root.length == 1 ? root[0] : root);
1826  }
1827
1828  /**
1829   * Parse an iCalendar property value into the jCal for a single property
1830   *
1831   * @function ICAL.parse.property
1832   * @param {String} str
1833   *   The iCalendar property string to parse
1834   * @param {ICAL.design.designSet=} designSet
1835   *   The design data to use for this property
1836   * @return {Object}
1837   *   The jCal Object containing the property
1838   */
1839  parser.property = function(str, designSet) {
1840    var state = {
1841      component: [[], []],
1842      designSet: designSet || design.defaultSet
1843    };
1844    parser._handleContentLine(str, state);
1845    return state.component[1][0];
1846  };
1847
1848  /**
1849   * Convenience method to parse a component. You can use ICAL.parse() directly
1850   * instead.
1851   *
1852   * @function ICAL.parse.component
1853   * @see ICAL.parse(function)
1854   * @param {String} str    The iCalendar component string to parse
1855   * @return {Object}       The jCal Object containing the component
1856   */
1857  parser.component = function(str) {
1858    return parser(str);
1859  };
1860
1861  // classes & constants
1862  parser.ParserError = ParserError;
1863
1864  /**
1865   * The state for parsing content lines from an iCalendar/vCard string.
1866   *
1867   * @private
1868   * @memberof ICAL.parse
1869   * @typedef {Object} parserState
1870   * @property {ICAL.design.designSet} designSet    The design set to use for parsing
1871   * @property {ICAL.Component[]} stack             The stack of components being processed
1872   * @property {ICAL.Component} component           The currently active component
1873   */
1874
1875
1876  /**
1877   * Handles a single line of iCalendar/vCard, updating the state.
1878   *
1879   * @private
1880   * @function ICAL.parse._handleContentLine
1881   * @param {String} line               The content line to process
1882   * @param {ICAL.parse.parserState}    The current state of the line parsing
1883   */
1884  parser._handleContentLine = function(line, state) {
1885    // break up the parts of the line
1886    var valuePos = line.indexOf(VALUE_DELIMITER);
1887    var paramPos = line.indexOf(PARAM_DELIMITER);
1888
1889    var lastParamIndex;
1890    var lastValuePos;
1891
1892    // name of property or begin/end
1893    var name;
1894    var value;
1895    // params is only overridden if paramPos !== -1.
1896    // we can't do params = params || {} later on
1897    // because it sacrifices ops.
1898    var params = {};
1899
1900    /**
1901     * Different property cases
1902     *
1903     *
1904     * 1. RRULE:FREQ=foo
1905     *    // FREQ= is not a param but the value
1906     *
1907     * 2. ATTENDEE;ROLE=REQ-PARTICIPANT;
1908     *    // ROLE= is a param because : has not happened yet
1909     */
1910      // when the parameter delimiter is after the
1911      // value delimiter then its not a parameter.
1912
1913    if ((paramPos !== -1 && valuePos !== -1)) {
1914      // when the parameter delimiter is after the
1915      // value delimiter then its not a parameter.
1916      if (paramPos > valuePos) {
1917        paramPos = -1;
1918      }
1919    }
1920
1921    var parsedParams;
1922    if (paramPos !== -1) {
1923      name = line.substring(0, paramPos).toLowerCase();
1924      parsedParams = parser._parseParameters(line.substring(paramPos), 0, state.designSet);
1925      if (parsedParams[2] == -1) {
1926        throw new ParserError("Invalid parameters in '" + line + "'");
1927      }
1928      params = parsedParams[0];
1929      lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos;
1930      if ((lastValuePos =
1931        line.substring(lastParamIndex).indexOf(VALUE_DELIMITER)) !== -1) {
1932        value = line.substring(lastParamIndex + lastValuePos + 1);
1933      } else {
1934        throw new ParserError("Missing parameter value in '" + line + "'");
1935      }
1936    } else if (valuePos !== -1) {
1937      // without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC)
1938      name = line.substring(0, valuePos).toLowerCase();
1939      value = line.substring(valuePos + 1);
1940
1941      if (name === 'begin') {
1942        var newComponent = [value.toLowerCase(), [], []];
1943        if (state.stack.length === 1) {
1944          state.component.push(newComponent);
1945        } else {
1946          state.component[2].push(newComponent);
1947        }
1948        state.stack.push(state.component);
1949        state.component = newComponent;
1950        if (!state.designSet) {
1951          state.designSet = design.getDesignSet(state.component[0]);
1952        }
1953        return;
1954      } else if (name === 'end') {
1955        state.component = state.stack.pop();
1956        return;
1957      }
1958      // If its not begin/end, then this is a property with an empty value,
1959      // which should be considered valid.
1960    } else {
1961      /**
1962       * Invalid line.
1963       * The rational to throw an error is we will
1964       * never be certain that the rest of the file
1965       * is sane and its unlikely that we can serialize
1966       * the result correctly either.
1967       */
1968      throw new ParserError(
1969        'invalid line (no token ";" or ":") "' + line + '"'
1970      );
1971    }
1972
1973    var valueType;
1974    var multiValue = false;
1975    var structuredValue = false;
1976    var propertyDetails;
1977
1978    if (name in state.designSet.property) {
1979      propertyDetails = state.designSet.property[name];
1980
1981      if ('multiValue' in propertyDetails) {
1982        multiValue = propertyDetails.multiValue;
1983      }
1984
1985      if ('structuredValue' in propertyDetails) {
1986        structuredValue = propertyDetails.structuredValue;
1987      }
1988
1989      if (value && 'detectType' in propertyDetails) {
1990        valueType = propertyDetails.detectType(value);
1991      }
1992    }
1993
1994    // attempt to determine value
1995    if (!valueType) {
1996      if (!('value' in params)) {
1997        if (propertyDetails) {
1998          valueType = propertyDetails.defaultType;
1999        } else {
2000          valueType = DEFAULT_VALUE_TYPE;
2001        }
2002      } else {
2003        // possible to avoid this?
2004        valueType = params.value.toLowerCase();
2005      }
2006    }
2007
2008    delete params.value;
2009
2010    /**
2011     * Note on `var result` juggling:
2012     *
2013     * I observed that building the array in pieces has adverse
2014     * effects on performance, so where possible we inline the creation.
2015     * Its a little ugly but resulted in ~2000 additional ops/sec.
2016     */
2017
2018    var result;
2019    if (multiValue && structuredValue) {
2020      value = parser._parseMultiValue(value, structuredValue, valueType, [], multiValue, state.designSet, structuredValue);
2021      result = [name, params, valueType, value];
2022    } else if (multiValue) {
2023      result = [name, params, valueType];
2024      parser._parseMultiValue(value, multiValue, valueType, result, null, state.designSet, false);
2025    } else if (structuredValue) {
2026      value = parser._parseMultiValue(value, structuredValue, valueType, [], null, state.designSet, structuredValue);
2027      result = [name, params, valueType, value];
2028    } else {
2029      value = parser._parseValue(value, valueType, state.designSet, false);
2030      result = [name, params, valueType, value];
2031    }
2032    // rfc6350 requires that in vCard 4.0 the first component is the VERSION
2033    // component with as value 4.0, note that 3.0 does not have this requirement.
2034    if (state.component[0] === 'vcard' && state.component[1].length === 0 &&
2035            !(name === 'version' && value === '4.0')) {
2036      state.designSet = design.getDesignSet("vcard3");
2037    }
2038    state.component[1].push(result);
2039  };
2040
2041  /**
2042   * Parse a value from the raw value into the jCard/jCal value.
2043   *
2044   * @private
2045   * @function ICAL.parse._parseValue
2046   * @param {String} value          Original value
2047   * @param {String} type           Type of value
2048   * @param {Object} designSet      The design data to use for this value
2049   * @return {Object} varies on type
2050   */
2051  parser._parseValue = function(value, type, designSet, structuredValue) {
2052    if (type in designSet.value && 'fromICAL' in designSet.value[type]) {
2053      return designSet.value[type].fromICAL(value, structuredValue);
2054    }
2055    return value;
2056  };
2057
2058  /**
2059   * Parse parameters from a string to object.
2060   *
2061   * @function ICAL.parse._parseParameters
2062   * @private
2063   * @param {String} line           A single unfolded line
2064   * @param {Numeric} start         Position to start looking for properties
2065   * @param {Object} designSet      The design data to use for this property
2066   * @return {Object} key/value pairs
2067   */
2068  parser._parseParameters = function(line, start, designSet) {
2069    var lastParam = start;
2070    var pos = 0;
2071    var delim = PARAM_NAME_DELIMITER;
2072    var result = {};
2073    var name, lcname;
2074    var value, valuePos = -1;
2075    var type, multiValue, mvdelim;
2076
2077    // find the next '=' sign
2078    // use lastParam and pos to find name
2079    // check if " is used if so get value from "->"
2080    // then increment pos to find next ;
2081
2082    while ((pos !== false) &&
2083           (pos = helpers.unescapedIndexOf(line, delim, pos + 1)) !== -1) {
2084
2085      name = line.substr(lastParam + 1, pos - lastParam - 1);
2086      if (name.length == 0) {
2087        throw new ParserError("Empty parameter name in '" + line + "'");
2088      }
2089      lcname = name.toLowerCase();
2090      mvdelim = false;
2091      multiValue = false;
2092
2093      if (lcname in designSet.param && designSet.param[lcname].valueType) {
2094        type = designSet.param[lcname].valueType;
2095      } else {
2096        type = DEFAULT_PARAM_TYPE;
2097      }
2098
2099      if (lcname in designSet.param) {
2100        multiValue = designSet.param[lcname].multiValue;
2101        if (designSet.param[lcname].multiValueSeparateDQuote) {
2102          mvdelim = parser._rfc6868Escape('"' + multiValue + '"');
2103        }
2104      }
2105
2106      var nextChar = line[pos + 1];
2107      if (nextChar === '"') {
2108        valuePos = pos + 2;
2109        pos = helpers.unescapedIndexOf(line, '"', valuePos);
2110        if (multiValue && pos != -1) {
2111            var extendedValue = true;
2112            while (extendedValue) {
2113              if (line[pos + 1] == multiValue && line[pos + 2] == '"') {
2114                pos = helpers.unescapedIndexOf(line, '"', pos + 3);
2115              } else {
2116                extendedValue = false;
2117              }
2118            }
2119          }
2120        if (pos === -1) {
2121          throw new ParserError(
2122            'invalid line (no matching double quote) "' + line + '"'
2123          );
2124        }
2125        value = line.substr(valuePos, pos - valuePos);
2126        lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos);
2127        if (lastParam === -1) {
2128          pos = false;
2129        }
2130      } else {
2131        valuePos = pos + 1;
2132
2133        // move to next ";"
2134        var nextPos = helpers.unescapedIndexOf(line, PARAM_DELIMITER, valuePos);
2135        var propValuePos = helpers.unescapedIndexOf(line, VALUE_DELIMITER, valuePos);
2136        if (propValuePos !== -1 && nextPos > propValuePos) {
2137          // this is a delimiter in the property value, let's stop here
2138          nextPos = propValuePos;
2139          pos = false;
2140        } else if (nextPos === -1) {
2141          // no ";"
2142          if (propValuePos === -1) {
2143            nextPos = line.length;
2144          } else {
2145            nextPos = propValuePos;
2146          }
2147          pos = false;
2148        } else {
2149          lastParam = nextPos;
2150          pos = nextPos;
2151        }
2152
2153        value = line.substr(valuePos, nextPos - valuePos);
2154      }
2155
2156      value = parser._rfc6868Escape(value);
2157      if (multiValue) {
2158        var delimiter = mvdelim || multiValue;
2159        value = parser._parseMultiValue(value, delimiter, type, [], null, designSet);
2160      } else {
2161        value = parser._parseValue(value, type, designSet);
2162      }
2163
2164      if (multiValue && (lcname in result)) {
2165        if (Array.isArray(result[lcname])) {
2166          result[lcname].push(value);
2167        } else {
2168          result[lcname] = [
2169            result[lcname],
2170            value
2171          ];
2172        }
2173      } else {
2174        result[lcname] = value;
2175      }
2176    }
2177    return [result, value, valuePos];
2178  };
2179
2180  /**
2181   * Internal helper for rfc6868. Exposing this on ICAL.parse so that
2182   * hackers can disable the rfc6868 parsing if the really need to.
2183   *
2184   * @function ICAL.parse._rfc6868Escape
2185   * @param {String} val        The value to escape
2186   * @return {String}           The escaped value
2187   */
2188  parser._rfc6868Escape = function(val) {
2189    return val.replace(/\^['n^]/g, function(x) {
2190      return RFC6868_REPLACE_MAP[x];
2191    });
2192  };
2193  var RFC6868_REPLACE_MAP = { "^'": '"', "^n": "\n", "^^": "^" };
2194
2195  /**
2196   * Parse a multi value string. This function is used either for parsing
2197   * actual multi-value property's values, or for handling parameter values. It
2198   * can be used for both multi-value properties and structured value properties.
2199   *
2200   * @private
2201   * @function ICAL.parse._parseMultiValue
2202   * @param {String} buffer     The buffer containing the full value
2203   * @param {String} delim      The multi-value delimiter
2204   * @param {String} type       The value type to be parsed
2205   * @param {Array.<?>} result        The array to append results to, varies on value type
2206   * @param {String} innerMulti The inner delimiter to split each value with
2207   * @param {ICAL.design.designSet} designSet   The design data for this value
2208   * @return {?|Array.<?>}            Either an array of results, or the first result
2209   */
2210  parser._parseMultiValue = function(buffer, delim, type, result, innerMulti, designSet, structuredValue) {
2211    var pos = 0;
2212    var lastPos = 0;
2213    var value;
2214    if (delim.length === 0) {
2215      return buffer;
2216    }
2217
2218    // split each piece
2219    while ((pos = helpers.unescapedIndexOf(buffer, delim, lastPos)) !== -1) {
2220      value = buffer.substr(lastPos, pos - lastPos);
2221      if (innerMulti) {
2222        value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
2223      } else {
2224        value = parser._parseValue(value, type, designSet, structuredValue);
2225      }
2226      result.push(value);
2227      lastPos = pos + delim.length;
2228    }
2229
2230    // on the last piece take the rest of string
2231    value = buffer.substr(lastPos);
2232    if (innerMulti) {
2233      value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
2234    } else {
2235      value = parser._parseValue(value, type, designSet, structuredValue);
2236    }
2237    result.push(value);
2238
2239    return result.length == 1 ? result[0] : result;
2240  };
2241
2242  /**
2243   * Process a complete buffer of iCalendar/vCard data line by line, correctly
2244   * unfolding content. Each line will be processed with the given callback
2245   *
2246   * @private
2247   * @function ICAL.parse._eachLine
2248   * @param {String} buffer                         The buffer to process
2249   * @param {function(?String, String)} callback    The callback for each line
2250   */
2251  parser._eachLine = function(buffer, callback) {
2252    var len = buffer.length;
2253    var lastPos = buffer.search(CHAR);
2254    var pos = lastPos;
2255    var line;
2256    var firstChar;
2257
2258    var newlineOffset;
2259
2260    do {
2261      pos = buffer.indexOf('\n', lastPos) + 1;
2262
2263      if (pos > 1 && buffer[pos - 2] === '\r') {
2264        newlineOffset = 2;
2265      } else {
2266        newlineOffset = 1;
2267      }
2268
2269      if (pos === 0) {
2270        pos = len;
2271        newlineOffset = 0;
2272      }
2273
2274      firstChar = buffer[lastPos];
2275
2276      if (firstChar === ' ' || firstChar === '\t') {
2277        // add to line
2278        line += buffer.substr(
2279          lastPos + 1,
2280          pos - lastPos - (newlineOffset + 1)
2281        );
2282      } else {
2283        if (line)
2284          callback(null, line);
2285        // push line
2286        line = buffer.substr(
2287          lastPos,
2288          pos - lastPos - newlineOffset
2289        );
2290      }
2291
2292      lastPos = pos;
2293    } while (pos !== len);
2294
2295    // extra ending line
2296    line = line.trim();
2297
2298    if (line.length)
2299      callback(null, line);
2300  };
2301
2302  return parser;
2303
2304}());
2305/* This Source Code Form is subject to the terms of the Mozilla Public
2306 * License, v. 2.0. If a copy of the MPL was not distributed with this
2307 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
2308 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
2309
2310
2311/**
2312 * This symbol is further described later on
2313 * @ignore
2314 */
2315ICAL.Component = (function() {
2316  'use strict';
2317
2318  var PROPERTY_INDEX = 1;
2319  var COMPONENT_INDEX = 2;
2320  var NAME_INDEX = 0;
2321
2322  /**
2323   * @classdesc
2324   * Wraps a jCal component, adding convenience methods to add, remove and
2325   * update subcomponents and properties.
2326   *
2327   * @class
2328   * @alias ICAL.Component
2329   * @param {Array|String} jCal         Raw jCal component data OR name of new
2330   *                                      component
2331   * @param {ICAL.Component} parent     Parent component to associate
2332   */
2333  function Component(jCal, parent) {
2334    if (typeof(jCal) === 'string') {
2335      // jCal spec (name, properties, components)
2336      jCal = [jCal, [], []];
2337    }
2338
2339    // mostly for legacy reasons.
2340    this.jCal = jCal;
2341
2342    this.parent = parent || null;
2343  }
2344
2345  Component.prototype = {
2346    /**
2347     * Hydrated properties are inserted into the _properties array at the same
2348     * position as in the jCal array, so its possible the array contains
2349     * undefined values for unhydrdated properties. To avoid iterating the
2350     * array when checking if all properties have been hydrated, we save the
2351     * count here.
2352     *
2353     * @type {Number}
2354     * @private
2355     */
2356    _hydratedPropertyCount: 0,
2357
2358    /**
2359     * The same count as for _hydratedPropertyCount, but for subcomponents
2360     *
2361     * @type {Number}
2362     * @private
2363     */
2364    _hydratedComponentCount: 0,
2365
2366    /**
2367     * The name of this component
2368     * @readonly
2369     */
2370    get name() {
2371      return this.jCal[NAME_INDEX];
2372    },
2373
2374    /**
2375     * The design set for this component, e.g. icalendar vs vcard
2376     *
2377     * @type {ICAL.design.designSet}
2378     * @private
2379     */
2380    get _designSet() {
2381      var parentDesign = this.parent && this.parent._designSet;
2382      return parentDesign || ICAL.design.getDesignSet(this.name);
2383    },
2384
2385    _hydrateComponent: function(index) {
2386      if (!this._components) {
2387        this._components = [];
2388        this._hydratedComponentCount = 0;
2389      }
2390
2391      if (this._components[index]) {
2392        return this._components[index];
2393      }
2394
2395      var comp = new Component(
2396        this.jCal[COMPONENT_INDEX][index],
2397        this
2398      );
2399
2400      this._hydratedComponentCount++;
2401      return (this._components[index] = comp);
2402    },
2403
2404    _hydrateProperty: function(index) {
2405      if (!this._properties) {
2406        this._properties = [];
2407        this._hydratedPropertyCount = 0;
2408      }
2409
2410      if (this._properties[index]) {
2411        return this._properties[index];
2412      }
2413
2414      var prop = new ICAL.Property(
2415        this.jCal[PROPERTY_INDEX][index],
2416        this
2417      );
2418
2419      this._hydratedPropertyCount++;
2420      return (this._properties[index] = prop);
2421    },
2422
2423    /**
2424     * Finds first sub component, optionally filtered by name.
2425     *
2426     * @param {String=} name        Optional name to filter by
2427     * @return {?ICAL.Component}     The found subcomponent
2428     */
2429    getFirstSubcomponent: function(name) {
2430      if (name) {
2431        var i = 0;
2432        var comps = this.jCal[COMPONENT_INDEX];
2433        var len = comps.length;
2434
2435        for (; i < len; i++) {
2436          if (comps[i][NAME_INDEX] === name) {
2437            var result = this._hydrateComponent(i);
2438            return result;
2439          }
2440        }
2441      } else {
2442        if (this.jCal[COMPONENT_INDEX].length) {
2443          return this._hydrateComponent(0);
2444        }
2445      }
2446
2447      // ensure we return a value (strict mode)
2448      return null;
2449    },
2450
2451    /**
2452     * Finds all sub components, optionally filtering by name.
2453     *
2454     * @param {String=} name            Optional name to filter by
2455     * @return {ICAL.Component[]}       The found sub components
2456     */
2457    getAllSubcomponents: function(name) {
2458      var jCalLen = this.jCal[COMPONENT_INDEX].length;
2459      var i = 0;
2460
2461      if (name) {
2462        var comps = this.jCal[COMPONENT_INDEX];
2463        var result = [];
2464
2465        for (; i < jCalLen; i++) {
2466          if (name === comps[i][NAME_INDEX]) {
2467            result.push(
2468              this._hydrateComponent(i)
2469            );
2470          }
2471        }
2472        return result;
2473      } else {
2474        if (!this._components ||
2475            (this._hydratedComponentCount !== jCalLen)) {
2476          for (; i < jCalLen; i++) {
2477            this._hydrateComponent(i);
2478          }
2479        }
2480
2481        return this._components || [];
2482      }
2483    },
2484
2485    /**
2486     * Returns true when a named property exists.
2487     *
2488     * @param {String} name     The property name
2489     * @return {Boolean}        True, when property is found
2490     */
2491    hasProperty: function(name) {
2492      var props = this.jCal[PROPERTY_INDEX];
2493      var len = props.length;
2494
2495      var i = 0;
2496      for (; i < len; i++) {
2497        // 0 is property name
2498        if (props[i][NAME_INDEX] === name) {
2499          return true;
2500        }
2501      }
2502
2503      return false;
2504    },
2505
2506    /**
2507     * Finds the first property, optionally with the given name.
2508     *
2509     * @param {String=} name        Lowercase property name
2510     * @return {?ICAL.Property}     The found property
2511     */
2512    getFirstProperty: function(name) {
2513      if (name) {
2514        var i = 0;
2515        var props = this.jCal[PROPERTY_INDEX];
2516        var len = props.length;
2517
2518        for (; i < len; i++) {
2519          if (props[i][NAME_INDEX] === name) {
2520            var result = this._hydrateProperty(i);
2521            return result;
2522          }
2523        }
2524      } else {
2525        if (this.jCal[PROPERTY_INDEX].length) {
2526          return this._hydrateProperty(0);
2527        }
2528      }
2529
2530      return null;
2531    },
2532
2533    /**
2534     * Returns first property's value, if available.
2535     *
2536     * @param {String=} name    Lowercase property name
2537     * @return {?String}        The found property value.
2538     */
2539    getFirstPropertyValue: function(name) {
2540      var prop = this.getFirstProperty(name);
2541      if (prop) {
2542        return prop.getFirstValue();
2543      }
2544
2545      return null;
2546    },
2547
2548    /**
2549     * Get all properties in the component, optionally filtered by name.
2550     *
2551     * @param {String=} name        Lowercase property name
2552     * @return {ICAL.Property[]}    List of properties
2553     */
2554    getAllProperties: function(name) {
2555      var jCalLen = this.jCal[PROPERTY_INDEX].length;
2556      var i = 0;
2557
2558      if (name) {
2559        var props = this.jCal[PROPERTY_INDEX];
2560        var result = [];
2561
2562        for (; i < jCalLen; i++) {
2563          if (name === props[i][NAME_INDEX]) {
2564            result.push(
2565              this._hydrateProperty(i)
2566            );
2567          }
2568        }
2569        return result;
2570      } else {
2571        if (!this._properties ||
2572            (this._hydratedPropertyCount !== jCalLen)) {
2573          for (; i < jCalLen; i++) {
2574            this._hydrateProperty(i);
2575          }
2576        }
2577
2578        return this._properties || [];
2579      }
2580    },
2581
2582    _removeObjectByIndex: function(jCalIndex, cache, index) {
2583      cache = cache || [];
2584      // remove cached version
2585      if (cache[index]) {
2586        var obj = cache[index];
2587        if ("parent" in obj) {
2588            obj.parent = null;
2589        }
2590      }
2591
2592      cache.splice(index, 1);
2593
2594      // remove it from the jCal
2595      this.jCal[jCalIndex].splice(index, 1);
2596    },
2597
2598    _removeObject: function(jCalIndex, cache, nameOrObject) {
2599      var i = 0;
2600      var objects = this.jCal[jCalIndex];
2601      var len = objects.length;
2602      var cached = this[cache];
2603
2604      if (typeof(nameOrObject) === 'string') {
2605        for (; i < len; i++) {
2606          if (objects[i][NAME_INDEX] === nameOrObject) {
2607            this._removeObjectByIndex(jCalIndex, cached, i);
2608            return true;
2609          }
2610        }
2611      } else if (cached) {
2612        for (; i < len; i++) {
2613          if (cached[i] && cached[i] === nameOrObject) {
2614            this._removeObjectByIndex(jCalIndex, cached, i);
2615            return true;
2616          }
2617        }
2618      }
2619
2620      return false;
2621    },
2622
2623    _removeAllObjects: function(jCalIndex, cache, name) {
2624      var cached = this[cache];
2625
2626      // Unfortunately we have to run through all children to reset their
2627      // parent property.
2628      var objects = this.jCal[jCalIndex];
2629      var i = objects.length - 1;
2630
2631      // descending search required because splice
2632      // is used and will effect the indices.
2633      for (; i >= 0; i--) {
2634        if (!name || objects[i][NAME_INDEX] === name) {
2635          this._removeObjectByIndex(jCalIndex, cached, i);
2636        }
2637      }
2638    },
2639
2640    /**
2641     * Adds a single sub component.
2642     *
2643     * @param {ICAL.Component} component        The component to add
2644     * @return {ICAL.Component}                 The passed in component
2645     */
2646    addSubcomponent: function(component) {
2647      if (!this._components) {
2648        this._components = [];
2649        this._hydratedComponentCount = 0;
2650      }
2651
2652      if (component.parent) {
2653        component.parent.removeSubcomponent(component);
2654      }
2655
2656      var idx = this.jCal[COMPONENT_INDEX].push(component.jCal);
2657      this._components[idx - 1] = component;
2658      this._hydratedComponentCount++;
2659      component.parent = this;
2660      return component;
2661    },
2662
2663    /**
2664     * Removes a single component by name or the instance of a specific
2665     * component.
2666     *
2667     * @param {ICAL.Component|String} nameOrComp    Name of component, or component
2668     * @return {Boolean}                            True when comp is removed
2669     */
2670    removeSubcomponent: function(nameOrComp) {
2671      var removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp);
2672      if (removed) {
2673        this._hydratedComponentCount--;
2674      }
2675      return removed;
2676    },
2677
2678    /**
2679     * Removes all components or (if given) all components by a particular
2680     * name.
2681     *
2682     * @param {String=} name            Lowercase component name
2683     */
2684    removeAllSubcomponents: function(name) {
2685      var removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name);
2686      this._hydratedComponentCount = 0;
2687      return removed;
2688    },
2689
2690    /**
2691     * Adds an {@link ICAL.Property} to the component.
2692     *
2693     * @param {ICAL.Property} property      The property to add
2694     * @return {ICAL.Property}              The passed in property
2695     */
2696    addProperty: function(property) {
2697      if (!(property instanceof ICAL.Property)) {
2698        throw new TypeError('must instance of ICAL.Property');
2699      }
2700
2701      if (!this._properties) {
2702        this._properties = [];
2703        this._hydratedPropertyCount = 0;
2704      }
2705
2706      if (property.parent) {
2707        property.parent.removeProperty(property);
2708      }
2709
2710      var idx = this.jCal[PROPERTY_INDEX].push(property.jCal);
2711      this._properties[idx - 1] = property;
2712      this._hydratedPropertyCount++;
2713      property.parent = this;
2714      return property;
2715    },
2716
2717    /**
2718     * Helper method to add a property with a value to the component.
2719     *
2720     * @param {String}               name         Property name to add
2721     * @param {String|Number|Object} value        Property value
2722     * @return {ICAL.Property}                    The created property
2723     */
2724    addPropertyWithValue: function(name, value) {
2725      var prop = new ICAL.Property(name);
2726      prop.setValue(value);
2727
2728      this.addProperty(prop);
2729
2730      return prop;
2731    },
2732
2733    /**
2734     * Helper method that will update or create a property of the given name
2735     * and sets its value. If multiple properties with the given name exist,
2736     * only the first is updated.
2737     *
2738     * @param {String}               name         Property name to update
2739     * @param {String|Number|Object} value        Property value
2740     * @return {ICAL.Property}                    The created property
2741     */
2742    updatePropertyWithValue: function(name, value) {
2743      var prop = this.getFirstProperty(name);
2744
2745      if (prop) {
2746        prop.setValue(value);
2747      } else {
2748        prop = this.addPropertyWithValue(name, value);
2749      }
2750
2751      return prop;
2752    },
2753
2754    /**
2755     * Removes a single property by name or the instance of the specific
2756     * property.
2757     *
2758     * @param {String|ICAL.Property} nameOrProp     Property name or instance to remove
2759     * @return {Boolean}                            True, when deleted
2760     */
2761    removeProperty: function(nameOrProp) {
2762      var removed = this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp);
2763      if (removed) {
2764        this._hydratedPropertyCount--;
2765      }
2766      return removed;
2767    },
2768
2769    /**
2770     * Removes all properties associated with this component, optionally
2771     * filtered by name.
2772     *
2773     * @param {String=} name        Lowercase property name
2774     * @return {Boolean}            True, when deleted
2775     */
2776    removeAllProperties: function(name) {
2777      var removed = this._removeAllObjects(PROPERTY_INDEX, '_properties', name);
2778      this._hydratedPropertyCount = 0;
2779      return removed;
2780    },
2781
2782    /**
2783     * Returns the Object representation of this component. The returned object
2784     * is a live jCal object and should be cloned if modified.
2785     * @return {Object}
2786     */
2787    toJSON: function() {
2788      return this.jCal;
2789    },
2790
2791    /**
2792     * The string representation of this component.
2793     * @return {String}
2794     */
2795    toString: function() {
2796      return ICAL.stringify.component(
2797        this.jCal, this._designSet
2798      );
2799    }
2800  };
2801
2802  /**
2803   * Create an {@link ICAL.Component} by parsing the passed iCalendar string.
2804   *
2805   * @param {String} str        The iCalendar string to parse
2806   */
2807  Component.fromString = function(str) {
2808    return new Component(ICAL.parse.component(str));
2809  };
2810
2811  return Component;
2812}());
2813/* This Source Code Form is subject to the terms of the Mozilla Public
2814 * License, v. 2.0. If a copy of the MPL was not distributed with this
2815 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
2816 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
2817
2818
2819/**
2820 * This symbol is further described later on
2821 * @ignore
2822 */
2823ICAL.Property = (function() {
2824  'use strict';
2825
2826  var NAME_INDEX = 0;
2827  var PROP_INDEX = 1;
2828  var TYPE_INDEX = 2;
2829  var VALUE_INDEX = 3;
2830
2831  var design = ICAL.design;
2832
2833  /**
2834   * @classdesc
2835   * Provides a layer on top of the raw jCal object for manipulating a single
2836   * property, with its parameters and value.
2837   *
2838   * @description
2839   * Its important to note that mutations done in the wrapper
2840   * directly mutate the jCal object used to initialize.
2841   *
2842   * Can also be used to create new properties by passing
2843   * the name of the property (as a String).
2844   *
2845   * @class
2846   * @alias ICAL.Property
2847   * @param {Array|String} jCal         Raw jCal representation OR
2848   *  the new name of the property
2849   *
2850   * @param {ICAL.Component=} parent    Parent component
2851   */
2852  function Property(jCal, parent) {
2853    this._parent = parent || null;
2854
2855    if (typeof(jCal) === 'string') {
2856      // We are creating the property by name and need to detect the type
2857      this.jCal = [jCal, {}, design.defaultType];
2858      this.jCal[TYPE_INDEX] = this.getDefaultType();
2859    } else {
2860      this.jCal = jCal;
2861    }
2862    this._updateType();
2863  }
2864
2865  Property.prototype = {
2866
2867    /**
2868     * The value type for this property
2869     * @readonly
2870     * @type {String}
2871     */
2872    get type() {
2873      return this.jCal[TYPE_INDEX];
2874    },
2875
2876    /**
2877     * The name of this property, in lowercase.
2878     * @readonly
2879     * @type {String}
2880     */
2881    get name() {
2882      return this.jCal[NAME_INDEX];
2883    },
2884
2885    /**
2886     * The parent component for this property.
2887     * @type {ICAL.Component}
2888     */
2889    get parent() {
2890      return this._parent;
2891    },
2892
2893    set parent(p) {
2894      // Before setting the parent, check if the design set has changed. If it
2895      // has, we later need to update the type if it was unknown before.
2896      var designSetChanged = !this._parent || (p && p._designSet != this._parent._designSet);
2897
2898      this._parent = p;
2899
2900      if (this.type == design.defaultType && designSetChanged) {
2901        this.jCal[TYPE_INDEX] = this.getDefaultType();
2902        this._updateType();
2903      }
2904
2905      return p;
2906    },
2907
2908    /**
2909     * The design set for this property, e.g. icalendar vs vcard
2910     *
2911     * @type {ICAL.design.designSet}
2912     * @private
2913     */
2914    get _designSet() {
2915      return this.parent ? this.parent._designSet : design.defaultSet;
2916    },
2917
2918    /**
2919     * Updates the type metadata from the current jCal type and design set.
2920     *
2921     * @private
2922     */
2923    _updateType: function() {
2924      var designSet = this._designSet;
2925
2926      if (this.type in designSet.value) {
2927        var designType = designSet.value[this.type];
2928
2929        if ('decorate' in designSet.value[this.type]) {
2930          this.isDecorated = true;
2931        } else {
2932          this.isDecorated = false;
2933        }
2934
2935        if (this.name in designSet.property) {
2936          this.isMultiValue = ('multiValue' in designSet.property[this.name]);
2937          this.isStructuredValue = ('structuredValue' in designSet.property[this.name]);
2938        }
2939      }
2940    },
2941
2942    /**
2943     * Hydrate a single value. The act of hydrating means turning the raw jCal
2944     * value into a potentially wrapped object, for example {@link ICAL.Time}.
2945     *
2946     * @private
2947     * @param {Number} index        The index of the value to hydrate
2948     * @return {Object}             The decorated value.
2949     */
2950    _hydrateValue: function(index) {
2951      if (this._values && this._values[index]) {
2952        return this._values[index];
2953      }
2954
2955      // for the case where there is no value.
2956      if (this.jCal.length <= (VALUE_INDEX + index)) {
2957        return null;
2958      }
2959
2960      if (this.isDecorated) {
2961        if (!this._values) {
2962          this._values = [];
2963        }
2964        return (this._values[index] = this._decorate(
2965          this.jCal[VALUE_INDEX + index]
2966        ));
2967      } else {
2968        return this.jCal[VALUE_INDEX + index];
2969      }
2970    },
2971
2972    /**
2973     * Decorate a single value, returning its wrapped object. This is used by
2974     * the hydrate function to actually wrap the value.
2975     *
2976     * @private
2977     * @param {?} value         The value to decorate
2978     * @return {Object}         The decorated value
2979     */
2980    _decorate: function(value) {
2981      return this._designSet.value[this.type].decorate(value, this);
2982    },
2983
2984    /**
2985     * Undecorate a single value, returning its raw jCal data.
2986     *
2987     * @private
2988     * @param {Object} value         The value to undecorate
2989     * @return {?}                   The undecorated value
2990     */
2991    _undecorate: function(value) {
2992      return this._designSet.value[this.type].undecorate(value, this);
2993    },
2994
2995    /**
2996     * Sets the value at the given index while also hydrating it. The passed
2997     * value can either be a decorated or undecorated value.
2998     *
2999     * @private
3000     * @param {?} value             The value to set
3001     * @param {Number} index        The index to set it at
3002     */
3003    _setDecoratedValue: function(value, index) {
3004      if (!this._values) {
3005        this._values = [];
3006      }
3007
3008      if (typeof(value) === 'object' && 'icaltype' in value) {
3009        // decorated value
3010        this.jCal[VALUE_INDEX + index] = this._undecorate(value);
3011        this._values[index] = value;
3012      } else {
3013        // undecorated value
3014        this.jCal[VALUE_INDEX + index] = value;
3015        this._values[index] = this._decorate(value);
3016      }
3017    },
3018
3019    /**
3020     * Gets a parameter on the property.
3021     *
3022     * @param {String}        name   Property name (lowercase)
3023     * @return {Array|String}        Property value
3024     */
3025    getParameter: function(name) {
3026      if (name in this.jCal[PROP_INDEX]) {
3027        return this.jCal[PROP_INDEX][name];
3028      } else {
3029        return undefined;
3030      }
3031    },
3032
3033    /**
3034     * Gets first parameter on the property.
3035     *
3036     * @param {String}        name   Property name (lowercase)
3037     * @return {String}        Property value
3038     */
3039    getFirstParameter: function(name) {
3040      var parameters = this.getParameter(name);
3041
3042      if (Array.isArray(parameters)) {
3043        return parameters[0];
3044      }
3045
3046      return parameters;
3047    },
3048
3049    /**
3050     * Sets a parameter on the property.
3051     *
3052     * @param {String}       name     The parameter name
3053     * @param {Array|String} value    The parameter value
3054     */
3055    setParameter: function(name, value) {
3056      var lcname = name.toLowerCase();
3057      if (typeof value === "string" &&
3058          lcname in this._designSet.param &&
3059          'multiValue' in this._designSet.param[lcname]) {
3060          value = [value];
3061      }
3062      this.jCal[PROP_INDEX][name] = value;
3063    },
3064
3065    /**
3066     * Removes a parameter
3067     *
3068     * @param {String} name     The parameter name
3069     */
3070    removeParameter: function(name) {
3071      delete this.jCal[PROP_INDEX][name];
3072    },
3073
3074    /**
3075     * Get the default type based on this property's name.
3076     *
3077     * @return {String}     The default type for this property
3078     */
3079    getDefaultType: function() {
3080      var name = this.jCal[NAME_INDEX];
3081      var designSet = this._designSet;
3082
3083      if (name in designSet.property) {
3084        var details = designSet.property[name];
3085        if ('defaultType' in details) {
3086          return details.defaultType;
3087        }
3088      }
3089      return design.defaultType;
3090    },
3091
3092    /**
3093     * Sets type of property and clears out any existing values of the current
3094     * type.
3095     *
3096     * @param {String} type     New iCAL type (see design.*.values)
3097     */
3098    resetType: function(type) {
3099      this.removeAllValues();
3100      this.jCal[TYPE_INDEX] = type;
3101      this._updateType();
3102    },
3103
3104    /**
3105     * Finds the first property value.
3106     *
3107     * @return {String}         First property value
3108     */
3109    getFirstValue: function() {
3110      return this._hydrateValue(0);
3111    },
3112
3113    /**
3114     * Gets all values on the property.
3115     *
3116     * NOTE: this creates an array during each call.
3117     *
3118     * @return {Array}          List of values
3119     */
3120    getValues: function() {
3121      var len = this.jCal.length - VALUE_INDEX;
3122
3123      if (len < 1) {
3124        // its possible for a property to have no value.
3125        return [];
3126      }
3127
3128      var i = 0;
3129      var result = [];
3130
3131      for (; i < len; i++) {
3132        result[i] = this._hydrateValue(i);
3133      }
3134
3135      return result;
3136    },
3137
3138    /**
3139     * Removes all values from this property
3140     */
3141    removeAllValues: function() {
3142      if (this._values) {
3143        this._values.length = 0;
3144      }
3145      this.jCal.length = 3;
3146    },
3147
3148    /**
3149     * Sets the values of the property.  Will overwrite the existing values.
3150     * This can only be used for multi-value properties.
3151     *
3152     * @param {Array} values    An array of values
3153     */
3154    setValues: function(values) {
3155      if (!this.isMultiValue) {
3156        throw new Error(
3157          this.name + ': does not not support mulitValue.\n' +
3158          'override isMultiValue'
3159        );
3160      }
3161
3162      var len = values.length;
3163      var i = 0;
3164      this.removeAllValues();
3165
3166      if (len > 0 &&
3167          typeof(values[0]) === 'object' &&
3168          'icaltype' in values[0]) {
3169        this.resetType(values[0].icaltype);
3170      }
3171
3172      if (this.isDecorated) {
3173        for (; i < len; i++) {
3174          this._setDecoratedValue(values[i], i);
3175        }
3176      } else {
3177        for (; i < len; i++) {
3178          this.jCal[VALUE_INDEX + i] = values[i];
3179        }
3180      }
3181    },
3182
3183    /**
3184     * Sets the current value of the property. If this is a multi-value
3185     * property, all other values will be removed.
3186     *
3187     * @param {String|Object} value     New property value.
3188     */
3189    setValue: function(value) {
3190      this.removeAllValues();
3191      if (typeof(value) === 'object' && 'icaltype' in value) {
3192        this.resetType(value.icaltype);
3193      }
3194
3195      if (this.isDecorated) {
3196        this._setDecoratedValue(value, 0);
3197      } else {
3198        this.jCal[VALUE_INDEX] = value;
3199      }
3200    },
3201
3202    /**
3203     * Returns the Object representation of this component. The returned object
3204     * is a live jCal object and should be cloned if modified.
3205     * @return {Object}
3206     */
3207    toJSON: function() {
3208      return this.jCal;
3209    },
3210
3211    /**
3212     * The string representation of this component.
3213     * @return {String}
3214     */
3215    toICALString: function() {
3216      return ICAL.stringify.property(
3217        this.jCal, this._designSet, true
3218      );
3219    }
3220  };
3221
3222  /**
3223   * Create an {@link ICAL.Property} by parsing the passed iCalendar string.
3224   *
3225   * @param {String} str                        The iCalendar string to parse
3226   * @param {ICAL.design.designSet=} designSet  The design data to use for this property
3227   * @return {ICAL.Property}                    The created iCalendar property
3228   */
3229  Property.fromString = function(str, designSet) {
3230    return new Property(ICAL.parse.property(str, designSet));
3231  };
3232
3233  return Property;
3234}());
3235/* This Source Code Form is subject to the terms of the Mozilla Public
3236 * License, v. 2.0. If a copy of the MPL was not distributed with this
3237 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
3238 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
3239
3240
3241/**
3242 * This symbol is further described later on
3243 * @ignore
3244 */
3245ICAL.UtcOffset = (function() {
3246
3247  /**
3248   * @classdesc
3249   * This class represents the "duration" value type, with various calculation
3250   * and manipulation methods.
3251   *
3252   * @class
3253   * @alias ICAL.UtcOffset
3254   * @param {Object} aData          An object with members of the utc offset
3255   * @param {Number=} aData.hours   The hours for the utc offset
3256   * @param {Number=} aData.minutes The minutes in the utc offset
3257   * @param {Number=} aData.factor  The factor for the utc-offset, either -1 or 1
3258   */
3259  function UtcOffset(aData) {
3260    this.fromData(aData);
3261  }
3262
3263  UtcOffset.prototype = {
3264
3265    /**
3266     * The hours in the utc-offset
3267     * @type {Number}
3268     */
3269    hours: 0,
3270
3271    /**
3272     * The minutes in the utc-offset
3273     * @type {Number}
3274     */
3275    minutes: 0,
3276
3277    /**
3278     * The sign of the utc offset, 1 for positive offset, -1 for negative
3279     * offsets.
3280     * @type {Number}
3281     */
3282    factor: 1,
3283
3284    /**
3285     * The type name, to be used in the jCal object.
3286     * @constant
3287     * @type {String}
3288     * @default "utc-offset"
3289     */
3290    icaltype: "utc-offset",
3291
3292    /**
3293     * Returns a clone of the utc offset object.
3294     *
3295     * @return {ICAL.UtcOffset}     The cloned object
3296     */
3297    clone: function() {
3298      return ICAL.UtcOffset.fromSeconds(this.toSeconds());
3299    },
3300
3301    /**
3302     * Sets up the current instance using members from the passed data object.
3303     *
3304     * @param {Object} aData          An object with members of the utc offset
3305     * @param {Number=} aData.hours   The hours for the utc offset
3306     * @param {Number=} aData.minutes The minutes in the utc offset
3307     * @param {Number=} aData.factor  The factor for the utc-offset, either -1 or 1
3308     */
3309    fromData: function(aData) {
3310      if (aData) {
3311        for (var key in aData) {
3312          /* istanbul ignore else */
3313          if (aData.hasOwnProperty(key)) {
3314            this[key] = aData[key];
3315          }
3316        }
3317      }
3318      this._normalize();
3319    },
3320
3321    /**
3322     * Sets up the current instance from the given seconds value. The seconds
3323     * value is truncated to the minute. Offsets are wrapped when the world
3324     * ends, the hour after UTC+14:00 is UTC-12:00.
3325     *
3326     * @param {Number} aSeconds         The seconds to convert into an offset
3327     */
3328    fromSeconds: function(aSeconds) {
3329      var secs = Math.abs(aSeconds);
3330
3331      this.factor = aSeconds < 0 ? -1 : 1;
3332      this.hours = ICAL.helpers.trunc(secs / 3600);
3333
3334      secs -= (this.hours * 3600);
3335      this.minutes = ICAL.helpers.trunc(secs / 60);
3336      return this;
3337    },
3338
3339    /**
3340     * Convert the current offset to a value in seconds
3341     *
3342     * @return {Number}                 The offset in seconds
3343     */
3344    toSeconds: function() {
3345      return this.factor * (60 * this.minutes + 3600 * this.hours);
3346    },
3347
3348    /**
3349     * Compare this utc offset with another one.
3350     *
3351     * @param {ICAL.UtcOffset} other        The other offset to compare with
3352     * @return {Number}                     -1, 0 or 1 for less/equal/greater
3353     */
3354    compare: function icaltime_compare(other) {
3355      var a = this.toSeconds();
3356      var b = other.toSeconds();
3357      return (a > b) - (b > a);
3358    },
3359
3360    _normalize: function() {
3361      // Range: 97200 seconds (with 1 hour inbetween)
3362      var secs = this.toSeconds();
3363      var factor = this.factor;
3364      while (secs < -43200) { // = UTC-12:00
3365        secs += 97200;
3366      }
3367      while (secs > 50400) { // = UTC+14:00
3368        secs -= 97200;
3369      }
3370
3371      this.fromSeconds(secs);
3372
3373      // Avoid changing the factor when on zero seconds
3374      if (secs == 0) {
3375        this.factor = factor;
3376      }
3377    },
3378
3379    /**
3380     * The iCalendar string representation of this utc-offset.
3381     * @return {String}
3382     */
3383    toICALString: function() {
3384      return ICAL.design.icalendar.value['utc-offset'].toICAL(this.toString());
3385    },
3386
3387    /**
3388     * The string representation of this utc-offset.
3389     * @return {String}
3390     */
3391    toString: function toString() {
3392      return (this.factor == 1 ? "+" : "-") +
3393              ICAL.helpers.pad2(this.hours) + ':' +
3394              ICAL.helpers.pad2(this.minutes);
3395    }
3396  };
3397
3398  /**
3399   * Creates a new {@link ICAL.UtcOffset} instance from the passed string.
3400   *
3401   * @param {String} aString    The string to parse
3402   * @return {ICAL.Duration}    The created utc-offset instance
3403   */
3404  UtcOffset.fromString = function(aString) {
3405    // -05:00
3406    var options = {};
3407    //TODO: support seconds per rfc5545 ?
3408    options.factor = (aString[0] === '+') ? 1 : -1;
3409    options.hours = ICAL.helpers.strictParseInt(aString.substr(1, 2));
3410    options.minutes = ICAL.helpers.strictParseInt(aString.substr(4, 2));
3411
3412    return new ICAL.UtcOffset(options);
3413  };
3414
3415  /**
3416   * Creates a new {@link ICAL.UtcOffset} instance from the passed seconds
3417   * value.
3418   *
3419   * @param {Number} aSeconds       The number of seconds to convert
3420   */
3421  UtcOffset.fromSeconds = function(aSeconds) {
3422    var instance = new UtcOffset();
3423    instance.fromSeconds(aSeconds);
3424    return instance;
3425  };
3426
3427  return UtcOffset;
3428}());
3429/* This Source Code Form is subject to the terms of the Mozilla Public
3430 * License, v. 2.0. If a copy of the MPL was not distributed with this
3431 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
3432 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
3433
3434
3435/**
3436 * This symbol is further described later on
3437 * @ignore
3438 */
3439ICAL.Binary = (function() {
3440
3441  /**
3442   * @classdesc
3443   * Represents the BINARY value type, which contains extra methods for
3444   * encoding and decoding.
3445   *
3446   * @class
3447   * @alias ICAL.Binary
3448   * @param {String} aValue     The binary data for this value
3449   */
3450  function Binary(aValue) {
3451    this.value = aValue;
3452  }
3453
3454  Binary.prototype = {
3455    /**
3456     * The type name, to be used in the jCal object.
3457     * @default "binary"
3458     * @constant
3459     */
3460    icaltype: "binary",
3461
3462    /**
3463     * Base64 decode the current value
3464     *
3465     * @return {String}         The base64-decoded value
3466     */
3467    decodeValue: function decodeValue() {
3468      return this._b64_decode(this.value);
3469    },
3470
3471    /**
3472     * Encodes the passed parameter with base64 and sets the internal
3473     * value to the result.
3474     *
3475     * @param {String} aValue      The raw binary value to encode
3476     */
3477    setEncodedValue: function setEncodedValue(aValue) {
3478      this.value = this._b64_encode(aValue);
3479    },
3480
3481    _b64_encode: function base64_encode(data) {
3482      // http://kevin.vanzonneveld.net
3483      // +   original by: Tyler Akins (http://rumkin.com)
3484      // +   improved by: Bayron Guevara
3485      // +   improved by: Thunder.m
3486      // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
3487      // +   bugfixed by: Pellentesque Malesuada
3488      // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
3489      // +   improved by: Rafał Kukawski (http://kukawski.pl)
3490      // *     example 1: base64_encode('Kevin van Zonneveld');
3491      // *     returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
3492      // mozilla has this native
3493      // - but breaks in 2.0.0.12!
3494      //if (typeof this.window['atob'] == 'function') {
3495      //    return atob(data);
3496      //}
3497      var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
3498                "abcdefghijklmnopqrstuvwxyz0123456789+/=";
3499      var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
3500        ac = 0,
3501        enc = "",
3502        tmp_arr = [];
3503
3504      if (!data) {
3505        return data;
3506      }
3507
3508      do { // pack three octets into four hexets
3509        o1 = data.charCodeAt(i++);
3510        o2 = data.charCodeAt(i++);
3511        o3 = data.charCodeAt(i++);
3512
3513        bits = o1 << 16 | o2 << 8 | o3;
3514
3515        h1 = bits >> 18 & 0x3f;
3516        h2 = bits >> 12 & 0x3f;
3517        h3 = bits >> 6 & 0x3f;
3518        h4 = bits & 0x3f;
3519
3520        // use hexets to index into b64, and append result to encoded string
3521        tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
3522      } while (i < data.length);
3523
3524      enc = tmp_arr.join('');
3525
3526      var r = data.length % 3;
3527
3528      return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3);
3529
3530    },
3531
3532    _b64_decode: function base64_decode(data) {
3533      // http://kevin.vanzonneveld.net
3534      // +   original by: Tyler Akins (http://rumkin.com)
3535      // +   improved by: Thunder.m
3536      // +      input by: Aman Gupta
3537      // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
3538      // +   bugfixed by: Onno Marsman
3539      // +   bugfixed by: Pellentesque Malesuada
3540      // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
3541      // +      input by: Brett Zamir (http://brett-zamir.me)
3542      // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
3543      // *     example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==');
3544      // *     returns 1: 'Kevin van Zonneveld'
3545      // mozilla has this native
3546      // - but breaks in 2.0.0.12!
3547      //if (typeof this.window['btoa'] == 'function') {
3548      //    return btoa(data);
3549      //}
3550      var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
3551                "abcdefghijklmnopqrstuvwxyz0123456789+/=";
3552      var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
3553        ac = 0,
3554        dec = "",
3555        tmp_arr = [];
3556
3557      if (!data) {
3558        return data;
3559      }
3560
3561      data += '';
3562
3563      do { // unpack four hexets into three octets using index points in b64
3564        h1 = b64.indexOf(data.charAt(i++));
3565        h2 = b64.indexOf(data.charAt(i++));
3566        h3 = b64.indexOf(data.charAt(i++));
3567        h4 = b64.indexOf(data.charAt(i++));
3568
3569        bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
3570
3571        o1 = bits >> 16 & 0xff;
3572        o2 = bits >> 8 & 0xff;
3573        o3 = bits & 0xff;
3574
3575        if (h3 == 64) {
3576          tmp_arr[ac++] = String.fromCharCode(o1);
3577        } else if (h4 == 64) {
3578          tmp_arr[ac++] = String.fromCharCode(o1, o2);
3579        } else {
3580          tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
3581        }
3582      } while (i < data.length);
3583
3584      dec = tmp_arr.join('');
3585
3586      return dec;
3587    },
3588
3589    /**
3590     * The string representation of this value
3591     * @return {String}
3592     */
3593    toString: function() {
3594      return this.value;
3595    }
3596  };
3597
3598  /**
3599   * Creates a binary value from the given string.
3600   *
3601   * @param {String} aString        The binary value string
3602   * @return {ICAL.Binary}          The binary value instance
3603   */
3604  Binary.fromString = function(aString) {
3605    return new Binary(aString);
3606  };
3607
3608  return Binary;
3609}());
3610/* This Source Code Form is subject to the terms of the Mozilla Public
3611 * License, v. 2.0. If a copy of the MPL was not distributed with this
3612 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
3613 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
3614
3615
3616
3617(function() {
3618  /**
3619   * @classdesc
3620   * This class represents the "period" value type, with various calculation
3621   * and manipulation methods.
3622   *
3623   * @description
3624   * The passed data object cannot contain both and end date and a duration.
3625   *
3626   * @class
3627   * @param {Object} aData                  An object with members of the period
3628   * @param {ICAL.Time=} aData.start        The start of the period
3629   * @param {ICAL.Time=} aData.end          The end of the period
3630   * @param {ICAL.Duration=} aData.duration The duration of the period
3631   */
3632  ICAL.Period = function icalperiod(aData) {
3633    this.wrappedJSObject = this;
3634
3635    if (aData && 'start' in aData) {
3636      if (aData.start && !(aData.start instanceof ICAL.Time)) {
3637        throw new TypeError('.start must be an instance of ICAL.Time');
3638      }
3639      this.start = aData.start;
3640    }
3641
3642    if (aData && aData.end && aData.duration) {
3643      throw new Error('cannot accept both end and duration');
3644    }
3645
3646    if (aData && 'end' in aData) {
3647      if (aData.end && !(aData.end instanceof ICAL.Time)) {
3648        throw new TypeError('.end must be an instance of ICAL.Time');
3649      }
3650      this.end = aData.end;
3651    }
3652
3653    if (aData && 'duration' in aData) {
3654      if (aData.duration && !(aData.duration instanceof ICAL.Duration)) {
3655        throw new TypeError('.duration must be an instance of ICAL.Duration');
3656      }
3657      this.duration = aData.duration;
3658    }
3659  };
3660
3661  ICAL.Period.prototype = {
3662
3663    /**
3664     * The start of the period
3665     * @type {ICAL.Time}
3666     */
3667    start: null,
3668
3669    /**
3670     * The end of the period
3671     * @type {ICAL.Time}
3672     */
3673    end: null,
3674
3675    /**
3676     * The duration of the period
3677     * @type {ICAL.Duration}
3678     */
3679    duration: null,
3680
3681    /**
3682     * The class identifier.
3683     * @constant
3684     * @type {String}
3685     * @default "icalperiod"
3686     */
3687    icalclass: "icalperiod",
3688
3689    /**
3690     * The type name, to be used in the jCal object.
3691     * @constant
3692     * @type {String}
3693     * @default "period"
3694     */
3695    icaltype: "period",
3696
3697    /**
3698     * Returns a clone of the duration object.
3699     *
3700     * @return {ICAL.Period}      The cloned object
3701     */
3702    clone: function() {
3703      return ICAL.Period.fromData({
3704        start: this.start ? this.start.clone() : null,
3705        end: this.end ? this.end.clone() : null,
3706        duration: this.duration ? this.duration.clone() : null
3707      });
3708    },
3709
3710    /**
3711     * Calculates the duration of the period, either directly or by subtracting
3712     * start from end date.
3713     *
3714     * @return {ICAL.Duration}      The calculated duration
3715     */
3716    getDuration: function duration() {
3717      if (this.duration) {
3718        return this.duration;
3719      } else {
3720        return this.end.subtractDate(this.start);
3721      }
3722    },
3723
3724    /**
3725     * Calculates the end date of the period, either directly or by adding
3726     * duration to start date.
3727     *
3728     * @return {ICAL.Time}          The calculated end date
3729     */
3730    getEnd: function() {
3731      if (this.end) {
3732        return this.end;
3733      } else {
3734        var end = this.start.clone();
3735        end.addDuration(this.duration);
3736        return end;
3737      }
3738    },
3739
3740    /**
3741     * The string representation of this period.
3742     * @return {String}
3743     */
3744    toString: function toString() {
3745      return this.start + "/" + (this.end || this.duration);
3746    },
3747
3748    /**
3749     * The jCal representation of this period type.
3750     * @return {Object}
3751     */
3752    toJSON: function() {
3753      return [this.start.toString(), (this.end || this.duration).toString()];
3754    },
3755
3756    /**
3757     * The iCalendar string representation of this period.
3758     * @return {String}
3759     */
3760    toICALString: function() {
3761      return this.start.toICALString() + "/" +
3762             (this.end || this.duration).toICALString();
3763    }
3764  };
3765
3766  /**
3767   * Creates a new {@link ICAL.Period} instance from the passed string.
3768   *
3769   * @param {String} str            The string to parse
3770   * @param {ICAL.Property} prop    The property this period will be on
3771   * @return {ICAL.Period}          The created period instance
3772   */
3773  ICAL.Period.fromString = function fromString(str, prop) {
3774    var parts = str.split('/');
3775
3776    if (parts.length !== 2) {
3777      throw new Error(
3778        'Invalid string value: "' + str + '" must contain a "/" char.'
3779      );
3780    }
3781
3782    var options = {
3783      start: ICAL.Time.fromDateTimeString(parts[0], prop)
3784    };
3785
3786    var end = parts[1];
3787
3788    if (ICAL.Duration.isValueString(end)) {
3789      options.duration = ICAL.Duration.fromString(end);
3790    } else {
3791      options.end = ICAL.Time.fromDateTimeString(end, prop);
3792    }
3793
3794    return new ICAL.Period(options);
3795  };
3796
3797  /**
3798   * Creates a new {@link ICAL.Period} instance from the given data object.
3799   * The passed data object cannot contain both and end date and a duration.
3800   *
3801   * @param {Object} aData                  An object with members of the period
3802   * @param {ICAL.Time=} aData.start        The start of the period
3803   * @param {ICAL.Time=} aData.end          The end of the period
3804   * @param {ICAL.Duration=} aData.duration The duration of the period
3805   * @return {ICAL.Period}                  The period instance
3806   */
3807  ICAL.Period.fromData = function fromData(aData) {
3808    return new ICAL.Period(aData);
3809  };
3810
3811  /**
3812   * Returns a new period instance from the given jCal data array. The first
3813   * member is always the start date string, the second member is either a
3814   * duration or end date string.
3815   *
3816   * @param {Array<String,String>} aData    The jCal data array
3817   * @param {ICAL.Property} aProp           The property this jCal data is on
3818   * @param {Boolean} aLenient              If true, data value can be both date and date-time
3819   * @return {ICAL.Period}                  The period instance
3820   */
3821  ICAL.Period.fromJSON = function(aData, aProp, aLenient) {
3822    function fromDateOrDateTimeString(aValue, aProp) {
3823      if (aLenient) {
3824        return ICAL.Time.fromString(aValue, aProp);
3825      } else {
3826        return ICAL.Time.fromDateTimeString(aValue, aProp);
3827      }
3828    }
3829
3830    if (ICAL.Duration.isValueString(aData[1])) {
3831      return ICAL.Period.fromData({
3832        start: fromDateOrDateTimeString(aData[0], aProp),
3833        duration: ICAL.Duration.fromString(aData[1])
3834      });
3835    } else {
3836      return ICAL.Period.fromData({
3837        start: fromDateOrDateTimeString(aData[0], aProp),
3838        end: fromDateOrDateTimeString(aData[1], aProp)
3839      });
3840    }
3841  };
3842})();
3843/* This Source Code Form is subject to the terms of the Mozilla Public
3844 * License, v. 2.0. If a copy of the MPL was not distributed with this
3845 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
3846 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
3847
3848
3849
3850(function() {
3851  var DURATION_LETTERS = /([PDWHMTS]{1,1})/;
3852
3853  /**
3854   * @classdesc
3855   * This class represents the "duration" value type, with various calculation
3856   * and manipulation methods.
3857   *
3858   * @class
3859   * @alias ICAL.Duration
3860   * @param {Object} data               An object with members of the duration
3861   * @param {Number} data.weeks         Duration in weeks
3862   * @param {Number} data.days          Duration in days
3863   * @param {Number} data.hours         Duration in hours
3864   * @param {Number} data.minutes       Duration in minutes
3865   * @param {Number} data.seconds       Duration in seconds
3866   * @param {Boolean} data.isNegative   If true, the duration is negative
3867   */
3868  ICAL.Duration = function icalduration(data) {
3869    this.wrappedJSObject = this;
3870    this.fromData(data);
3871  };
3872
3873  ICAL.Duration.prototype = {
3874    /**
3875     * The weeks in this duration
3876     * @type {Number}
3877     * @default 0
3878     */
3879    weeks: 0,
3880
3881    /**
3882     * The days in this duration
3883     * @type {Number}
3884     * @default 0
3885     */
3886    days: 0,
3887
3888    /**
3889     * The days in this duration
3890     * @type {Number}
3891     * @default 0
3892     */
3893    hours: 0,
3894
3895    /**
3896     * The minutes in this duration
3897     * @type {Number}
3898     * @default 0
3899     */
3900    minutes: 0,
3901
3902    /**
3903     * The seconds in this duration
3904     * @type {Number}
3905     * @default 0
3906     */
3907    seconds: 0,
3908
3909    /**
3910     * The seconds in this duration
3911     * @type {Boolean}
3912     * @default false
3913     */
3914    isNegative: false,
3915
3916    /**
3917     * The class identifier.
3918     * @constant
3919     * @type {String}
3920     * @default "icalduration"
3921     */
3922    icalclass: "icalduration",
3923
3924    /**
3925     * The type name, to be used in the jCal object.
3926     * @constant
3927     * @type {String}
3928     * @default "duration"
3929     */
3930    icaltype: "duration",
3931
3932    /**
3933     * Returns a clone of the duration object.
3934     *
3935     * @return {ICAL.Duration}      The cloned object
3936     */
3937    clone: function clone() {
3938      return ICAL.Duration.fromData(this);
3939    },
3940
3941    /**
3942     * The duration value expressed as a number of seconds.
3943     *
3944     * @return {Number}             The duration value in seconds
3945     */
3946    toSeconds: function toSeconds() {
3947      var seconds = this.seconds + 60 * this.minutes + 3600 * this.hours +
3948                    86400 * this.days + 7 * 86400 * this.weeks;
3949      return (this.isNegative ? -seconds : seconds);
3950    },
3951
3952    /**
3953     * Reads the passed seconds value into this duration object. Afterwards,
3954     * members like {@link ICAL.Duration#days days} and {@link ICAL.Duration#weeks weeks} will be set up
3955     * accordingly.
3956     *
3957     * @param {Number} aSeconds     The duration value in seconds
3958     * @return {ICAL.Duration}      Returns this instance
3959     */
3960    fromSeconds: function fromSeconds(aSeconds) {
3961      var secs = Math.abs(aSeconds);
3962
3963      this.isNegative = (aSeconds < 0);
3964      this.days = ICAL.helpers.trunc(secs / 86400);
3965
3966      // If we have a flat number of weeks, use them.
3967      if (this.days % 7 == 0) {
3968        this.weeks = this.days / 7;
3969        this.days = 0;
3970      } else {
3971        this.weeks = 0;
3972      }
3973
3974      secs -= (this.days + 7 * this.weeks) * 86400;
3975
3976      this.hours = ICAL.helpers.trunc(secs / 3600);
3977      secs -= this.hours * 3600;
3978
3979      this.minutes = ICAL.helpers.trunc(secs / 60);
3980      secs -= this.minutes * 60;
3981
3982      this.seconds = secs;
3983      return this;
3984    },
3985
3986    /**
3987     * Sets up the current instance using members from the passed data object.
3988     *
3989     * @param {Object} aData               An object with members of the duration
3990     * @param {Number} aData.weeks         Duration in weeks
3991     * @param {Number} aData.days          Duration in days
3992     * @param {Number} aData.hours         Duration in hours
3993     * @param {Number} aData.minutes       Duration in minutes
3994     * @param {Number} aData.seconds       Duration in seconds
3995     * @param {Boolean} aData.isNegative   If true, the duration is negative
3996     */
3997    fromData: function fromData(aData) {
3998      var propsToCopy = ["weeks", "days", "hours",
3999                         "minutes", "seconds", "isNegative"];
4000      for (var key in propsToCopy) {
4001        /* istanbul ignore if */
4002        if (!propsToCopy.hasOwnProperty(key)) {
4003          continue;
4004        }
4005        var prop = propsToCopy[key];
4006        if (aData && prop in aData) {
4007          this[prop] = aData[prop];
4008        } else {
4009          this[prop] = 0;
4010        }
4011      }
4012    },
4013
4014    /**
4015     * Resets the duration instance to the default values, i.e. PT0S
4016     */
4017    reset: function reset() {
4018      this.isNegative = false;
4019      this.weeks = 0;
4020      this.days = 0;
4021      this.hours = 0;
4022      this.minutes = 0;
4023      this.seconds = 0;
4024    },
4025
4026    /**
4027     * Compares the duration instance with another one.
4028     *
4029     * @param {ICAL.Duration} aOther        The instance to compare with
4030     * @return {Number}                     -1, 0 or 1 for less/equal/greater
4031     */
4032    compare: function compare(aOther) {
4033      var thisSeconds = this.toSeconds();
4034      var otherSeconds = aOther.toSeconds();
4035      return (thisSeconds > otherSeconds) - (thisSeconds < otherSeconds);
4036    },
4037
4038    /**
4039     * Normalizes the duration instance. For example, a duration with a value
4040     * of 61 seconds will be normalized to 1 minute and 1 second.
4041     */
4042    normalize: function normalize() {
4043      this.fromSeconds(this.toSeconds());
4044    },
4045
4046    /**
4047     * The string representation of this duration.
4048     * @return {String}
4049     */
4050    toString: function toString() {
4051      if (this.toSeconds() == 0) {
4052        return "PT0S";
4053      } else {
4054        var str = "";
4055        if (this.isNegative) str += "-";
4056        str += "P";
4057        if (this.weeks) str += this.weeks + "W";
4058        if (this.days) str += this.days + "D";
4059
4060        if (this.hours || this.minutes || this.seconds) {
4061          str += "T";
4062          if (this.hours) str += this.hours + "H";
4063          if (this.minutes) str += this.minutes + "M";
4064          if (this.seconds) str += this.seconds + "S";
4065        }
4066        return str;
4067      }
4068    },
4069
4070    /**
4071     * The iCalendar string representation of this duration.
4072     * @return {String}
4073     */
4074    toICALString: function() {
4075      return this.toString();
4076    }
4077  };
4078
4079  /**
4080   * Returns a new ICAL.Duration instance from the passed seconds value.
4081   *
4082   * @param {Number} aSeconds       The seconds to create the instance from
4083   * @return {ICAL.Duration}        The newly created duration instance
4084   */
4085  ICAL.Duration.fromSeconds = function icalduration_from_seconds(aSeconds) {
4086    return (new ICAL.Duration()).fromSeconds(aSeconds);
4087  };
4088
4089  /**
4090   * Internal helper function to handle a chunk of a duration.
4091   *
4092   * @param {String} letter type of duration chunk
4093   * @param {String} number numeric value or -/+
4094   * @param {Object} dict target to assign values to
4095   */
4096  function parseDurationChunk(letter, number, object) {
4097    var type;
4098    switch (letter) {
4099      case 'P':
4100        if (number && number === '-') {
4101          object.isNegative = true;
4102        } else {
4103          object.isNegative = false;
4104        }
4105        // period
4106        break;
4107      case 'D':
4108        type = 'days';
4109        break;
4110      case 'W':
4111        type = 'weeks';
4112        break;
4113      case 'H':
4114        type = 'hours';
4115        break;
4116      case 'M':
4117        type = 'minutes';
4118        break;
4119      case 'S':
4120        type = 'seconds';
4121        break;
4122      default:
4123        // Not a valid chunk
4124        return 0;
4125    }
4126
4127    if (type) {
4128      if (!number && number !== 0) {
4129        throw new Error(
4130          'invalid duration value: Missing number before "' + letter + '"'
4131        );
4132      }
4133      var num = parseInt(number, 10);
4134      if (ICAL.helpers.isStrictlyNaN(num)) {
4135        throw new Error(
4136          'invalid duration value: Invalid number "' + number + '" before "' + letter + '"'
4137        );
4138      }
4139      object[type] = num;
4140    }
4141
4142    return 1;
4143  }
4144
4145  /**
4146   * Checks if the given string is an iCalendar duration value.
4147   *
4148   * @param {String} value      The raw ical value
4149   * @return {Boolean}          True, if the given value is of the
4150   *                              duration ical type
4151   */
4152  ICAL.Duration.isValueString = function(string) {
4153    return (string[0] === 'P' || string[1] === 'P');
4154  };
4155
4156  /**
4157   * Creates a new {@link ICAL.Duration} instance from the passed string.
4158   *
4159   * @param {String} aStr       The string to parse
4160   * @return {ICAL.Duration}    The created duration instance
4161   */
4162  ICAL.Duration.fromString = function icalduration_from_string(aStr) {
4163    var pos = 0;
4164    var dict = Object.create(null);
4165    var chunks = 0;
4166
4167    while ((pos = aStr.search(DURATION_LETTERS)) !== -1) {
4168      var type = aStr[pos];
4169      var numeric = aStr.substr(0, pos);
4170      aStr = aStr.substr(pos + 1);
4171
4172      chunks += parseDurationChunk(type, numeric, dict);
4173    }
4174
4175    if (chunks < 2) {
4176      // There must be at least a chunk with "P" and some unit chunk
4177      throw new Error(
4178        'invalid duration value: Not enough duration components in "' + aStr + '"'
4179      );
4180    }
4181
4182    return new ICAL.Duration(dict);
4183  };
4184
4185  /**
4186   * Creates a new ICAL.Duration instance from the given data object.
4187   *
4188   * @param {Object} aData               An object with members of the duration
4189   * @param {Number} aData.weeks         Duration in weeks
4190   * @param {Number} aData.days          Duration in days
4191   * @param {Number} aData.hours         Duration in hours
4192   * @param {Number} aData.minutes       Duration in minutes
4193   * @param {Number} aData.seconds       Duration in seconds
4194   * @param {Boolean} aData.isNegative   If true, the duration is negative
4195   * @return {ICAL.Duration}             The createad duration instance
4196   */
4197  ICAL.Duration.fromData = function icalduration_from_data(aData) {
4198    return new ICAL.Duration(aData);
4199  };
4200})();
4201/* This Source Code Form is subject to the terms of the Mozilla Public
4202 * License, v. 2.0. If a copy of the MPL was not distributed with this
4203 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4204 * Portions Copyright (C) Philipp Kewisch, 2011-2012 */
4205
4206
4207
4208(function() {
4209  var OPTIONS = ["tzid", "location", "tznames",
4210                 "latitude", "longitude"];
4211
4212  /**
4213   * @classdesc
4214   * Timezone representation, created by passing in a tzid and component.
4215   *
4216   * @example
4217   * var vcalendar;
4218   * var timezoneComp = vcalendar.getFirstSubcomponent('vtimezone');
4219   * var tzid = timezoneComp.getFirstPropertyValue('tzid');
4220   *
4221   * var timezone = new ICAL.Timezone({
4222   *   component: timezoneComp,
4223   *   tzid
4224   * });
4225   *
4226   * @class
4227   * @param {ICAL.Component|Object} data options for class
4228   * @param {String|ICAL.Component} data.component
4229   *        If data is a simple object, then this member can be set to either a
4230   *        string containing the component data, or an already parsed
4231   *        ICAL.Component
4232   * @param {String} data.tzid      The timezone identifier
4233   * @param {String} data.location  The timezone locationw
4234   * @param {String} data.tznames   An alternative string representation of the
4235   *                                  timezone
4236   * @param {Number} data.latitude  The latitude of the timezone
4237   * @param {Number} data.longitude The longitude of the timezone
4238   */
4239  ICAL.Timezone = function icaltimezone(data) {
4240    this.wrappedJSObject = this;
4241    this.fromData(data);
4242  };
4243
4244  ICAL.Timezone.prototype = {
4245
4246    /**
4247     * Timezone identifier
4248     * @type {String}
4249     */
4250    tzid: "",
4251
4252    /**
4253     * Timezone location
4254     * @type {String}
4255     */
4256    location: "",
4257
4258    /**
4259     * Alternative timezone name, for the string representation
4260     * @type {String}
4261     */
4262    tznames: "",
4263
4264    /**
4265     * The primary latitude for the timezone.
4266     * @type {Number}
4267     */
4268    latitude: 0.0,
4269
4270    /**
4271     * The primary longitude for the timezone.
4272     * @type {Number}
4273     */
4274    longitude: 0.0,
4275
4276    /**
4277     * The vtimezone component for this timezone.
4278     * @type {ICAL.Component}
4279     */
4280    component: null,
4281
4282    /**
4283     * The year this timezone has been expanded to. All timezone transition
4284     * dates until this year are known and can be used for calculation
4285     *
4286     * @private
4287     * @type {Number}
4288     */
4289    expandedUntilYear: 0,
4290
4291    /**
4292     * The class identifier.
4293     * @constant
4294     * @type {String}
4295     * @default "icaltimezone"
4296     */
4297    icalclass: "icaltimezone",
4298
4299    /**
4300     * Sets up the current instance using members from the passed data object.
4301     *
4302     * @param {ICAL.Component|Object} aData options for class
4303     * @param {String|ICAL.Component} aData.component
4304     *        If aData is a simple object, then this member can be set to either a
4305     *        string containing the component data, or an already parsed
4306     *        ICAL.Component
4307     * @param {String} aData.tzid      The timezone identifier
4308     * @param {String} aData.location  The timezone locationw
4309     * @param {String} aData.tznames   An alternative string representation of the
4310     *                                  timezone
4311     * @param {Number} aData.latitude  The latitude of the timezone
4312     * @param {Number} aData.longitude The longitude of the timezone
4313     */
4314    fromData: function fromData(aData) {
4315      this.expandedUntilYear = 0;
4316      this.changes = [];
4317
4318      if (aData instanceof ICAL.Component) {
4319        // Either a component is passed directly
4320        this.component = aData;
4321      } else {
4322        // Otherwise the component may be in the data object
4323        if (aData && "component" in aData) {
4324          if (typeof aData.component == "string") {
4325            // If a string was passed, parse it as a component
4326            var jCal = ICAL.parse(aData.component);
4327            this.component = new ICAL.Component(jCal);
4328          } else if (aData.component instanceof ICAL.Component) {
4329            // If it was a component already, then just set it
4330            this.component = aData.component;
4331          } else {
4332            // Otherwise just null out the component
4333            this.component = null;
4334          }
4335        }
4336
4337        // Copy remaining passed properties
4338        for (var key in OPTIONS) {
4339          /* istanbul ignore else */
4340          if (OPTIONS.hasOwnProperty(key)) {
4341            var prop = OPTIONS[key];
4342            if (aData && prop in aData) {
4343              this[prop] = aData[prop];
4344            }
4345          }
4346        }
4347      }
4348
4349      // If we have a component but no TZID, attempt to get it from the
4350      // component's properties.
4351      if (this.component instanceof ICAL.Component && !this.tzid) {
4352        this.tzid = this.component.getFirstPropertyValue('tzid');
4353      }
4354
4355      return this;
4356    },
4357
4358    /**
4359     * Finds the utcOffset the given time would occur in this timezone.
4360     *
4361     * @param {ICAL.Time} tt        The time to check for
4362     * @return {Number} utc offset in seconds
4363     */
4364    utcOffset: function utcOffset(tt) {
4365      if (this == ICAL.Timezone.utcTimezone || this == ICAL.Timezone.localTimezone) {
4366        return 0;
4367      }
4368
4369      this._ensureCoverage(tt.year);
4370
4371      if (!this.changes.length) {
4372        return 0;
4373      }
4374
4375      var tt_change = {
4376        year: tt.year,
4377        month: tt.month,
4378        day: tt.day,
4379        hour: tt.hour,
4380        minute: tt.minute,
4381        second: tt.second
4382      };
4383
4384      var change_num = this._findNearbyChange(tt_change);
4385      var change_num_to_use = -1;
4386      var step = 1;
4387
4388      // TODO: replace with bin search?
4389      for (;;) {
4390        var change = ICAL.helpers.clone(this.changes[change_num], true);
4391        if (change.utcOffset < change.prevUtcOffset) {
4392          ICAL.Timezone.adjust_change(change, 0, 0, 0, change.utcOffset);
4393        } else {
4394          ICAL.Timezone.adjust_change(change, 0, 0, 0,
4395                                          change.prevUtcOffset);
4396        }
4397
4398        var cmp = ICAL.Timezone._compare_change_fn(tt_change, change);
4399
4400        if (cmp >= 0) {
4401          change_num_to_use = change_num;
4402        } else {
4403          step = -1;
4404        }
4405
4406        if (step == -1 && change_num_to_use != -1) {
4407          break;
4408        }
4409
4410        change_num += step;
4411
4412        if (change_num < 0) {
4413          return 0;
4414        }
4415
4416        if (change_num >= this.changes.length) {
4417          break;
4418        }
4419      }
4420
4421      var zone_change = this.changes[change_num_to_use];
4422      var utcOffset_change = zone_change.utcOffset - zone_change.prevUtcOffset;
4423
4424      if (utcOffset_change < 0 && change_num_to_use > 0) {
4425        var tmp_change = ICAL.helpers.clone(zone_change, true);
4426        ICAL.Timezone.adjust_change(tmp_change, 0, 0, 0,
4427                                        tmp_change.prevUtcOffset);
4428
4429        if (ICAL.Timezone._compare_change_fn(tt_change, tmp_change) < 0) {
4430          var prev_zone_change = this.changes[change_num_to_use - 1];
4431
4432          var want_daylight = false; // TODO
4433
4434          if (zone_change.is_daylight != want_daylight &&
4435              prev_zone_change.is_daylight == want_daylight) {
4436            zone_change = prev_zone_change;
4437          }
4438        }
4439      }
4440
4441      // TODO return is_daylight?
4442      return zone_change.utcOffset;
4443    },
4444
4445    _findNearbyChange: function icaltimezone_find_nearby_change(change) {
4446      // find the closest match
4447      var idx = ICAL.helpers.binsearchInsert(
4448        this.changes,
4449        change,
4450        ICAL.Timezone._compare_change_fn
4451      );
4452
4453      if (idx >= this.changes.length) {
4454        return this.changes.length - 1;
4455      }
4456
4457      return idx;
4458    },
4459
4460    _ensureCoverage: function(aYear) {
4461      if (ICAL.Timezone._minimumExpansionYear == -1) {
4462        var today = ICAL.Time.now();
4463        ICAL.Timezone._minimumExpansionYear = today.year;
4464      }
4465
4466      var changesEndYear = aYear;
4467      if (changesEndYear < ICAL.Timezone._minimumExpansionYear) {
4468        changesEndYear = ICAL.Timezone._minimumExpansionYear;
4469      }
4470
4471      changesEndYear += ICAL.Timezone.EXTRA_COVERAGE;
4472
4473      if (changesEndYear > ICAL.Timezone.MAX_YEAR) {
4474        changesEndYear = ICAL.Timezone.MAX_YEAR;
4475      }
4476
4477      if (!this.changes.length || this.expandedUntilYear < aYear) {
4478        var subcomps = this.component.getAllSubcomponents();
4479        var compLen = subcomps.length;
4480        var compIdx = 0;
4481
4482        for (; compIdx < compLen; compIdx++) {
4483          this._expandComponent(
4484            subcomps[compIdx], changesEndYear, this.changes
4485          );
4486        }
4487
4488        this.changes.sort(ICAL.Timezone._compare_change_fn);
4489        this.expandedUntilYear = changesEndYear;
4490      }
4491    },
4492
4493    _expandComponent: function(aComponent, aYear, changes) {
4494      if (!aComponent.hasProperty("dtstart") ||
4495          !aComponent.hasProperty("tzoffsetto") ||
4496          !aComponent.hasProperty("tzoffsetfrom")) {
4497        return null;
4498      }
4499
4500      var dtstart = aComponent.getFirstProperty("dtstart").getFirstValue();
4501      var change;
4502
4503      function convert_tzoffset(offset) {
4504        return offset.factor * (offset.hours * 3600 + offset.minutes * 60);
4505      }
4506
4507      function init_changes() {
4508        var changebase = {};
4509        changebase.is_daylight = (aComponent.name == "daylight");
4510        changebase.utcOffset = convert_tzoffset(
4511          aComponent.getFirstProperty("tzoffsetto").getFirstValue()
4512        );
4513
4514        changebase.prevUtcOffset = convert_tzoffset(
4515          aComponent.getFirstProperty("tzoffsetfrom").getFirstValue()
4516        );
4517
4518        return changebase;
4519      }
4520
4521      if (!aComponent.hasProperty("rrule") && !aComponent.hasProperty("rdate")) {
4522        change = init_changes();
4523        change.year = dtstart.year;
4524        change.month = dtstart.month;
4525        change.day = dtstart.day;
4526        change.hour = dtstart.hour;
4527        change.minute = dtstart.minute;
4528        change.second = dtstart.second;
4529
4530        ICAL.Timezone.adjust_change(change, 0, 0, 0,
4531                                        -change.prevUtcOffset);
4532        changes.push(change);
4533      } else {
4534        var props = aComponent.getAllProperties("rdate");
4535        for (var rdatekey in props) {
4536          /* istanbul ignore if */
4537          if (!props.hasOwnProperty(rdatekey)) {
4538            continue;
4539          }
4540          var rdate = props[rdatekey];
4541          var time = rdate.getFirstValue();
4542          change = init_changes();
4543
4544          change.year = time.year;
4545          change.month = time.month;
4546          change.day = time.day;
4547
4548          if (time.isDate) {
4549            change.hour = dtstart.hour;
4550            change.minute = dtstart.minute;
4551            change.second = dtstart.second;
4552
4553            if (dtstart.zone != ICAL.Timezone.utcTimezone) {
4554              ICAL.Timezone.adjust_change(change, 0, 0, 0,
4555                                              -change.prevUtcOffset);
4556            }
4557          } else {
4558            change.hour = time.hour;
4559            change.minute = time.minute;
4560            change.second = time.second;
4561
4562            if (time.zone != ICAL.Timezone.utcTimezone) {
4563              ICAL.Timezone.adjust_change(change, 0, 0, 0,
4564                                              -change.prevUtcOffset);
4565            }
4566          }
4567
4568          changes.push(change);
4569        }
4570
4571        var rrule = aComponent.getFirstProperty("rrule");
4572
4573        if (rrule) {
4574          rrule = rrule.getFirstValue();
4575          change = init_changes();
4576
4577          if (rrule.until && rrule.until.zone == ICAL.Timezone.utcTimezone) {
4578            rrule.until.adjust(0, 0, 0, change.prevUtcOffset);
4579            rrule.until.zone = ICAL.Timezone.localTimezone;
4580          }
4581
4582          var iterator = rrule.iterator(dtstart);
4583
4584          var occ;
4585          while ((occ = iterator.next())) {
4586            change = init_changes();
4587            if (occ.year > aYear || !occ) {
4588              break;
4589            }
4590
4591            change.year = occ.year;
4592            change.month = occ.month;
4593            change.day = occ.day;
4594            change.hour = occ.hour;
4595            change.minute = occ.minute;
4596            change.second = occ.second;
4597            change.isDate = occ.isDate;
4598
4599            ICAL.Timezone.adjust_change(change, 0, 0, 0,
4600                                            -change.prevUtcOffset);
4601            changes.push(change);
4602          }
4603        }
4604      }
4605
4606      return changes;
4607    },
4608
4609    /**
4610     * The string representation of this timezone.
4611     * @return {String}
4612     */
4613    toString: function toString() {
4614      return (this.tznames ? this.tznames : this.tzid);
4615    }
4616  };
4617
4618  ICAL.Timezone._compare_change_fn = function icaltimezone_compare_change_fn(a, b) {
4619    if (a.year < b.year) return -1;
4620    else if (a.year > b.year) return 1;
4621
4622    if (a.month < b.month) return -1;
4623    else if (a.month > b.month) return 1;
4624
4625    if (a.day < b.day) return -1;
4626    else if (a.day > b.day) return 1;
4627
4628    if (a.hour < b.hour) return -1;
4629    else if (a.hour > b.hour) return 1;
4630
4631    if (a.minute < b.minute) return -1;
4632    else if (a.minute > b.minute) return 1;
4633
4634    if (a.second < b.second) return -1;
4635    else if (a.second > b.second) return 1;
4636
4637    return 0;
4638  };
4639
4640  /**
4641   * Convert the date/time from one zone to the next.
4642   *
4643   * @param {ICAL.Time} tt                  The time to convert
4644   * @param {ICAL.Timezone} from_zone       The source zone to convert from
4645   * @param {ICAL.Timezone} to_zone         The target zone to convert to
4646   * @return {ICAL.Time}                    The converted date/time object
4647   */
4648  ICAL.Timezone.convert_time = function icaltimezone_convert_time(tt, from_zone, to_zone) {
4649    if (tt.isDate ||
4650        from_zone.tzid == to_zone.tzid ||
4651        from_zone == ICAL.Timezone.localTimezone ||
4652        to_zone == ICAL.Timezone.localTimezone) {
4653      tt.zone = to_zone;
4654      return tt;
4655    }
4656
4657    var utcOffset = from_zone.utcOffset(tt);
4658    tt.adjust(0, 0, 0, - utcOffset);
4659
4660    utcOffset = to_zone.utcOffset(tt);
4661    tt.adjust(0, 0, 0, utcOffset);
4662
4663    return null;
4664  };
4665
4666  /**
4667   * Creates a new ICAL.Timezone instance from the passed data object.
4668   *
4669   * @param {ICAL.Component|Object} aData options for class
4670   * @param {String|ICAL.Component} aData.component
4671   *        If aData is a simple object, then this member can be set to either a
4672   *        string containing the component data, or an already parsed
4673   *        ICAL.Component
4674   * @param {String} aData.tzid      The timezone identifier
4675   * @param {String} aData.location  The timezone locationw
4676   * @param {String} aData.tznames   An alternative string representation of the
4677   *                                  timezone
4678   * @param {Number} aData.latitude  The latitude of the timezone
4679   * @param {Number} aData.longitude The longitude of the timezone
4680   */
4681  ICAL.Timezone.fromData = function icaltimezone_fromData(aData) {
4682    var tt = new ICAL.Timezone();
4683    return tt.fromData(aData);
4684  };
4685
4686  /**
4687   * The instance describing the UTC timezone
4688   * @type {ICAL.Timezone}
4689   * @constant
4690   * @instance
4691   */
4692  ICAL.Timezone.utcTimezone = ICAL.Timezone.fromData({
4693    tzid: "UTC"
4694  });
4695
4696  /**
4697   * The instance describing the local timezone
4698   * @type {ICAL.Timezone}
4699   * @constant
4700   * @instance
4701   */
4702  ICAL.Timezone.localTimezone = ICAL.Timezone.fromData({
4703    tzid: "floating"
4704  });
4705
4706  /**
4707   * Adjust a timezone change object.
4708   * @private
4709   * @param {Object} change     The timezone change object
4710   * @param {Number} days       The extra amount of days
4711   * @param {Number} hours      The extra amount of hours
4712   * @param {Number} minutes    The extra amount of minutes
4713   * @param {Number} seconds    The extra amount of seconds
4714   */
4715  ICAL.Timezone.adjust_change = function icaltimezone_adjust_change(change, days, hours, minutes, seconds) {
4716    return ICAL.Time.prototype.adjust.call(
4717      change,
4718      days,
4719      hours,
4720      minutes,
4721      seconds,
4722      change
4723    );
4724  };
4725
4726  ICAL.Timezone._minimumExpansionYear = -1;
4727  ICAL.Timezone.MAX_YEAR = 2035; // TODO this is because of time_t, which we don't need. Still usefull?
4728  ICAL.Timezone.EXTRA_COVERAGE = 5;
4729})();
4730/* This Source Code Form is subject to the terms of the Mozilla Public
4731 * License, v. 2.0. If a copy of the MPL was not distributed with this
4732 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4733 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
4734
4735
4736/**
4737 * This symbol is further described later on
4738 * @ignore
4739 */
4740ICAL.TimezoneService = (function() {
4741  var zones;
4742
4743  /**
4744   * @classdesc
4745   * Singleton class to contain timezones.  Right now its all manual registry in
4746   * the future we may use this class to download timezone information or handle
4747   * loading pre-expanded timezones.
4748   *
4749   * @namespace
4750   * @alias ICAL.TimezoneService
4751   */
4752  var TimezoneService = {
4753    get count() {
4754      return Object.keys(zones).length;
4755    },
4756
4757    reset: function() {
4758      zones = Object.create(null);
4759      var utc = ICAL.Timezone.utcTimezone;
4760
4761      zones.Z = utc;
4762      zones.UTC = utc;
4763      zones.GMT = utc;
4764    },
4765
4766    /**
4767     * Checks if timezone id has been registered.
4768     *
4769     * @param {String} tzid     Timezone identifier (e.g. America/Los_Angeles)
4770     * @return {Boolean}        False, when not present
4771     */
4772    has: function(tzid) {
4773      return !!zones[tzid];
4774    },
4775
4776    /**
4777     * Returns a timezone by its tzid if present.
4778     *
4779     * @param {String} tzid     Timezone identifier (e.g. America/Los_Angeles)
4780     * @return {?ICAL.Timezone} The timezone, or null if not found
4781     */
4782    get: function(tzid) {
4783      return zones[tzid];
4784    },
4785
4786    /**
4787     * Registers a timezone object or component.
4788     *
4789     * @param {String=} name
4790     *        The name of the timezone. Defaults to the component's TZID if not
4791     *        passed.
4792     * @param {ICAL.Component|ICAL.Timezone} zone
4793     *        The initialized zone or vtimezone.
4794     */
4795    register: function(name, timezone) {
4796      if (name instanceof ICAL.Component) {
4797        if (name.name === 'vtimezone') {
4798          timezone = new ICAL.Timezone(name);
4799          name = timezone.tzid;
4800        }
4801      }
4802
4803      if (timezone instanceof ICAL.Timezone) {
4804        zones[name] = timezone;
4805      } else {
4806        throw new TypeError('timezone must be ICAL.Timezone or ICAL.Component');
4807      }
4808    },
4809
4810    /**
4811     * Removes a timezone by its tzid from the list.
4812     *
4813     * @param {String} tzid     Timezone identifier (e.g. America/Los_Angeles)
4814     * @return {?ICAL.Timezone} The removed timezone, or null if not registered
4815     */
4816    remove: function(tzid) {
4817      return (delete zones[tzid]);
4818    }
4819  };
4820
4821  // initialize defaults
4822  TimezoneService.reset();
4823
4824  return TimezoneService;
4825}());
4826/* This Source Code Form is subject to the terms of the Mozilla Public
4827 * License, v. 2.0. If a copy of the MPL was not distributed with this
4828 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4829 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
4830
4831
4832
4833(function() {
4834
4835  /**
4836   * @classdesc
4837   * iCalendar Time representation (similar to JS Date object).  Fully
4838   * independent of system (OS) timezone / time.  Unlike JS Date, the month
4839   * January is 1, not zero.
4840   *
4841   * @example
4842   * var time = new ICAL.Time({
4843   *   year: 2012,
4844   *   month: 10,
4845   *   day: 11
4846   *   minute: 0,
4847   *   second: 0,
4848   *   isDate: false
4849   * });
4850   *
4851   *
4852   * @alias ICAL.Time
4853   * @class
4854   * @param {Object} data           Time initialization
4855   * @param {Number=} data.year     The year for this date
4856   * @param {Number=} data.month    The month for this date
4857   * @param {Number=} data.day      The day for this date
4858   * @param {Number=} data.hour     The hour for this date
4859   * @param {Number=} data.minute   The minute for this date
4860   * @param {Number=} data.second   The second for this date
4861   * @param {Boolean=} data.isDate  If true, the instance represents a date (as
4862   *                                  opposed to a date-time)
4863   * @param {ICAL.Timezone} zone timezone this position occurs in
4864   */
4865  ICAL.Time = function icaltime(data, zone) {
4866    this.wrappedJSObject = this;
4867    var time = this._time = Object.create(null);
4868
4869    /* time defaults */
4870    time.year = 0;
4871    time.month = 1;
4872    time.day = 1;
4873    time.hour = 0;
4874    time.minute = 0;
4875    time.second = 0;
4876    time.isDate = false;
4877
4878    this.fromData(data, zone);
4879  };
4880
4881  ICAL.Time._dowCache = {};
4882  ICAL.Time._wnCache = {};
4883
4884  ICAL.Time.prototype = {
4885
4886    /**
4887     * The class identifier.
4888     * @constant
4889     * @type {String}
4890     * @default "icaltime"
4891     */
4892    icalclass: "icaltime",
4893    _cachedUnixTime: null,
4894
4895    /**
4896     * The type name, to be used in the jCal object. This value may change and
4897     * is strictly defined by the {@link ICAL.Time#isDate isDate} member.
4898     * @readonly
4899     * @type {String}
4900     * @default "date-time"
4901     */
4902    get icaltype() {
4903      return this.isDate ? 'date' : 'date-time';
4904    },
4905
4906    /**
4907     * The timezone for this time.
4908     * @type {ICAL.Timezone}
4909     */
4910    zone: null,
4911
4912    /**
4913     * Internal uses to indicate that a change has been made and the next read
4914     * operation must attempt to normalize the value (for example changing the
4915     * day to 33).
4916     *
4917     * @type {Boolean}
4918     * @private
4919     */
4920    _pendingNormalization: false,
4921
4922    /**
4923     * Returns a clone of the time object.
4924     *
4925     * @return {ICAL.Time}              The cloned object
4926     */
4927    clone: function() {
4928      return new ICAL.Time(this._time, this.zone);
4929    },
4930
4931    /**
4932     * Reset the time instance to epoch time
4933     */
4934    reset: function icaltime_reset() {
4935      this.fromData(ICAL.Time.epochTime);
4936      this.zone = ICAL.Timezone.utcTimezone;
4937    },
4938
4939    /**
4940     * Reset the time instance to the given date/time values.
4941     *
4942     * @param {Number} year             The year to set
4943     * @param {Number} month            The month to set
4944     * @param {Number} day              The day to set
4945     * @param {Number} hour             The hour to set
4946     * @param {Number} minute           The minute to set
4947     * @param {Number} second           The second to set
4948     * @param {ICAL.Timezone} timezone  The timezone to set
4949     */
4950    resetTo: function icaltime_resetTo(year, month, day,
4951                                       hour, minute, second, timezone) {
4952      this.fromData({
4953        year: year,
4954        month: month,
4955        day: day,
4956        hour: hour,
4957        minute: minute,
4958        second: second,
4959        zone: timezone
4960      });
4961    },
4962
4963    /**
4964     * Set up the current instance from the Javascript date value.
4965     *
4966     * @param {?Date} aDate     The Javascript Date to read, or null to reset
4967     * @param {Boolean} useUTC  If true, the UTC values of the date will be used
4968     */
4969    fromJSDate: function icaltime_fromJSDate(aDate, useUTC) {
4970      if (!aDate) {
4971        this.reset();
4972      } else {
4973        if (useUTC) {
4974          this.zone = ICAL.Timezone.utcTimezone;
4975          this.year = aDate.getUTCFullYear();
4976          this.month = aDate.getUTCMonth() + 1;
4977          this.day = aDate.getUTCDate();
4978          this.hour = aDate.getUTCHours();
4979          this.minute = aDate.getUTCMinutes();
4980          this.second = aDate.getUTCSeconds();
4981        } else {
4982          this.zone = ICAL.Timezone.localTimezone;
4983          this.year = aDate.getFullYear();
4984          this.month = aDate.getMonth() + 1;
4985          this.day = aDate.getDate();
4986          this.hour = aDate.getHours();
4987          this.minute = aDate.getMinutes();
4988          this.second = aDate.getSeconds();
4989        }
4990      }
4991      this._cachedUnixTime = null;
4992      return this;
4993    },
4994
4995    /**
4996     * Sets up the current instance using members from the passed data object.
4997     *
4998     * @param {Object} aData            Time initialization
4999     * @param {Number=} aData.year      The year for this date
5000     * @param {Number=} aData.month     The month for this date
5001     * @param {Number=} aData.day       The day for this date
5002     * @param {Number=} aData.hour      The hour for this date
5003     * @param {Number=} aData.minute    The minute for this date
5004     * @param {Number=} aData.second    The second for this date
5005     * @param {Boolean=} aData.isDate   If true, the instance represents a date
5006     *                                    (as opposed to a date-time)
5007     * @param {ICAL.Timezone=} aZone    Timezone this position occurs in
5008     */
5009    fromData: function fromData(aData, aZone) {
5010      if (aData) {
5011        for (var key in aData) {
5012          /* istanbul ignore else */
5013          if (Object.prototype.hasOwnProperty.call(aData, key)) {
5014            // ical type cannot be set
5015            if (key === 'icaltype') continue;
5016            this[key] = aData[key];
5017          }
5018        }
5019      }
5020
5021      if (aZone) {
5022        this.zone = aZone;
5023      }
5024
5025      if (aData && !("isDate" in aData)) {
5026        this.isDate = !("hour" in aData);
5027      } else if (aData && ("isDate" in aData)) {
5028        this.isDate = aData.isDate;
5029      }
5030
5031      if (aData && "timezone" in aData) {
5032        var zone = ICAL.TimezoneService.get(
5033          aData.timezone
5034        );
5035
5036        this.zone = zone || ICAL.Timezone.localTimezone;
5037      }
5038
5039      if (aData && "zone" in aData) {
5040        this.zone = aData.zone;
5041      }
5042
5043      if (!this.zone) {
5044        this.zone = ICAL.Timezone.localTimezone;
5045      }
5046
5047      this._cachedUnixTime = null;
5048      return this;
5049    },
5050
5051    /**
5052     * Calculate the day of week.
5053     * @param {ICAL.Time.weekDay=} aWeekStart
5054     *        The week start weekday, defaults to SUNDAY
5055     * @return {ICAL.Time.weekDay}
5056     */
5057    dayOfWeek: function icaltime_dayOfWeek(aWeekStart) {
5058      var firstDow = aWeekStart || ICAL.Time.SUNDAY;
5059      var dowCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + firstDow;
5060      if (dowCacheKey in ICAL.Time._dowCache) {
5061        return ICAL.Time._dowCache[dowCacheKey];
5062      }
5063
5064      // Using Zeller's algorithm
5065      var q = this.day;
5066      var m = this.month + (this.month < 3 ? 12 : 0);
5067      var Y = this.year - (this.month < 3 ? 1 : 0);
5068
5069      var h = (q + Y + ICAL.helpers.trunc(((m + 1) * 26) / 10) + ICAL.helpers.trunc(Y / 4));
5070      /* istanbul ignore else */
5071      if (true /* gregorian */) {
5072        h += ICAL.helpers.trunc(Y / 100) * 6 + ICAL.helpers.trunc(Y / 400);
5073      } else {
5074        h += 5;
5075      }
5076
5077      // Normalize to 1 = wkst
5078      h = ((h + 7 - firstDow) % 7) + 1;
5079      ICAL.Time._dowCache[dowCacheKey] = h;
5080      return h;
5081    },
5082
5083    /**
5084     * Calculate the day of year.
5085     * @return {Number}
5086     */
5087    dayOfYear: function dayOfYear() {
5088      var is_leap = (ICAL.Time.isLeapYear(this.year) ? 1 : 0);
5089      var diypm = ICAL.Time.daysInYearPassedMonth;
5090      return diypm[is_leap][this.month - 1] + this.day;
5091    },
5092
5093    /**
5094     * Returns a copy of the current date/time, rewound to the start of the
5095     * week. The resulting ICAL.Time instance is of icaltype date, even if this
5096     * is a date-time.
5097     *
5098     * @param {ICAL.Time.weekDay=} aWeekStart
5099     *        The week start weekday, defaults to SUNDAY
5100     * @return {ICAL.Time}      The start of the week (cloned)
5101     */
5102    startOfWeek: function startOfWeek(aWeekStart) {
5103      var firstDow = aWeekStart || ICAL.Time.SUNDAY;
5104      var result = this.clone();
5105      result.day -= ((this.dayOfWeek() + 7 - firstDow) % 7);
5106      result.isDate = true;
5107      result.hour = 0;
5108      result.minute = 0;
5109      result.second = 0;
5110      return result;
5111    },
5112
5113    /**
5114     * Returns a copy of the current date/time, shifted to the end of the week.
5115     * The resulting ICAL.Time instance is of icaltype date, even if this is a
5116     * date-time.
5117     *
5118     * @param {ICAL.Time.weekDay=} aWeekStart
5119     *        The week start weekday, defaults to SUNDAY
5120     * @return {ICAL.Time}      The end of the week (cloned)
5121     */
5122    endOfWeek: function endOfWeek(aWeekStart) {
5123      var firstDow = aWeekStart || ICAL.Time.SUNDAY;
5124      var result = this.clone();
5125      result.day += (7 - this.dayOfWeek() + firstDow - ICAL.Time.SUNDAY) % 7;
5126      result.isDate = true;
5127      result.hour = 0;
5128      result.minute = 0;
5129      result.second = 0;
5130      return result;
5131    },
5132
5133    /**
5134     * Returns a copy of the current date/time, rewound to the start of the
5135     * month. The resulting ICAL.Time instance is of icaltype date, even if
5136     * this is a date-time.
5137     *
5138     * @return {ICAL.Time}      The start of the month (cloned)
5139     */
5140    startOfMonth: function startOfMonth() {
5141      var result = this.clone();
5142      result.day = 1;
5143      result.isDate = true;
5144      result.hour = 0;
5145      result.minute = 0;
5146      result.second = 0;
5147      return result;
5148    },
5149
5150    /**
5151     * Returns a copy of the current date/time, shifted to the end of the
5152     * month.  The resulting ICAL.Time instance is of icaltype date, even if
5153     * this is a date-time.
5154     *
5155     * @return {ICAL.Time}      The end of the month (cloned)
5156     */
5157    endOfMonth: function endOfMonth() {
5158      var result = this.clone();
5159      result.day = ICAL.Time.daysInMonth(result.month, result.year);
5160      result.isDate = true;
5161      result.hour = 0;
5162      result.minute = 0;
5163      result.second = 0;
5164      return result;
5165    },
5166
5167    /**
5168     * Returns a copy of the current date/time, rewound to the start of the
5169     * year. The resulting ICAL.Time instance is of icaltype date, even if
5170     * this is a date-time.
5171     *
5172     * @return {ICAL.Time}      The start of the year (cloned)
5173     */
5174    startOfYear: function startOfYear() {
5175      var result = this.clone();
5176      result.day = 1;
5177      result.month = 1;
5178      result.isDate = true;
5179      result.hour = 0;
5180      result.minute = 0;
5181      result.second = 0;
5182      return result;
5183    },
5184
5185    /**
5186     * Returns a copy of the current date/time, shifted to the end of the
5187     * year.  The resulting ICAL.Time instance is of icaltype date, even if
5188     * this is a date-time.
5189     *
5190     * @return {ICAL.Time}      The end of the year (cloned)
5191     */
5192    endOfYear: function endOfYear() {
5193      var result = this.clone();
5194      result.day = 31;
5195      result.month = 12;
5196      result.isDate = true;
5197      result.hour = 0;
5198      result.minute = 0;
5199      result.second = 0;
5200      return result;
5201    },
5202
5203    /**
5204     * First calculates the start of the week, then returns the day of year for
5205     * this date. If the day falls into the previous year, the day is zero or negative.
5206     *
5207     * @param {ICAL.Time.weekDay=} aFirstDayOfWeek
5208     *        The week start weekday, defaults to SUNDAY
5209     * @return {Number}     The calculated day of year
5210     */
5211    startDoyWeek: function startDoyWeek(aFirstDayOfWeek) {
5212      var firstDow = aFirstDayOfWeek || ICAL.Time.SUNDAY;
5213      var delta = this.dayOfWeek() - firstDow;
5214      if (delta < 0) delta += 7;
5215      return this.dayOfYear() - delta;
5216    },
5217
5218    /**
5219     * Get the dominical letter for the current year. Letters range from A - G
5220     * for common years, and AG to GF for leap years.
5221     *
5222     * @param {Number} yr           The year to retrieve the letter for
5223     * @return {String}             The dominical letter.
5224     */
5225    getDominicalLetter: function() {
5226      return ICAL.Time.getDominicalLetter(this.year);
5227    },
5228
5229    /**
5230     * Finds the nthWeekDay relative to the current month (not day).  The
5231     * returned value is a day relative the month that this month belongs to so
5232     * 1 would indicate the first of the month and 40 would indicate a day in
5233     * the following month.
5234     *
5235     * @param {Number} aDayOfWeek   Day of the week see the day name constants
5236     * @param {Number} aPos         Nth occurrence of a given week day values
5237     *        of 1 and 0 both indicate the first weekday of that type. aPos may
5238     *        be either positive or negative
5239     *
5240     * @return {Number} numeric value indicating a day relative
5241     *                   to the current month of this time object
5242     */
5243    nthWeekDay: function icaltime_nthWeekDay(aDayOfWeek, aPos) {
5244      var daysInMonth = ICAL.Time.daysInMonth(this.month, this.year);
5245      var weekday;
5246      var pos = aPos;
5247
5248      var start = 0;
5249
5250      var otherDay = this.clone();
5251
5252      if (pos >= 0) {
5253        otherDay.day = 1;
5254
5255        // because 0 means no position has been given
5256        // 1 and 0 indicate the same day.
5257        if (pos != 0) {
5258          // remove the extra numeric value
5259          pos--;
5260        }
5261
5262        // set current start offset to current day.
5263        start = otherDay.day;
5264
5265        // find the current day of week
5266        var startDow = otherDay.dayOfWeek();
5267
5268        // calculate the difference between current
5269        // day of the week and desired day of the week
5270        var offset = aDayOfWeek - startDow;
5271
5272
5273        // if the offset goes into the past
5274        // week we add 7 so its goes into the next
5275        // week. We only want to go forward in time here.
5276        if (offset < 0)
5277          // this is really important otherwise we would
5278          // end up with dates from in the past.
5279          offset += 7;
5280
5281        // add offset to start so start is the same
5282        // day of the week as the desired day of week.
5283        start += offset;
5284
5285        // because we are going to add (and multiply)
5286        // the numeric value of the day we subtract it
5287        // from the start position so not to add it twice.
5288        start -= aDayOfWeek;
5289
5290        // set week day
5291        weekday = aDayOfWeek;
5292      } else {
5293
5294        // then we set it to the last day in the current month
5295        otherDay.day = daysInMonth;
5296
5297        // find the ends weekday
5298        var endDow = otherDay.dayOfWeek();
5299
5300        pos++;
5301
5302        weekday = (endDow - aDayOfWeek);
5303
5304        if (weekday < 0) {
5305          weekday += 7;
5306        }
5307
5308        weekday = daysInMonth - weekday;
5309      }
5310
5311      weekday += pos * 7;
5312
5313      return start + weekday;
5314    },
5315
5316    /**
5317     * Checks if current time is the nth weekday, relative to the current
5318     * month.  Will always return false when rule resolves outside of current
5319     * month.
5320     *
5321     * @param {ICAL.Time.weekDay} aDayOfWeek       Day of week to check
5322     * @param {Number} aPos                        Relative position
5323     * @return {Boolean}                           True, if its the nth weekday
5324     */
5325    isNthWeekDay: function(aDayOfWeek, aPos) {
5326      var dow = this.dayOfWeek();
5327
5328      if (aPos === 0 && dow === aDayOfWeek) {
5329        return true;
5330      }
5331
5332      // get pos
5333      var day = this.nthWeekDay(aDayOfWeek, aPos);
5334
5335      if (day === this.day) {
5336        return true;
5337      }
5338
5339      return false;
5340    },
5341
5342    /**
5343     * Calculates the ISO 8601 week number. The first week of a year is the
5344     * week that contains the first Thursday. The year can have 53 weeks, if
5345     * January 1st is a Friday.
5346     *
5347     * Note there are regions where the first week of the year is the one that
5348     * starts on January 1st, which may offset the week number. Also, if a
5349     * different week start is specified, this will also affect the week
5350     * number.
5351     *
5352     * @see ICAL.Time.weekOneStarts
5353     * @param {ICAL.Time.weekDay} aWeekStart        The weekday the week starts with
5354     * @return {Number}                             The ISO week number
5355     */
5356    weekNumber: function weekNumber(aWeekStart) {
5357      var wnCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + aWeekStart;
5358      if (wnCacheKey in ICAL.Time._wnCache) {
5359        return ICAL.Time._wnCache[wnCacheKey];
5360      }
5361      // This function courtesty of Julian Bucknall, published under the MIT license
5362      // http://www.boyet.com/articles/publishedarticles/calculatingtheisoweeknumb.html
5363      // plus some fixes to be able to use different week starts.
5364      var week1;
5365
5366      var dt = this.clone();
5367      dt.isDate = true;
5368      var isoyear = this.year;
5369
5370      if (dt.month == 12 && dt.day > 25) {
5371        week1 = ICAL.Time.weekOneStarts(isoyear + 1, aWeekStart);
5372        if (dt.compare(week1) < 0) {
5373          week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart);
5374        } else {
5375          isoyear++;
5376        }
5377      } else {
5378        week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart);
5379        if (dt.compare(week1) < 0) {
5380          week1 = ICAL.Time.weekOneStarts(--isoyear, aWeekStart);
5381        }
5382      }
5383
5384      var daysBetween = (dt.subtractDate(week1).toSeconds() / 86400);
5385      var answer = ICAL.helpers.trunc(daysBetween / 7) + 1;
5386      ICAL.Time._wnCache[wnCacheKey] = answer;
5387      return answer;
5388    },
5389
5390    /**
5391     * Adds the duration to the current time. The instance is modified in
5392     * place.
5393     *
5394     * @param {ICAL.Duration} aDuration         The duration to add
5395     */
5396    addDuration: function icaltime_add(aDuration) {
5397      var mult = (aDuration.isNegative ? -1 : 1);
5398
5399      // because of the duration optimizations it is much
5400      // more efficient to grab all the values up front
5401      // then set them directly (which will avoid a normalization call).
5402      // So we don't actually normalize until we need it.
5403      var second = this.second;
5404      var minute = this.minute;
5405      var hour = this.hour;
5406      var day = this.day;
5407
5408      second += mult * aDuration.seconds;
5409      minute += mult * aDuration.minutes;
5410      hour += mult * aDuration.hours;
5411      day += mult * aDuration.days;
5412      day += mult * 7 * aDuration.weeks;
5413
5414      this.second = second;
5415      this.minute = minute;
5416      this.hour = hour;
5417      this.day = day;
5418
5419      this._cachedUnixTime = null;
5420    },
5421
5422    /**
5423     * Subtract the date details (_excluding_ timezone).  Useful for finding
5424     * the relative difference between two time objects excluding their
5425     * timezone differences.
5426     *
5427     * @param {ICAL.Time} aDate     The date to substract
5428     * @return {ICAL.Duration}      The difference as a duration
5429     */
5430    subtractDate: function icaltime_subtract(aDate) {
5431      var unixTime = this.toUnixTime() + this.utcOffset();
5432      var other = aDate.toUnixTime() + aDate.utcOffset();
5433      return ICAL.Duration.fromSeconds(unixTime - other);
5434    },
5435
5436    /**
5437     * Subtract the date details, taking timezones into account.
5438     *
5439     * @param {ICAL.Time} aDate  The date to subtract
5440     * @return {ICAL.Duration}  The difference in duration
5441     */
5442    subtractDateTz: function icaltime_subtract_abs(aDate) {
5443      var unixTime = this.toUnixTime();
5444      var other = aDate.toUnixTime();
5445      return ICAL.Duration.fromSeconds(unixTime - other);
5446    },
5447
5448    /**
5449     * Compares the ICAL.Time instance with another one.
5450     *
5451     * @param {ICAL.Duration} aOther        The instance to compare with
5452     * @return {Number}                     -1, 0 or 1 for less/equal/greater
5453     */
5454    compare: function icaltime_compare(other) {
5455      var a = this.toUnixTime();
5456      var b = other.toUnixTime();
5457
5458      if (a > b) return 1;
5459      if (b > a) return -1;
5460      return 0;
5461    },
5462
5463    /**
5464     * Compares only the date part of this instance with another one.
5465     *
5466     * @param {ICAL.Duration} other         The instance to compare with
5467     * @param {ICAL.Timezone} tz            The timezone to compare in
5468     * @return {Number}                     -1, 0 or 1 for less/equal/greater
5469     */
5470    compareDateOnlyTz: function icaltime_compareDateOnlyTz(other, tz) {
5471      function cmp(attr) {
5472        return ICAL.Time._cmp_attr(a, b, attr);
5473      }
5474      var a = this.convertToZone(tz);
5475      var b = other.convertToZone(tz);
5476      var rc = 0;
5477
5478      if ((rc = cmp("year")) != 0) return rc;
5479      if ((rc = cmp("month")) != 0) return rc;
5480      if ((rc = cmp("day")) != 0) return rc;
5481
5482      return rc;
5483    },
5484
5485    /**
5486     * Convert the instance into another timzone. The returned ICAL.Time
5487     * instance is always a copy.
5488     *
5489     * @param {ICAL.Timezone} zone      The zone to convert to
5490     * @return {ICAL.Time}              The copy, converted to the zone
5491     */
5492    convertToZone: function convertToZone(zone) {
5493      var copy = this.clone();
5494      var zone_equals = (this.zone.tzid == zone.tzid);
5495
5496      if (!this.isDate && !zone_equals) {
5497        ICAL.Timezone.convert_time(copy, this.zone, zone);
5498      }
5499
5500      copy.zone = zone;
5501      return copy;
5502    },
5503
5504    /**
5505     * Calculates the UTC offset of the current date/time in the timezone it is
5506     * in.
5507     *
5508     * @return {Number}     UTC offset in seconds
5509     */
5510    utcOffset: function utc_offset() {
5511      if (this.zone == ICAL.Timezone.localTimezone ||
5512          this.zone == ICAL.Timezone.utcTimezone) {
5513        return 0;
5514      } else {
5515        return this.zone.utcOffset(this);
5516      }
5517    },
5518
5519    /**
5520     * Returns an RFC 5545 compliant ical representation of this object.
5521     *
5522     * @return {String} ical date/date-time
5523     */
5524    toICALString: function() {
5525      var string = this.toString();
5526
5527      if (string.length > 10) {
5528        return ICAL.design.icalendar.value['date-time'].toICAL(string);
5529      } else {
5530        return ICAL.design.icalendar.value.date.toICAL(string);
5531      }
5532    },
5533
5534    /**
5535     * The string representation of this date/time, in jCal form
5536     * (including : and - separators).
5537     * @return {String}
5538     */
5539    toString: function toString() {
5540      var result = this.year + '-' +
5541                   ICAL.helpers.pad2(this.month) + '-' +
5542                   ICAL.helpers.pad2(this.day);
5543
5544      if (!this.isDate) {
5545          result += 'T' + ICAL.helpers.pad2(this.hour) + ':' +
5546                    ICAL.helpers.pad2(this.minute) + ':' +
5547                    ICAL.helpers.pad2(this.second);
5548
5549        if (this.zone === ICAL.Timezone.utcTimezone) {
5550          result += 'Z';
5551        }
5552      }
5553
5554      return result;
5555    },
5556
5557    /**
5558     * Converts the current instance to a Javascript date
5559     * @return {Date}
5560     */
5561    toJSDate: function toJSDate() {
5562      if (this.zone == ICAL.Timezone.localTimezone) {
5563        if (this.isDate) {
5564          return new Date(this.year, this.month - 1, this.day);
5565        } else {
5566          return new Date(this.year, this.month - 1, this.day,
5567                          this.hour, this.minute, this.second, 0);
5568        }
5569      } else {
5570        return new Date(this.toUnixTime() * 1000);
5571      }
5572    },
5573
5574    _normalize: function icaltime_normalize() {
5575      var isDate = this._time.isDate;
5576      if (this._time.isDate) {
5577        this._time.hour = 0;
5578        this._time.minute = 0;
5579        this._time.second = 0;
5580      }
5581      this.adjust(0, 0, 0, 0);
5582
5583      return this;
5584    },
5585
5586    /**
5587     * Adjust the date/time by the given offset
5588     *
5589     * @param {Number} aExtraDays       The extra amount of days
5590     * @param {Number} aExtraHours      The extra amount of hours
5591     * @param {Number} aExtraMinutes    The extra amount of minutes
5592     * @param {Number} aExtraSeconds    The extra amount of seconds
5593     * @param {Number=} aTime           The time to adjust, defaults to the
5594     *                                    current instance.
5595     */
5596    adjust: function icaltime_adjust(aExtraDays, aExtraHours,
5597                                     aExtraMinutes, aExtraSeconds, aTime) {
5598
5599      var minutesOverflow, hoursOverflow,
5600          daysOverflow = 0, yearsOverflow = 0;
5601
5602      var second, minute, hour, day;
5603      var daysInMonth;
5604
5605      var time = aTime || this._time;
5606
5607      if (!time.isDate) {
5608        second = time.second + aExtraSeconds;
5609        time.second = second % 60;
5610        minutesOverflow = ICAL.helpers.trunc(second / 60);
5611        if (time.second < 0) {
5612          time.second += 60;
5613          minutesOverflow--;
5614        }
5615
5616        minute = time.minute + aExtraMinutes + minutesOverflow;
5617        time.minute = minute % 60;
5618        hoursOverflow = ICAL.helpers.trunc(minute / 60);
5619        if (time.minute < 0) {
5620          time.minute += 60;
5621          hoursOverflow--;
5622        }
5623
5624        hour = time.hour + aExtraHours + hoursOverflow;
5625
5626        time.hour = hour % 24;
5627        daysOverflow = ICAL.helpers.trunc(hour / 24);
5628        if (time.hour < 0) {
5629          time.hour += 24;
5630          daysOverflow--;
5631        }
5632      }
5633
5634
5635      // Adjust month and year first, because we need to know what month the day
5636      // is in before adjusting it.
5637      if (time.month > 12) {
5638        yearsOverflow = ICAL.helpers.trunc((time.month - 1) / 12);
5639      } else if (time.month < 1) {
5640        yearsOverflow = ICAL.helpers.trunc(time.month / 12) - 1;
5641      }
5642
5643      time.year += yearsOverflow;
5644      time.month -= 12 * yearsOverflow;
5645
5646      // Now take care of the days (and adjust month if needed)
5647      day = time.day + aExtraDays + daysOverflow;
5648
5649      if (day > 0) {
5650        for (;;) {
5651          daysInMonth = ICAL.Time.daysInMonth(time.month, time.year);
5652          if (day <= daysInMonth) {
5653            break;
5654          }
5655
5656          time.month++;
5657          if (time.month > 12) {
5658            time.year++;
5659            time.month = 1;
5660          }
5661
5662          day -= daysInMonth;
5663        }
5664      } else {
5665        while (day <= 0) {
5666          if (time.month == 1) {
5667            time.year--;
5668            time.month = 12;
5669          } else {
5670            time.month--;
5671          }
5672
5673          day += ICAL.Time.daysInMonth(time.month, time.year);
5674        }
5675      }
5676
5677      time.day = day;
5678
5679      this._cachedUnixTime = null;
5680      return this;
5681    },
5682
5683    /**
5684     * Sets up the current instance from unix time, the number of seconds since
5685     * January 1st, 1970.
5686     *
5687     * @param {Number} seconds      The seconds to set up with
5688     */
5689    fromUnixTime: function fromUnixTime(seconds) {
5690      this.zone = ICAL.Timezone.utcTimezone;
5691      var epoch = ICAL.Time.epochTime.clone();
5692      epoch.adjust(0, 0, 0, seconds);
5693
5694      this.year = epoch.year;
5695      this.month = epoch.month;
5696      this.day = epoch.day;
5697      this.hour = epoch.hour;
5698      this.minute = epoch.minute;
5699      this.second = Math.floor(epoch.second);
5700
5701      this._cachedUnixTime = null;
5702    },
5703
5704    /**
5705     * Converts the current instance to seconds since January 1st 1970.
5706     *
5707     * @return {Number}         Seconds since 1970
5708     */
5709    toUnixTime: function toUnixTime() {
5710      if (this._cachedUnixTime !== null) {
5711        return this._cachedUnixTime;
5712      }
5713      var offset = this.utcOffset();
5714
5715      // we use the offset trick to ensure
5716      // that we are getting the actual UTC time
5717      var ms = Date.UTC(
5718        this.year,
5719        this.month - 1,
5720        this.day,
5721        this.hour,
5722        this.minute,
5723        this.second - offset
5724      );
5725
5726      // seconds
5727      this._cachedUnixTime = ms / 1000;
5728      return this._cachedUnixTime;
5729    },
5730
5731    /**
5732     * Converts time to into Object which can be serialized then re-created
5733     * using the constructor.
5734     *
5735     * @example
5736     * // toJSON will automatically be called
5737     * var json = JSON.stringify(mytime);
5738     *
5739     * var deserialized = JSON.parse(json);
5740     *
5741     * var time = new ICAL.Time(deserialized);
5742     *
5743     * @return {Object}
5744     */
5745    toJSON: function() {
5746      var copy = [
5747        'year',
5748        'month',
5749        'day',
5750        'hour',
5751        'minute',
5752        'second',
5753        'isDate'
5754      ];
5755
5756      var result = Object.create(null);
5757
5758      var i = 0;
5759      var len = copy.length;
5760      var prop;
5761
5762      for (; i < len; i++) {
5763        prop = copy[i];
5764        result[prop] = this[prop];
5765      }
5766
5767      if (this.zone) {
5768        result.timezone = this.zone.tzid;
5769      }
5770
5771      return result;
5772    }
5773
5774  };
5775
5776  (function setupNormalizeAttributes() {
5777    // This needs to run before any instances are created!
5778    function defineAttr(attr) {
5779      Object.defineProperty(ICAL.Time.prototype, attr, {
5780        get: function getTimeAttr() {
5781          if (this._pendingNormalization) {
5782            this._normalize();
5783            this._pendingNormalization = false;
5784          }
5785
5786          return this._time[attr];
5787        },
5788        set: function setTimeAttr(val) {
5789          // Check if isDate will be set and if was not set to normalize date.
5790          // This avoids losing days when seconds, minutes and hours are zeroed
5791          // what normalize will do when time is a date.
5792          if (attr === "isDate" && val && !this._time.isDate) {
5793            this.adjust(0, 0, 0, 0);
5794          }
5795          this._cachedUnixTime = null;
5796          this._pendingNormalization = true;
5797          this._time[attr] = val;
5798
5799          return val;
5800        }
5801      });
5802
5803    }
5804
5805    /* istanbul ignore else */
5806    if ("defineProperty" in Object) {
5807      defineAttr("year");
5808      defineAttr("month");
5809      defineAttr("day");
5810      defineAttr("hour");
5811      defineAttr("minute");
5812      defineAttr("second");
5813      defineAttr("isDate");
5814    }
5815  })();
5816
5817  /**
5818   * Returns the days in the given month
5819   *
5820   * @param {Number} month      The month to check
5821   * @param {Number} year       The year to check
5822   * @return {Number}           The number of days in the month
5823   */
5824  ICAL.Time.daysInMonth = function icaltime_daysInMonth(month, year) {
5825    var _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
5826    var days = 30;
5827
5828    if (month < 1 || month > 12) return days;
5829
5830    days = _daysInMonth[month];
5831
5832    if (month == 2) {
5833      days += ICAL.Time.isLeapYear(year);
5834    }
5835
5836    return days;
5837  };
5838
5839  /**
5840   * Checks if the year is a leap year
5841   *
5842   * @param {Number} year       The year to check
5843   * @return {Boolean}          True, if the year is a leap year
5844   */
5845  ICAL.Time.isLeapYear = function isLeapYear(year) {
5846    if (year <= 1752) {
5847      return ((year % 4) == 0);
5848    } else {
5849      return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0));
5850    }
5851  };
5852
5853  /**
5854   * Create a new ICAL.Time from the day of year and year. The date is returned
5855   * in floating timezone.
5856   *
5857   * @param {Number} aDayOfYear     The day of year
5858   * @param {Number} aYear          The year to create the instance in
5859   * @return {ICAL.Time}            The created instance with the calculated date
5860   */
5861  ICAL.Time.fromDayOfYear = function icaltime_fromDayOfYear(aDayOfYear, aYear) {
5862    var year = aYear;
5863    var doy = aDayOfYear;
5864    var tt = new ICAL.Time();
5865    tt.auto_normalize = false;
5866    var is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0);
5867
5868    if (doy < 1) {
5869      year--;
5870      is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0);
5871      doy += ICAL.Time.daysInYearPassedMonth[is_leap][12];
5872      return ICAL.Time.fromDayOfYear(doy, year);
5873    } else if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][12]) {
5874      is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0);
5875      doy -= ICAL.Time.daysInYearPassedMonth[is_leap][12];
5876      year++;
5877      return ICAL.Time.fromDayOfYear(doy, year);
5878    }
5879
5880    tt.year = year;
5881    tt.isDate = true;
5882
5883    for (var month = 11; month >= 0; month--) {
5884      if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][month]) {
5885        tt.month = month + 1;
5886        tt.day = doy - ICAL.Time.daysInYearPassedMonth[is_leap][month];
5887        break;
5888      }
5889    }
5890
5891    tt.auto_normalize = true;
5892    return tt;
5893  };
5894
5895  /**
5896   * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02.
5897   *
5898   * @deprecated                Use {@link ICAL.Time.fromDateString} instead
5899   * @param {String} str        The string to create from
5900   * @return {ICAL.Time}        The date/time instance
5901   */
5902  ICAL.Time.fromStringv2 = function fromString(str) {
5903    return new ICAL.Time({
5904      year: parseInt(str.substr(0, 4), 10),
5905      month: parseInt(str.substr(5, 2), 10),
5906      day: parseInt(str.substr(8, 2), 10),
5907      isDate: true
5908    });
5909  };
5910
5911  /**
5912   * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02.
5913   *
5914   * @param {String} aValue     The string to create from
5915   * @return {ICAL.Time}        The date/time instance
5916   */
5917  ICAL.Time.fromDateString = function(aValue) {
5918    // Dates should have no timezone.
5919    // Google likes to sometimes specify Z on dates
5920    // we specifically ignore that to avoid issues.
5921
5922    // YYYY-MM-DD
5923    // 2012-10-10
5924    return new ICAL.Time({
5925      year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)),
5926      month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)),
5927      day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)),
5928      isDate: true
5929    });
5930  };
5931
5932  /**
5933   * Returns a new ICAL.Time instance from a date-time string, e.g
5934   * 2015-01-02T03:04:05. If a property is specified, the timezone is set up
5935   * from the property's TZID parameter.
5936   *
5937   * @param {String} aValue         The string to create from
5938   * @param {ICAL.Property=} prop   The property the date belongs to
5939   * @return {ICAL.Time}            The date/time instance
5940   */
5941  ICAL.Time.fromDateTimeString = function(aValue, prop) {
5942    if (aValue.length < 19) {
5943      throw new Error(
5944        'invalid date-time value: "' + aValue + '"'
5945      );
5946    }
5947
5948    var zone;
5949
5950    if (aValue[19] && aValue[19] === 'Z') {
5951      zone = 'Z';
5952    } else if (prop) {
5953      zone = prop.getParameter('tzid');
5954    }
5955
5956    // 2012-10-10T10:10:10(Z)?
5957    var time = new ICAL.Time({
5958      year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)),
5959      month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)),
5960      day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)),
5961      hour: ICAL.helpers.strictParseInt(aValue.substr(11, 2)),
5962      minute: ICAL.helpers.strictParseInt(aValue.substr(14, 2)),
5963      second: ICAL.helpers.strictParseInt(aValue.substr(17, 2)),
5964      timezone: zone
5965    });
5966
5967    return time;
5968  };
5969
5970  /**
5971   * Returns a new ICAL.Time instance from a date or date-time string,
5972   *
5973   * @param {String} aValue         The string to create from
5974   * @param {ICAL.Property=} prop   The property the date belongs to
5975   * @return {ICAL.Time}            The date/time instance
5976   */
5977  ICAL.Time.fromString = function fromString(aValue, aProperty) {
5978    if (aValue.length > 10) {
5979      return ICAL.Time.fromDateTimeString(aValue, aProperty);
5980    } else {
5981      return ICAL.Time.fromDateString(aValue);
5982    }
5983  };
5984
5985  /**
5986   * Creates a new ICAL.Time instance from the given Javascript Date.
5987   *
5988   * @param {?Date} aDate     The Javascript Date to read, or null to reset
5989   * @param {Boolean} useUTC  If true, the UTC values of the date will be used
5990   */
5991  ICAL.Time.fromJSDate = function fromJSDate(aDate, useUTC) {
5992    var tt = new ICAL.Time();
5993    return tt.fromJSDate(aDate, useUTC);
5994  };
5995
5996  /**
5997   * Creates a new ICAL.Time instance from the the passed data object.
5998   *
5999   * @param {Object} aData            Time initialization
6000   * @param {Number=} aData.year      The year for this date
6001   * @param {Number=} aData.month     The month for this date
6002   * @param {Number=} aData.day       The day for this date
6003   * @param {Number=} aData.hour      The hour for this date
6004   * @param {Number=} aData.minute    The minute for this date
6005   * @param {Number=} aData.second    The second for this date
6006   * @param {Boolean=} aData.isDate   If true, the instance represents a date
6007   *                                    (as opposed to a date-time)
6008   * @param {ICAL.Timezone=} aZone    Timezone this position occurs in
6009   */
6010  ICAL.Time.fromData = function fromData(aData, aZone) {
6011    var t = new ICAL.Time();
6012    return t.fromData(aData, aZone);
6013  };
6014
6015  /**
6016   * Creates a new ICAL.Time instance from the current moment.
6017   * @return {ICAL.Time}
6018   */
6019  ICAL.Time.now = function icaltime_now() {
6020    return ICAL.Time.fromJSDate(new Date(), false);
6021  };
6022
6023  /**
6024   * Returns the date on which ISO week number 1 starts.
6025   *
6026   * @see ICAL.Time#weekNumber
6027   * @param {Number} aYear                  The year to search in
6028   * @param {ICAL.Time.weekDay=} aWeekStart The week start weekday, used for calculation.
6029   * @return {ICAL.Time}                    The date on which week number 1 starts
6030   */
6031  ICAL.Time.weekOneStarts = function weekOneStarts(aYear, aWeekStart) {
6032    var t = ICAL.Time.fromData({
6033      year: aYear,
6034      month: 1,
6035      day: 1,
6036      isDate: true
6037    });
6038
6039    var dow = t.dayOfWeek();
6040    var wkst = aWeekStart || ICAL.Time.DEFAULT_WEEK_START;
6041    if (dow > ICAL.Time.THURSDAY) {
6042      t.day += 7;
6043    }
6044    if (wkst > ICAL.Time.THURSDAY) {
6045      t.day -= 7;
6046    }
6047
6048    t.day -= dow - wkst;
6049
6050    return t;
6051  };
6052
6053  /**
6054   * Get the dominical letter for the given year. Letters range from A - G for
6055   * common years, and AG to GF for leap years.
6056   *
6057   * @param {Number} yr           The year to retrieve the letter for
6058   * @return {String}             The dominical letter.
6059   */
6060  ICAL.Time.getDominicalLetter = function(yr) {
6061    var LTRS = "GFEDCBA";
6062    var dom = (yr + (yr / 4 | 0) + (yr / 400 | 0) - (yr / 100 | 0) - 1) % 7;
6063    var isLeap = ICAL.Time.isLeapYear(yr);
6064    if (isLeap) {
6065      return LTRS[(dom + 6) % 7] + LTRS[dom];
6066    } else {
6067      return LTRS[dom];
6068    }
6069  };
6070
6071  /**
6072   * January 1st, 1970 as an ICAL.Time.
6073   * @type {ICAL.Time}
6074   * @constant
6075   * @instance
6076   */
6077  ICAL.Time.epochTime = ICAL.Time.fromData({
6078    year: 1970,
6079    month: 1,
6080    day: 1,
6081    hour: 0,
6082    minute: 0,
6083    second: 0,
6084    isDate: false,
6085    timezone: "Z"
6086  });
6087
6088  ICAL.Time._cmp_attr = function _cmp_attr(a, b, attr) {
6089    if (a[attr] > b[attr]) return 1;
6090    if (a[attr] < b[attr]) return -1;
6091    return 0;
6092  };
6093
6094  /**
6095   * The days that have passed in the year after a given month. The array has
6096   * two members, one being an array of passed days for non-leap years, the
6097   * other analog for leap years.
6098   * @example
6099   * var isLeapYear = ICAL.Time.isLeapYear(year);
6100   * var passedDays = ICAL.Time.daysInYearPassedMonth[isLeapYear][month];
6101   * @type {Array.<Array.<Number>>}
6102   */
6103  ICAL.Time.daysInYearPassedMonth = [
6104    [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365],
6105    [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
6106  ];
6107
6108  /**
6109   * The weekday, 1 = SUNDAY, 7 = SATURDAY. Access via
6110   * ICAL.Time.MONDAY, ICAL.Time.TUESDAY, ...
6111   *
6112   * @typedef {Number} weekDay
6113   * @memberof ICAL.Time
6114   */
6115
6116  ICAL.Time.SUNDAY = 1;
6117  ICAL.Time.MONDAY = 2;
6118  ICAL.Time.TUESDAY = 3;
6119  ICAL.Time.WEDNESDAY = 4;
6120  ICAL.Time.THURSDAY = 5;
6121  ICAL.Time.FRIDAY = 6;
6122  ICAL.Time.SATURDAY = 7;
6123
6124  /**
6125   * The default weekday for the WKST part.
6126   * @constant
6127   * @default ICAL.Time.MONDAY
6128   */
6129  ICAL.Time.DEFAULT_WEEK_START = ICAL.Time.MONDAY;
6130})();
6131/* This Source Code Form is subject to the terms of the Mozilla Public
6132 * License, v. 2.0. If a copy of the MPL was not distributed with this
6133 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6134 * Portions Copyright (C) Philipp Kewisch, 2015 */
6135
6136
6137
6138(function() {
6139
6140  /**
6141   * Describes a vCard time, which has slight differences to the ICAL.Time.
6142   * Properties can be null if not specified, for example for dates with
6143   * reduced accuracy or truncation.
6144   *
6145   * Note that currently not all methods are correctly re-implemented for
6146   * VCardTime. For example, comparison will have undefined results when some
6147   * members are null.
6148   *
6149   * Also, normalization is not yet implemented for this class!
6150   *
6151   * @alias ICAL.VCardTime
6152   * @class
6153   * @extends {ICAL.Time}
6154   * @param {Object} data                           The data for the time instance
6155   * @param {Number=} data.year                     The year for this date
6156   * @param {Number=} data.month                    The month for this date
6157   * @param {Number=} data.day                      The day for this date
6158   * @param {Number=} data.hour                     The hour for this date
6159   * @param {Number=} data.minute                   The minute for this date
6160   * @param {Number=} data.second                   The second for this date
6161   * @param {ICAL.Timezone|ICAL.UtcOffset} zone     The timezone to use
6162   * @param {String} icaltype                       The type for this date/time object
6163   */
6164  ICAL.VCardTime = function(data, zone, icaltype) {
6165    this.wrappedJSObject = this;
6166    var time = this._time = Object.create(null);
6167
6168    time.year = null;
6169    time.month = null;
6170    time.day = null;
6171    time.hour = null;
6172    time.minute = null;
6173    time.second = null;
6174
6175    this.icaltype = icaltype || "date-and-or-time";
6176
6177    this.fromData(data, zone);
6178  };
6179  ICAL.helpers.inherits(ICAL.Time, ICAL.VCardTime, /** @lends ICAL.VCardTime */ {
6180
6181    /**
6182     * The class identifier.
6183     * @constant
6184     * @type {String}
6185     * @default "vcardtime"
6186     */
6187    icalclass: "vcardtime",
6188
6189    /**
6190     * The type name, to be used in the jCal object.
6191     * @type {String}
6192     * @default "date-and-or-time"
6193     */
6194    icaltype: "date-and-or-time",
6195
6196    /**
6197     * The timezone. This can either be floating, UTC, or an instance of
6198     * ICAL.UtcOffset.
6199     * @type {ICAL.Timezone|ICAL.UtcOFfset}
6200     */
6201    zone: null,
6202
6203    /**
6204     * Returns a clone of the vcard date/time object.
6205     *
6206     * @return {ICAL.VCardTime}     The cloned object
6207     */
6208    clone: function() {
6209      return new ICAL.VCardTime(this._time, this.zone, this.icaltype);
6210    },
6211
6212    _normalize: function() {
6213      return this;
6214    },
6215
6216    /**
6217     * @inheritdoc
6218     */
6219    utcOffset: function() {
6220      if (this.zone instanceof ICAL.UtcOffset) {
6221        return this.zone.toSeconds();
6222      } else {
6223        return ICAL.Time.prototype.utcOffset.apply(this, arguments);
6224      }
6225    },
6226
6227    /**
6228     * Returns an RFC 6350 compliant representation of this object.
6229     *
6230     * @return {String}         vcard date/time string
6231     */
6232    toICALString: function() {
6233      return ICAL.design.vcard.value[this.icaltype].toICAL(this.toString());
6234    },
6235
6236    /**
6237     * The string representation of this date/time, in jCard form
6238     * (including : and - separators).
6239     * @return {String}
6240     */
6241    toString: function toString() {
6242      var p2 = ICAL.helpers.pad2;
6243      var y = this.year, m = this.month, d = this.day;
6244      var h = this.hour, mm = this.minute, s = this.second;
6245
6246      var hasYear = y !== null, hasMonth = m !== null, hasDay = d !== null;
6247      var hasHour = h !== null, hasMinute = mm !== null, hasSecond = s !== null;
6248
6249      var datepart = (hasYear ? p2(y) + (hasMonth || hasDay ? '-' : '') : (hasMonth || hasDay ? '--' : '')) +
6250                     (hasMonth ? p2(m) : '') +
6251                     (hasDay ? '-' + p2(d) : '');
6252      var timepart = (hasHour ? p2(h) : '-') + (hasHour && hasMinute ? ':' : '') +
6253                     (hasMinute ? p2(mm) : '') + (!hasHour && !hasMinute ? '-' : '') +
6254                     (hasMinute && hasSecond ? ':' : '') +
6255                     (hasSecond ? p2(s) : '');
6256
6257      var zone;
6258      if (this.zone === ICAL.Timezone.utcTimezone) {
6259        zone = 'Z';
6260      } else if (this.zone instanceof ICAL.UtcOffset) {
6261        zone = this.zone.toString();
6262      } else if (this.zone === ICAL.Timezone.localTimezone) {
6263        zone = '';
6264      } else if (this.zone instanceof ICAL.Timezone) {
6265        var offset = ICAL.UtcOffset.fromSeconds(this.zone.utcOffset(this));
6266        zone = offset.toString();
6267      } else {
6268        zone = '';
6269      }
6270
6271      switch (this.icaltype) {
6272        case "time":
6273          return timepart + zone;
6274        case "date-and-or-time":
6275        case "date-time":
6276          return datepart + (timepart == '--' ? '' : 'T' + timepart + zone);
6277        case "date":
6278          return datepart;
6279      }
6280      return null;
6281    }
6282  });
6283
6284  /**
6285   * Returns a new ICAL.VCardTime instance from a date and/or time string.
6286   *
6287   * @param {String} aValue     The string to create from
6288   * @param {String} aIcalType  The type for this instance, e.g. date-and-or-time
6289   * @return {ICAL.VCardTime}   The date/time instance
6290   */
6291  ICAL.VCardTime.fromDateAndOrTimeString = function(aValue, aIcalType) {
6292    function part(v, s, e) {
6293      return v ? ICAL.helpers.strictParseInt(v.substr(s, e)) : null;
6294    }
6295    var parts = aValue.split('T');
6296    var dt = parts[0], tmz = parts[1];
6297    var splitzone = tmz ? ICAL.design.vcard.value.time._splitZone(tmz) : [];
6298    var zone = splitzone[0], tm = splitzone[1];
6299
6300    var stoi = ICAL.helpers.strictParseInt;
6301    var dtlen = dt ? dt.length : 0;
6302    var tmlen = tm ? tm.length : 0;
6303
6304    var hasDashDate = dt && dt[0] == '-' && dt[1] == '-';
6305    var hasDashTime = tm && tm[0] == '-';
6306
6307    var o = {
6308      year: hasDashDate ? null : part(dt, 0, 4),
6309      month: hasDashDate && (dtlen == 4 || dtlen == 7) ? part(dt, 2, 2) : dtlen == 7 ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 5, 2) : null,
6310      day: dtlen == 5 ? part(dt, 3, 2) : dtlen == 7 && hasDashDate ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 8, 2) : null,
6311
6312      hour: hasDashTime ? null : part(tm, 0, 2),
6313      minute: hasDashTime && tmlen == 3 ? part(tm, 1, 2) : tmlen > 4 ? hasDashTime ? part(tm, 1, 2) : part(tm, 3, 2) : null,
6314      second: tmlen == 4 ? part(tm, 2, 2) : tmlen == 6 ? part(tm, 4, 2) : tmlen == 8 ? part(tm, 6, 2) : null
6315    };
6316
6317    if (zone == 'Z') {
6318      zone = ICAL.Timezone.utcTimezone;
6319    } else if (zone && zone[3] == ':') {
6320      zone = ICAL.UtcOffset.fromString(zone);
6321    } else {
6322      zone = null;
6323    }
6324
6325    return new ICAL.VCardTime(o, zone, aIcalType);
6326  };
6327})();
6328/* This Source Code Form is subject to the terms of the Mozilla Public
6329 * License, v. 2.0. If a copy of the MPL was not distributed with this
6330 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6331 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
6332
6333
6334
6335(function() {
6336  var DOW_MAP = {
6337    SU: ICAL.Time.SUNDAY,
6338    MO: ICAL.Time.MONDAY,
6339    TU: ICAL.Time.TUESDAY,
6340    WE: ICAL.Time.WEDNESDAY,
6341    TH: ICAL.Time.THURSDAY,
6342    FR: ICAL.Time.FRIDAY,
6343    SA: ICAL.Time.SATURDAY
6344  };
6345
6346  var REVERSE_DOW_MAP = {};
6347  for (var key in DOW_MAP) {
6348    /* istanbul ignore else */
6349    if (DOW_MAP.hasOwnProperty(key)) {
6350      REVERSE_DOW_MAP[DOW_MAP[key]] = key;
6351    }
6352  }
6353
6354  var COPY_PARTS = ["BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY",
6355                    "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO",
6356                    "BYMONTH", "BYSETPOS"];
6357
6358  /**
6359   * @classdesc
6360   * This class represents the "recur" value type, with various calculation
6361   * and manipulation methods.
6362   *
6363   * @class
6364   * @alias ICAL.Recur
6365   * @param {Object} data                               An object with members of the recurrence
6366   * @param {ICAL.Recur.frequencyValues=} data.freq     The frequency value
6367   * @param {Number=} data.interval                     The INTERVAL value
6368   * @param {ICAL.Time.weekDay=} data.wkst              The week start value
6369   * @param {ICAL.Time=} data.until                     The end of the recurrence set
6370   * @param {Number=} data.count                        The number of occurrences
6371   * @param {Array.<Number>=} data.bysecond             The seconds for the BYSECOND part
6372   * @param {Array.<Number>=} data.byminute             The minutes for the BYMINUTE part
6373   * @param {Array.<Number>=} data.byhour               The hours for the BYHOUR part
6374   * @param {Array.<String>=} data.byday                The BYDAY values
6375   * @param {Array.<Number>=} data.bymonthday           The days for the BYMONTHDAY part
6376   * @param {Array.<Number>=} data.byyearday            The days for the BYYEARDAY part
6377   * @param {Array.<Number>=} data.byweekno             The weeks for the BYWEEKNO part
6378   * @param {Array.<Number>=} data.bymonth              The month for the BYMONTH part
6379   * @param {Array.<Number>=} data.bysetpos             The positionals for the BYSETPOS part
6380   */
6381  ICAL.Recur = function icalrecur(data) {
6382    this.wrappedJSObject = this;
6383    this.parts = {};
6384
6385    if (data && typeof(data) === 'object') {
6386      this.fromData(data);
6387    }
6388  };
6389
6390  ICAL.Recur.prototype = {
6391    /**
6392     * An object holding the BY-parts of the recurrence rule
6393     * @type {Object}
6394     */
6395    parts: null,
6396
6397    /**
6398     * The interval value for the recurrence rule.
6399     * @type {Number}
6400     */
6401    interval: 1,
6402
6403    /**
6404     * The week start day
6405     *
6406     * @type {ICAL.Time.weekDay}
6407     * @default ICAL.Time.MONDAY
6408     */
6409    wkst: ICAL.Time.MONDAY,
6410
6411    /**
6412     * The end of the recurrence
6413     * @type {?ICAL.Time}
6414     */
6415    until: null,
6416
6417    /**
6418     * The maximum number of occurrences
6419     * @type {?Number}
6420     */
6421    count: null,
6422
6423    /**
6424     * The frequency value.
6425     * @type {ICAL.Recur.frequencyValues}
6426     */
6427    freq: null,
6428
6429    /**
6430     * The class identifier.
6431     * @constant
6432     * @type {String}
6433     * @default "icalrecur"
6434     */
6435    icalclass: "icalrecur",
6436
6437    /**
6438     * The type name, to be used in the jCal object.
6439     * @constant
6440     * @type {String}
6441     * @default "recur"
6442     */
6443    icaltype: "recur",
6444
6445    /**
6446     * Create a new iterator for this recurrence rule. The passed start date
6447     * must be the start date of the event, not the start of the range to
6448     * search in.
6449     *
6450     * @example
6451     * var recur = comp.getFirstPropertyValue('rrule');
6452     * var dtstart = comp.getFirstPropertyValue('dtstart');
6453     * var iter = recur.iterator(dtstart);
6454     * for (var next = iter.next(); next; next = iter.next()) {
6455     *   if (next.compare(rangeStart) < 0) {
6456     *     continue;
6457     *   }
6458     *   console.log(next.toString());
6459     * }
6460     *
6461     * @param {ICAL.Time} aStart        The item's start date
6462     * @return {ICAL.RecurIterator}     The recurrence iterator
6463     */
6464    iterator: function(aStart) {
6465      return new ICAL.RecurIterator({
6466        rule: this,
6467        dtstart: aStart
6468      });
6469    },
6470
6471    /**
6472     * Returns a clone of the recurrence object.
6473     *
6474     * @return {ICAL.Recur}      The cloned object
6475     */
6476    clone: function clone() {
6477      return new ICAL.Recur(this.toJSON());
6478    },
6479
6480    /**
6481     * Checks if the current rule is finite, i.e. has a count or until part.
6482     *
6483     * @return {Boolean}        True, if the rule is finite
6484     */
6485    isFinite: function isfinite() {
6486      return !!(this.count || this.until);
6487    },
6488
6489    /**
6490     * Checks if the current rule has a count part, and not limited by an until
6491     * part.
6492     *
6493     * @return {Boolean}        True, if the rule is by count
6494     */
6495    isByCount: function isbycount() {
6496      return !!(this.count && !this.until);
6497    },
6498
6499    /**
6500     * Adds a component (part) to the recurrence rule. This is not a component
6501     * in the sense of {@link ICAL.Component}, but a part of the recurrence
6502     * rule, i.e. BYMONTH.
6503     *
6504     * @param {String} aType            The name of the component part
6505     * @param {Array|String} aValue     The component value
6506     */
6507    addComponent: function addPart(aType, aValue) {
6508      var ucname = aType.toUpperCase();
6509      if (ucname in this.parts) {
6510        this.parts[ucname].push(aValue);
6511      } else {
6512        this.parts[ucname] = [aValue];
6513      }
6514    },
6515
6516    /**
6517     * Sets the component value for the given by-part.
6518     *
6519     * @param {String} aType        The component part name
6520     * @param {Array} aValues       The component values
6521     */
6522    setComponent: function setComponent(aType, aValues) {
6523      this.parts[aType.toUpperCase()] = aValues.slice();
6524    },
6525
6526    /**
6527     * Gets (a copy) of the requested component value.
6528     *
6529     * @param {String} aType        The component part name
6530     * @return {Array}              The component part value
6531     */
6532    getComponent: function getComponent(aType) {
6533      var ucname = aType.toUpperCase();
6534      return (ucname in this.parts ? this.parts[ucname].slice() : []);
6535    },
6536
6537    /**
6538     * Retrieves the next occurrence after the given recurrence id. See the
6539     * guide on {@tutorial terminology} for more details.
6540     *
6541     * NOTE: Currently, this method iterates all occurrences from the start
6542     * date. It should not be called in a loop for performance reasons. If you
6543     * would like to get more than one occurrence, you can iterate the
6544     * occurrences manually, see the example on the
6545     * {@link ICAL.Recur#iterator iterator} method.
6546     *
6547     * @param {ICAL.Time} aStartTime        The start of the event series
6548     * @param {ICAL.Time} aRecurrenceId     The date of the last occurrence
6549     * @return {ICAL.Time}                  The next occurrence after
6550     */
6551    getNextOccurrence: function getNextOccurrence(aStartTime, aRecurrenceId) {
6552      var iter = this.iterator(aStartTime);
6553      var next, cdt;
6554
6555      do {
6556        next = iter.next();
6557      } while (next && next.compare(aRecurrenceId) <= 0);
6558
6559      if (next && aRecurrenceId.zone) {
6560        next.zone = aRecurrenceId.zone;
6561      }
6562
6563      return next;
6564    },
6565
6566    /**
6567     * Sets up the current instance using members from the passed data object.
6568     *
6569     * @param {Object} data                               An object with members of the recurrence
6570     * @param {ICAL.Recur.frequencyValues=} data.freq     The frequency value
6571     * @param {Number=} data.interval                     The INTERVAL value
6572     * @param {ICAL.Time.weekDay=} data.wkst              The week start value
6573     * @param {ICAL.Time=} data.until                     The end of the recurrence set
6574     * @param {Number=} data.count                        The number of occurrences
6575     * @param {Array.<Number>=} data.bysecond             The seconds for the BYSECOND part
6576     * @param {Array.<Number>=} data.byminute             The minutes for the BYMINUTE part
6577     * @param {Array.<Number>=} data.byhour               The hours for the BYHOUR part
6578     * @param {Array.<String>=} data.byday                The BYDAY values
6579     * @param {Array.<Number>=} data.bymonthday           The days for the BYMONTHDAY part
6580     * @param {Array.<Number>=} data.byyearday            The days for the BYYEARDAY part
6581     * @param {Array.<Number>=} data.byweekno             The weeks for the BYWEEKNO part
6582     * @param {Array.<Number>=} data.bymonth              The month for the BYMONTH part
6583     * @param {Array.<Number>=} data.bysetpos             The positionals for the BYSETPOS part
6584     */
6585    fromData: function(data) {
6586      for (var key in data) {
6587        var uckey = key.toUpperCase();
6588
6589        if (uckey in partDesign) {
6590          if (Array.isArray(data[key])) {
6591            this.parts[uckey] = data[key];
6592          } else {
6593            this.parts[uckey] = [data[key]];
6594          }
6595        } else {
6596          this[key] = data[key];
6597        }
6598      }
6599
6600      if (this.interval && typeof this.interval != "number") {
6601        optionDesign.INTERVAL(this.interval, this);
6602      }
6603
6604      if (this.wkst && typeof this.wkst != "number") {
6605        this.wkst = ICAL.Recur.icalDayToNumericDay(this.wkst);
6606      }
6607
6608      if (this.until && !(this.until instanceof ICAL.Time)) {
6609        this.until = ICAL.Time.fromString(this.until);
6610      }
6611    },
6612
6613    /**
6614     * The jCal representation of this recurrence type.
6615     * @return {Object}
6616     */
6617    toJSON: function() {
6618      var res = Object.create(null);
6619      res.freq = this.freq;
6620
6621      if (this.count) {
6622        res.count = this.count;
6623      }
6624
6625      if (this.interval > 1) {
6626        res.interval = this.interval;
6627      }
6628
6629      for (var k in this.parts) {
6630        /* istanbul ignore if */
6631        if (!this.parts.hasOwnProperty(k)) {
6632          continue;
6633        }
6634        var kparts = this.parts[k];
6635        if (Array.isArray(kparts) && kparts.length == 1) {
6636          res[k.toLowerCase()] = kparts[0];
6637        } else {
6638          res[k.toLowerCase()] = ICAL.helpers.clone(this.parts[k]);
6639        }
6640      }
6641
6642      if (this.until) {
6643        res.until = this.until.toString();
6644      }
6645      if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) {
6646        res.wkst = ICAL.Recur.numericDayToIcalDay(this.wkst);
6647      }
6648      return res;
6649    },
6650
6651    /**
6652     * The string representation of this recurrence rule.
6653     * @return {String}
6654     */
6655    toString: function icalrecur_toString() {
6656      // TODO retain order
6657      var str = "FREQ=" + this.freq;
6658      if (this.count) {
6659        str += ";COUNT=" + this.count;
6660      }
6661      if (this.interval > 1) {
6662        str += ";INTERVAL=" + this.interval;
6663      }
6664      for (var k in this.parts) {
6665        /* istanbul ignore else */
6666        if (this.parts.hasOwnProperty(k)) {
6667          str += ";" + k + "=" + this.parts[k];
6668        }
6669      }
6670      if (this.until) {
6671        str += ';UNTIL=' + this.until.toICALString();
6672      }
6673      if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) {
6674        str += ';WKST=' + ICAL.Recur.numericDayToIcalDay(this.wkst);
6675      }
6676      return str;
6677    }
6678  };
6679
6680  function parseNumericValue(type, min, max, value) {
6681    var result = value;
6682
6683    if (value[0] === '+') {
6684      result = value.substr(1);
6685    }
6686
6687    result = ICAL.helpers.strictParseInt(result);
6688
6689    if (min !== undefined && value < min) {
6690      throw new Error(
6691        type + ': invalid value "' + value + '" must be > ' + min
6692      );
6693    }
6694
6695    if (max !== undefined && value > max) {
6696      throw new Error(
6697        type + ': invalid value "' + value + '" must be < ' + min
6698      );
6699    }
6700
6701    return result;
6702  }
6703
6704  /**
6705   * Convert an ical representation of a day (SU, MO, etc..)
6706   * into a numeric value of that day.
6707   *
6708   * @param {String} string     The iCalendar day name
6709   * @param {ICAL.Time.weekDay=} aWeekStart
6710   *        The week start weekday, defaults to SUNDAY
6711   * @return {Number}           Numeric value of given day
6712   */
6713  ICAL.Recur.icalDayToNumericDay = function toNumericDay(string, aWeekStart) {
6714    //XXX: this is here so we can deal
6715    //     with possibly invalid string values.
6716    var firstDow = aWeekStart || ICAL.Time.SUNDAY;
6717    return ((DOW_MAP[string] - firstDow + 7) % 7) + 1;
6718  };
6719
6720  /**
6721   * Convert a numeric day value into its ical representation (SU, MO, etc..)
6722   *
6723   * @param {Number} num        Numeric value of given day
6724   * @param {ICAL.Time.weekDay=} aWeekStart
6725   *        The week start weekday, defaults to SUNDAY
6726   * @return {String}           The ICAL day value, e.g SU,MO,...
6727   */
6728  ICAL.Recur.numericDayToIcalDay = function toIcalDay(num, aWeekStart) {
6729    //XXX: this is here so we can deal with possibly invalid number values.
6730    //     Also, this allows consistent mapping between day numbers and day
6731    //     names for external users.
6732    var firstDow = aWeekStart || ICAL.Time.SUNDAY;
6733    var dow = (num + firstDow - ICAL.Time.SUNDAY);
6734    if (dow > 7) {
6735      dow -= 7;
6736    }
6737    return REVERSE_DOW_MAP[dow];
6738  };
6739
6740  var VALID_DAY_NAMES = /^(SU|MO|TU|WE|TH|FR|SA)$/;
6741  var VALID_BYDAY_PART = /^([+-])?(5[0-3]|[1-4][0-9]|[1-9])?(SU|MO|TU|WE|TH|FR|SA)$/;
6742
6743  /**
6744   * Possible frequency values for the FREQ part
6745   * (YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY)
6746   *
6747   * @typedef {String} frequencyValues
6748   * @memberof ICAL.Recur
6749   */
6750
6751  var ALLOWED_FREQ = ['SECONDLY', 'MINUTELY', 'HOURLY',
6752                      'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'];
6753
6754  var optionDesign = {
6755    FREQ: function(value, dict, fmtIcal) {
6756      // yes this is actually equal or faster then regex.
6757      // upside here is we can enumerate the valid values.
6758      if (ALLOWED_FREQ.indexOf(value) !== -1) {
6759        dict.freq = value;
6760      } else {
6761        throw new Error(
6762          'invalid frequency "' + value + '" expected: "' +
6763          ALLOWED_FREQ.join(', ') + '"'
6764        );
6765      }
6766    },
6767
6768    COUNT: function(value, dict, fmtIcal) {
6769      dict.count = ICAL.helpers.strictParseInt(value);
6770    },
6771
6772    INTERVAL: function(value, dict, fmtIcal) {
6773      dict.interval = ICAL.helpers.strictParseInt(value);
6774      if (dict.interval < 1) {
6775        // 0 or negative values are not allowed, some engines seem to generate
6776        // it though. Assume 1 instead.
6777        dict.interval = 1;
6778      }
6779    },
6780
6781    UNTIL: function(value, dict, fmtIcal) {
6782      if (value.length > 10) {
6783        dict.until = ICAL.design.icalendar.value['date-time'].fromICAL(value);
6784      } else {
6785        dict.until = ICAL.design.icalendar.value.date.fromICAL(value);
6786      }
6787      if (!fmtIcal) {
6788        dict.until = ICAL.Time.fromString(dict.until);
6789      }
6790    },
6791
6792    WKST: function(value, dict, fmtIcal) {
6793      if (VALID_DAY_NAMES.test(value)) {
6794        dict.wkst = ICAL.Recur.icalDayToNumericDay(value);
6795      } else {
6796        throw new Error('invalid WKST value "' + value + '"');
6797      }
6798    }
6799  };
6800
6801  var partDesign = {
6802    BYSECOND: parseNumericValue.bind(this, 'BYSECOND', 0, 60),
6803    BYMINUTE: parseNumericValue.bind(this, 'BYMINUTE', 0, 59),
6804    BYHOUR: parseNumericValue.bind(this, 'BYHOUR', 0, 23),
6805    BYDAY: function(value) {
6806      if (VALID_BYDAY_PART.test(value)) {
6807        return value;
6808      } else {
6809        throw new Error('invalid BYDAY value "' + value + '"');
6810      }
6811    },
6812    BYMONTHDAY: parseNumericValue.bind(this, 'BYMONTHDAY', -31, 31),
6813    BYYEARDAY: parseNumericValue.bind(this, 'BYYEARDAY', -366, 366),
6814    BYWEEKNO: parseNumericValue.bind(this, 'BYWEEKNO', -53, 53),
6815    BYMONTH: parseNumericValue.bind(this, 'BYMONTH', 0, 12),
6816    BYSETPOS: parseNumericValue.bind(this, 'BYSETPOS', -366, 366)
6817  };
6818
6819
6820  /**
6821   * Creates a new {@link ICAL.Recur} instance from the passed string.
6822   *
6823   * @param {String} string         The string to parse
6824   * @return {ICAL.Recur}           The created recurrence instance
6825   */
6826  ICAL.Recur.fromString = function(string) {
6827    var data = ICAL.Recur._stringToData(string, false);
6828    return new ICAL.Recur(data);
6829  };
6830
6831  /**
6832   * Creates a new {@link ICAL.Recur} instance using members from the passed
6833   * data object.
6834   *
6835   * @param {Object} aData                              An object with members of the recurrence
6836   * @param {ICAL.Recur.frequencyValues=} aData.freq    The frequency value
6837   * @param {Number=} aData.interval                    The INTERVAL value
6838   * @param {ICAL.Time.weekDay=} aData.wkst             The week start value
6839   * @param {ICAL.Time=} aData.until                    The end of the recurrence set
6840   * @param {Number=} aData.count                       The number of occurrences
6841   * @param {Array.<Number>=} aData.bysecond            The seconds for the BYSECOND part
6842   * @param {Array.<Number>=} aData.byminute            The minutes for the BYMINUTE part
6843   * @param {Array.<Number>=} aData.byhour              The hours for the BYHOUR part
6844   * @param {Array.<String>=} aData.byday               The BYDAY values
6845   * @param {Array.<Number>=} aData.bymonthday          The days for the BYMONTHDAY part
6846   * @param {Array.<Number>=} aData.byyearday           The days for the BYYEARDAY part
6847   * @param {Array.<Number>=} aData.byweekno            The weeks for the BYWEEKNO part
6848   * @param {Array.<Number>=} aData.bymonth             The month for the BYMONTH part
6849   * @param {Array.<Number>=} aData.bysetpos            The positionals for the BYSETPOS part
6850   */
6851  ICAL.Recur.fromData = function(aData) {
6852    return new ICAL.Recur(aData);
6853  };
6854
6855  /**
6856   * Converts a recurrence string to a data object, suitable for the fromData
6857   * method.
6858   *
6859   * @param {String} string     The string to parse
6860   * @param {Boolean} fmtIcal   If true, the string is considered to be an
6861   *                              iCalendar string
6862   * @return {ICAL.Recur}       The recurrence instance
6863   */
6864  ICAL.Recur._stringToData = function(string, fmtIcal) {
6865    var dict = Object.create(null);
6866
6867    // split is slower in FF but fast enough.
6868    // v8 however this is faster then manual split?
6869    var values = string.split(';');
6870    var len = values.length;
6871
6872    for (var i = 0; i < len; i++) {
6873      var parts = values[i].split('=');
6874      var ucname = parts[0].toUpperCase();
6875      var lcname = parts[0].toLowerCase();
6876      var name = (fmtIcal ? lcname : ucname);
6877      var value = parts[1];
6878
6879      if (ucname in partDesign) {
6880        var partArr = value.split(',');
6881        var partArrIdx = 0;
6882        var partArrLen = partArr.length;
6883
6884        for (; partArrIdx < partArrLen; partArrIdx++) {
6885          partArr[partArrIdx] = partDesign[ucname](partArr[partArrIdx]);
6886        }
6887        dict[name] = (partArr.length == 1 ? partArr[0] : partArr);
6888      } else if (ucname in optionDesign) {
6889        optionDesign[ucname](value, dict, fmtIcal);
6890      } else {
6891        // Don't swallow unknown values. Just set them as they are.
6892        dict[lcname] = value;
6893      }
6894    }
6895
6896    return dict;
6897  };
6898})();
6899/* This Source Code Form is subject to the terms of the Mozilla Public
6900 * License, v. 2.0. If a copy of the MPL was not distributed with this
6901 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6902 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
6903
6904
6905/**
6906 * This symbol is further described later on
6907 * @ignore
6908 */
6909ICAL.RecurIterator = (function() {
6910
6911  /**
6912   * @classdesc
6913   * An iterator for a single recurrence rule. This class usually doesn't have
6914   * to be instanciated directly, the convenience method
6915   * {@link ICAL.Recur#iterator} can be used.
6916   *
6917   * @description
6918   * The options object may contain additional members when resuming iteration from a previous run
6919   *
6920   * @description
6921   * The options object may contain additional members when resuming iteration
6922   * from a previous run.
6923   *
6924   * @class
6925   * @alias ICAL.RecurIterator
6926   * @param {Object} options                The iterator options
6927   * @param {ICAL.Recur} options.rule       The rule to iterate.
6928   * @param {ICAL.Time} options.dtstart     The start date of the event.
6929   * @param {Boolean=} options.initialized  When true, assume that options are
6930   *        from a previously constructed iterator. Initialization will not be
6931   *        repeated.
6932   */
6933  function icalrecur_iterator(options) {
6934    this.fromData(options);
6935  }
6936
6937  icalrecur_iterator.prototype = {
6938
6939    /**
6940     * True when iteration is finished.
6941     * @type {Boolean}
6942     */
6943    completed: false,
6944
6945    /**
6946     * The rule that is being iterated
6947     * @type {ICAL.Recur}
6948     */
6949    rule: null,
6950
6951    /**
6952     * The start date of the event being iterated.
6953     * @type {ICAL.Time}
6954     */
6955    dtstart: null,
6956
6957    /**
6958     * The last occurrence that was returned from the
6959     * {@link ICAL.RecurIterator#next} method.
6960     * @type {ICAL.Time}
6961     */
6962    last: null,
6963
6964    /**
6965     * The sequence number from the occurrence
6966     * @type {Number}
6967     */
6968    occurrence_number: 0,
6969
6970    /**
6971     * The indices used for the {@link ICAL.RecurIterator#by_data} object.
6972     * @type {Object}
6973     * @private
6974     */
6975    by_indices: null,
6976
6977    /**
6978     * If true, the iterator has already been initialized
6979     * @type {Boolean}
6980     * @private
6981     */
6982    initialized: false,
6983
6984    /**
6985     * The initializd by-data.
6986     * @type {Object}
6987     * @private
6988     */
6989    by_data: null,
6990
6991    /**
6992     * The expanded yeardays
6993     * @type {Array}
6994     * @private
6995     */
6996    days: null,
6997
6998    /**
6999     * The index in the {@link ICAL.RecurIterator#days} array.
7000     * @type {Number}
7001     * @private
7002     */
7003    days_index: 0,
7004
7005    /**
7006     * Initialize the recurrence iterator from the passed data object. This
7007     * method is usually not called directly, you can initialize the iterator
7008     * through the constructor.
7009     *
7010     * @param {Object} options                The iterator options
7011     * @param {ICAL.Recur} options.rule       The rule to iterate.
7012     * @param {ICAL.Time} options.dtstart     The start date of the event.
7013     * @param {Boolean=} options.initialized  When true, assume that options are
7014     *        from a previously constructed iterator. Initialization will not be
7015     *        repeated.
7016     */
7017    fromData: function(options) {
7018      this.rule = ICAL.helpers.formatClassType(options.rule, ICAL.Recur);
7019
7020      if (!this.rule) {
7021        throw new Error('iterator requires a (ICAL.Recur) rule');
7022      }
7023
7024      this.dtstart = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time);
7025
7026      if (!this.dtstart) {
7027        throw new Error('iterator requires a (ICAL.Time) dtstart');
7028      }
7029
7030      if (options.by_data) {
7031        this.by_data = options.by_data;
7032      } else {
7033        this.by_data = ICAL.helpers.clone(this.rule.parts, true);
7034      }
7035
7036      if (options.occurrence_number)
7037        this.occurrence_number = options.occurrence_number;
7038
7039      this.days = options.days || [];
7040      if (options.last) {
7041        this.last = ICAL.helpers.formatClassType(options.last, ICAL.Time);
7042      }
7043
7044      this.by_indices = options.by_indices;
7045
7046      if (!this.by_indices) {
7047        this.by_indices = {
7048          "BYSECOND": 0,
7049          "BYMINUTE": 0,
7050          "BYHOUR": 0,
7051          "BYDAY": 0,
7052          "BYMONTH": 0,
7053          "BYWEEKNO": 0,
7054          "BYMONTHDAY": 0
7055        };
7056      }
7057
7058      this.initialized = options.initialized || false;
7059
7060      if (!this.initialized) {
7061        this.init();
7062      }
7063    },
7064
7065    /**
7066     * Intialize the iterator
7067     * @private
7068     */
7069    init: function icalrecur_iterator_init() {
7070      this.initialized = true;
7071      this.last = this.dtstart.clone();
7072      var parts = this.by_data;
7073
7074      if ("BYDAY" in parts) {
7075        // libical does this earlier when the rule is loaded, but we postpone to
7076        // now so we can preserve the original order.
7077        this.sort_byday_rules(parts.BYDAY);
7078      }
7079
7080      // If the BYYEARDAY appares, no other date rule part may appear
7081      if ("BYYEARDAY" in parts) {
7082        if ("BYMONTH" in parts || "BYWEEKNO" in parts ||
7083            "BYMONTHDAY" in parts || "BYDAY" in parts) {
7084          throw new Error("Invalid BYYEARDAY rule");
7085        }
7086      }
7087
7088      // BYWEEKNO and BYMONTHDAY rule parts may not both appear
7089      if ("BYWEEKNO" in parts && "BYMONTHDAY" in parts) {
7090        throw new Error("BYWEEKNO does not fit to BYMONTHDAY");
7091      }
7092
7093      // For MONTHLY recurrences (FREQ=MONTHLY) neither BYYEARDAY nor
7094      // BYWEEKNO may appear.
7095      if (this.rule.freq == "MONTHLY" &&
7096          ("BYYEARDAY" in parts || "BYWEEKNO" in parts)) {
7097        throw new Error("For MONTHLY recurrences neither BYYEARDAY nor BYWEEKNO may appear");
7098      }
7099
7100      // For WEEKLY recurrences (FREQ=WEEKLY) neither BYMONTHDAY nor
7101      // BYYEARDAY may appear.
7102      if (this.rule.freq == "WEEKLY" &&
7103          ("BYYEARDAY" in parts || "BYMONTHDAY" in parts)) {
7104        throw new Error("For WEEKLY recurrences neither BYMONTHDAY nor BYYEARDAY may appear");
7105      }
7106
7107      // BYYEARDAY may only appear in YEARLY rules
7108      if (this.rule.freq != "YEARLY" && "BYYEARDAY" in parts) {
7109        throw new Error("BYYEARDAY may only appear in YEARLY rules");
7110      }
7111
7112      this.last.second = this.setup_defaults("BYSECOND", "SECONDLY", this.dtstart.second);
7113      this.last.minute = this.setup_defaults("BYMINUTE", "MINUTELY", this.dtstart.minute);
7114      this.last.hour = this.setup_defaults("BYHOUR", "HOURLY", this.dtstart.hour);
7115      this.last.day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day);
7116      this.last.month = this.setup_defaults("BYMONTH", "MONTHLY", this.dtstart.month);
7117
7118      if (this.rule.freq == "WEEKLY") {
7119        if ("BYDAY" in parts) {
7120          var bydayParts = this.ruleDayOfWeek(parts.BYDAY[0], this.rule.wkst);
7121          var pos = bydayParts[0];
7122          var dow = bydayParts[1];
7123          var wkdy = dow - this.last.dayOfWeek(this.rule.wkst);
7124          if ((this.last.dayOfWeek(this.rule.wkst) < dow && wkdy >= 0) || wkdy < 0) {
7125            // Initial time is after first day of BYDAY data
7126            this.last.day += wkdy;
7127          }
7128        } else {
7129          var dayName = ICAL.Recur.numericDayToIcalDay(this.dtstart.dayOfWeek());
7130          parts.BYDAY = [dayName];
7131        }
7132      }
7133
7134      if (this.rule.freq == "YEARLY") {
7135        for (;;) {
7136          this.expand_year_days(this.last.year);
7137          if (this.days.length > 0) {
7138            break;
7139          }
7140          this.increment_year(this.rule.interval);
7141        }
7142
7143        this._nextByYearDay();
7144      }
7145
7146      if (this.rule.freq == "MONTHLY" && this.has_by_data("BYDAY")) {
7147        var tempLast = null;
7148        var initLast = this.last.clone();
7149        var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
7150
7151        // Check every weekday in BYDAY with relative dow and pos.
7152        for (var i in this.by_data.BYDAY) {
7153          /* istanbul ignore if */
7154          if (!this.by_data.BYDAY.hasOwnProperty(i)) {
7155            continue;
7156          }
7157          this.last = initLast.clone();
7158          var bydayParts = this.ruleDayOfWeek(this.by_data.BYDAY[i]);
7159          var pos = bydayParts[0];
7160          var dow = bydayParts[1];
7161          var dayOfMonth = this.last.nthWeekDay(dow, pos);
7162
7163          // If |pos| >= 6, the byday is invalid for a monthly rule.
7164          if (pos >= 6 || pos <= -6) {
7165            throw new Error("Malformed values in BYDAY part");
7166          }
7167
7168          // If a Byday with pos=+/-5 is not in the current month it
7169          // must be searched in the next months.
7170          if (dayOfMonth > daysInMonth || dayOfMonth <= 0) {
7171            // Skip if we have already found a "last" in this month.
7172            if (tempLast && tempLast.month == initLast.month) {
7173              continue;
7174            }
7175            while (dayOfMonth > daysInMonth || dayOfMonth <= 0) {
7176              this.increment_month();
7177              daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
7178              dayOfMonth = this.last.nthWeekDay(dow, pos);
7179            }
7180          }
7181
7182          this.last.day = dayOfMonth;
7183          if (!tempLast || this.last.compare(tempLast) < 0) {
7184            tempLast = this.last.clone();
7185          }
7186        }
7187        this.last = tempLast.clone();
7188
7189        //XXX: This feels like a hack, but we need to initialize
7190        //     the BYMONTHDAY case correctly and byDayAndMonthDay handles
7191        //     this case. It accepts a special flag which will avoid incrementing
7192        //     the initial value without the flag days that match the start time
7193        //     would be missed.
7194        if (this.has_by_data('BYMONTHDAY')) {
7195          this._byDayAndMonthDay(true);
7196        }
7197
7198        if (this.last.day > daysInMonth || this.last.day == 0) {
7199          throw new Error("Malformed values in BYDAY part");
7200        }
7201
7202      } else if (this.has_by_data("BYMONTHDAY")) {
7203        if (this.last.day < 0) {
7204          var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
7205          this.last.day = daysInMonth + this.last.day + 1;
7206        }
7207      }
7208
7209    },
7210
7211    /**
7212     * Retrieve the next occurrence from the iterator.
7213     * @return {ICAL.Time}
7214     */
7215    next: function icalrecur_iterator_next() {
7216      var before = (this.last ? this.last.clone() : null);
7217
7218      if ((this.rule.count && this.occurrence_number >= this.rule.count) ||
7219          (this.rule.until && this.last.compare(this.rule.until) > 0)) {
7220
7221        //XXX: right now this is just a flag and has no impact
7222        //     we can simplify the above case to check for completed later.
7223        this.completed = true;
7224
7225        return null;
7226      }
7227
7228      if (this.occurrence_number == 0 && this.last.compare(this.dtstart) >= 0) {
7229        // First of all, give the instance that was initialized
7230        this.occurrence_number++;
7231        return this.last;
7232      }
7233
7234
7235      var valid;
7236      do {
7237        valid = 1;
7238
7239        switch (this.rule.freq) {
7240        case "SECONDLY":
7241          this.next_second();
7242          break;
7243        case "MINUTELY":
7244          this.next_minute();
7245          break;
7246        case "HOURLY":
7247          this.next_hour();
7248          break;
7249        case "DAILY":
7250          this.next_day();
7251          break;
7252        case "WEEKLY":
7253          this.next_week();
7254          break;
7255        case "MONTHLY":
7256          valid = this.next_month();
7257          break;
7258        case "YEARLY":
7259          this.next_year();
7260          break;
7261
7262        default:
7263          return null;
7264        }
7265      } while (!this.check_contracting_rules() ||
7266               this.last.compare(this.dtstart) < 0 ||
7267               !valid);
7268
7269      // TODO is this valid?
7270      if (this.last.compare(before) == 0) {
7271        throw new Error("Same occurrence found twice, protecting " +
7272                        "you from death by recursion");
7273      }
7274
7275      if (this.rule.until && this.last.compare(this.rule.until) > 0) {
7276        this.completed = true;
7277        return null;
7278      } else {
7279        this.occurrence_number++;
7280        return this.last;
7281      }
7282    },
7283
7284    next_second: function next_second() {
7285      return this.next_generic("BYSECOND", "SECONDLY", "second", "minute");
7286    },
7287
7288    increment_second: function increment_second(inc) {
7289      return this.increment_generic(inc, "second", 60, "minute");
7290    },
7291
7292    next_minute: function next_minute() {
7293      return this.next_generic("BYMINUTE", "MINUTELY",
7294                               "minute", "hour", "next_second");
7295    },
7296
7297    increment_minute: function increment_minute(inc) {
7298      return this.increment_generic(inc, "minute", 60, "hour");
7299    },
7300
7301    next_hour: function next_hour() {
7302      return this.next_generic("BYHOUR", "HOURLY", "hour",
7303                               "monthday", "next_minute");
7304    },
7305
7306    increment_hour: function increment_hour(inc) {
7307      this.increment_generic(inc, "hour", 24, "monthday");
7308    },
7309
7310    next_day: function next_day() {
7311      var has_by_day = ("BYDAY" in this.by_data);
7312      var this_freq = (this.rule.freq == "DAILY");
7313
7314      if (this.next_hour() == 0) {
7315        return 0;
7316      }
7317
7318      if (this_freq) {
7319        this.increment_monthday(this.rule.interval);
7320      } else {
7321        this.increment_monthday(1);
7322      }
7323
7324      return 0;
7325    },
7326
7327    next_week: function next_week() {
7328      var end_of_data = 0;
7329
7330      if (this.next_weekday_by_week() == 0) {
7331        return end_of_data;
7332      }
7333
7334      if (this.has_by_data("BYWEEKNO")) {
7335        var idx = ++this.by_indices.BYWEEKNO;
7336
7337        if (this.by_indices.BYWEEKNO == this.by_data.BYWEEKNO.length) {
7338          this.by_indices.BYWEEKNO = 0;
7339          end_of_data = 1;
7340        }
7341
7342        // HACK should be first month of the year
7343        this.last.month = 1;
7344        this.last.day = 1;
7345
7346        var week_no = this.by_data.BYWEEKNO[this.by_indices.BYWEEKNO];
7347
7348        this.last.day += 7 * week_no;
7349
7350        if (end_of_data) {
7351          this.increment_year(1);
7352        }
7353      } else {
7354        // Jump to the next week
7355        this.increment_monthday(7 * this.rule.interval);
7356      }
7357
7358      return end_of_data;
7359    },
7360
7361    /**
7362     * Normalize each by day rule for a given year/month.
7363     * Takes into account ordering and negative rules
7364     *
7365     * @private
7366     * @param {Number} year         Current year.
7367     * @param {Number} month        Current month.
7368     * @param {Array}  rules        Array of rules.
7369     *
7370     * @return {Array} sorted and normalized rules.
7371     *                 Negative rules will be expanded to their
7372     *                 correct positive values for easier processing.
7373     */
7374    normalizeByMonthDayRules: function(year, month, rules) {
7375      var daysInMonth = ICAL.Time.daysInMonth(month, year);
7376
7377      // XXX: This is probably bad for performance to allocate
7378      //      a new array for each month we scan, if possible
7379      //      we should try to optimize this...
7380      var newRules = [];
7381
7382      var ruleIdx = 0;
7383      var len = rules.length;
7384      var rule;
7385
7386      for (; ruleIdx < len; ruleIdx++) {
7387        rule = rules[ruleIdx];
7388
7389        // if this rule falls outside of given
7390        // month discard it.
7391        if (Math.abs(rule) > daysInMonth) {
7392          continue;
7393        }
7394
7395        // negative case
7396        if (rule < 0) {
7397          // we add (not subtract its a negative number)
7398          // one from the rule because 1 === last day of month
7399          rule = daysInMonth + (rule + 1);
7400        } else if (rule === 0) {
7401          // skip zero its invalid.
7402          continue;
7403        }
7404
7405        // only add unique items...
7406        if (newRules.indexOf(rule) === -1) {
7407          newRules.push(rule);
7408        }
7409
7410      }
7411
7412      // unique and sort
7413      return newRules.sort(function(a, b) { return a - b; });
7414    },
7415
7416    /**
7417     * NOTES:
7418     * We are given a list of dates in the month (BYMONTHDAY) (23, etc..)
7419     * Also we are given a list of days (BYDAY) (MO, 2SU, etc..) when
7420     * both conditions match a given date (this.last.day) iteration stops.
7421     *
7422     * @private
7423     * @param {Boolean=} isInit     When given true will not increment the
7424     *                                current day (this.last).
7425     */
7426    _byDayAndMonthDay: function(isInit) {
7427      var byMonthDay; // setup in initMonth
7428      var byDay = this.by_data.BYDAY;
7429
7430      var date;
7431      var dateIdx = 0;
7432      var dateLen; // setup in initMonth
7433      var dayLen = byDay.length;
7434
7435      // we are not valid by default
7436      var dataIsValid = 0;
7437
7438      var daysInMonth;
7439      var self = this;
7440      // we need a copy of this, because a DateTime gets normalized
7441      // automatically if the day is out of range. At some points we
7442      // set the last day to 0 to start counting.
7443      var lastDay = this.last.day;
7444
7445      function initMonth() {
7446        daysInMonth = ICAL.Time.daysInMonth(
7447          self.last.month, self.last.year
7448        );
7449
7450        byMonthDay = self.normalizeByMonthDayRules(
7451          self.last.year,
7452          self.last.month,
7453          self.by_data.BYMONTHDAY
7454        );
7455
7456        dateLen = byMonthDay.length;
7457
7458        // For the case of more than one occurrence in one month
7459        // we have to be sure to start searching after the last
7460        // found date or at the last BYMONTHDAY, unless we are
7461        // initializing the iterator because in this case we have
7462        // to consider the last found date too.
7463        while (byMonthDay[dateIdx] <= lastDay &&
7464               !(isInit && byMonthDay[dateIdx] == lastDay) &&
7465               dateIdx < dateLen - 1) {
7466          dateIdx++;
7467        }
7468      }
7469
7470      function nextMonth() {
7471        // since the day is incremented at the start
7472        // of the loop below, we need to start at 0
7473        lastDay = 0;
7474        self.increment_month();
7475        dateIdx = 0;
7476        initMonth();
7477      }
7478
7479      initMonth();
7480
7481      // should come after initMonth
7482      if (isInit) {
7483        lastDay -= 1;
7484      }
7485
7486      // Use a counter to avoid an infinite loop with malformed rules.
7487      // Stop checking after 4 years so we consider also a leap year.
7488      var monthsCounter = 48;
7489
7490      while (!dataIsValid && monthsCounter) {
7491        monthsCounter--;
7492        // increment the current date. This is really
7493        // important otherwise we may fall into the infinite
7494        // loop trap. The initial date takes care of the case
7495        // where the current date is the date we are looking
7496        // for.
7497        date = lastDay + 1;
7498
7499        if (date > daysInMonth) {
7500          nextMonth();
7501          continue;
7502        }
7503
7504        // find next date
7505        var next = byMonthDay[dateIdx++];
7506
7507        // this logic is dependant on the BYMONTHDAYS
7508        // being in order (which is done by #normalizeByMonthDayRules)
7509        if (next >= date) {
7510          // if the next month day is in the future jump to it.
7511          lastDay = next;
7512        } else {
7513          // in this case the 'next' monthday has past
7514          // we must move to the month.
7515          nextMonth();
7516          continue;
7517        }
7518
7519        // Now we can loop through the day rules to see
7520        // if one matches the current month date.
7521        for (var dayIdx = 0; dayIdx < dayLen; dayIdx++) {
7522          var parts = this.ruleDayOfWeek(byDay[dayIdx]);
7523          var pos = parts[0];
7524          var dow = parts[1];
7525
7526          this.last.day = lastDay;
7527          if (this.last.isNthWeekDay(dow, pos)) {
7528            // when we find the valid one we can mark
7529            // the conditions as met and break the loop.
7530            // (Because we have this condition above
7531            //  it will also break the parent loop).
7532            dataIsValid = 1;
7533            break;
7534          }
7535        }
7536
7537        // Its completely possible that the combination
7538        // cannot be matched in the current month.
7539        // When we reach the end of possible combinations
7540        // in the current month we iterate to the next one.
7541        // since dateIdx is incremented right after getting
7542        // "next", we don't need dateLen -1 here.
7543        if (!dataIsValid && dateIdx === dateLen) {
7544          nextMonth();
7545          continue;
7546        }
7547      }
7548
7549      if (monthsCounter <= 0) {
7550        // Checked 4 years without finding a Byday that matches
7551        // a Bymonthday. Maybe the rule is not correct.
7552        throw new Error("Malformed values in BYDAY combined with BYMONTHDAY parts");
7553      }
7554
7555
7556      return dataIsValid;
7557    },
7558
7559    next_month: function next_month() {
7560      var this_freq = (this.rule.freq == "MONTHLY");
7561      var data_valid = 1;
7562
7563      if (this.next_hour() == 0) {
7564        return data_valid;
7565      }
7566
7567      if (this.has_by_data("BYDAY") && this.has_by_data("BYMONTHDAY")) {
7568        data_valid = this._byDayAndMonthDay();
7569      } else if (this.has_by_data("BYDAY")) {
7570        var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
7571        var setpos = 0;
7572        var setpos_total = 0;
7573
7574        if (this.has_by_data("BYSETPOS")) {
7575          var last_day = this.last.day;
7576          for (var day = 1; day <= daysInMonth; day++) {
7577            this.last.day = day;
7578            if (this.is_day_in_byday(this.last)) {
7579              setpos_total++;
7580              if (day <= last_day) {
7581                setpos++;
7582              }
7583            }
7584          }
7585          this.last.day = last_day;
7586        }
7587
7588        data_valid = 0;
7589        for (var day = this.last.day + 1; day <= daysInMonth; day++) {
7590          this.last.day = day;
7591
7592          if (this.is_day_in_byday(this.last)) {
7593            if (!this.has_by_data("BYSETPOS") ||
7594                this.check_set_position(++setpos) ||
7595                this.check_set_position(setpos - setpos_total - 1)) {
7596
7597              data_valid = 1;
7598              break;
7599            }
7600          }
7601        }
7602
7603        if (day > daysInMonth) {
7604          this.last.day = 1;
7605          this.increment_month();
7606
7607          if (this.is_day_in_byday(this.last)) {
7608            if (!this.has_by_data("BYSETPOS") || this.check_set_position(1)) {
7609              data_valid = 1;
7610            }
7611          } else {
7612            data_valid = 0;
7613          }
7614        }
7615      } else if (this.has_by_data("BYMONTHDAY")) {
7616        this.by_indices.BYMONTHDAY++;
7617
7618        if (this.by_indices.BYMONTHDAY >= this.by_data.BYMONTHDAY.length) {
7619          this.by_indices.BYMONTHDAY = 0;
7620          this.increment_month();
7621        }
7622
7623        var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
7624        var day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY];
7625
7626        if (day < 0) {
7627          day = daysInMonth + day + 1;
7628        }
7629
7630        if (day > daysInMonth) {
7631          this.last.day = 1;
7632          data_valid = this.is_day_in_byday(this.last);
7633        } else {
7634          this.last.day = day;
7635        }
7636
7637      } else {
7638        this.increment_month();
7639        var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
7640        if (this.by_data.BYMONTHDAY[0] > daysInMonth) {
7641          data_valid = 0;
7642        } else {
7643          this.last.day = this.by_data.BYMONTHDAY[0];
7644        }
7645      }
7646
7647      return data_valid;
7648    },
7649
7650    next_weekday_by_week: function next_weekday_by_week() {
7651      var end_of_data = 0;
7652
7653      if (this.next_hour() == 0) {
7654        return end_of_data;
7655      }
7656
7657      if (!this.has_by_data("BYDAY")) {
7658        return 1;
7659      }
7660
7661      for (;;) {
7662        var tt = new ICAL.Time();
7663        this.by_indices.BYDAY++;
7664
7665        if (this.by_indices.BYDAY == Object.keys(this.by_data.BYDAY).length) {
7666          this.by_indices.BYDAY = 0;
7667          end_of_data = 1;
7668        }
7669
7670        var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY];
7671        var parts = this.ruleDayOfWeek(coded_day);
7672        var dow = parts[1];
7673
7674        dow -= this.rule.wkst;
7675
7676        if (dow < 0) {
7677          dow += 7;
7678        }
7679
7680        tt.year = this.last.year;
7681        tt.month = this.last.month;
7682        tt.day = this.last.day;
7683
7684        var startOfWeek = tt.startDoyWeek(this.rule.wkst);
7685
7686        if (dow + startOfWeek < 1) {
7687          // The selected date is in the previous year
7688          if (!end_of_data) {
7689            continue;
7690          }
7691        }
7692
7693        var next = ICAL.Time.fromDayOfYear(startOfWeek + dow,
7694                                                  this.last.year);
7695
7696        /**
7697         * The normalization horrors below are due to
7698         * the fact that when the year/month/day changes
7699         * it can effect the other operations that come after.
7700         */
7701        this.last.year = next.year;
7702        this.last.month = next.month;
7703        this.last.day = next.day;
7704
7705        return end_of_data;
7706      }
7707    },
7708
7709    next_year: function next_year() {
7710
7711      if (this.next_hour() == 0) {
7712        return 0;
7713      }
7714
7715      if (++this.days_index == this.days.length) {
7716        this.days_index = 0;
7717        do {
7718          this.increment_year(this.rule.interval);
7719          this.expand_year_days(this.last.year);
7720        } while (this.days.length == 0);
7721      }
7722
7723      this._nextByYearDay();
7724
7725      return 1;
7726    },
7727
7728    _nextByYearDay: function _nextByYearDay() {
7729        var doy = this.days[this.days_index];
7730        var year = this.last.year;
7731        if (doy < 1) {
7732            // Time.fromDayOfYear(doy, year) indexes relative to the
7733            // start of the given year. That is different from the
7734            // semantics of BYYEARDAY where negative indexes are an
7735            // offset from the end of the given year.
7736            doy += 1;
7737            year += 1;
7738        }
7739        var next = ICAL.Time.fromDayOfYear(doy, year);
7740        this.last.day = next.day;
7741        this.last.month = next.month;
7742    },
7743
7744    /**
7745     * @param dow (eg: '1TU', '-1MO')
7746     * @param {ICAL.Time.weekDay=} aWeekStart The week start weekday
7747     * @return [pos, numericDow] (eg: [1, 3]) numericDow is relative to aWeekStart
7748     */
7749    ruleDayOfWeek: function ruleDayOfWeek(dow, aWeekStart) {
7750      var matches = dow.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/);
7751      if (matches) {
7752        var pos = parseInt(matches[1] || 0, 10);
7753        dow = ICAL.Recur.icalDayToNumericDay(matches[2], aWeekStart);
7754        return [pos, dow];
7755      } else {
7756        return [0, 0];
7757      }
7758    },
7759
7760    next_generic: function next_generic(aRuleType, aInterval, aDateAttr,
7761                                        aFollowingAttr, aPreviousIncr) {
7762      var has_by_rule = (aRuleType in this.by_data);
7763      var this_freq = (this.rule.freq == aInterval);
7764      var end_of_data = 0;
7765
7766      if (aPreviousIncr && this[aPreviousIncr]() == 0) {
7767        return end_of_data;
7768      }
7769
7770      if (has_by_rule) {
7771        this.by_indices[aRuleType]++;
7772        var idx = this.by_indices[aRuleType];
7773        var dta = this.by_data[aRuleType];
7774
7775        if (this.by_indices[aRuleType] == dta.length) {
7776          this.by_indices[aRuleType] = 0;
7777          end_of_data = 1;
7778        }
7779        this.last[aDateAttr] = dta[this.by_indices[aRuleType]];
7780      } else if (this_freq) {
7781        this["increment_" + aDateAttr](this.rule.interval);
7782      }
7783
7784      if (has_by_rule && end_of_data && this_freq) {
7785        this["increment_" + aFollowingAttr](1);
7786      }
7787
7788      return end_of_data;
7789    },
7790
7791    increment_monthday: function increment_monthday(inc) {
7792      for (var i = 0; i < inc; i++) {
7793        var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
7794        this.last.day++;
7795
7796        if (this.last.day > daysInMonth) {
7797          this.last.day -= daysInMonth;
7798          this.increment_month();
7799        }
7800      }
7801    },
7802
7803    increment_month: function increment_month() {
7804      this.last.day = 1;
7805      if (this.has_by_data("BYMONTH")) {
7806        this.by_indices.BYMONTH++;
7807
7808        if (this.by_indices.BYMONTH == this.by_data.BYMONTH.length) {
7809          this.by_indices.BYMONTH = 0;
7810          this.increment_year(1);
7811        }
7812
7813        this.last.month = this.by_data.BYMONTH[this.by_indices.BYMONTH];
7814      } else {
7815        if (this.rule.freq == "MONTHLY") {
7816          this.last.month += this.rule.interval;
7817        } else {
7818          this.last.month++;
7819        }
7820
7821        this.last.month--;
7822        var years = ICAL.helpers.trunc(this.last.month / 12);
7823        this.last.month %= 12;
7824        this.last.month++;
7825
7826        if (years != 0) {
7827          this.increment_year(years);
7828        }
7829      }
7830    },
7831
7832    increment_year: function increment_year(inc) {
7833      this.last.year += inc;
7834    },
7835
7836    increment_generic: function increment_generic(inc, aDateAttr,
7837                                                  aFactor, aNextIncrement) {
7838      this.last[aDateAttr] += inc;
7839      var nextunit = ICAL.helpers.trunc(this.last[aDateAttr] / aFactor);
7840      this.last[aDateAttr] %= aFactor;
7841      if (nextunit != 0) {
7842        this["increment_" + aNextIncrement](nextunit);
7843      }
7844    },
7845
7846    has_by_data: function has_by_data(aRuleType) {
7847      return (aRuleType in this.rule.parts);
7848    },
7849
7850    expand_year_days: function expand_year_days(aYear) {
7851      var t = new ICAL.Time();
7852      this.days = [];
7853
7854      // We need our own copy with a few keys set
7855      var parts = {};
7856      var rules = ["BYDAY", "BYWEEKNO", "BYMONTHDAY", "BYMONTH", "BYYEARDAY"];
7857      for (var p in rules) {
7858        /* istanbul ignore else */
7859        if (rules.hasOwnProperty(p)) {
7860          var part = rules[p];
7861          if (part in this.rule.parts) {
7862            parts[part] = this.rule.parts[part];
7863          }
7864        }
7865      }
7866
7867      if ("BYMONTH" in parts && "BYWEEKNO" in parts) {
7868        var valid = 1;
7869        var validWeeks = {};
7870        t.year = aYear;
7871        t.isDate = true;
7872
7873        for (var monthIdx = 0; monthIdx < this.by_data.BYMONTH.length; monthIdx++) {
7874          var month = this.by_data.BYMONTH[monthIdx];
7875          t.month = month;
7876          t.day = 1;
7877          var first_week = t.weekNumber(this.rule.wkst);
7878          t.day = ICAL.Time.daysInMonth(month, aYear);
7879          var last_week = t.weekNumber(this.rule.wkst);
7880          for (monthIdx = first_week; monthIdx < last_week; monthIdx++) {
7881            validWeeks[monthIdx] = 1;
7882          }
7883        }
7884
7885        for (var weekIdx = 0; weekIdx < this.by_data.BYWEEKNO.length && valid; weekIdx++) {
7886          var weekno = this.by_data.BYWEEKNO[weekIdx];
7887          if (weekno < 52) {
7888            valid &= validWeeks[weekIdx];
7889          } else {
7890            valid = 0;
7891          }
7892        }
7893
7894        if (valid) {
7895          delete parts.BYMONTH;
7896        } else {
7897          delete parts.BYWEEKNO;
7898        }
7899      }
7900
7901      var partCount = Object.keys(parts).length;
7902
7903      if (partCount == 0) {
7904        var t1 = this.dtstart.clone();
7905        t1.year = this.last.year;
7906        this.days.push(t1.dayOfYear());
7907      } else if (partCount == 1 && "BYMONTH" in parts) {
7908        for (var monthkey in this.by_data.BYMONTH) {
7909          /* istanbul ignore if */
7910          if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) {
7911            continue;
7912          }
7913          var t2 = this.dtstart.clone();
7914          t2.year = aYear;
7915          t2.month = this.by_data.BYMONTH[monthkey];
7916          t2.isDate = true;
7917          this.days.push(t2.dayOfYear());
7918        }
7919      } else if (partCount == 1 && "BYMONTHDAY" in parts) {
7920        for (var monthdaykey in this.by_data.BYMONTHDAY) {
7921          /* istanbul ignore if */
7922          if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) {
7923            continue;
7924          }
7925          var t3 = this.dtstart.clone();
7926          var day_ = this.by_data.BYMONTHDAY[monthdaykey];
7927          if (day_ < 0) {
7928            var daysInMonth = ICAL.Time.daysInMonth(t3.month, aYear);
7929            day_ = day_ + daysInMonth + 1;
7930          }
7931          t3.day = day_;
7932          t3.year = aYear;
7933          t3.isDate = true;
7934          this.days.push(t3.dayOfYear());
7935        }
7936      } else if (partCount == 2 &&
7937                 "BYMONTHDAY" in parts &&
7938                 "BYMONTH" in parts) {
7939        for (var monthkey in this.by_data.BYMONTH) {
7940          /* istanbul ignore if */
7941          if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) {
7942            continue;
7943          }
7944          var month_ = this.by_data.BYMONTH[monthkey];
7945          var daysInMonth = ICAL.Time.daysInMonth(month_, aYear);
7946          for (var monthdaykey in this.by_data.BYMONTHDAY) {
7947            /* istanbul ignore if */
7948            if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) {
7949              continue;
7950            }
7951            var day_ = this.by_data.BYMONTHDAY[monthdaykey];
7952            if (day_ < 0) {
7953              day_ = day_ + daysInMonth + 1;
7954            }
7955            t.day = day_;
7956            t.month = month_;
7957            t.year = aYear;
7958            t.isDate = true;
7959
7960            this.days.push(t.dayOfYear());
7961          }
7962        }
7963      } else if (partCount == 1 && "BYWEEKNO" in parts) {
7964        // TODO unimplemented in libical
7965      } else if (partCount == 2 &&
7966                 "BYWEEKNO" in parts &&
7967                 "BYMONTHDAY" in parts) {
7968        // TODO unimplemented in libical
7969      } else if (partCount == 1 && "BYDAY" in parts) {
7970        this.days = this.days.concat(this.expand_by_day(aYear));
7971      } else if (partCount == 2 && "BYDAY" in parts && "BYMONTH" in parts) {
7972        for (var monthkey in this.by_data.BYMONTH) {
7973          /* istanbul ignore if */
7974          if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) {
7975            continue;
7976          }
7977          var month = this.by_data.BYMONTH[monthkey];
7978          var daysInMonth = ICAL.Time.daysInMonth(month, aYear);
7979
7980          t.year = aYear;
7981          t.month = this.by_data.BYMONTH[monthkey];
7982          t.day = 1;
7983          t.isDate = true;
7984
7985          var first_dow = t.dayOfWeek();
7986          var doy_offset = t.dayOfYear() - 1;
7987
7988          t.day = daysInMonth;
7989          var last_dow = t.dayOfWeek();
7990
7991          if (this.has_by_data("BYSETPOS")) {
7992            var set_pos_counter = 0;
7993            var by_month_day = [];
7994            for (var day = 1; day <= daysInMonth; day++) {
7995              t.day = day;
7996              if (this.is_day_in_byday(t)) {
7997                by_month_day.push(day);
7998              }
7999            }
8000
8001            for (var spIndex = 0; spIndex < by_month_day.length; spIndex++) {
8002              if (this.check_set_position(spIndex + 1) ||
8003                  this.check_set_position(spIndex - by_month_day.length)) {
8004                this.days.push(doy_offset + by_month_day[spIndex]);
8005              }
8006            }
8007          } else {
8008            for (var daycodedkey in this.by_data.BYDAY) {
8009              /* istanbul ignore if */
8010              if (!this.by_data.BYDAY.hasOwnProperty(daycodedkey)) {
8011                continue;
8012              }
8013              var coded_day = this.by_data.BYDAY[daycodedkey];
8014              var bydayParts = this.ruleDayOfWeek(coded_day);
8015              var pos = bydayParts[0];
8016              var dow = bydayParts[1];
8017              var month_day;
8018
8019              var first_matching_day = ((dow + 7 - first_dow) % 7) + 1;
8020              var last_matching_day = daysInMonth - ((last_dow + 7 - dow) % 7);
8021
8022              if (pos == 0) {
8023                for (var day = first_matching_day; day <= daysInMonth; day += 7) {
8024                  this.days.push(doy_offset + day);
8025                }
8026              } else if (pos > 0) {
8027                month_day = first_matching_day + (pos - 1) * 7;
8028
8029                if (month_day <= daysInMonth) {
8030                  this.days.push(doy_offset + month_day);
8031                }
8032              } else {
8033                month_day = last_matching_day + (pos + 1) * 7;
8034
8035                if (month_day > 0) {
8036                  this.days.push(doy_offset + month_day);
8037                }
8038              }
8039            }
8040          }
8041        }
8042        // Return dates in order of occurrence (1,2,3,...) instead
8043        // of by groups of weekdays (1,8,15,...,2,9,16,...).
8044        this.days.sort(function(a, b) { return a - b; }); // Comparator function allows to sort numbers.
8045      } else if (partCount == 2 && "BYDAY" in parts && "BYMONTHDAY" in parts) {
8046        var expandedDays = this.expand_by_day(aYear);
8047
8048        for (var daykey in expandedDays) {
8049          /* istanbul ignore if */
8050          if (!expandedDays.hasOwnProperty(daykey)) {
8051            continue;
8052          }
8053          var day = expandedDays[daykey];
8054          var tt = ICAL.Time.fromDayOfYear(day, aYear);
8055          if (this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) {
8056            this.days.push(day);
8057          }
8058        }
8059      } else if (partCount == 3 &&
8060                 "BYDAY" in parts &&
8061                 "BYMONTHDAY" in parts &&
8062                 "BYMONTH" in parts) {
8063        var expandedDays = this.expand_by_day(aYear);
8064
8065        for (var daykey in expandedDays) {
8066          /* istanbul ignore if */
8067          if (!expandedDays.hasOwnProperty(daykey)) {
8068            continue;
8069          }
8070          var day = expandedDays[daykey];
8071          var tt = ICAL.Time.fromDayOfYear(day, aYear);
8072
8073          if (this.by_data.BYMONTH.indexOf(tt.month) >= 0 &&
8074              this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) {
8075            this.days.push(day);
8076          }
8077        }
8078      } else if (partCount == 2 && "BYDAY" in parts && "BYWEEKNO" in parts) {
8079        var expandedDays = this.expand_by_day(aYear);
8080
8081        for (var daykey in expandedDays) {
8082          /* istanbul ignore if */
8083          if (!expandedDays.hasOwnProperty(daykey)) {
8084            continue;
8085          }
8086          var day = expandedDays[daykey];
8087          var tt = ICAL.Time.fromDayOfYear(day, aYear);
8088          var weekno = tt.weekNumber(this.rule.wkst);
8089
8090          if (this.by_data.BYWEEKNO.indexOf(weekno)) {
8091            this.days.push(day);
8092          }
8093        }
8094      } else if (partCount == 3 &&
8095                 "BYDAY" in parts &&
8096                 "BYWEEKNO" in parts &&
8097                 "BYMONTHDAY" in parts) {
8098        // TODO unimplemted in libical
8099      } else if (partCount == 1 && "BYYEARDAY" in parts) {
8100        this.days = this.days.concat(this.by_data.BYYEARDAY);
8101      } else {
8102        this.days = [];
8103      }
8104      return 0;
8105    },
8106
8107    expand_by_day: function expand_by_day(aYear) {
8108
8109      var days_list = [];
8110      var tmp = this.last.clone();
8111
8112      tmp.year = aYear;
8113      tmp.month = 1;
8114      tmp.day = 1;
8115      tmp.isDate = true;
8116
8117      var start_dow = tmp.dayOfWeek();
8118
8119      tmp.month = 12;
8120      tmp.day = 31;
8121      tmp.isDate = true;
8122
8123      var end_dow = tmp.dayOfWeek();
8124      var end_year_day = tmp.dayOfYear();
8125
8126      for (var daykey in this.by_data.BYDAY) {
8127        /* istanbul ignore if */
8128        if (!this.by_data.BYDAY.hasOwnProperty(daykey)) {
8129          continue;
8130        }
8131        var day = this.by_data.BYDAY[daykey];
8132        var parts = this.ruleDayOfWeek(day);
8133        var pos = parts[0];
8134        var dow = parts[1];
8135
8136        if (pos == 0) {
8137          var tmp_start_doy = ((dow + 7 - start_dow) % 7) + 1;
8138
8139          for (var doy = tmp_start_doy; doy <= end_year_day; doy += 7) {
8140            days_list.push(doy);
8141          }
8142
8143        } else if (pos > 0) {
8144          var first;
8145          if (dow >= start_dow) {
8146            first = dow - start_dow + 1;
8147          } else {
8148            first = dow - start_dow + 8;
8149          }
8150
8151          days_list.push(first + (pos - 1) * 7);
8152        } else {
8153          var last;
8154          pos = -pos;
8155
8156          if (dow <= end_dow) {
8157            last = end_year_day - end_dow + dow;
8158          } else {
8159            last = end_year_day - end_dow + dow - 7;
8160          }
8161
8162          days_list.push(last - (pos - 1) * 7);
8163        }
8164      }
8165      return days_list;
8166    },
8167
8168    is_day_in_byday: function is_day_in_byday(tt) {
8169      for (var daykey in this.by_data.BYDAY) {
8170        /* istanbul ignore if */
8171        if (!this.by_data.BYDAY.hasOwnProperty(daykey)) {
8172          continue;
8173        }
8174        var day = this.by_data.BYDAY[daykey];
8175        var parts = this.ruleDayOfWeek(day);
8176        var pos = parts[0];
8177        var dow = parts[1];
8178        var this_dow = tt.dayOfWeek();
8179
8180        if ((pos == 0 && dow == this_dow) ||
8181            (tt.nthWeekDay(dow, pos) == tt.day)) {
8182          return 1;
8183        }
8184      }
8185
8186      return 0;
8187    },
8188
8189    /**
8190     * Checks if given value is in BYSETPOS.
8191     *
8192     * @private
8193     * @param {Numeric} aPos position to check for.
8194     * @return {Boolean} false unless BYSETPOS rules exist
8195     *                   and the given value is present in rules.
8196     */
8197    check_set_position: function check_set_position(aPos) {
8198      if (this.has_by_data('BYSETPOS')) {
8199        var idx = this.by_data.BYSETPOS.indexOf(aPos);
8200        // negative numbers are not false-y
8201        return idx !== -1;
8202      }
8203      return false;
8204    },
8205
8206    sort_byday_rules: function icalrecur_sort_byday_rules(aRules) {
8207      for (var i = 0; i < aRules.length; i++) {
8208        for (var j = 0; j < i; j++) {
8209          var one = this.ruleDayOfWeek(aRules[j], this.rule.wkst)[1];
8210          var two = this.ruleDayOfWeek(aRules[i], this.rule.wkst)[1];
8211
8212          if (one > two) {
8213            var tmp = aRules[i];
8214            aRules[i] = aRules[j];
8215            aRules[j] = tmp;
8216          }
8217        }
8218      }
8219    },
8220
8221    check_contract_restriction: function check_contract_restriction(aRuleType, v) {
8222      var indexMapValue = icalrecur_iterator._indexMap[aRuleType];
8223      var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue];
8224      var pass = false;
8225
8226      if (aRuleType in this.by_data &&
8227          ruleMapValue == icalrecur_iterator.CONTRACT) {
8228
8229        var ruleType = this.by_data[aRuleType];
8230
8231        for (var bydatakey in ruleType) {
8232          /* istanbul ignore else */
8233          if (ruleType.hasOwnProperty(bydatakey)) {
8234            if (ruleType[bydatakey] == v) {
8235              pass = true;
8236              break;
8237            }
8238          }
8239        }
8240      } else {
8241        // Not a contracting byrule or has no data, test passes
8242        pass = true;
8243      }
8244      return pass;
8245    },
8246
8247    check_contracting_rules: function check_contracting_rules() {
8248      var dow = this.last.dayOfWeek();
8249      var weekNo = this.last.weekNumber(this.rule.wkst);
8250      var doy = this.last.dayOfYear();
8251
8252      return (this.check_contract_restriction("BYSECOND", this.last.second) &&
8253              this.check_contract_restriction("BYMINUTE", this.last.minute) &&
8254              this.check_contract_restriction("BYHOUR", this.last.hour) &&
8255              this.check_contract_restriction("BYDAY", ICAL.Recur.numericDayToIcalDay(dow)) &&
8256              this.check_contract_restriction("BYWEEKNO", weekNo) &&
8257              this.check_contract_restriction("BYMONTHDAY", this.last.day) &&
8258              this.check_contract_restriction("BYMONTH", this.last.month) &&
8259              this.check_contract_restriction("BYYEARDAY", doy));
8260    },
8261
8262    setup_defaults: function setup_defaults(aRuleType, req, deftime) {
8263      var indexMapValue = icalrecur_iterator._indexMap[aRuleType];
8264      var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue];
8265
8266      if (ruleMapValue != icalrecur_iterator.CONTRACT) {
8267        if (!(aRuleType in this.by_data)) {
8268          this.by_data[aRuleType] = [deftime];
8269        }
8270        if (this.rule.freq != req) {
8271          return this.by_data[aRuleType][0];
8272        }
8273      }
8274      return deftime;
8275    },
8276
8277    /**
8278     * Convert iterator into a serialize-able object.  Will preserve current
8279     * iteration sequence to ensure the seamless continuation of the recurrence
8280     * rule.
8281     * @return {Object}
8282     */
8283    toJSON: function() {
8284      var result = Object.create(null);
8285
8286      result.initialized = this.initialized;
8287      result.rule = this.rule.toJSON();
8288      result.dtstart = this.dtstart.toJSON();
8289      result.by_data = this.by_data;
8290      result.days = this.days;
8291      result.last = this.last.toJSON();
8292      result.by_indices = this.by_indices;
8293      result.occurrence_number = this.occurrence_number;
8294
8295      return result;
8296    }
8297  };
8298
8299  icalrecur_iterator._indexMap = {
8300    "BYSECOND": 0,
8301    "BYMINUTE": 1,
8302    "BYHOUR": 2,
8303    "BYDAY": 3,
8304    "BYMONTHDAY": 4,
8305    "BYYEARDAY": 5,
8306    "BYWEEKNO": 6,
8307    "BYMONTH": 7,
8308    "BYSETPOS": 8
8309  };
8310
8311  icalrecur_iterator._expandMap = {
8312    "SECONDLY": [1, 1, 1, 1, 1, 1, 1, 1],
8313    "MINUTELY": [2, 1, 1, 1, 1, 1, 1, 1],
8314    "HOURLY": [2, 2, 1, 1, 1, 1, 1, 1],
8315    "DAILY": [2, 2, 2, 1, 1, 1, 1, 1],
8316    "WEEKLY": [2, 2, 2, 2, 3, 3, 1, 1],
8317    "MONTHLY": [2, 2, 2, 2, 2, 3, 3, 1],
8318    "YEARLY": [2, 2, 2, 2, 2, 2, 2, 2]
8319  };
8320  icalrecur_iterator.UNKNOWN = 0;
8321  icalrecur_iterator.CONTRACT = 1;
8322  icalrecur_iterator.EXPAND = 2;
8323  icalrecur_iterator.ILLEGAL = 3;
8324
8325  return icalrecur_iterator;
8326
8327}());
8328/* This Source Code Form is subject to the terms of the Mozilla Public
8329 * License, v. 2.0. If a copy of the MPL was not distributed with this
8330 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8331 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
8332
8333
8334/**
8335 * This symbol is further described later on
8336 * @ignore
8337 */
8338ICAL.RecurExpansion = (function() {
8339  function formatTime(item) {
8340    return ICAL.helpers.formatClassType(item, ICAL.Time);
8341  }
8342
8343  function compareTime(a, b) {
8344    return a.compare(b);
8345  }
8346
8347  function isRecurringComponent(comp) {
8348    return comp.hasProperty('rdate') ||
8349           comp.hasProperty('rrule') ||
8350           comp.hasProperty('recurrence-id');
8351  }
8352
8353  /**
8354   * @classdesc
8355   * Primary class for expanding recurring rules.  Can take multiple rrules,
8356   * rdates, exdate(s) and iterate (in order) over each next occurrence.
8357   *
8358   * Once initialized this class can also be serialized saved and continue
8359   * iteration from the last point.
8360   *
8361   * NOTE: it is intended that this class is to be used
8362   *       with ICAL.Event which handles recurrence exceptions.
8363   *
8364   * @example
8365   * // assuming event is a parsed ical component
8366   * var event;
8367   *
8368   * var expand = new ICAL.RecurExpansion({
8369   *   component: event,
8370   *   dtstart: event.getFirstPropertyValue('dtstart')
8371   * });
8372   *
8373   * // remember there are infinite rules
8374   * // so its a good idea to limit the scope
8375   * // of the iterations then resume later on.
8376   *
8377   * // next is always an ICAL.Time or null
8378   * var next;
8379   *
8380   * while (someCondition && (next = expand.next())) {
8381   *   // do something with next
8382   * }
8383   *
8384   * // save instance for later
8385   * var json = JSON.stringify(expand);
8386   *
8387   * //...
8388   *
8389   * // NOTE: if the component's properties have
8390   * //       changed you will need to rebuild the
8391   * //       class and start over. This only works
8392   * //       when the component's recurrence info is the same.
8393   * var expand = new ICAL.RecurExpansion(JSON.parse(json));
8394   *
8395   * @description
8396   * The options object can be filled with the specified initial values. It can
8397   * also contain additional members, as a result of serializing a previous
8398   * expansion state, as shown in the example.
8399   *
8400   * @class
8401   * @alias ICAL.RecurExpansion
8402   * @param {Object} options
8403   *        Recurrence expansion options
8404   * @param {ICAL.Time} options.dtstart
8405   *        Start time of the event
8406   * @param {ICAL.Component=} options.component
8407   *        Component for expansion, required if not resuming.
8408   */
8409  function RecurExpansion(options) {
8410    this.ruleDates = [];
8411    this.exDates = [];
8412    this.fromData(options);
8413  }
8414
8415  RecurExpansion.prototype = {
8416    /**
8417     * True when iteration is fully completed.
8418     * @type {Boolean}
8419     */
8420    complete: false,
8421
8422    /**
8423     * Array of rrule iterators.
8424     *
8425     * @type {ICAL.RecurIterator[]}
8426     * @private
8427     */
8428    ruleIterators: null,
8429
8430    /**
8431     * Array of rdate instances.
8432     *
8433     * @type {ICAL.Time[]}
8434     * @private
8435     */
8436    ruleDates: null,
8437
8438    /**
8439     * Array of exdate instances.
8440     *
8441     * @type {ICAL.Time[]}
8442     * @private
8443     */
8444    exDates: null,
8445
8446    /**
8447     * Current position in ruleDates array.
8448     * @type {Number}
8449     * @private
8450     */
8451    ruleDateInc: 0,
8452
8453    /**
8454     * Current position in exDates array
8455     * @type {Number}
8456     * @private
8457     */
8458    exDateInc: 0,
8459
8460    /**
8461     * Current negative date.
8462     *
8463     * @type {ICAL.Time}
8464     * @private
8465     */
8466    exDate: null,
8467
8468    /**
8469     * Current additional date.
8470     *
8471     * @type {ICAL.Time}
8472     * @private
8473     */
8474    ruleDate: null,
8475
8476    /**
8477     * Start date of recurring rules.
8478     *
8479     * @type {ICAL.Time}
8480     */
8481    dtstart: null,
8482
8483    /**
8484     * Last expanded time
8485     *
8486     * @type {ICAL.Time}
8487     */
8488    last: null,
8489
8490    /**
8491     * Initialize the recurrence expansion from the data object. The options
8492     * object may also contain additional members, see the
8493     * {@link ICAL.RecurExpansion constructor} for more details.
8494     *
8495     * @param {Object} options
8496     *        Recurrence expansion options
8497     * @param {ICAL.Time} options.dtstart
8498     *        Start time of the event
8499     * @param {ICAL.Component=} options.component
8500     *        Component for expansion, required if not resuming.
8501     */
8502    fromData: function(options) {
8503      var start = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time);
8504
8505      if (!start) {
8506        throw new Error('.dtstart (ICAL.Time) must be given');
8507      } else {
8508        this.dtstart = start;
8509      }
8510
8511      if (options.component) {
8512        this._init(options.component);
8513      } else {
8514        this.last = formatTime(options.last) || start.clone();
8515
8516        if (!options.ruleIterators) {
8517          throw new Error('.ruleIterators or .component must be given');
8518        }
8519
8520        this.ruleIterators = options.ruleIterators.map(function(item) {
8521          return ICAL.helpers.formatClassType(item, ICAL.RecurIterator);
8522        });
8523
8524        this.ruleDateInc = options.ruleDateInc;
8525        this.exDateInc = options.exDateInc;
8526
8527        if (options.ruleDates) {
8528          this.ruleDates = options.ruleDates.map(formatTime);
8529          this.ruleDate = this.ruleDates[this.ruleDateInc];
8530        }
8531
8532        if (options.exDates) {
8533          this.exDates = options.exDates.map(formatTime);
8534          this.exDate = this.exDates[this.exDateInc];
8535        }
8536
8537        if (typeof(options.complete) !== 'undefined') {
8538          this.complete = options.complete;
8539        }
8540      }
8541    },
8542
8543    /**
8544     * Retrieve the next occurrence in the series.
8545     * @return {ICAL.Time}
8546     */
8547    next: function() {
8548      var iter;
8549      var ruleOfDay;
8550      var next;
8551      var compare;
8552
8553      var maxTries = 500;
8554      var currentTry = 0;
8555
8556      while (true) {
8557        if (currentTry++ > maxTries) {
8558          throw new Error(
8559            'max tries have occured, rule may be impossible to forfill.'
8560          );
8561        }
8562
8563        next = this.ruleDate;
8564        iter = this._nextRecurrenceIter(this.last);
8565
8566        // no more matches
8567        // because we increment the rule day or rule
8568        // _after_ we choose a value this should be
8569        // the only spot where we need to worry about the
8570        // end of events.
8571        if (!next && !iter) {
8572          // there are no more iterators or rdates
8573          this.complete = true;
8574          break;
8575        }
8576
8577        // no next rule day or recurrence rule is first.
8578        if (!next || (iter && next.compare(iter.last) > 0)) {
8579          // must be cloned, recur will reuse the time element.
8580          next = iter.last.clone();
8581          // move to next so we can continue
8582          iter.next();
8583        }
8584
8585        // if the ruleDate is still next increment it.
8586        if (this.ruleDate === next) {
8587          this._nextRuleDay();
8588        }
8589
8590        this.last = next;
8591
8592        // check the negative rules
8593        if (this.exDate) {
8594          compare = this.exDate.compare(this.last);
8595
8596          if (compare < 0) {
8597            this._nextExDay();
8598          }
8599
8600          // if the current rule is excluded skip it.
8601          if (compare === 0) {
8602            this._nextExDay();
8603            continue;
8604          }
8605        }
8606
8607        //XXX: The spec states that after we resolve the final
8608        //     list of dates we execute exdate this seems somewhat counter
8609        //     intuitive to what I have seen most servers do so for now
8610        //     I exclude based on the original date not the one that may
8611        //     have been modified by the exception.
8612        return this.last;
8613      }
8614    },
8615
8616    /**
8617     * Converts object into a serialize-able format. This format can be passed
8618     * back into the expansion to resume iteration.
8619     * @return {Object}
8620     */
8621    toJSON: function() {
8622      function toJSON(item) {
8623        return item.toJSON();
8624      }
8625
8626      var result = Object.create(null);
8627      result.ruleIterators = this.ruleIterators.map(toJSON);
8628
8629      if (this.ruleDates) {
8630        result.ruleDates = this.ruleDates.map(toJSON);
8631      }
8632
8633      if (this.exDates) {
8634        result.exDates = this.exDates.map(toJSON);
8635      }
8636
8637      result.ruleDateInc = this.ruleDateInc;
8638      result.exDateInc = this.exDateInc;
8639      result.last = this.last.toJSON();
8640      result.dtstart = this.dtstart.toJSON();
8641      result.complete = this.complete;
8642
8643      return result;
8644    },
8645
8646    /**
8647     * Extract all dates from the properties in the given component. The
8648     * properties will be filtered by the property name.
8649     *
8650     * @private
8651     * @param {ICAL.Component} component        The component to search in
8652     * @param {String} propertyName             The property name to search for
8653     * @return {ICAL.Time[]}                    The extracted dates.
8654     */
8655    _extractDates: function(component, propertyName) {
8656      function handleProp(prop) {
8657        idx = ICAL.helpers.binsearchInsert(
8658          result,
8659          prop,
8660          compareTime
8661        );
8662
8663        // ordered insert
8664        result.splice(idx, 0, prop);
8665      }
8666
8667      var result = [];
8668      var props = component.getAllProperties(propertyName);
8669      var len = props.length;
8670      var i = 0;
8671      var prop;
8672
8673      var idx;
8674
8675      for (; i < len; i++) {
8676        props[i].getValues().forEach(handleProp);
8677      }
8678
8679      return result;
8680    },
8681
8682    /**
8683     * Initialize the recurrence expansion.
8684     *
8685     * @private
8686     * @param {ICAL.Component} component    The component to initialize from.
8687     */
8688    _init: function(component) {
8689      this.ruleIterators = [];
8690
8691      this.last = this.dtstart.clone();
8692
8693      // to provide api consistency non-recurring
8694      // events can also use the iterator though it will
8695      // only return a single time.
8696      if (!isRecurringComponent(component)) {
8697        this.ruleDate = this.last.clone();
8698        this.complete = true;
8699        return;
8700      }
8701
8702      if (component.hasProperty('rdate')) {
8703        this.ruleDates = this._extractDates(component, 'rdate');
8704
8705        // special hack for cases where first rdate is prior
8706        // to the start date. We only check for the first rdate.
8707        // This is mostly for google's crazy recurring date logic
8708        // (contacts birthdays).
8709        if ((this.ruleDates[0]) &&
8710            (this.ruleDates[0].compare(this.dtstart) < 0)) {
8711
8712          this.ruleDateInc = 0;
8713          this.last = this.ruleDates[0].clone();
8714        } else {
8715          this.ruleDateInc = ICAL.helpers.binsearchInsert(
8716            this.ruleDates,
8717            this.last,
8718            compareTime
8719          );
8720        }
8721
8722        this.ruleDate = this.ruleDates[this.ruleDateInc];
8723      }
8724
8725      if (component.hasProperty('rrule')) {
8726        var rules = component.getAllProperties('rrule');
8727        var i = 0;
8728        var len = rules.length;
8729
8730        var rule;
8731        var iter;
8732
8733        for (; i < len; i++) {
8734          rule = rules[i].getFirstValue();
8735          iter = rule.iterator(this.dtstart);
8736          this.ruleIterators.push(iter);
8737
8738          // increment to the next occurrence so future
8739          // calls to next return times beyond the initial iteration.
8740          // XXX: I find this suspicious might be a bug?
8741          iter.next();
8742        }
8743      }
8744
8745      if (component.hasProperty('exdate')) {
8746        this.exDates = this._extractDates(component, 'exdate');
8747        // if we have a .last day we increment the index to beyond it.
8748        this.exDateInc = ICAL.helpers.binsearchInsert(
8749          this.exDates,
8750          this.last,
8751          compareTime
8752        );
8753
8754        this.exDate = this.exDates[this.exDateInc];
8755      }
8756    },
8757
8758    /**
8759     * Advance to the next exdate
8760     * @private
8761     */
8762    _nextExDay: function() {
8763      this.exDate = this.exDates[++this.exDateInc];
8764    },
8765
8766    /**
8767     * Advance to the next rule date
8768     * @private
8769     */
8770    _nextRuleDay: function() {
8771      this.ruleDate = this.ruleDates[++this.ruleDateInc];
8772    },
8773
8774    /**
8775     * Find and return the recurrence rule with the most recent event and
8776     * return it.
8777     *
8778     * @private
8779     * @return {?ICAL.RecurIterator}    Found iterator.
8780     */
8781    _nextRecurrenceIter: function() {
8782      var iters = this.ruleIterators;
8783
8784      if (iters.length === 0) {
8785        return null;
8786      }
8787
8788      var len = iters.length;
8789      var iter;
8790      var iterTime;
8791      var iterIdx = 0;
8792      var chosenIter;
8793
8794      // loop through each iterator
8795      for (; iterIdx < len; iterIdx++) {
8796        iter = iters[iterIdx];
8797        iterTime = iter.last;
8798
8799        // if iteration is complete
8800        // then we must exclude it from
8801        // the search and remove it.
8802        if (iter.completed) {
8803          len--;
8804          if (iterIdx !== 0) {
8805            iterIdx--;
8806          }
8807          iters.splice(iterIdx, 1);
8808          continue;
8809        }
8810
8811        // find the most recent possible choice
8812        if (!chosenIter || chosenIter.last.compare(iterTime) > 0) {
8813          // that iterator is saved
8814          chosenIter = iter;
8815        }
8816      }
8817
8818      // the chosen iterator is returned but not mutated
8819      // this iterator contains the most recent event.
8820      return chosenIter;
8821    }
8822  };
8823
8824  return RecurExpansion;
8825}());
8826/* This Source Code Form is subject to the terms of the Mozilla Public
8827 * License, v. 2.0. If a copy of the MPL was not distributed with this
8828 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8829 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
8830
8831
8832/**
8833 * This symbol is further described later on
8834 * @ignore
8835 */
8836ICAL.Event = (function() {
8837
8838  /**
8839   * @classdesc
8840   * ICAL.js is organized into multiple layers. The bottom layer is a raw jCal
8841   * object, followed by the component/property layer. The highest level is the
8842   * event representation, which this class is part of. See the
8843   * {@tutorial layers} guide for more details.
8844   *
8845   * @class
8846   * @alias ICAL.Event
8847   * @param {ICAL.Component=} component         The ICAL.Component to base this event on
8848   * @param {Object} options                    Options for this event
8849   * @param {Boolean} options.strictExceptions
8850   *          When true, will verify exceptions are related by their UUID
8851   * @param {Array<ICAL.Component|ICAL.Event>} options.exceptions
8852   *          Exceptions to this event, either as components or events. If not
8853   *            specified exceptions will automatically be set in relation of
8854   *            component's parent
8855   */
8856  function Event(component, options) {
8857    if (!(component instanceof ICAL.Component)) {
8858      options = component;
8859      component = null;
8860    }
8861
8862    if (component) {
8863      this.component = component;
8864    } else {
8865      this.component = new ICAL.Component('vevent');
8866    }
8867
8868    this._rangeExceptionCache = Object.create(null);
8869    this.exceptions = Object.create(null);
8870    this.rangeExceptions = [];
8871
8872    if (options && options.strictExceptions) {
8873      this.strictExceptions = options.strictExceptions;
8874    }
8875
8876    if (options && options.exceptions) {
8877      options.exceptions.forEach(this.relateException, this);
8878    } else if (this.component.parent && !this.isRecurrenceException()) {
8879      this.component.parent.getAllSubcomponents('vevent').forEach(function(event) {
8880        if (event.hasProperty('recurrence-id')) {
8881          this.relateException(event);
8882        }
8883      }, this);
8884    }
8885  }
8886
8887  Event.prototype = {
8888
8889    THISANDFUTURE: 'THISANDFUTURE',
8890
8891    /**
8892     * List of related event exceptions.
8893     *
8894     * @type {ICAL.Event[]}
8895     */
8896    exceptions: null,
8897
8898    /**
8899     * When true, will verify exceptions are related by their UUID.
8900     *
8901     * @type {Boolean}
8902     */
8903    strictExceptions: false,
8904
8905    /**
8906     * Relates a given event exception to this object.  If the given component
8907     * does not share the UID of this event it cannot be related and will throw
8908     * an exception.
8909     *
8910     * If this component is an exception it cannot have other exceptions
8911     * related to it.
8912     *
8913     * @param {ICAL.Component|ICAL.Event} obj       Component or event
8914     */
8915    relateException: function(obj) {
8916      if (this.isRecurrenceException()) {
8917        throw new Error('cannot relate exception to exceptions');
8918      }
8919
8920      if (obj instanceof ICAL.Component) {
8921        obj = new ICAL.Event(obj);
8922      }
8923
8924      if (this.strictExceptions && obj.uid !== this.uid) {
8925        throw new Error('attempted to relate unrelated exception');
8926      }
8927
8928      var id = obj.recurrenceId.toString();
8929
8930      // we don't sort or manage exceptions directly
8931      // here the recurrence expander handles that.
8932      this.exceptions[id] = obj;
8933
8934      // index RANGE=THISANDFUTURE exceptions so we can
8935      // look them up later in getOccurrenceDetails.
8936      if (obj.modifiesFuture()) {
8937        var item = [
8938          obj.recurrenceId.toUnixTime(), id
8939        ];
8940
8941        // we keep them sorted so we can find the nearest
8942        // value later on...
8943        var idx = ICAL.helpers.binsearchInsert(
8944          this.rangeExceptions,
8945          item,
8946          compareRangeException
8947        );
8948
8949        this.rangeExceptions.splice(idx, 0, item);
8950      }
8951    },
8952
8953    /**
8954     * Checks if this record is an exception and has the RANGE=THISANDFUTURE
8955     * value.
8956     *
8957     * @return {Boolean}        True, when exception is within range
8958     */
8959    modifiesFuture: function() {
8960      if (!this.component.hasProperty('recurrence-id')) {
8961        return false;
8962      }
8963
8964      var range = this.component.getFirstProperty('recurrence-id').getParameter('range');
8965      return range === this.THISANDFUTURE;
8966    },
8967
8968    /**
8969     * Finds the range exception nearest to the given date.
8970     *
8971     * @param {ICAL.Time} time usually an occurrence time of an event
8972     * @return {?ICAL.Event} the related event/exception or null
8973     */
8974    findRangeException: function(time) {
8975      if (!this.rangeExceptions.length) {
8976        return null;
8977      }
8978
8979      var utc = time.toUnixTime();
8980      var idx = ICAL.helpers.binsearchInsert(
8981        this.rangeExceptions,
8982        [utc],
8983        compareRangeException
8984      );
8985
8986      idx -= 1;
8987
8988      // occurs before
8989      if (idx < 0) {
8990        return null;
8991      }
8992
8993      var rangeItem = this.rangeExceptions[idx];
8994
8995      /* istanbul ignore next: sanity check only */
8996      if (utc < rangeItem[0]) {
8997        return null;
8998      }
8999
9000      return rangeItem[1];
9001    },
9002
9003    /**
9004     * This object is returned by {@link ICAL.Event#getOccurrenceDetails getOccurrenceDetails}
9005     *
9006     * @typedef {Object} occurrenceDetails
9007     * @memberof ICAL.Event
9008     * @property {ICAL.Time} recurrenceId       The passed in recurrence id
9009     * @property {ICAL.Event} item              The occurrence
9010     * @property {ICAL.Time} startDate          The start of the occurrence
9011     * @property {ICAL.Time} endDate            The end of the occurrence
9012     */
9013
9014    /**
9015     * Returns the occurrence details based on its start time.  If the
9016     * occurrence has an exception will return the details for that exception.
9017     *
9018     * NOTE: this method is intend to be used in conjunction
9019     *       with the {@link ICAL.Event#iterator iterator} method.
9020     *
9021     * @param {ICAL.Time} occurrence time occurrence
9022     * @return {ICAL.Event.occurrenceDetails} Information about the occurrence
9023     */
9024    getOccurrenceDetails: function(occurrence) {
9025      var id = occurrence.toString();
9026      var utcId = occurrence.convertToZone(ICAL.Timezone.utcTimezone).toString();
9027      var item;
9028      var result = {
9029        //XXX: Clone?
9030        recurrenceId: occurrence
9031      };
9032
9033      if (id in this.exceptions) {
9034        item = result.item = this.exceptions[id];
9035        result.startDate = item.startDate;
9036        result.endDate = item.endDate;
9037        result.item = item;
9038      } else if (utcId in this.exceptions) {
9039        item = this.exceptions[utcId];
9040        result.startDate = item.startDate;
9041        result.endDate = item.endDate;
9042        result.item = item;
9043      } else {
9044        // range exceptions (RANGE=THISANDFUTURE) have a
9045        // lower priority then direct exceptions but
9046        // must be accounted for first. Their item is
9047        // always the first exception with the range prop.
9048        var rangeExceptionId = this.findRangeException(
9049          occurrence
9050        );
9051        var end;
9052
9053        if (rangeExceptionId) {
9054          var exception = this.exceptions[rangeExceptionId];
9055
9056          // range exception must modify standard time
9057          // by the difference (if any) in start/end times.
9058          result.item = exception;
9059
9060          var startDiff = this._rangeExceptionCache[rangeExceptionId];
9061
9062          if (!startDiff) {
9063            var original = exception.recurrenceId.clone();
9064            var newStart = exception.startDate.clone();
9065
9066            // zones must be same otherwise subtract may be incorrect.
9067            original.zone = newStart.zone;
9068            startDiff = newStart.subtractDate(original);
9069
9070            this._rangeExceptionCache[rangeExceptionId] = startDiff;
9071          }
9072
9073          var start = occurrence.clone();
9074          start.zone = exception.startDate.zone;
9075          start.addDuration(startDiff);
9076
9077          end = start.clone();
9078          end.addDuration(exception.duration);
9079
9080          result.startDate = start;
9081          result.endDate = end;
9082        } else {
9083          // no range exception standard expansion
9084          end = occurrence.clone();
9085          end.addDuration(this.duration);
9086
9087          result.endDate = end;
9088          result.startDate = occurrence;
9089          result.item = this;
9090        }
9091      }
9092
9093      return result;
9094    },
9095
9096    /**
9097     * Builds a recur expansion instance for a specific point in time (defaults
9098     * to startDate).
9099     *
9100     * @param {ICAL.Time} startTime     Starting point for expansion
9101     * @return {ICAL.RecurExpansion}    Expansion object
9102     */
9103    iterator: function(startTime) {
9104      return new ICAL.RecurExpansion({
9105        component: this.component,
9106        dtstart: startTime || this.startDate
9107      });
9108    },
9109
9110    /**
9111     * Checks if the event is recurring
9112     *
9113     * @return {Boolean}        True, if event is recurring
9114     */
9115    isRecurring: function() {
9116      var comp = this.component;
9117      return comp.hasProperty('rrule') || comp.hasProperty('rdate');
9118    },
9119
9120    /**
9121     * Checks if the event describes a recurrence exception. See
9122     * {@tutorial terminology} for details.
9123     *
9124     * @return {Boolean}    True, if the even describes a recurrence exception
9125     */
9126    isRecurrenceException: function() {
9127      return this.component.hasProperty('recurrence-id');
9128    },
9129
9130    /**
9131     * Returns the types of recurrences this event may have.
9132     *
9133     * Returned as an object with the following possible keys:
9134     *
9135     *    - YEARLY
9136     *    - MONTHLY
9137     *    - WEEKLY
9138     *    - DAILY
9139     *    - MINUTELY
9140     *    - SECONDLY
9141     *
9142     * @return {Object.<ICAL.Recur.frequencyValues, Boolean>}
9143     *          Object of recurrence flags
9144     */
9145    getRecurrenceTypes: function() {
9146      var rules = this.component.getAllProperties('rrule');
9147      var i = 0;
9148      var len = rules.length;
9149      var result = Object.create(null);
9150
9151      for (; i < len; i++) {
9152        var value = rules[i].getFirstValue();
9153        result[value.freq] = true;
9154      }
9155
9156      return result;
9157    },
9158
9159    /**
9160     * The uid of this event
9161     * @type {String}
9162     */
9163    get uid() {
9164      return this._firstProp('uid');
9165    },
9166
9167    set uid(value) {
9168      this._setProp('uid', value);
9169    },
9170
9171    /**
9172     * The start date
9173     * @type {ICAL.Time}
9174     */
9175    get startDate() {
9176      return this._firstProp('dtstart');
9177    },
9178
9179    set startDate(value) {
9180      this._setTime('dtstart', value);
9181    },
9182
9183    /**
9184     * The end date. This can be the result directly from the property, or the
9185     * end date calculated from start date and duration. Setting the property
9186     * will remove any duration properties.
9187     * @type {ICAL.Time}
9188     */
9189    get endDate() {
9190      var endDate = this._firstProp('dtend');
9191      if (!endDate) {
9192          var duration = this._firstProp('duration');
9193          endDate = this.startDate.clone();
9194          if (duration) {
9195              endDate.addDuration(duration);
9196          } else if (endDate.isDate) {
9197              endDate.day += 1;
9198          }
9199      }
9200      return endDate;
9201    },
9202
9203    set endDate(value) {
9204      if (this.component.hasProperty('duration')) {
9205        this.component.removeProperty('duration');
9206      }
9207      this._setTime('dtend', value);
9208    },
9209
9210    /**
9211     * The duration. This can be the result directly from the property, or the
9212     * duration calculated from start date and end date. Setting the property
9213     * will remove any `dtend` properties.
9214     * @type {ICAL.Duration}
9215     */
9216    get duration() {
9217      var duration = this._firstProp('duration');
9218      if (!duration) {
9219        return this.endDate.subtractDateTz(this.startDate);
9220      }
9221      return duration;
9222    },
9223
9224    set duration(value) {
9225      if (this.component.hasProperty('dtend')) {
9226        this.component.removeProperty('dtend');
9227      }
9228
9229      this._setProp('duration', value);
9230    },
9231
9232    /**
9233     * The location of the event.
9234     * @type {String}
9235     */
9236    get location() {
9237      return this._firstProp('location');
9238    },
9239
9240    set location(value) {
9241      return this._setProp('location', value);
9242    },
9243
9244    /**
9245     * The attendees in the event
9246     * @type {ICAL.Property[]}
9247     * @readonly
9248     */
9249    get attendees() {
9250      //XXX: This is way lame we should have a better
9251      //     data structure for this later.
9252      return this.component.getAllProperties('attendee');
9253    },
9254
9255
9256    /**
9257     * The event summary
9258     * @type {String}
9259     */
9260    get summary() {
9261      return this._firstProp('summary');
9262    },
9263
9264    set summary(value) {
9265      this._setProp('summary', value);
9266    },
9267
9268    /**
9269     * The event description.
9270     * @type {String}
9271     */
9272    get description() {
9273      return this._firstProp('description');
9274    },
9275
9276    set description(value) {
9277      this._setProp('description', value);
9278    },
9279
9280    /**
9281     * The organizer value as an uri. In most cases this is a mailto: uri, but
9282     * it can also be something else, like urn:uuid:...
9283     * @type {String}
9284     */
9285    get organizer() {
9286      return this._firstProp('organizer');
9287    },
9288
9289    set organizer(value) {
9290      this._setProp('organizer', value);
9291    },
9292
9293    /**
9294     * The sequence value for this event. Used for scheduling
9295     * see {@tutorial terminology}.
9296     * @type {Number}
9297     */
9298    get sequence() {
9299      return this._firstProp('sequence');
9300    },
9301
9302    set sequence(value) {
9303      this._setProp('sequence', value);
9304    },
9305
9306    /**
9307     * The recurrence id for this event. See {@tutorial terminology} for details.
9308     * @type {ICAL.Time}
9309     */
9310    get recurrenceId() {
9311      return this._firstProp('recurrence-id');
9312    },
9313
9314    set recurrenceId(value) {
9315      this._setTime('recurrence-id', value);
9316    },
9317
9318    /**
9319     * Set/update a time property's value.
9320     * This will also update the TZID of the property.
9321     *
9322     * TODO: this method handles the case where we are switching
9323     * from a known timezone to an implied timezone (one without TZID).
9324     * This does _not_ handle the case of moving between a known
9325     *  (by TimezoneService) timezone to an unknown timezone...
9326     *
9327     * We will not add/remove/update the VTIMEZONE subcomponents
9328     *  leading to invalid ICAL data...
9329     * @private
9330     * @param {String} propName     The property name
9331     * @param {ICAL.Time} time      The time to set
9332     */
9333    _setTime: function(propName, time) {
9334      var prop = this.component.getFirstProperty(propName);
9335
9336      if (!prop) {
9337        prop = new ICAL.Property(propName);
9338        this.component.addProperty(prop);
9339      }
9340
9341      // utc and local don't get a tzid
9342      if (
9343        time.zone === ICAL.Timezone.localTimezone ||
9344        time.zone === ICAL.Timezone.utcTimezone
9345      ) {
9346        // remove the tzid
9347        prop.removeParameter('tzid');
9348      } else {
9349        prop.setParameter('tzid', time.zone.tzid);
9350      }
9351
9352      prop.setValue(time);
9353    },
9354
9355    _setProp: function(name, value) {
9356      this.component.updatePropertyWithValue(name, value);
9357    },
9358
9359    _firstProp: function(name) {
9360      return this.component.getFirstPropertyValue(name);
9361    },
9362
9363    /**
9364     * The string representation of this event.
9365     * @return {String}
9366     */
9367    toString: function() {
9368      return this.component.toString();
9369    }
9370
9371  };
9372
9373  function compareRangeException(a, b) {
9374    if (a[0] > b[0]) return 1;
9375    if (b[0] > a[0]) return -1;
9376    return 0;
9377  }
9378
9379  return Event;
9380}());
9381/* This Source Code Form is subject to the terms of the Mozilla Public
9382 * License, v. 2.0. If a copy of the MPL was not distributed with this
9383 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9384 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */
9385
9386
9387/**
9388 * This symbol is further described later on
9389 * @ignore
9390 */
9391ICAL.ComponentParser = (function() {
9392  /**
9393   * @classdesc
9394   * The ComponentParser is used to process a String or jCal Object,
9395   * firing callbacks for various found components, as well as completion.
9396   *
9397   * @example
9398   * var options = {
9399   *   // when false no events will be emitted for type
9400   *   parseEvent: true,
9401   *   parseTimezone: true
9402   * };
9403   *
9404   * var parser = new ICAL.ComponentParser(options);
9405   *
9406   * parser.onevent(eventComponent) {
9407   *   //...
9408   * }
9409   *
9410   * // ontimezone, etc...
9411   *
9412   * parser.oncomplete = function() {
9413   *
9414   * };
9415   *
9416   * parser.process(stringOrComponent);
9417   *
9418   * @class
9419   * @alias ICAL.ComponentParser
9420   * @param {Object=} options        Component parser options
9421   * @param {Boolean} options.parseEvent        Whether events should be parsed
9422   * @param {Boolean} options.parseTimezeone    Whether timezones should be parsed
9423   */
9424  function ComponentParser(options) {
9425    if (typeof(options) === 'undefined') {
9426      options = {};
9427    }
9428
9429    var key;
9430    for (key in options) {
9431      /* istanbul ignore else */
9432      if (options.hasOwnProperty(key)) {
9433        this[key] = options[key];
9434      }
9435    }
9436  }
9437
9438  ComponentParser.prototype = {
9439
9440    /**
9441     * When true, parse events
9442     *
9443     * @type {Boolean}
9444     */
9445    parseEvent: true,
9446
9447    /**
9448     * When true, parse timezones
9449     *
9450     * @type {Boolean}
9451     */
9452    parseTimezone: true,
9453
9454
9455    /* SAX like events here for reference */
9456
9457    /**
9458     * Fired when parsing is complete
9459     * @callback
9460     */
9461    oncomplete: /* istanbul ignore next */ function() {},
9462
9463    /**
9464     * Fired if an error occurs during parsing.
9465     *
9466     * @callback
9467     * @param {Error} err details of error
9468     */
9469    onerror: /* istanbul ignore next */ function(err) {},
9470
9471    /**
9472     * Fired when a top level component (VTIMEZONE) is found
9473     *
9474     * @callback
9475     * @param {ICAL.Timezone} component     Timezone object
9476     */
9477    ontimezone: /* istanbul ignore next */ function(component) {},
9478
9479    /**
9480     * Fired when a top level component (VEVENT) is found.
9481     *
9482     * @callback
9483     * @param {ICAL.Event} component    Top level component
9484     */
9485    onevent: /* istanbul ignore next */ function(component) {},
9486
9487    /**
9488     * Process a string or parse ical object.  This function itself will return
9489     * nothing but will start the parsing process.
9490     *
9491     * Events must be registered prior to calling this method.
9492     *
9493     * @param {ICAL.Component|String|Object} ical      The component to process,
9494     *        either in its final form, as a jCal Object, or string representation
9495     */
9496    process: function(ical) {
9497      //TODO: this is sync now in the future we will have a incremental parser.
9498      if (typeof(ical) === 'string') {
9499        ical = ICAL.parse(ical);
9500      }
9501
9502      if (!(ical instanceof ICAL.Component)) {
9503        ical = new ICAL.Component(ical);
9504      }
9505
9506      var components = ical.getAllSubcomponents();
9507      var i = 0;
9508      var len = components.length;
9509      var component;
9510
9511      for (; i < len; i++) {
9512        component = components[i];
9513
9514        switch (component.name) {
9515          case 'vtimezone':
9516            if (this.parseTimezone) {
9517              var tzid = component.getFirstPropertyValue('tzid');
9518              if (tzid) {
9519                this.ontimezone(new ICAL.Timezone({
9520                  tzid: tzid,
9521                  component: component
9522                }));
9523              }
9524            }
9525            break;
9526          case 'vevent':
9527            if (this.parseEvent) {
9528              this.onevent(new ICAL.Event(component));
9529            }
9530            break;
9531          default:
9532            continue;
9533        }
9534      }
9535
9536      //XXX: ideally we should do a "nextTick" here
9537      //     so in all cases this is actually async.
9538      this.oncomplete();
9539    }
9540  };
9541
9542  return ComponentParser;
9543}());
9544