[california] Quick Add should allow numeric dates such as 7/2: Bug #732032
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california] Quick Add should allow numeric dates such as 7/2: Bug #732032
- Date: Fri, 15 Aug 2014 00:01:09 +0000 (UTC)
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]