Hi, Kedeb.
Sorry I’ve been so long getting back. I’m having a busy week and also ran into some hitherto unsuspected iCal peculiarities that needed to be checked out before I posted the second script below.
A “recurring event” is just a single event with a recurrence rule, which iCal interprets “on the fly” to display the subsequent reiterations in the calendar. The iCalendar recurrence rule for the first non-(Saturday-or-Sunday) of every month would be “FREQ=MONTHLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1”. But although iCal observes many recurrence types that can’t be set in its GUI, the BYSETPOS rule part isn’t one of them (at least, up to iCal 2.0.5). So a “recurring event” of the type you want isn’t possible in iCal.
The easiest thing with AppleScript would be to create a separate event for each month ” though of course it’s only sensible to do this for a certain distance ahead. Five years’ worth of reminders would require sixty events:
-- Adjust these properties to your own requirements.
property theSummary : "On-line payments" -- Event legend.
property calendarNameOrNumber : -1 -- The calendar name or index number.
property numberOfMonths : 60 -- Number of months in the period to cover.
set nextEventDate to (current date)
set nextEventDate's time to 0
repeat numberOfMonths times
-- Get the 1st day of next month.
tell nextEventDate + (32 - (nextEventDate's day)) * days to set nextEventDate to it + (1 - (its day)) * days
-- If it's a Saturday or a Sunday, advance to the following Monday.
set w to nextEventDate's weekday
if (w is Saturday) then
set nextEventDate to nextEventDate + 2 * days
else if (w is Sunday) then
set nextEventDate to nextEventDate + days
end if
-- If it's the New Year bank holiday, adjust accordingly.
if (nextEventDate's month is January) then
if (w is Friday) then
set nextEventDate to nextEventDate + 3 * days
else
set nextEventDate to nextEventDate + days
end if
end if
-- Make an all-day event in iCal for the calculated day.
tell application "iCal" to make new event at end of events of calendar calendarNameOrNumber with properties {summary:theSummary, start date:nextEventDate, allday event:true}
end repeat
An alternative would be to create an event that recurred on the first day of every month, then go through the calendar, manually dragging iterations that fell on a Saturday or Sunday to the following Monday. (This can’t easily be done with AppleScript.) Again, it’s only sensible to do it for a certain amount of time ahead, but the “detached events” so created would be relatively few in number. For the five years beginning next month, there’d be 17 detached events (or 20 if you were also avoiding New Year bank holidays) plus the original recurring one. “Detached events” are linked to specific iteration times of a recurring event and are expressed instead of the iterations that would otherwise have appeared at those times. iCal 2.0.5 hides them from AppleScript.
An AppleScriptable variation on this would be to delete (or “exclude”) iterations that fell on the offending days and create completely independent events for the alternative days. The number of independent events would be the same as the number of detached events with the previous method.
The iCal peculiarities I mentioned affect this script:
- With both iCal 1.5.5 and iCal 2.0.5, if a recurring event is specified to iterate n times, and n > 1, and the event’s an all-day event, then iCal only expresses (n - 1) iterations. I haven’t done anything about this bug in the script.
- iCal returns index references to events. But I find that iCal 2.0.5 cavalierly reindexes events on the fly if it feels like it, rendering event references in variables obsolete. The script therefore sets up a filter-by-UID reference for the recurring event so that it can be identified when the time comes to exclude its unwanted iteration dates.
- The events the script creates, although testable with AppleScript, don’t become visible in iCal (2.0.5) until I quit it and reopen it. The script therefore arranges for this to happen.
-- Adjust these properties to your own requirements.
property theSummary : "On-line payments" -- Event legend.
property calendarNameOrNumber : -1 -- The calendar name or index number.
property numberOfMonths : 60 -- Number of months in the period to cover.
-- Get the first day of the month after a given date.
on firstOfNextMonth(now)
tell now + (32 - (now's day)) * days to return it + (1 - (its day)) * days
end firstOfNextMonth
-- If a 1st-of-month date falls during a weekend or is New Year's Day, get the next working day.
on nudge(thisDate)
set w to thisDate's weekday
if (w is Saturday) then
set thisDate to thisDate + 2 * days
else if (w is Sunday) then
set thisDate to thisDate + days
end if
if (thisDate's month is January) then
if (w is Friday) then
set thisDate to thisDate + 3 * days
else
set thisDate to thisDate + days
end if
end if
return thisDate
end nudge
set startDate to nudge(firstOfNextMonth(current date))
set startDate's time to 0
-- Create a recurring all-day event that repeats on the first of every month for the set number of months.
-- Get a filter-by-UID reference to it for reliability in iCal 2.0.5.
tell application "iCal"
set rootEventUID to uid of (make new event at end of events of calendar calendarNameOrNumber with properties {start date:startDate, summary:theSummary, allday event:true})
set rootEvent to a reference to (first event of calendar calendarNameOrNumber whose uid is rootEventUID)
set rootEvent's recurrence to "FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1;COUNT=" & numberOfMonths
end tell
-- Trace the recurrence sequence, looking out for weekend and New Year dates.
set recurrenceDate to startDate
set excludedDates to {}
repeat (numberOfMonths - 1) times
-- If the next recurrence date falls during a weekend or is New Year's Day, append it
-- to the excluded-date list and create an alternative event for the next working day.
set recurrenceDate to firstOfNextMonth(recurrenceDate)
if (recurrenceDate's weekday is in {Saturday, Sunday}) or (recurrenceDate's month is January) then
set end of excludedDates to recurrenceDate
set recurrenceDate to nudge(recurrenceDate)
tell application "iCal" to make new event at end of events of calendar calendarNameOrNumber with properties {start date:recurrenceDate, summary:theSummary, allday event:true}
end if
end repeat
-- Set the recurring event's 'excluded dates' to the excluded-date list to stop those particular recurrence
-- instances being expressed, then quit and reactivate iCal to make the changes visible in the calendar (in iCal 2.0.5).
tell application "iCal"
set rootEvent's excluded dates to excludedDates
quit
end tell
tell application "System Events" to repeat while (application process "iCal" exists)
delay 0.2
end repeat
tell application "iCal" to activate