[california] Quick Add should allow numeric dates such as 7/2: Bug #732032



commit 0ecea3fe9943c9f6d934bafde77b5db547806eba
Author: Jim Nelson <jim yorba org>
Date:   Thu Aug 14 16:59:59 2014 -0700

    Quick Add should allow numeric dates such as 7/2: Bug #732032
    
    Numeric dates are now parsed.  California also determines
    month-day-year ordering at startup.

 src/Makefile.am                             |    1 +
 src/calendar/calendar-date-ordering.vala    |   47 ++++++++
 src/calendar/calendar-span.vala             |    2 +-
 src/calendar/calendar-system.vala           |   74 +++++++++++++
 src/component/component-details-parser.vala |  103 ++++++++++++++++++-
 src/tests/tests-quick-add.vala              |  151 +++++++++++++++++++++++++++
 6 files changed, 376 insertions(+), 2 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index de238ac..a00e24b 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -55,6 +55,7 @@ california_VALASOURCES = \
        calendar/calendar-day-of-month.vala \
        calendar/calendar-day-of-week.vala \
        calendar/calendar-date.vala \
+       calendar/calendar-date-ordering.vala \
        calendar/calendar-dbus.vala \
        calendar/calendar-duration.vala \
        calendar/calendar-error.vala \
diff --git a/src/calendar/calendar-date-ordering.vala b/src/calendar/calendar-date-ordering.vala
new file mode 100644
index 0000000..08e05f7
--- /dev/null
+++ b/src/calendar/calendar-date-ordering.vala
@@ -0,0 +1,47 @@
+/* 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.Calendar {
+
+/**
+ * Represents a calendar date ordering, usually locale-dependent.
+ */
+
+public enum DateOrdering {
+    DMY,
+    MDY,
+    YMD,
+    YDM,
+    
+    /**
+     * Default date ordering (usually used when cannot be determined programmatically).
+     *
+     * The assumption here is that DMY is more common than any other (in terms of general usage).
+     */
+    DEFAULT = DMY;
+    
+    public string to_string() {
+        switch (this) {
+            case DMY:
+                return "DMY";
+            
+            case MDY:
+                return "MDY";
+            
+            case YMD:
+                return "YMD";
+            
+            case YDM:
+                return "YDM";
+            
+            default:
+                assert_not_reached();
+        }
+    }
+}
+
+}
+
diff --git a/src/calendar/calendar-span.vala b/src/calendar/calendar-span.vala
index 4df9f97..2f7eaeb 100644
--- a/src/calendar/calendar-span.vala
+++ b/src/calendar/calendar-span.vala
@@ -72,7 +72,7 @@ public abstract class Span : BaseObject {
     /**
      * Returns the { link Duration} this { link Span} represents.
      */
-    public Duration duration { owned get { return new Duration(end_date.difference(start_date).abs()); } }
+    public Duration duration { owned get { return new Duration(end_date.difference(start_date).abs() + 1); } 
}
     
     protected Span(Date start_date, Date end_date) {
         init_span(start_date, end_date);
diff --git a/src/calendar/calendar-system.vala b/src/calendar/calendar-system.vala
index b97545a..5cc6765 100644
--- a/src/calendar/calendar-system.vala
+++ b/src/calendar/calendar-system.vala
@@ -50,6 +50,30 @@ public class System : BaseObject {
     public static bool is_24hr { get; private set; }
     
     /**
+     * The user's locale's { link DateOrdering}.
+     *
+     * Date ordering may be set, but this is only for unit testing (hence there's no signal
+     * reporting its change).  The application shouldn't set this and let the value be determined
+     * at startup.
+     *
+     * @see date_separator
+     */
+    public static DateOrdering date_ordering { get; set; }
+    
+    /**
+     * The user's locale's date separator character.
+     *
+     * Generally this is expected to be a slash ("/"), a dot ("."), or a dash ("-').  Not all
+     * cultures use consistent separators (i.e. Chinese uses marks indicating year, day, and month).
+     * It's assumed this is merely a common (or common enough) character to be used when displaying
+     * or parsing dates.
+     *
+     * Like { link date_ordering}, this may be set for unit testing, but the application should
+     * let this be determined at startup.
+     */
+    public static string date_separator { get; set; }
+    
+    /**
      * Returns the system's configured zone as an { link OlsonZone}.
      */
     public static OlsonZone zone { get; private set; }
@@ -171,6 +195,56 @@ public class System : BaseObject {
         scheduled_date_timer = new Scheduled.once_after_sec(next_check_today_interval_sec(),
             check_today_changed, CHECK_DATE_PRIORITY);
         
+        // determine the date ordering and separator by using strftime's response
+        Calendar.Date unique_date;
+        try {
+            unique_date = new Calendar.Date(Calendar.DayOfMonth.for_checked(3),
+                Calendar.Month.for_checked(4), new Calendar.Year(2001));
+        } catch (Error err) {
+            error("Unable to generate test date 3/4/2001: %s", err.message);
+        }
+        
+        string formatted = unique_date.format("%x");
+        
+        int a, b, c;
+        char first_separator, second_separator;
+        if (formatted.scanf("%d%c%d%c%d", out a, out first_separator, out b, out second_separator, out c) == 
5) {
+            // convert four-digit year to two-digit
+            a = (a == 2001) ? 1 : a;
+            b = (b == 2001) ? 1 : b;
+            c = (c == 2001) ? 1 : c;
+            
+            if (a == 3 && b == 4 && c == 1)
+                date_ordering = DateOrdering.DMY;
+            else if (a == 4 && b == 3 && c == 1)
+                date_ordering = DateOrdering.MDY;
+            else if (a == 1 && b == 3 && c == 4)
+                date_ordering = DateOrdering.YDM;
+            else if (a == 1 && b == 4 && c == 3)
+                date_ordering = DateOrdering.YMD;
+            else
+                date_ordering = DateOrdering.DEFAULT;
+        } else {
+            // couldn't determine
+            date_ordering = DateOrdering.DEFAULT;
+        }
+        
+        // use first separator as date separator ... do some sanity checking here
+        switch (first_separator) {
+            case '/':
+            case '.':
+            case '-':
+                date_separator = first_separator.to_string();
+            break;
+            
+            default:
+                date_separator = "/";
+            break;
+        }
+        
+        debug("Date ordering: %s, separator: %s (formatted=%s)", date_ordering.to_string(),
+            date_separator.to_string(), formatted);
+        
         // Borrowed liberally (but not exactly) from GtkCalendar; see gtk_calendar_init
 #if HAVE__NL_TIME_FIRST_WEEKDAY
         // 1-based day (1 == Sunday)
diff --git a/src/component/component-details-parser.vala b/src/component/component-details-parser.vala
index 2cd9f46..6fa28f8 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 = from_string(token).filter(c => !c.ispunct()).to_string(c => c.to_string()) ?? "";
+            casefolded = from_string(token.casefold()).filter(c => !c.ispunct()).to_string(c => 
c.to_string()) ?? "";
         }
         
         public bool equal_to(Token other) {
@@ -325,6 +325,13 @@ public class DetailsParser : BaseObject {
             return add_date(saturday) && add_date(sunday);
         }
         
+        // look for fully numeric date specifier
+        {
+            Calendar.Date? date = parse_numeric_date(specifier);
+            if (date != null && add_date(date))
+                return true;
+        }
+        
         // look for day/month specifiers, in any order
         stack.mark();
         {
@@ -740,6 +747,100 @@ public class DetailsParser : BaseObject {
         return true;
     }
     
+    private Calendar.Date? parse_numeric_date(Token token) {
+        // look for three-number then two-number dates ... use original because casefolded has
+        // punctuation removed
+        int a, b, c;
+        char[] separator = new char[token.original.length];
+        if (token.original.scanf("%d%[/.-]%d%[/.-]%d", out a, separator, out b, separator, out c) == 5) {
+            // good to go
+        } else if (token.original.scanf("%d%[/.-]%d", out a, separator, out b) == 3) {
+            // -1 means two-number date was found, i.e. year must be determined manually
+            c = -1;
+        } else {
+            // nothing doing
+            return null;
+        }
+        
+        int d, m, y;
+        switch (Calendar.System.date_ordering) {
+            case Calendar.DateOrdering.DMY:
+                d = a;
+                m = b;
+                y = c;
+            break;
+            
+            case Calendar.DateOrdering.MDY:
+                d = b;
+                m = a;
+                y = c;
+            break;
+            
+            case Calendar.DateOrdering.YDM:
+                // watch out for two-number date
+                if (c != -1) {
+                    d = b;
+                    m = c;
+                    y = a;
+                } else {
+                    // DM
+                    d = a;
+                    m = b;
+                    y = -1;
+                }
+            break;
+            
+            case Calendar.DateOrdering.YMD:
+                // watch out for two-number date
+                if (c != -1) {
+                    d = c;
+                    m = b;
+                    y = a;
+                } else {
+                    // MD
+                    d = b;
+                    m = a;
+                    y = -1;
+                }
+            break;
+            
+            default:
+                assert_not_reached();
+        }
+        
+        // Determine year
+        Calendar.Year year;
+        if (c != -1) {
+            // two-digit numbers get adjusted to this century
+            // TODO: Y3K problem!
+            year = new Calendar.Year(y < 100 ? y + 2000 : y);
+        } else {
+            // if year not specified, assume the nearest date in the future
+            try {
+                Calendar.Date test = new Calendar.Date(Calendar.DayOfMonth.for(d),
+                    Calendar.Month.for(m), Calendar.System.today.year);
+                if (test.compare_to(Calendar.System.today) >= 0)
+                    year = test.year;
+                else
+                    year = test.year.adjust(1);
+            } catch (Error err) {
+                // bogus date, bail out
+                debug("Unable to parse date %s: %s", token.to_string(), err.message);
+                
+                return null;
+            }
+        }
+        
+        // build final date and return it
+        try {
+            return new Calendar.Date(Calendar.DayOfMonth.for(d), Calendar.Month.for(m), year);
+        } catch (Error err) {
+            debug("Unable to parse date %s: %s", token.to_string(), err.message);
+            
+            return null;
+        }
+    }
+    
     // Parses a potential date specifier into a calendar date relative to today
     private Calendar.Date? parse_relative_date(Token token) {
         // attempt to parse into common words for relative dates
diff --git a/src/tests/tests-quick-add.vala b/src/tests/tests-quick-add.vala
index a1c398d..193e62a 100644
--- a/src/tests/tests-quick-add.vala
+++ b/src/tests/tests-quick-add.vala
@@ -37,6 +37,15 @@ private class QuickAdd : UnitTest.Harness {
         add_case("end-date-ordinal", end_date_ordinal);
         add_case("simple-and", simple_and);
         add_case("this-weekend", this_weekend);
+        add_case("numeric-md", numeric_md);
+        add_case("numeric-dm", numeric_dm);
+        add_case("numeric-mdy", numeric_mdy);
+        add_case("numeric-dmy", numeric_dmy);
+        add_case("numeric-mdyyyy", numeric_mdyyyy);
+        add_case("numeric-dmyyyy", numeric_dmyyyy);
+        add_case("numeric-dot", numeric_dot);
+        add_case("numeric-dash", numeric_dash);
+        add_case("numeric-leading-zeros", numeric_leading_zeroes);
     }
     
     protected override void setup() throws Error {
@@ -374,6 +383,148 @@ private class QuickAdd : UnitTest.Harness {
             && parser.event.date_span.start_date.day_of_week == Calendar.DayOfWeek.SAT
             && parser.event.date_span.end_date.day_of_week == Calendar.DayOfWeek.SUN;
     }
+    
+    private bool numeric_md(out string? dump) throws Error {
+        Calendar.System.date_ordering = Calendar.DateOrdering.MDY;
+        Calendar.System.date_separator = "/";
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "7/2 Offsite", null);
+        
+        dump = parser.event.source;
+        
+        return parser.event.summary == "Offsite"
+            && parser.event.is_all_day
+            && parser.event.date_span.duration.days == 1
+            && parser.event.date_span.start_date.month == Calendar.Month.JUL
+            && parser.event.date_span.start_date.day_of_month.value == 2;
+    }
+    
+    private bool numeric_dm(out string? dump) throws Error {
+        Calendar.System.date_ordering = Calendar.DateOrdering.DMY;
+        Calendar.System.date_separator = "/";
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "2/7 Offsite", null);
+        
+        dump = parser.event.source;
+        
+        return parser.event.summary == "Offsite"
+            && parser.event.is_all_day
+            && parser.event.date_span.duration.days == 1
+            && parser.event.date_span.start_date.month == Calendar.Month.JUL
+            && parser.event.date_span.start_date.day_of_month.value == 2;
+    }
+    
+    private bool numeric_mdy(out string? dump) throws Error {
+        Calendar.System.date_ordering = Calendar.DateOrdering.MDY;
+        Calendar.System.date_separator = "/";
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "7/2/14 Offsite", null);
+        
+        dump = parser.event.source;
+        
+        return parser.event.summary == "Offsite"
+            && parser.event.is_all_day
+            && parser.event.date_span.duration.days == 1
+            && parser.event.date_span.start_date.month == Calendar.Month.JUL
+            && parser.event.date_span.start_date.day_of_month.value == 2
+            && parser.event.date_span.start_date.year.value == 2014;
+    }
+    
+    private bool numeric_dmy(out string? dump) throws Error {
+        Calendar.System.date_ordering = Calendar.DateOrdering.DMY;
+        Calendar.System.date_separator = "/";
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "2/7/14 Offsite", null);
+        
+        dump = parser.event.source;
+        
+        return parser.event.summary == "Offsite"
+            && parser.event.is_all_day
+            && parser.event.date_span.duration.days == 1
+            && parser.event.date_span.start_date.month == Calendar.Month.JUL
+            && parser.event.date_span.start_date.day_of_month.value == 2
+            && parser.event.date_span.start_date.year.value == 2014;
+    }
+    
+    private bool numeric_mdyyyy(out string? dump) throws Error {
+        Calendar.System.date_ordering = Calendar.DateOrdering.MDY;
+        Calendar.System.date_separator = "/";
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "7/2/2014 Offsite", null);
+        
+        dump = parser.event.source;
+        
+        return parser.event.summary == "Offsite"
+            && parser.event.is_all_day
+            && parser.event.date_span.duration.days == 1
+            && parser.event.date_span.start_date.month == Calendar.Month.JUL
+            && parser.event.date_span.start_date.day_of_month.value == 2
+            && parser.event.date_span.start_date.year.value == 2014;
+    }
+    
+    private bool numeric_dmyyyy(out string? dump) throws Error {
+        Calendar.System.date_ordering = Calendar.DateOrdering.DMY;
+        Calendar.System.date_separator = "/";
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "2/7/2014 Offsite", null);
+        
+        dump = parser.event.source;
+        
+        return parser.event.summary == "Offsite"
+            && parser.event.is_all_day
+            && parser.event.date_span.duration.days == 1
+            && parser.event.date_span.start_date.month == Calendar.Month.JUL
+            && parser.event.date_span.start_date.day_of_month.value == 2
+            && parser.event.date_span.start_date.year.value == 2014;
+    }
+    
+    private bool numeric_dot(out string? dump) throws Error {
+        Calendar.System.date_ordering = Calendar.DateOrdering.MDY;
+        Calendar.System.date_separator = ".";
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "7.2.14 Offsite", null);
+        
+        dump = parser.event.source;
+        
+        return parser.event.summary == "Offsite"
+            && parser.event.is_all_day
+            && parser.event.date_span.duration.days == 1
+            && parser.event.date_span.start_date.month == Calendar.Month.JUL
+            && parser.event.date_span.start_date.day_of_month.value == 2
+            && parser.event.date_span.start_date.year.value == 2014;
+    }
+    
+    private bool numeric_dash(out string? dump) throws Error {
+        Calendar.System.date_ordering = Calendar.DateOrdering.MDY;
+        Calendar.System.date_separator = "-";
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "7-2-14 Offsite", null);
+        
+        dump = parser.event.source;
+        
+        return parser.event.summary == "Offsite"
+            && parser.event.is_all_day
+            && parser.event.date_span.duration.days == 1
+            && parser.event.date_span.start_date.month == Calendar.Month.JUL
+            && parser.event.date_span.start_date.day_of_month.value == 2
+            && parser.event.date_span.start_date.year.value == 2014;
+    }
+    
+    private bool numeric_leading_zeroes(out string? dump) throws Error {
+        Calendar.System.date_ordering = Calendar.DateOrdering.MDY;
+        Calendar.System.date_separator = "/";
+        Component.DetailsParser parser = new Component.DetailsParser(
+            "07-02-14 Offsite", null);
+        
+        dump = parser.event.source;
+        
+        return parser.event.summary == "Offsite"
+            && parser.event.is_all_day
+            && parser.event.date_span.duration.days == 1
+            && parser.event.date_span.start_date.month == Calendar.Month.JUL
+            && parser.event.date_span.start_date.day_of_month.value == 2
+            && parser.event.date_span.start_date.year.value == 2014;
+    }
 }
 
 }


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