[california] Create recurring events with Quick Add: Bug #725785
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california] Create recurring events with Quick Add: Bug #725785
- Date: Wed, 25 Jun 2014 20:10:07 +0000 (UTC)
commit c390436733c59e2a14093d5ecb144b922589c20d
Author: Jim Nelson <jim yorba org>
Date: Wed Jun 25 13:08:32 2014 -0700
Create recurring events with Quick Add: Bug #725785
Quick Add parser now accepts keywords for creating recurring events.
This patch also introduces code for interpreting and generating
iCal recurring rules (RRULE) and associated tests.
src/Makefile.am | 2 +
src/calendar/calendar-date.vala | 40 +-
src/calendar/calendar-day-of-month.vala | 10 +-
src/calendar/calendar-day-of-week.vala | 42 ++-
src/calendar/calendar-duration.vala | 35 --
src/calendar/calendar-week.vala | 16 +
src/calendar/calendar.vala | 21 -
src/collection/collection-iterable.vala | 111 ++++-
src/component/component-date-time.vala | 40 ++
src/component/component-details-parser.vala | 360 ++++++++++++-
src/component/component-event.vala | 53 ++
src/component/component-instance.vala | 39 +--
src/component/component-recurrence-rule.vala | 543 +++++++++++++++++++
src/component/component.vala | 132 +++++-
src/tests/tests-calendar-date.vala | 110 ++++-
src/tests/tests-quick-add-recurring.vala | 715 ++++++++++++++++++++++++++
src/tests/tests-quick-add.vala | 56 ++-
src/tests/tests-string.vala | 18 +
src/tests/tests.vala | 7 +-
src/unit-test/unit-test-harness.vala | 12 +-
src/util/util-string.vala | 4 +-
vapi/libical.vapi | 64 +--
22 files changed, 2220 insertions(+), 210 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 5bed4ce..f4c5fd0 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -85,6 +85,7 @@ california_VALASOURCES = \
component/component-event.vala \
component/component-icalendar.vala \
component/component-instance.vala \
+ component/component-recurrence-rule.vala \
component/component-uid.vala \
component/component-vtype.vala \
\
@@ -107,6 +108,7 @@ california_VALASOURCES = \
tests/tests-calendar-month-span.vala \
tests/tests-calendar-wall-time.vala \
tests/tests-quick-add.vala \
+ tests/tests-quick-add-recurring.vala \
tests/tests-string.vala \
\
toolkit/toolkit.vala \
diff --git a/src/calendar/calendar-date.vala b/src/calendar/calendar-date.vala
index 3181742..9f0e71f 100644
--- a/src/calendar/calendar-date.vala
+++ b/src/calendar/calendar-date.vala
@@ -72,6 +72,7 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
public DayOfWeek day_of_week { get; private set; }
public DayOfMonth day_of_month { get; private set; }
+ public int day_of_year { get; private set; }
public Month month { get; private set; }
public Year year { get; private set; }
@@ -93,6 +94,7 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
day_of_week = DayOfWeek.from_gdate(gdate);
this.day_of_month = day_of_month;
+ day_of_year = (int) gdate.get_day_of_year();
this.month = month;
this.year = year;
}
@@ -112,6 +114,7 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
assert(gdate.valid());
day_of_week = DayOfWeek.from_gdate(gdate);
+ day_of_year = (int) gdate.get_day_of_year();
}
/**
@@ -130,6 +133,7 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
day_of_week = DayOfWeek.from_gdate(gdate);
day_of_month = DayOfMonth.from_gdate(gdate);
+ day_of_year = (int) gdate.get_day_of_year();
month = Month.from_gdate(gdate);
year = new Year.from_gdate(gdate);
}
@@ -252,41 +256,35 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
}
/**
- * Returns the { link Date} of the upcoming (next chronological) { link DayOfWeek}.
+ * Returns the { link Date} of the upcoming (next chronological) Date that matches
+ * the predicate's requirements.
*
- * Set { link includes_this_day} to true if this Date is to be considered "upcoming", that is,
- * if it falls on the day of the week, it is returned.
+ * inclusive indicates if this Date is included in the search.
*
* @see prior
*/
- public Date upcoming(DayOfWeek dow, bool includes_this_day) {
- return upcoming_prior(dow, includes_this_day, 1);
+ public Date upcoming(bool inclusive, Gee.Predicate<Calendar.Date> predicate) {
+ return upcoming_prior(inclusive, 1, predicate);
}
/**
- * Returns the { link Date} of the prior (previous chronological) { link DayOfWeek}.
+ * Returns the { link Date} of the prior (next chronological) Date that matches
+ * the predicate's requirements.
*
- * Set { link includes_this_day} to true if this Date is to be considered "prior", that is,
- * if it falls on the day of the week, it is returned.
+ * inclusive indicates if this Date is included in the search.
*
* @see upcoming
*/
- public Date prior(DayOfWeek dow, bool includes_this_day) {
- return upcoming_prior(dow, includes_this_day, -1);
+ public Date prior(bool inclusive, Gee.Predicate<Calendar.Date> predicate) {
+ return upcoming_prior(inclusive, -1, predicate);
}
- private Date upcoming_prior(DayOfWeek dow, bool includes_this_day, int adjustment) {
- // look for current date being the one
- if (day_of_week.equal_to(dow) && includes_this_day)
- return this;
+ private Date upcoming_prior(bool inclusive, int adjustment, Gee.Predicate<Calendar.Date> predicate) {
+ Calendar.Date current = inclusive ? this : adjust(adjustment);
+ while (!predicate(current))
+ current = current.adjust(adjustment);
- // find a Date for day of the week ... brute force isn't great, but it works
- Date upcoming_prior = this;
- for (;;) {
- upcoming_prior = upcoming_prior.adjust(adjustment);
- if (upcoming_prior.day_of_week.equal_to(dow))
- return upcoming_prior;
- }
+ return current;
}
/**
diff --git a/src/calendar/calendar-day-of-month.vala b/src/calendar/calendar-day-of-month.vala
index a46ecf2..d616c38 100644
--- a/src/calendar/calendar-day-of-month.vala
+++ b/src/calendar/calendar-day-of-month.vala
@@ -16,6 +16,7 @@ namespace California.Calendar {
public class DayOfMonth : BaseObject, Gee.Comparable<DayOfMonth>, Gee.Hashable<DayOfMonth> {
public const int MIN = 1;
public const int MAX = 31;
+ public const int COUNT = MAX - MIN + 1;
private static DayOfMonth[]? days = null;
@@ -25,6 +26,13 @@ public class DayOfMonth : BaseObject, Gee.Comparable<DayOfMonth>, Gee.Hashable<D
public int value { get; private set; }
/**
+ * Returns the 1-based week of the month this day resides in.
+ *
+ * For example, if this is the first Monday of the month, returns 1.
+ */
+ public int week_of_month { get { return ((value - 1) / DayOfWeek.COUNT) + 1; } }
+
+ /**
* Returns the day number as an informal (no leading zero) string.
*/
public string informal_number { get; private set; }
@@ -44,7 +52,7 @@ public class DayOfMonth : BaseObject, Gee.Comparable<DayOfMonth>, Gee.Hashable<D
}
internal static void init() {
- days = new DayOfMonth[MAX - MIN + 1];
+ days = new DayOfMonth[COUNT];
for (int ctr = MIN; ctr <= MAX; ctr++)
days[ctr - MIN] = new DayOfMonth(ctr);
}
diff --git a/src/calendar/calendar-day-of-week.vala b/src/calendar/calendar-day-of-week.vala
index 4c2024c..4fc8500 100644
--- a/src/calendar/calendar-day-of-week.vala
+++ b/src/calendar/calendar-day-of-week.vala
@@ -52,6 +52,9 @@ public class DayOfWeek : BaseObject, Gee.Hashable<DayOfWeek> {
public static DayOfWeek SAT;
public static DayOfWeek SUN;
+ public static DayOfWeek[] weekdays;
+ public static DayOfWeek[] weekend_days;
+
public const int MIN = 1;
public const int MAX = 7;
public const int COUNT = MAX - MIN + 1;
@@ -142,11 +145,23 @@ public class DayOfWeek : BaseObject, Gee.Hashable<DayOfWeek> {
days_of_week_sunday[4] = THU;
days_of_week_sunday[5] = FRI;
days_of_week_sunday[6] = SAT;
+
+ weekdays = new DayOfWeek[5];
+ weekdays[0] = MON;
+ weekdays[1] = TUE;
+ weekdays[2] = WED;
+ weekdays[3] = THU;
+ weekdays[4] = FRI;
+
+ weekend_days = new DayOfWeek[2];
+ weekend_days[0] = SAT;
+ weekend_days[1] = SUN;
}
internal static void terminate() {
days_of_week_monday = days_of_week_sunday = null;
MON = TUE = WED = THU = FRI = SAT = SUN = null;
+ weekdays = weekend_days = null;
}
/**
@@ -158,16 +173,7 @@ public class DayOfWeek : BaseObject, Gee.Hashable<DayOfWeek> {
if (index < 0 || index >= COUNT)
throw new CalendarError.INVALID("Invalid day of week value %d", value);
- switch (first_of_week) {
- case FirstOfWeek.MONDAY:
- return days_of_week_monday[index];
-
- case FirstOfWeek.SUNDAY:
- return days_of_week_sunday[index];
-
- default:
- assert_not_reached();
- }
+ return all(first_of_week)[index];
}
/**
@@ -182,6 +188,22 @@ public class DayOfWeek : BaseObject, Gee.Hashable<DayOfWeek> {
}
}
+ /**
+ * Return all { link DayOfWeeks} ordered by the { link FirstOfWeek}.
+ */
+ public static unowned DayOfWeek[] all(FirstOfWeek first_of_week) {
+ switch (first_of_week) {
+ case FirstOfWeek.MONDAY:
+ return days_of_week_monday;
+
+ case FirstOfWeek.SUNDAY:
+ return days_of_week_sunday;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
internal static DayOfWeek from_gdate(GLib.Date date) {
assert(date.valid());
diff --git a/src/calendar/calendar-duration.vala b/src/calendar/calendar-duration.vala
index 1fdc9fb..24d13ac 100644
--- a/src/calendar/calendar-duration.vala
+++ b/src/calendar/calendar-duration.vala
@@ -42,41 +42,6 @@ public class Duration : BaseObject {
+ seconds;
}
- /**
- * Parses the two tokens into a { link Duration}.
- *
- * parse() is looking for a pattern where the first token is a number and the second a string
- * of units of time (localized), either hours, minutes, or seconds. null is returned if that
- * pattern is not located.
- *
- * Future expansion could include a pattern where the first token has a unit as a suffix, i.e.
- * "3hrs" or "4m".
- *
- * It's possible for this call to return a Duration of zero time.
- */
- public static Duration? parse(string value, string unit) {
- if (String.is_empty(value) || String.is_empty(unit))
- return null;
-
- if (!String.is_numeric(value))
- return null;
-
- int duration = int.parse(value);
- if (duration < 0)
- return null;
-
- if (unit in UNIT_DAYS)
- return new Duration(duration);
-
- if (unit in UNIT_HOURS)
- return new Duration(0, duration);
-
- if (unit in UNIT_MINS)
- return new Duration(0, 0, duration);
-
- return null;
- }
-
public override string to_string() {
return "%ss".printf(seconds.to_string());
}
diff --git a/src/calendar/calendar-week.vala b/src/calendar/calendar-week.vala
index 2ff35a0..f33292c 100644
--- a/src/calendar/calendar-week.vala
+++ b/src/calendar/calendar-week.vala
@@ -19,6 +19,9 @@ namespace California.Calendar {
*/
public class Week : Unit<Week>, Gee.Comparable<Week>, Gee.Hashable<Week> {
+ public const int MIN_WEEK_OF_MONTH = 1;
+ public const int MAX_WEEK_OF_MONTH = 6;
+
/**
* The one-based week of the month (1 to 6).
*/
@@ -61,6 +64,19 @@ public class Week : Unit<Week>, Gee.Comparable<Week>, Gee.Hashable<Week> {
}
/**
+ * Returns the { link Date} for the { link DayOfWeek}.
+ */
+ public Date date_at(DayOfWeek dow) {
+ // although mixing FirstOfWeek is dangerous, don't trust simple math here because of this issue
+ foreach (Date date in to_date_span()) {
+ if (date.day_of_week.equal_to(dow))
+ return date;
+ }
+
+ assert_not_reached();
+ }
+
+ /**
* @inheritDoc
*/
public override Week adjust(int quantity) {
diff --git a/src/calendar/calendar.vala b/src/calendar/calendar.vala
index 96fa49a..f16735d 100644
--- a/src/calendar/calendar.vala
+++ b/src/calendar/calendar.vala
@@ -75,10 +75,6 @@ private unowned string FMT_24HOUR_MIN_SEC;
private unowned string MIDNIGHT;
private unowned string NOON;
-private string[] UNIT_DAYS;
-private string[] UNIT_HOURS;
-private string[] UNIT_MINS;
-
public void init() throws Error {
if (!California.Unit.do_init(ref init_count))
return;
@@ -207,21 +203,6 @@ public void init() throws Error {
// The 24-hour time with minutes and seconds, i.e. "17:06:31"
FMT_24HOUR_MIN_SEC = _("%02d:%02d:%02d");
- // Used by quick-add to convert a user's day unit into an internal value. Common abbreviations
- // (without punctuation) should be included. Each word must be separated by semi-colons.
- // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
- UNIT_DAYS = _("day;days;").casefold().split(";");
-
- // Used by quick-add to convert a user's hours unit into an internal value. Common abbreviations
- // (without punctuation) should be included. Each word must be separated by semi-colons.
- // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
- UNIT_HOURS = _("hour;hours;hr;hrs").casefold().split(";");
-
- // Used by quick-add to convert a user's minute unit into an internal value. Common abbreviations
- // (without punctuation) should be included. Each word must be separated by semi-colons.
- // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
- UNIT_MINS = _("minute;minutes;min;mins").casefold().split(";");
-
// return LC_MESSAGES back to proper locale and return LANGUAGE environment variable
if (messages_locale != null)
Intl.setlocale(LocaleCategory.MESSAGES, messages_locale);
@@ -262,8 +243,6 @@ public void terminate() {
DayOfWeek.terminate();
OlsonZone.terminate();
Collection.terminate();
-
- UNIT_DAYS = UNIT_HOURS = UNIT_MINS = null;
}
}
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index 2276d8e..b551cd2 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -30,6 +30,32 @@ public California.Iterable<G> iterate<G>(G g, ...) {
}
/**
+ * Take a non-null array of non-null items (all of type G) and return a California.Iterable
+ * for convenience.
+ */
+public California.Iterable<G> from_array<G>(G[] ar) {
+ Gee.ArrayList<G> list = new Gee.ArrayList<G>();
+ foreach (G item in ar)
+ list.add(item);
+
+ return California.traverse<G>(list);
+}
+
+/**
+ * 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.
@@ -39,12 +65,17 @@ public California.Iterable<G> iterate<G>(G g, ...) {
* 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) {
@@ -63,10 +94,6 @@ public class Iterable<G> : BaseObject {
}
return true;
}
-
- public override string to_string() {
- return "GeeIterable";
- }
}
private Gee.Iterator<G> i;
@@ -165,6 +192,17 @@ public class Iterable<G> : BaseObject {
return new GeeIterable<G>(i);
}
+ /**
+ * Convert the { link Iterable} into a flat array of elements.
+ */
+ public G[] to_array() {
+ G[] ar = new G[0];
+ while (i.next())
+ ar += i.get();
+
+ return ar;
+ }
+
public Gee.Collection<G> add_all_to(Gee.Collection<G> c) {
while (i.next())
c.add(i get());
@@ -188,7 +226,11 @@ public class Iterable<G> : BaseObject {
return (Gee.TreeSet<G>) add_all_to(new Gee.TreeSet<G>((owned) compare_func));
}
- public Gee.Map<K, G> add_all_to_map<K>(Gee.Map<K, G> c, Gee.MapFunc<K, G> key_func) {
+ /**
+ * Add this { link Iterable}'s values to an existing Gee.Map, with this Iterable's values as
+ * values for the map.
+ */
+ public Gee.Map<K, G> add_to_map_values<K>(Gee.Map<K, G> c, Gee.MapFunc<K, G> key_func) {
while (i.next()) {
G g = i get();
c set(key_func(g), g);
@@ -196,16 +238,63 @@ public class Iterable<G> : BaseObject {
return c;
}
- public Gee.HashMap<K, G> to_hash_map<K>(Gee.MapFunc<K, G> key_func,
+ /**
+ * Add this { link Iterable}'s values to an existing Gee.Map, with this Iterable's values as
+ * keys for the map.
+ *
+ * @see add_to_map_keys
+ */
+ public Gee.Map<G, V> add_to_map_keys<V>(Gee.Map<G, V> map, Gee.MapFunc<V, G> value_func) {
+ while (i.next()) {
+ G g = i.get();
+ map.set(g, value_func(g));
+ }
+
+ return map;
+ }
+
+ /**
+ * Transform the { link Iterable} into a Gee.HashMap, with this Iterable's values as values
+ * for the map.
+ *
+ * @see add_to_map_values
+ */
+ public Gee.HashMap<K, G> to_hash_map_as_values<K>(Gee.MapFunc<K, G> key_func,
owned Gee.HashDataFunc<K>? key_hash_func = null,
owned Gee.EqualDataFunc<K>? key_equal_func = null,
owned Gee.EqualDataFunc<G>? value_equal_func = null) {
- return (Gee.HashMap<K, G>) add_all_to_map<K>(new Gee.HashMap<K, G>(
+ return (Gee.HashMap<K, G>) add_to_map_values<K>(new Gee.HashMap<K, G>(
(owned) key_hash_func, (owned) key_equal_func, (owned) value_equal_func), key_func);
}
- public override string to_string() {
- return "Iterable";
+ /**
+ * Transform the { link Iterable} into a Gee.HashMap, with this Iterable's values as keys
+ * for the map.
+ */
+ public Gee.HashMap<G, V> to_hash_map_as_keys<V>(Gee.MapFunc<V, G> value_func,
+ owned Gee.HashDataFunc<G>? key_hash_func = null,
+ owned Gee.EqualDataFunc<G>? key_equal_func = null,
+ owned Gee.EqualDataFunc<V>? value_equal_func = null) {
+ return (Gee.HashMap<G, V>) add_to_map_keys<V>(new Gee.HashMap<G, V>(
+ (owned) key_hash_func, (owned) key_equal_func, (owned) value_equal_func), value_func);
+ }
+
+ /**
+ * Convert the { link Iterable}'s values into a single 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-date-time.vala b/src/component/component-date-time.vala
index 546e807..d398f59 100644
--- a/src/component/component-date-time.vala
+++ b/src/component/component-date-time.vala
@@ -117,6 +117,46 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
}
/**
+ * Creates a new { link DateTime} for a component's RRULE UNTIL property.
+ */
+ public DateTime.rrule_until(iCal.icalrecurrencetype rrule, DateTime dtstart) throws ComponentError {
+ if (iCal.icaltime_is_null_time(rrule.until) != 0)
+ throw new ComponentError.INVALID("DATE-TIME for RRULE UNTIL is null time");
+
+ if (iCal.icaltime_is_valid_time(rrule.until) != 0)
+ throw new ComponentError.INVALID("DATE-TIME for RRULE UNTIL is invalid");
+
+ bool until_is_date = (iCal.icaltime_is_date(rrule.until) != 0);
+ bool until_is_utc = (iCal.icaltime_is_utc(rrule.until) != 0);
+
+ // "The value of the UNTIL rule part MUST have the same value type as the 'DTSTART'
+ // property."
+ if (dtstart.is_date != until_is_date)
+ throw new ComponentError.INVALID("RRULE UNTIL and DTSTART must be of same type
(DATE/DATE-TIME)");
+
+ // "If the 'DTSTART' property is specified as a date with local time, then the UNTIL rule
+ // part MUST also be specified as a date with local time."
+ if (dtstart.is_utc != until_is_utc)
+ throw new ComponentError.INVALID("RRULE UNTIL and DTSTART must be of same time type
(UTC/local)");
+
+ // "if the 'DTSTART' property is specified as a date with UTC time or a date with local time
+ // and a time zone reference, then the UNTIL rule part MUST be specified as a date with
+ // UTC time."
+ if (dtstart.is_date || (!dtstart.is_utc && dtstart.zone != null)) {
+ if (!until_is_utc)
+ throw new ComponentError.INVALID("RRULE UNTIL must be UTC for DTSTART DATE or w/ time zone");
+ }
+
+ // "If specified as a DATE-TIME value, then it MUST be specified in a UTC time format."
+ if (!until_is_date && !until_is_utc)
+ throw new ComponentError.INVALID("RRULE DATE-TIME UNTIL must be UTC");
+
+ kind = iCal.icalproperty_kind.RRULE_PROPERTY;
+ dt = rrule.until;
+ zone = (!until_is_date || until_is_utc) ? Calendar.OlsonZone.utc : null;
+ }
+
+ /**
* Converts the stored iCal DATE-TIME to an { link Calendar.ExactTime}.
*
* Returns null if { link is_date} is true.
diff --git a/src/component/component-details-parser.vala b/src/component/component-details-parser.vala
index fc784f4..d7481eb 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) {
@@ -64,6 +64,7 @@ public class DetailsParser : BaseObject {
private Calendar.Date? end_date = null;
private Calendar.Duration? duration = null;
private bool adding_location = false;
+ private RecurrenceRule? rrule = null;
/**
* Parses a user-entered string of event details into an { link Event}.
@@ -158,6 +159,13 @@ public class DetailsParser : BaseObject {
continue;
stack.restore();
+ // A recurring preposition suggests a regular occurrance is being described by the next
+ // two tokens
+ stack.mark();
+ if (token.casefolded in RECURRING_PREPOSITIONS && parse_recurring(stack.pop()))
+ continue;
+ stack.restore();
+
// 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 (because location
@@ -171,6 +179,24 @@ public class DetailsParser : BaseObject {
continue;
}
+ // if a recurring rule has been started and are adding to it, drop common prepositions
+ // that indicate linkage
+ if (rrule != null && token.casefolded in COMMON_PREPOSITIONS)
+ continue;
+
+ // if a recurring rule has not been started, look for keywords which transform the
+ // event into one
+ stack.mark();
+ if (rrule == null && parse_recurring_indicator(token))
+ continue;
+ stack.restore();
+
+ // 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()))
@@ -191,6 +217,9 @@ public class DetailsParser : BaseObject {
// assemble accumulated information in an Event, using defaults wherever appropriate
//
+ // track if end_date is "artificially" generated to complete the Event
+ bool generated_end_date = (end_date == null);
+
// 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) {
@@ -237,10 +266,26 @@ public class DetailsParser : BaseObject {
new Calendar.ExactTime(Calendar.System.timezone, start_date, start_time),
new Calendar.ExactTime(Calendar.System.timezone, end_date, end_time)
));
+
+ // for parser, RRULE UNTIL is always DTEND's date unless a duration (i.e. a count, as
+ // the parser doesn't set UNTIL elsewhere) is specified; parser only deals in date-based
+ // recurrences, but don't add UNTIL if parser auto-generated DTEND, since that's us
+ // filling in "obvious" details about the whole of the event that may not necessarily
+ // apply to the recurrence rule
+ if (rrule != null && !rrule.has_duration && !generated_end_date)
+ rrule.set_recurrence_end_date(end_date);
} else if (start_date != null && end_date != null) {
event.set_event_date_span(new Calendar.DateSpan(start_date, end_date));
+
+ // see above note about RRULE UNTIL and DTEND
+ if (rrule != null && !rrule.has_duration && !generated_end_date)
+ rrule.set_recurrence_end_date(end_date);
}
+ // recurrence rule, if specified
+ if (rrule != null)
+ event.make_recurring(rrule);
+
// other event details
if (!String.is_empty(summary.str))
event.summary = summary.str;
@@ -324,10 +369,52 @@ public class DetailsParser : BaseObject {
if (amount == null || unit == null)
return false;
+ // if setting up a recurring rule, duration can be used as a count
+ if (rrule != null) {
+ // if duration already specified, not interested
+ if (rrule.has_duration)
+ return false;
+
+ // convert duration into unit appropriate to rrule ... note that only date-based
+ // rrules are allowed by parser
+ int count = -1;
+ switch (rrule.freq) {
+ case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
+ if (unit.casefolded in UNIT_DAYS)
+ count = parse_amount(amount);
+ break;
+
+ case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
+ if (unit.casefolded in UNIT_WEEKS)
+ count = parse_amount(amount);
+ break;
+
+ case iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE:
+ if (unit.casefolded in UNIT_MONTHS)
+ count = parse_amount(amount);
+ break;
+
+ case iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE:
+ if (unit.casefolded in UNIT_YEARS)
+ count = parse_amount(amount);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (count > 0) {
+ rrule.set_recurrence_count(count);
+
+ return true;
+ }
+ }
+
+ // otherwise, if an end time or duration is already known, then done here
if (end_time != null || duration != null)
return false;
- duration = Calendar.Duration.parse(amount.casefolded, unit.casefolded);
+ duration = parse_amount_of_time(amount, unit);
return duration != null;
}
@@ -340,7 +427,7 @@ public class DetailsParser : BaseObject {
if (start_time != null)
return false;
- Calendar.Duration? delay = Calendar.Duration.parse(amount.casefolded, unit.casefolded);
+ Calendar.Duration? delay = parse_amount_of_time(amount, unit);
if (delay == null)
return false;
@@ -350,6 +437,249 @@ public class DetailsParser : BaseObject {
return true;
}
+ // Returns negative value if amount is invalid
+ private int parse_amount(Token? amount) {
+ if (amount == null)
+ return -1;
+
+ return String.is_numeric(amount.casefolded) ? int.parse(amount.casefolded) : -1;
+ }
+
+ // Returns negative value if ordinal is invalid
+ private int parse_ordinal(Token? ordinal) {
+ if (ordinal == null)
+ return -1;
+
+ // strip ordinal suffix if present
+ string ordinal_number = ordinal.casefolded;
+ foreach (string suffix in ORDINAL_SUFFIXES) {
+ if (!String.is_empty(suffix) && ordinal_number.has_suffix(suffix)) {
+ ordinal_number = ordinal_number.slice(0, ordinal_number.length - suffix.length);
+
+ break;
+ }
+ }
+
+ return String.is_numeric(ordinal_number) ? int.parse(ordinal_number) : -1;
+ }
+
+ private Calendar.Duration? parse_amount_of_time(Token? amount, Token? unit) {
+ if (amount == null || unit == null)
+ return null;
+
+ int amt = parse_amount(amount);
+ if (amt < 0)
+ return null;
+
+ if (unit.casefolded in UNIT_DAYS)
+ return new Calendar.Duration(amt);
+
+ if (unit.casefolded in UNIT_HOURS)
+ return new Calendar.Duration(0, amt);
+
+ if (unit.casefolded in UNIT_MINS)
+ return new Calendar.Duration(0, 0, amt);
+
+ return null;
+ }
+
+ // this can create a new RRULE if the token indicates a one-time event should be recurring
+ private bool parse_recurring_indicator(Token? specifier) {
+ // rrule can't already exist
+ if (rrule != null || specifier == null)
+ return false;
+
+ if (specifier.casefolded == DAILY)
+ return set_rrule_daily(1);
+
+ if (specifier.casefolded == WEEKLY) {
+ if (start_date != null)
+ set_rrule_weekly(iterate<Calendar.DayOfWeek>(start_date.day_of_week).to_array(), 1);
+ else
+ set_rrule(iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE, 1);
+
+ return true;
+ }
+
+ if (specifier.casefolded == YEARLY) {
+ set_rrule(iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE, 1);
+
+ return true;
+ }
+
+ if (specifier.casefolded in UNIT_WEEKDAYS)
+ return set_rrule_weekly(Calendar.DayOfWeek.weekdays, 1);
+
+ if (specifier.casefolded in UNIT_WEEKENDS)
+ return set_rrule_weekly(Calendar.DayOfWeek.weekend_days, 1);
+
+ return false;
+ }
+
+ // 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;
+
+ // look for an amount modifying the specifier (creating an interval, i.e. "every 2 days"
+ // or "every 2nd day", hence parsing for ordinal)
+ bool is_ordinal = false;
+ int interval = parse_ordinal(unit);
+ if (interval >= 1) {
+ unit = stack.pop();
+ if (unit == null)
+ return false;
+
+ is_ordinal = true;
+ } else {
+ interval = 1;
+ }
+
+ // a day of the week
+ Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(unit.casefolded);
+ if (dow != null) {
+ Calendar.DayOfWeek[] by_days = iterate<Calendar.DayOfWeek>(dow).to_array();
+
+ // if interval is an ordinal, the rule is for "nth day of the month", so it's a position
+ // (i.e. "1st tuesday")
+ if (!is_ordinal)
+ return set_rrule_weekly(by_days, interval);
+ else
+ return set_rrule_nth_day_of_week(by_days, interval);
+ }
+
+ // "day"
+ if (unit.casefolded in UNIT_DAYS)
+ return set_rrule_daily(interval);
+
+ // "weekday"
+ if (unit.casefolded in UNIT_WEEKDAYS)
+ return set_rrule_weekly(Calendar.DayOfWeek.weekdays, interval);
+
+ // "weekend"
+ if (unit.casefolded in UNIT_WEEKENDS)
+ return set_rrule_weekly(Calendar.DayOfWeek.weekend_days, interval);
+
+ //parse for date, and if so, treat as yearly event
+ stack.mark();
+ {
+ if (unit == specifier)
+ unit = stack.pop();
+
+ if (unit != null) {
+ Calendar.Date? date = parse_day_month(specifier, unit);
+ if (date == null)
+ date = parse_day_month(unit, specifier);
+
+ if (date != null)
+ return set_rrule_nth_day_of_year(date, 1);
+ }
+ }
+ stack.restore();
+
+ return false;
+ }
+
+ private void set_rrule(iCal.icalrecurrencetype_frequency freq, int interval) {
+ rrule = new RecurrenceRule(freq);
+ rrule.interval = interval;
+ rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
+ }
+
+ // Using the supplied by days, find the first upcoming start_date that matches one of them
+ // that is also the position (unless zero, which means "any")
+ private void set_byday_start_date(Calendar.DayOfWeek[]? by_days, int position) {
+ assert(position >= 0);
+
+ // 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) {
+ Gee.Set<Calendar.DayOfWeek> dows = from_array<Calendar.DayOfWeek>(by_days).to_hash_set();
+ Calendar.Date earliest = Calendar.System.today.upcoming(true, (date) => {
+ if (position != 0 && date.day_of_month.week_of_month != position)
+ return false;
+
+ return dows.contains(date.day_of_week);
+ });
+ 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;
+ }
+
+ // "every day"
+ private bool set_rrule_daily(int interval) {
+ if (rrule != null)
+ return false;
+
+ // no start_date at this point, then today is it
+ if (start_date == null)
+ start_date = Calendar.System.today;
+
+ set_rrule(iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE, interval);
+
+ return true;
+ }
+
+ // "every tuesday"
+ private bool set_rrule_weekly(Calendar.DayOfWeek[]? by_days, int interval) {
+ if (rrule == null)
+ set_rrule(iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE, interval);
+ else if (!rrule.is_weekly)
+ return false;
+
+ Gee.Map<Calendar.DayOfWeek?, int> map = from_array<Calendar.DayOfWeek>(by_days)
+ .to_hash_map_as_keys<int>(dow => 0);
+ rrule.add_by_rule(RecurrenceRule.ByRule.DAY, RecurrenceRule.encode_days(map));
+
+ set_byday_start_date(by_days, 0);
+
+ return true;
+ }
+
+ // "every 1st tuesday"
+ private bool set_rrule_nth_day_of_week(Calendar.DayOfWeek[]? by_days, int position) {
+ // Although a month can span 6 calendar weeks, a day of a week never appears in more than
+ // five of them
+ if (position < 1 || position > 5)
+ return false;
+
+ if (rrule == null)
+ set_rrule(iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE, 1);
+ else if (!rrule.is_monthly)
+ return false;
+
+ Gee.Map<Calendar.DayOfWeek?, int> map = from_array<Calendar.DayOfWeek>(by_days)
+ .to_hash_map_as_keys<int>(dow => position);
+ rrule.add_by_rule(RecurrenceRule.ByRule.DAY, RecurrenceRule.encode_days(map));
+
+ set_byday_start_date(by_days, position);
+
+ return true;
+ }
+
+ // "every july 4th"
+ private bool set_rrule_nth_day_of_year(Calendar.Date date, int interval) {
+ if (rrule == null)
+ set_rrule(iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE, interval);
+ else if (!rrule.is_yearly)
+ return false;
+
+ if (start_date == null)
+ start_date = date;
+
+ rrule.add_by_rule(RecurrenceRule.ByRule.YEAR_DAY, iterate<int>(date.day_of_year).to_array_list());
+
+ return true;
+ }
+
// Adds the text to the summary and location field, if adding_location is set
private void add_text(Token token) {
// always add to summary
@@ -396,22 +726,15 @@ public class DetailsParser : BaseObject {
// attempt to parse into day of the week
Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(token.casefolded);
- return (dow != null) ? Calendar.System.today.upcoming(dow, true) : null;
+ return (dow != null)
+ ? Calendar.System.today.upcoming(true, date => date.day_of_week.equal_to(dow))
+ : 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))
+ int day_ordinal = parse_ordinal(day);
+ if (day_ordinal < 0)
return null;
Calendar.Month? month = Calendar.Month.parse(mon.casefolded);
@@ -422,8 +745,7 @@ public class DetailsParser : BaseObject {
year = Calendar.System.today.year;
try {
- return new Calendar.Date(Calendar.DayOfMonth.for(int.parse(day.casefolded)),
- month, year);
+ return new Calendar.Date(Calendar.DayOfMonth.for(day_ordinal), month, year);
} catch (CalendarError calerr) {
// probably an out-of-bounds day of month
return null;
@@ -448,8 +770,10 @@ public class DetailsParser : BaseObject {
private bool add_date(Calendar.Date date) {
if (start_date == null)
start_date = date;
- else if (end_date == null)
+ else if (end_date == null && rrule == null)
end_date = date;
+ else if (rrule != null && rrule.until_date == null)
+ rrule.set_recurrence_end_date(date);
else
return false;
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index 3d5960f..99ea3bf 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -20,6 +20,7 @@ public class Event : Instance, Gee.Comparable<Event> {
public const string PROP_IS_ALL_DAY = "is-all-day";
public const string PROP_LOCATION = "location";
public const string PROP_STATUS = "status";
+ public const string PROP_RRULE = "rrule";
public enum Status {
TENTATIVE,
@@ -86,6 +87,15 @@ public class Event : Instance, Gee.Comparable<Event> {
public Status status { get; set; default = Status.CONFIRMED; }
/**
+ * { link RecurrenceRule} (RRULE) for { link Event}.
+ *
+ * If the RecurrenceRule is itself altered, that signal is reflected to { link Instance.altered}.
+ *
+ * @see make_recurring
+ */
+ public RecurrenceRule? rrule { get; private set; default = null; }
+
+ /**
* Create an { link Event} { link Component} from an EDS CalComponent object.
*
* Throws a BackingError if the E.CalComponent's VTYPE is not VEVENT.
@@ -151,6 +161,12 @@ public class Event : Instance, Gee.Comparable<Event> {
status = Status.CONFIRMED;
break;
}
+
+ try {
+ make_recurring(new RecurrenceRule.from_ical(ical_component));
+ } catch (ComponentError comperr) {
+ // ignored; generally means no RRULE in component
+ }
}
private void on_notify(ParamSpec pspec) {
@@ -219,6 +235,15 @@ public class Event : Instance, Gee.Comparable<Event> {
}
break;
+ case PROP_RRULE:
+ // always remove existing RRULE
+ remove_all_properties(iCal.icalproperty_kind.RRULE_PROPERTY);
+
+ // add new one, if added
+ if (rrule != null)
+ rrule.add_to_ical(ical_component);
+ break;
+
default:
altered = false;
break;
@@ -325,6 +350,34 @@ public class Event : Instance, Gee.Comparable<Event> {
}
/**
+ * Add a { link RecurrenceRule} to the { link Event}.
+ *
+ * Pass null to make Event non-recurring.
+ */
+ public void make_recurring(RecurrenceRule? rrule) {
+ if (this.rrule != null) {
+ this.rrule.notify.disconnect(on_rrule_updated);
+ this.rrule.by_rule_updated.disconnect(on_rrule_updated);
+ }
+
+ if (rrule != null) {
+ rrule.notify.connect(on_rrule_updated);
+ rrule.by_rule_updated.connect(on_rrule_updated);
+ }
+
+ this.rrule = rrule;
+ }
+
+ private void on_rrule_updated() {
+ // remove old property, replace with new one
+ remove_all_properties(iCal.icalproperty_kind.RRULE_PROPERTY);
+ rrule.add_to_ical(ical_component);
+
+ // count this as an alteration
+ notify_altered(false);
+ }
+
+ /**
* @inheritDoc
*/
public override bool is_valid() {
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 7ad5445..4444d9c 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -88,6 +88,11 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
public iCal.icalcomponent ical_component { get { return _ical_component; } }
/**
+ * Returns the iCal source for this { link Instance}.
+ */
+ public string source { get { return ical_component.as_ical_string(); } }
+
+ /**
* True if inside { link full_update}.
*
* Subclasses want to ignore updates to various properties (their own and { link Instance}'s)
@@ -300,22 +305,6 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
}
/**
- * Convenience method to convert a { link Calendar.Date} to an iCal DATE.
- */
- protected static void date_to_ical(Calendar.Date date, iCal.icaltimetype *ical_dt) {
- ical_dt->year = date.year.value;
- ical_dt->month = date.month.value;
- ical_dt->day = date.day_of_month.value;
- ical_dt->hour = 0;
- ical_dt->minute = 0;
- ical_dt->second = 0;
- ical_dt->is_utc = 0;
- ical_dt->is_date = 1;
- ical_dt->is_daylight = 0;
- ical_dt->zone = null;
- }
-
- /**
* Convenience method to convert a { link Calendar.DateSpan} to a pair of iCal DATEs.
*
* dtend_inclusive indicates whether the dt_end should be treated as inclusive or exclusive
@@ -330,24 +319,6 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
}
/**
- * Convenience method to convert a { link Calendar.ExactTime} to an iCal DATE-TIME.
- */
- protected static void exact_time_to_ical(Calendar.ExactTime exact_time, iCal.icaltimetype *ical_dt) {
- ical_dt->year = exact_time.year.value;
- ical_dt->month = exact_time.month.value;
- ical_dt->day = exact_time.day_of_month.value;
- ical_dt->hour = exact_time.hour;
- ical_dt->minute = exact_time.minute;
- ical_dt->second = exact_time.second;
- ical_dt->is_utc = exact_time.tz.is_utc ? 1 : 0;
- ical_dt->is_date = 0;
- ical_dt->is_daylight = exact_time.is_dst ? 1 : 0;
- ical_dt->zone = iCal.icaltimezone.get_builtin_timezone(exact_time.tz.zone.value);
- if (ical_dt->zone == null)
- message("Unable to get builtin iCal timezone for %s", exact_time.tz.zone.to_string());
- }
-
- /**
* Convenience method to convert a { link Calendar.ExactTimeSpan} to a pair of iCal DATE-TIMEs.
*/
protected static void exact_time_span_to_ical(Calendar.ExactTimeSpan exact_time_span,
diff --git a/src/component/component-recurrence-rule.vala b/src/component/component-recurrence-rule.vala
new file mode 100644
index 0000000..289a4b3
--- /dev/null
+++ b/src/component/component-recurrence-rule.vala
@@ -0,0 +1,543 @@
+/* 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.Component {
+
+/**
+ * A mutable convenience representation of an iCalendar recurrence rule (RRULE).
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.3.10]]
+ * and [[https://tools.ietf.org/html/rfc5545#section-3.8.5.3]]
+ */
+
+public class RecurrenceRule : BaseObject {
+ public const string PROP_FREQ = "freq";
+ public const string PROP_UNTIL = "until";
+ public const string PROP_COUNT = "count";
+ public const string PROP_INTERVAL = "interval";
+ public const string PROP_FIRST_OF_WEEK = "first-of-week";
+
+ /**
+ * Enumeration of various BY rules (BYSECOND, BYMINUTE, etc.)
+ */
+ public enum ByRule {
+ SECOND,
+ MINUTE,
+ HOUR,
+ DAY,
+ MONTH_DAY,
+ YEAR_DAY,
+ WEEK_NUM,
+ MONTH,
+ SET_POS
+ }
+
+ /**
+ * Frequency.
+ *
+ * This is the only required field in an RRULE.
+ */
+ public iCal.icalrecurrencetype_frequency freq { get; set; }
+
+ /**
+ * Returns true if { link freq} is iCal.icalrecurrencetype_frequence.DAILY_RECURRENCE,
+ */
+ public bool is_daily { get { return freq == iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE; } }
+
+ /**
+ * Returns true if { link freq} is iCal.icalrecurrencetype_frequence.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,
+ */
+ public bool is_monthly { get { return freq == iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE; } }
+
+ /**
+ * Returns true if { link freq} is iCal.icalrecurrencetype_frequence.YEARLY_RECURRENCE,
+ */
+ public bool is_yearly { get { return freq == iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE; } }
+
+ /**
+ * Until (end date), inclusive.
+ *
+ * This is mutually exclusive with { link count} and { link until_exact_time}.
+ *
+ * @see set_until_date
+ */
+ public Calendar.Date? until_date { get; private set; default = null; }
+
+ /**
+ * Until (end date/time).
+ *
+ * This is mutually exclusive with { link count} and { link until_date}.
+ *
+ * @see set_until_exact_time
+ */
+ public Calendar.ExactTime? until_exact_time { get; private set; default = null; }
+
+ /**
+ * Total number of recurrences.
+ *
+ * Zero indicates "not set", not zero recurrences.
+ *
+ * This is mutually exclusive with { link until_date} and { link until_exact_time}.
+ *
+ * @see set_recurrence_count
+ */
+ public int count { get; private set; default = 0; }
+
+ /**
+ * Returns true if the recurrence rule has a duration.
+ *
+ * @see until
+ * @see count
+ */
+ public bool has_duration { get { return until_date != null || until_exact_time != null || count > 0; } }
+
+ /**
+ * Interval between recurrences.
+ *
+ * A positive integer representing the interval (duration between) of each recurrence. The
+ * actual amount of time elapsed is determined by the { link frequency} property.
+ *
+ * interval may be any value from 1 to short.MAX.
+ */
+ private int _interval = 1;
+ public int interval {
+ get { return _interval; }
+ set { _interval = value.clamp(1, short.MAX); }
+ }
+
+ /**
+ * Start of work week (WKST).
+ */
+ public Calendar.DayOfWeek? first_of_week { get; set; default = null; }
+
+ private Gee.SortedSet<int> by_second = new Gee.TreeSet<int>();
+ private Gee.SortedSet<int> by_minute = new Gee.TreeSet<int>();
+ private Gee.SortedSet<int> by_hour = new Gee.TreeSet<int>();
+ private Gee.SortedSet<int> by_day = new Gee.TreeSet<int>();
+ private Gee.SortedSet<int> by_month_day = new Gee.TreeSet<int>();
+ private Gee.SortedSet<int> by_year_day = new Gee.TreeSet<int>();
+ private Gee.SortedSet<int> by_week_num = new Gee.TreeSet<int>();
+ private Gee.SortedSet<int> by_month = new Gee.TreeSet<int>();
+ private Gee.SortedSet<int> by_set_pos = new Gee.TreeSet<int>();
+
+ /**
+ * Fired when a BY rule is updated (BYSECOND, BYMINUTE, etc.)
+ */
+ public signal void by_rule_updated(ByRule by_rule);
+
+ public RecurrenceRule(iCal.icalrecurrencetype_frequency freq) {
+ this.freq = freq;
+ }
+
+ internal RecurrenceRule.from_ical(iCal.icalcomponent ical_component) throws Error {
+ // need DTSTART for timezone purposes
+ DateTime dtstart = new DateTime(ical_component, iCal.icalproperty_kind.DTSTART_PROPERTY);
+
+ // fetch the RRULE from the component
+ unowned iCal.icalproperty? rrule_property = ical_component.get_first_property(
+ iCal.icalproperty_kind.RRULE_PROPERTY);
+ if (rrule_property == null)
+ throw new ComponentError.UNAVAILABLE("No RRULE found in component");
+
+ iCal.icalrecurrencetype rrule = rrule_property.get_rrule();
+
+ freq = rrule.freq;
+ interval = rrule.interval;
+
+ if (rrule.count > 0) {
+ set_recurrence_count(rrule.count);
+ } else {
+ Component.DateTime date_time = new DateTime.rrule_until(rrule, dtstart);
+ if (date_time.is_date)
+ set_recurrence_end_date(date_time.to_date());
+ else
+ set_recurrence_end_exact_time(date_time.to_exact_time());
+ }
+
+ switch (rrule.week_start) {
+ case iCal.icalrecurrencetype_weekday.SUNDAY_WEEKDAY:
+ first_of_week = Calendar.DayOfWeek.SUN;
+ break;
+
+ case iCal.icalrecurrencetype_weekday.MONDAY_WEEKDAY:
+ first_of_week = Calendar.DayOfWeek.MON;
+ break;
+
+ case iCal.icalrecurrencetype_weekday.TUESDAY_WEEKDAY:
+ first_of_week = Calendar.DayOfWeek.TUE;
+ break;
+
+ case iCal.icalrecurrencetype_weekday.WEDNESDAY_WEEKDAY:
+ first_of_week = Calendar.DayOfWeek.WED;
+ break;
+
+ case iCal.icalrecurrencetype_weekday.THURSDAY_WEEKDAY:
+ first_of_week = Calendar.DayOfWeek.THU;
+ break;
+
+ case iCal.icalrecurrencetype_weekday.FRIDAY_WEEKDAY:
+ first_of_week = Calendar.DayOfWeek.FRI;
+ break;
+
+ case iCal.icalrecurrencetype_weekday.SATURDAY_WEEKDAY:
+ first_of_week = Calendar.DayOfWeek.SAT;
+ break;
+
+ case iCal.icalrecurrencetype_weekday.NO_WEEKDAY:
+ default:
+ first_of_week = null;
+ break;
+ }
+
+ fill_by(rrule.by_second, by_second);
+ fill_by(rrule.by_minute, by_minute);
+ fill_by(rrule.by_hour, by_hour);
+ fill_by(rrule.by_day, by_day);
+ fill_by(rrule.by_month_day, by_month_day);
+ fill_by(rrule.by_year_day, by_year_day);
+ fill_by(rrule.by_week_no, by_week_num);
+ fill_by(rrule.by_month, by_month);
+ fill_by(rrule.by_set_pos, by_set_pos);
+ }
+
+ private void fill_by(short[] ical_by_ar, Gee.SortedSet<int> by_set) {
+ for (int ctr = 0; ctr < ical_by_ar.length; ctr++) {
+ short by = ical_by_ar[ctr];
+ if (by == iCal.RECURRENCE_ARRAY_MAX)
+ break;
+
+ by_set.add(by);
+ }
+ }
+
+ /**
+ * Sets the { link until_date} property.
+ *
+ * Also sets { link count} to zero and nulls out { link until_exact_time}.
+ *
+ * Passing null will clear all these properties.
+ */
+ public void set_recurrence_end_date(Calendar.Date? date) {
+ freeze_notify();
+
+ until_date = date;
+ until_exact_time = null;
+ count = 0;
+
+ thaw_notify();
+ }
+
+ /**
+ * Sets the { link until_exact_time} property.
+ *
+ * Also sets { link count} to zero and nulls out { link until_date}.
+ *
+ * Passing null will clear all these properties.
+ */
+ public void set_recurrence_end_exact_time(Calendar.ExactTime? exact_time) {
+ freeze_notify();
+
+ until_date = null;
+ until_exact_time = exact_time;
+ count = 0;
+
+ thaw_notify();
+ }
+
+ /**
+ * Sets the { link count} property.
+ *
+ * Also clears { link until_date} and { link until_exact_time}.
+ *
+ * Passing zero will clear all these properties.
+ */
+ public void set_recurrence_count(int count) {
+ freeze_notify();
+
+ until_date = null;
+ until_exact_time = null;
+ this.count = count.clamp(0, int.MAX);
+
+ thaw_notify();
+ }
+
+ /**
+ * Encode a { 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}.
+ *
+ * For position, 1 = first, 2 = second, -1 = last, -2 = second to last, etc.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.3.10]] for information how these values
+ * operate according to this RRULE's { link freq}.
+ *
+ * Use null for DayOfWeek and zero for position to mean "any" or "every".
+ *
+ * @see encode_days
+ * @see decode_day
+ */
+ public static int encode_day(Calendar.DayOfWeek? dow, int position) {
+ // these encodings are mapped to iCal.icalrecurrencetype_weekday, which is SUNDAY-based
+ int dow_value = (dow != null) ? dow.ordinal(Calendar.FirstOfWeek.SUNDAY) : 0;
+
+ position = position.clamp(short.MIN, short.MAX);
+ int value = (position * 8) + (position >= 0 ? dow_value : 0 - dow_value);
+
+ return value;
+ }
+
+ /**
+ * Decode the integer returned by { link get_by_rule} when { link ByRule.DAY} passed in.
+ *
+ * If null is returned for DayOfWeek or zero for position, that indicates "any" or "every".
+ * See { link encode_day} for more information.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.3.10]] for information how these values
+ * operate according to this RRULE's { link freq}.
+ *
+ * Returns false if the supplied value is definitely not encoded correctly.
+ */
+ public static bool decode_day(int value, out Calendar.DayOfWeek? dow, out int position) {
+ position = iCal.icalrecurrencetype.day_position((short) value);
+
+ dow = null;
+ int dow_value = (int) iCal.icalrecurrencetype.day_day_of_week((short) value);
+ if (dow_value != 0) {
+ try {
+ // iCal.icalrecurrencetype_weekday is SUNDAY-based
+ dow = Calendar.DayOfWeek.for(dow_value, Calendar.FirstOfWeek.SUNDAY);
+ } catch (CalendarError calerr) {
+ debug("Unable to decode day of week value %d: %s", dow_value, calerr.message);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Encode a Gee.Map of { link Calendar.DayOfWeek} and its position 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_day
+ */
+ public static Gee.Collection<int> encode_days(Gee.Map<Calendar.DayOfWeek?, int>? day_values) {
+ if (day_values == null || day_values.size == 0)
+ return Gee.Collection.empty<int>();
+
+ Gee.Collection<int> encoded = new Gee.ArrayList<int>();
+ Gee.MapIterator<Calendar.DayOfWeek?, int> iter = day_values.map_iterator();
+ while (iter.next())
+ encoded.add(encode_day(iter.get_key(), iter.get_value()));
+
+ return encoded;
+ }
+
+ /**
+ * Decode a Gee.Collection of encoded { link ByRule.DAY} values into their positions and
+ * { link Calendar.DayOfWeek}.
+ *
+ * Invalid values are skipped.
+ *
+ * @see encode_day
+ * @see encode_days
+ * @see decode_day
+ */
+ public static Gee.Map<Calendar.DayOfWeek?, int> decode_days(Gee.Collection<int>? values) {
+ if (values == null || values.size == 0)
+ return Gee.Map.empty<Calendar.DayOfWeek?, int>();
+
+ Gee.Map<Calendar.DayOfWeek?, int> decoded = new Gee.HashMap<Calendar.DayOfWeek?, int>();
+ foreach (int value in values) {
+ Calendar.DayOfWeek? dow;
+ int position;
+ if (decode_day(value, out dow, out position))
+ decoded.set(dow, position);
+ }
+
+ return decoded;
+ }
+
+ private Gee.SortedSet<int> get_by_set(ByRule by_rule) {
+ switch (by_rule) {
+ case ByRule.SECOND:
+ return by_second;
+
+ case ByRule.MINUTE:
+ return by_minute;
+
+ case ByRule.HOUR:
+ return by_hour;
+
+ case ByRule.DAY:
+ return by_day;
+
+ case ByRule.MONTH_DAY:
+ return by_month_day;
+
+ case ByRule.YEAR_DAY:
+ return by_year_day;
+
+ case ByRule.WEEK_NUM:
+ return by_week_num;
+
+ case ByRule.MONTH:
+ return by_month;
+
+ case ByRule.SET_POS:
+ return by_set_pos;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ /**
+ * Returns a read-only sorted set of BY rule settings for the specified { link ByRule}.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.3.10]] for information how these values
+ * operate according to their associated ByRule and this RRULE's { link freq}.
+ */
+ public Gee.SortedSet<int> get_by_rule(ByRule by_rule) {
+ return get_by_set(by_rule).read_only_view;
+ }
+
+ private bool is_int_short(int value) {
+ return value >= short.MIN && value <= short.MAX;
+ }
+
+ /**
+ * Replaces the existing set of values for the BY rules with the supplied values.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.3.10]] for information how these values
+ * operate according to their associated ByRule and this RRULE's { link freq}.
+ *
+ * Pass null or an empty Collection to clear the by-rules values.
+ *
+ * Any value greater than short.MAX or less than short.MIN will be dropped.
+ *
+ * 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)
+ by_set.add_all(traverse<int>(values).filter(is_int_short).to_array_list());
+
+ by_rule_updated(by_rule);
+ }
+
+ /**
+ * Adds the supplied values to the existing set of values for the BY rules.
+ *
+ * See [[https://tools.ietf.org/html/rfc5545#section-3.3.10]] for information how these values
+ * operate according to their associated ByRule and this RRULE's { link freq}.
+ *
+ * Null or an empty Collection is a no-op.
+ *
+ * Any value greater than short.MAX or less than short.MIN will be dropped.
+ *
+ * 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(traverse<int>(values).filter(is_int_short).to_array_list());
+
+ by_rule_updated(by_rule);
+ }
+
+ /**
+ * Converts a { link RecurrenceRule} into an iCalendar RRULE property and adds it to the
+ * iCal component.
+ *
+ * This call makes no attempt to remove an existing RRULE property; that should be performed by
+ * the caller first.
+ */
+ internal void add_to_ical(iCal.icalcomponent ical_component) {
+ iCal.icalrecurrencetype rrule = { 0 };
+
+ rrule.freq = freq;
+
+ if (until_date != null)
+ date_to_ical(until_date, &rrule.until);
+ else if (until_exact_time != null)
+ exact_time_to_ical(until_exact_time, &rrule.until);
+ else if (count > 0)
+ rrule.count = count;
+
+ rrule.interval = (short) interval;
+
+ if (first_of_week == null)
+ rrule.week_start = iCal.icalrecurrencetype_weekday.NO_WEEKDAY;
+ else if (first_of_week == Calendar.DayOfWeek.SUN)
+ rrule.week_start = iCal.icalrecurrencetype_weekday.SUNDAY_WEEKDAY;
+ else if (first_of_week == Calendar.DayOfWeek.MON)
+ rrule.week_start = iCal.icalrecurrencetype_weekday.MONDAY_WEEKDAY;
+ else if (first_of_week == Calendar.DayOfWeek.TUE)
+ rrule.week_start = iCal.icalrecurrencetype_weekday.TUESDAY_WEEKDAY;
+ else if (first_of_week == Calendar.DayOfWeek.WED)
+ rrule.week_start = iCal.icalrecurrencetype_weekday.WEDNESDAY_WEEKDAY;
+ else if (first_of_week == Calendar.DayOfWeek.THU)
+ rrule.week_start = iCal.icalrecurrencetype_weekday.THURSDAY_WEEKDAY;
+ else if (first_of_week == Calendar.DayOfWeek.FRI)
+ rrule.week_start = iCal.icalrecurrencetype_weekday.FRIDAY_WEEKDAY;
+ else if (first_of_week == Calendar.DayOfWeek.SAT)
+ rrule.week_start = iCal.icalrecurrencetype_weekday.SATURDAY_WEEKDAY;
+ else
+ assert_not_reached();
+
+ fill_ical_by(by_second, &rrule.by_second[0], rrule.by_second.length);
+ fill_ical_by(by_minute, &rrule.by_minute[0], rrule.by_minute.length);
+ fill_ical_by(by_hour, &rrule.by_hour[0], rrule.by_hour.length);
+ fill_ical_by(by_day, &rrule.by_day[0], rrule.by_day.length);
+ fill_ical_by(by_month_day, &rrule.by_month_day[0], rrule.by_month_day.length);
+ fill_ical_by(by_year_day, &rrule.by_year_day[0], rrule.by_year_day.length);
+ fill_ical_by(by_week_num, &rrule.by_week_no[0], rrule.by_week_no.length);
+ fill_ical_by(by_month, &rrule.by_month[0], rrule.by_month.length);
+ fill_ical_by(by_set_pos, &rrule.by_set_pos[0], rrule.by_set_pos.length);
+
+ iCal.icalproperty rrule_property = new iCal.icalproperty(iCal.icalproperty_kind.RRULE_PROPERTY);
+ rrule_property.set_rrule(rrule);
+
+ ical_component.add_property(rrule_property);
+ }
+
+ private void fill_ical_by(Gee.SortedSet<int> by_set, short *ical_by_ar, int ar_length) {
+ int index = 0;
+ foreach (int by in by_set) {
+ ical_by_ar[index++] = (short) by;
+
+ // watch for overflow
+ if (index >= ar_length)
+ break;
+ }
+
+ if (index < ar_length)
+ ical_by_ar[index] = (short) iCal.RECURRENCE_ARRAY_MAX;
+ }
+
+ public override string to_string() {
+ return "RRULE %s".printf(freq.to_string());
+ }
+}
+
+}
+
diff --git a/src/component/component.vala b/src/component/component.vala
index 9c917c8..d4521d8 100644
--- a/src/component/component.vala
+++ b/src/component/component.vala
@@ -20,10 +20,23 @@ private int init_count = 0;
private string TODAY;
private string TOMORROW;
private string YESTERDAY;
+private string DAILY;
+private string WEEKLY;
+private string YEARLY;
+private string[] UNIT_WEEKDAYS;
+private string[] UNIT_WEEKENDS;
+private string[] UNIT_YEARS;
+private string[] UNIT_MONTHS;
+private string[] UNIT_WEEKS;
+private string[] UNIT_DAYS;
+private string[] UNIT_HOURS;
+private string[] UNIT_MINS;
+private string[] COMMON_PREPOSITIONS;
private string[] TIME_PREPOSITIONS;
private string[] LOCATION_PREPOSITIONS;
private string[] DURATION_PREPOSITIONS;
private string[] DELAY_PREPOSITIONS;
+private string[] RECURRING_PREPOSITIONS;
private string[] ORDINAL_SUFFIXES;
public void init() throws Error {
@@ -46,14 +59,78 @@ public void init() throws Error {
// For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
YESTERDAY = _("yesterday").casefold();
+ // Used by quick-add to indicate the user wants to create a daily recurring event
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ DAILY = _("daily").casefold();
+
+ // Used by quick-add to indicate the user wants to create a weekly recurring event
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ WEEKLY = _("weekly").casefold();
+
+ // Used by quick-add to indicate the user wants to create a yearly recurring event
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ YEARLY = _("yearly").casefold();
+
+ // Used by quick-add to indicate the user wants to create an event for every weekday
+ // (in most Western countries, this means Monday through Friday, i.e. the work week)
+ // Common abbreviations (without punctuation) should be included. Each word must be separated
+ // by semi-colons.
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ UNIT_WEEKDAYS = _("weekday;weekdays;").casefold().split(";");
+
+ // Used by quick-add to indicate the user wants to create an event for every weekend
+ // (in most Western countries, this means Saturday and Sunday, i.e. non-work days)
+ // Common abbreviations (without punctuation) should be included. Each word must be separated
+ // by semi-colons.
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ UNIT_WEEKENDS = _("weekend;weekends;").casefold().split(";");
+
+ // Used by quick-add to convert a user's years unit into an internal value. Common abbreviations
+ // (without punctuation) should be included. Each word must be separated by semi-colons.
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ UNIT_YEARS = _("year;years;yr;yrs;").casefold().split(";");
+
+ // Used by quick-add to convert a user's month unit into an internal value. Common abbreviations
+ // (without punctuation) should be included. Each word must be separated by semi-colons.
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ UNIT_MONTHS = _("month;months;mo;mos;").casefold().split(";");
+
+ // Used by quick-add to convert a user's week unit into an internal value. Common abbreviations
+ // (without punctuation) should be included. Each word must be separated by semi-colons.
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ UNIT_WEEKS = _("week;weeks;wk;weeks;").casefold().split(";");
+
+ // Used by quick-add to convert a user's day unit into an internal value. Common abbreviations
+ // (without punctuation) should be included. Each word must be separated by semi-colons.
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ UNIT_DAYS = _("day;days;").casefold().split(";");
+
+ // Used by quick-add to convert a user's hours unit into an internal value. Common abbreviations
+ // (without punctuation) should be included. Each word must be separated by semi-colons.
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ UNIT_HOURS = _("hour;hours;hr;hrs").casefold().split(";");
+
+ // Used by quick-add to convert a user's minute unit into an internal value. Common abbreviations
+ // (without punctuation) should be included. Each word must be separated by semi-colons.
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ UNIT_MINS = _("minute;minutes;min;mins").casefold().split(";");
+
+ // Used by quick-add to determine if the word is a COMMON preposition (indicating linkage or a
+ // connection). Each word must be separate by semi-colons.
+ // These words should not be duplicated in another other preposition list.
+ // This list can be empty but that will limit the parser or cause unexpected results.
+ // Examples: "wednesday and thursday", "monday or friday"
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ COMMON_PREPOSITIONS = _("and;or;").casefold().split(";");
+
// Used by quick-add to determine if the word is a TIME preposition (indicating a
// specific time of day, not a duration). Each word must be separated by semi-colons.
// 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"
+ // Examples: "at 9am", "from 10pm to 11:30pm", "on monday", "until June 3rd"
// For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
- TIME_PREPOSITIONS = _("at;from;to;on;").casefold().split(";");
+ TIME_PREPOSITIONS = _("at;from;to;on;until;").casefold().split(";");
// Used by quick-add to determine if the word is a DURATION preposition (indicating a
// a duration of time, not a specific time). Each word must be separated by semi-colons.
@@ -73,6 +150,15 @@ public void init() throws Error {
// For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
DELAY_PREPOSITIONS = _("in;").casefold().split(";");
+ // Used by quick-add to determine if the word is a RECURRING preposition (indicating a
+ // regular occurrance in time). Each word must be separated by semi-colons.
+ // 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: "every 3 days", "every Friday"
+ // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
+ RECURRING_PREPOSITIONS = _("every;").casefold().split(";");
+
// Used by quick-add to determine if the word is a LOCATION preposition (indicating a
// specific place). Each word must be separated by semi-colons.
// It's allowable for some or all of these words to be duplicated in
@@ -95,13 +181,49 @@ public void terminate() {
if (!Unit.do_terminate(ref init_count))
return;
- TIME_PREPOSITIONS = LOCATION_PREPOSITIONS = DURATION_PREPOSITIONS = ORDINAL_SUFFIXES =
- DELAY_PREPOSITIONS =null;
- TODAY = TOMORROW = YESTERDAY = null;
+ TIME_PREPOSITIONS = LOCATION_PREPOSITIONS = DURATION_PREPOSITIONS = ORDINAL_SUFFIXES = null;
+ COMMON_PREPOSITIONS = DELAY_PREPOSITIONS = RECURRING_PREPOSITIONS = null;
+ TODAY = TOMORROW = YESTERDAY = DAILY = WEEKLY = YEARLY = null;
+ UNIT_WEEKDAYS = UNIT_WEEKENDS = UNIT_YEARS = UNIT_MONTHS = UNIT_WEEKS = UNIT_DAYS = UNIT_HOURS
+ = UNIT_MINS = null;
Calendar.terminate();
Collection.terminate();
}
+/**
+ * Convenience method to convert a { link Calendar.Date} to an iCal DATE.
+ */
+private void date_to_ical(Calendar.Date date, iCal.icaltimetype *ical_dt) {
+ ical_dt->year = date.year.value;
+ ical_dt->month = date.month.value;
+ ical_dt->day = date.day_of_month.value;
+ ical_dt->hour = 0;
+ ical_dt->minute = 0;
+ ical_dt->second = 0;
+ ical_dt->is_utc = 0;
+ ical_dt->is_date = 1;
+ ical_dt->is_daylight = 0;
+ ical_dt->zone = null;
+}
+
+/**
+ * Convenience method to convert a { link Calendar.ExactTime} to an iCal DATE-TIME.
+ */
+private void exact_time_to_ical(Calendar.ExactTime exact_time, iCal.icaltimetype *ical_dt) {
+ ical_dt->year = exact_time.year.value;
+ ical_dt->month = exact_time.month.value;
+ ical_dt->day = exact_time.day_of_month.value;
+ ical_dt->hour = exact_time.hour;
+ ical_dt->minute = exact_time.minute;
+ ical_dt->second = exact_time.second;
+ ical_dt->is_utc = exact_time.tz.is_utc ? 1 : 0;
+ ical_dt->is_date = 0;
+ ical_dt->is_daylight = exact_time.is_dst ? 1 : 0;
+ ical_dt->zone = iCal.icaltimezone.get_builtin_timezone(exact_time.tz.zone.value);
+ if (ical_dt->zone == null)
+ message("Unable to get builtin iCal timezone for %s", exact_time.tz.zone.to_string());
+}
+
}
diff --git a/src/tests/tests-calendar-date.vala b/src/tests/tests-calendar-date.vala
index 0291696..0276c3e 100644
--- a/src/tests/tests-calendar-date.vala
+++ b/src/tests/tests-calendar-date.vala
@@ -14,10 +14,17 @@ private class CalendarDate : UnitTest.Harness {
add_case("clamp-neither", clamp_neither);
add_case("difference-pos", difference_pos);
add_case("difference-neg", difference_neg);
- add_case("upcoming", upcoming);
- add_case("prior", prior);
+ add_case("upcoming-inclusive", upcoming_inclusive);
+ add_case("upcoming-exclusive", upcoming_exclusive);
+ add_case("prior-inclusive", prior_inclusive);
+ add_case("prior-exclusive", prior_exclusive);
add_case("upcoming-today", upcoming_today);
add_case("upcoming-next-week", upcoming_next_week);
+ add_case("day-of-week-position-1", day_of_week_position_1);
+ add_case("day-of-week-position-2", day_of_week_position_2);
+ add_case("day-of-week-position-3", day_of_week_position_3);
+ add_case("day-of-week-position-4", day_of_week_position_4);
+ add_case("day-of-week-position-5", day_of_week_position_5);
}
protected override void setup() throws Error {
@@ -82,25 +89,70 @@ private class CalendarDate : UnitTest.Harness {
return today.difference(day_before_yesterday) == -2;
}
- private bool upcoming() throws Error {
+ private bool upcoming(bool inclusive, out string? dump) throws Error {
+ dump = null;
+
Calendar.Date today = Calendar.System.today;
- Calendar.Date upcoming_fri = today.upcoming(Calendar.DayOfWeek.FRI, false);
- int diff = today.difference(upcoming_fri);
- return diff > 0 && diff <= 7;
+ foreach (Calendar.DayOfWeek dow in Calendar.DayOfWeek.all(Calendar.FirstOfWeek.SUNDAY)) {
+ Calendar.Date upcoming = Calendar.System.today.upcoming(inclusive,
+ date => date.day_of_week.equal_to(dow));
+ int diff = today.difference(upcoming);
+
+ dump = "%s - %s = %d".printf(today.to_string(), upcoming.to_string(), diff);
+
+ if (!inclusive && diff == 0)
+ return false;
+
+ if (diff < 0 || diff > 7)
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool upcoming_inclusive(out string? dump) throws Error {
+ return upcoming(true, out dump);
}
- private bool prior() throws Error {
+ private bool upcoming_exclusive(out string? dump) throws Error {
+ return upcoming(false, out dump);
+ }
+
+ private bool prior(bool inclusive, out string? dump) throws Error {
+ dump = null;
+
Calendar.Date today = Calendar.System.today;
- Calendar.Date prior_tue = today.prior(Calendar.DayOfWeek.TUE, false);
- int diff = today.difference(prior_tue);
- return diff < 0 && diff >= -7;
+ foreach (Calendar.DayOfWeek dow in Calendar.DayOfWeek.all(Calendar.FirstOfWeek.SUNDAY)) {
+ Calendar.Date upcoming = Calendar.System.today.prior(inclusive,
+ date => date.day_of_week.equal_to(dow));
+ int diff = today.difference(upcoming);
+
+ dump = "%s - %s = %d".printf(today.to_string(), upcoming.to_string(), diff);
+
+ if (!inclusive && diff == 0)
+ return false;
+
+ if (diff > 0 || diff < -7)
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool prior_inclusive(out string? dump) throws Error {
+ return prior(false, out dump);
+ }
+
+ private bool prior_exclusive(out string? dump) throws Error {
+ return prior(false, out dump);
}
private bool upcoming_today() throws Error {
Calendar.Date today = Calendar.System.today;
- Calendar.Date another_today = today.upcoming(today.day_of_week, true);
+ Calendar.Date another_today = today.upcoming(true,
+ date => date.day_of_week.equal_to(today.day_of_week));
int diff = today.difference(another_today);
return diff == 0;
@@ -108,11 +160,45 @@ private class CalendarDate : UnitTest.Harness {
private bool upcoming_next_week() throws Error {
Calendar.Date today = Calendar.System.today;
- Calendar.Date next_week = today.upcoming(today.day_of_week, false);
+ Calendar.Date next_week = today.upcoming(false,
+ date => date.day_of_week.equal_to(today.day_of_week));
int diff = today.difference(next_week);
return diff == 7;
}
+
+ private bool test_dow_position(Calendar.Date date, int expected, out string? dump) throws Error {
+ int position = date.day_of_month.week_of_month;
+
+ dump = "%s position=%d, expected=%d".printf(date.to_string(), position, expected);
+
+ return position == expected;
+ }
+
+ private Calendar.Date jun2014(int dom) throws Error {
+ return new Calendar.Date(Calendar.DayOfMonth.for(dom), Calendar.Month.JUN,
+ new Calendar.Year(2014));
+ }
+
+ private bool day_of_week_position_1(out string? dump) throws Error {
+ return test_dow_position(jun2014(1), 1, out dump);
+ }
+
+ private bool day_of_week_position_2(out string? dump) throws Error {
+ return test_dow_position(jun2014(9), 2, out dump);
+ }
+
+ private bool day_of_week_position_3(out string? dump) throws Error {
+ return test_dow_position(jun2014(20), 3, out dump);
+ }
+
+ private bool day_of_week_position_4(out string? dump) throws Error {
+ return test_dow_position(jun2014(23), 4, out dump);
+ }
+
+ private bool day_of_week_position_5(out string? dump) throws Error {
+ return test_dow_position(jun2014(30), 5, out dump);
+ }
}
}
diff --git a/src/tests/tests-quick-add-recurring.vala b/src/tests/tests-quick-add-recurring.vala
new file mode 100644
index 0000000..2db35d4
--- /dev/null
+++ b/src/tests/tests-quick-add-recurring.vala
@@ -0,0 +1,715 @@
+/* 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.Tests {
+
+/**
+ * Note that some tests are repeated with different days of the week to avoid false positives when
+ * the current day of the week (at time of execution) matches quick-add details.
+ */
+
+private class QuickAddRecurring : UnitTest.Harness {
+ public QuickAddRecurring() {
+ // ByRule.DAY encoding/decoding tests
+ add_case("encode-decode-day-every-week", encode_decode_day_every_week);
+ add_case("encode-decode-day-every-month", encode_decode_day_every_month);
+ add_case("encode-decode-days-every-week", encode_decode_days_every_week);
+ add_case("encode-decode-days-every-month", encode_decode_days_every_month);
+ add_case("encode-decode-all", encode_decode_all);
+ add_case("encode-decode-all_negative", encode_decode_all_negative);
+
+ // DAILY tests
+ add_case("every-day", every_day);
+ add_case("all-day", all_day);
+ add_case("daily", daily);
+ add_case("every-day-10-days", every_day_10_days);
+ add_case("every-2-days", every_2_days);
+ 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-friday", every_friday);
+ add_case("every-saturday-until", every_saturday_until);
+ add_case("all-day-saturday-until", all_day_saturday_until);
+ add_case("weekly-meeting-monday", weekly_meeting_monday);
+ add_case("weekly-meeting-tuesday", weekly_meeting_tuesday);
+ add_case("tuesday_weekly", tuesday_weekly);
+ add_case("thursday-weekly", thursday_weekly);
+ add_case("weekdays_to_1pm", weekdays_to_1pm);
+ add_case("weekends", weekends);
+ add_case("every_weekend", every_weekend);
+ add_case("every-tuesday-thursday", every_tuesday_thursday);
+ add_case("every-tuesday-and-thursday", every_tuesday_and_thursday);
+ add_case("every-tuesday-and-thursday-for-3-weeks", every_tuesday_and_thursday_for_3_weeks);
+
+ // MONTHLY
+ add_case("every-first-tuesday", every_first_tuesday);
+ add_case("every-first-tuesday-for-3-weeks", every_first_tuesday_for_3_weeks);
+ add_case("every-second-sunday-until", every_second_sunday_until);
+ add_case("every-sixth-tuesday", every_sixth_tuesday);
+
+ // YEARLY
+ add_case("every-july-4th", every_july_4th);
+ add_case("every-july-15th", every_july_15th);
+ add_case("every-4th-july", every_4th_july);
+ add_case("every-15th-july", every_15th_july);
+ add_case("july-4th-yearly", july_4th_yearly);
+ add_case("july-15th-yearly", july_15th_yearly);
+ add_case("yearly-july-4th", yearly_july_4th);
+ add_case("yearly-july-15th", yearly_july_15th);
+ add_case("yearly-meeting-july-4th", yearly_meeting_july_4th);
+ add_case("yearly-meeting-july-15th", yearly_meeting_july_15th);
+ add_case("meeting-every-july-4th-15th", meeting_every_july_4th_15th);
+ add_case("every-july-4th-3-years", every_july_4th_3_years);
+ add_case("every-aug-1st-until", every_aug_1st_until);
+ }
+
+ protected override void setup() throws Error {
+ Component.init();
+ Calendar.init();
+ }
+
+ protected override void teardown() {
+ Component.terminate();
+ Calendar.terminate();
+ }
+
+ private bool encode_decode_day_every_week(out string? dump) throws Error {
+ int value = Component.RecurrenceRule.encode_day(Calendar.DayOfWeek.THU, 0);
+
+ dump = "THU 0 -> %d".printf(value);
+
+ Calendar.DayOfWeek? dow;
+ int position;
+ return Component.RecurrenceRule.decode_day(value, out dow, out position)
+ && dow != null
+ && dow.equal_to(Calendar.DayOfWeek.THU)
+ && position == 0;
+ }
+
+ private bool encode_decode_day_every_month(out string? dump) throws Error {
+ int value = Component.RecurrenceRule.encode_day(Calendar.DayOfWeek.MON, 3);
+
+ dump = "MON 3 -> %d".printf(value);
+
+ Calendar.DayOfWeek? dow;
+ int position;
+ return Component.RecurrenceRule.decode_day(value, out dow, out position)
+ && dow != null
+ && dow.equal_to(Calendar.DayOfWeek.MON)
+ && position == 3;
+ }
+
+ private bool encode_decode_days_every_week(out string? dump) throws Error {
+ Gee.Collection<int> values = Component.RecurrenceRule.encode_days(
+ iterate<Calendar.DayOfWeek?>(Calendar.DayOfWeek.TUE,
Calendar.DayOfWeek.THU).to_hash_map_as_keys<int>(dow => 0));
+ Gee.Map<Calendar.DayOfWeek?, int> dows = Component.RecurrenceRule.decode_days(values);
+
+ dump = "values.size=%d size=%d".printf(values.size, dows.size);
+
+ return dows.size == 2
+ && dows.contains(Calendar.DayOfWeek.TUE)
+ && dows.contains(Calendar.DayOfWeek.THU)
+ && dows[Calendar.DayOfWeek.TUE] == 0
+ && dows[Calendar.DayOfWeek.THU] == 0;
+ }
+
+ private bool encode_decode_days_every_month(out string? dump) throws Error {
+ int iter = 1;
+ Gee.Collection<int> values = Component.RecurrenceRule.encode_days(
+ iterate<Calendar.DayOfWeek?>(Calendar.DayOfWeek.MON,
Calendar.DayOfWeek.WED).to_hash_map_as_keys<int>(dow => iter++));
+ Gee.Map<Calendar.DayOfWeek?, int> dows = Component.RecurrenceRule.decode_days(values);
+
+ dump = "values.size=%d size=%d".printf(values.size, dows.size);
+
+ return dows.size == 2
+ && dows.contains(Calendar.DayOfWeek.MON)
+ && dows.contains(Calendar.DayOfWeek.WED)
+ && dows[Calendar.DayOfWeek.MON] == 1
+ && dows[Calendar.DayOfWeek.WED] == 2;
+ }
+
+ private bool encode_decode_all() throws Error {
+ Gee.Collection<Calendar.DayOfWeek> all =
+
from_array<Calendar.DayOfWeek?>(Calendar.DayOfWeek.all(Calendar.FirstOfWeek.SUNDAY)).to_array_list();
+
+ int iter = 0;
+ Gee.Collection<int> values = Component.RecurrenceRule.encode_days(
+ traverse<Calendar.DayOfWeek>(all).to_hash_map_as_keys<int>(dow => iter++));
+ Gee.Map<Calendar.DayOfWeek?, int> dows = Component.RecurrenceRule.decode_days(values);
+
+ return dows.size == 7
+ && dows.has_key(Calendar.DayOfWeek.SUN)
+ && dows[Calendar.DayOfWeek.SUN] == 0
+ && dows[Calendar.DayOfWeek.MON] == 1
+ && dows[Calendar.DayOfWeek.TUE] == 2
+ && dows[Calendar.DayOfWeek.WED] == 3
+ && dows[Calendar.DayOfWeek.THU] == 4
+ && dows[Calendar.DayOfWeek.FRI] == 5
+ && dows[Calendar.DayOfWeek.SAT] == 6;
+ }
+
+ private bool encode_decode_all_negative() throws Error {
+ Gee.Collection<Calendar.DayOfWeek> all =
+
from_array<Calendar.DayOfWeek?>(Calendar.DayOfWeek.all(Calendar.FirstOfWeek.SUNDAY)).to_array_list();
+
+ int iter = -1;
+ Gee.Collection<int> values = Component.RecurrenceRule.encode_days(
+ traverse<Calendar.DayOfWeek>(all).to_hash_map_as_keys<int>(dow => iter--));
+ Gee.Map<Calendar.DayOfWeek?, int> dows = Component.RecurrenceRule.decode_days(values);
+
+ return dows.size == 7
+ && dows.has_key(Calendar.DayOfWeek.SUN)
+ && dows[Calendar.DayOfWeek.SUN] == -1
+ && dows[Calendar.DayOfWeek.MON] == -2
+ && dows[Calendar.DayOfWeek.TUE] == -3
+ && dows[Calendar.DayOfWeek.WED] == -4
+ && dows[Calendar.DayOfWeek.THU] == -5
+ && dows[Calendar.DayOfWeek.FRI] == -6
+ && dows[Calendar.DayOfWeek.SAT] == -7;
+ }
+
+ // Checks that an RRULE was generated,
+ // the summary is meeting at work
+ // the location is work
+ // the start time is 10am
+ private bool basic(string details, out Component.Event event, out string? dump) {
+ Component.DetailsParser parser = new Component.DetailsParser(details, null);
+ event = parser.event;
+
+ dump = "%s\n%s".printf(details, event.source);
+
+ return event.rrule != null
+ && event.summary == "meeting at work"
+ && event.location == "work"
+ && !event.is_all_day
+ && event.exact_time_span.start_exact_time.to_wall_time().equal_to(new Calendar.WallTime(10, 0,
0));
+ }
+
+ // Checks that an RRULE was generated,
+ // the summary is meeting at work
+ // the location is work
+ // is all day
+ private bool multiday(string details, out Component.Event event, out string? dump) {
+ Component.DetailsParser parser = new Component.DetailsParser(details, null);
+ event = parser.event;
+
+ dump = "%s\n%s".printf(details, event.source);
+
+ return event.rrule != null
+ && event.summary == "meeting at work"
+ && event.location == "work"
+ && event.is_all_day;
+ }
+
+ //
+ // 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)
+ && event.rrule.is_daily
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration;
+ }
+
+ private bool all_day(out string? dump) throws Error {
+ Component.Event event;
+ return multiday("meeting at work every day", out event, out dump)
+ && event.rrule.is_daily
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration;
+ }
+
+ private bool daily(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work daily at 10am", out event, out dump)
+ && event.rrule.is_daily
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration;
+ }
+
+ private bool every_day_10_days(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work every day at 10am for 10 days", out event, out dump)
+ && event.rrule.is_daily
+ && event.rrule.interval == 1
+ && event.rrule.count == 10;
+ }
+
+ private bool every_2_days(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at 10am every 2 days at work", out event, out dump)
+ && event.rrule.is_daily
+ && event.rrule.interval == 2
+ && !event.rrule.has_duration;
+ }
+
+ private bool every_3rd_day(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at 10am every 3rd day at work", out event, out dump)
+ && event.rrule.is_daily
+ && event.rrule.interval == 3
+ && !event.rrule.has_duration;
+ }
+
+ private bool every_2_days_for_10_days(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work every 2 days for 10 days at 10am", out event, out dump)
+ && event.rrule.is_daily
+ && event.rrule.interval == 2
+ && event.rrule.count == 10;
+ }
+
+ private bool every_2_days_until(out string? dump) throws Error {
+ Calendar.Date end = new Calendar.Date(Calendar.DayOfMonth.for(31), Calendar.Month.DEC,
+ Calendar.System.today.year);
+
+ Component.Event event;
+ return basic("meeting at work at 10am every 2 days until December 31", out event, out dump)
+ && event.rrule.is_daily
+ && event.rrule.interval == 2
+ && event.rrule.until_date != null
+ && event.rrule.until_date.equal_to(end);
+ }
+
+ //
+ // WEEKLY
+ //
+
+ private bool check_byrule_day(Component.Event event, Gee.Map<Calendar.DayOfWeek?, int> by_days) {
+ Gee.SortedSet<int> values = event.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY);
+ if (values.size != by_days.size)
+ return false;
+
+ foreach (int value in values) {
+ Calendar.DayOfWeek? dow;
+ int position;
+ if (!Component.RecurrenceRule.decode_day(value, out dow, out position))
+ return false;
+
+ if (!by_days.has_key(dow) || by_days.get(dow) != position)
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool every_tuesday(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.TUE).to_hash_map_as_keys<int>(dow => 0);
+
+ 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.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.TUE)
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool every_friday(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.FRI).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("meeting at work at 10am every friday", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.FRI)
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool every_saturday_until(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.SAT).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("meeting at work at 10am every saturday until dec 31", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && event.rrule.until_date != null
+ && event.rrule.until_date.equal_to(new Calendar.Date(Calendar.DayOfMonth.for(31),
+ Calendar.Month.DEC, Calendar.System.today.year))
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.SAT)
+ && check_byrule_day(event, by_days)
+ && event.exact_time_span.end_date.equal_to(event.exact_time_span.start_date);
+ }
+
+ private bool all_day_saturday_until(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.SAT).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return multiday("meeting at work every saturday until dec 31", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && event.rrule.until_date != null
+ && event.rrule.until_date.equal_to(new Calendar.Date(Calendar.DayOfMonth.for(31),
+ Calendar.Month.DEC, Calendar.System.today.year))
+ && event.date_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.SAT)
+ && check_byrule_day(event, by_days)
+ && event.date_span.end_date.equal_to(event.date_span.start_date);
+ }
+
+ private bool weekly_meeting_monday(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.MON).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("weekly meeting at work monday at 10am", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.MON)
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool weekly_meeting_tuesday(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.TUE).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("weekly meeting at work tuesday at 10am", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.TUE)
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool tuesday_weekly(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.TUE).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("meeting at work tuesday at 10am weekly", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.TUE)
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool thursday_weekly(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.THU).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("meeting at work thursday at 10am weekly", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.THU)
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool weekdays_to_1pm(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = from_array<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.weekdays).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("meeting at work weekdays from 10am to 1pm", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && by_days.keys.contains(event.exact_time_span.start_date.day_of_week)
+ && event.exact_time_span.end_exact_time.to_wall_time().equal_to(new Calendar.WallTime(13, 0, 0))
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool weekends(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = from_array<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.weekend_days).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("meeting weekends at work at 10am", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && by_days.keys.contains(event.exact_time_span.start_date.day_of_week)
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool every_weekend(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = from_array<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.weekend_days).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("meeting at work every weekend at 10am", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && by_days.keys.contains(event.exact_time_span.start_date.day_of_week)
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool every_tuesday_thursday(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.TUE, Calendar.DayOfWeek.THU).to_hash_map_as_keys<int>(dow => 0);
+
+ 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.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))
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool every_tuesday_and_thursday(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.TUE, Calendar.DayOfWeek.THU).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("meeting at work at 10am every tuesday and thursday", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && (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))
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool every_tuesday_and_thursday_for_3_weeks(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.TUE, Calendar.DayOfWeek.THU).to_hash_map_as_keys<int>(dow => 0);
+
+ Component.Event event;
+ return basic("meeting at work at 10am every tuesday and thursday for 3 weeks", out event, out dump)
+ && event.rrule.is_weekly
+ && event.rrule.interval == 1
+ && (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))
+ && check_byrule_day(event, by_days)
+ && event.rrule.count == 3;
+ }
+
+ //
+ // MONTHLY
+ //
+
+ private bool every_first_tuesday(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.TUE).to_hash_map_as_keys<int>(dow => 1);
+
+ Component.Event event;
+ return basic("meeting at work at 10am every 1st tuesday", out event, out dump)
+ && event.rrule.is_monthly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.TUE)
+ && event.exact_time_span.start_date.day_of_month.value <= 7
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool every_first_tuesday_for_3_weeks(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.TUE).to_hash_map_as_keys<int>(dow => 1);
+
+ Component.Event event;
+ return basic("meeting at work at 10am every 1st tuesday for 3 months", out event, out dump)
+ && event.rrule.is_monthly
+ && event.rrule.interval == 1
+ && event.rrule.count == 3
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.TUE)
+ && event.exact_time_span.start_date.day_of_month.value <= 7
+ && check_byrule_day(event, by_days);
+ }
+
+ private bool every_second_sunday_until(out string? dump) throws Error {
+ Gee.Map<Calendar.DayOfWeek?, int> by_days = iterate<Calendar.DayOfWeek?>(
+ Calendar.DayOfWeek.SUN).to_hash_map_as_keys<int>(dow => 2);
+
+ Component.Event event;
+ return basic("meeting at work at 10am every 2nd sunday until august 1st", out event, out dump)
+ && event.rrule.is_monthly
+ && event.rrule.interval == 1
+ && event.rrule.until_date != null
+ && event.rrule.until_date.month == Calendar.Month.AUG
+ && event.rrule.until_date.day_of_month.value == 1
+ && event.rrule.until_date.year.compare_to(Calendar.System.today.year) >= 0
+ && event.exact_time_span.start_date.day_of_week.equal_to(Calendar.DayOfWeek.SUN)
+ && event.exact_time_span.start_date.day_of_month.value >= 7
+ && event.exact_time_span.start_date.day_of_month.value <= 14
+ && check_byrule_day(event, by_days);
+ }
+
+ // bad input
+ private bool every_sixth_tuesday(out string? dump) throws Error {
+ Component.DetailsParser parser = new Component.DetailsParser(
+ "meeting at work at 10am every 6th tuesday", null);
+ Component.Event event = parser.event;
+
+ dump = event.source;
+
+ return event.rrule == null
+ && event.summary == "meeting at work every 6th";
+ }
+
+ //
+ // YEARLY
+ //
+
+ private bool check_byrule_yearday(Component.Event event, Gee.Collection<int> by_yeardays) {
+ Gee.SortedSet<int> values = event.rrule.get_by_rule(Component.RecurrenceRule.ByRule.YEAR_DAY);
+ if (values.size != by_yeardays.size)
+ return false;
+
+ return traverse<int>(by_yeardays).all(yearday => values.contains(yearday));
+ }
+
+ private bool every_july_4th(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work at 10am every july 4th", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 4;
+ }
+
+ private bool every_july_15th(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work at 10am every july 15th", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 15;
+ }
+
+ private bool every_4th_july(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work at 10am every 4th july", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 4;
+ }
+
+ private bool every_15th_july(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work at 10am every 15th july", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 15;
+ }
+
+ private bool july_4th_yearly(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work july 4th 10am yearly", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 4;
+ }
+
+ private bool july_15th_yearly(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work july 15th 10am yearly", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 15;
+ }
+
+ private bool yearly_july_4th(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work yearly july 4th 10am", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 4;
+ }
+
+ private bool yearly_july_15th(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work yearly july 15th 10am", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 15;
+ }
+
+ private bool yearly_meeting_july_4th(out string? dump) throws Error {
+ Component.Event event;
+ return basic("yearly meeting at work july 4th 10am", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 4;
+ }
+
+ private bool yearly_meeting_july_15th(out string? dump) throws Error {
+ Component.Event event;
+ return basic("yearly meeting at work july 15th 10am", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 15;
+ }
+
+ private bool meeting_every_july_4th_15th(out string? dump) throws Error {
+ Calendar.Date july4 = new Calendar.Date(Calendar.DayOfMonth.for(4), Calendar.Month.JUL,
+ Calendar.System.today.year);
+ Calendar.Date july15 = new Calendar.Date(Calendar.DayOfMonth.for(15), Calendar.Month.JUL,
+ Calendar.System.today.year);
+
+ Component.Event event;
+ return basic("meeting every july 4th and july 15 10am at work", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && !event.rrule.has_duration
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && (event.exact_time_span.start_date.day_of_month.value == 15
+ || event.exact_time_span.start_date.day_of_month.value == 4)
+ && event.exact_time_span.start_date.equal_to(event.exact_time_span.end_date)
+ && check_byrule_yearday(event, iterate<Calendar.Date>(july4, july15).map<int>(d =>
d.day_of_year).to_array_list());
+ }
+
+ private bool every_july_4th_3_years(out string? dump) throws Error {
+ Component.Event event;
+ return basic("meeting at work at 10am every july 4th for 3 years", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && event.rrule.count == 3
+ && event.exact_time_span.start_date.month == Calendar.Month.JUL
+ && event.exact_time_span.start_date.day_of_month.value == 4;
+ }
+
+ private bool every_aug_1st_until(out string? dump) throws Error {
+ Component.Event event;
+ return multiday("meeting at work aug 15 yearly until sep 1", out event, out dump)
+ && event.rrule.is_yearly
+ && event.rrule.interval == 1
+ && event.rrule.until_date != null
+ && event.rrule.until_date.month == Calendar.Month.SEP
+ && event.rrule.until_date.day_of_month.value == 1
+ && event.rrule.until_date.year.compare_to(Calendar.System.today.year) >= 0
+ && event.date_span.start_date.month == Calendar.Month.AUG
+ && event.date_span.start_date.day_of_month.value == 15
+ && event.date_span.start_date.year.compare_to(Calendar.System.today.year) >= 0
+ && event.date_span.end_date.equal_to(event.date_span.start_date);
+ }
+}
+
+}
+
diff --git a/src/tests/tests-quick-add.vala b/src/tests/tests-quick-add.vala
index e24e58c..c01ca84 100644
--- a/src/tests/tests-quick-add.vala
+++ b/src/tests/tests-quick-add.vala
@@ -8,7 +8,10 @@ namespace California.Tests {
private class QuickAdd : UnitTest.Harness {
public QuickAdd() {
+ add_case("null", null_details);
+ add_case("blank", blank);
add_case("summary", summary);
+ add_case("summary-with-blanks", summary_with_blanks);
add_case("summary-location", summary_location);
add_case("with-12hr-time", with_12hr_time);
add_case("with-24hr-time", with_24hr_time);
@@ -27,6 +30,8 @@ private class QuickAdd : UnitTest.Harness {
add_case("midnight-to-one", midnight_to_one);
add_case("separate-am", separate_am);
add_case("separate-pm", separate_pm);
+ add_case("start-date-ordinal", start_date_ordinal);
+ add_case("end-date-ordinal", end_date_ordinal);
}
protected override void setup() throws Error {
@@ -39,6 +44,18 @@ private class QuickAdd : UnitTest.Harness {
Calendar.terminate();
}
+ private bool null_details() throws Error {
+ Component.DetailsParser parser = new Component.DetailsParser(null, null);
+
+ return !parser.event.is_valid();
+ }
+
+ private bool blank() throws Error {
+ Component.DetailsParser parser = new Component.DetailsParser(" ", null);
+
+ return !parser.event.is_valid();
+ }
+
private bool summary() throws Error {
Component.DetailsParser parser = new Component.DetailsParser("meet with Alice", null);
@@ -48,6 +65,15 @@ private class QuickAdd : UnitTest.Harness {
&& parser.event.date_span == null;
}
+ private bool summary_with_blanks() throws Error {
+ Component.DetailsParser parser = new Component.DetailsParser(" meet with Alice ", null);
+
+ return parser.event.summary == "meet with Alice"
+ && parser.event.location == null
+ && parser.event.exact_time_span == null
+ && parser.event.date_span == null;
+ }
+
private bool summary_location() throws Error {
Component.DetailsParser parser = new Component.DetailsParser("meet with Alice at Bob's", null);
@@ -177,7 +203,8 @@ private class QuickAdd : UnitTest.Harness {
Component.DetailsParser parser = new Component.DetailsParser(
"12:30pm Friday Lunch with Eric and Charles", null);
- Calendar.Date friday = Calendar.System.today.upcoming(Calendar.DayOfWeek.FRI, true);
+ Calendar.Date friday = Calendar.System.today.upcoming(true,
+ date => date.day_of_week.equal_to(Calendar.DayOfWeek.FRI));
Calendar.ExactTime start = new Calendar.ExactTime(Calendar.Timezone.local, friday,
new Calendar.WallTime(12, 30, 0));
@@ -261,6 +288,33 @@ private class QuickAdd : UnitTest.Harness {
return parser.event.summary == "Dinner"
&& parser.event.exact_time_span.start_exact_time.equal_to(start);
}
+
+ private bool start_date_ordinal() throws Error {
+ Component.DetailsParser parser = new Component.DetailsParser(
+ "Dinner May 1st", null);
+
+ Calendar.Date start = new Calendar.Date(Calendar.DayOfMonth.for(1), Calendar.Month.MAY,
+ Calendar.System.today.year);
+
+ return parser.event.summary == "Dinner"
+ && parser.event.date_span.start_date.equal_to(start);
+ }
+
+ private bool end_date_ordinal(out string? dump) throws Error {
+ Component.DetailsParser parser = new Component.DetailsParser(
+ "Off-site May 1st to May 2nd", null);
+
+ dump = parser.event.source;
+
+ Calendar.Date start = new Calendar.Date(Calendar.DayOfMonth.for(1), Calendar.Month.MAY,
+ Calendar.System.today.year);
+ Calendar.Date end = new Calendar.Date(Calendar.DayOfMonth.for(2), Calendar.Month.MAY,
+ Calendar.System.today.year);
+
+ return parser.event.summary == "Off-site"
+ && parser.event.date_span.start_date.equal_to(start)
+ && parser.event.date_span.end_date.equal_to(end);
+ }
}
}
diff --git a/src/tests/tests-string.vala b/src/tests/tests-string.vala
index 436cf81..e1bc457 100644
--- a/src/tests/tests-string.vala
+++ b/src/tests/tests-string.vala
@@ -11,6 +11,8 @@ private class String : UnitTest.Harness {
add_case("strip-zeroes-space", strip_zeroes_space);
add_case("strip-zeroes-slash", strip_zeroes_slash);
add_case("strip-zeroes-multiple", strip_zeroes_multiple);
+ add_case("reduce-whitespace", reduce_whitespace);
+ add_case("reduce-nonspace-whitespace", reduce_nonspace_whitespace);
}
protected override void setup() throws Error {
@@ -38,6 +40,22 @@ private class String : UnitTest.Harness {
return result == "1/2/3/4";
}
+
+ private bool test_reduce_whitespace(string instr, string expected, out string? dump) throws Error {
+ string result = California.String.reduce_whitespace(instr);
+
+ dump = "\"%s\" => \"%s\", expected \"%s\"".printf(instr, result, expected);
+
+ return result == expected;
+ }
+
+ private bool reduce_whitespace(out string? dump) throws Error {
+ return test_reduce_whitespace(" a b c ", "a b c", out dump);
+ }
+
+ private bool reduce_nonspace_whitespace(out string? dump) throws Error {
+ return test_reduce_whitespace("\t\ta\n\nb\r\rc\t\t", "a\nb\rc", out dump);
+ }
}
}
diff --git a/src/tests/tests.vala b/src/tests/tests.vala
index c85530b..b8dd74c 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -7,12 +7,17 @@
namespace California.Tests {
public int run(string[] args) {
+ // make warnings and criticals fatal to catch during tests
+ GLib.Log.set_always_fatal(
+ LogLevelFlags.LEVEL_WARNING | LogLevelFlags.LEVEL_ERROR | LogLevelFlags.LEVEL_CRITICAL);
+
UnitTest.Harness.register(new String());
- UnitTest.Harness.register(new QuickAdd());
UnitTest.Harness.register(new CalendarDate());
UnitTest.Harness.register(new CalendarMonthSpan());
UnitTest.Harness.register(new CalendarMonthOfYear());
UnitTest.Harness.register(new CalendarWallTime());
+ UnitTest.Harness.register(new QuickAdd());
+ UnitTest.Harness.register(new QuickAddRecurring());
return UnitTest.Harness.exec_all();
}
diff --git a/src/unit-test/unit-test-harness.vala b/src/unit-test/unit-test-harness.vala
index 249aae9..759801d 100644
--- a/src/unit-test/unit-test-harness.vala
+++ b/src/unit-test/unit-test-harness.vala
@@ -11,7 +11,7 @@ namespace California.UnitTest {
*/
public abstract class Harness : BaseObject {
- public delegate bool Case() throws Error;
+ public delegate bool Case(out string? dump = null) throws Error;
private class TestCase : BaseObject {
public string name;
@@ -113,17 +113,21 @@ public abstract class Harness : BaseObject {
}
bool success = false;
+ string? dump = null;
Error? err = null;
try {
- success = test_case.unit_test();
+ success = test_case.unit_test(out dump);
} catch (Error caught) {
err = caught;
}
if (err != null)
- stdout.printf("\nFailed: %s.%s\n\t%s\n", name, test_case.name, err.message);
+ stdout.printf("failed (thrown error):\n\t\"%s\"\n", err.message);
else if (!success)
- stdout.printf("\nFailed: %s.%s\n", name, test_case.name);
+ stdout.printf("failed (test):\n");
+
+ if ((err != null || !success) && !String.is_empty(dump))
+ stdout.printf("%s\n", dump);
if (err != null || !success)
Posix.exit(Posix.EXIT_FAILURE);
diff --git a/src/util/util-string.vala b/src/util/util-string.vala
index 05b9c33..b4ca45e 100644
--- a/src/util/util-string.vala
+++ b/src/util/util-string.vala
@@ -49,7 +49,9 @@ public string reduce_whitespace(string str) {
last_ch = ch;
}
- return builder.str;
+ // due to get_next_char()'s interface, don't know when char is last, so it's possible for trailing
+ // whitespace to exist
+ return builder.str.chomp();
}
/**
diff --git a/vapi/libical.vapi b/vapi/libical.vapi
index 837687e..2bf00a2 100644
--- a/vapi/libical.vapi
+++ b/vapi/libical.vapi
@@ -57,9 +57,9 @@ namespace iCal {
[CCode (cname = "icalcomponent_add_property")]
public void add_property (iCal.icalproperty property);
[CCode (cname = "icalcomponent_as_ical_string")]
- public string as_ical_string ();
+ public unowned string as_ical_string ();
[CCode (cname = "icalcomponent_as_ical_string_r")]
- public unowned string as_ical_string_r ();
+ public string as_ical_string_r ();
[CCode (cname = "icalcomponent_begin_component")]
public unowned iCal.icalcompiter begin_component (iCal.icalcomponent_kind kind);
[CCode (cname = "icalcomponent_check_restrictions")]
@@ -225,7 +225,7 @@ namespace iCal {
[CCode (cname = "icaldurationtype_as_ical_string")]
public unowned string as_ical_string ();
[CCode (cname = "icaldurationtype_as_ical_string_r")]
- public unowned string as_ical_string_r ();
+ public string as_ical_string_r ();
[CCode (cname = "icaldurationtype_as_int")]
public int as_int ();
[CCode (cname = "icaldurationtype_bad_duration")]
@@ -253,7 +253,7 @@ namespace iCal {
[CCode (cname = "icalparameter_as_ical_string")]
public unowned string as_ical_string ();
[CCode (cname = "icalparameter_as_ical_string_r")]
- public unowned string as_ical_string_r ();
+ public string as_ical_string_r ();
[CCode (cname = "icalparameter_new_charset", has_construct_function = false)]
public icalparameter.charset (string v);
[CCode (cname = "icalparameter_new_clone", has_construct_function = false)]
@@ -507,7 +507,7 @@ namespace iCal {
[CCode (cname = "icalproperty_as_ical_string")]
public unowned string as_ical_string ();
[CCode (cname = "icalproperty_as_ical_string_r")]
- public unowned string as_ical_string_r ();
+ public string as_ical_string_r ();
[CCode (cname = "icalproperty_new_attach", has_construct_function = false)]
public icalproperty.attach (iCal.icalattach v);
[CCode (cname = "icalproperty_new_attendee", has_construct_function = false)]
@@ -579,7 +579,7 @@ namespace iCal {
[CCode (cname = "icalproperty_enum_to_string")]
public static unowned string enum_to_string (int e);
[CCode (cname = "icalproperty_enum_to_string_r")]
- public static unowned string enum_to_string_r (int e);
+ public string enum_to_string_r (int e);
[CCode (cname = "icalproperty_new_exdate", has_construct_function = false)]
public icalproperty.exdate (iCal.icaltimetype v);
[CCode (cname = "icalproperty_new_expand", has_construct_function = false)]
@@ -703,7 +703,7 @@ namespace iCal {
[CCode (cname = "icalproperty_get_parameter_as_string")]
public unowned string get_parameter_as_string (string name);
[CCode (cname = "icalproperty_get_parameter_as_string_r")]
- public unowned string get_parameter_as_string_r (string name);
+ public string get_parameter_as_string_r (string name);
[CCode (cname = "icalproperty_get_percentcomplete")]
public int get_percentcomplete ();
[CCode (cname = "icalproperty_get_permission")]
@@ -715,7 +715,7 @@ namespace iCal {
[CCode (cname = "icalproperty_get_property_name")]
public unowned string get_property_name ();
[CCode (cname = "icalproperty_get_property_name_r")]
- public unowned string get_property_name_r ();
+ public string get_property_name_r ();
[CCode (cname = "icalproperty_get_query")]
public unowned string get_query ();
[CCode (cname = "icalproperty_get_queryid")]
@@ -783,7 +783,7 @@ namespace iCal {
[CCode (cname = "icalproperty_get_value_as_string")]
public unowned string get_value_as_string ();
[CCode (cname = "icalproperty_get_value_as_string_r")]
- public unowned string get_value_as_string_r ();
+ public string get_value_as_string_r ();
[CCode (cname = "icalproperty_get_version")]
public unowned string get_version ();
[CCode (cname = "icalproperty_get_x")]
@@ -1442,7 +1442,7 @@ namespace iCal {
[CCode (cname = "icalvalue_as_ical_string")]
public unowned global::string as_ical_string ();
[CCode (cname = "icalvalue_as_ical_string_r")]
- public unowned global::string as_ical_string_r ();
+ public global::string as_ical_string_r ();
[CCode (cname = "icalvalue_new_attach", has_construct_function = false)]
public icalvalue.attach (iCal.icalattach attach);
[CCode (cname = "icalvalue_new_binary", has_construct_function = false)]
@@ -1696,7 +1696,7 @@ namespace iCal {
[CCode (cname = "icalperiodtype_as_ical_string")]
public unowned string as_ical_string ();
[CCode (cname = "icalperiodtype_as_ical_string_r")]
- public unowned string as_ical_string_r ();
+ public string as_ical_string_r ();
[CCode (cname = "icalperiodtype_from_string")]
public static iCal.icalperiodtype from_string (string str);
[CCode (cname = "icalperiodtype_is_null_period")]
@@ -1706,35 +1706,29 @@ namespace iCal {
[CCode (cname = "icalperiodtype_null_period")]
public static iCal.icalperiodtype null_period ();
}
- [CCode (cheader_filename = "libical/ical.h")]
+ public const int RECURRENCE_ARRAY_MAX;
+ public const int RUCURRENCE_ARRAY_MAX_BYTE;
+ [CCode (cheader_filename = "libical/ical.h", cname="struct icalrecurrencetype")]
+ [SimpleType]
public struct icalrecurrencetype {
public iCal.icalrecurrencetype_frequency freq;
public iCal.icaltimetype until;
public int count;
public short interval;
public iCal.icalrecurrencetype_weekday week_start;
- [CCode (array_length = false)]
- public weak short[] by_second;
- [CCode (array_length = false)]
- public weak short[] by_minute;
- [CCode (array_length = false)]
- public weak short[] by_hour;
- [CCode (array_length = false)]
- public weak short[] by_day;
- [CCode (array_length = false)]
- public weak short[] by_month_day;
- [CCode (array_length = false)]
- public weak short[] by_year_day;
- [CCode (array_length = false)]
- public weak short[] by_week_no;
- [CCode (array_length = false)]
- public weak short[] by_month;
- [CCode (array_length = false)]
- public weak short[] by_set_pos;
+ public unowned short by_second[61];
+ public unowned short by_minute[61];
+ public unowned short by_hour[25];
+ public unowned short by_day[364];
+ public unowned short by_month_day[32];
+ public unowned short by_year_day[367];
+ public unowned short by_week_no[54];
+ public unowned short by_month[13];
+ public unowned short by_set_pos[367];
[CCode (cname = "icalrecurrencetype_as_string")]
public unowned string as_string ();
[CCode (cname = "icalrecurrencetype_as_string_r")]
- public unowned string as_string_r ();
+ public string as_string_r ();
[CCode (cname = "icalrecurrencetype_clear")]
public void clear ();
[CCode (cname = "icalrecurrencetype_day_day_of_week")]
@@ -1752,7 +1746,7 @@ namespace iCal {
[CCode (cname = "icalreqstattype_as_string")]
public unowned string as_string ();
[CCode (cname = "icalreqstattype_as_string_r")]
- public unowned string as_string_r ();
+ public string as_string_r ();
[CCode (cname = "icalreqstattype_from_string")]
public static iCal.icalreqstattype from_string (string str);
}
@@ -2460,7 +2454,7 @@ namespace iCal {
[CCode (cheader_filename = "libical/ical.h", cname = "icalenum_reqstat_code")]
public static unowned string icalenum_reqstat_code (iCal.icalrequeststatus stat);
[CCode (cheader_filename = "libical/ical.h", cname = "icalenum_reqstat_code_r")]
- public static unowned string icalenum_reqstat_code_r (iCal.icalrequeststatus stat);
+ public static string icalenum_reqstat_code_r (iCal.icalrequeststatus stat);
[CCode (cheader_filename = "libical/ical.h", cname = "icalenum_reqstat_desc")]
public static unowned string icalenum_reqstat_desc (iCal.icalrequeststatus stat);
[CCode (cheader_filename = "libical/ical.h", cname = "icalenum_reqstat_major")]
@@ -2518,11 +2512,11 @@ namespace iCal {
[CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_property_eval_string")]
public static unowned string icallangbind_property_eval_string (iCal.icalproperty prop, string sep);
[CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_property_eval_string_r")]
- public static unowned string icallangbind_property_eval_string_r (iCal.icalproperty prop, string sep);
+ public static string icallangbind_property_eval_string_r (iCal.icalproperty prop, string sep);
[CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_quote_as_ical")]
public static unowned string icallangbind_quote_as_ical (string str);
[CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_quote_as_ical_r")]
- public static unowned string icallangbind_quote_as_ical_r (string str);
+ public static string icallangbind_quote_as_ical_r (string str);
[CCode (cheader_filename = "libical/ical.h", cname = "icallangbind_string_to_open_flag")]
public static int icallangbind_string_to_open_flag (string str);
[CCode (cheader_filename = "libical/ical.h", cname = "icalmemory_add_tmp_buffer")]
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]