[california/wip/725785-create-recurring] Daily recurring rules parser fleshed out w/ tests



commit 0bdda9869b53ade040e57c559430d752b27f3604
Author: Jim Nelson <jim yorba org>
Date:   Thu Jun 19 18:08:01 2014 -0700

    Daily recurring rules parser fleshed out w/ tests

 src/Makefile.am                              |    1 +
 src/calendar/calendar-day-of-month.vala      |    3 +-
 src/calendar/calendar-duration.vala          |   35 ------
 src/calendar/calendar.vala                   |   21 ----
 src/component/component-date-time.vala       |    2 +-
 src/component/component-details-parser.vala  |  167 +++++++++++++++++++++-----
 src/component/component-instance.vala        |   39 +-----
 src/component/component-recurrence-rule.vala |  131 ++++++++++++++++----
 src/component/component.vala                 |   91 +++++++++++++--
 src/tests/tests-quick-add-recurring.vala     |  100 +++++++++++++++
 src/tests/tests-quick-add.vala               |   29 +++++
 src/tests/tests.vala                         |    1 +
 src/unit-test/unit-test-harness.vala         |   12 ++-
 vapi/libical.vapi                            |   32 +++---
 14 files changed, 487 insertions(+), 177 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 642af78..1de537a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -108,6 +108,7 @@ california_VALASOURCES = \
        tests/tests-calendar-month-span.vala \
        tests/tests-calendar-wall-time.vala \
        tests/tests-quick-add.vala \
+       tests/tests-quick-add-recurring.vala \
        tests/tests-string.vala \
        \
        toolkit/toolkit.vala \
diff --git a/src/calendar/calendar-day-of-month.vala b/src/calendar/calendar-day-of-month.vala
index a46ecf2..5732405 100644
--- a/src/calendar/calendar-day-of-month.vala
+++ b/src/calendar/calendar-day-of-month.vala
@@ -16,6 +16,7 @@ namespace California.Calendar {
 public class DayOfMonth : BaseObject, Gee.Comparable<DayOfMonth>, Gee.Hashable<DayOfMonth> {
     public const int MIN = 1;
     public const int MAX = 31;
+    public const int COUNT = MAX - MIN + 1;
     
     private static DayOfMonth[]? days = null;
     
@@ -44,7 +45,7 @@ public class DayOfMonth : BaseObject, Gee.Comparable<DayOfMonth>, Gee.Hashable<D
     }
     
     internal static void init() {
-        days = new DayOfMonth[MAX - MIN + 1];
+        days = new DayOfMonth[COUNT];
         for (int ctr = MIN; ctr <= MAX; ctr++)
             days[ctr - MIN] = new DayOfMonth(ctr);
     }
diff --git a/src/calendar/calendar-duration.vala b/src/calendar/calendar-duration.vala
index 1fdc9fb..24d13ac 100644
--- a/src/calendar/calendar-duration.vala
+++ b/src/calendar/calendar-duration.vala
@@ -42,41 +42,6 @@ public class Duration : BaseObject {
             + seconds;
     }
     
-    /**
-     * Parses the two tokens into a { link Duration}.
-     *
-     * parse() is looking for a pattern where the first token is a number and the second a string
-     * of units of time (localized), either hours, minutes, or seconds.  null is returned if that
-     * pattern is not located.
-     *
-     * Future expansion could include a pattern where the first token has a unit as a suffix, i.e.
-     * "3hrs" or "4m".
-     *
-     * It's possible for this call to return a Duration of zero time.
-     */
-    public static Duration? parse(string value, string unit) {
-        if (String.is_empty(value) || String.is_empty(unit))
-            return null;
-        
-        if (!String.is_numeric(value))
-            return null;
-        
-        int duration = int.parse(value);
-        if (duration < 0)
-            return null;
-        
-        if (unit in UNIT_DAYS)
-            return new Duration(duration);
-        
-        if (unit in UNIT_HOURS)
-            return new Duration(0, duration);
-        
-        if (unit in UNIT_MINS)
-            return new Duration(0, 0, duration);
-        
-        return null;
-    }
-    
     public override string to_string() {
         return "%ss".printf(seconds.to_string());
     }
diff --git a/src/calendar/calendar.vala b/src/calendar/calendar.vala
index 96fa49a..f16735d 100644
--- a/src/calendar/calendar.vala
+++ b/src/calendar/calendar.vala
@@ -75,10 +75,6 @@ private unowned string FMT_24HOUR_MIN_SEC;
 private unowned string MIDNIGHT;
 private unowned string NOON;
 
-private string[] UNIT_DAYS;
-private string[] UNIT_HOURS;
-private string[] UNIT_MINS;
-
 public void init() throws Error {
     if (!California.Unit.do_init(ref init_count))
         return;
@@ -207,21 +203,6 @@ public void init() throws Error {
     // The 24-hour time with minutes and seconds, i.e. "17:06:31"
     FMT_24HOUR_MIN_SEC = _("%02d:%02d:%02d");
     
-    // Used by quick-add to convert a user's day unit into an internal value.  Common abbreviations
-    // (without punctuation) should be included.  Each word must be separated by semi-colons.
-    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
-    UNIT_DAYS = _("day;days;").casefold().split(";");
-    
-    // Used by quick-add to convert a user's hours unit into an internal value.  Common abbreviations
-    // (without punctuation) should be included.  Each word must be separated by semi-colons.
-    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
-    UNIT_HOURS = _("hour;hours;hr;hrs").casefold().split(";");
-    
-    // Used by quick-add to convert a user's minute unit into an internal value.  Common abbreviations
-    // (without punctuation) should be included.  Each word must be separated by semi-colons.
-    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
-    UNIT_MINS = _("minute;minutes;min;mins").casefold().split(";");
-    
     // return LC_MESSAGES back to proper locale and return LANGUAGE environment variable
     if (messages_locale != null)
         Intl.setlocale(LocaleCategory.MESSAGES, messages_locale);
@@ -262,8 +243,6 @@ public void terminate() {
     DayOfWeek.terminate();
     OlsonZone.terminate();
     Collection.terminate();
-    
-    UNIT_DAYS = UNIT_HOURS = UNIT_MINS = null;
 }
 
 }
diff --git a/src/component/component-date-time.vala b/src/component/component-date-time.vala
index 8b06127..d398f59 100644
--- a/src/component/component-date-time.vala
+++ b/src/component/component-date-time.vala
@@ -130,7 +130,7 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
         bool until_is_utc = (iCal.icaltime_is_utc(rrule.until) != 0);
         
         // "The value of the UNTIL rule part MUST have the same value type as the 'DTSTART'
-        // property
+        // property."
         if (dtstart.is_date != until_is_date)
             throw new ComponentError.INVALID("RRULE UNTIL and DTSTART must be of same type 
(DATE/DATE-TIME)");
         
diff --git a/src/component/component-details-parser.vala b/src/component/component-details-parser.vala
index 607bddd..d2356fc 100644
--- a/src/component/component-details-parser.vala
+++ b/src/component/component-details-parser.vala
@@ -199,6 +199,9 @@ public class DetailsParser : BaseObject {
         // assemble accumulated information in an Event, using defaults wherever appropriate
         //
         
+        // track if end_date is "artificially" generated to complete the Event
+        bool generated_end_date = (end_date == null);
+        
         // if no start time or date but a duration was specified, assume start is now and use
         // duration for end time
         if (start_time == null && start_date == null && duration != null) {
@@ -245,8 +248,20 @@ public class DetailsParser : BaseObject {
                 new Calendar.ExactTime(Calendar.System.timezone, start_date, start_time),
                 new Calendar.ExactTime(Calendar.System.timezone, end_date, end_time)
             ));
+            
+            // for parser, RRULE UNTIL is always DTEND's date unless a duration (i.e. a count, as
+            // the parser doesn't set UNTIL elsewhere) is specified; parser only deals in date-based
+            // recurrences, but don't add UNTIL if parser auto-generated DTEND, since that's us
+            // filling in "obvious" details about the whole of the event that may not necessarily
+            // apply to the recurrence rule
+            if (rrule != null && !rrule.has_duration && !generated_end_date)
+                rrule.set_recurrence_end_date(end_date);
         } else if (start_date != null && end_date != null) {
             event.set_event_date_span(new Calendar.DateSpan(start_date, end_date));
+            
+            // see above note about RRULE UNTIL and DTEND
+            if (rrule != null && !rrule.has_duration && !generated_end_date)
+                rrule.set_recurrence_end_date(end_date);
         }
         
         // recurrence rule, if specified
@@ -265,8 +280,6 @@ public class DetailsParser : BaseObject {
             event.description = details;
         else
             event.description += "\n" + details;
-        
-        debug("%s", event.ical_component.as_ical_string());
     }
     
     private bool parse_time(Token? specifier, bool strict) {
@@ -344,10 +357,52 @@ public class DetailsParser : BaseObject {
         if (amount == null || unit == null)
             return false;
         
+        // if setting up a recurring rule, duration can be used as a count
+        if (rrule != null) {
+            // if duration already specified, not interested
+            if (rrule.has_duration)
+                return false;
+            
+            // convert duration into unit appropriate to rrule ... note that only date-based
+            // rrules are allowed by parser
+            int count = -1;
+            switch (rrule.freq) {
+                case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
+                    if (unit.casefolded in UNIT_DAYS)
+                        count = parse_amount(amount);
+                break;
+                
+                case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
+                    if (unit.casefolded in UNIT_WEEKS)
+                        count = parse_amount(amount);
+                break;
+                
+                case iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE:
+                    if (unit.casefolded in UNIT_MONTHS)
+                        count = parse_amount(amount);
+                break;
+                
+                case iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE:
+                    if (unit.casefolded in UNIT_YEARS)
+                        count = parse_amount(amount);
+                break;
+                
+                default:
+                    assert_not_reached();
+            }
+            
+            if (count > 0) {
+                rrule.set_recurrence_count(count);
+                
+                return true;
+            }
+        }
+        
+        // otherwise, if an end time or duration is already known, then done here
         if (end_time != null || duration != null)
             return false;
         
-        duration = Calendar.Duration.parse(amount.casefolded, unit.casefolded);
+        duration = parse_amount_of_time(amount, unit);
         
         return duration != null;
     }
@@ -360,7 +415,7 @@ public class DetailsParser : BaseObject {
         if (start_time != null)
             return false;
         
-        Calendar.Duration? delay = Calendar.Duration.parse(amount.casefolded, unit.casefolded);
+        Calendar.Duration? delay = parse_amount_of_time(amount, unit);
         if (delay == null)
             return false;
         
@@ -370,41 +425,103 @@ public class DetailsParser : BaseObject {
         return true;
     }
     
+    // Returns negative value if amount is invalid
+    private int parse_amount(Token? amount) {
+        if (amount == null)
+            return -1;
+        
+        return String.is_numeric(amount.casefolded) ? int.parse(amount.casefolded) : -1;
+    }
+    
+    // Returns negative value if ordinalis invalid
+    private int parse_ordinal(Token? ordinal) {
+        if (ordinal == null)
+            return -1;
+        
+        // strip ordinal suffix if present
+        string ordinal_number = ordinal.casefolded;
+        foreach (string suffix in ORDINAL_SUFFIXES) {
+            if (!String.is_empty(suffix) && ordinal_number.has_suffix(suffix)) {
+                ordinal_number = ordinal_number.slice(0, ordinal_number.length - suffix.length);
+                
+                break;
+            }
+        }
+        
+        return String.is_numeric(ordinal_number) ? int.parse(ordinal_number) : -1;
+    }
+    
+    private Calendar.Duration? parse_amount_of_time(Token? amount, Token? unit) {
+        if (amount == null || unit == null)
+            return null;
+        
+        int amt = parse_amount(amount);
+        if (amt < 0)
+            return null;
+        
+        if (unit.casefolded in UNIT_DAYS)
+            return new Calendar.Duration(amt);
+        
+        if (unit.casefolded in UNIT_HOURS)
+            return new Calendar.Duration(0, amt);
+        
+        if (unit.casefolded in UNIT_MINS)
+            return new Calendar.Duration(0, 0, amt);
+        
+        return null;
+    }
+    
     private bool parse_recurring(Token? specifier) {
+        // take ownership in case specifier is an ordinal amount
+        Token? unit = specifier;
+        
         // if a recurring rule has already been specified, another recurring cannot be made and
         // the current cannot be edited (yet)
-        if (specifier == null || rrule != null)
+        if (unit == null || rrule != null)
             return false;
         
+        // look for an amount modifying the specifier (creating an interval, i.e. "every 2 days"
+        // or "every 2nd day", hence parsing for ordinal)
+        int interval = parse_ordinal(unit);
+        if (interval >= 1) {
+            unit = stack.pop();
+            if (unit == null)
+                return false;
+        } else {
+            interval = 1;
+        }
+        
         // a day of the week
-        Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(specifier.casefolded);
+        Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(unit.casefolded);
         if (dow != null) {
             start_date = Calendar.System.today.upcoming(dow, true);
             rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE);
+            rrule.interval = interval;
             rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
             
             return true;
         }
         
         // "day"
-        if (specifier.casefolded == DAY) {
+        if (unit.casefolded in UNIT_DAYS) {
             start_date = Calendar.System.today;
             rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE);
+            rrule.interval = interval;
             rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
             
             return true;
         }
         
         // "weekday"
-        if (specifier.casefolded == WEEKDAY) {
-            set_rrule_weekly(Calendar.DayOfWeek.weekdays);
+        if (unit.casefolded in UNIT_WEEKDAYS) {
+            set_rrule_weekly(Calendar.DayOfWeek.weekdays, interval);
             
             return true;
         }
         
         // "weekend"
-        if (specifier.casefolded == WEEKEND) {
-            set_rrule_weekly(Calendar.DayOfWeek.weekend_days);
+        if (unit.casefolded in UNIT_WEEKENDS) {
+            set_rrule_weekly(Calendar.DayOfWeek.weekend_days, interval);
             
             return true;
         }
@@ -415,12 +532,12 @@ public class DetailsParser : BaseObject {
             {
                 Token? second = stack.pop();
                 if (second != null) {
-                    Calendar.Date? date = parse_day_month(specifier, second);
+                    Calendar.Date? date = parse_day_month(unit, second);
                     if (date == null)
-                        date = parse_day_month(second, specifier);
+                        date = parse_day_month(second, unit);
                     
                     if (date != null) {
-                        set_rrule_yearly(date);
+                        set_rrule_yearly(date, interval);
                         
                         return true;
                     }
@@ -432,7 +549,7 @@ public class DetailsParser : BaseObject {
         return false;
     }
     
-    private void set_rrule_weekly(Calendar.DayOfWeek[]? by_days) {
+    private void set_rrule_weekly(Calendar.DayOfWeek[]? by_days, int interval) {
         Gee.Map<Calendar.DayOfWeek, int> map = new Gee.HashMap<Calendar.DayOfWeek, int>();
         if (by_days != null) {
             foreach (Calendar.DayOfWeek by_day in by_days)
@@ -445,14 +562,16 @@ public class DetailsParser : BaseObject {
             start_date = start_date.upcoming_in_set(from_array<Calendar.DayOfWeek>(by_days).to_hash_set(), 
true);
         
         rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE);
+        rrule.interval = interval;
         rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
         rrule.set_by_rule(RecurrenceRule.ByRule.DAY, RecurrenceRule.encode_days(map));
     }
     
-    private void set_rrule_yearly(Calendar.Date date) {
+    private void set_rrule_yearly(Calendar.Date date, int interval) {
         start_date = date;
         
         rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE);
+        rrule.interval = interval;
         rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
         rrule.set_by_rule(RecurrenceRule.ByRule.YEAR_DAY, iterate<int>(date.day_of_year).to_array_list());
     }
@@ -508,17 +627,8 @@ public class DetailsParser : BaseObject {
     
     // Parses potential date specifiers into a specific calendar date
     private Calendar.Date? parse_day_month(Token day, Token mon, Calendar.Year? year = null) {
-        // strip ordinal suffix if present
-        string day_number = day.casefolded;
-        foreach (string suffix in ORDINAL_SUFFIXES) {
-            if (!String.is_empty(suffix) && day_number.has_suffix(suffix)) {
-                day_number = day_number.slice(0, day_number.length - suffix.length);
-                
-                break;
-            }
-        }
-        
-        if (!String.is_numeric(day_number))
+        int day_ordinal = parse_ordinal(day);
+        if (day_ordinal < 0)
             return null;
         
         Calendar.Month? month = Calendar.Month.parse(mon.casefolded);
@@ -529,8 +639,7 @@ public class DetailsParser : BaseObject {
             year = Calendar.System.today.year;
         
         try {
-            return new Calendar.Date(Calendar.DayOfMonth.for(int.parse(day.casefolded)),
-                month, year);
+            return new Calendar.Date(Calendar.DayOfMonth.for(day_ordinal), month, year);
         } catch (CalendarError calerr) {
             // probably an out-of-bounds day of month
             return null;
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 7ad5445..4444d9c 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -88,6 +88,11 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     public iCal.icalcomponent ical_component { get { return _ical_component; } }
     
     /**
+     * Returns the iCal source for this { link Instance}.
+     */
+    public string source { get { return ical_component.as_ical_string(); } }
+    
+    /**
      * True if inside { link full_update}.
      *
      * Subclasses want to ignore updates to various properties (their own and { link Instance}'s)
@@ -300,22 +305,6 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     }
     
     /**
-     * Convenience method to convert a { link Calendar.Date} to an iCal DATE.
-     */
-    protected static void date_to_ical(Calendar.Date date, iCal.icaltimetype *ical_dt) {
-        ical_dt->year = date.year.value;
-        ical_dt->month = date.month.value;
-        ical_dt->day = date.day_of_month.value;
-        ical_dt->hour = 0;
-        ical_dt->minute = 0;
-        ical_dt->second = 0;
-        ical_dt->is_utc = 0;
-        ical_dt->is_date = 1;
-        ical_dt->is_daylight = 0;
-        ical_dt->zone = null;
-    }
-    
-    /**
      * Convenience method to convert a { link Calendar.DateSpan} to a pair of iCal DATEs.
      *
      * dtend_inclusive indicates whether the dt_end should be treated as inclusive or exclusive
@@ -330,24 +319,6 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     }
     
     /**
-     * Convenience method to convert a { link Calendar.ExactTime} to an iCal DATE-TIME.
-     */
-    protected static void exact_time_to_ical(Calendar.ExactTime exact_time, iCal.icaltimetype *ical_dt) {
-        ical_dt->year = exact_time.year.value;
-        ical_dt->month = exact_time.month.value;
-        ical_dt->day = exact_time.day_of_month.value;
-        ical_dt->hour = exact_time.hour;
-        ical_dt->minute = exact_time.minute;
-        ical_dt->second = exact_time.second;
-        ical_dt->is_utc = exact_time.tz.is_utc ? 1 : 0;
-        ical_dt->is_date = 0;
-        ical_dt->is_daylight = exact_time.is_dst ? 1 : 0;
-        ical_dt->zone = iCal.icaltimezone.get_builtin_timezone(exact_time.tz.zone.value);
-        if (ical_dt->zone == null)
-            message("Unable to get builtin iCal timezone for %s", exact_time.tz.zone.to_string());
-    }
-    
-    /**
      * Convenience method to convert a { link Calendar.ExactTimeSpan} to a pair of iCal DATE-TIMEs.
      */
     protected static void exact_time_span_to_ical(Calendar.ExactTimeSpan exact_time_span,
diff --git a/src/component/component-recurrence-rule.vala b/src/component/component-recurrence-rule.vala
index 12dd603..6f7178a 100644
--- a/src/component/component-recurrence-rule.vala
+++ b/src/component/component-recurrence-rule.vala
@@ -14,6 +14,12 @@ namespace California.Component {
  */
 
 public class RecurrenceRule : BaseObject {
+    public const string PROP_FREQ = "freq";
+    public const string PROP_UNTIL = "until";
+    public const string PROP_COUNT = "count";
+    public const string PROP_INTERVAL = "interval";
+    public const string PROP_FIRST_OF_WEEK = "first-of-week";
+    
     /**
      * Enumeration of various BY rules (BYSECOND, BYMINUTE, etc.)
      */
@@ -37,36 +43,74 @@ public class RecurrenceRule : BaseObject {
     public iCal.icalrecurrencetype_frequency freq { get; set; }
     
     /**
+     * Returns true if { link freq} is iCal.icalrecurrencetype_frequence.DAILY_RECURRENCE,
+     */
+    public bool is_daily { get { return freq == iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE; } }
+    
+    /**
+     * Returns true if { link freq} is iCal.icalrecurrencetype_frequence.DAILY_RECURRENCE,
+     */
+    public bool is_weekly { get { return freq == iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE; } }
+    
+    /**
+     * Returns true if { link freq} is iCal.icalrecurrencetype_frequence.MONTHLY_RECURRENCE,
+     */
+    public bool is_monthly { get { return freq == iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE; } }
+    
+    /**
+     * Returns true if { link freq} is iCal.icalrecurrencetype_frequence.YEARLY_RECURRENCE,
+     */
+    public bool is_yearly { get { return freq == iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE; } }
+    
+    /**
      * Until (end date), inclusive.
      *
-     * This is mutually exclusive with { link count}.
+     * This is mutually exclusive with { link count} and { link until_exact_time}.
+     *
+     * @see set_until_date
+     */
+    public Calendar.Date? until_date { get; private set; default = null; }
+    
+    /**
+     * Until (end date/time).
+     *
+     * This is mutually exclusive with { link count} and { link until_date}.
      *
-     * @see set_until_date_time
+     * @see set_until_exact_time
      */
-    public DateTime? until { get; private set; default = null; }
+    public Calendar.ExactTime? until_exact_time { get; private set; default = null; }
     
     /**
      * Total number of recurrences.
      *
      * Zero indicates "not set", not zero recurrences.
      *
-     * This is mutually exclusive with { link until}.
+     * This is mutually exclusive with { link until_date} and { link until_exact_time}.
      *
      * @see set_recurrence_count
      */
     public int count { get; private set; default = 0; }
     
     /**
+     * Returns true if the recurrence rule has a duration.
+     *
+     * @see until
+     * @see count
+     */
+    public bool has_duration { get { return until_date != null || until_exact_time != null || count > 0; } }
+    
+    /**
      * Interval between recurrences.
      *
      * A positive integer representing the interval (duration between) of each recurrence.  The
      * actual amount of time elapsed is determined by the { link frequency} property.
+     *
+     * interval may be any value from 1 to short.MAX.
      */
-    private short _interval;
-    public short interval {
+    private int _interval = 1;
+    public int interval {
         get { return _interval; }
-        set { _interval = value.clamp(0, short.MAX); }
-        default = 0;
+        set { _interval = value.clamp(1, short.MAX); }
     }
     
     /**
@@ -93,7 +137,7 @@ public class RecurrenceRule : BaseObject {
         this.freq = freq;
     }
     
-    internal RecurrenceRule.from_ical(iCal.icalcomponent ical_component) throws ComponentError {
+    internal RecurrenceRule.from_ical(iCal.icalcomponent ical_component) throws Error {
         // need DTSTART for timezone purposes
         DateTime dtstart = new DateTime(ical_component, iCal.icalproperty_kind.DTSTART_PROPERTY);
         
@@ -106,12 +150,18 @@ public class RecurrenceRule : BaseObject {
         iCal.icalrecurrencetype rrule = rrule_property.get_rrule();
         
         freq = rrule.freq;
-        if (rrule.count > 0)
-            set_recurrence_count(rrule.count);
-        else
-            set_until_date_time(new DateTime.rrule_until(rrule, dtstart));
         interval = rrule.interval;
         
+        if (rrule.count > 0) {
+            set_recurrence_count(rrule.count);
+        } else {
+            Component.DateTime date_time = new DateTime.rrule_until(rrule, dtstart);
+            if (date_time.is_date)
+                set_recurrence_end_date(date_time.to_date());
+            else
+                set_recurrence_end_exact_time(date_time.to_exact_time());
+        }
+        
         switch (rrule.week_start) {
             case iCal.icalrecurrencetype_weekday.SUNDAY_WEEKDAY:
                 first_of_week = Calendar.DayOfWeek.SUN;
@@ -169,27 +219,54 @@ public class RecurrenceRule : BaseObject {
     }
     
     /**
-     * Sets the { link until} property.
+     * Sets the { link until_date} property.
      *
-     * Also sets { link count} to zero.
+     * Also sets { link count} to zero and nulls out { link until_exact_time}.
      *
-     * Passing null will clear both properties.
+     * Passing null will clear all these properties.
      */
-    public void set_until_date_time(DateTime? date_time) {
-        until = date_time;
+    public void set_recurrence_end_date(Calendar.Date? date) {
+        freeze_notify();
+        
+        until_date = date;
+        until_exact_time = null;
         count = 0;
+        
+        thaw_notify();
+    }
+    
+    /**
+     * Sets the { link until_exact_time} property.
+     *
+     * Also sets { link count} to zero and nulls out { link until_date}.
+     *
+     * Passing null will clear all these properties.
+     */
+    public void set_recurrence_end_exact_time(Calendar.ExactTime? exact_time) {
+        freeze_notify();
+        
+        until_date = null;
+        until_exact_time = exact_time;
+        count = 0;
+        
+        thaw_notify();
     }
     
     /**
      * Sets the { link count} property.
      *
-     * Also clears { link until}.
+     * Also clears { link until_date} and { link until_exact_time}.
      *
-     * Passing zero will clear both properties.
+     * Passing zero will clear all these properties.
      */
     public void set_recurrence_count(int count) {
-        this.count = count;
-        until = null;
+        freeze_notify();
+        
+        until_date = null;
+        until_exact_time = null;
+        this.count = count.clamp(0, int.MAX);
+        
+        thaw_notify();
     }
     
     /**
@@ -321,15 +398,17 @@ public class RecurrenceRule : BaseObject {
      */
     internal void add_to_ical(iCal.icalcomponent ical_component) {
         iCal.icalrecurrencetype rrule = { 0 };
+        
         rrule.freq = freq;
         
-        if (until != null)
-            rrule.until = until.dt;
+        if (until_date != null)
+            date_to_ical(until_date, &rrule.until);
+        else if (until_exact_time != null)
+            exact_time_to_ical(until_exact_time, &rrule.until);
         else if (count > 0)
             rrule.count = count;
         
-        if (interval > 0)
-            rrule.interval = interval;
+        rrule.interval = (short) interval;
         
         if (first_of_week == null)
             rrule.week_start = iCal.icalrecurrencetype_weekday.NO_WEEKDAY;
diff --git a/src/component/component.vala b/src/component/component.vala
index c7be790..7bac566 100644
--- a/src/component/component.vala
+++ b/src/component/component.vala
@@ -20,9 +20,14 @@ private int init_count = 0;
 private string TODAY;
 private string TOMORROW;
 private string YESTERDAY;
-private string DAY;
-private string WEEKDAY;
-private string WEEKEND;
+private string[] UNIT_WEEKDAYS;
+private string[] UNIT_WEEKENDS;
+private string[] UNIT_YEARS;
+private string[] UNIT_MONTHS;
+private string[] UNIT_WEEKS;
+private string[] UNIT_DAYS;
+private string[] UNIT_HOURS;
+private string[] UNIT_MINS;
 private string[] TIME_PREPOSITIONS;
 private string[] LOCATION_PREPOSITIONS;
 private string[] DURATION_PREPOSITIONS;
@@ -50,19 +55,49 @@ public void init() throws Error {
     // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
     YESTERDAY = _("yesterday").casefold();
     
-    // Used by quick-add to indicate the user wants to create an event for every day
-    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
-    DAY = _("day").casefold();
-    
     // Used by quick-add to indicate the user wants to create an event for every weekday
     // (in most Western countries, this means Monday through Friday, i.e. the work week)
+    // Common abbreviations (without punctuation) should be included.  Each word must be separated
+    // by semi-colons.
     // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
-    WEEKDAY = _("weekday").casefold();
+    UNIT_WEEKDAYS = _("weekday;weekdays;").casefold().split(";");
     
     // Used by quick-add to indicate the user wants to create an event for every weekend
     // (in most Western countries, this means Saturday and Sunday, i.e. non-work days)
+    // Common abbreviations (without punctuation) should be included.  Each word must be separated
+    // by semi-colons.
+    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+    UNIT_WEEKENDS = _("weekend;weekends;").casefold().split(";");
+    
+    // Used by quick-add to convert a user's years unit into an internal value.  Common abbreviations
+    // (without punctuation) should be included.  Each word must be separated by semi-colons.
+    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+    UNIT_YEARS = _("year;years;yr;yrs;").casefold().split(";");
+    
+    // Used by quick-add to convert a user's month unit into an internal value.  Common abbreviations
+    // (without punctuation) should be included.  Each word must be separated by semi-colons.
+    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+    UNIT_MONTHS = _("month;months;mo;mos;").casefold().split(";");
+    
+    // Used by quick-add to convert a user's week unit into an internal value.  Common abbreviations
+    // (without punctuation) should be included.  Each word must be separated by semi-colons.
+    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+    UNIT_WEEKS = _("week;weeks;wk;weeks;").casefold().split(";");
+    
+    // Used by quick-add to convert a user's day unit into an internal value.  Common abbreviations
+    // (without punctuation) should be included.  Each word must be separated by semi-colons.
+    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+    UNIT_DAYS = _("day;days;").casefold().split(";");
+    
+    // Used by quick-add to convert a user's hours unit into an internal value.  Common abbreviations
+    // (without punctuation) should be included.  Each word must be separated by semi-colons.
+    // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+    UNIT_HOURS = _("hour;hours;hr;hrs").casefold().split(";");
+    
+    // Used by quick-add to convert a user's minute unit into an internal value.  Common abbreviations
+    // (without punctuation) should be included.  Each word must be separated by semi-colons.
     // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
-    WEEKEND = _("weekend").casefold();
+    UNIT_MINS = _("minute;minutes;min;mins").casefold().split(";");
     
     // Used by quick-add to determine if the word is a TIME preposition (indicating a
     // specific time of day, not a duration).  Each word must be separated by semi-colons.
@@ -124,11 +159,47 @@ public void terminate() {
     
     TIME_PREPOSITIONS = LOCATION_PREPOSITIONS = DURATION_PREPOSITIONS = ORDINAL_SUFFIXES = null;
     DELAY_PREPOSITIONS = RECURRING_PREPOSITIONS = null;
-    TODAY = TOMORROW = YESTERDAY = DAY = WEEKDAY = WEEKEND = null;
+    TODAY = TOMORROW = YESTERDAY = null;
+    UNIT_WEEKDAYS = UNIT_WEEKENDS = UNIT_YEARS = UNIT_MONTHS = UNIT_WEEKS = UNIT_DAYS = UNIT_HOURS
+        = UNIT_MINS = null;
     
     Calendar.terminate();
     Collection.terminate();
 }
 
+/**
+ * Convenience method to convert a { link Calendar.Date} to an iCal DATE.
+ */
+private void date_to_ical(Calendar.Date date, iCal.icaltimetype *ical_dt) {
+    ical_dt->year = date.year.value;
+    ical_dt->month = date.month.value;
+    ical_dt->day = date.day_of_month.value;
+    ical_dt->hour = 0;
+    ical_dt->minute = 0;
+    ical_dt->second = 0;
+    ical_dt->is_utc = 0;
+    ical_dt->is_date = 1;
+    ical_dt->is_daylight = 0;
+    ical_dt->zone = null;
+}
+
+/**
+ * Convenience method to convert a { link Calendar.ExactTime} to an iCal DATE-TIME.
+ */
+private void exact_time_to_ical(Calendar.ExactTime exact_time, iCal.icaltimetype *ical_dt) {
+    ical_dt->year = exact_time.year.value;
+    ical_dt->month = exact_time.month.value;
+    ical_dt->day = exact_time.day_of_month.value;
+    ical_dt->hour = exact_time.hour;
+    ical_dt->minute = exact_time.minute;
+    ical_dt->second = exact_time.second;
+    ical_dt->is_utc = exact_time.tz.is_utc ? 1 : 0;
+    ical_dt->is_date = 0;
+    ical_dt->is_daylight = exact_time.is_dst ? 1 : 0;
+    ical_dt->zone = iCal.icaltimezone.get_builtin_timezone(exact_time.tz.zone.value);
+    if (ical_dt->zone == null)
+        message("Unable to get builtin iCal timezone for %s", exact_time.tz.zone.to_string());
+}
+
 }
 
diff --git a/src/tests/tests-quick-add-recurring.vala b/src/tests/tests-quick-add-recurring.vala
new file mode 100644
index 0000000..6f6d591
--- /dev/null
+++ b/src/tests/tests-quick-add-recurring.vala
@@ -0,0 +1,100 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+namespace California.Tests {
+
+private class QuickAddRecurring : UnitTest.Harness {
+    public QuickAddRecurring() {
+        // DAILY tests
+        add_case("every-day", every_day);
+        add_case("every-day-10-days", every_day_10_days);
+        add_case("every-2-days", every_2_days);
+        add_case("every-3rd-day", every_3rd_day);
+        add_case("every-2-days-for-10-days", every_2_days_for_10_days);
+        add_case("every-2-days-until", every_2_days_until);
+    }
+    
+    protected override void setup() throws Error {
+        Component.init();
+        Calendar.init();
+    }
+    
+    protected override void teardown() {
+        Component.terminate();
+        Calendar.terminate();
+    }
+    
+    // Checks that an RRULE was generated,
+    // the summary is       meeting at work
+    // the location is      work
+    // the start time is    10am
+    private bool basic(string details, out Component.Event event, out string? dump) {
+        Component.DetailsParser parser = new Component.DetailsParser(details, null);
+        event = parser.event;
+        
+        dump = event.source;
+        
+        return event.rrule != null
+            && event.summary == "meeting at work"
+            && event.location == "work"
+            && event.exact_time_span.start_exact_time.to_wall_time().equal_to(new Calendar.WallTime(10, 0, 
0));
+    }
+    
+    private bool every_day(out string? dump) throws Error {
+        Component.Event event;
+        return basic("meeting at work every day at 10am", out event, out dump)
+            && event.rrule.is_daily
+            && event.rrule.interval == 1
+            && !event.rrule.has_duration;
+    }
+    
+    private bool every_day_10_days(out string? dump) throws Error {
+        Component.Event event;
+        return basic("meeting at work every day at 10am for 10 days", out event, out dump)
+            && event.rrule.is_daily
+            && event.rrule.interval == 1
+            && event.rrule.count == 10;
+    }
+    
+    private bool every_2_days(out string? dump) throws Error {
+        Component.Event event;
+        return basic("meeting at 10am every 2 days at work", out event, out dump)
+            && event.rrule.is_daily
+            && event.rrule.interval == 2
+            && !event.rrule.has_duration;
+    }
+    
+    private bool every_3rd_day(out string? dump) throws Error {
+        Component.Event event;
+        return basic("meeting at 10am every 3rd day at work", out event, out dump)
+            && event.rrule.is_daily
+            && event.rrule.interval == 3
+            && !event.rrule.has_duration;
+    }
+    
+    private bool every_2_days_for_10_days(out string? dump) throws Error {
+        Component.Event event;
+        return basic("meeting at work every 2 days for 10 days at 10am", out event, out dump)
+            && event.rrule.is_daily
+            && event.rrule.interval == 2
+            && event.rrule.count == 10;
+    }
+    
+    private bool every_2_days_until(out string? dump) throws Error {
+        Calendar.Date end = new Calendar.Date(Calendar.DayOfMonth.for(31), Calendar.Month.DEC,
+            Calendar.System.today.year);
+        
+        Component.Event event;
+        return basic("meeting at work at 10am every 2 days until December 31", out event, out dump)
+            && event.rrule.is_daily
+            && event.rrule.interval == 2
+            && event.rrule.until_date != null
+            && event.rrule.until_date.equal_to(end);
+    }
+}
+
+}
+
diff --git a/src/tests/tests-quick-add.vala b/src/tests/tests-quick-add.vala
index e24e58c..0972812 100644
--- a/src/tests/tests-quick-add.vala
+++ b/src/tests/tests-quick-add.vala
@@ -27,6 +27,8 @@ private class QuickAdd : UnitTest.Harness {
         add_case("midnight-to-one", midnight_to_one);
         add_case("separate-am", separate_am);
         add_case("separate-pm", separate_pm);
+        add_case("start-date-ordinal", start_date_ordinal);
+        add_case("end-date-ordinal", end_date_ordinal);
     }
     
     protected override void setup() throws Error {
@@ -261,6 +263,33 @@ private class QuickAdd : UnitTest.Harness {
         return parser.event.summary == "Dinner"
             && parser.event.exact_time_span.start_exact_time.equal_to(start);
     }
+    
+    private bool start_date_ordinal() throws Error {
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "Dinner May 1st", null);
+        
+        Calendar.Date start = new Calendar.Date(Calendar.DayOfMonth.for(1), Calendar.Month.MAY,
+            Calendar.System.today.year);
+        
+        return parser.event.summary == "Dinner"
+            && parser.event.date_span.start_date.equal_to(start);
+    }
+    
+    private bool end_date_ordinal(out string? dump) throws Error {
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "Off-site May 1st to May 2nd", null);
+        
+        dump = parser.event.source;
+        
+        Calendar.Date start = new Calendar.Date(Calendar.DayOfMonth.for(1), Calendar.Month.MAY,
+            Calendar.System.today.year);
+        Calendar.Date end = new Calendar.Date(Calendar.DayOfMonth.for(2), Calendar.Month.MAY,
+            Calendar.System.today.year);
+        
+        return parser.event.summary == "Off-site"
+            && parser.event.date_span.start_date.equal_to(start)
+            && parser.event.date_span.end_date.equal_to(end);
+    }
 }
 
 }
diff --git a/src/tests/tests.vala b/src/tests/tests.vala
index c85530b..0f88f76 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -9,6 +9,7 @@ namespace California.Tests {
 public int run(string[] args) {
     UnitTest.Harness.register(new String());
     UnitTest.Harness.register(new QuickAdd());
+    UnitTest.Harness.register(new QuickAddRecurring());
     UnitTest.Harness.register(new CalendarDate());
     UnitTest.Harness.register(new CalendarMonthSpan());
     UnitTest.Harness.register(new CalendarMonthOfYear());
diff --git a/src/unit-test/unit-test-harness.vala b/src/unit-test/unit-test-harness.vala
index 249aae9..759801d 100644
--- a/src/unit-test/unit-test-harness.vala
+++ b/src/unit-test/unit-test-harness.vala
@@ -11,7 +11,7 @@ namespace California.UnitTest {
  */
 
 public abstract class Harness : BaseObject {
-    public delegate bool Case() throws Error;
+    public delegate bool Case(out string? dump = null) throws Error;
     
     private class TestCase : BaseObject {
         public string name;
@@ -113,17 +113,21 @@ public abstract class Harness : BaseObject {
             }
             
             bool success = false;
+            string? dump = null;
             Error? err = null;
             try {
-                success = test_case.unit_test();
+                success = test_case.unit_test(out dump);
             } catch (Error caught) {
                 err = caught;
             }
             
             if (err != null)
-                stdout.printf("\nFailed: %s.%s\n\t%s\n", name, test_case.name, err.message);
+                stdout.printf("failed (thrown error):\n\t\"%s\"\n", err.message);
             else if (!success)
-                stdout.printf("\nFailed: %s.%s\n", name, test_case.name);
+                stdout.printf("failed (test):\n");
+            
+            if ((err != null || !success) && !String.is_empty(dump))
+                stdout.printf("%s\n", dump);
             
             if (err != null || !success)
                 Posix.exit(Posix.EXIT_FAILURE);
diff --git a/vapi/libical.vapi b/vapi/libical.vapi
index 7bb9795..2bf00a2 100644
--- a/vapi/libical.vapi
+++ b/vapi/libical.vapi
@@ -57,9 +57,9 @@ namespace iCal {
                [CCode (cname = "icalcomponent_add_property")]
                public void add_property (iCal.icalproperty property);
                [CCode (cname = "icalcomponent_as_ical_string")]
-               public string as_ical_string ();
+               public unowned string as_ical_string ();
                [CCode (cname = "icalcomponent_as_ical_string_r")]
-               public unowned string as_ical_string_r ();
+               public string as_ical_string_r ();
                [CCode (cname = "icalcomponent_begin_component")]
                public unowned iCal.icalcompiter begin_component (iCal.icalcomponent_kind kind);
                [CCode (cname = "icalcomponent_check_restrictions")]
@@ -225,7 +225,7 @@ namespace iCal {
                [CCode (cname = "icaldurationtype_as_ical_string")]
                public unowned string as_ical_string ();
                [CCode (cname = "icaldurationtype_as_ical_string_r")]
-               public unowned string as_ical_string_r ();
+               public string as_ical_string_r ();
                [CCode (cname = "icaldurationtype_as_int")]
                public int as_int ();
                [CCode (cname = "icaldurationtype_bad_duration")]
@@ -253,7 +253,7 @@ namespace iCal {
                [CCode (cname = "icalparameter_as_ical_string")]
                public unowned string as_ical_string ();
                [CCode (cname = "icalparameter_as_ical_string_r")]
-               public unowned string as_ical_string_r ();
+               public string as_ical_string_r ();
                [CCode (cname = "icalparameter_new_charset", has_construct_function = false)]
                public icalparameter.charset (string v);
                [CCode (cname = "icalparameter_new_clone", has_construct_function = false)]
@@ -507,7 +507,7 @@ namespace iCal {
                [CCode (cname = "icalproperty_as_ical_string")]
                public unowned string as_ical_string ();
                [CCode (cname = "icalproperty_as_ical_string_r")]
-               public unowned string as_ical_string_r ();
+               public string as_ical_string_r ();
                [CCode (cname = "icalproperty_new_attach", has_construct_function = false)]
                public icalproperty.attach (iCal.icalattach v);
                [CCode (cname = "icalproperty_new_attendee", has_construct_function = false)]
@@ -579,7 +579,7 @@ namespace iCal {
                [CCode (cname = "icalproperty_enum_to_string")]
                public static unowned string enum_to_string (int e);
                [CCode (cname = "icalproperty_enum_to_string_r")]
-               public static unowned string enum_to_string_r (int e);
+               public string enum_to_string_r (int e);
                [CCode (cname = "icalproperty_new_exdate", has_construct_function = false)]
                public icalproperty.exdate (iCal.icaltimetype v);
                [CCode (cname = "icalproperty_new_expand", has_construct_function = false)]
@@ -703,7 +703,7 @@ namespace iCal {
                [CCode (cname = "icalproperty_get_parameter_as_string")]
                public unowned string get_parameter_as_string (string name);
                [CCode (cname = "icalproperty_get_parameter_as_string_r")]
-               public unowned string get_parameter_as_string_r (string name);
+               public string get_parameter_as_string_r (string name);
                [CCode (cname = "icalproperty_get_percentcomplete")]
                public int get_percentcomplete ();
                [CCode (cname = "icalproperty_get_permission")]
@@ -715,7 +715,7 @@ namespace iCal {
                [CCode (cname = "icalproperty_get_property_name")]
                public unowned string get_property_name ();
                [CCode (cname = "icalproperty_get_property_name_r")]
-               public unowned string get_property_name_r ();
+               public string get_property_name_r ();
                [CCode (cname = "icalproperty_get_query")]
                public unowned string get_query ();
                [CCode (cname = "icalproperty_get_queryid")]
@@ -783,7 +783,7 @@ namespace iCal {
                [CCode (cname = "icalproperty_get_value_as_string")]
                public unowned string get_value_as_string ();
                [CCode (cname = "icalproperty_get_value_as_string_r")]
-               public unowned string get_value_as_string_r ();
+               public string get_value_as_string_r ();
                [CCode (cname = "icalproperty_get_version")]
                public unowned string get_version ();
                [CCode (cname = "icalproperty_get_x")]
@@ -1442,7 +1442,7 @@ namespace iCal {
                [CCode (cname = "icalvalue_as_ical_string")]
                public unowned global::string as_ical_string ();
                [CCode (cname = "icalvalue_as_ical_string_r")]
-               public unowned global::string as_ical_string_r ();
+               public global::string as_ical_string_r ();
                [CCode (cname = "icalvalue_new_attach", has_construct_function = false)]
                public icalvalue.attach (iCal.icalattach attach);
                [CCode (cname = "icalvalue_new_binary", has_construct_function = false)]
@@ -1696,7 +1696,7 @@ namespace iCal {
                [CCode (cname = "icalperiodtype_as_ical_string")]
                public unowned string as_ical_string ();
                [CCode (cname = "icalperiodtype_as_ical_string_r")]
-               public unowned string as_ical_string_r ();
+               public string as_ical_string_r ();
                [CCode (cname = "icalperiodtype_from_string")]
                public static iCal.icalperiodtype from_string (string str);
                [CCode (cname = "icalperiodtype_is_null_period")]
@@ -1728,7 +1728,7 @@ namespace iCal {
                [CCode (cname = "icalrecurrencetype_as_string")]
                public unowned string as_string ();
                [CCode (cname = "icalrecurrencetype_as_string_r")]
-               public unowned string as_string_r ();
+               public string as_string_r ();
                [CCode (cname = "icalrecurrencetype_clear")]
                public void clear ();
                [CCode (cname = "icalrecurrencetype_day_day_of_week")]
@@ -1746,7 +1746,7 @@ namespace iCal {
                [CCode (cname = "icalreqstattype_as_string")]
                public unowned string as_string ();
                [CCode (cname = "icalreqstattype_as_string_r")]
-               public unowned string as_string_r ();
+               public string as_string_r ();
                [CCode (cname = "icalreqstattype_from_string")]
                public static iCal.icalreqstattype from_string (string str);
        }
@@ -2454,7 +2454,7 @@ namespace iCal {
        [CCode (cheader_filename = "libical/ical.h", cname = "icalenum_reqstat_code")]
        public static unowned string icalenum_reqstat_code (iCal.icalrequeststatus stat);
        [CCode (cheader_filename = "libical/ical.h", cname = "icalenum_reqstat_code_r")]
-       public static unowned string icalenum_reqstat_code_r (iCal.icalrequeststatus stat);
+       public static string icalenum_reqstat_code_r (iCal.icalrequeststatus stat);
        [CCode (cheader_filename = "libical/ical.h", cname = "icalenum_reqstat_desc")]
        public static unowned string icalenum_reqstat_desc (iCal.icalrequeststatus stat);
        [CCode (cheader_filename = "libical/ical.h", cname = "icalenum_reqstat_major")]
@@ -2512,11 +2512,11 @@ namespace iCal {
        [CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_property_eval_string")]
        public static unowned string icallangbind_property_eval_string (iCal.icalproperty prop, string sep);
        [CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_property_eval_string_r")]
-       public static unowned string icallangbind_property_eval_string_r (iCal.icalproperty prop, string sep);
+       public static string icallangbind_property_eval_string_r (iCal.icalproperty prop, string sep);
        [CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_quote_as_ical")]
        public static unowned string icallangbind_quote_as_ical (string str);
        [CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_quote_as_ical_r")]
-       public static unowned string icallangbind_quote_as_ical_r (string str);
+       public static string icallangbind_quote_as_ical_r (string str);
        [CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_string_to_open_flag")]
        public static int icallangbind_string_to_open_flag (string str);
        [CCode (cheader_filename = "libical/ical.h", cname = "icalmemory_add_tmp_buffer")]


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]