home *** CD-ROM | disk | FTP | other *** search
/ PC Professionell 2006 June / PCpro_2006_06.ISO / files / firefox / calendar_windows_latest.xpi / components / calRecurrenceInfo.js < prev    next >
Encoding:
JavaScript  |  2005-10-02  |  23.0 KB  |  664 lines

  1. /* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is lightning code.
  16.  *
  17.  * The Initial Developer of the Original Code is
  18.  *  Oracle Corporation
  19.  * Portions created by the Initial Developer are Copyright (C) 2005
  20.  * the Initial Developer. All Rights Reserved.
  21.  *
  22.  * Contributor(s):
  23.  *   Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
  24.  *
  25.  * Alternatively, the contents of this file may be used under the terms of
  26.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  27.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  28.  * in which case the provisions of the GPL or the LGPL are applicable instead
  29.  * of those above. If you wish to allow use of your version of this file only
  30.  * under the terms of either the GPL or the LGPL, and not to allow others to
  31.  * use your version of this file under the terms of the MPL, indicate your
  32.  * decision by deleting the provisions above and replace them with the notice
  33.  * and other provisions required by the GPL or the LGPL. If you do not delete
  34.  * the provisions above, a recipient may use your version of this file under
  35.  * the terms of any one of the MPL, the GPL or the LGPL.
  36.  *
  37.  * ***** END LICENSE BLOCK ***** */
  38.  
  39. function calRecurrenceInfo() {
  40.     this.mRecurrenceItems = new Array();
  41.     this.mExceptions = new Array();
  42. }
  43.  
  44. function calDebug() {
  45.     dump.apply(null, arguments);
  46. }
  47.  
  48. var calRecurrenceInfoClassInfo = {
  49.     getInterfaces: function (count) {
  50.         var ifaces = [
  51.             Components.interfaces.nsISupports,
  52.             Components.interfaces.calIRecurrenceInfo,
  53.             Components.interfaces.nsIClassInfo
  54.         ];
  55.         count.value = ifaces.length;
  56.         return ifaces;
  57.     },
  58.  
  59.     getHelperForLanguage: function (language) {
  60.         return null;
  61.     },
  62.  
  63.     contractID: "@mozilla.org/calendar/recurrence-info;1",
  64.     classDescription: "Calendar Recurrence Info",
  65.     classID: Components.ID("{04027036-5884-4a30-b4af-f2cad79f6edf}"),
  66.     implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT,
  67.     flags: 0
  68. };
  69.  
  70. calRecurrenceInfo.prototype = {
  71.     // QI with CI
  72.     QueryInterface: function(aIID) {
  73.         if (aIID.equals(Components.interfaces.nsISupports) ||
  74.             aIID.equals(Components.interfaces.calIRecurrenceInfo))
  75.             return this;
  76.  
  77.         if (aIID.equals(Components.interfaces.nsIClassInfo))
  78.             return calRecurrenceInfoClassInfo;
  79.  
  80.         throw Components.results.NS_ERROR_NO_INTERFACE;
  81.     },
  82.  
  83.     //
  84.     // Mutability bits
  85.     //
  86.     mImmutable: false,
  87.     get isMutable() { return !this.mImmutable; },
  88.     makeImmutable: function() {
  89.         if (this.mImmutable)
  90.             return;
  91.  
  92.         for each (ritem in this.mRecurrenceItems) {
  93.             if (ritem.isMutable)
  94.                 ritem.makeImmutable();
  95.         }
  96.  
  97.         for each (ex in this.mExceptions) {
  98.             if (ex.item.isMutable)
  99.                 ex.item.makeImmutable();
  100.         }
  101.  
  102.         this.mImmutable = true;
  103.     },
  104.  
  105.     clone: function() {
  106.         var cloned = new calRecurrenceInfo();
  107.         cloned.mBaseItem = this.mBaseItem;
  108.  
  109.         var clonedItems = [];
  110.         for each (ritem in this.mRecurrenceItems)
  111.             clonedItems.push(ritem.clone());
  112.         cloned.mRecurrenceItems = clonedItems;
  113.  
  114.         var clonedExceptions = [];
  115.         for each (exitem in this.mExceptions) {
  116.             var c = exitem.item.cloneShallow(this.mBaseItem);
  117.             clonedExceptions.push( { id: exitem.id, item: c } );
  118.         }
  119.         cloned.mExceptions = clonedExceptions;
  120.  
  121.         return cloned;
  122.     },
  123.  
  124.     //
  125.     // calIRecurrenceInfo impl
  126.     //
  127.     mBaseItem: null,
  128.  
  129.     get item() {
  130.         return this.mBaseItem;
  131.     },
  132.  
  133.     set item(value) {
  134.         if (this.mImmutable)
  135.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  136.  
  137.         this.mBaseItem = value;
  138.     },
  139.  
  140.     mRecurrenceItems: null,
  141.  
  142.     get isFinite() {
  143.         if (!this.mBaseItem)
  144.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  145.  
  146.         for each (ritem in this.mRecurrenceItems) {
  147.             if (!ritem.isFinite)
  148.                 return false;
  149.         }
  150.  
  151.         return true;
  152.     },
  153.  
  154.     getRecurrenceItems: function(aCount) {
  155.         if (!this.mBaseItem)
  156.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  157.  
  158.         aCount.value = this.mRecurrenceItems.length;
  159.         return this.mRecurrenceItems;
  160.     },
  161.  
  162.     setRecurrenceItems: function(aCount, aItems) {
  163.         if (!this.mBaseItem)
  164.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  165.  
  166.         if (this.mImmutable)
  167.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  168.  
  169.         // should we clone these?
  170.         this.mRecurrenceItems = aItems;
  171.     },
  172.  
  173.     countRecurrenceItems: function() {
  174.         if (!this.mBaseItem)
  175.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  176.  
  177.         return this.mRecurrenceItems.length;
  178.     },
  179.  
  180.     getRecurrenceItemAt: function(aIndex) {
  181.         if (!this.mBaseItem)
  182.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  183.  
  184.         if (aIndex < 0 || aIndex >= mRecurrenceItems.length)
  185.             throw Components.results.NS_ERROR_INVALID_ARG;
  186.  
  187.         return this.mRecurrenceItems[aIndex];
  188.     },
  189.  
  190.     appendRecurrenceItem: function(aItem) {
  191.         if (!this.mBaseItem)
  192.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  193.  
  194.         if (this.mImmutable)
  195.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  196.  
  197.         this.mRecurrenceItems.push(aItem);
  198.     },
  199.  
  200.     deleteRecurrenceItemAt: function(aIndex) {
  201.         if (!this.mBaseItem)
  202.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  203.  
  204.         if (this.mImmutable)
  205.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  206.  
  207.         if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length)
  208.             throw Components.results.NS_ERROR_INVALID_ARG;
  209.  
  210.         this.mRecurrenceItems.splice(aIndex, 1);
  211.     },
  212.  
  213.     deleteRecurrenceItem: function(aItem) {
  214.         if (!this.mBaseItem)
  215.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  216.  
  217.         if (this.mImmutable)
  218.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  219.  
  220.         for (var i = 0; i < this.mRecurrenceItems.length; i++) {
  221.             if (this.mRecurrenceItems[i] == aItem) {
  222.                 this.deleteRecurrenceItemAt(i);
  223.                 return;
  224.             }
  225.         }
  226.  
  227.         throw Components.results.NS_ERROR_INVALID_ARG;
  228.     },
  229.  
  230.     insertRecurrenceItemAt: function(aItem, aIndex) {
  231.         if (!this.mBaseItem)
  232.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  233.  
  234.         if (this.mImmutable)
  235.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  236.  
  237.         if (aIndex < 0 || aIndex > this.mRecurrenceItems.length)
  238.             throw Components.results.NS_ERROR_INVALID_ARG;
  239.  
  240.         this.mRecurrenceItems.splice(aIndex, 0, aItem);
  241.     },
  242.  
  243.     clearRecurrenceItems: function() {
  244.         if (!this.mBaseItem)
  245.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  246.  
  247.         if (this.mImmutable)
  248.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  249.  
  250.         this.mRecurrenceItems = new Array();
  251.     },
  252.  
  253.     //
  254.     // calculations
  255.     //
  256.  
  257.     getNextOccurrenceDate: function (aTime) {
  258.         if (!this.mBaseItem)
  259.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  260.  
  261.         var startDate = this.mBaseItem.recurrenceStartDate;
  262.         var dates = [];
  263.  
  264.         for each (ritem in this.mRecurrenceItems) {
  265.             var date = ritem.getNextOccurrence(startDate, aTime);
  266.             if (!date)
  267.                 continue;
  268.  
  269.             if (ritem.isNegative)
  270.                 dates = dates.filter(function (d) { return (d.compare(date) != 0); });
  271.             else
  272.                 dates.push(date);
  273.         }
  274.  
  275.         // if no dates, there's no next
  276.         if (dates.length == 0)
  277.             return null;
  278.  
  279.         // find the earliest date
  280.         var earliestDate = dates[0];
  281.         dates.forEach(function (d) { if (d.compare(earliestDate) < 0) earliestDate = d; });
  282.  
  283.         return earliestDate;
  284.     },
  285.  
  286.     getNextOccurrence: function (aTime) {
  287.         var earliestDate = this.getNextOccurrenceDate (aTime);
  288.         if (!earliestDate)
  289.             return null;
  290.  
  291.         if (this.mExceptions) {
  292.             // scan exceptions for any dates earlier than
  293.             // earliestDate (but still after aTime)
  294.             this.mExceptions.forEach (function (ex) {
  295.                                           var dtstart = ex.item.getProperty("DTSTART");
  296.                                           if (aTime.compare(dtstart) <= 0 &&
  297.                                               earliestDate.compare(dtstart) > 0)
  298.                                           {
  299.                                               earliestDate = dtstart;
  300.                                           }
  301.                                       });
  302.         }
  303.  
  304.         var startDate = earliestDate.clone();
  305.         var endDate = null;
  306.  
  307.         if (this.mBaseItem.hasProperty("DTEND")) {
  308.             endDate = earliestDate.clone();
  309.             endDate.addDuration(this.mBaseItem.duration);
  310.         }
  311.  
  312.         var proxy = this.mBaseItem.createProxy();
  313.         proxy.setRecurrenceId(earliestDate);
  314.  
  315.         proxy.setProperty("DTSTART", startDate);
  316.         if (endDate)
  317.             proxy.setProperty("DTEND", endDate);
  318.  
  319.         return proxy;
  320.     },
  321.  
  322.     // internal helper function; 
  323.     calculateDates: function (aRangeStart, aRangeEnd,
  324.                               aMaxCount, aIncludeExceptions, aReturnRIDs)
  325.     {
  326.         if (!this.mBaseItem)
  327.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  328.  
  329.         var startDate = this.mBaseItem.recurrenceStartDate;
  330.         var dates = [];
  331.  
  332.         for each (ritem in this.mRecurrenceItems) {
  333.             var cur_dates;
  334.  
  335.             // if both range start and end are specified, we ask for all of the occurrences,
  336.             // to make sure we catch all possible exceptions.  If aRangeEnd isn't specified,
  337.             // then we have to ask for aMaxCount, and hope for the best.
  338.             if (aRangeStart && aRangeEnd)
  339.                 cur_dates = ritem.getOccurrences(startDate, aRangeStart, aRangeEnd, 0, {});
  340.             else
  341.                 cur_dates = ritem.getOccurrences(startDate, aRangeStart, aRangeEnd, aMaxCount, {});
  342.  
  343.             if (cur_dates.length == 0)
  344.                 continue;
  345.  
  346.             if (ritem.isNegative) {
  347.                 // if this is negative, we look for any of the given dates
  348.                 // in the existing set, and remove them if they're
  349.                 // present.
  350.  
  351.                 // XXX: i'm pretty sure negative dates can't really have exceptions
  352.                 // (like, you can't make a date "real" by defining an RECURRENCE-ID which
  353.                 // is an EXDATE, and then giving it a real DTSTART) -- so we don't
  354.                 // check exceptions here
  355.                 cur_dates.forEach (function (dateToRemove) {
  356.                                        dates = dates.filter(function (d) { return d.compare(dateToRemove) != 0; });
  357.                                    });
  358.             } else {
  359.                 // if positive, we just add these date to the existing set,
  360.                 // but only if they're not already there
  361.                 var datesToAdd = [];
  362.                 var rinfo = this;
  363.                 cur_dates.forEach (function (dateToAdd) {
  364.                                        if (!dates.some(function (d) { return d.compare(dateToAdd) == 0; })) {
  365.                                            if (aIncludeExceptions && rinfo.mExceptions)
  366.                                            {
  367.                                                // only add if there's no exception for this;
  368.                                                // we'll test all exception dates later on
  369.                                                if (!rinfo.getExceptionFor(dateToAdd, false))
  370.                                                    dates.push(dateToAdd);
  371.                                            } else {
  372.                                                dates.push(dateToAdd);
  373.                                            }
  374.                                        }
  375.                                    });
  376.             }
  377.         }
  378.  
  379.         // Now toss in exceptions into this list; we just scan them all.
  380.         if (aIncludeExceptions && this.mExceptions) {
  381.             this.mExceptions.forEach(function(ex) {
  382.                                          var dtstart = ex.item.getProperty("DTSTART");
  383.                                          var dateToReturn;
  384.                                          if (aReturnRIDs)
  385.                                              dateToReturn = ex.id;
  386.                                          else
  387.                                              dateToReturn = dtstart;
  388.                                          // is our startdate within the range?
  389.                                          if ((!aRangeStart || aRangeStart.compare(dtstart) <= 0) &&
  390.                                              (!aRangeEnd || aRangeEnd.compare(dtstart) > 0))
  391.                                          {
  392.                                              dates.push(dateToReturn);
  393.                                              return;
  394.                                          }
  395.  
  396.                                          // is our end date within the range?
  397.                                          var dtend = ex.item.getProperty("DTEND");
  398.                                          if ((!aRangeStart || aRangeStart.compare(dtend) <= 0) &&
  399.                                              (!aRangeEnd || aRangeEnd.compare(dtend) > 0))
  400.                                          {
  401.                                              dates.push(dateToReturn);
  402.                                              return;
  403.                                          }
  404.  
  405.                                          // is the range in the middle of a long event?
  406.                                          if (aRangeStart && aRangeEnd &&
  407.                                              aRangeStart.compare(dtstart) >= 0 &&
  408.                                              aRangeEnd.compare(dtend) <= 0)
  409.                                          {
  410.                                              dates.push(dateToReturn);
  411.                                              return;
  412.                                          }
  413.                                      });
  414.         }
  415.  
  416.         // now sort the list
  417.         dates.sort(function (a,b) { return a.compare(b); });
  418.  
  419.         // chop anything over aMaxCount, if specified
  420.         if (aMaxCount && dates.length > aMaxCount)
  421.             dates = dates.splice(aMaxCount, dates.length - aMaxCount);
  422.  
  423.         return dates;
  424.     },
  425.  
  426.     getOccurrenceDates: function (aRangeStart, aRangeEnd,
  427.                                   aMaxCount, aCount)
  428.     {
  429.         var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, true, false);
  430.         aCount.value = dates.length;
  431.         return dates;
  432.     },
  433.  
  434.     getOccurrences: function (aRangeStart, aRangeEnd,
  435.                               aMaxCount,
  436.                               aCount)
  437.     {
  438.         var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, true, true);
  439.         if (dates.length == 0) {
  440.             aCount.value = 0;
  441.             return [];
  442.         }
  443.  
  444.         var count = aMaxCount;
  445.         if (!count)
  446.             count = dates.length;
  447.  
  448.         var results = [];
  449.  
  450.         for (var i = 0; i < count; i++) {
  451.             var proxy = this.getOccurrenceFor(dates[i]);
  452.             results.push(proxy);
  453.         }
  454.  
  455.         aCount.value = results.length;
  456.         return results;
  457.     },
  458.  
  459.     getOccurrenceFor: function (aRecurrenceId) {
  460.         var proxy = this.getExceptionFor(aRecurrenceId, false);
  461.         if (!proxy) {
  462.             var duration = null;
  463.             if (this.mBaseItem.hasProperty("DTEND"))
  464.                 duration = this.mBaseItem.duration;
  465.  
  466.             proxy = this.mBaseItem.createProxy();
  467.             proxy.recurrenceId = aRecurrenceId;
  468.             proxy.setProperty("DTSTART", aRecurrenceId.clone());
  469.             if (duration) {
  470.                 var enddate = aRecurrenceId.clone();
  471.                 enddate.addDuration(duration);
  472.                 proxy.setProperty("DTEND", enddate);
  473.             }
  474.             if (!this.mBaseItem.isMutable)
  475.                 proxy.makeImmutable();
  476.         }
  477.         return proxy;
  478.     },
  479.  
  480.     removeOccurrenceAt: function (aRecurrenceId) {
  481.         if (!this.mBaseItem)
  482.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  483.  
  484.         if (this.mImmutable)
  485.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  486.  
  487.         var d = Components.classes["@mozilla.org/calendar/recurrence-date;1"].createInstance(Components.interfaces.calIRecurrenceDate);
  488.         d.isNegative = true;
  489.         d.date = aRecurrenceId.clone();
  490.  
  491.         return this.appendRecurrenceItem(d);
  492.     },
  493.  
  494.     restoreOccurrenceAt: function (aRecurrenceId) {
  495.         if (!this.mBaseItem)
  496.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  497.  
  498.         if (this.mImmutable)
  499.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  500.  
  501.         for (var i = 0; i < this.mRecurrenceItems.length; i++) {
  502.             if (this.mRecurrenceItems[i] instanceof Components.interfaces.calIRecurrenceDate) {
  503.                 var rd = this.mRecurrenceItems[i].QueryInterface(Components.interfaces.calIRecurrenceDate);
  504.                 if (rd.isNegative && rd.date.compare(aRecurrenceId) == 0) {
  505.                     return this.deleteRecurrenceItemAt(i);
  506.                 }
  507.             }
  508.         }
  509.  
  510.         throw Components.results.NS_ERROR_INVALID_ARG;
  511.     },
  512.  
  513.     //
  514.     // exceptions
  515.     //
  516.  
  517.     //
  518.     // Some notes:
  519.     //
  520.     // The way I read ICAL, RECURRENCE-ID is used to specify a
  521.     // particular instance of a recurring event, according to the
  522.     // RRULEs/RDATEs/etc. specified in the base event.  If one of
  523.     // these is to be changed ("an exception"), then it can be
  524.     // referenced via the UID of the original event, and a
  525.     // RECURRENCE-ID of the start time of the instance to change.
  526.     // This, to me, means that an event where one of the instances has
  527.     // changed to a different time has a RECURRENCE-ID of the original
  528.     // start time, and a DTSTART/DTEND representing the new time.
  529.     //
  530.     // ITIP, however, seems to want something different -- you're
  531.     // supposed to use UID/RECURRENCE-ID to select from the current
  532.     // set of occurrences of an event.  If you change the DTSTART for
  533.     // an instance, you're supposed to use the old (original) DTSTART
  534.     // as the RECURRENCE-ID, and put the new time as the DTSTART.
  535.     // However, after that change, to refer to that instance in the
  536.     // future, you have to use the modified DTSTART as the
  537.     // RECURRENCE-ID.  This madness is described in ITIP end of
  538.     // section 3.7.1.
  539.     // 
  540.     // This implementation does the first approach (RECURRENCE-ID will
  541.     // never change even if DTSTART for that instance changes), which
  542.     // I think is the right thing to do for CalDAV; I don't know what
  543.     // we'll do for incoming ITIP events though.
  544.     //
  545.  
  546.     mExceptions: null,
  547.  
  548.     modifyException: function (anItem) {
  549.         if (!this.mBaseItem)
  550.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  551.  
  552.         // the item must be an occurrence
  553.         if (anItem.parentItem == anItem)
  554.             throw Components.results.NS_ERROR_UNEXPECTED;
  555.  
  556.         if (anItem.parentItem.calendar != this.mBaseItem.calendar &&
  557.             anItem.parentItem.id != this.mBaseItem.id)
  558.         {
  559.             calDebug ("recurrenceInfo::addException: item parentItem != this.mBaseItem (calendar/id)!\n");
  560.             throw Components.results.NS_ERROR_INVALID_ARG;
  561.         }
  562.  
  563.         if (anItem.recurrenceId == null) {
  564.             calDebug ("recurrenceInfo::addException: item with null recurrenceId!\n");
  565.             throw Components.results.NS_ERROR_INVALID_ARG;
  566.         }
  567.  
  568.         var itemtoadd;
  569.         if (anItem.isMutable) {
  570.             itemtoadd = anItem.cloneShallow(this.mBaseItem);
  571.             itemtoadd.makeImmutable();
  572.         } else {
  573.             itemtoadd = anItem;
  574.         }
  575.  
  576.         // we're going to assume that the recurrenceId is valid here,
  577.         // because presumably the item came from one of our functions
  578.  
  579.         // remove any old one, if present
  580.         this.removeExceptionFor(anItem.recurrenceId);
  581.  
  582.         this.mExceptions.push( { id: itemtoadd.recurrenceId, item: itemtoadd } );
  583.     },
  584.  
  585.     createExceptionFor: function (aRecurrenceId) {
  586.         if (!this.mBaseItem)
  587.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  588.  
  589.         // XX should it be an error to createExceptionFor
  590.         // an already-existing recurrenceId?
  591.         var existing = this.getExceptionFor(aRecurrenceId, false);
  592.         if (existing)
  593.             return existing;
  594.  
  595.         // check if aRecurrenceId is valid.
  596.  
  597.         // this is a bit of a hack; we know that ranges are defined as [start, end),
  598.         // so we do a search on aRecurrenceId and aRecurrenceId.seconds + 1.
  599.         var rangeStart = aRecurrenceId;
  600.         var rangeEnd = aRecurrenceId.clone();
  601.         rangeEnd.second += 1;
  602.         rangeEnd.normalize();
  603.  
  604.         var dates = this.getOccurrenceDates (rangeStart, rangeEnd, 1, {});
  605.         var found = false;
  606.         for each (d in dates) {
  607.             if (d.compare(aRecurrenceId) == 0) {
  608.                 found = true;
  609.                 break;
  610.             }
  611.         }
  612.  
  613.         // not found; the recurrence id is invalid
  614.         if (!found)
  615.             throw Components.results.NS_ERROR_INVALID_ARG;
  616.  
  617.         var rid = aRecurrenceId.clone();
  618.         rid.makeImmutable();
  619.  
  620.         var newex = this.mBaseItem.createProxy();
  621.         newex.recurrenceId = rid;
  622.  
  623.         this.mExceptions.push({id: rid, item: newex});
  624.  
  625.         return newex;
  626.     },
  627.  
  628.     getExceptionFor: function (aRecurrenceId, aCreate) {
  629.         if (!this.mBaseItem)
  630.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  631.  
  632.         for each (ex in this.mExceptions) {
  633.             if (ex.id.compare(aRecurrenceId) == 0)
  634.                 return ex.item;
  635.         }
  636.  
  637.         if (aCreate) {
  638.             return this.createExceptionFor(aRecurrenceId);
  639.         }
  640.         return null;
  641.     },
  642.  
  643.     removeExceptionFor: function (aRecurrenceId) {
  644.         if (!this.mBaseItem)
  645.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  646.  
  647.         this.mExceptions = this.mExceptions.filter (function(ex) {
  648.                                                         return (ex.id.compare(aRecurrenceId) != 0);
  649.                                                     });
  650.     },
  651.  
  652.     getExceptionIds: function (aCount) {
  653.         if (!this.mBaseItem)
  654.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  655.  
  656.         var ids = this.mExceptions.map (function(ex) {
  657.                                             return ex.id;
  658.                                         });
  659.  
  660.         aCount.value = ids.length;
  661.         return ids;
  662.     },
  663. };
  664.