[california/wip/725792-quick-add] Code simplifications and cleanup
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california/wip/725792-quick-add] Code simplifications and cleanup
- Date: Sat, 19 Apr 2014 02:45:22 +0000 (UTC)
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]