I really like the way Google Calendar can email a summary of the upcoming day’s events so have been trying to produce a similar process for iCal using AppleScript. The difficult part for me (who is pretty new to this) is getting the events, including recurring ones. I have been trying Nigel Garvey’s script, as follows:
(* The vanilla code in this script is AS 1.9.1 compatible. *)
(* Oversee the calculation of the next due recurrence date (if any) of an iCal
event, given its recurrence value, start date, and the date of the check. Return
the recurrence date or, if the event's expired, missing value. *)
on getNextRecurrence(RFC2445, startdate, today)
-- copy startDate to startDate
-- copy today to today
-- Or, as here, take the times of day out of the process.
set startdate to startdate - (startdate's time)
set today to today - (today's time)
set nextRecurrence to missing value -- In case we don't find anything.
if (startdate comes before today) then -- The start date's in the past.
if ((count RFC2445) > 0) then -- This is a recurring event.
considering case
set endDate to getEndDate(RFC2445) + (startdate's time)
if (endDate does not come before today) then -- Not yet expired.
set interval to getInterval(RFC2445)
set maxRecurrences to getMaxRecurrences(RFC2445)
set freq to getRule(RFC2445, "FREQ")
if (freq is "DAILY") then
set nextRecurrence to nextDaily(startdate, today, endDate, interval, maxRecurrences)
else if (freq is "WEEKLY") then
set nextRecurrence to nextWeekly(RFC2445, startdate, today, endDate, interval, maxRecurrences)
else if (freq is "MONTHLY") then
set nextRecurrence to nextMonthly(RFC2445, startdate, today, endDate, interval, maxRecurrences)
else if (freq is "YEARLY") then
set nextRecurrence to nextYearly(RFC2445, startdate, today, endDate, interval, maxRecurrences)
end if
end if
end considering
end if
else -- A start date in the present or future is any event's (recurring or not) "next recurrence".
set nextRecurrence to startdate
end if
return nextRecurrence
end getNextRecurrence
(* Get the next due recurrence date of a "DAILY" event. (Usually today.) *)
on nextDaily(startdate, today, endDate, interval, maxRecurrences)
set interval to interval * days
set nextRecurrence to today - (today - 1 - startdate) mod interval + interval - 1
if ((nextRecurrence - startdate) div interval + 1 > maxRecurrences) or (nextRecurrence comes after endDate) then set nextRecurrence to missing value
return nextRecurrence
end nextDaily
(* Get the next due recurrence date of a "WEEKLY" event. *)
on nextWeekly(RFC2445, startdate, today, endDate, interval, maxRecurrences)
set BYDAYspecified to (RFC2445 contains "BYDAY")
if (BYDAYspecified) then
-- Get the offsets (in seconds) of the specified week-start weekday from Sunday and the recurrence weekdays from that.
set {WKSToffset, weekdayOffsets} to getWeekdayInstanceStuff(RFC2445)
-- Work out the start date of the week that starts on the WKST weekday and contains the event's start date.
set weekStart to startdate - (startdate - ((«data isot313030302D30312D3035» as date) + (startdate's time) + WKSToffset)) mod weeks
else
-- Otherwise, regard the event's start date as the week start and use an offset of 0.
set weekStart to startdate
set weekdayOffsets to {0}
end if
set recurrencesPerWeek to (count weekdayOffsets)
set interval to interval * weeks
-- Run the recurrence sequence from the week of the start date, starting with the first weekday in the list.
-- Start counting after passing the start date, which has already been checked in getNextRecurrence().
-- Continue to the first recurrence on or after today, or until maxRecurrences is exceeded.
set nextRecurrence to startdate
set recurrenceNo to 1 -- Recurrence 1 is the start date.
set i to 0 -- Index into weekdayOffsets.
repeat while (nextRecurrence comes before today) and (recurrenceNo < maxRecurrences)
set i to i + 1
if (i > recurrencesPerWeek) then
set i to 1
set weekStart to weekStart + interval
end if
set nextRecurrence to weekStart + (item i of weekdayOffsets)
if (nextRecurrence comes after startdate) then set recurrenceNo to recurrenceNo + 1
end repeat
-- No next recurrence if maxRecurrences is exceeded or endDate passed.
if (nextRecurrence comes before today) or (nextRecurrence comes after endDate) then set nextRecurrence to missing value
return nextRecurrence
end nextWeekly
(* Get the next due recurrence date of a "MONTHLY" event. *)
on nextMonthly(RFC2445, startdate, today, endDate, interval, maxRecurrences)
set BYDAYspecified to (RFC2445 contains "BYDAY=")
if (BYDAYspecified) then
set {weekdayCode, weekdayInstanceNos} to getWeekdayInstanceStuff(RFC2445)
set recurrencesPerMonth to (count weekdayInstanceNos)
else
set dayNumbers to getTargetDays(RFC2445, startdate)
set recurrencesPerMonth to (count dayNumbers)
end if
-- Run the recurrence sequence from the month of the start date, starting with the first recurrence day or weekday in the list.
-- Start counting after passing the start date, which has already been checked in getNextRecurrence().
-- Continue to the first valid occurrence on or after today, or until maxRecurrences is exceeded.
copy startdate to nextRecurrence
set recurrenceNo to 1 -- Recurrence 1 is the start date.
set dayInMonth to true -- Changed to false when a recurrence day doesn't exist in the month.
set i to 0 -- Index into weekdayInstanceNos or dayNumbers.
repeat while ((nextRecurrence comes before today) or not (dayInMonth)) and (recurrenceNo < maxRecurrences)
set i to i + 1
if (i > recurrencesPerMonth) then
set i to 1
set nextRecurrence to addMonths(nextRecurrence, interval)
end if
if (BYDAYspecified) then
-- Get the date of the specified weekday in this month.
set nextRecurrence to getWeekdayDate(nextRecurrence, weekdayCode, item i of weekdayInstanceNos)
set dayInMonth to true
else
-- Get the date of the (next) specified numbered day in this month.
set nextRecurrence's day to item i of dayNumbers
-- If the day doesn't exist in the month, the day and month will now have changed.
set dayInMonth to (nextRecurrence's day is result)
if not (dayInMonth) then set nextRecurrence to nextRecurrence - weeks -- Correct the month.
end if
if (dayInMonth) and (nextRecurrence comes after startdate) then set recurrenceNo to recurrenceNo + 1
end repeat
-- No next recurrence if maxRecurrences is exceeded or endDate passed.
if (nextRecurrence comes before today) or (nextRecurrence comes after endDate) then set nextRecurrence to missing value
return nextRecurrence
end nextMonthly
(* Get the next due recurrence date of a "YEARLY" event. *)
on nextYearly(RFC2445, startdate, today, endDate, interval, maxRecurrences)
set targetMonths to getTargetMonths(RFC2445, startdate)
set recurrencesPerYear to (count targetMonths)
set BYDAYspecified to (RFC2445 contains "BYDAY=")
if (BYDAYspecified) then set {weekdayCode, weekdayInstanceNos} to getWeekdayInstanceStuff(RFC2445)
-- Run the recurrence sequence from the year of the start date, starting with the relevant day of the first month in the list.
-- Start counting after passing the start date, which has already been checked in getNextRecurrence().
-- Continue to the first occurrence on or after today, or until maxRecurrences is exceeded.
copy startdate to nextRecurrence
set recurrenceNo to 1 -- Recurrence 1 is the start date.
set defaultDayNo to startdate's day
set dayInMonth to true -- Set to false when a recurrence day doesn't exist in a target month.
set i to 0 -- Index into targetMonths.
repeat while ((nextRecurrence comes before today) or not (dayInMonth)) and (recurrenceNo < maxRecurrences)
set i to i + 1
if (i > recurrencesPerYear) then
set i to 1
set nextRecurrence's year to (nextRecurrence's year) + interval
end if
set targetMonth to item i of targetMonths -- Get the next specified month.
if (BYDAYspecified) then
-- Get the date of the specified weekday in this month of this year.
set nextRecurrence's day to 1
set nextRecurrence's month to targetMonth
set nextRecurrence to getWeekdayDate(nextRecurrence, weekdayCode, beginning of weekdayInstanceNos)
else
-- Get the date of the specified numbered day in this month of this year.
set nextRecurrence's day to defaultDayNo
set nextRecurrence's month to targetMonth
end if
-- If the day doesn't exist in the month, the day and month will now have changed.
set dayInMonth to (nextRecurrence's month is targetMonth)
if (dayInMonth) and (nextRecurrence comes after startdate) then set recurrenceNo to recurrenceNo + 1
end repeat
-- No next recurrence if maxRecurrences is exceeded or endDate passed.
if (nextRecurrence comes before today) or (nextRecurrence comes after endDate) then set nextRecurrence to missing value
return nextRecurrence
end nextYearly
(* Odd jobs. *)
(* Read a given rule from the event's 'recurrence' text. *)
on getRule(RFC2445, ruleKey)
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ruleKey & "="
set rule to text item 2 of RFC2445
set AppleScript's text item delimiters to ";"
set rule to text item 1 of rule
set AppleScript's text item delimiters to astid
return rule
end getRule
(* Get the recurrence's "UNTIL" date (if specified) in AppleScript form or default to 31st December 9999. (Ignore time.) *)
on getEndDate(RFC2445)
set endDate to «data isot393939392D31322D3331» as date -- 31st December 9999.
if (RFC2445 contains "UNTIL") then
set n to (text 1 thru 8 of getRule(RFC2445, "UNTIL")) as integer
set endDate's day to n mod 100
set endDate's year to n div 10000
set endDate's month to item (n mod 10000 div 100) of {January, February, March, April, May, June, July, August, September, October, November, December}
end if
return endDate -- +- (time to GMT)?
end getEndDate
(* Get the event's recurrence interval (if specified) or default to 1. *)
on getInterval(RFC2445)
if (RFC2445 contains "INTERVAL") then return getRule(RFC2445, "INTERVAL") as integer
return 1
end getInterval
(* Get the event's recurrence count (if specified) or default to an arbitrarily high number. *)
on getMaxRecurrences(RFC2445)
if ((RFC2445 contains "COUNT") and (RFC2445 does not contain "COUNT=-1")) then return getRule(RFC2445, "COUNT") as integer
return 1000000
end getMaxRecurrences
(* Return a list of the text items in a comma-delimited rule result. *)
on getListFromRule(rule)
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ","
set theList to rule's text items
set AppleScript's text item delimiters to astid
return theList
end getListFromRule
(* Derive a list of AppleScript months from the numbers in a "BYMONTH" rule or default to the month of the event's start date. *)
on getTargetMonths(RFC2445, startdate)
if (RFC2445 contains "BYMONTH=") then
set targetMonths to getListFromRule(getRule(RFC2445, "BYMONTH"))
set monthList to {January, February, March, April, May, June, July, August, September, October, November, December}
repeat with thisMonth in targetMonths
set thisMonth's contents to item (thisMonth as integer) of monthList
end repeat
else
set targetMonths to {startdate's month}
end if
return targetMonths
end getTargetMonths
(* Get a list of the day numbers specified in a "BYMONTHDAY" rule or default to the day of the event's start date. *)
on getTargetDays(RFC2445, startdate)
if (RFC2445 contains "BYMONTHDAY=") then
set targetDays to getListFromRule(getRule(RFC2445, "BYMONTHDAY"))
repeat with thisDay in targetDays
set thisDay's contents to thisDay as integer
end repeat
else
set targetDays to {startdate's day}
end if
return targetDays
end getTargetDays
(* Get and analyse the weekday(s) specified in a "BYDAY" rule, returning appropriate results. *)
on getWeekdayInstanceStuff(RFC2445)
set BYDAY to getRule(RFC2445, "BYDAY")
if (RFC2445 contains "WEEKLY") then
-- In a "WEEKLY" environment, return a list of offsets, in seconds, of the specified weekdays from the specified week start.
-- The week start itself is returned as an offset in seconds from Sunday.
set weekdayCodes to "SUMOTUWETHFRSA"
set WKSToffset to (offset of getRule(RFC2445, "WKST") in weekdayCodes) div 2 * days
set weekdayOffsets to getListFromRule(BYDAY)
repeat with thisEntry in weekdayOffsets
-- Each weekday offset must be the offset AFTER or including the week start.
set thisEntry's contents to ((offset of thisEntry in weekdayCodes) div 2 * days + weeks - WKSToffset) mod weeks
end repeat
return {WKSToffset, weekdayOffsets}
else
-- In a "MONTHLY" environment, only one weekday is specified, along with an instance-in-month figure.
-- The standard allows for the lack of an instance number to mean "every instance in the month".
-- iCal doesn't currently implement this, but there's a hook for it in the script. :)
set weekdayCode to text -2 thru -1 of BYDAY
if ((count BYDAY) > 2) then
set instanceNos to {(text 1 thru -3 of BYDAY) as integer}
else
set instanceNos to {1, 2, 3, 4, -1}
end if
return {weekdayCode, instanceNos}
end if
end getWeekdayInstanceStuff
(* Get a date that's m calendar months after (or before, if m is negative) the input date. *)
to addMonths(oldDate, m)
copy oldDate to newDate
set {y, m} to {m div 12, m mod 12}
set newDate's year to (newDate's year) + y
-- Add the odd months (at 32 days per month) and set the day.
if (m is not 0) then tell newDate to set {day, day} to {32 * m, day}
-- If the day's changed, the original doesn't exist in the target month.
-- Subtract the overflow into the following month to return to the last day of the target month.
if (newDate's day is not oldDate's day) then set newDate to newDate - (newDate's day) * days
return newDate
end addMonths
(* Get the date of a given instance of a given weekday in the month of a given date. *)
on getWeekdayDate(givenDate, weekdayCode, instanceNo) -- (AS date, 2-letter BYDAY code, integer)
-- Get a date in the past that's known to have the required weekday. (Sunday 5th January 1000 + 0 to 6 days.)
set refDate to («data isot313030302D30312D3035» as date) + (givenDate's time) + (offset of weekdayCode in "SUMOTUWETHFRSA") div 2 * days
-- Get the last day of the seven-day period in the current month that includes the given instance of any weekday.
if (instanceNo is -1) then
tell givenDate to tell it + (32 - (its day)) * days to set periodEnd to it - (its day) * days -- Last day of month.
else
copy givenDate to periodEnd
set periodEnd's day to instanceNo * 7 -- 7th, 14th, 21st, or 28th of month.
end if
-- Round down to an exact number of weeks after the known-weekday date.
return periodEnd - (periodEnd - refDate) mod weeks
end getWeekdayDate
tell application "iCal"
set {allEvents, allSummaries, allStartDates, allRecurrenceTexts} to {it, summary, start date, recurrence} of events of calendar "Zarafa Calendar"
end tell
set today to (current date)
set todaysSummaries to {}
repeat with i from 1 to (count allSummaries)
set nextRecurrence to my getNextRecurrence(item i of allRecurrenceTexts, item i of allStartDates, today)
if (nextRecurrence is today) then set end of todaysSummaries to item i of allSummaries
end repeat
if (todaysSummaries is {}) then
display dialog "Nothing to do today." with icon note
else
choose from list todaysSummaries with prompt "Here's what's happening today."
end if
However, when I run the above, I get an error:
“Can’t get text item 2 of "FREQ=WEEKLY;INTERVAL=1;BYDAY=WE".”
This seems to be set in the “Odd jobs” section, but I’ve no idea why it’s happening.
I’d appreciate any feedback.
Thanks,
Des Dougan