This is the next stage worth posting of my initial attempt at a script to predict when the next expression of a repeating iCal event will occur. Whereas the earlier script simply returned a date calculated from the event’s start date and recurrence rule, this one takes into account detachments, exclusions, time zones, and times of day and returns a full set of data for the repeat instance found. If the instance has been moved or edited in iCal, the data returned will reflect this.
The main handler, getNextExpression, takes an iCal event UID and an AppleScript date and returns a record containing information about the next expression occurring after the date ” or at the very moment of the date if a repeat happens to start then. If the event isn’t expressed at all after the date, the return is ‘missing value’. A third, boolean parameter (‘straddle’) allows the acceptance or not of an expression whose start and end dates happen to straddle the given date.
Since the script can also return the very first instance of a repeating event under the right circumstances, it also works with non-repeating events.
The code was originally written and tested against iCal 1.5 and iCal 2.0. It now works with iCal 4.0 too and with the impaired date manipulation in AppleScript 2.1 (Snow Leopard). It should also work with iCal 3.0. (Thanks to StefanK and Adam Bell for information regarding iCal 3.0’s calendar file storage.)
The input date and returned dates are for the computer’s local time zone, but, as in iCal itself, the recurrence sequence is followed in the time zone where the event was created. All-day events are timezoneless.
The script handles every repeat type that can be set in iCal’s user interface and with those which iCal obeys but which can only be set by AppleScripting the ‘recurrence’ value or by manually editing the .ics files. (See the following post.) The iCalendar standard allows an event to have more than one recurrence rule. iCals 1.5.5 and 2.0.5 honoured multiple rules where they existed in the .ics files, but iCal 4.0 only heeds the first. The script, however, retains its multiple-recurrence ability.
No version of iCal is known to observe the BYSETPOS rule part or rule parts governing repeat intervals of less than a day, so the script doesn’t bother either.
Because iCal’s AppleScript implementation doesn’t return the necessary time-zones or detached-event information, the script gets everything it needs from the .ics files. This makes it somewhat less than instantaneous and it won’t work properly with recently created or edited events if applied to them before iCal gets around to updating the files.
-- A known Monday in the past in the proleptic Gregorian calendar. Although its string representation is incorrect in most manifestations of AppleScript 2 so far, the date object itself is fine. Other reference dates used in the script are derived from it as offsets.
property Monday10000106 : «data isot313030302D30312D3036» as date -- date "Monday 6 January 1000 00:00:00".
(*** Main handler. ***)
(* Return the effective elements and properties of the next (or only) expression of an iCal/Calendar event at or after a given moment, or return 'missing value' if none. *)
-- checkDate (AppleScript date object): the computer-local date/time from which to search.
-- eventUID (text): the event's UID as returned by iCal or Calendar.
-- straddle (boolean): 'true' to include expressions already in progress at the check date; 'false' to go strictly by start times.
on getNextExpression from checkDate for eventUID given straddle:straddleOK
-- A faster time-zone transposition technique is possible with AppleScript 2.
global AppleScript_2
set AppleScript_2 to ((system attribute "ascv") mod 65536 ≥ 512) -- 'true' if AS 2.0 or later.
considering case
-- Get the available details of the root event and of any detached events having this UID.
set theData to getAllData(eventUID, checkDate)
set {|computer TZ|:computerTZ, |event TZ|:eventTZ, |root event data|:{|RRULEs|:RRULEs, |start date|:startDate}} to theData
-- Transpose the check date to the event's time zone.
set checkDate to TZtoTZ(checkDate, computerTZ, eventTZ)
set hits to {}
set RRULECount to (count RRULEs)
if (RRULECount > 0) then
-- This is a repeating event. Get the details of the event's next expression under each of its governing RRULEs (although iCal and Calendar only allow one RRULE to be set and only iCals 1 and 2 honoured multiple RRULEs in imported calendars). The dates returned will have been transposed where necessary to the computer's time zone.
repeat with i from 1 to RRULECount
set RRULE to item i of RRULEs
set freq to getRulePartValue(RRULE, "FREQ")
if (freq is "DAILY") then
set end of hits to nextDaily(theData, checkDate, RRULE, straddleOK)
else if (freq is "WEEKLY") then
set end of hits to nextWeekly(theData, checkDate, RRULE, straddleOK)
else if (freq is "MONTHLY") then
set end of hits to nextMonthly(theData, checkDate, RRULE, straddleOK)
else if (freq is "YEARLY") then
set end of hits to nextYearly(theData, checkDate, RRULE, straddleOK)
end if
end repeat
else if ((startDate ≥ checkDate) or ((straddleOK) and (startDate + (theData's |root event data|'s duration) > checkDate))) then
-- This is non-recurring event on, after, or acceptably straddling the check date.
set end of hits to getInstanceData(theData, startDate)
end if
end considering
set hits to hits's records
if (hits is {}) then
-- No expressions of the event after the check date.
set topHit to missing value
else
-- Otherwise get the earliest one.
set topHit to beginning of hits
repeat with i from 2 to (count hits)
if (|start date| of item i of hits comes before topHit's |start date|) then set topHit to item i of hits
end repeat
end if
return topHit
end getNextExpression
(*** Handlers for the four "FREQ" values recognised by iCal/Calendar. ***)
(* Get the next expression of a "DAILY" rule. *)
on nextDaily(theData, checkDate, RRULE, straddleOK)
set {|root event data|:{|start date|:startDate}, |excluded dates|:exdates} to theData
set interval to getInterval(RRULE) * days
set expiryDate to getExpiryDate(RRULE, theData's |event TZ|)
set maxExpressions to getMaxExpressions(RRULE)
set BYMONTH to (RRULE contains "BYMONTH=")
if ((BYMONTH) or (RRULE contains "BYWEEKNO=")) then -- BYMONTH or BYWEEKNO sub-rule.
-- Run the recurrence sequence from the start date to first valid expression on or after the check date or until any recurrence limit is exceeded.
if (BYMONTH) then
set targetMonths to getTargetMonths(RRULE, startDate)
else -- BYWEEKNO
set {weekYearStart, weekYearEnd} to getWeekYear(RRULE, startDate)
set targetWeeks to getIntegerList(getRulePartValue(RRULE, "BYWEEKNO"))
end if
set recurrenceDate to startDate
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
set hit to true
set recurrenceNo to 1
repeat while (((expressionDate comes before checkDate) and not ((straddleOK) and (checkDate - expressionDate < duration)) or (recurrenceDate is in exdates) or (not hit)) and (recurrenceNo < maxExpressions))
set recurrenceDate to recurrenceDate + interval
if (BYMONTH) then
set hit to (recurrenceDate's month is in targetMonths)
else -- BYWEEKNO
if (recurrenceDate comes after weekYearEnd) then
set weekYearStart to weekYearEnd + 1
set weekYearEnd to weekYearEnd + 53 * weeks
if (weekYearEnd's day comes after 3) then set weekYearEnd to weekYearEnd - weeks
end if
set hit to (((recurrenceDate - weekYearStart) div weeks + 1 is in targetWeeks) or ((recurrenceDate - weekYearEnd) div weeks - 1 is in targetWeeks))
end if
if (hit) then set recurrenceNo to recurrenceNo + 1
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
end repeat
if (((expressionDate comes before checkDate) and not ((straddleOK) and (checkDate - expressionDate < duration))) or (recurrenceDate comes after expiryDate) or (recurrenceDate is in exdates)) then set recurrenceDate to missing value
else -- No BYxxx rule parts. The recurrence date before the check date can be calculated without having to run the sequence.
if (checkDate comes after startDate) then
set recurrenceDate to checkDate - (checkDate - 1 - startDate) mod interval - 1
else
set recurrenceDate to startDate
end if
-- . but then a little sequencing's needed to sort out the particular circumstances.
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
repeat while ((recurrenceDate is in theData's |excluded dates|) or ((expressionDate comes before checkDate) and not ((straddleOK) and (checkDate - expressionDate < duration))))
set recurrenceDate to recurrenceDate + interval
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
end repeat
if (((recurrenceDate - startDate) div interval + 1 > maxExpressions) or (recurrenceDate comes after expiryDate)) then set recurrenceDate to missing value
end if
return getInstanceData(theData, recurrenceDate)
end nextDaily
(* Get the next expression of a "WEEKLY" rule. *)
on nextWeekly(theData, checkDate, RRULE, straddleOK)
set {|root event data|:{|start date|:startDate}, |excluded dates|:exdates} to theData
set interval to getInterval(RRULE) * weeks
set expiryDate to getExpiryDate(RRULE, theData's |event TZ|)
set maxExpressions to getMaxExpressions(RRULE)
set BYMONTH to (RRULE contains "BYMONTH")
if (BYMONTH) then set targetMonths to getTargetMonths(RRULE, startDate)
-- Get the start of the week which contains the event's start date and the offset(s) of the recurrence weekday(s) from that.
set {weekStart, weekdayOffsets} to getWeekdayStuff(RRULE, startDate)
set weekEnd to weekStart + weeks - 1
set recurrencesPerWeek to (count weekdayOffsets)
-- The event's start date isn't necessarily a recurrence weekday.
-- Initialise an index into weekdayOffsets such that (index + 1) indexes the recurrence weekday after the start date.
set i to recurrencesPerWeek
repeat while ((i > 0) and (weekStart + (beginning of item i of weekdayOffsets) comes after startDate))
set i to i - 1
end repeat
-- Run the recurrence sequence from the start date.
set recurrenceDate to startDate
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
set hit to true
set recurrenceNo to 1
repeat while ((((expressionDate comes before checkDate) and not ((straddleOK) and (checkDate - expressionDate < duration))) or (recurrenceDate is in exdates) or (not hit)) and (recurrenceNo < maxExpressions))
set i to i + 1
if (i > recurrencesPerWeek) then
set i to 1
set weekStart to weekStart + interval
set weekEnd to weekEnd + interval
end if
set recurrenceDate to weekStart + (beginning of item i of weekdayOffsets) + (startDate's time)
if (BYMONTH) then set hit to (weekStart's month is in targetMonths) or (weekEnd's month is in targetMonths)
if (hit) then set recurrenceNo to recurrenceNo + 1
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
end repeat
-- No next expression if maxExpressions exceeded or expiryDate passed.
if (((expressionDate comes before checkDate) and not ((straddleOK) and (checkDate - expressionDate < duration))) or (recurrenceDate comes after expiryDate) or (recurrenceDate is in exdates)) then set recurrenceDate to missing value
return getInstanceData(theData, recurrenceDate)
end nextWeekly
(* Get the next expression of a "MONTHLY" rule. *)
on nextMonthly(theData, checkDate, RRULE, straddleOK)
script BYscript -- Used because the BYMONTHDAY and BYDAY script objects need a parent.
property child : missing value
property hits : missing value
on getRecurrenceDates(anchorDate)
set my hits to {}
child's getRecurrenceDates(anchorDate)
return my hits
end getRecurrenceDates
(* The getWeekdayDate() handler called from BYDAY is inherited from main script. *)
end script
set {|root event data|:{|start date|:startDate}, |excluded dates|:exdates} to theData
set interval to getInterval(RRULE)
set expiryDate to getExpiryDate(RRULE, theData's |event TZ|)
set maxExpressions to getMaxExpressions(RRULE)
if (RRULE contains "BYDAY=") then
set BYscript's child to BYDAYscript(RRULE, startDate, BYscript)
else -- BYMONTHDAY or no BYxxx rule part.
set BYscript's child to BYMONTHDAYscript(RRULE, startDate, BYscript)
end if
set recurrenceDates to BYscript's getRecurrenceDates(startDate)
set recurrencesInMonth to (count recurrenceDates)
-- The event's start date isn't necessarily a recurrence weekday or monthday.
-- Initialise an index into recurrenceDates such that (index + 1) indexes the recurrence instance after the start date.
set i to recurrencesInMonth
repeat while (i > 0) and (item i of recurrenceDates comes after startDate)
set i to i - 1
end repeat
set recurrenceNo to 1
set misses to 0
-- Run the recurrence sequence from the start date.
set anchorDate to startDate
copy anchorDate to recurrenceDate
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
repeat while ((((expressionDate comes before checkDate) and not ((straddleOK) and (checkDate - expressionDate < duration))) or (recurrenceDate is in exdates)) and (recurrenceNo < maxExpressions))
set i to i + 1
if (i > recurrencesInMonth) then
set i to 1
set anchorDate to addMonths(anchorDate, interval)
set recurrenceDates to BYscript's getRecurrenceDates(anchorDate)
set recurrencesInMonth to (count recurrenceDates)
if (recurrencesInMonth is 0) then
set misses to misses + 1
if (misses ≥ ((anchorDate's year) - (startDate's year)) * interval) then exit repeat
end if
sort(recurrenceDates)
end if
if (recurrencesInMonth) > 0 then
set recurrenceDate to item i of BYscript's hits
if (recurrenceDate comes after startDate) then
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
set recurrenceNo to recurrenceNo + 1
end if
end if
end repeat
-- No next expression if maxExpressions exceeded or expiryDate passed.
if (((expressionDate comes before checkDate) and not ((straddleOK) and (checkDate - expressionDate < duration))) or (recurrenceDate comes after expiryDate) or (recurrenceDate is in exdates)) then set recurrenceDate to missing value
return getInstanceData(theData, recurrenceDate)
end nextMonthly
(* Get the next expression of a "YEARLY" rule. *)
on nextYearly(theData, checkDate, RRULE, straddleOK)
set {|root event data|:{|start date|:startDate}, |excluded dates|:exdates} to theData
set interval to getInterval(RRULE)
set expiryDate to getExpiryDate(RRULE, theData's |event TZ|)
set maxExpressions to getMaxExpressions(RRULE)
set BYMONTH to (RRULE contains "BYMONTH=")
set BYWEEKNO to (RRULE contains "BYWEEKNO=")
set BYYEARDAY to (RRULE contains "BYYEARDAY=")
set BYMONTHDAY to (RRULE contains "BYMONTHDAY=")
set BYDAY to (RRULE contains "BYDAY=")
(* The Dawson & Stenerson document says: "If multiple BYxxx rule parts are specified, then after evaluating the specified FREQ and INTERVAL rule parts, the BYxxx rule parts are applied to the current set of evaluated occurrences in the following order: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR, BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are evaluated." It then goes on to give an example whereby a yearly rule has BYMONTH applied to it, then BYDAY, etc., anything not defined being taken from the event's start date. But iCal currently (vs 1 & 2) does as follows, which only makes a difference with badly formed rules: if a rule has incompatible parts, say, both BYWEEKNO and BYMONTHDAY, iCal ignores BYWEEKNO (presumably because BYMONTHDAY implies BYMONTH, which takes precedence over BYWEEKNO), whereas my understanding would be to ignore BYMONTHDAY (BYWEEKNO is defined, BYMONTH isn't). *)
-- iCal logic:
if (BYDAY) then
if (BYMONTH) or not (BYWEEKNO) then
set BYscript to BYMONTHscript(RRULE, startDate, BYDAY) -- BYMONTH with BYDAY. (Default month if necessary).
else
set BYscript to BYWEEKNOscript(RRULE, startDate) -- BYWEEKNO with BYDAY.
end if
else if (BYMONTH) or (BYMONTHDAY) then
set BYscript to BYMONTHscript(RRULE, startDate, BYDAY) -- BYMONTH with BYMONTHDAY. (Default month or day).
else if (BYWEEKNO) then
set BYscript to BYWEEKNOscript(RRULE, startDate) -- BYWEEKNO with BYDAY. (Default weekday).
else if (BYYEARDAY) then
set BYscript to BYYEARDAYscript(RRULE, startDate)
else
set BYscript to BYMONTHscript(RRULE, startDate, false) -- BYMONTH with MONTHDAY. (Default month and day).
end if
set defaultDayNo to startDate's day
set recurrenceDates to BYscript's getRecurrenceDates(startDate)
set recurrencesInYear to (count recurrenceDates)
-- The event's start date isn't necessarily one of the recurrence days.
-- Initialise an index into recurrenceDates such that (index + 1) indexes the recurrence instance after the start date.
set i to recurrencesInYear
repeat while (i > 0) and (item i of recurrenceDates comes after startDate)
set i to i - 1
end repeat
-- Run the recurrence sequence from the start date.
copy startDate to anchorDate
copy startDate to recurrenceDate
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
set recurrenceNo to 1
set maxExpressions to getMaxExpressions(RRULE)
set misses to 0
repeat while ((((expressionDate comes before checkDate) and not ((straddleOK) and (checkDate - expressionDate < duration))) or (recurrenceDate is in exdates)) and (recurrenceNo < maxExpressions))
set i to i + 1
if (i > recurrencesInYear) then
set i to 1
set anchorDate's year to (anchorDate's year) + interval
set recurrenceDates to BYscript's getRecurrenceDates(anchorDate)
set recurrencesInYear to (count recurrenceDates)
if (recurrencesInYear is 0) then
set misses to misses + 1
if (misses ≥ ((anchorDate's year) - (startDate's year)) * interval) then exit repeat
end if
sort(recurrenceDates)
end if
if (recurrencesInYear > 0) then
set recurrenceDate to item i of recurrenceDates
if (recurrenceDate comes after startDate) then
set recurrenceNo to recurrenceNo + 1
set {expressionDate, duration} to getExpressionDate(recurrenceDate, theData)
end if
end if
end repeat
-- No next expression if maxExpressions is exceeded or expiryDate passed.
if ((expressionDate comes before checkDate) and not ((straddleOK) and (checkDate - expressionDate < duration))) or (recurrenceDate comes after expiryDate) or (recurrenceDate is in exdates) then set recurrenceDate to missing value
return getInstanceData(theData, recurrenceDate)
end nextYearly
(*** BYxxx sequence script object constructors and linkers for "MONTHLY" & "YEARLY" recurrences. The scripts are linked as required to delegate recurrence sequencing subtasks to each other. ***)
on BYMONTHscript(RRULE, startDate, BYDAY)
script thisScript
property child : missing value
property targetMonths : getTargetMonths(RRULE, startDate)
property hits : missing value
on getRecurrenceDates(anchorDate)
set my hits to {}
repeat with i from 1 to (count targetMonths)
copy anchorDate to recurrenceDate
set recurrenceDate's day to 1
set recurrenceDate's month to item i of my targetMonths
child's getRecurrenceDates(recurrenceDate)
end repeat
return my hits
end getRecurrenceDates
(* The getWeekdayDate() handler called from BYDAY is inherited from main script. *)
end script
if (BYDAY) then
set thisScript's child to BYDAYscript(RRULE, startDate, thisScript)
else
set thisScript's child to BYMONTHDAYscript(RRULE, startDate, thisScript)
end if
return thisScript
end BYMONTHscript
on BYMONTHDAYscript(RRULE, startDate, dad)
script thisScript
property parent : dad
property monthDays : getTargetDays(RRULE, startDate)
on getRecurrenceDates(anchorDate)
repeat with i from 1 to (count monthDays)
copy anchorDate to recurrenceDate
set d to item i of my monthDays
if (d > 0) then
set recurrenceDate's day to d
else
set recurrenceDate's day to 32
set recurrenceDate to recurrenceDate - ((recurrenceDate's day) - d - 1) * days
end if
if (recurrenceDate's month is anchorDate's month) then set end of dad's hits to recurrenceDate
end repeat
end getRecurrenceDates
end script
return thisScript
end BYMONTHDAYscript
on BYDAYscript(RRULE, startDate, dad)
script thisScript
property parent : dad
property weekStart : missing value
property weekdayInstances : missing value
property instanceNos : missing value
on getRecurrenceDates(anchorDate)
repeat with i from 1 to (count weekdayInstances)
set {weekdayOffset, my instanceNos} to item i of my weekdayInstances
repeat with j from 1 to (count instanceNos)
set recurrenceDate to getWeekdayDate(anchorDate, weekdayOffset, item j of my instanceNos)
if (recurrenceDate is not in my hits) then set end of my hits to recurrenceDate
end repeat
end repeat
end getRecurrenceDates
end script
set {thisScript's weekStart, thisScript's weekdayInstances} to getWeekdayStuff(RRULE, startDate)
return thisScript
end BYDAYscript
on BYWEEKNOscript(RRULE, startDate)
script thisScript
property child : missing value
property weekNos : getIntegerList(getRulePartValue(RRULE, "BYWEEKNO"))
property weekdayBaseDate : missing value -- Week start + time of start date.
property hits : missing value
on getRecurrenceDates(anchorDate)
set {weekYearStart, weekYearEnd} to getWeekYear(anchorDate)
set my hits to {}
repeat with i from 1 to (count weekNos)
set thisWeekNo to item i of my weekNos
if (thisWeekNo > 0) then
child's getRecurrenceDates(weekYearStart + (thisWeekNo - 1) * weeks)
else
child's getRecurrenceDates(weekYearEnd + (1 + thisWeekNo * weeks))
end if
end repeat
return my hits
end getRecurrenceDates
(* Get the first and last seconds of a year of weeks. ISO-style weeks of year. User-defined week start. *)
on getWeekYear(anchorDate)
-- The first week of any year contains 4th January.
copy anchorDate to Jan4
set Jan4's day to 4
set Jan4's month to January
-- Use that and the WKST day code to get the beginning of that week.
set weekYearStart to Jan4 - (Jan4 - weekdayBaseDate) mod weeks
set weekYearEnd to weekYearStart + (53 * weeks - 1)
if (weekYearEnd's day > 3) then set weekYearEnd to weekYearEnd - weeks
return {weekYearStart, weekYearEnd}
end getWeekYear
(* Get the date of a given instance of a given weekday before or after a given date. (Called by BYDAY script object.) *)
on getWeekdayDate(givenDate, weekdayOffset, instanceNo)
if (instanceNo > 0) then
return givenDate + (weekdayOffset + (instanceNo - 1) * weeks)
else
return givenDate + (weekdayOffset + (instanceNo + 1) * weeks)
end if
end getWeekdayDate
end script
set thisScript's child to BYDAYscript(RRULE, startDate, thisScript)
set thisScript's weekdayBaseDate to (thisScript's child's weekStart) + (startDate's time)
return thisScript
end BYWEEKNOscript
on BYYEARDAYscript(RRULE, startDate)
script thisScript
property yearDays : getTargetDays(RRULE, startDate)
property hits : missing value
on getRecurrenceDates(anchorDate)
set refYear to anchorDate's year
set hits to {}
repeat with i from 1 to (count yearDays)
copy anchorDate to recurrenceDate
set targetDay to item i of my yearDays
if (targetDay > 0) then
set recurrenceDate's month to January
set recurrenceDate to recurrenceDate + (targetDay - (recurrenceDate's day)) * days
else
set recurrenceDate's month to December
set recurrenceDate to recurrenceDate + (32 + targetDay - (recurrenceDate's day)) * days
end if
if (recurrenceDate's year is refYear) then set end of my hits to recurrenceDate
end repeat
return hits
end getRecurrenceDates
end script
return thisScript
end BYYEARDAYscript
(*** Odd jobs. ***)
(* Read a given rule part value from a recurrence rule. *)
on getRulePartValue(RRULE, rulePartKey)
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to rulePartKey & "="
set rulePartValue to text item 2 of RRULE
set AppleScript's text item delimiters to ";"
set rulePartValue to text item 1 of rulePartValue
set AppleScript's text item delimiters to astid
return rulePartValue
end getRulePartValue
(* Get an "UNTIL" value as an AppleScript date or default to 31st December 9999 23:59:59 *)
on getExpiryDate(RRULE, eventTZ)
if (RRULE contains "UNTIL") then return recordToNewTZDate(getDateAndTZ(":" & getRulePartValue(RRULE, "UNTIL")), eventTZ)
return «data isot393939392D31322D33315432333A35393A3539» as date -- date "Friday 31 December 9999 23:59:59"
end getExpiryDate
(* Get an "INTERVAL" value or default to 1. *)
on getInterval(RRULE)
if (RRULE contains "INTERVAL") then return getRulePartValue(RRULE, "INTERVAL") as integer
return 1
end getInterval
(* Get a "COUNT" value or default to an arbitrarily high number. *)
on getMaxExpressions(RRULE)
if (RRULE contains "COUNT") then
getRulePartValue(RRULE, "COUNT") as integer
-- Sometimes temporarily get "COUNT=-1" instead of no COUNT. :\ 1.5.5 bug?
if (result > -1) then return result
end if
return 150000000
end getMaxExpressions
(* Return comma-delimited items from a rule part value as an AppleScript list.. *)
on getBYlist(rulePartValue)
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ","
set theList to rulePartValue's text items
set AppleScript's text item delimiters to astid
return theList
end getBYlist
(* Return comma-delimited numbers from a rule part as a list of AppleScript integers. *)
on getIntegerList(rulePartValue)
set theList to getBYlist(rulePartValue)
repeat with thisItem in theList
set thisItem's contents to thisItem as integer
end repeat
return theList
end getIntegerList
(* Derive a list of AppleScript months from the numbers in a "BYMONTH" rule part or default to the month of the event's start date. *)
on getTargetMonths(RRULE, startDate)
if (RRULE contains "BYMONTH=") then
set targetMonths to getBYlist(getRulePartValue(RRULE, "BYMONTH"))
set monthList to {January, February, March, April, May, June, July, August, September, October, November, December}
repeat with monthNo in targetMonths
set monthNo's contents to item monthNo 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 "BYYEARDAY" or "BYMONTHDAY" rule part (using that priority) or default to the day of the event start date. *)
on getTargetDays(RRULE, startDate)
if (RRULE contains "BYMONTHDAY=") then
return getIntegerList(getRulePartValue(RRULE, "BYMONTHDAY"))
else if (RRULE contains "BYYEARDAY=") then
return getIntegerList(getRulePartValue(RRULE, "BYYEARDAY"))
else
return {startDate's day}
end if
end getTargetDays
(* Get the first and last seconds in a year of weeks. ISO-style weeks of year. User-defined week start. *)
on getWeekYear(RRULE, startDate)
-- The first week of any year contains 4th January.
copy startDate to Jan4
set Jan4's day to 4
set Jan4's month to January
-- Use that and the WKST day code to get the beginning of that week.
set anchorDate to Monday10000106 + getWKSToffset(RRULE)
set weekYearStart to Jan4 - (Jan4 - anchorDate) mod weeks
set weekYearEnd to weekYearStart + (53 * weeks - 1)
if (weekYearEnd's day > 3) then set weekYearEnd to weekYearEnd - weeks
return {weekYearStart, weekYearEnd}
end getWeekYear
(* Analyse the weekday(s) implied or specified in the RRULE and return the results in a convenient form, ie.:
{start of start date's week, {{offset of weekday from week start, {weekday instance number(s)}} [, {., {.}}, .] }} *)
on getWeekdayStuff(RRULE, startDate)
set weekdayCodes to "MOTUWETHFRSASU"
set WEEKLYorBYWEEKNO to (RRULE contains "WEEK")
-- Offset of the implied or specified week-start day from Monday.
if (WEEKLYorBYWEEKNO) then -- Week start relevant. Monday if WKST omitted.
set WKSToffset to getWKSToffset(RRULE)
else -- Week start not relevant. 0 used for convenience.
set WKSToffset to 0
end if
set weekStart to startDate - (startDate - Monday10000106) mod weeks + WKSToffset
if (weekStart comes after startDate) then set weekStart to weekStart - weeks
-- Get the weekday parameter(s), eg. {"1WE", "2TH"}.
if (RRULE contains "BYDAY=") then -- Weekday(s) specified.
set weekdayEntries to getBYlist(getRulePartValue(RRULE, "BYDAY"))
else -- Use the start date's weekday.
set codeOffset to (startDate - weekStart) div days * 2 + 1
set weekdayEntries to {"1" & text codeOffset thru (codeOffset + 1) of weekdayCodes}
end if
-- Convert to offset-from-week-start/instance-list pairs.
repeat with thisEntry in weekdayEntries
set weekdayOffset to ((offset of (text -2 thru -1 of thisEntry) in weekdayCodes) div 2 * days + weeks - WKSToffset) mod weeks
if (WEEKLYorBYWEEKNO) then -- Only one instance of weekday in a week.
set instanceNos to {1}
else if ((count thisEntry) > 2) then -- Instance number included.
set instanceNos to {(text 1 thru -3 of thisEntry) as integer}
else -- No instance number means EVERY instance in the month.
set instanceNos to {1, 2, 3, 4, -1}
end if
set thisEntry's contents to {weekdayOffset, instanceNos}
end repeat
return {weekStart, weekdayEntries}
end getWeekdayStuff
(* Get the offset in seconds of the specified week-start from Monday, or default to 0 (for Monday itself). *)
on getWKSToffset(RRULE)
if (RRULE contains "WKST=") then return (offset of getRulePartValue(RRULE, "WKST") in "MOTUWETHFRSASU") div 2 * days
return 0
end getWKSToffset
(* Get a date that's m calendar months after (or before, if m is negative) the input date. *)
on addMonths(oldDate, m)
copy oldDate to newDate
set {y, m} to {m div 12, m mod 12}
if (m < 0) then set {y, m} to {y - 1, m + 12}
set newDate's year to (newDate's year) + y
set d to oldDate's day
if (m > 0) then tell newDate + (32 * m - d) * days to set newDate to it + (d - (its day)) * days
if (newDate's day is not d) 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, weekdayOffset, instanceNo)
-- A date in the past known to have the required weekday.
set anchorDate to Monday10000106 + ((givenDate's time) + weekdayOffset)
-- Get the last day of the seven-day period covered by the instance number in the current month.
if (instanceNo > 0) then
copy givenDate to periodEnd
set periodEnd's day to instanceNo * 7 -- 7th, 14th, 21st, or 28th of month.
else
tell givenDate to tell it + (32 - (its day)) * days to set periodEnd to it - ((its day) - (instanceNo + 1) * 7) * days
end if
-- Round down to an exact number of weeks after the known-weekday date.
return periodEnd - (periodEnd - anchorDate) mod weeks
end getWeekdayDate
(* Insertion sort. By kai? *)
on sort(l)
set lenPlus1 to (count l) + 1
if (lenPlus1 > 2) then
repeat with i from lenPlus1 - 2 to 1 by -1
set v to item i of l
repeat with j from i + 1 to lenPlus1
if (j < lenPlus1) then
set w to item j of l
if (v > w) then
set item (j - 1) of l to w
else
exit repeat
end if
end if
end repeat
set item (j - 1) of l to v
end repeat
end if
end sort
(* Get a recurrence instance's expression date. Same as the recurrence date unless the instance has been moved.
Now also return the instance duration for "straddle" testing. *)
on getExpressionDate(recurrenceDate, theData)
set detachmentDates to theData's |detachment dates|
if (recurrenceDate is in detachmentDates) then
repeat with i from 1 to (count detachmentDates)
if (item i of detachmentDates is recurrenceDate) then return {|start date| of item i of theData's |detached event data|, duration of item i of theData's |detached event data|}
end repeat
else
return {recurrenceDate, duration of theData's |root event data|}
end if
end getExpressionDate
(* Construct a data record for the found recurrence instance. *)
on getInstanceData(theData, recurrenceDate)
if (recurrenceDate is missing value) then return missing value -- No suitable recurrence instance.
set {|event TZ|:eventTZ, |computer TZ|:computerTZ, |detachment dates|:detachmentDates, |root event data|:rootEventData} to theData
if (recurrenceDate is in detachmentDates) then -- Detached event.
repeat with i from 1 to (count detachmentDates)
if (item i of detachmentDates is recurrenceDate) then
set instanceData to (item i of theData's |detached event data|) & {|recurrence date|:recurrenceDate, |event zone|:eventTZ} & rootEventData
exit repeat
end if
end repeat
else -- Main-sequence recurrence.
set instanceData to {|start date|:recurrenceDate, |recurrence date|:recurrenceDate, |event zone|:eventTZ} & rootEventData
end if
-- Transpose the instance's date details to the computer's time zone. (Stamp date already done in getAllData().)
set instanceData's |start date| to TZtoTZ(instanceData's |start date|, eventTZ, computerTZ)
set instanceData's |end date| to TZtoTZ((instanceData's |start date|) + (instanceData's duration), computerTZ, computerTZ)
if (instanceData's |RRULEs|) is {} then
-- A recurrence date isn't relevant to a non-recurring event.
set instanceData's |recurrence date| to missing value
else
set instanceData's |recurrence date| to TZtoTZ(recurrenceDate, eventTZ, computerTZ)
end if
return instanceData
end getInstanceData
(*** Handlers to get the data from the calendar file. ***)
(* Locate and parse the relevant calendar file, given the event UID. *)
on getAllData(eventUID, checkDate)
set iCalVersion to getiCalVersion()
if (iCalVersion begins with "1.") then
set sc to "find -f ~/Library/Calendars/ \\( -name \"*.ics\" \\)"
else if (iCalVersion begins with "2.") then
set sc to "find -f ~/Library/'Application Support'/iCal/Sources/ \\( -path \"*.calendar/corestorage.ics\" \\)"
else -- Assume a version using the same .ics storage locations as iCal 3.0 and 4.0.
set sc to "find -f ~/Library/Calendars/ \\( -path \"*.calendar/Events/" & eventUID & ".ics\" \\)"
end if
set icsPOSIXPaths to paragraphs of (do shell script sc)
set computerTZ to (do shell script ("readlink 'etc/localtime' | sed 's|/usr/share/zoneinfo/||'"))
set UIDline to ("UID:" & eventUID) as Unicode text
set astid to AppleScript's text item delimiters
-- This repeat was originally to find and parse the event's data in the calendar file(s) returned with iCals 1.5 and 2.0, but it also works for the single event file returned with iCals 3.0 and 4.0.
repeat with i from 1 to (count icsPOSIXPaths) -- Check each calendar file for the event UID.
set thisText to (read (item i of icsPOSIXPaths as POSIX file) as «class utf8»)
if (thisText contains UIDline) then -- The event UID line's in this file.
set definiteHit to false -- . though conceivably as text in some other event!
-- Extract the computer's time zone from the POSIX path pointed to by the symbolic link "/etc/localtime" and set up a few defaults and start values.
set theData to {|computer TZ|:computerTZ, |event TZ|:missing value, |root event data|:missing value, |excluded dates|:{}, |detachment dates|:{}, |detached event data|:{}}
set RRULEs to {}
set attendees to {}
-- Split the file text into blocks which begin with a paragraph end and the data for each event.
set AppleScript's text item delimiters to "BEGIN:VEVENT" as Unicode text
set eventBlocks to thisText's text items 2 thru -1
set AppleScript's text item delimiters to ":" as Unicode text
repeat with j from 1 to (count eventBlocks)
set thisBlock to item j of eventBlocks
if (thisBlock contains UIDline) then -- The UID definition's in this block.
set propertyLines to unsplit(paragraphs 2 thru -1 of thisBlock)
if (propertyLines contains UIDline) then -- . and occupies an entire line.
-- Extract the data from each line of the VEVENT block.
-- Dates are returned as date/time-zone records to be sorted out when they're all in.
set definiteHit to true
set detachmentDate to missing value
set duration to missing value
set eventData to {|start date|:missing value, |end date|:missing value, duration:missing value, |allday event|:false, uid:eventUID}
set parsingAlarm to false
repeat with k from 1 to (count propertyLines) -- Actually stops at "END:VEVENT".
set thisLine to item k of propertyLines
set thisValue to text from text item 2 to text item -1 of thisLine
if (parsingAlarm) then
if (thisLine begins with "ACTION") then
set alarmType to thisValue
else if (thisLine begins with "TRIGGER") then
set anAlarm's |trigger interval| to isoToDuration(thisValue) div minutes
else if (thisLine begins with "ATTACH") then
set |attachment| to thisValue
else if (thisLine is "END:VALARM") then
if (alarmType is "DISPLAY") then
set eventData to eventData & {|display alarms|:{}}
set end of eventData's |display alarms| to anAlarm
else if (alarmType is "EMAIL") then
set eventData to eventData & {|mail alarms|:{}}
set end of eventData's |mail alarms| to anAlarm
else if (alarmType is "PROCEDURE") then
set anAlarm to anAlarm & {filepath:|attachment|}
set eventData to eventData & {|open file alarms|:{}}
set end of eventData's |open file alarms| to anAlarm
else if (alarmType is "AUDIO") then
set anAlarm to anAlarm & {|sound name|:|attachment|}
set eventData to eventData & {|sound alarms|:{}}
set end of eventData's |sound alarms| to anAlarm
end if
set parsingAlarm to false
end if
else if (thisLine begins with "SUMMARY") then
set eventData to eventData & {summary:unescape(thisValue)}
else if (thisLine begins with "LOCATION") then
set eventData to eventData & {location:unescape(thisValue)}
else if (thisLine begins with "DTSTART") then
set eventData's |start date| to getDateAndTZ(thisLine)
-- If no TZ, this is an all-day event. Use the computer time zone, as per iCal 2.0.5.
if (result's TZ is missing value) then
set eventData's |allday event| to true
set TZ of eventData's |start date| to theData's |computer TZ|
end if
else if (thisLine begins with "DTEND") then
set eventData's |end date| to getDateAndTZ(thisLine)
else if (thisLine begins with "DTSTAMP") then
-- The stamp date can be matched to the computer's time zone immediately.
set eventData to eventData & {|stamp date|:recordToNewTZDate(getDateAndTZ(thisLine), theData's |computer TZ|)}
else if (thisLine begins with "SEQUENCE") then
set eventData to eventData & {sequence:thisValue as integer}
else if (thisLine begins with "DURATION") then
set eventData's duration to isoToDuration(thisValue)
else if (thisLine begins with "RRULE") then
set end of RRULEs to thisValue
else if (thisLine begins with "EXDATE") then
set end of theData's |excluded dates| to getDateAndTZ(thisLine)
else if (thisLine begins with "RECURRENCE-ID") then
set detachmentDate to getDateAndTZ(thisLine)
else if (thisLine begins with "DESCRIPTION") then
set eventData to eventData & {|description|:unescape(thisValue)}
else if (thisLine begins with "STATUS") then
set eventData to eventData & {status:thisValue}
else if (thisLine begins with "URL") then
set eventData to eventData & {|url|:thisValue}
else if (thisLine begins with "ATTENDEE") then
set end of attendees to getAttendee(thisLine)
else if (thisLine is "BEGIN:VALARM") then
set anAlarm to {|trigger interval|:0}
set parsingAlarm to true
else if (thisLine is "END:VEVENT") then
exit repeat
end if
end repeat
-- If a duration's not specified, an end date will be. Prefer a duration.
if (eventData's duration is missing value) then set eventData's duration to (|date| of eventData's |end date|) - (get |date| of eventData's |start date|)
if (detachmentDate is missing value) then -- Root event.
-- The recurrence will be based on the root event's start date and time zone.
set theData's |event TZ| to TZ of eventData's |start date|
set eventData's |start date| to |date| of eventData's |start date|
set theData's |root event data| to eventData & {summary:"" as Unicode text, location:missing value, |description|:missing value, status:"NONE" as Unicode text, |url|:missing value, |RRULEs|:RRULEs, attendees:attendees, |display alarms|:{}, |mail alarms|:{}, |open file alarms|:{}, |sound alarms|:{}}
else -- Detached event.
set end of theData's |detachment dates| to detachmentDate
set end of theData's |detached event data| to eventData
end if
end if
end if
end repeat
if (definiteHit) then exit repeat -- No need to check any more files.
end if
end repeat
set AppleScript's text item delimiters to astid
-- Replace any date/timezone records in |excluded dates| or |detachment dates| with dates in the root event's time zone.
set eventTZ to theData's |event TZ|
set recurrenceTime to time of theData's |root event data|'s |start date|
repeat with flaggedDates in {theData's |excluded dates|, theData's |detachment dates|}
repeat with i from 1 to (count flaggedDates)
-- Each "flagged date" is a date/TZ record representing an instance on the event's recurrence sequence whose expression has been either deleted or modified in some way. We just need a date in the root event's timezone.
set eventTZDate to recordToNewTZDate(item i of flaggedDates, eventTZ)
-- The time should be the same as that of the root event. If it's different (by up to an hour) because of a clocks-forward discontinuity, standardise it for the script's convenience.
if (eventTZDate's time is not recurrenceTime) then
set diff to (eventTZDate's time) - recurrenceTime
if (diff > hours) then
set diff to diff - days
else if (diff < -hours) then
set diff to diff + days
end if
set eventTZDate to eventTZDate - diff
end if
set item i of flaggedDates to eventTZDate
end repeat
end repeat
-- Do the same (without the clocks-forward adjustment) with the |start date| values of any detached events. These may or may not be the same as the recurrence sequence dates.
repeat with thisItem in theData's |detached event data|
set thisItem's |start date| to recordToNewTZDate(thisItem's |start date|, eventTZ)
end repeat
return theData
end getAllData
(* Get the iCal or Calendar version from its application file, which should work with all versions and associated OSs. *)
on getiCalVersion()
tell application "Finder" to set iCalPath to (application file id "wrbt") as text
try
return (version of (info for file iCalPath)) as text
on error
tell application "System Events" to return (version of file iCalPath) as text
end try
end getiCalVersion
(* Rejoin any split lines extracted from a VEVENT block. *)
on unsplit(theLines)
set i to 1
repeat with j from 2 to (count theLines)
set thisLine to item j of theLines
if ((thisLine begins with space) or (thisLine begins with tab)) then -- Continuation lines begin with a single white space.
try
set item i of theLines to item i of theLines & text 2 thru -1 of thisLine
end try
set item j of theLines to missing value
else
set i to j
end if
end repeat
return theLines's every Unicode text
end unsplit
(* Remove any unwanted escaping from text data. *)
on unescape(thisText)
if (thisText contains "\\") then
set thisText to replaceText("\\,", ",", thisText)
set thisText to replaceText("\\;", ";", thisText)
set thisText to replaceText("\"", "\\\"", thisText) -- But actually add escaping to quotes!
set thisText to (run script ("\"" & thisText & "\"")) as Unicode text
end if
return thisText
end unescape
(* Text substitution. *)
on replaceText(original, replacement, source)
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to original as Unicode text
set source to source's text items
set AppleScript's text item delimiters to replacement as Unicode text
set source to source as Unicode text
set AppleScript's text item delimiters to astid
return source
end replaceText
(* Return an AppleScript date and a time zone id from a VEVENT date property line. *)
on getDateAndTZ(thisLine)
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ":" as Unicode text
set {lineBeginning, isot} to thisLine's text items
set AppleScript's text item delimiters to astid
set ASDate to isotToDate(isot)
set TZ to missing value
if (thisLine ends with "Z") then
set TZ to "GMT"
else if (lineBeginning contains ";") then
set param to beginning of getLineParams(lineBeginning)
if (param begins with "TZID=") then set TZ to text 6 thru -1 of param
else
set TZ to missing value
end if
return {|date|:ASDate, TZ:TZ}
end getDateAndTZ
(* Convert an ISO-format date string to an AppleScript date. *)
on isotToDate(isot)
set n to (text 1 thru 8 of isot) as integer
copy Monday10000106 to ASDate
set ASDate's year to n div 10000
set ASDate's month to item (n mod 10000 div 100) of {January, February, March, April, May, June, July, August, September, October, November, December}
set ASDate's day to n mod 100
if ((count isot) > 8) then
set n to (text 10 thru 15 of isot) as integer
set ASDate's time to n div 10000 * hours + n mod 10000 div 100 * minutes + n mod 100
end if
return ASDate
end isotToDate
(* Convert an ISO-format duration string to seconds (as integer). *)
on isoToDuration(ISO8601)
set prefices to "PT" as Unicode text
set suffices to "WDHMS" as Unicode text
set duration to 0
set i to (count ISO8601)
repeat with j from 1 to i
set c to character j of ISO8601
if (c is in prefices) then
set i to j + 1
else if (c is in suffices) then
if (c is "W") then
set t to weeks
else if (c is "D") then
set t to days
else if (c is "H") then
set t to hours
else if (c is "M") then
set t to minutes
else -- c is "S".
set t to 1
end if
set duration to duration + t * (text i thru (j - 1) of ISO8601)
set i to j + 1
end if
end repeat
if (ISO8601 begins with "-") then set duration to -duration
return duration
end isoToDuration
(* Parse an ATTENDEE line for display name, email, and participation status. *)
on getAttendee(theLine)
set UNKNOWN to "UNKNOWN" as Unicode text
set anAttendee to {|display name|:UNKNOWN, email:UNKNOWN, |participation status|:UNKNOWN}
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ":" as Unicode text
set lineParts to theLine's text items
set AppleScript's text item delimiters to astid
if ((count lineParts) is 3) then set anAttendee's email to end of lineParts
set params to getLineParams(beginning of lineParts)
repeat with thisParam in params
if (thisParam begins with "CN") then
set anAttendee's |display name| to text from word 3 to word -1 of thisParam
else if (thisParam begins with "PARTSTAT") then
set anAttendee's |participation status| to text 10 thru -1 of thisParam
end if
end repeat
return anAttendee
end getAttendee
(* Return ";"-delimited parameter(s) from a given line beginning. *)
on getLineParams(linePart)
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ";" as Unicode text
set params to rest of linePart's text items
set AppleScript's text item delimiters to astid
return params
end getLineParams
(*** Time zone handlers. ***)
(* Take a record containing an AppleScript date and a time zone ID and return the equivalent date in another time zone. *)
on recordToNewTZDate(dateTZRecord, targetTZ)
set {|date|:ASDate, TZ:dateTZ} to dateTZRecord
if (dateTZ is targetTZ) then return ASDate
return TZtoTZ(ASDate, dateTZ, targetTZ)
end recordToNewTZDate
(* Transpose an AppleScript date/time from one time zone to another (or correct a date in the same zone when the clocks go forward). *)
on TZtoTZ(TZ1date, TZ1, TZ2)
global AppleScript_2
if (AppleScript_2) then
-- Get the Unix era time for the date in time zone 1; get the date for the time in time zone 2 as «class isot»; coerce to AS date.
return (do shell script ("eraT=$(TZ=" & TZ1 & " date -jf '%Y-%m-%dT%H:%M:%S' '" & (TZ1date as «class isot» as string) & "' '+%s') ; TZ=" & TZ2 & " date -r \"$eraT\" '+%Y-%m-%dT%H:%M:%S'") as «class isot») as date
else
-- The old laborious workaround going through GMT.
if (TZ1 is "GMT") then
set GMTDate to TZ1date
else
set GMTDate to TZtoGMT(TZ1date, TZ1)
end if
if (TZ2 is "GMT") then return GMTDate
return GMTtoTZ(GMTDate, TZ2)
end if
end TZtoTZ
(* Transpose an AppleScript date/time from the given time zone to GMT. *)
on TZtoGMT(TZDate, TZ)
-- The difference between TZDate when it's local and the GMT date we want is usually
-- the same as the difference between the local date when TZDate is GMT and TZDate itself .
set GMTDate to TZDate - (GMTtoTZ(TZDate, TZ) - TZDate)
-- . but not around the time the clocks go forward. If the GMT obtained doesn't reciprocate to TZDate,
-- shift to a nearby local date where the above DOES work, get a new GMT, unshift it by the same amount.
set testDate to GMTtoTZ(GMTDate, TZ)
if (testDate is not TZDate) then
if (GMTDate > testDate) then -- "Clocks forward" is towards GMT.
set shift to GMTDate - testDate
else -- "Clocks forward" is away from GMT.
set shift to -days
end if
set nearbyDate to TZDate + shift
set GMTDate to nearbyDate - (GMTtoTZ(nearbyDate, TZ) - nearbyDate) - shift
end if
return GMTDate
end TZtoGMT
(* Transpose an AppleScript date/time from GMT to the given time zone. Thanks to Yvan Koenig for pointing out a number format dependency in the original version of this handler. *)
on GMTtoTZ(GMTDate, TZ)
-- Subtract date "Thursday 1 January 1970 00:00:00" from the GMT date. Result in seconds, as text.
set eraTime to GMTDate - («data isot313937302D30312D3031» as date)
if (eraTime > 99999999) then
set eraTime to (eraTime div 100000000 as text) & text 2 thru 9 of (100000000 + eraTime mod 100000000 as integer as text)
else if (eraTime < -99999999) then
set eraTime to (eraTime div 100000000 as text) & text 3 thru 10 of (-100000000 + eraTime mod 100000000 as integer as text)
else
set eraTime to eraTime as text
end if
return isotToDate(do shell script ("TZ='" & TZ & "' /bin/date -r " & eraTime & " +%Y%m%dT%H%M%S"))
end GMTtoTZ
(* tell application "Calendar" to set eventUID to uid of event 1 of calendar 4
set checkDate to (current date)
-- Get the event's next expression starting at or after the check date.
getNextExpression from checkDate for eventUID without straddle
-- Get the event's next expression starting at or after the check date or in progress at the time.
getNextExpression from checkDate for eventUID with straddle *)
Edit: Main handler and some variables renamed for consistency, bug fixes, adaptation for Mountain Lion (iCal now called Calendar, new date bugs in ML), faster time-zone transposition code for systems which can use it, the |recurrence date| for any non-repeating event returned is now ‘missing value’.