Fix bug 932217 - Update ical.js to latest version.
authorMarkus Adrario <mozilla@adrario.de>
Fri, 21 Feb 2014 16:26:48 +0100
changeset 17599 9cbd527a1075de83f0da0e3557a14b678eeff6b4
parent 17598 e2b29f224ade93d5cff8f10e86bb36621fe71bd4
child 17600 0e575a37a7f50ea8885b8fe44989ef564c4ec6cb
push id1175
push usermbanner@mozilla.com
push dateTue, 18 Mar 2014 08:37:15 +0000
treeherdercomm-aurora@3b5242ee031a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs932217
Fix bug 932217 - Update ical.js to latest version.
calendar/base/modules/ical.js
--- a/calendar/base/modules/ical.js
+++ b/calendar/base/modules/ical.js
@@ -4,17 +4,17 @@
  * Portions Copyright (C) Philipp Kewisch, 2011-2012 */
 
 /**
  * This is ical.js from <https://github.com/mozilla-comm/ical.js>.
  *
  * If you would like to change anything in ical.js, it is required to do so
  * upstream first.
  *
- * Current ical.js git revision: b2112a5eb6fe2c311ee945150b2a267213c1f18c
+ * Current ical.js git revision: 9d3ee67d1ce9e0bb960e73a45e2dd434360cbd8d
  */
 
 var EXPORTED_SYMBOLS = ["ICAL", "unwrap", "unwrapSetter", "unwrapSingle", "wrapGetter"];
 
 function wrapGetter(type, val) {
     return val ? new type(val) : null;
 }
 
@@ -38,17 +38,17 @@ function unwrapSingle(type, val) {
     }
 }
 
 // -- start ical.js --
 
 if (typeof(ICAL) === 'undefined')
   (typeof(window) !== 'undefined') ? this.ICAL = {} : ICAL = {};
 
-ICAL.foldLength = 74;
+ICAL.foldLength = 75;
 ICAL.newLineChar = '\r\n';
 ICAL.debug = false;
 
 /**
  * Helper functions used in various places within ical.js
  */
 ICAL.helpers = {
   initState: function initState(aLine, aLineNr) {
@@ -1231,66 +1231,74 @@ ICAL.parse = (function() {
     return name.toLowerCase();
   }
 
   parser._handleContentLine = function(line, state) {
     // break up the parts of the line
     var valuePos = line.indexOf(VALUE_DELIMITER);
     var paramPos = line.indexOf(PARAM_DELIMITER);
 
-    var nextPos = 0;
+    var lastParamIndex;
+    var lastValuePos;
+
     // name of property or begin/end
     var name;
     var value;
-    var params;
+    // params is only overridden if paramPos !== -1.
+    // we can't do params = params || {} later on
+    // because it sacrifices ops.
+    var params = {};
 
     /**
      * Different property cases
      *
      *
      * 1. RRULE:FREQ=foo
      *    // FREQ= is not a param but the value
      *
      * 2. ATTENDEE;ROLE=REQ-PARTICIPANT;
      *    // ROLE= is a param because : has not happened yet
      */
+      // when the parameter delimiter is after the
+      // value delimiter then its not a parameter.
 
     if ((paramPos !== -1 && valuePos !== -1)) {
       // when the parameter delimiter is after the
       // value delimiter then its not a parameter.
       if (paramPos > valuePos) {
         paramPos = -1;
       }
     }
 
+    var parsedParams;
     if (paramPos !== -1) {
-      // when there are parameters (ATTENDEE;RSVP=TRUE;)
-      name = parser._formatName(line.substr(0, paramPos));
-      params = parser._parseParameters(line, paramPos);
-      if (valuePos !== -1) {
-        value = line.substr(valuePos + 1);
+      name = line.substring(0, paramPos).toLowerCase();
+      parsedParams = parser._parseParameters(line.substring(paramPos), 0);
+      params = parsedParams[0];
+      lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos;
+      if ((lastValuePos =
+        line.substring(lastParamIndex).indexOf(VALUE_DELIMITER)) !== -1) {
+        value = line.substring(lastParamIndex + lastValuePos + 1);
       }
     } else if (valuePos !== -1) {
       // without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC)
-      name = parser._formatName(line.substr(0, valuePos));
-      value = line.substr(valuePos + 1);
+      name = line.substring(0, valuePos).toLowerCase();
+      value = line.substring(valuePos + 1);
 
       if (name === 'begin') {
-        var newComponent = [parser._formatName(value), [], []];
+        var newComponent = [value.toLowerCase(), [], []];
         if (state.stack.length === 1) {
           state.component.push(newComponent);
         } else {
           state.component[2].push(newComponent);
         }
         state.stack.push(state.component);
         state.component = newComponent;
         return;
-      }
-
-      if (name === 'end') {
+      } else if (name === 'end') {
         state.component = state.stack.pop();
         return;
       }
     } else {
       /**
        * Invalid line.
        * The rational to throw an error is we will
        * never be certain that the rest of the file
@@ -1313,19 +1321,16 @@ ICAL.parse = (function() {
         multiValue = propertyDetails.multiValue;
       }
 
       if (value && 'detectType' in propertyDetails) {
         valueType = propertyDetails.detectType(value);
       }
     }
 
-    // at this point params is mandatory per jcal spec
-    params = params || {};
-
     // attempt to determine value
     if (!valueType) {
       if (!('value' in params)) {
         if (propertyDetails) {
           valueType = propertyDetails.defaultType;
         } else {
           valueType = DEFAULT_TYPE;
         }
@@ -1380,73 +1385,65 @@ ICAL.parse = (function() {
    * @param {Numeric} maxPos position at which values start.
    * @return {Object} key/value pairs.
    */
   parser._parseParameters = function(line, start) {
     var lastParam = start;
     var pos = 0;
     var delim = PARAM_NAME_DELIMITER;
     var result = {};
+    var name;
+    var value;
+    var type;
 
     // find the next '=' sign
     // use lastParam and pos to find name
     // check if " is used if so get value from "->"
     // then increment pos to find next ;
 
     while ((pos !== false) &&
            (pos = helpers.unescapedIndexOf(line, delim, pos + 1)) !== -1) {
 
-      var name = line.substr(lastParam + 1, pos - lastParam - 1);
+      name = line.substr(lastParam + 1, pos - lastParam - 1);
 
       var nextChar = line[pos + 1];
-      var substrOffset = -2;
-
       if (nextChar === '"') {
         var valuePos = pos + 2;
         pos = helpers.unescapedIndexOf(line, '"', valuePos);
-        var value = line.substr(valuePos, pos - valuePos);
+        value = line.substr(valuePos, pos - valuePos);
         lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos);
       } else {
         var valuePos = pos + 1;
-        substrOffset = -1;
 
         // move to next ";"
         var nextPos = helpers.unescapedIndexOf(line, PARAM_DELIMITER, valuePos);
-
         if (nextPos === -1) {
           // when there is no ";" attempt to locate ":"
           nextPos = helpers.unescapedIndexOf(line, VALUE_DELIMITER, valuePos);
-          // no more tokens end of the line use .length
+
           if (nextPos === -1) {
             nextPos = line.length;
-            // because we are at the end we don't need to trim
-            // the found value of substr offset is zero
-            substrOffset = 0;
-          } else {
-            // next token is the beginning of the value
-            // so we must stop looking for the '=' token.
-            pos = false;
           }
+          pos = false;
         } else {
           lastParam = nextPos;
         }
 
-        var value = line.substr(valuePos, nextPos - valuePos);
-      }
-
-      var type = DEFAULT_TYPE;
+        value = line.substr(valuePos, nextPos - valuePos);
+      }
 
       if (name in design.param && design.param[name].valueType) {
         type = design.param[name].valueType;
-      }
-
-      result[parser._formatName(name)] = parser._parseValue(value, type);
+      } else {
+        type = DEFAULT_TYPE;
+      }
+
+      result[name.toLowerCase()] = parser._parseValue(value, type);
     }
-
-    return result;
+    return [result, value, valuePos];
   }
 
   /**
    * Parse a multi value string
    */
   parser._parseMultiValue = function(buffer, delim, type, result) {
     var pos = 0;
     var lastPos = 0;
@@ -1538,19 +1535,17 @@ ICAL.Component = (function() {
     if (typeof(jCal) === 'string') {
       // jCal spec (name, properties, components)
       jCal = [jCal, [], []];
     }
 
     // mostly for legacy reasons.
     this.jCal = jCal;
 
-    if (parent) {
-      this.parent = parent;
-    }
+    this.parent = parent || null;
   }
 
   Component.prototype = {
     /**
      * Hydrated properties are inserted into the _properties array at the same
      * position as in the jCal array, so its possible the array contains
      * undefined values for unhydrdated properties. To avoid iterating the
      * array when checking if all properties have been hydrated, we save the
@@ -1766,16 +1761,20 @@ ICAL.Component = (function() {
       }
 
       return null;
     },
 
     _removeObjectByIndex: function(jCalIndex, cache, index) {
       // remove cached version
       if (cache && cache[index]) {
+        var obj = cache[index];
+        if ("parent" in obj) {
+            obj.parent = null;
+        }
         cache.splice(index, 1);
       }
 
       // remove it from the jCal
       this.jCal[jCalIndex].splice(index, 1);
     },
 
     _removeObject: function(jCalIndex, cache, nameOrObject) {
@@ -1801,50 +1800,45 @@ ICAL.Component = (function() {
       }
 
       return false;
     },
 
     _removeAllObjects: function(jCalIndex, cache, name) {
       var cached = this[cache];
 
-      if (name) {
-        var objects = this.jCal[jCalIndex];
-        var i = objects.length - 1;
-
-        // descending search required because splice
-        // is used and will effect the indices.
-        for (; i >= 0; i--) {
-          if (objects[i][NAME_INDEX] === name) {
-            this._removeObjectByIndex(jCalIndex, cached, i);
-          }
+      // Unfortunately we have to run through all children to reset their
+      // parent property.
+      var objects = this.jCal[jCalIndex];
+      var i = objects.length - 1;
+
+      // descending search required because splice
+      // is used and will effect the indices.
+      for (; i >= 0; i--) {
+        if (!name || objects[i][NAME_INDEX] === name) {
+          this._removeObjectByIndex(jCalIndex, cached, i);
         }
-      } else {
-        if (cache in this) {
-          // I think its probable that when we remove all
-          // of a type we may want to add to it again so it
-          // makes sense to reuse the object in that case.
-          // For now we remove the contents of the array.
-          this[cache].length = 0;
-        }
-        this.jCal[jCalIndex].length = 0;
       }
     },
 
     /**
      * Adds a single sub component.
      *
      * @param {ICAL.Component} component to add.
      */
     addSubcomponent: function(component) {
       if (!this._components) {
         this._components = [];
         this._hydratedComponentCount = 0;
       }
 
+      if (component.parent) {
+        component.parent.removeSubcomponent(component);
+      }
+
       var idx = this.jCal[COMPONENT_INDEX].push(component.jCal);
       this._components[idx - 1] = component;
       this._hydratedComponentCount++;
       component.parent = this;
     },
 
     /**
      * Removes a single component by name or
@@ -1878,39 +1872,43 @@ ICAL.Component = (function() {
      *
      * @param {ICAL.Property} property object.
      */
     addProperty: function(property) {
       if (!(property instanceof ICAL.Property)) {
         throw new TypeError('must instance of ICAL.Property');
       }
 
-      var idx = this.jCal[PROPERTY_INDEX].push(property.jCal);
-      property.component = this;
-
       if (!this._properties) {
         this._properties = [];
         this._hydratedPropertyCount = 0;
       }
 
+
+      if (property.parent) {
+        property.parent.removeProperty(property);
+      }
+
+      var idx = this.jCal[PROPERTY_INDEX].push(property.jCal);
       this._properties[idx - 1] = property;
       this._hydratedPropertyCount++;
+      property.parent = this;
     },
 
     /**
      * Helper method to add a property with a value to the component.
      *
      * @param {String} name property name to add.
      * @param {Object} value property value.
      */
     addPropertyWithValue: function(name, value) {
-      var prop = new ICAL.Property(name, this);
+      var prop = new ICAL.Property(name);
       prop.setValue(value);
 
-      this.addProperty(prop, this);
+      this.addProperty(prop);
 
       return prop;
     },
 
     /**
      * Helper method that will update or create a property
      * of the given name and sets its value.
      *
@@ -1988,19 +1986,19 @@ ICAL.Property = (function() {
    *
    * Can also be used to create new properties by passing
    * the name of the property (as a String).
    *
    *
    * @param {Array|String} jCal raw jCal representation OR
    *  the new name of the property (when creating).
    *
-   * @param {ICAL.Component} [component] parent component.
+   * @param {ICAL.Component} [parent] parent component.
    */
-  function Property(jCal, component) {
+  function Property(jCal, parent) {
     if (typeof(jCal) === 'string') {
       // because we a creating by name we need
       // to find the type when creating the property.
       var name = jCal;
 
       if (name in design.property) {
         var prop = design.property[name];
         if ('defaultType' in prop) {
@@ -2011,17 +2009,17 @@ ICAL.Property = (function() {
       } else {
         var type = design.defaultType;
       }
 
       jCal = [name, {}, type];
     }
 
     this.jCal = jCal;
-    this.component = component;
+    this.parent = parent || null;
     this._updateType();
   }
 
   Property.prototype = {
     get type() {
       return this.jCal[TYPE_INDEX];
     },
 
@@ -2226,17 +2224,18 @@ ICAL.Property = (function() {
       } else {
         for (; i < len; i++) {
           this.jCal[VALUE_INDEX + i] = values[i];
         }
       }
     },
 
     /**
-     * Sets the current value of the property.
+     * Sets the current value of the property. If this is a multi-value
+     * property, all other values will be removed.
      *
      * @param {String|Object} value new prop value.
      */
     setValue: function(value) {
       this.removeAllValues();
       if (typeof(value) === 'object' && 'icaltype' in value) {
         this.resetType(value.icaltype);
       }
@@ -2457,17 +2456,17 @@ ICAL.Binary = (function() {
 
     if (aData && 'start' in aData) {
       if (aData.start && !(aData.start instanceof ICAL.Time)) {
         throw new TypeError('.start must be an instance of ICAL.Time');
       }
       this.start = aData.start;
     }
 
-    if (aData && ('end' in aData) && ('duration' in aData)) {
+    if (aData && aData.end && aData.duration) {
       throw new Error('cannot accept both end and duration');
     }
 
     if (aData && 'end' in aData) {
       if (aData.end && !(aData.end instanceof ICAL.Time)) {
         throw new TypeError('.end must be an instance of ICAL.Time');
       }
       this.end = aData.end;
@@ -2484,16 +2483,24 @@ ICAL.Binary = (function() {
   ICAL.Period.prototype = {
 
     start: null,
     end: null,
     duration: null,
     icalclass: "icalperiod",
     icaltype: "period",
 
+    clone: function() {
+      return ICAL.Period.fromData({
+        start: this.start ? this.start.clone() : null,
+        end: this.end ? this.end.clone() : null,
+        duration: this.duration ? this.duration.clone() : null
+      });
+    },
+
     getDuration: function duration() {
       if (this.duration) {
         return this.duration;
       } else {
         return this.end.subtractDate(this.start);
       }
     },
 
@@ -3288,17 +3295,21 @@ ICAL.TimezoneService = (function() {
     time.isDate = false;
 
     this.fromData(data, zone);
   };
 
   ICAL.Time.prototype = {
 
     icalclass: "icaltime",
-    icaltype: "date-time",
+
+    // is read only strictly defined by isDate
+    get icaltype() {
+      return this.isDate ? 'date' : 'date-time';
+    },
 
     /**
      * @type ICAL.Timezone
      */
     zone: null,
 
     /**
      * Internal uses to indicate that a change has been
@@ -3366,31 +3377,31 @@ ICAL.TimezoneService = (function() {
           this.second = aDate.getSeconds();
         }
       }
       return this;
     },
 
     fromData: function fromData(aData, aZone) {
       for (var key in aData) {
+        // ical type cannot be set
+        if (key === 'icaltype') continue;
         this[key] = aData[key];
       }
 
       if (aZone) {
         this.zone = aZone;
       }
 
       if (aData && !("isDate" in aData)) {
         this.isDate = !("hour" in aData);
       } else if (aData && ("isDate" in aData)) {
         this.isDate = aData.isDate;
       }
 
-      this.icaltype = (this.isDate ? "date" : "date-time");
-
       if (aData && "timezone" in aData) {
         var zone = ICAL.TimezoneService.get(
           aData.timezone
         );
 
         this.zone = zone || ICAL.Timezone.localTimezone;
       }
 
@@ -3779,17 +3790,16 @@ ICAL.TimezoneService = (function() {
 
     _normalize: function icaltime_normalize() {
       var isDate = this._time.isDate;
       if (this._time.isDate) {
         this._time.hour = 0;
         this._time.minute = 0;
         this._time.second = 0;
       }
-      this.icaltype = (isDate ? "date" : "date-time");
       this.adjust(0, 0, 0, 0);
 
       return this;
     },
 
     adjust: function icaltime_adjust(aExtraDays, aExtraHours,
                                      aExtraMinutes, aExtraSeconds, aTime) {
 
@@ -4437,17 +4447,17 @@ ICAL.TimezoneService = (function() {
     BYYEARDAY: parseNumericValue.bind(this, 'BYYEARDAY', -366, 366),
     BYWEEKNO: parseNumericValue.bind(this, 'BYWEEKNO', -53, 53),
     BYMONTH: parseNumericValue.bind(this, 'BYMONTH', 0, 12),
     BYSETPOS: parseNumericValue.bind(this, 'BYSETPOS', -366, 366)
   };
 
   ICAL.Recur.fromString = function(string) {
     var dict = Object.create(null);
-    var dictParts = dict.parts = {};
+    var dictParts = dict.parts = Object.create(null);
 
     // split is slower in FF but fast enough.
     // v8 however this is faster then manual split?
     var values = string.split(';');
     var len = values.length;
 
     for (var i = 0; i < len; i++) {
       var parts = values[i].split('=');
@@ -4877,129 +4887,137 @@ ICAL.RecurIterator = (function() {
         // only add unique items...
         if (newRules.indexOf(rule) === -1) {
           newRules.push(rule);
         }
 
       }
 
       // unique and sort
-      return newRules.sort();
+      return newRules.sort(function(a,b){return a - b});
     },
 
     /**
      * NOTES:
      * We are given a list of dates in the month (BYMONTHDAY) (23, etc..)
      * Also we are given a list of days (BYDAY) (MO, 2SU, etc..) when
      * both conditions match a given date (this.last.day) iteration stops.
      *
      * @param {Boolean} [isInit] when given true will not
      *                           increment the current day (this.last).
      */
     _byDayAndMonthDay: function(isInit) {
-     var byMonthDay; // setup in initMonth
+      var byMonthDay; // setup in initMonth
       var byDay = this.by_data.BYDAY;
 
       var date;
       var dateIdx = 0;
       var dateLen; // setup in initMonth
-      var dayIdx = 0;
       var dayLen = byDay.length;
 
       // we are not valid by default
       var dataIsValid = 0;
 
       var daysInMonth;
       var self = this;
+      // we need a copy of this, because a DateTime gets normalized
+      // automatically if the day is out of range. At some points we 
+      // set the last day to 0 to start counting.
+      var lastDay = this.last.day;
 
       function initMonth() {
         daysInMonth = ICAL.Time.daysInMonth(
           self.last.month, self.last.year
         );
 
         byMonthDay = self.normalizeByMonthDayRules(
           self.last.year,
           self.last.month,
           self.by_data.BYMONTHDAY
         );
 
         dateLen = byMonthDay.length;
+
+        // For the case of more than one occurrence in one month
+        // we have to be sure to start searching after the last
+        // found date or at the last BYMONTHDAY.
+        while (byMonthDay[dateIdx] <= lastDay && dateIdx < dateLen - 1) {
+          dateIdx++;
+        }
       }
 
       function nextMonth() {
-        self.last.day = 1;
+        // since the day is incremented at the start
+        // of the loop below, we need to start at 0
+        lastDay = 0;
         self.increment_month();
+        dateIdx = 0;
         initMonth();
-
-        dateIdx = 0;
-        dayIdx = 0;
       }
 
       initMonth();
 
       // should come after initMonth
       if (isInit) {
-        this.last.day -= 1;
+        lastDay -= 1;
       }
 
       while (!dataIsValid) {
-        // find next date
-        var next = byMonthDay[dateIdx++];
-
         // increment the current date. This is really
         // important otherwise we may fall into the infinite
         // loop trap. The initial date takes care of the case
         // where the current date is the date we are looking
         // for.
-        date = this.last.day + 1;
+        date = lastDay + 1;
 
         if (date > daysInMonth) {
           nextMonth();
           continue;
         }
 
-        // after verify that the next date
-        // is in the current month we can increment
-        // it permanently.
-        this.last.day = date;
+        // find next date
+        var next = byMonthDay[dateIdx++];
 
         // this logic is dependant on the BYMONTHDAYS
         // being in order (which is done by #normalizeByMonthDayRules)
-        if (next >= this.last.day) {
+        if (next >= date) {
           // if the next month day is in the future jump to it.
-          this.last.day = next;
+          lastDay = next;
         } else {
           // in this case the 'next' monthday has past
           // we must move to the month.
           nextMonth();
           continue;
         }
 
         // Now we can loop through the day rules to see
         // if one matches the current month date.
-        for (dayIdx = 0; dayIdx < dayLen; dayIdx++) {
+        for (var dayIdx = 0; dayIdx < dayLen; dayIdx++) {
           var parts = this.ruleDayOfWeek(byDay[dayIdx]);
           var pos = parts[0];
           var dow = parts[1];
 
+          this.last.day = lastDay;
           if (this.last.isNthWeekDay(dow, pos)) {
             // when we find the valid one we can mark
             // the conditions as met and break the loop.
             // (Because we have this condition above
             //  it will also break the parent loop).
             dataIsValid = 1;
             break;
           }
         }
 
         // Its completely possible that the combination
         // cannot be matched in the current month.
         // When we reach the end of possible combinations
         // in the current month we iterate to the next one.
-        if (!dataIsValid && dateIdx === (dateLen - 1)) {
+        // since dateIdx is incremented right after getting
+        // "next", we don't need dateLen -1 here.
+        if (!dataIsValid && dateIdx === dateLen) {
           nextMonth();
           continue;
         }
       }
 
       return dataIsValid;
     },
 
@@ -5068,19 +5086,20 @@ ICAL.RecurIterator = (function() {
 
         if (day < 0) {
           day = daysInMonth + day + 1;
         }
 
         if (day > daysInMonth) {
           this.last.day = 1;
           data_valid = this.is_day_in_byday(this.last);
+        } else {
+          this.last.day = day;
         }
 
-        this.last.day = day;
       } else {
         this.last.day = this.by_data.BYMONTHDAY[0];
         this.increment_month();
         var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
         this.last.day = Math.min(this.last.day, daysInMonth);
       }
 
       return data_valid;
@@ -5096,17 +5115,17 @@ ICAL.RecurIterator = (function() {
       if (!this.has_by_data("BYDAY")) {
         return 1;
       }
 
       for (;;) {
         var tt = new ICAL.Time();
         this.by_indices.BYDAY++;
 
-        if (this.by_indices.BYDAY == this.by_data.BYDAY.length) {
+        if (this.by_indices.BYDAY == Object.keys(this.by_data.BYDAY).length) {
           this.by_indices.BYDAY = 0;
           end_of_data = 1;
         }
 
         var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY];
         var parts = this.ruleDayOfWeek(coded_day);
         var dow = parts[1];
 
@@ -5218,27 +5237,27 @@ ICAL.RecurIterator = (function() {
         if (this.last.day > daysInMonth) {
           this.last.day -= daysInMonth;
           this.increment_month();
         }
       }
     },
 
     increment_month: function increment_month() {
+      this.last.day = 1;
       if (this.has_by_data("BYMONTH")) {
         this.by_indices.BYMONTH++;
 
         if (this.by_indices.BYMONTH == this.by_data.BYMONTH.length) {
           this.by_indices.BYMONTH = 0;
           this.increment_year(1);
         }
 
         this.last.month = this.by_data.BYMONTH[this.by_indices.BYMONTH];
       } else {
-        var inc;
         if (this.rule.freq == "MONTHLY") {
           this.last.month += this.rule.interval;
         } else {
           this.last.month++;
         }
 
         this.last.month--;
         var years = ICAL.helpers.trunc(this.last.month / 12);
@@ -6277,26 +6296,32 @@ ICAL.Event = (function() {
      *
      * NOTE: this method is intend to be used in conjunction
      *       with the #iterator method.
      *
      * @param {ICAL.Time} occurrence time occurrence.
      */
     getOccurrenceDetails: function(occurrence) {
       var id = occurrence.toString();
+      var utcId = occurrence.convertToZone(ICAL.Timezone.utcTimezone).toString();
       var result = {
         //XXX: Clone?
         recurrenceId: occurrence
       };
 
       if (id in this.exceptions) {
         var item = result.item = this.exceptions[id];
         result.startDate = item.startDate;
         result.endDate = item.endDate;
         result.item = item;
+      } else if (utcId in this.exceptions) {
+        var item = this.exceptions[utcId];
+        result.startDate = item.startDate;
+        result.endDate = item.endDate;
+        result.item = item;
       } else {
         // range exceptions (RANGE=THISANDFUTURE) have a
         // lower priority then direct exceptions but
         // must be accounted for first. Their item is
         // always the first exception with the range prop.
         var rangeExceptionId = this.findRangeException(
           occurrence
         );