[california/wip/725785-create-recurring] Start of weekly recurrence parsing



commit 57925d479531e178f7172cce2f574fad1a454e5a
Author: Jim Nelson <jim yorba org>
Date:   Thu Jun 19 20:09:33 2014 -0700

    Start of weekly recurrence parsing

 src/collection/collection-iterable.vala      |   45 ++++++++++--
 src/component/component-details-parser.vala  |   55 +++++++++-----
 src/component/component-recurrence-rule.vala |  100 ++++++++++++++++----------
 src/tests/tests-quick-add-recurring.vala     |   33 +++++++++
 4 files changed, 169 insertions(+), 64 deletions(-)
---
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index 61e9b32..1465a49 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -42,6 +42,20 @@ public California.Iterable<G> from_array<G>(G[] ar) {
 }
 
 /**
+ * Returns an { link Iterable} of Unicode characters for each in the supplied string.
+ */
+public Iterable<unichar> from_string(string str) {
+    Gee.ArrayList<unichar> list = new Gee.ArrayList<unichar>();
+    
+    int index = 0;
+    unichar ch;
+    while (str.get_next_char(ref index, out ch))
+        list.add(ch);
+    
+    return California.traverse<unichar>(list);
+}
+
+/**
  * An Iterable that simply wraps an existing Iterator.  You get one iteration,
  * and only one iteration.  Basically every method triggers one iteration and
  * returns a new object.
@@ -51,12 +65,17 @@ public California.Iterable<G> from_array<G>(G[] ar) {
  * works in foreach.
  */
 
-public class Iterable<G> : BaseObject {
+public class Iterable<G> : Object {
+    /**
+     * For { link to_string}.
+     */
+    public delegate string? ToString<G>(G element);
+    
     /**
      * A private class that lets us take a California.Iterable and convert it back
      * into a Gee.Iterable.
      */
-    private class GeeIterable<G> : Gee.Traversable<G>, Gee.Iterable<G>, BaseObject {
+    private class GeeIterable<G> : Gee.Traversable<G>, Gee.Iterable<G>, Object {
         private Gee.Iterator<G> i;
         
         public GeeIterable(Gee.Iterator<G> iterator) {
@@ -75,10 +94,6 @@ public class Iterable<G> : BaseObject {
             }
             return true;
         }
-        
-        public override string to_string() {
-            return "GeeIterable";
-        }
     }
     
     private Gee.Iterator<G> i;
@@ -216,8 +231,22 @@ public class Iterable<G> : BaseObject {
             (owned) key_hash_func, (owned) key_equal_func, (owned) value_equal_func), key_func);
     }
     
-    public override string to_string() {
-        return "Iterable";
+    /**
+     * Convert the { link Iterable} into a plain string.
+     *
+     * If { link ToString} returns null or an empty string, nothing is appended to the final string.
+     *
+     * If the final string is empty, null is returned instead.
+     */
+    public string? to_string(ToString<G> string_cb) {
+        StringBuilder builder = new StringBuilder();
+        foreach (G element in this) {
+            string? str = string_cb(element);
+            if (!String.is_empty(str))
+                builder.append(str);
+        }
+        
+        return !String.is_empty(builder.str) ? builder.str : null;
     }
 }
 
diff --git a/src/component/component-details-parser.vala b/src/component/component-details-parser.vala
index d2356fc..4b892c3 100644
--- a/src/component/component-details-parser.vala
+++ b/src/component/component-details-parser.vala
@@ -25,7 +25,7 @@ public class DetailsParser : BaseObject {
         
         public Token(string token) {
             original = token;
-            casefolded = token.casefold();
+            casefolded = from_string(token).filter(c => !c.ispunct()).to_string(c => c.to_string());
         }
         
         public bool equal_to(Token other) {
@@ -179,6 +179,12 @@ public class DetailsParser : BaseObject {
                 continue;
             }
             
+            // if a recurring rule has been started, attempt to parse into additions for the rule
+            stack.mark();
+            if (rrule != null && parse_recurring(token))
+                continue;
+            stack.restore();
+            
             // if this token and next describe a duration, use them
             stack.mark();
             if (parse_duration(token, stack.pop()))
@@ -471,15 +477,15 @@ public class DetailsParser : BaseObject {
         return null;
     }
     
+    // this can create a new RRULE or edit an existing one, but will not create multiple RRULEs
+    // for the same VEVENT
     private bool parse_recurring(Token? specifier) {
+        if (specifier == null)
+            return false;
+        
         // 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 (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);
@@ -494,10 +500,9 @@ public class DetailsParser : BaseObject {
         // a day of the week
         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();
+            set_rrule_weekly(iterate<Calendar.DayOfWeek>(
+                Calendar.System.today.upcoming(dow, true).day_of_week).to_array_list().to_array(),
+                interval);
             
             return true;
         }
@@ -550,21 +555,33 @@ public class DetailsParser : BaseObject {
     }
     
     private void set_rrule_weekly(Calendar.DayOfWeek[]? by_days, int interval) {
-        Gee.Map<Calendar.DayOfWeek, int> map = new Gee.HashMap<Calendar.DayOfWeek, int>();
+        Gee.Map<Calendar.DayOfWeek?, int> map = new Gee.HashMap<Calendar.DayOfWeek?, int>();
         if (by_days != null) {
             foreach (Calendar.DayOfWeek by_day in by_days)
                 map.set(by_day, 0);
         }
         
-        // start at the first day in the by_days
-        start_date = Calendar.System.today;
-        if (by_days != null)
-            start_date = start_date.upcoming_in_set(from_array<Calendar.DayOfWeek>(by_days).to_hash_set(), 
true);
+        if (rrule == null) {
+            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));
+        } else {
+            rrule.add_by_rule(RecurrenceRule.ByRule.DAY, RecurrenceRule.encode_days(map));
+        }
         
-        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));
+        // find the earliest date in the by_days; if it's earlier than the start_date or the
+        // start_date isn't defined, use the earliest
+        if (by_days != null) {
+             Calendar.Date earliest = Calendar.System.today.upcoming_in_set(
+                from_array<Calendar.DayOfWeek>(by_days).to_hash_set(), true);
+            if (start_date == null || earliest.compare_to(start_date) < 0)
+                start_date = earliest;
+        }
+        
+        // no start_date at this point, then today is it
+        if (start_date == null)
+            start_date = Calendar.System.today;
     }
     
     private void set_rrule_yearly(Calendar.Date date, int interval) {
diff --git a/src/component/component-recurrence-rule.vala b/src/component/component-recurrence-rule.vala
index 6f7178a..1ee658a 100644
--- a/src/component/component-recurrence-rule.vala
+++ b/src/component/component-recurrence-rule.vala
@@ -50,7 +50,7 @@ public class RecurrenceRule : BaseObject {
     /**
      * Returns true if { link freq} is iCal.icalrecurrencetype_frequence.DAILY_RECURRENCE,
      */
-    public bool is_weekly { get { return freq == iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE; } }
+    public bool is_weekly { get { return freq == iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE; } }
     
     /**
      * Returns true if { link freq} is iCal.icalrecurrencetype_frequence.MONTHLY_RECURRENCE,
@@ -307,11 +307,28 @@ public class RecurrenceRule : BaseObject {
     }
     
     /**
+     * Encode a { link Calendar.DayOfWeek} and its position (i.e. Second Thursday of the month,
+     * last Wednesday of the yar) into a value for { link set_by_rule} when using
+     * { link ByRule.DAY}.
+     *
+     * Use null for DayOfWeek and zero for position to mean "any" or "every".
+     *
+     * @see encode_days
+     */
+    public static int encode_day(Calendar.DayOfWeek? dow, int position) {
+        int dow_value = (dow != null) ? dow.ordinal(Calendar.System.first_of_week) : 0;
+        
+        return (position.clamp(0, int.MAX) * 7) + dow_value;
+    }
+    
+    /**
      * Encode a Gee.Map of { link Calendar.DayOfWeek} and its position (i.e. Second Thursday of
      * the month, last Wednesday of the year) into a value for { link set_by_rule} when using
      * { link ByRule.DAY}.
      *
      * Use null for DayOfWeek and zero for position to mean "any" or "every".
+     *
+     * @encode_day
      */
     public static Gee.Collection<int>? encode_days(Gee.Map<Calendar.DayOfWeek?, int>? day_values) {
         if (day_values == null || day_values.size == 0)
@@ -319,68 +336,58 @@ public class RecurrenceRule : BaseObject {
         
         Gee.Collection<int> encoded = new Gee.ArrayList<int>();
         Gee.MapIterator<Calendar.DayOfWeek?, int> iter = day_values.map_iterator();
-        while (iter.next()) {
-            Calendar.DayOfWeek? dow = iter.get_key();
-            int dow_value = (dow != null) ? dow.ordinal(Calendar.FirstOfWeek.SUNDAY) : 0;
-            int position = iter.get_value().clamp(0, int.MAX);
-            
-            encoded.add((position * 7) + dow_value);
-        }
+        while (iter.next())
+            encoded.add(encode_day(iter.get_key(), iter.get_value()));
         
         return encoded;
     }
     
-    /**
-     * Replaces the existing set of values for the BY rules with the supplied values.
-     *
-     * Pass null or an empty Collection to clear the by-rules values.
-     *
-     * Use { link encode_days} when passing values for { link ByRule.DAY}.
-     *
-     * @see by_rule_updated
-     */
-    public void set_by_rule(ByRule by_rule, Gee.Collection<int>? values) {
-        Gee.SortedSet<int> by_set;
+    private Gee.SortedSet<int> get_by_set(ByRule by_rule) {
         switch (by_rule) {
             case ByRule.SECOND:
-                by_set = by_second;
-            break;
+                return by_second;
             
             case ByRule.MINUTE:
-                by_set = by_minute;
-            break;
+                return by_minute;
             
             case ByRule.HOUR:
-                by_set = by_hour;
-            break;
+                return by_hour;
             
             case ByRule.DAY:
-                by_set = by_day;
-            break;
+                return by_day;
             
             case ByRule.MONTH_DAY:
-                by_set = by_month_day;
-            break;
+                return by_month_day;
             
             case ByRule.YEAR_DAY:
-                by_set = by_year_day;
-            break;
+                return by_year_day;
             
             case ByRule.WEEK_NUM:
-                by_set = by_week_num;
-            break;
+                return by_week_num;
             
             case ByRule.MONTH:
-                by_set = by_month;
-            break;
+                return by_month;
             
             case ByRule.SET_POS:
-                by_set = by_set_pos;
-            break;
+                return by_set_pos;
             
             default:
                 assert_not_reached();
         }
+    }
+    
+    /**
+     * Replaces the existing set of values for the BY rules with the supplied values.
+     *
+     * Pass null or an empty Collection to clear the by-rules values.
+     *
+     * Use { link encode_days} when passing values for { link ByRule.DAY}.
+     *
+     * @see add_by_rule
+     * @see by_rule_updated
+     */
+    public void set_by_rule(ByRule by_rule, Gee.Collection<int>? values) {
+        Gee.SortedSet<int> by_set = get_by_set(by_rule);
         
         by_set.clear();
         if (values != null && values.size > 0)
@@ -390,6 +397,25 @@ public class RecurrenceRule : BaseObject {
     }
     
     /**
+     * Adds the supplied values to the existing set of values for the BY rules.
+     *
+     * Null or an empty Collection is a no-op.
+     *
+     * Use { link encode_days} when passing values for { link ByRule.DAY}.
+     *
+     * @see set_by_rule
+     * @see by_rule_updated
+     */
+    public void add_by_rule(ByRule by_rule, Gee.Collection<int>? values) {
+        Gee.SortedSet<int> by_set = get_by_set(by_rule);
+        
+        if (values != null && values.size > 0)
+            by_set.add_all(values);
+        
+        by_rule_updated(by_rule);
+    }
+    
+    /**
      * Converts a { link RecurrenceRule} into an iCalendar RRULE property and adds it to the
      * iCal component.
      *
diff --git a/src/tests/tests-quick-add-recurring.vala b/src/tests/tests-quick-add-recurring.vala
index 6f6d591..c6ff4cd 100644
--- a/src/tests/tests-quick-add-recurring.vala
+++ b/src/tests/tests-quick-add-recurring.vala
@@ -15,6 +15,10 @@ private class QuickAddRecurring : UnitTest.Harness {
         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);
+        
+        // WEEKLY
+        add_case("every-tuesday", every_tuesday);
+        add_case("every-tuesday-thursday", every_tuesday_thursday);
     }
     
     protected override void setup() throws Error {
@@ -43,6 +47,10 @@ private class QuickAddRecurring : UnitTest.Harness {
             && event.exact_time_span.start_exact_time.to_wall_time().equal_to(new Calendar.WallTime(10, 0, 
0));
     }
     
+    //
+    // DAILY
+    //
+    
     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)
@@ -94,6 +102,31 @@ private class QuickAddRecurring : UnitTest.Harness {
             && event.rrule.until_date != null
             && event.rrule.until_date.equal_to(end);
     }
+    
+    //
+    // WEEKLY
+    //
+    
+    private bool every_tuesday(out string? dump) throws Error {
+        Component.Event event;
+        return basic("meeting at work at 10am every tuesday", out event, out dump)
+            && event.rrule.is_weekly
+            && event.rrule.interval == 1
+            && !event.rrule.has_duration
+            && !event.is_all_day
+            && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.TUE);
+    }
+    
+    private bool every_tuesday_thursday(out string? dump) throws Error {
+        Component.Event event;
+        return basic("meeting at work at 10am every tuesday, thursday", out event, out dump)
+            && event.rrule.is_weekly
+            && event.rrule.interval == 1
+            && !event.rrule.has_duration
+            && !event.is_all_day
+            && (event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.TUE)
+                || event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.THU));
+    }
 }
 
 }


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