Newbie help: How to identify which open file alarm triggered a script

A small apology in advance. This is my first applescript and my first post here!

I am trying to write a script for events in iCal, to create a ToDo with the name of the event when the open file alarm from a recurring event triggers.

Apple doesn’t seem to contemplate recurring ToDo’s :wink:

However, it seems that the open file alarm, although running the file, does not pass anything to it - in other words I can’t work out which event has triggered it. (if anyone can tell me how to work this out, i need go no further)

I have resorted to trying to do it manually, ie to work though a list of all of the events in all calendars, recurring or otherwise, for which the event has not yet occurred but for which the time on the open file alarm has already past. So what I need to do is set a object to contain all events of myCal whose open file alarm’s filepath ends with “iCalToToDo.app”

but this doesn’t work (I didn’t really expect it to)


tell application "iCal"
	
	repeat with myCal in calendars
		set myEvents to (events of myCal whose open file alarm's filepath ends with "iCalToToDo.app")
		
	end repeat
end tell

Is there a simple way of doing this?

As an alternative, I have tried nesting a long sequence of set/repeat commands to work through each layer of properties to get to the relevant events that have an alarm with this filepath one by one. It is below, but doesn’t yet work. It’s late and I am going to bed!

Finally, I’d like to know how to add the calendar events, as I identify each one, to an object so that I can then pass the whole lot to a handler to deal with them all at once. Whether or not I need to do that (I appreciate I could just pass them to the handler one by one), I’d really like to know how to do it. ie although I can “set myEvents to (events of myCal)”, which seems to create an object containing calendar events that I can then pass to a handler, I don’t know how to start that from scratch and add events to myEvents one by one.

Any help appreciated. My apologies if these questions are obvious (or even stupid), but I am stuck. I have read the tutorials and as much other guff on the web as I can handle, but have not found my answer.

Regards
AndyR

PS I have found and used the excellent script by Nigel Garvey at http://macscripter.net/viewtopic.php?pid=94958 to provide the next recurrence of an event.

PPS In case it helps, my full script to attempt to just identify the relevant events in the next two weeks follows:


set myStart to date (date string of ((current date)))
set myEnd to myStart + 14 * days

tell application "iCal"
	set listOfEvents to {} -- sets a blank checklist
	repeat with myCal in calendars -- first layer - deals with multiple calendars
		
		set myEvents to (events of myCal)
		
		repeat with myEvent in myEvents -- second layer - gets each event
			
			if ((count recurrence of myEvent) > 0) then -- This is a recurring event.
				
				set myOccurence to my getNextRecurrence(recurrence of myEvent, start date of myEvent, (current date)) -- uses this handler to return the next event date of the recurring event
				
				if myOccurence is greater than or equal to myStart and myOccurence is less than or equal to myEnd then -- checks that it is within my two week period
					
					set myOpenPathAlarms to open file alarms in myEvent --- third layer, those with open file alarms
					
					repeat with myOpenPathAlarm in myOpenPathAlarms
						set myFilePath to filepath of myOpenPathAlarm
						if myFilePath ends with "iCalToToDo.app" then
							try
								make todo at end of todos of myCal with properties {summary:(summary of myEvent), due date:(start date of myEvent)} -- makes a todo using that info
								copy summary of myEvent to end of listOfEvents -- adds it to my checklist so that I can see what is going on
							end try
						end if
					end repeat
					
				end if
			end if
			
		end repeat
	end repeat
	return listOfEvents -- returns my checklist
end tell

(* With many thanks to Nigel Garvey at http://macscripter.net/viewtopic.php?pid=94958 for getNextOccurrence , which 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)
	set startDate's time to 0
	set today's time to 0
	
	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)
				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 - days - startDate) mod interval + interval - days
	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) + 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") 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) + (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

(* -- Demo code:
tell application "iCal"
   set {allEvents, allSummaries, allStartDates, allRecurrenceTexts} to {it, summary, start date, recurrence} of events of calendar "My Calendar"
end tell
set today to (current date)

repeat with i from 1 to (count allEvents)
   set nextRecurrence to my getNextRecurrence(item i of allRecurrenceTexts, item i of allStartDates, today)
   if (nextRecurrence is missing value) then
       -- Item i of allEvents, whose summary is item i of allSummaries, has expired.
   else if (nextRecurrence is today) then
       -- The event recurs today.
   else
       -- The event will recur on the date contained in nextRecurrence.
   end if
end repeat *)



I’m not good with ical so I can’t help with your specific question, but here’s a different approach which might help. It seems you want the alarm to run iCalToToDo.app so you’re using the open file alarm. How about if you run an applescript as the alarm instead. The applescript could open iCalToToDo.app for you and then do the other stuff you want. Since the applescript is triggered by the alarm then you know what alarm triggered it.

Hi, Andy. Welcome to these fora.

My recurrence prediction code wasn’t quite suitable for your needs as it was deliberately coarse-tuned to ignore times of day. The script below includes a variation which returns recurrence times more precisely. As you say, it isn’t possible to tell definitely which alarm triggered the script (unless you used a dedicated script for each ” as I see Hank has suggested). You can only infer it from testing which alarms appear to have just gone off. This might cause confusion if alarms coincide, but perhaps it doesn’t matter in that case which alarm causes which todo to be created, or if one achieves the lot.

(* To be run by an open file alarm attached to a recurring event in iCal.
The alarm should fire at or before the recurrence time, not after it. *)

set reactionMargin to 5 -- Allow a margin of, say, five seconds for the script to get going.
set now to (current date) - reactionMargin
tell application "System Events" to set myURL to URL of file (path to me as Unicode text)

tell application "iCal"
	-- Quiz all the calendars for events with matching open file alarms.
	set calendarResponses to (events of calendars whose filepath of open file alarms contains myURL)
	-- The result is a list wherein a 'missing value' means a calendar with no matching events, an event reference
	-- means a calendar with a single match, and a list of events means a calendar with multiple matches.
	repeat with i from 1 to (count calendarResponses)
		set thisCalendarResponse to item i of calendarResponses
		if (thisCalendarResponse is not missing value) then
			if (thisCalendarResponse's class is event) then set thisCalendarResponse to {thisCalendarResponse}
			repeat with thisEvent in thisCalendarResponse
				-- Work out when each matching event's next recurrence is due.
				set {start date:thisStartDate, recurrence:thisRecurrence} to thisEvent
				set nextRecurrence to my getNextRecurrence(thisRecurrence, thisStartDate, now)
				-- If now is within the alarm trigger interval for it, the alarm may have triggered this script.
				-- Make a matching todo (if one doesn't already exist) in the same calendar as the event.
				if ((nextRecurrence's class is date) and (nextRecurrence - now ≤ (trigger interval of thisEvent's first open file alarm) * -minutes + reactionMargin)) then
					set newTodoProperties to {summary:thisEvent's summary, due date:nextRecurrence} -- Or whatever.
					-- Check in case the todo has already been created by another alarm happening at the same time.
					set alreadyDone to false
					repeat with thisTodosProperties in (get properties of todos of calendar i)
						set alreadyDone to (thisTodosProperties contains newTodoProperties)
						if (alreadyDone) then exit repeat
					end repeat
					if (not alreadyDone) then make new todo at end of todos of calendar i with properties newTodoProperties
				end if
			end repeat
		end if
	end repeat
end tell

(* 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
	
	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