[california/wip/725785-create-recurring] Start of weekly recurrence parsing
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california/wip/725785-create-recurring] Start of weekly recurrence parsing
- Date: Fri, 20 Jun 2014 03:09:36 +0000 (UTC)
commit 57925d479531e178f7172cce2f574fad1a454e5a
Author: Jim Nelson <jim yorba org>
Date: Thu Jun 19 20:09:33 2014 -0700
Start of weekly recurrence parsing
src/collection/collection-iterable.vala | 45 ++++++++++--
src/component/component-details-parser.vala | 55 +++++++++-----
src/component/component-recurrence-rule.vala | 100 ++++++++++++++++----------
src/tests/tests-quick-add-recurring.vala | 33 +++++++++
4 files changed, 169 insertions(+), 64 deletions(-)
---
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index 61e9b32..1465a49 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -42,6 +42,20 @@ public California.Iterable<G> from_array<G>(G[] ar) {
}
/**
+ * Returns an { link Iterable} of Unicode characters for each in the supplied string.
+ */
+public Iterable<unichar> from_string(string str) {
+ Gee.ArrayList<unichar> list = new Gee.ArrayList<unichar>();
+
+ int index = 0;
+ unichar ch;
+ while (str.get_next_char(ref index, out ch))
+ list.add(ch);
+
+ return California.traverse<unichar>(list);
+}
+
+/**
* An Iterable that simply wraps an existing Iterator. You get one iteration,
* and only one iteration. Basically every method triggers one iteration and
* returns a new object.
@@ -51,12 +65,17 @@ public California.Iterable<G> from_array<G>(G[] ar) {
* works in foreach.
*/
-public class Iterable<G> : BaseObject {
+public class Iterable<G> : Object {
+ /**
+ * For { link to_string}.
+ */
+ public delegate string? ToString<G>(G element);
+
/**
* A private class that lets us take a California.Iterable and convert it back
* into a Gee.Iterable.
*/
- private class GeeIterable<G> : Gee.Traversable<G>, Gee.Iterable<G>, BaseObject {
+ private class GeeIterable<G> : Gee.Traversable<G>, Gee.Iterable<G>, Object {
private Gee.Iterator<G> i;
public GeeIterable(Gee.Iterator<G> iterator) {
@@ -75,10 +94,6 @@ public class Iterable<G> : BaseObject {
}
return true;
}
-
- public override string to_string() {
- return "GeeIterable";
- }
}
private Gee.Iterator<G> i;
@@ -216,8 +231,22 @@ public class Iterable<G> : BaseObject {
(owned) key_hash_func, (owned) key_equal_func, (owned) value_equal_func), key_func);
}
- public override string to_string() {
- return "Iterable";
+ /**
+ * Convert the { link Iterable} into a plain string.
+ *
+ * If { link ToString} returns null or an empty string, nothing is appended to the final string.
+ *
+ * If the final string is empty, null is returned instead.
+ */
+ public string? to_string(ToString<G> string_cb) {
+ StringBuilder builder = new StringBuilder();
+ foreach (G element in this) {
+ string? str = string_cb(element);
+ if (!String.is_empty(str))
+ builder.append(str);
+ }
+
+ return !String.is_empty(builder.str) ? builder.str : null;
}
}
diff --git a/src/component/component-details-parser.vala b/src/component/component-details-parser.vala
index d2356fc..4b892c3 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 = token.casefold();
+ casefolded = from_string(token).filter(c => !c.ispunct()).to_string(c => c.to_string());
}
public bool equal_to(Token other) {
@@ -179,6 +179,12 @@ public class DetailsParser : BaseObject {
continue;
}
+ // if a recurring rule has been started, attempt to parse into additions for the rule
+ stack.mark();
+ if (rrule != null && parse_recurring(token))
+ continue;
+ stack.restore();
+
// if this token and next describe a duration, use them
stack.mark();
if (parse_duration(token, stack.pop()))
@@ -471,15 +477,15 @@ public class DetailsParser : BaseObject {
return null;
}
+ // this can create a new RRULE or edit an existing one, but will not create multiple RRULEs
+ // for the same VEVENT
private bool parse_recurring(Token? specifier) {
+ if (specifier == null)
+ return false;
+
// take ownership in case specifier is an ordinal amount
Token? unit = specifier;
- // if a recurring rule has already been specified, another recurring cannot be made and
- // the current cannot be edited (yet)
- if (unit == null || rrule != null)
- return false;
-
// look for an amount modifying the specifier (creating an interval, i.e. "every 2 days"
// or "every 2nd day", hence parsing for ordinal)
int interval = parse_ordinal(unit);
@@ -494,10 +500,9 @@ public class DetailsParser : BaseObject {
// a day of the week
Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(unit.casefolded);
if (dow != null) {
- start_date = Calendar.System.today.upcoming(dow, true);
- rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE);
- rrule.interval = interval;
- rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
+ set_rrule_weekly(iterate<Calendar.DayOfWeek>(
+ Calendar.System.today.upcoming(dow, true).day_of_week).to_array_list().to_array(),
+ interval);
return true;
}
@@ -550,21 +555,33 @@ public class DetailsParser : BaseObject {
}
private void set_rrule_weekly(Calendar.DayOfWeek[]? by_days, int interval) {
- Gee.Map<Calendar.DayOfWeek, int> map = new Gee.HashMap<Calendar.DayOfWeek, int>();
+ Gee.Map<Calendar.DayOfWeek?, int> map = new Gee.HashMap<Calendar.DayOfWeek?, int>();
if (by_days != null) {
foreach (Calendar.DayOfWeek by_day in by_days)
map.set(by_day, 0);
}
- // start at the first day in the by_days
- start_date = Calendar.System.today;
- if (by_days != null)
- start_date = start_date.upcoming_in_set(from_array<Calendar.DayOfWeek>(by_days).to_hash_set(),
true);
+ if (rrule == null) {
+ rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE);
+ rrule.interval = interval;
+ rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
+ rrule.set_by_rule(RecurrenceRule.ByRule.DAY, RecurrenceRule.encode_days(map));
+ } else {
+ rrule.add_by_rule(RecurrenceRule.ByRule.DAY, RecurrenceRule.encode_days(map));
+ }
- rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE);
- rrule.interval = interval;
- rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
- rrule.set_by_rule(RecurrenceRule.ByRule.DAY, RecurrenceRule.encode_days(map));
+ // find the earliest date in the by_days; if it's earlier than the start_date or the
+ // start_date isn't defined, use the earliest
+ if (by_days != null) {
+ Calendar.Date earliest = Calendar.System.today.upcoming_in_set(
+ from_array<Calendar.DayOfWeek>(by_days).to_hash_set(), true);
+ if (start_date == null || earliest.compare_to(start_date) < 0)
+ start_date = earliest;
+ }
+
+ // no start_date at this point, then today is it
+ if (start_date == null)
+ start_date = Calendar.System.today;
}
private void set_rrule_yearly(Calendar.Date date, int interval) {
diff --git a/src/component/component-recurrence-rule.vala b/src/component/component-recurrence-rule.vala
index 6f7178a..1ee658a 100644
--- a/src/component/component-recurrence-rule.vala
+++ b/src/component/component-recurrence-rule.vala
@@ -50,7 +50,7 @@ public class RecurrenceRule : BaseObject {
/**
* Returns true if { link freq} is iCal.icalrecurrencetype_frequence.DAILY_RECURRENCE,
*/
- public bool is_weekly { get { return freq == iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE; } }
+ public bool is_weekly { get { return freq == iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE; } }
/**
* Returns true if { link freq} is iCal.icalrecurrencetype_frequence.MONTHLY_RECURRENCE,
@@ -307,11 +307,28 @@ public class RecurrenceRule : BaseObject {
}
/**
+ * Encode a { link Calendar.DayOfWeek} and its position (i.e. Second Thursday of the month,
+ * last Wednesday of the yar) into a value for { link set_by_rule} when using
+ * { link ByRule.DAY}.
+ *
+ * Use null for DayOfWeek and zero for position to mean "any" or "every".
+ *
+ * @see encode_days
+ */
+ public static int encode_day(Calendar.DayOfWeek? dow, int position) {
+ int dow_value = (dow != null) ? dow.ordinal(Calendar.System.first_of_week) : 0;
+
+ return (position.clamp(0, int.MAX) * 7) + dow_value;
+ }
+
+ /**
* Encode a Gee.Map of { link Calendar.DayOfWeek} and its position (i.e. Second Thursday of
* the month, last Wednesday of the year) into a value for { link set_by_rule} when using
* { link ByRule.DAY}.
*
* Use null for DayOfWeek and zero for position to mean "any" or "every".
+ *
+ * @encode_day
*/
public static Gee.Collection<int>? encode_days(Gee.Map<Calendar.DayOfWeek?, int>? day_values) {
if (day_values == null || day_values.size == 0)
@@ -319,68 +336,58 @@ public class RecurrenceRule : BaseObject {
Gee.Collection<int> encoded = new Gee.ArrayList<int>();
Gee.MapIterator<Calendar.DayOfWeek?, int> iter = day_values.map_iterator();
- while (iter.next()) {
- Calendar.DayOfWeek? dow = iter.get_key();
- int dow_value = (dow != null) ? dow.ordinal(Calendar.FirstOfWeek.SUNDAY) : 0;
- int position = iter.get_value().clamp(0, int.MAX);
-
- encoded.add((position * 7) + dow_value);
- }
+ while (iter.next())
+ encoded.add(encode_day(iter.get_key(), iter.get_value()));
return encoded;
}
- /**
- * Replaces the existing set of values for the BY rules with the supplied values.
- *
- * Pass null or an empty Collection to clear the by-rules values.
- *
- * Use { link encode_days} when passing values for { link ByRule.DAY}.
- *
- * @see by_rule_updated
- */
- public void set_by_rule(ByRule by_rule, Gee.Collection<int>? values) {
- Gee.SortedSet<int> by_set;
+ private Gee.SortedSet<int> get_by_set(ByRule by_rule) {
switch (by_rule) {
case ByRule.SECOND:
- by_set = by_second;
- break;
+ return by_second;
case ByRule.MINUTE:
- by_set = by_minute;
- break;
+ return by_minute;
case ByRule.HOUR:
- by_set = by_hour;
- break;
+ return by_hour;
case ByRule.DAY:
- by_set = by_day;
- break;
+ return by_day;
case ByRule.MONTH_DAY:
- by_set = by_month_day;
- break;
+ return by_month_day;
case ByRule.YEAR_DAY:
- by_set = by_year_day;
- break;
+ return by_year_day;
case ByRule.WEEK_NUM:
- by_set = by_week_num;
- break;
+ return by_week_num;
case ByRule.MONTH:
- by_set = by_month;
- break;
+ return by_month;
case ByRule.SET_POS:
- by_set = by_set_pos;
- break;
+ return by_set_pos;
default:
assert_not_reached();
}
+ }
+
+ /**
+ * Replaces the existing set of values for the BY rules with the supplied values.
+ *
+ * Pass null or an empty Collection to clear the by-rules values.
+ *
+ * Use { link encode_days} when passing values for { link ByRule.DAY}.
+ *
+ * @see add_by_rule
+ * @see by_rule_updated
+ */
+ public void set_by_rule(ByRule by_rule, Gee.Collection<int>? values) {
+ Gee.SortedSet<int> by_set = get_by_set(by_rule);
by_set.clear();
if (values != null && values.size > 0)
@@ -390,6 +397,25 @@ public class RecurrenceRule : BaseObject {
}
/**
+ * Adds the supplied values to the existing set of values for the BY rules.
+ *
+ * Null or an empty Collection is a no-op.
+ *
+ * Use { link encode_days} when passing values for { link ByRule.DAY}.
+ *
+ * @see set_by_rule
+ * @see by_rule_updated
+ */
+ public void add_by_rule(ByRule by_rule, Gee.Collection<int>? values) {
+ Gee.SortedSet<int> by_set = get_by_set(by_rule);
+
+ if (values != null && values.size > 0)
+ by_set.add_all(values);
+
+ by_rule_updated(by_rule);
+ }
+
+ /**
* Converts a { link RecurrenceRule} into an iCalendar RRULE property and adds it to the
* iCal component.
*
diff --git a/src/tests/tests-quick-add-recurring.vala b/src/tests/tests-quick-add-recurring.vala
index 6f6d591..c6ff4cd 100644
--- a/src/tests/tests-quick-add-recurring.vala
+++ b/src/tests/tests-quick-add-recurring.vala
@@ -15,6 +15,10 @@ private class QuickAddRecurring : UnitTest.Harness {
add_case("every-3rd-day", every_3rd_day);
add_case("every-2-days-for-10-days", every_2_days_for_10_days);
add_case("every-2-days-until", every_2_days_until);
+
+ // WEEKLY
+ add_case("every-tuesday", every_tuesday);
+ add_case("every-tuesday-thursday", every_tuesday_thursday);
}
protected override void setup() throws Error {
@@ -43,6 +47,10 @@ private class QuickAddRecurring : UnitTest.Harness {
&& event.exact_time_span.start_exact_time.to_wall_time().equal_to(new Calendar.WallTime(10, 0,
0));
}
+ //
+ // DAILY
+ //
+
private bool every_day(out string? dump) throws Error {
Component.Event event;
return basic("meeting at work every day at 10am", out event, out dump)
@@ -94,6 +102,31 @@ private class QuickAddRecurring : UnitTest.Harness {
&& event.rrule.until_date != null
&& event.rrule.until_date.equal_to(end);
}
+
+ //
+ // WEEKLY
+ //
+
+ private bool every_tuesday(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work at 10am every tuesday", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && !event.is_all_day
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.TUE);
+ }
+
+ private bool every_tuesday_thursday(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work at 10am every tuesday, thursday", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && !event.is_all_day
+ && (event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.TUE)
+ || event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.THU));
+ }
}
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]