[california/wip/725792-quick-add] Additional date parsing and better StackLookahead mark/restore



commit 2fcb47ce344146c4a87e7927daa5fd4f3cc82109
Author: Jim Nelson <jim yorba org>
Date:   Fri Apr 18 19:44:51 2014 -0700

    Additional date parsing and better StackLookahead mark/restore

 src/calendar/calendar-day-of-week.vala         |    1 +
 src/calendar/calendar-month.vala               |   19 ++++
 src/collection/collection-lookahead-stack.vala |   92 +++++++++++--------
 src/component/component-details-parser.vala    |  118 ++++++++++++++++++++----
 src/component/component.vala                   |    9 ++-
 5 files changed, 182 insertions(+), 57 deletions(-)
---
diff --git a/src/calendar/calendar-day-of-week.vala b/src/calendar/calendar-day-of-week.vala
index 80aeb57..dae4e05 100644
--- a/src/calendar/calendar-day-of-week.vala
+++ b/src/calendar/calendar-day-of-week.vala
@@ -186,6 +186,7 @@ public class DayOfWeek : BaseObject, Gee.Hashable<DayOfWeek> {
     public static DayOfWeek? parse(string str) {
         string token = str.strip().casefold();
         
+        // a lookup map may make sense here
         foreach (DayOfWeek dow in days_of_week_monday) {
             if (dow.abbrev_name.casefold() == token)
                 return dow;
diff --git a/src/calendar/calendar-month.vala b/src/calendar/calendar-month.vala
index 5a62adc..ffdca5a 100644
--- a/src/calendar/calendar-month.vala
+++ b/src/calendar/calendar-month.vala
@@ -131,6 +131,25 @@ public class Month : BaseObject, Gee.Comparable<Month>, Gee.Hashable<Month> {
         return for_checked(gdate.get_month());
     }
     
+    /**
+     * Compares the supplied string with all translated { link Month} names, both { link abbrev_name}
+     * and { link full_name}.
+     */
+    public static Month? parse(string str) {
+        string casefolded = str.casefold();
+        
+        // a lookup map may make sense here
+        foreach (Month month in months) {
+            if (month.full_name.casefold() == casefolded)
+                return month;
+            
+            if (month.abbrev_name.casefold() == casefolded)
+                return month;
+        }
+        
+        return null;
+    }
+    
     internal inline DateMonth to_date_month() {
         return (DateMonth) value;
     }
diff --git a/src/collection/collection-lookahead-stack.vala b/src/collection/collection-lookahead-stack.vala
index 0bfadad..0b996ce 100644
--- a/src/collection/collection-lookahead-stack.vala
+++ b/src/collection/collection-lookahead-stack.vala
@@ -18,54 +18,50 @@ public class LookaheadStack<G> : BaseObject {
     /**
      * Returns true if no elements are in the queue.
      */
-    public bool is_empty { get { return deque.is_empty; } }
+    public bool is_empty { get { return stack.is_empty; } }
     
     /**
      * Returns the number of elements remaining in the stack.
      */
-    public int size { get { return deque.size; } }
+    public int size { get { return stack.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; } }
+    public int markpoint_count { get { return markpoints.size + (markpoint != null ? 1 : 0); } }
     
     /**
      * Returns the current element at the top of the stack.
      */
-    public G? top { owned get { return deque.peek_head(); } }
+    public G? top { owned get { return stack.peek_head(); } }
     
-    private Gee.Deque<G> deque;
-    private Gee.Queue<G>? marked = null;
+    private Gee.Deque<G> stack;
+    private Gee.Deque<Gee.Deque<G>>? markpoints;
+    private Gee.Deque<G>? markpoint = 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);
+        stack = new Gee.LinkedList<G>();
+        stack.add_all(init);
+        
+        markpoints = new Gee.LinkedList<Gee.Deque<G>>();
     }
     
     /**
      * Returns null if empty.
      */
     public G? pop() {
-        if (deque.is_empty)
+        if (stack.is_empty)
             return null;
         
-        G element = deque.poll_head();
+        G element = stack.poll_head();
         
-        // if stack is marked, save for potential restoration
-        if (marked != null)
-            marked.offer(element);
+        // if markpoint set, save element for later
+        if (markpoint != null)
+            markpoint.offer_head(element);
         
         return element;
     }
@@ -73,37 +69,59 @@ public class LookaheadStack<G> : BaseObject {
     /**
      * 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.
+     * Multiple markpoints can be made, each requiring a matching { link restore} to return to the
+     * state.
      */
     public void mark() {
-        marked = new Gee.LinkedList<G>();
+        if (markpoint != null)
+            markpoints.offer_head(markpoint);
+        
+        markpoint = new Gee.LinkedList<G>();
     }
     
     /**
-     * Restores the state of the stack to the point when { link mark} was called.
-     *
-     * restore() implies { link unmark}.
+     * Restores the state of the stack to the point when the last markpoint was made.
      *
-     * This does nothing if mark() was not first called.
+     * This does nothing if { link mark} was not first called.
      */
     public void restore() {
-        if (marked == null)
-            return;
+        if (markpoint != null) {
+            // restore elements as stored in marked queue
+            while (!markpoint.is_empty)
+                stack.offer_head(markpoint.poll_head());
+        }
         
-        // restore elements as stored in marked queue
-        while (!marked.is_empty)
-            deque.offer_head(marked.poll());
-        
-        // now unmarked
-        marked = null;
+        // pop last marked state, if any, as the current marked state
+        pop_markpoint();
     }
     
     /**
-     * Unmarks the stack, making restoration of popped elements impossible.
+     * Drops the last markpoint, if any.
+     *
+     * This is functionally equivalent to { link restore}, but the current markpoint elements are
+     * not added back to the stack.  Prior markpoints remain.
+     *
+     * @see mark
      */
     public void unmark() {
-        marked = null;
+        pop_markpoint();
+    }
+    
+    /**
+     * Drops all markpoints.
+     *
+     * @see mark
+     */
+    public void clear_marks() {
+        markpoint = null;
+        markpoints.clear();
+    }
+    
+    private void pop_markpoint() {
+        if (!markpoints.is_empty)
+            markpoint = markpoints.poll_head();
+        else
+            markpoint = null;
     }
     
     public override string to_string() {
diff --git a/src/component/component-details-parser.vala b/src/component/component-details-parser.vala
index 724b3af..7454ce6 100644
--- a/src/component/component-details-parser.vala
+++ b/src/component/component-details-parser.vala
@@ -42,6 +42,11 @@ public class DetailsParser : BaseObject {
     }
     
     /**
+     * The original string of text generating the { link event}.
+     */
+    public string details { get; private set; }
+    
+    /**
      * The generated { link Event}.
      */
     public Component.Event event { get; private set; default = new Component.Event.blank(); }
@@ -66,6 +71,8 @@ public class DetailsParser : BaseObject {
      * If the details string is empty, a blank Event is generated.
      */
     public DetailsParser(string? details) {
+        this.details = 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>();
@@ -174,6 +181,7 @@ public class DetailsParser : BaseObject {
         if (start_date != null && end_date == null)
             end_date = midnight_crossed ? start_date.adjust(1, Calendar.DateUnit.DAY) : start_date;
         
+        debug("details: %s", details);
         debug("start time: %s", (start_time != null) ? start_time.to_string() : "(null)");
         debug("end time: %s", (end_time != null) ? end_time.to_string() : "(null)");
         debug("duration: %s", (duration != null) ? duration.to_string() : "(null)");
@@ -207,23 +215,50 @@ public class DetailsParser : BaseObject {
         if (specifier == null)
             return false;
         
-        // 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;
+        // go for the gusto and look for 2 or 3 specifiers in total, month and day or month/day/year
+        // in any order
+        stack.mark();
+        {
+            Token? second = stack.pop();
+            if (second != null) {
+                Calendar.Date? date = parse_day_month(specifier, second);
+                if (date == null)
+                    date = parse_day_month(second, specifier);
+                
+                if (date != null && add_date(date))
+                    return true;
+            }
         }
+        stack.restore();
         
-        if (wall_time != null)
-            return add_wall_time(wall_time);
-        else if (date != null)
-            return add_date(date);
-        else
+        stack.mark();
+        {
+            Token? second = stack.pop();
+            Token? third = stack.pop();
+            if (second != null && third != null) {
+                // try d/m/y followed by m/d/y ... every other combination seems overkill
+                Calendar.Date? date = parse_day_month_year(specifier, second, third);
+                if (date == null)
+                    date = parse_day_month_year(second, specifier, third);
+                
+                if (date != null && add_date(date))
+                    return true;
+            }
+        }
+        stack.restore();
+        
+        // parse single specifier looking for date first, then time
+        Calendar.Date? date = parse_relative_date(specifier);
+        if (date != null && add_date(date))
+            return true;
+        
+        bool liberally_parsed;
+        Calendar.WallTime? wall_time = Calendar.WallTime.parse(specifier.casefolded,
+            out liberally_parsed);
+        if (wall_time != null && liberally_parsed && strict)
             return false;
+        
+        return (wall_time != null) ? add_wall_time(wall_time) : false;
     }
     
     // Add a duration to the event if not already specified and an end time has not already been
@@ -289,17 +324,17 @@ public class DetailsParser : BaseObject {
     }
     
     // Parses a potential date specifier into a calendar date relative to today
-    private Calendar.Date? parse_relative_date(string casefolded) {
+    private Calendar.Date? parse_relative_date(Token token) {
         // attempt to parse into common words for relative dates
-        if (casefolded == TODAY)
+        if (token.casefolded == TODAY)
             return Calendar.System.today;
-        else if (casefolded == TOMORROW)
+        else if (token.casefolded == TOMORROW)
             return Calendar.System.today.next();
-        else if (casefolded == YESTERDAY)
+        else if (token.casefolded == YESTERDAY)
             return Calendar.System.today.previous();
         
         // attempt to parse into day of the week
-        Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(casefolded);
+        Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(token.casefolded);
         if (dow == null)
             return null;
         
@@ -317,6 +352,51 @@ public class DetailsParser : BaseObject {
         return null;
     }
     
+    // 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))
+            return null;
+        
+        Calendar.Month? month = Calendar.Month.parse(mon.casefolded);
+        if (month == null)
+            return null;
+        
+        if (year == null)
+            year = Calendar.System.today.year;
+        
+        try {
+            return new Calendar.Date(Calendar.DayOfMonth.for(int.parse(day.casefolded)),
+                month, year);
+        } catch (CalendarError calerr) {
+            // probably an out-of-bounds day of month
+            return null;
+        }
+    }
+    
+    // Parses potential date specifiers into a specific calendar date
+    private Calendar.Date? parse_day_month_year(Token day, Token mon, Token yr) {
+        if (!String.is_numeric(yr.casefolded))
+            return null;
+        
+        // a *sane* year
+        int year = int.parse(yr.casefolded);
+        int current_year = Calendar.System.today.year.value;
+        if (year < (current_year - 1) || (year > current_year + 6))
+            return null;
+        
+        return parse_day_month(day, mon, new Calendar.Year(year));
+    }
+    
     // 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)
diff --git a/src/component/component.vala b/src/component/component.vala
index a4e80a2..e929eda 100644
--- a/src/component/component.vala
+++ b/src/component/component.vala
@@ -24,6 +24,7 @@ private string[] TIME_PREPOSITIONS;
 private string[] LOCATION_PREPOSITIONS;
 private string[] DURATION_PREPOSITIONS;
 private string[] DELAY_PREPOSITIONS;
+private string[] ORDINAL_SUFFIXES;
 
 public void init() throws Error {
     if (!Unit.do_init(ref init_count))
@@ -76,13 +77,19 @@ public void init() throws Error {
     // parser.
     // Example: "at supermarket", "at Eiffel Tower"
     LOCATION_PREPOSITIONS = _("at;").split(";");
+    
+    // Used by quick-add to strip date numbers of common ordinal suffices.  All suffixes should
+    // be casefolded (lowercase) and delimited by semi-colons.  The list can be empty, but that
+    // will limit the parser if your language supports ordinal suffixes.
+    // Example: "1st", "2nd", "3rd", "4th"
+    ORDINAL_SUFFIXES = _("st;nd;rd;th").split(";");
 }
 
 public void terminate() {
     if (!Unit.do_terminate(ref init_count))
         return;
     
-    TIME_PREPOSITIONS = LOCATION_PREPOSITIONS = DURATION_PREPOSITIONS = null;
+    TIME_PREPOSITIONS = LOCATION_PREPOSITIONS = DURATION_PREPOSITIONS = ORDINAL_SUFFIXES = null;
     
     Calendar.terminate();
     Collection.terminate();


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