Calendar: Incrementing Name property for recurring events

Hi all,

Fairly new to scripting, in the past couple months I’ve learned a little VBA, but I’d never used Applescript before last night, so I may be missing something obvious.

Anyway, I was trying to add the anniversaries from my Contacts to my Calendar, basically mimicking the subscribed Birthdays calendar. That went fine; if I’d found this site a little earlier I’d probably have saved myself some time, but oh well, got some practice with Applescript.

The issue was that I was trying to make each event say which anniversary it was, e.g. “John Smith’s 2nd anniversary.” Code below.


tell application "Calendar"
	if (exists calendar "Anniversaries") is false then
		make new calendar with properties {name:"Anniversaries"}
	end if
	delete every event of calendar "Anniversaries"
end tell

tell application "Contacts"
	set every_marriedperson to people where label of custom dates contains "anniversary"
	repeat with this_marriedperson in every_marriedperson
		set every_customDate to custom dates of this_marriedperson
		repeat with this_customDate in every_customDate
			if label of this_customDate = "anniversary" then
				set wedding_date to value of this_customDate
			end if
		end repeat
		if (exists year of wedding_date) is false then
			set which_anniversary to ""
		else if ((year of (current date)) - (year of wedding_date)) ends with 1 then
			set which_anniversary to ((year of (current date)) - (year of wedding_date)) & "st "
		else if ((year of (current date)) - (year of wedding_date)) ends with 2 then
			set which_anniversary to ((year of (current date)) - (year of wedding_date)) & "nd "
		else if ((year of (current date)) - (year of wedding_date)) ends with 3 then
			set which_anniversary to ((year of (current date)) - (year of wedding_date)) & "rd "
		else
			set which_anniversary to ((year of (current date)) - (year of wedding_date)) & "th "
		end if
		set this_name to the name of this_marriedperson
		tell application "Calendar" to tell calendar "Anniversaries" to make new event with properties {summary:this_name & "'s " & which_anniversary & "Anniversary", start date:wedding_date, end date:wedding_date, recurrence:"FREQ=YEARLY;INTERVAL=1", allday event:true}
	end repeat
end tell

And this did work… for 2013. In 2063, though, it will still say “John Smith’s 2nd Anniversary” instead of “52nd Anniversary.”

I do see the problem. I’m calculating by subtracting the wedding year from the current year, so I’m getting a constant. I ended up spending a couple hours last night trying to mess with the event properties directly instead, but I couldn’t figure it out. Are there ways to update the properties as the years go up?

Now, I could probably use whatever Applescript’s equivalent of a for-next loop is to create separate events for each person for the next 20 or 30 years-- they wouldn’t be recurring/linked, but since I could just run the script again for any updates, that wouldn’t be much of a problem. However, that’s probably going to be slower, and it doesn’t seem so neat to me. Is incrementing recurrences like I originally wanted even possible?

Hi,

the operator ends with works only with text objects or lists
If you want to check the last digit numerically use the modulus operator

((year of (current date)) - (year of wedding_date)) mod 10 =  1 

Cool, thanks. It actually worked for single digit years, so I didn’t even notice that. When I tried a double digit anniversary I saw what you were talking about.

Edit: So, fixed that ordinal issue. Also fixed another issue, got rid of the “if (exists year of wedding date) is false” bit, which didn’t work the way I wanted it to since no year just defaults to 1604. Current script:


tell application "Calendar"
	if (exists calendar "Anniversaries") is false then
		make new calendar with properties {name:"Anniversaries"}
	end if
	delete every event of calendar "Anniversaries"
end tell

tell application "Contacts"
	set every_marriedperson to people where label of custom dates contains "anniversary"
	repeat with this_marriedperson in every_marriedperson
		set every_customDate to custom dates of this_marriedperson
		repeat with this_customDate in every_customDate
			if label of this_customDate = "anniversary" then
				set wedding_date to value of this_customDate
			end if
		end repeat
		if year of wedding_date = 1604 then
			set which_anniversary to "Anniversary"
		else
			set annDiff to ((year of (current date)) - (year of wedding_date))
			if annDiff = 0 then
				set which_anniversary to "Wedding"
			else
				set lastDigit to (annDiff mod 100) mod 10
				if lastDigit < 4 and not (annDiff > 10 and annDiff < 14) then
					set ordinal to item (lastDigit + 1) of {"0", "st", "nd", "rd"}
				else
					set ordinal to "th"
				end if
				set which_anniversary to annDiff & ordinal & " Anniversary"
			end if
		end if
		set this_name to the name of this_marriedperson
		tell application "Calendar" to tell calendar "Anniversaries" to make new event with properties {sequence:annDiff, summary:this_name & "'s " & which_anniversary, start date:wedding_date, end date:wedding_date, recurrence:"FREQ=YEARLY;INTERVAL=1", allday event:true}
	end repeat
end tell

For the original issue, am I off-track in thinking I might be able to set something up with the sequence property?

So I did finally get around to making a version of this with a loop and a counter. Like I thought, it takes about five minutes to run. You run it twice and you have to kill it for a few minutes because you can’t get it to wait before it deletes all the old events. You’ve also got a hard end date; I made mine go for 75 years, which should be safe enough, but it’s not perfect. Finally, they aren’t linked, so you can’t just delete one and then delete all future events. I know there’s got to be a way to automatically increment the year because the Birthdays calendar does it, but I’m missing something and I don’t know what.


on ordinal(y)
	set lastDigit to y mod 10
	if (0 < lastDigit and lastDigit < 4) and not (10 < y and y < 14) then
		return item (lastDigit) of {"st", "nd", "rd"}
	else
		return "th"
	end if
end ordinal

tell application "Calendar"
	if (exists calendar "Anniversaries") is false then
		make new calendar with properties {name:"Anniversaries"}
	end if
	delete every event of calendar "Anniversaries"
end tell

tell application "Contacts"
	set every_marriedPerson to people where label of custom dates contains "anniversary"
	repeat with this_marriedPerson in every_marriedPerson
		set every_customDate to custom dates of this_marriedPerson
		repeat with this_customDate in every_customDate
			if label of this_customDate = "anniversary" then
				set wedding_date to value of this_customDate as date
			end if
		end repeat
		set this_name to the name of this_marriedPerson & "'s "
		set ABURL to "addressbook://" & id of this_marriedPerson
		if year of wedding_date = 1604 then
			tell application "Calendar" to tell calendar "Anniversaries" to make new event with properties {summary:this_name & "Anniversary", sequence:0, start date:wedding_date, end date:wedding_date, url:ABURL, recurrence:"FREQ=YEARLY;INTERVAL=1", allday event:true}
		else
			tell application "Calendar"
				tell calendar "Anniversaries" to make new event with properties {summary:this_name & "Wedding", sequence:0, start date:wedding_date, end date:wedding_date, url:ABURL, allday event:true}
				set annDate to current date
				set annmonth to month of wedding_date as integer as string
				set month of annDate to annmonth
				set annday to day of wedding_date as string
				set day of annDate to annday
				set counter to 1
				repeat until counter = 75
					set annyear to ((year of wedding_date) + counter) as string
					set year of annDate to annyear
					tell calendar "Anniversaries" to make new event with properties {summary:this_name & counter & my ordinal(counter) & " Anniversary", start date:annDate, end date:annDate, url:ABURL, allday event:true}
					set counter to counter + 1
				end repeat
			end tell
		end if
	end repeat
end tell

Hi.

There are various strategies for speed when scripting applications, whose efficacy varies with the application.

¢ Don’t give an application too much to think about. ‘whose’ filters can be quite expensive timewise.
¢ Don’t send individual commands to an application if it can do or return what you want in bulk.
¢ Don’t nest ‘tell’ statements for one application inside those for another.

There’s not much you can do to speed up the Calendar (formerly iCal) part of your script. There’s no automatic increment of recurring-event summaries in an ordinary calendar. Your anniversary events all have to be created individually. (Of course, if you’re only interested in anniversaries which occur after you’ve set up the calendar, you could just create the events which occur on or after that date.)

Rather than delete all the existing events in the calendar, you could simply delete the calendar ” unless there was some reason why you didn’t want to.

With Contacts (formerly Address Book), since you’re only getting information from it, you could give it just one command to dump all its names, ids, and custom address details to your own variables and let the script decide what’s relevant. This saves Contacts having to think about whether or not the labels of people’s custom addresses contain “anniversary” and cuts the communication time between it and the script to just the overhead for that one command.

Unfortunately, except for a couple of “test persons”, I don’t keep anniversaries and birthdays in Address Book, so I can’t boast how much faster the script below is than yours. But hopefully you should see a slight improvement.

Edit: The script will need a modification if any of your contacts were married on 29th February.

on ordinal(y)
	set lastDigit to y mod 10
	if (0 < lastDigit and lastDigit < 4) and not (10 < y and y < 14) then
		return item (lastDigit) of {"st", "nd", "rd"}
	else
		return "th"
	end if
end ordinal

on populate_anniversaries()
	tell application "Calendar"
		if (exists calendar "Anniversaries") then
			delete calendar "Anniversaries"
		end if
		make new calendar with properties {name:"Anniversaries"}
	end tell
	
	-- Dump all Contacts's names, ids, and custom date labels and values into AppleScript lists, which will all correspond in 'people' order.	
	tell application "Contacts"
		set {all_names, all_ids, {all_customdate_labels, all_customdate_values}} to {name, id, {label, value} of custom dates} of people
	end tell
	-- Finished with Contacts.
	
	-- Parse the lists rather than sending individual commands to Contacts to retrieve each morsel of data from its database.
	repeat with person_idx from 1 to (count all_names)
		-- Each item in all_customdate_labels is a list of a person's custom date labels.
		if (item person_idx of all_customdate_labels contains "anniversary") then
			-- If one of the labels is "anniversary", find its position in the label list and use that to index the corresponding value in the person's custom date value list.
			set marriedpersons_labels to item person_idx of all_customdate_labels
			repeat with customdate_idx from 1 to (count marriedpersons_labels)
				if (item customdate_idx of marriedpersons_labels is "anniversary") then
					set wedding_date to item customdate_idx of item person_idx of all_customdate_values
					exit repeat
				end if
			end repeat
			-- Also get the corresponding name and URL for this person.
			set this_name to item person_idx of all_names & "'s "
			set ABURL to "addressbook://" & item person_idx of all_ids
			
			tell application "Calendar"
				tell calendar "Anniversaries"
					-- Create this person's wedding day event .
					make new event at end of events with properties {summary:this_name & "Wedding", start date:wedding_date, end date:wedding_date, url:ABURL, allday event:true}
					-- . and the first 75 anniversary events.
					set annDate to wedding_date -- Same date object, but it doesn't matter here.
					set wedding_year to wedding_date's year
					repeat with counter from 1 to 75
						set annDate's year to wedding_year + counter
						make new event at end of events with properties {summary:this_name & counter & my ordinal(counter) & " Anniversary", start date:annDate, end date:annDate, url:ABURL, allday event:true}
					end repeat
				end tell
			end tell
		end if
	end repeat
end populate_anniversaries

populate_anniversaries()

This has been bothering me for the past few days. I’ve now cooked up the sprawling hack below, which sets up anniversaries as modified recurrences of wedding day events and seems reasonably fast. It works with Address Book and iCal in Snow Leopard, but I’ve no idea if it works with their current incarnations: Contacts, Calendar, and Mountain Lion.

The modus operandi is to create wedding day events as in the script above, but make them annually recurring. iCal/Calendar is then quit and the .ics files for the just-created events are read and used to derive an iCalendar specification for the entire calendar which includes the required recurrence variations. This is written to an .ics file on the desktop which is in turn imported to the new calendar in iCal/Calendar. It would be less fuss just to edit the original event files, but iCal ignores any changes to them, so it must store the information somewhere else as well.

main()

on main()
	set calendar_name to "Anniversaries"
	set number_of_anniversaries to 75
	
	tell application "System Events"
		set Contacts_open to (application process "Contacts") exists
		set Calendar_open to (application process "Calendar") exists
	end tell
	if (not Contacts_open) then launch application "Contacts"
	if (not Calendar_open) then launch application "Calendar"
	
	-- Get the relevant details, where they exist, from EVERY person in Contacts.
	set Contacts_data to get_Contacts_data()
	if (not Contacts_open) then quit application "Contacts"
	
	-- Create a new calendar with the required name and populate it with recurring events for the weddings of people who have "anniversary" dates.
	set calendar_ID to make_calendar(calendar_name)
	set event_IDs to make_events(calendar_ID, Contacts_data, number_of_anniversaries)
	
	if (event_IDs is {}) then
		display dialog "None of your contacts have anniversaries!" buttons {"!"} default button 1 with icon stop
	else
		-- Quit Calendar to ensure it forgets the contents of any previous calendars with this name.
		quit application "Calendar"
		-- Create an iCalendar specification for the existing calendar, with the recurrence summaries edited as required.
		set iCalendar_text to compose_ics(calendar_ID, event_IDs, number_of_anniversaries)
		-- Write this text to an ics file on the desktop.
		write_ics_file(iCalendar_text, (path to desktop as text) & (calendar_name & ".ics"))
		
		-- Reopen Calendar and use some trickery to select the new calendar.
		tell application "Calendar"
			activate
			show event 1 of calendar calendar_name
			view calendar at (current date)
		end tell
		-- "Double-click" the ics file. Calendar will offer to add the new events to the selected calendar.
		tell application "Finder" to open file (calendar_name & ".ics") of desktop
		-- Click the "OK" button in the Add Events dialog. (Uses GUI Scripting.)
		tell application "System Events"
			repeat until (window 2 of application process "Calendar" exists)
				delay 0.2
			end repeat
			keystroke return
		end tell
	end if
	
	-- if (not Calendar_open) then quit application "Calendar"
end main

-- Return the name, id, and custom-date labels and values for every person in Contacts.  
on get_Contacts_data()
	tell application "Contacts" to return {name, id, {label, value} of custom dates} of people
end get_Contacts_data

-- Make a completely new calendar with the given name and return its UID.
on make_calendar(cal_name)
	tell application "Calendar"
		if (calendar cal_name exists) then delete calendar cal_name
		return uid of (make new calendar with properties {name:cal_name})
	end tell
end make_calendar

-- Parse the lists returned by Contacts and, where any custom dates are labelled "anniversary", create corresponding wedding events in Calendar with yearly recurrences. Return the events' IDs.
on make_events(calendar_ID, {names, ids, {date_labels, date_values}}, number_of_anniversaries)
	set event_IDs to {}
	
	-- The lists are all in the same order with respect to people.
	repeat with person_index from 1 to (count names)
		-- Act if this person has a custom date labelled "anniversary".
		if (item person_index of date_labels contains "anniversary") then
			-- Get the wedding date, the genitive case of the person's name, and a URL for the entry in Contacts.
			set persons_date_labels to item person_index of date_labels
			repeat with date_index from 1 to (count persons_date_labels)
				if (item date_index of persons_date_labels is "anniversary") then
					set wedding_date to item date_index of item person_index of date_values
					exit repeat
				end if
			end repeat
			set genitive_name to item person_index of names & "'s "
			set ABURL to "addressbook://" & item person_index of ids
			
			tell application "Calendar"
				tell calendar id calendar_ID
					-- Create the wedding event.
					set new_event to (make new event at end of events with properties {summary:genitive_name & "Wedding", start date:wedding_date, end date:wedding_date, url:ABURL, allday event:true})
					-- Add its uid to the list to return when all the events are done.
					set end of event_IDs to new_event's uid
					-- If the wedding date's 29th February, set the recurrence according to when the person celebrates anniversaries. Otherwise simply recur annually from the event date.
					if ((wedding_date's day is 29) and (wedding_date's month is February)) then
						if (button returned of (display dialog (genitive_name & "Wedding Day is/was 29th February! Are the non-leap anniversaries celebrated on 28th February or 1st March?") buttons {"28th February", "1st March"} default button 2 with icon note) is "28th February") then
							-- Last day of every February.
							set new_event's recurrence to "FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=-1;COUNT=" & (number_of_anniversaries + 1)
						else
							-- 60th day of every year.
							set new_event's recurrence to "FREQ=YEARLY;INTERVAL=1;BYYEARDAY=60;COUNT=" & (number_of_anniversaries + 1)
						end if
					else
						-- Same date every year.
						set new_event's recurrence to "FREQ=YEARLY;INTERVAL=1;COUNT=" & (number_of_anniversaries + 1)
					end if
				end tell
			end tell
		end if
	end repeat
	
	return event_IDs
end make_events

-- Put together an iCalendar specification for the entire calendar containing the VCALENDAR and VEVENT data just written by Contacts and also linked VEVENTs to vary the summaries of the anniversary recurrences.
on compose_ics(calendar_ID, event_IDs, number_of_anniversaries)
	-- The list of iCalendar components could get very long, so it'll be referenced via a script object for speed of access.
	script iCalendar
		property components : {}
	end script
	
	-- Prepare a linked-VEVENT template and store some other useful values.
	set linked_VEVENT to {"BEGIN:VEVENT", missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, {}, "END:VEVENT"}
	set events_folder_path to (path to library folder from user domain as text) & "Calendars:" & calendar_ID & ".calendar:Events:"
	set CRLF to return & linefeed
	set astid to AppleScript's text item delimiters
	
	-- Get the beginning and end of the VCALENDAR data by subtracting the VEVENT section from one of the target calendar files. (Calendar stores one event per file.) 	
	set AppleScript's text item delimiters to {"BEGIN:VEVENT", "END:VEVENT"}
	tell text items of (read file (events_folder_path & beginning of event_IDs & ".ics") as «class utf8») to set VCALENDAR_shell to {beginning, end}
	-- Ascertain the kind of line endings used.
	if (beginning of VCALENDAR_shell contains CRLF) then
		set line_end to CRLF
	else if (beginning of VCALENDAR_shell contains linefeed) then
		set line_end to linefeed
	else
		set line_end to return
	end if
	
	-- Work through the UIDs of the events created above.
	repeat with event_ID in event_IDs
		-- Read the event file for this UID and extract the VEVENT section.
		set AppleScript's text item delimiters to VCALENDAR_shell
		set base_VEVENT to text item 2 of (read file (events_folder_path & event_ID & ".ics") as «class utf8»)
		-- Append this original VEVENT data to the output components.
		set end of iCalendar's components to base_VEVENT
		
		-- Parse the lines of the original VEVENT and prime the linked-VEVENT template accordingly.
		set base_VEVENT to base_VEVENT's paragraphs
		set root_line_count to (count base_VEVENT)
		repeat with i from 1 to root_line_count
			set thisLine to item i of base_VEVENT
			if (thisLine begins with "CREATED:") then
				set item 2 of linked_VEVENT to thisLine
			else if (thisLine begins with "UID:") then
				set item 3 of linked_VEVENT to thisLine
			else if (thisLine begins with "DTEND;") then
				set end_date to (text -8 thru -1 of thisLine) as integer
			else if (thisLine begins with "RRULE:") then
				set RRULE to thisLine
			else if (thisLine begins with "TRANSP:") then
				set item 6 of linked_VEVENT to thisLine
			else if (thisLine begins with "SUMMARY:") then
				-- Reconsititue the SUMMARY entry if split over more than one line.
				repeat with j from (i + 1) to root_line_count
					if (item j of base_VEVENT begins with space) then
						set thisLine to thisLine & text 2 thru -1 of item j of base_VEVENT
					else
						exit repeat
					end if
				end repeat
				set SUMMARY_root to text 1 thru -8 of thisLine
			else if (thisLine begins with "DTSTART;") then
				set start_date to (text -8 thru -1 of thisLine) as integer
			else if (thisLine begins with "SEQUENCE:") then
				set item 10 of linked_VEVENT to "SEQUENCE:" & (text 10 thru -1 of thisLine) + 1
			else if (thisLine begins with "URL;") then
				-- If the URL entry's split over more than one line, the bits don't need to be rejoined.
				set item 11 of linked_VEVENT to {thisLine}
				repeat with j from (i + 1) to root_line_count
					if (item j of base_VEVENT begins with space) then
						set end of item 11 of linked_VEVENT to item j of base_VEVENT
					else
						exit repeat
					end if
				end repeat
			end if
		end repeat
		
		-- Create the required number of linked VEVENTS for the anniversaries, inserting the relevant dates and summaries into the template and coercing it to text each time.
		set AppleScript's text item delimiters to line_end
		-- For speed and convenience, the 8-digit ISO dates (yyyymmdd) are handled as integers.
		set leap_wedding to (start_date mod 10000 is 229)
		if (leap_wedding) then set using_Mar1 to (RRULE contains "BYYEARDAY")
		repeat with counter from 1 to number_of_anniversaries
			set start_date to start_date + 10000 -- Add 1 to the year.
			set end_date to end_date + 10000 -- Ditto.
			-- If the wedding was on 29th February, use the appropriate month/day digits in the anniversary date integer.
			if (leap_wedding) then
				set y to start_date div 10000
				if (isLeapYear(y)) then
					set start_date to y * 10000 + 229
					set end_date to y * 10000 + 301
				else if (using_Mar1) then
					set start_date to y * 10000 + 301
					set end_date to y * 10000 + 302
				else -- Last day of February. Always ends on 1st March.
					set start_date to y * 10000 + 228
				end if
			end if
			set item 4 of linked_VEVENT to "DTEND;VALUE=DATE:" & end_date
			-- This entry links this VEVENT to an expression date of the original event's recurrence.
			set item 5 of linked_VEVENT to "RECURRENCE-ID;VALUE=DATE:" & start_date
			-- Compose the SUMMARY entry for this anniversary.
			set anniversary_SUMMARY to SUMMARY_root & counter & ordinal(counter) & " anniversary"
			-- If it's longer than 72 characters, split it as necessary. Otherwise don't.
			set anniversary_SUMMARY_length to (count anniversary_SUMMARY)
			if (anniversary_SUMMARY_length > 72) then
				set split_SUMMARY to {text 1 thru 72 of anniversary_SUMMARY}
				repeat with i from 73 to anniversary_SUMMARY_length by 71
					set j to i + 70
					if (j > anniversary_SUMMARY_length) then set j to anniversary_SUMMARY_length
					set end of split_SUMMARY to " " & text i thru j of anniversary_SUMMARY
				end repeat
				set item 7 of linked_VEVENT to split_SUMMARY
			else
				set item 7 of linked_VEVENT to anniversary_SUMMARY
			end if
			set item 8 of linked_VEVENT to "DTSTART;VALUE=DATE:" & start_date
			-- As a nicety, use the current date/time as the date stamp for the creation of this VEVENT.
			set item 9 of linked_VEVENT to compose_DTSTAMP()
			
			-- Add the coerced-to-text version of the template's current contents to the output components.
			set end of iCalendar's components to (linked_VEVENT as text)
		end repeat
	end repeat
	
	-- When all the components for all the events are in place, coerce the lot into one iCalendar text.
	set iCalendar_text to beginning of VCALENDAR_shell & iCalendar's components & end of VCALENDAR_shell
	
	set AppleScript's text item delimiters to astid
	
	return iCalendar_text
end compose_ics

on write_ics_file(iCalendar_text, hfs_path)
	set fRef to (open for access file hfs_path with write permission)
	try
		set eof fRef to 0
		write iCalendar_text as «class utf8» to fRef
	end try
	close access fRef
end write_ics_file

on isLeapYear(y)
	return ((y mod 4 is 0) and (y mod 400 is not in {100, 200, 300}))
end isLeapYear

on ordinal(n)
	set units to n mod 10
	if ((units > 3) or ((n - units) mod 100 is 10) or (units < 1)) then
		return "th"
	else
		return item units of {"st", "nd", "rd"}
	end if
end ordinal

on compose_DTSTAMP()
	set {year:y, month:m, day:d, time:t} to (current date) - (time to GMT)
	return "DTSTAMP:" & (y * 10000 + m * 100 + d) & "T" & text 2 thru -1 of ((1000000 + t div hours * 10000 + t mod hours div minutes * 100 + t mod minutes) as text) & "Z"
end compose_DTSTAMP

The reason the script in the previous post creates the calendar and main events in Calendar first is to use the UIDs they’re given. But if the script creates its own UIDs, the calendar file can be written from scratch immediately and the initial Calendar business omitted. In this case, when importing the file, it’s best to use the “New Calendar” option in the dialog’s pop-up menu. The calendar’s then created automatically with the name of the source file.

Again, this has been tested with Address Book and iCal in Snow Leopard. It probably works with Contacts and Calendar in Mountain Lion, but I don’t know for sure.

main()

on main()
	set calendar_name to "Anniversaries"
	set number_of_anniversaries to 75
	
	-- Get the required info from Contacts in iCalendar or intermediate form.
	set seed_data to get_seed_data()
	if (seed_data is {}) then
		display dialog "None of your contacts have anniversaries!" buttons {"!"} default button 1 with icon stop
	else
		-- Compose an iCalendar spec for a calendar containing the required events and save it to an .ics file on the desktop.
		set iCalendar_text to compose_ics(seed_data, number_of_anniversaries)
		write_ics_file(iCalendar_text, (path to desktop as text) & (calendar_name & ".ics"))
		
		-- Import the file into Calendar as a new calendar.
		import_calendar(calendar_name)
	end if
end main

-- Identify "anniversary" custom dates in Contacts and return the corresponding names, ids, and dates adapted for use in the construction of an iCalendar spec. Monogamous and once-only marriages assumed!
on get_seed_data()
	tell application "System Events" to set Contacts_open to (application process "Contacts") exists
	
	-- Get the name, id, and custom-date data for every person in Contacts.  
	tell application "Contacts"
		launch
		set {names, ids, {date_labels, date_values}} to {name, id, {label, value} of custom dates} of people
		if (not Contacts_open) then quit
	end tell
	
	-- Find any "anniversary" date labels and store the associated person/date data converted as follows:
	-- name: as the start of an iCalendar SUMMARY entry, with the name in the genitive and a trailing space.
	-- id: as a complete iCalendar URL entry, the id being part of an "addressbook"-protocol URL.
	-- "anniversary" date value: as event start and end dates in zoneless ISO format, but stored as integers instead of text for ease and speed of manipulation.
	set seed_data to {}
	repeat with person from 1 to (count names)
		set these_labels to item person of date_labels
		if (these_labels contains "anniversary") then
			repeat with custom_date from 1 to (count these_labels)
				if (item custom_date of these_labels is "anniversary") then
					set start_date to item custom_date of item person of date_values
					set end of seed_data to {|SUMMARY start|:"SUMMARY:" & (item person of names) & "'s ", |URL entry|:"URL;VALUE=URI:addressbook://" & item person of ids, |start date|:ISOT_integer(start_date), |end date|:ISOT_integer(start_date + days)}
					exit repeat
				end if
			end repeat
		end if
	end repeat
	
	return seed_data
end get_seed_data

-- Put together an iCalendar specification for a calendar containing recurring events whose initial summaries indicate weddings and recurrence summaries indicate the inividual anniversaries.
on compose_ics(seed_data, number_of_anniversaries)
	-- The list of iCalendar components could get very long, so it'll be referenced via a script object for speed of access.
	script iCalendar
		property components : {}
	end script
	
	-- Initialise a VEVENT template. It'll serve for both main events and recurrence instances.
	set VEVENT_template to {"BEGIN:VEVENT", missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, "END:VEVENT"}
	-- Get process and host data for use in the UIDs.
	set my_Name to name of current application
	tell application "System Events" to set unix_id to unix id of first application process whose displayed name is my_Name
	set host_name to host name of (system info)
	-- Prepare to use CRLF line endings.
	set CRLF to return & linefeed
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to CRLF
	
	-- Create and store the opening VCALENDAR spiel.
	set beginning of iCalendar's components to {"BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Apple Inc.//iCal 4.0.4//EN", "CALSCALE:GREGORIAN"} as text
	
	-- Create and store the VEVENT sections for each main event.
	repeat with event_number from 1 to (count seed_data)
		set {|SUMMARY start|:SUMMARY_start, |URL entry|:URL_entry, |start date|:start_date, |end date|:end_date} to item event_number of seed_data
		-- Load the VEVENT template with the data for the main, recurring event.
		set item 2 of VEVENT_template to "CREATED:" & ISOT_GMT(current date)
		set item 3 of VEVENT_template to "UID:" & new_UID(unix_id, host_name, event_number)
		set item 4 of VEVENT_template to "DTEND;VALUE=" & end_date
		set leap_wedding to (start_date mod 10000 is 229) -- Did these idiots get married on 29th February?
		if (leap_wedding) then
			set using_Mar1 to (button returned of (display dialog (text 9 thru -1 of SUMMARY_start & "wedding is/was on 29th February! Are the non-leap anniversaries celebrated on 28th February or 1st March?") buttons {"28th February", "1st March"} default button 2 with icon note) is "1st March")
			if (using_Mar1) then -- The anniversaries have to recur on the 60th day of every year .
				set item 5 of VEVENT_template to "RRULE:FREQ=YEARLY;INTERVAL=1;BYYEARDAY=60;COUNT=" & (number_of_anniversaries + 1)
			else -- . or else on the last day of every February.
				set item 5 of VEVENT_template to "RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=-1;COUNT=" & (number_of_anniversaries + 1)
			end if
		else -- Normal anniversaries simply recur on the same date every year.
			set item 5 of VEVENT_template to "RRULE:FREQ=YEARLY;INTERVAL=1;COUNT=" & (number_of_anniversaries + 1)
		end if
		set item 6 of VEVENT_template to SUMMARY_start & "wedding"
		set item 7 of VEVENT_template to "DTSTART;VALUE=DATE:" & start_date
		set item 8 of VEVENT_template to "DTSTAMP:" & ISOT_GMT(current date)
		set item 9 of VEVENT_template to "SEQUENCE:7" -- Cheating with the sequence number!
		set item 10 of VEVENT_template to URL_entry
		
		-- Coerce the template to text and append the resulting VEVENT entry to the component list.
		set end of iCalendar's components to VEVENT_template as text
		
		-- Now create the required number of linked VEVENTS for the anniversaries, inserting the relevant dates and summaries into the template and coercing it to text each time.
		repeat with anniversary_number from 1 to number_of_anniversaries
			set start_date to start_date + 10000 -- Add 1 to the year.
			set end_date to end_date + 10000 -- Ditto.
			if (leap_wedding) then
				-- Use months and days pertinent to the anniverary-date convention.
				set y to start_date div 10000
				if (isLeapYear(y)) then
					set start_date to y * 10000 + 229
					set end_date to y * 10000 + 301
				else if (using_Mar1) then
					set start_date to y * 10000 + 301
					set end_date to y * 10000 + 302
				else
					set start_date to y * 10000 + 228
				end if
			end if
			set item 4 of VEVENT_template to "DTEND;VALUE=DATE:" & end_date
			-- This entry links this VEVENT to an expression date of the original event's recurrence.
			set item 5 of VEVENT_template to "RECURRENCE-ID;VALUE=DATE:" & start_date
			set item 6 of VEVENT_template to SUMMARY_start & anniversary_number & ordinal(anniversary_number) & " anniversary"
			set item 7 of VEVENT_template to "DTSTART;VALUE=DATE:" & start_date
			set item 8 of VEVENT_template to "DTSTAMP:" & ISOT_GMT(current date)
			set item 9 of VEVENT_template to "SEQUENCE:8" -- Cheating with the sequence number!
			
			-- Append this linked-VEVENT text to the output components.
			set end of iCalendar's components to VEVENT_template as text
		end repeat
	end repeat
	
	-- Lastly, append the END:VCALENDAR line.
	set end of iCalendar's components to "END:VCALENDAR" & CRLF
	-- When all the components are gathered, coerce the lot into one text.
	set iCalendar_text to iCalendar's components as text
	
	set AppleScript's text item delimiters to astid
	
	return iCalendar_text
end compose_ics

on write_ics_file(iCalendar_text, hfs_path)
	set fRef to (open for access file hfs_path with write permission)
	try
		set eof fRef to 0
		write iCalendar_text as «class utf8» to fRef
	end try
	close access fRef
end write_ics_file

-- Delete any current calendar with the given name and import an .ics file to a replacement.
on import_calendar(calendar_name)
	tell application "Calendar"
		activate
		if (calendar calendar_name exists) then delete calendar calendar_name
		-- This assumes there'll be at most one calendar with the name, but it's easy to modify.
	end tell
	
	-- "Double-click" the ics file. Calendar will offer to import the new events.
	tell application "Finder" to open file (calendar_name & ".ics") of desktop
	
	-- In the Add Events dialog, select the bottom item ("New Calendar") in the pop-up menu and hit return. The calendar should be created automatically with the name of the ics file.
	tell application "System Events"
		tell application process "Calendar" -- GUI Scripting.
			repeat until (window 2 exists)
				delay 0.2
			end repeat
			tell pop up button 1 of window 1
				perform action "AXPress" -- Click the pop-up menu button.
				perform action "AXPress" of menu item -1 of menu 1 -- Click the "New Calendar" item.
			end tell
		end tell
		keystroke return -- "Click" the dialog's "OK" button.
	end tell
end import_calendar

-- Return the "yyyymmdd" representation of a date as an integer.
on ISOT_integer(theDate)
	set {year:y, month:m, day:d} to theDate
	return y * 10000 + m * 100 + d
end ISOT_integer

-- Return the GMT equivalent of a date in ISOT format.
on ISOT_GMT(theDate)
	set {year:y, month:m, day:d, time:t} to theDate - (time to GMT)
	return ((y * 10000 + m * 100 + d) as text) & "T" & text 2 thru -1 of ((1000000 + t div hours * 10000 + t mod hours div minutes * 100 + t mod minutes) as text) & "Z"
end ISOT_GMT

-- Construct an "@"-style UID from the current date and time and the given data.
on new_UID(unix_id, host_name, iteration)
	return ISOT_GMT(current date) & "-" & unix_id & "-" & iteration & "@" & host_name
end new_UID

on isLeapYear(y)
	return ((y mod 4 is 0) and (y mod 400 is not in {100, 200, 300}))
end isLeapYear

on ordinal(n)
	set units to n mod 10
	if ((units > 3) or ((n - units) mod 100 is 10) or (units < 1)) then
		return "th"
	else
		return item units of {"st", "nd", "rd"}
	end if
end ordinal