[california/wip/725792-quick-add] Code simplifications and cleanup



commit 122e0bc1fb4403760563414137082c59157f1655
Author: Jim Nelson <jim yorba org>
Date:   Fri Apr 18 18:19:59 2014 -0700

    Code simplifications and cleanup

 src/Makefile.am                                |    2 +
 src/calendar/calendar.vala                     |    2 +
 src/collection/collection-lookahead-stack.vala |  115 +++++++++
 src/collection/collection.vala                 |   22 ++
 src/component/component-details-parser.vala    |  297 ++++++++++++++----------
 src/component/component.vala                   |    9 +-
 6 files changed, 322 insertions(+), 125 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index e83888e..868360f 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -69,7 +69,9 @@ california_VALASOURCES = \
        calendar/calendar-week-span.vala \
        calendar/calendar-year.vala \
        \
+       collection/collection.vala \
        collection/collection-iterable.vala \
+       collection/collection-lookahead-stack.vala \
        collection/collection-simple-iterator.vala \
        collection/collection-simple-iterable.vala \
        \
diff --git a/src/calendar/calendar.vala b/src/calendar/calendar.vala
index 1f61887..7f0d1b5 100644
--- a/src/calendar/calendar.vala
+++ b/src/calendar/calendar.vala
@@ -161,6 +161,7 @@ public void init() throws Error {
     System.preinit();
     
     // internal initialization
+    Collection.init();
     OlsonZone.init();
     DayOfWeek.init();
     DayOfMonth.init();
@@ -181,6 +182,7 @@ public void terminate() {
     DayOfMonth.terminate();
     DayOfWeek.terminate();
     OlsonZone.terminate();
+    Collection.terminate();
     
     UNIT_DAYS = UNIT_HOURS = UNIT_MINS = null;
 }
diff --git a/src/collection/collection-lookahead-stack.vala b/src/collection/collection-lookahead-stack.vala
new file mode 100644
index 0000000..0bfadad
--- /dev/null
+++ b/src/collection/collection-lookahead-stack.vala
@@ -0,0 +1,115 @@
+/* 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.Collection {
+
+/**
+ * A remove-only stack of elements that allows for marking (saving state) and restoration.
+ *
+ * To make saving and restoring as efficient as possible, additions are not possible with this
+ * collection.  The stack is initialized with elements in the constructor.  Thereafter, elements
+ * may only be { link pop}ped and elements added back via { link restore}.
+ */
+
+public class LookaheadStack<G> : BaseObject {
+    /**
+     * Returns true if no elements are in the queue.
+     */
+    public bool is_empty { get { return deque.is_empty; } }
+    
+    /**
+     * Returns the number of elements remaining in the stack.
+     */
+    public int size { get { return deque.size; } }
+    
+    /**
+     * Returns true if the stack is marked.
+     *
+     * @see mark
+     */
+    public bool is_marked { get { return marked != null; } }
+    
+    /**
+     * Returns the number of elements residing as marked state.
+     *
+     * @see mark
+     */
+    public int marked_size { get { return (marked != null) ? marked.size : 0; } }
+    
+    /**
+     * Returns the current element at the top of the stack.
+     */
+    public G? top { owned get { return deque.peek_head(); } }
+    
+    private Gee.Deque<G> deque;
+    private Gee.Queue<G>? marked = null;
+    
+    public LookaheadStack(Gee.Collection<G> init) {
+        // must be initialized here; see
+        // https://bugzilla.gnome.org/show_bug.cgi?id=523767
+        deque = new Gee.LinkedList<G>();
+        deque.add_all(init);
+    }
+    
+    /**
+     * Returns null if empty.
+     */
+    public G? pop() {
+        if (deque.is_empty)
+            return null;
+        
+        G element = deque.poll_head();
+        
+        // if stack is marked, save for potential restoration
+        if (marked != null)
+            marked.offer(element);
+        
+        return element;
+    }
+    
+    /**
+     * Marks the state of the stack so it can be restored with { link restore}.
+     *
+     * Marking a marked stack is functionally equivalent as calling { link unmark} followed by
+     * this method.
+     */
+    public void mark() {
+        marked = new Gee.LinkedList<G>();
+    }
+    
+    /**
+     * Restores the state of the stack to the point when { link mark} was called.
+     *
+     * restore() implies { link unmark}.
+     *
+     * This does nothing if mark() was not first called.
+     */
+    public void restore() {
+        if (marked == null)
+            return;
+        
+        // restore elements as stored in marked queue
+        while (!marked.is_empty)
+            deque.offer_head(marked.poll());
+        
+        // now unmarked
+        marked = null;
+    }
+    
+    /**
+     * Unmarks the stack, making restoration of popped elements impossible.
+     */
+    public void unmark() {
+        marked = null;
+    }
+    
+    public override string to_string() {
+        return "LookaheadStack (%d elements)".printf(size);
+    }
+}
+
+}
+
diff --git a/src/collection/collection.vala b/src/collection/collection.vala
new file mode 100644
index 0000000..ba28f21
--- /dev/null
+++ b/src/collection/collection.vala
@@ -0,0 +1,22 @@
+/* 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.Collection {
+
+private int init_count = 0;
+
+public void init() throws Error {
+    if (!Unit.do_init(ref init_count))
+        return;
+}
+
+public void terminate() {
+    if (!Unit.do_terminate(ref init_count))
+        return;
+}
+
+}
+
diff --git a/src/component/component-details-parser.vala b/src/component/component-details-parser.vala
index 55ac53a..724b3af 100644
--- a/src/component/component-details-parser.vala
+++ b/src/component/component-details-parser.vala
@@ -19,11 +19,34 @@ namespace California.Component {
  */
 
 public class DetailsParser : BaseObject {
+    private class Token : BaseObject, Gee.Hashable<Token> {
+        public string original;
+        public string casefolded;
+        
+        public Token(string token) {
+            original = token;
+            casefolded = token.casefold();
+        }
+        
+        public bool equal_to(Token other) {
+            return (this != other) ? original == other.original : true;
+        }
+        
+        public uint hash() {
+            return original.hash();
+        }
+        
+        public override string to_string() {
+            return original;
+        }
+    }
+    
     /**
      * The generated { link Event}.
      */
-    public Component.Event event { get; private set; }
+    public Component.Event event { get; private set; default = new Component.Event.blank(); }
     
+    private Collection.LookaheadStack<Token> stack;
     private StringBuilder summary = new StringBuilder();
     private StringBuilder location = new StringBuilder();
     private Calendar.WallTime? start_time = null;
@@ -43,49 +66,57 @@ public class DetailsParser : BaseObject {
      * If the details string is empty, a blank Event is generated.
      */
     public DetailsParser(string? details) {
-        event = parse(details);
+        // tokenize the string and arrange as a stack for the parser
+        string[] tokenized = String.reduce_whitespace(details ?? "").split(" ");
+        Gee.LinkedList<Token> list = new Gee.LinkedList<Token>();
+        foreach (string token in tokenized)
+            list.add(new Token(token));
+        
+        stack = new Collection.LookaheadStack<Token>(list);
+        
+        parse();
     }
     
-    private Component.Event parse(string details) {
-        string[] tokens = String.reduce_whitespace(details).split(" ");
-        for (int ctr = 0; ctr < tokens.length; ctr++) {
-            string token = tokens[ctr].casefold();
-            string? next_token = (ctr + 1 < tokens.length) ? tokens[ctr + 1].casefold() : null;
-            string? next_next_token = (ctr + 2 < tokens.length) ? tokens[ctr + 2].casefold() : null;
+    private void parse() {
+        for (;;) {
+            Token? token = stack.pop();
+            if (token == null)
+                break;
             
-            // strip time prepositions if actually followed by time (even if liberally parsed, i.e.
-            // "8"-> 8am
-            if (next_token != null && token in TIME_PREPOSITIONS && is_time_or_date(next_token, true)) {
-                debug("is time");
-                if (!add_wall_clock_time(next_token, true))
-                    assert(add_date(next_token));
-                
-                ctr++;
-                
+            // mark the stack branch for each parsing branch so if it fails the state can be
+            // restored and the next branch's read-ahead gets a chance; don't restore on success
+            // as each method is responsible for consuming all tokens it needs to complete its work
+            // and no more.
+            
+            // look for prepositions indicating time or location follows; this depends on
+            // translated strings, obviously, and does not apply to all languages, but we do what
+            // we can here.
+            
+            // A time preposition suggests a specific point of time is being described in the
+            // following token.  Don't require strict parsing of time ("8" -> "8am") because the
+            // preposition offers a clue that a time is being specified.
+            stack.mark();
+            if (token.casefolded in TIME_PREPOSITIONS && parse_time(stack.pop(), false))
                 continue;
-            }
+            stack.restore();
             
-            if (next_token != null && token in DURATION_PREPOSITIONS && parse_duration(next_token, 
next_next_token) != null) {
-                debug("is duration");
-                add_duration(next_token, next_next_token);
-                ctr += 2;
-                
+            // A duration preposition suggests a specific amount of positive time is being described
+            // by the next two tokens.
+            stack.mark();
+            if (token.casefolded in DURATION_PREPOSITIONS && parse_duration(stack.pop(), stack.pop()))
                 continue;
-            }
+            stack.restore();
             
-            if (next_token != null && start_time == null && token in DELAY_PREPOSITIONS && 
parse_duration(next_token, next_next_token) != null) {
-                debug("is delay");
-                Calendar.Duration duration = parse_duration(next_token, next_next_token);
-                start_time = new Calendar.WallTime.from_exact_time(Calendar.System.now.adjust_time((int) 
duration.minutes, Calendar.TimeUnit.MINUTE));
-                ctr += 2;
-                
+            // A delay preposition suggests a specific point of time is being described by a
+            // positive duration of time after the current time by the next two tokens.
+            stack.mark();
+            if (token.casefolded in DELAY_PREPOSITIONS && parse_delay(stack.pop(), stack.pop()))
                 continue;
-            }
+            stack.restore();
             
-            // start adding to location field if location preposition encountered
-            if (!adding_location && token in LOCATION_PREPOSITIONS) {
-                debug("is location");
-                // add current token to summary
+            // only look for location prepositions if not already adding text to the location field
+            if (!adding_location && token.casefolded in LOCATION_PREPOSITIONS) {
+                // add current token (the preposition) to summary but not location
                 add_text(token);
                 
                 // now adding to both summary and location
@@ -94,31 +125,44 @@ public class DetailsParser : BaseObject {
                 continue;
             }
             
-            if (add_duration(token, next_token)) {
-                ctr++;
-                
+            // if this token and next describe a duration, use them
+            stack.mark();
+            if (parse_duration(token, stack.pop()))
                 continue;
-            }
+            stack.restore();
             
             // attempt to (strictly) parse into wall-clock time
-            if (add_wall_clock_time(token, false))
-                continue;
-            
-            // ditto for dates
-            if (add_date(token))
+            stack.mark();
+            if (parse_time(token, true))
                 continue;
+            stack.restore();
             
-            // append original to current text field as fallback
-            add_text(tokens[ctr]);
+            // append original to current text field(s) as fallback
+            add_text(token);
+        }
+        
+        //
+        // assemble accumulated information in an Event, using defaults wherever appropriate
+        //
+        
+        // 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) {
+            start_time = new Calendar.WallTime.from_exact_time(Calendar.System.now);
+            end_time = new Calendar.WallTime.from_exact_time(
+                Calendar.System.now.adjust_time((int) duration.minutes, Calendar.TimeUnit.MINUTE));
+            duration = null;
         }
         
         // if a start time was described but not end time, use a 1 hour duration default
         bool midnight_crossed = false;
         if (start_time != null && end_time == null) {
-            if (duration != null)
-                end_time = start_time.adjust((int) duration.minutes, Calendar.TimeUnit.MINUTE, out 
midnight_crossed);
-            else
+            if (duration != null) {
+                end_time = start_time.adjust((int) duration.minutes, Calendar.TimeUnit.MINUTE,
+                    out midnight_crossed);
+            } else {
                 end_time = start_time.adjust(1, Calendar.TimeUnit.HOUR, out midnight_crossed);
+            }
         }
         
         // if no start date was described but a start time was, assume for today
@@ -138,9 +182,7 @@ public class DetailsParser : BaseObject {
         debug("summary: \"%s\"", summary.str);
         debug("location: \"%s\"", location.str);
         
-        Component.Event event = new Component.Event.blank();
-        
-        // fill in time/date, if specified
+        // Specify event start/end time, if specified
         if (start_time != null && end_time != null) {
             assert(start_date != null);
             assert(end_date != null);
@@ -153,95 +195,111 @@ public class DetailsParser : BaseObject {
             event.set_event_date_span(new Calendar.DateSpan(start_date, end_date));
         }
         
-        // other details
+        // other event details
         if (!String.is_empty(summary.str))
             event.summary = summary.str;
+        
         if (!String.is_empty(location.str))
             event.location = location.str;
-        
-        return event;
     }
     
-    private void add_text(string token) {
-        // always add to summary
-        add_to_builder(summary, token);
-        
-        // add to location if in that mode
-        if (adding_location)
-            add_to_builder(location, token);
-    }
-    
-    private void add_to_builder(StringBuilder builder, string token) {
-        // keep everything space-delimited
-        if (!String.is_empty(builder.str))
-            builder.append_unichar(' ');
+    private bool parse_time(Token? specifier, bool strict) {
+        if (specifier == null)
+            return false;
         
-        builder.append(token);
-    }
-    
-    private bool is_time_or_date(string token, bool liberal_ok) {
-        bool liberally_parsed;
-        if (parse_time(token, out liberally_parsed) != null)
-            return liberal_ok ? true : liberally_parsed;
+        // parse looking for time or date ... time specifier doesn't have to be strictly parsed,
+        // i.e. "8" -> 8am is okay, since the time preposition offers a clue
+        Calendar.WallTime? wall_time = null;
+        Calendar.Date? date = parse_relative_date(specifier.casefolded);
+        if (date == null) {
+            bool liberally_parsed;
+            wall_time = Calendar.WallTime.parse(specifier.casefolded, out liberally_parsed);
+            if (wall_time != null && liberally_parsed && strict)
+                return false;
+        }
         
-        return parse_date(token) != null;
+        if (wall_time != null)
+            return add_wall_time(wall_time);
+        else if (date != null)
+            return add_date(date);
+        else
+            return false;
     }
     
-    private Calendar.Duration? parse_duration(string token, string? next_token) {
-        if (String.is_empty(next_token))
-            return null;
+    // Add a duration to the event if not already specified and an end time has not already been
+    // specified
+    private bool parse_duration(Token? amount, Token? unit) {
+        if (amount == null || unit == null)
+            return false;
         
-        return Calendar.Duration.parse(token, next_token);
-    }
-    
-    private bool add_duration(string token, string? next_token) {
         if (end_time != null || duration != null)
             return false;
         
-        duration = parse_duration(token, next_token);
+        duration = Calendar.Duration.parse(amount.casefolded, unit.casefolded);
         
         return duration != null;
     }
     
-    private Calendar.WallTime? parse_time(string token, out bool liberally_parsed) {
-        return Calendar.WallTime.parse(token, out liberally_parsed);
-    }
-    
-    private bool add_wall_clock_time(string token, bool liberal_ok) {
-        if (start_time != null && end_time != null)
+    private bool parse_delay(Token? amount, Token? unit) {
+        if (amount == null || unit == null)
             return false;
         
-        // attempt to parse into wall clock time .. first one found is start time, next is
-        // end time, rest are ignored
-        bool liberally_parsed;
-        Calendar.WallTime? wall_time = parse_time(token, out liberally_parsed);
-        if (wall_time != null) debug("%s %s %s", wall_time.to_string(), liberally_parsed.to_string(), 
liberal_ok.to_string());
-        if (wall_time == null || (liberally_parsed && !liberal_ok))
+        // Since delay is a way of specifying the start time, don't add if already known
+        if (start_time != null)
             return false;
         
-        if (start_time == null) {
-            start_time = wall_time;
-            
-            return true;
-        }
+        Calendar.Duration? delay = Calendar.Duration.parse(amount.casefolded, unit.casefolded);
+        if (delay == null)
+            return false;
         
-        assert(end_time == null);
-        end_time = wall_time;
+        start_time = new Calendar.WallTime.from_exact_time(
+            Calendar.System.now.adjust_time((int) delay.minutes, Calendar.TimeUnit.MINUTE));
         
         return true;
     }
     
-    private Calendar.Date? parse_date(string token) {
+    // Adds the text to the summary and location field, if adding_location is set
+    private void add_text(Token token) {
+        // always add to summary
+        add_to_builder(summary, token);
+        
+        // add to location if in that mode
+        if (adding_location)
+            add_to_builder(location, token);
+    }
+    
+    private static void add_to_builder(StringBuilder builder, Token token) {
+        // keep everything space-delimited
+        if (!String.is_empty(builder.str))
+            builder.append_unichar(' ');
+        
+        builder.append(token.original);
+    }
+    
+    // Adds a time to the event, start time first, then end time, dropping thereafter
+    private bool add_wall_time(Calendar.WallTime wall_time) {
+        if (start_time == null)
+            start_time = wall_time;
+        else if (end_time == null)
+            end_time = wall_time;
+        else
+            return false;
+        
+        return true;
+    }
+    
+    // Parses a potential date specifier into a calendar date relative to today
+    private Calendar.Date? parse_relative_date(string casefolded) {
         // attempt to parse into common words for relative dates
-        if (token == TODAY)
+        if (casefolded == TODAY)
             return Calendar.System.today;
-        else if (token == TOMORROW)
+        else if (casefolded == TOMORROW)
             return Calendar.System.today.next();
-        else if (token == YESTERDAY)
+        else if (casefolded == YESTERDAY)
             return Calendar.System.today.previous();
         
         // attempt to parse into day of the week
-        Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(token);
+        Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(casefolded);
         if (dow == null)
             return null;
         
@@ -259,23 +317,14 @@ public class DetailsParser : BaseObject {
         return null;
     }
     
-    private bool add_date(string token) {
-        if (start_date != null && end_date != null)
-            return false;
-        
-        Calendar.Date? date = parse_date(token);
-        if (date == null)
-            return false;
-        
-        // like wall clock time, first is start date, next is end date, after that, ignored
-        if (start_date == null) {
+    // Adds a date to the event, start time first, then end time, dropping dates thereafter
+    private bool add_date(Calendar.Date date) {
+        if (start_date == null)
             start_date = date;
-            
-            return true;
-        }
-        
-        assert(end_date == null);
-        end_date = date;
+        else if (end_date == null)
+            end_date = date;
+        else
+            return false;
         
         return true;
     }
diff --git a/src/component/component.vala b/src/component/component.vala
index ac7464e..a4e80a2 100644
--- a/src/component/component.vala
+++ b/src/component/component.vala
@@ -30,6 +30,7 @@ public void init() throws Error {
         return;
     
     // external unit init
+    Collection.init();
     Calendar.init();
     
     // Used by quick-add to indicate the user wants to create an event for today.  Should be
@@ -48,6 +49,7 @@ public void init() throws Error {
     // specific time, not a duration).  Each word must be separated by semi-colons.  All
     // words should be casefolded (lowercase).  It's allowable for some or all of these words to
     // be duplicated in the location prepositions list (elsewhere) but not another time list.
+    // The list can be empty, but that will limit the parser.
     // Examples: "at 9am", "from 10pm to 11:30pm", "on monday"
     TIME_PREPOSITIONS = _("at;from;to;on;").split(";");
     
@@ -55,6 +57,7 @@ public void init() throws Error {
     // a duration, not a specific time).  Each word must be separated by semi-colons.  All
     // words should be casefolded (lowercase).  It's allowable for some or all of these words to
     // be duplicated in the location prepositions list (elsewhere) but not another time list.
+    // The list can be empty, but that will limit the parser.
     // Examples: "for 3 hours", "for 90 minutes"
     DURATION_PREPOSITIONS = _("for;").split(";");
     
@@ -62,13 +65,16 @@ public void init() throws Error {
     // time from the current moment).  Each word must be separated by semi-colons.  All
     // words should be casefolded (lowercase).  It's allowable for some or all of these words to
     // be duplicated in the location prepositions list (elsewhere) but not another time list.
+    // The list can be empty, but that will limit the parser.
     // Example: "in 3 hours" (meaning 3 hours from now)
     DELAY_PREPOSITIONS = _("in;").split(";");
     
     // Used by quick-add to determine if the word is a location-based preposition (indicating a
     // specific place).  Each word must be separated by semi-colons.  All words should be
     // casefolded (lowercase).  It's allowable for some or all of these words to be duplicated in
-    // the time prepositions list (elsewhere).
+    // the time prepositions list (elsewhere).  The list can be empty, but that will limit the
+    // parser.
+    // Example: "at supermarket", "at Eiffel Tower"
     LOCATION_PREPOSITIONS = _("at;").split(";");
 }
 
@@ -79,6 +85,7 @@ public void terminate() {
     TIME_PREPOSITIONS = LOCATION_PREPOSITIONS = DURATION_PREPOSITIONS = null;
     
     Calendar.terminate();
+    Collection.terminate();
 }
 
 }


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