[california/wip/725785-create-recurring] Daily recurring rules parser fleshed out w/ tests
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california/wip/725785-create-recurring] Daily recurring rules parser fleshed out w/ tests
- Date: Fri, 20 Jun 2014 01:08:13 +0000 (UTC)
commit 0bdda9869b53ade040e57c559430d752b27f3604
Author: Jim Nelson <jim yorba org>
Date: Thu Jun 19 18:08:01 2014 -0700
Daily recurring rules parser fleshed out w/ tests
src/Makefile.am | 1 +
src/calendar/calendar-day-of-month.vala | 3 +-
src/calendar/calendar-duration.vala | 35 ------
src/calendar/calendar.vala | 21 ----
src/component/component-date-time.vala | 2 +-
src/component/component-details-parser.vala | 167 +++++++++++++++++++++-----
src/component/component-instance.vala | 39 +-----
src/component/component-recurrence-rule.vala | 131 ++++++++++++++++----
src/component/component.vala | 91 +++++++++++++--
src/tests/tests-quick-add-recurring.vala | 100 +++++++++++++++
src/tests/tests-quick-add.vala | 29 +++++
src/tests/tests.vala | 1 +
src/unit-test/unit-test-harness.vala | 12 ++-
vapi/libical.vapi | 32 +++---
14 files changed, 487 insertions(+), 177 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 642af78..1de537a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -108,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-day-of-month.vala b/src/calendar/calendar-day-of-month.vala
index a46ecf2..5732405 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;
@@ -44,7 +45,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-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.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/component/component-date-time.vala b/src/component/component-date-time.vala
index 8b06127..d398f59 100644
--- a/src/component/component-date-time.vala
+++ b/src/component/component-date-time.vala
@@ -130,7 +130,7 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
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
+ // property."
if (dtstart.is_date != until_is_date)
throw new ComponentError.INVALID("RRULE UNTIL and DTSTART must be of same type
(DATE/DATE-TIME)");
diff --git a/src/component/component-details-parser.vala b/src/component/component-details-parser.vala
index 607bddd..d2356fc 100644
--- a/src/component/component-details-parser.vala
+++ b/src/component/component-details-parser.vala
@@ -199,6 +199,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) {
@@ -245,8 +248,20 @@ 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
@@ -265,8 +280,6 @@ public class DetailsParser : BaseObject {
event.description = details;
else
event.description += "\n" + details;
-
- debug("%s", event.ical_component.as_ical_string());
}
private bool parse_time(Token? specifier, bool strict) {
@@ -344,10 +357,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;
}
@@ -360,7 +415,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;
@@ -370,41 +425,103 @@ 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 ordinalis 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;
+ }
+
private bool parse_recurring(Token? specifier) {
+ // take ownership in case specifier is an ordinal amount
+ Token? unit = specifier;
+
// if a recurring rule has already been specified, another recurring cannot be made and
// the current cannot be edited (yet)
- if (specifier == null || rrule != null)
+ if (unit == null || rrule != null)
return false;
+ // look for an amount modifying the specifier (creating an interval, i.e. "every 2 days"
+ // or "every 2nd day", hence parsing for ordinal)
+ int interval = parse_ordinal(unit);
+ if (interval >= 1) {
+ unit = stack.pop();
+ if (unit == null)
+ return false;
+ } else {
+ interval = 1;
+ }
+
// a day of the week
- Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(specifier.casefolded);
+ Calendar.DayOfWeek? dow = Calendar.DayOfWeek.parse(unit.casefolded);
if (dow != null) {
start_date = Calendar.System.today.upcoming(dow, true);
rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE);
+ rrule.interval = interval;
rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
return true;
}
// "day"
- if (specifier.casefolded == DAY) {
+ if (unit.casefolded in UNIT_DAYS) {
start_date = Calendar.System.today;
rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE);
+ rrule.interval = interval;
rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
return true;
}
// "weekday"
- if (specifier.casefolded == WEEKDAY) {
- set_rrule_weekly(Calendar.DayOfWeek.weekdays);
+ if (unit.casefolded in UNIT_WEEKDAYS) {
+ set_rrule_weekly(Calendar.DayOfWeek.weekdays, interval);
return true;
}
// "weekend"
- if (specifier.casefolded == WEEKEND) {
- set_rrule_weekly(Calendar.DayOfWeek.weekend_days);
+ if (unit.casefolded in UNIT_WEEKENDS) {
+ set_rrule_weekly(Calendar.DayOfWeek.weekend_days, interval);
return true;
}
@@ -415,12 +532,12 @@ public class DetailsParser : BaseObject {
{
Token? second = stack.pop();
if (second != null) {
- Calendar.Date? date = parse_day_month(specifier, second);
+ Calendar.Date? date = parse_day_month(unit, second);
if (date == null)
- date = parse_day_month(second, specifier);
+ date = parse_day_month(second, unit);
if (date != null) {
- set_rrule_yearly(date);
+ set_rrule_yearly(date, interval);
return true;
}
@@ -432,7 +549,7 @@ public class DetailsParser : BaseObject {
return false;
}
- private void set_rrule_weekly(Calendar.DayOfWeek[]? by_days) {
+ private void set_rrule_weekly(Calendar.DayOfWeek[]? by_days, int interval) {
Gee.Map<Calendar.DayOfWeek, int> map = new Gee.HashMap<Calendar.DayOfWeek, int>();
if (by_days != null) {
foreach (Calendar.DayOfWeek by_day in by_days)
@@ -445,14 +562,16 @@ public class DetailsParser : BaseObject {
start_date = start_date.upcoming_in_set(from_array<Calendar.DayOfWeek>(by_days).to_hash_set(),
true);
rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE);
+ rrule.interval = interval;
rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
rrule.set_by_rule(RecurrenceRule.ByRule.DAY, RecurrenceRule.encode_days(map));
}
- private void set_rrule_yearly(Calendar.Date date) {
+ private void set_rrule_yearly(Calendar.Date date, int interval) {
start_date = date;
rrule = new RecurrenceRule(iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE);
+ rrule.interval = interval;
rrule.first_of_week = Calendar.System.first_of_week.as_day_of_week();
rrule.set_by_rule(RecurrenceRule.ByRule.YEAR_DAY, iterate<int>(date.day_of_year).to_array_list());
}
@@ -508,17 +627,8 @@ public class DetailsParser : BaseObject {
// 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);
@@ -529,8 +639,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;
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
index 12dd603..6f7178a 100644
--- a/src/component/component-recurrence-rule.vala
+++ b/src/component/component-recurrence-rule.vala
@@ -14,6 +14,12 @@ namespace California.Component {
*/
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.)
*/
@@ -37,36 +43,74 @@ public class RecurrenceRule : BaseObject {
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.DAILY_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}.
+ * 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_date_time
+ * @see set_until_exact_time
*/
- public DateTime? until { get; private set; default = null; }
+ 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}.
+ * 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 short _interval;
- public short interval {
+ private int _interval = 1;
+ public int interval {
get { return _interval; }
- set { _interval = value.clamp(0, short.MAX); }
- default = 0;
+ set { _interval = value.clamp(1, short.MAX); }
}
/**
@@ -93,7 +137,7 @@ public class RecurrenceRule : BaseObject {
this.freq = freq;
}
- internal RecurrenceRule.from_ical(iCal.icalcomponent ical_component) throws ComponentError {
+ 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);
@@ -106,12 +150,18 @@ public class RecurrenceRule : BaseObject {
iCal.icalrecurrencetype rrule = rrule_property.get_rrule();
freq = rrule.freq;
- if (rrule.count > 0)
- set_recurrence_count(rrule.count);
- else
- set_until_date_time(new DateTime.rrule_until(rrule, dtstart));
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;
@@ -169,27 +219,54 @@ public class RecurrenceRule : BaseObject {
}
/**
- * Sets the { link until} property.
+ * Sets the { link until_date} property.
*
- * Also sets { link count} to zero.
+ * Also sets { link count} to zero and nulls out { link until_exact_time}.
*
- * Passing null will clear both properties.
+ * Passing null will clear all these properties.
*/
- public void set_until_date_time(DateTime? date_time) {
- until = date_time;
+ 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}.
+ * Also clears { link until_date} and { link until_exact_time}.
*
- * Passing zero will clear both properties.
+ * Passing zero will clear all these properties.
*/
public void set_recurrence_count(int count) {
- this.count = count;
- until = null;
+ freeze_notify();
+
+ until_date = null;
+ until_exact_time = null;
+ this.count = count.clamp(0, int.MAX);
+
+ thaw_notify();
}
/**
@@ -321,15 +398,17 @@ public class RecurrenceRule : BaseObject {
*/
internal void add_to_ical(iCal.icalcomponent ical_component) {
iCal.icalrecurrencetype rrule = { 0 };
+
rrule.freq = freq;
- if (until != null)
- rrule.until = until.dt;
+ 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;
- if (interval > 0)
- rrule.interval = interval;
+ rrule.interval = (short) interval;
if (first_of_week == null)
rrule.week_start = iCal.icalrecurrencetype_weekday.NO_WEEKDAY;
diff --git a/src/component/component.vala b/src/component/component.vala
index c7be790..7bac566 100644
--- a/src/component/component.vala
+++ b/src/component/component.vala
@@ -20,9 +20,14 @@ private int init_count = 0;
private string TODAY;
private string TOMORROW;
private string YESTERDAY;
-private string DAY;
-private string WEEKDAY;
-private string WEEKEND;
+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[] TIME_PREPOSITIONS;
private string[] LOCATION_PREPOSITIONS;
private string[] DURATION_PREPOSITIONS;
@@ -50,19 +55,49 @@ 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 an event for every day
- // For more information see https://wiki.gnome.org/Apps/California/TranslatingQuickAdd
- DAY = _("day").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
- WEEKDAY = _("weekday").casefold();
+ 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
- WEEKEND = _("weekend").casefold();
+ UNIT_MINS = _("minute;minutes;min;mins").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.
@@ -124,11 +159,47 @@ public void terminate() {
TIME_PREPOSITIONS = LOCATION_PREPOSITIONS = DURATION_PREPOSITIONS = ORDINAL_SUFFIXES = null;
DELAY_PREPOSITIONS = RECURRING_PREPOSITIONS = null;
- TODAY = TOMORROW = YESTERDAY = DAY = WEEKDAY = WEEKEND = null;
+ TODAY = TOMORROW = YESTERDAY = 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-quick-add-recurring.vala b/src/tests/tests-quick-add-recurring.vala
new file mode 100644
index 0000000..6f6d591
--- /dev/null
+++ b/src/tests/tests-quick-add-recurring.vala
@@ -0,0 +1,100 @@
+/* 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 {
+
+private class QuickAddRecurring : UnitTest.Harness {
+ public QuickAddRecurring() {
+ // DAILY tests
+ add_case("every-day", every_day);
+ 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);
+ }
+
+ protected override void setup() throws Error {
+ Component.init();
+ Calendar.init();
+ }
+
+ protected override void teardown() {
+ Component.terminate();
+ Calendar.terminate();
+ }
+
+ // 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 = event.source;
+
+ return event.rrule != null
+ && event.summary == "meeting at work"
+ && event.location == "work"
+ && event.exact_time_span.start_exact_time.to_wall_time().equal_to(new Calendar.WallTime(10, 0,
0));
+ }
+
+ 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 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);
+ }
+}
+
+}
+
diff --git a/src/tests/tests-quick-add.vala b/src/tests/tests-quick-add.vala
index e24e58c..0972812 100644
--- a/src/tests/tests-quick-add.vala
+++ b/src/tests/tests-quick-add.vala
@@ -27,6 +27,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 {
@@ -261,6 +263,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.vala b/src/tests/tests.vala
index c85530b..0f88f76 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -9,6 +9,7 @@ namespace California.Tests {
public int run(string[] args) {
UnitTest.Harness.register(new String());
UnitTest.Harness.register(new QuickAdd());
+ UnitTest.Harness.register(new QuickAddRecurring());
UnitTest.Harness.register(new CalendarDate());
UnitTest.Harness.register(new CalendarMonthSpan());
UnitTest.Harness.register(new CalendarMonthOfYear());
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/vapi/libical.vapi b/vapi/libical.vapi
index 7bb9795..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")]
@@ -1728,7 +1728,7 @@ namespace iCal {
[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")]
@@ -1746,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);
}
@@ -2454,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")]
@@ -2512,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]