AppleScript to create list of Events over a specified date range

I came up with this when I wanted to paste an itinerary for an upcoming car trip into a text document. I found some inelegant solutions for Calendar.app (OS X Mountain Lion), but not quite what I wanted. I wanted something like this:

August 18, 2013
Montreal, Canada

August 19, 2013
Burlington, Vermont

and so on. Dates are Event.start date at text is Event.summary (I’m making up syntax here). Output is copied to clipboard or can be saved to plaintext file.

I found some scripts that suggest this is pretty doable (acknowledgements here: http://tech.kateva.org/2013/08/using-applescript-to-create-plaintext.html)

For example, I can get names of calendars (or go with selected?) and I can also see how to pass a date range and get EventIDs back and then use ‘summary’ and the link to get Event values:

tell application "iCal"
set theCalendars to every calendar
end tell

AND

set {year:y, month:m, day:d} to current date
set str to (m as string) & " " & (d as string) & " " & (y as string)
set today to date str
set tomorrow to today + 60 * 60 * 24

tell application "Calendar"
tell calendar "FL Family Calendar"
set curr to every event whose start date is greater than or equal to today ¬
and start date is less than or equal to tomorrow
end tell
end tell

So all looks very doable. Anyone know someone who has done something even closer to this example?

Hi,

Something like this?

tell application "Calendar"
	set the_calendar to calendar "USA Holidays"
	set info_list to {start date, summary} of every event of the_calendar
end tell

It’s interesting what you would do with the info from there.

gl,
kel

Here’s one way:

tell application "Calendar"
	set the_calendar to calendar "USA Holidays"
	set {start_dates, the_summaries} to {start date, summary} of every event of the_calendar
end tell
set c to count start_dates
set text_list to {}
repeat with i from 1 to c
	set this_date to item i of start_dates
	set this_summary to item i of the_summaries
	set end of text_list to this_date & linefeed & this_summary & return & return
end repeat
set the_text to text_list as string

Edited: why did I do that with the returns:

tell application "Calendar"
	set the_calendar to calendar "USA Holidays"
	set {start_dates, the_summaries} to {start date, summary} of every event of the_calendar
end tell
set c to count start_dates
set text_list to {}
repeat with i from 1 to c
	set this_date to item i of start_dates
	set this_summary to item i of the_summaries
	set end of text_list to this_date & linefeed & this_summary & linefeed & linefeed
end repeat
set the_text to text_list as string

gl,
kel

Thank you, that produced a list in the correct format, though it wasn’t sorted by date. I got back about 800 events.

I’d like to be able to produce a similar output, but

  1. Be able to specify > 1 calendar (or even all calendars)
  2. Be able to specify a start and end date
  3. Have results be sorted by date

Your code illustrated something novel to me, that AppleScript appears to support array assignment, as in:

set {start_dates, the_summaries} to {start date, summary} of every event of the_calendar

That’s good that we separated the dates.

If you had 800 events, then this is a job for minds greater than mine. :slight_smile: If you want speed that is.

gl,
kel

I told you it was going to be interesting.

Think I’ve found a way to speed it up. If I can only remember it.

It was actually pretty quick really. Looking at this code
http://stackoverflow.com/questions/5907368/getting-todays-events-from-ical-with-applescript


set {year:y, month:m, day:d} to current date
set str to (m as string) & " " & (d as string) & " " & (y as string)
set today to date str
set tomorrow to today + 60 * 60 * 24

tell application "iCal"
tell calendar "Lotus Notes"
set curr to every event whose start date is greater than or equal to today ¬
and start date is less than or equal to tomorrow
end tell
end tell

It looked like we could get the events back using a variant of the data criteria, then process them as you did.

I don’t know about sorting by date though.

Hi.

That’s rubbish in that it’s inefficient and only works on machines where the preferences are set for the US date format. This is better:

set today to (current date)
set today's time to 0
set tomorrow to today + days

If you’ve got a lot of events in your calendar, a ‘whose’ filter can take quite a while to execute. (Applications can be very slow when they’re asked to think.) A faster way would be to grab the start dates and summaries for all the events in the calendar, as kel’s done above, and use vanilla AppleScript to identify which dates and summaries are in required range. The following may get you started. It gets date-range input from the user, narrows down the start dates and summaries to those in the range, sorts the results by start date, composes the text, and makes a TextEdit document containing it:

-- Ask the user for the range of dates to be covered.
on getDateRange()
	set today to (current date)
	set d1 to today's short date string
	set d2 to short date string of (today + 6 * days)
	
	set dateRange to text returned of (display dialog "Enter the required date range:" default answer d1 & " - " & d2)
	set dateRangeStart to date (text from word 1 to word 3 of dateRange)
	set dateRangeEnd to date (text from word -3 to word -1 of dateRange)
	set dateRangeEnd's time to days - 1 -- Sets the last date's time to 23:59:59, the last second of the range.
	
	return {dateRangeStart, dateRangeEnd}
end getDateRange

-- Return the start dates and summaries which are in the given date range.
on filterToDateRange(theStartDates, theSummaries, dateRangeStart, dateRangeEnd)
	set {eventDatesInRange, eventSummariesInRange} to {{}, {}}
	repeat with i from 1 to (count theStartDates)
		set thisStartDate to item i of theStartDates
		if (not ((thisStartDate comes before dateRangeStart) or (thisStartDate comes after dateRangeEnd))) then
			set end of eventDatesInRange to thisStartDate
			set end of eventSummariesInRange to item i of theSummaries
		end if
	end repeat
	
	return {eventDatesInRange, eventSummariesInRange}
end filterToDateRange

-- Sort both the start-date and summary lists by start date.
on sortByDate(eventDatesInRange, eventSummariesInRange)
	-- A sort-customisation object for sorting the summary list in parallel with the date list.
	script custom
		property summaries : eventSummariesInRange
		
		on swap(i, j)
			tell item i of my summaries
				set item i of my summaries to item j of my summaries
				set item j of my summaries to it
			end tell
		end swap
	end script
	
	CustomBubbleSort(eventDatesInRange, 1, -1, {slave:custom})
end sortByDate

-- CustomBubbleSort from "A Dose of Sorts" by Nigel Garvey.
-- The number of items to be sorted here is likely to be small.
on CustomBubbleSort(theList, l, r, customiser)
	script o
		property comparer : me
		property slave : me
		property lst : theList
		
		on bsrt(l, r)
			set l2 to l + 1
			repeat with j from r to l2 by -1
				set a to item l of o's lst
				repeat with i from l2 to j
					set b to item i of o's lst
					if (comparer's isGreater(a, b)) then
						set item (i - 1) of o's lst to b
						set item i of o's lst to a
						slave's swap(i - 1, i)
					else
						set a to b
					end if
				end repeat
			end repeat
		end bsrt
		
		-- Default comparison and slave handlers for an ordinary sort.
		on isGreater(a, b)
			(a > b)
		end isGreater
		
		on swap(a, b)
		end swap
	end script
	
	-- Process the input parameters.
	set listLen to (count theList)
	if (listLen > 1) then
		-- Negative and/or transposed range indices.
		if (l < 0) then set l to listLen + l + 1
		if (r < 0) then set r to listLen + r + 1
		if (l > r) then set {l, r} to {r, l}
		
		-- Supplied or default customisation scripts.
		if (customiser's class is record) then set {comparer:o's comparer, slave:o's slave} to (customiser & {comparer:o, slave:o})
		
		-- Do the sort.
		o's bsrt(l, r)
	end if
	
	return -- nothing 
end CustomBubbleSort

-- Compose the text from the items in the start-date and summary lists.
on composeText(eventDatesInRange, eventSummariesInRange)
	set txt to ""
	set gap to linefeed & linefeed
	
	repeat with i from 1 to (count eventDatesInRange)
		set txt to txt & (date string of item i of eventDatesInRange) & (linefeed & item i of eventSummariesInRange & gap)
	end repeat
	
	return text 1 thru -3 of txt
end composeText

on main()
	tell application "iCal" to set {theStartDates, theSummaries} to {start date, summary} of events of calendar "FL Family Calendar"
	
	set {dateRangeStart, dateRangeEnd} to getDateRange()
	set {eventDatesInRange, eventSummariesInRange} to filterToDateRange(theStartDates, theSummaries, dateRangeStart, dateRangeEnd)
	sortByDate(eventDatesInRange, eventSummariesInRange)
	set txt to composeText(eventDatesInRange, eventSummariesInRange)
	
	tell application "TextEdit"
		make new document with properties {text:txt}
		activate
	end tell
end main

main()

Wow, that looks awfully good. I should be able to run with that for the moment. I’ve got a bit of crunch before a family car trip, but then I’ll have a chance to study and dissect the script, and tweak it if I need to. I’ll document it on a blog post with, of course, full reference to this post and MacScripter.

Thanks NG, the new script works very well. It was easy to adjust the text output to be more compressed.

I’ll enjoy playing with this further, but it solves my original problem as is.

I realize the is a pretty old thread but I came across is because I was wanting to do something very similar in Calendar. The code works great for what I need but I’ve also been trying to modify it to do the same thing for several calendars. I’d still like to get just a single text doc that has date headers and then listing the events for that date (including which calendar it’s on) - something like this (yes I would like the dates in bold if possible):

Monday, January 18, 2016
Calendar1: a thing that’s happening
Calendar2: another thing that’s happening

Wednesday, January 20, 2016
Calendar1: another day, another thing to do

Can anyone guide me on how to make this script work for several calendars?

Thanks.

I wouldn’t describe collating the event data from several calendars by calendar date and then formatting the output text as being particularly similar… :wink:

The version below has only been tested in El Capitan. Instead of sorting a single calendar’s event summaries in parallel with the associated start dates, it collates the data from all the calendars into a list of lists ” each sublist containing the start date, summary, and calendar of an event. This list is then sorted by the sublists’ start dates. A new document is then created in TextEdit and text representing the events is output to it as it’s derived from the sorted data.

With more calendars involved, there’ll usually be more events to process, so I’ve used script object referencing in the list collation and replaced the bubble sort with a shell sort. However, most of the running time will be the initial wait for Calendar to return the data to the script, which could take up to a minute if there are a lot of events in the calendars. I think there are faster ways to get the data using ASObjC, but I haven’t got round to learning them or it yet.

-- Tested with Calendar 8.0 and TextEdit 1.11 (new documents defaulting to RTF) in Mac OS 10.11.2

-- Ask the user for the range of dates to be covered.
on getDateRange()
	set today to (current date)
	set d1 to today's short date string
	set d2 to short date string of (today + 6 * days)
	
	set dateRange to text returned of (display dialog "Enter the required date range:" default answer d1 & " - " & d2)
	set dateRangeStart to date (text from word 1 to word 3 of dateRange)
	set dateRangeEnd to date (text from word -3 to word -1 of dateRange)
	set dateRangeEnd's time to days - 1 -- Sets the last date's time to 23:59:59, the last second of the range.
	
	return {dateRangeStart, dateRangeEnd}
end getDateRange

-- Return the start dates, summaries, and calendar names of events in the given date range.
-- {{start date, summary, calendar name}, {start date, summary, calendar name}, . }
on filterToDateRange(theStartDates, theSummaries, theCalendarNames, dateRangeStart, dateRangeEnd)
	script o
		property sDates : theStartDates
		property summaries : theSummaries
		property cNames : theCalendarNames
		property filteredData : {}
	end script
	
	repeat with i from 1 to (count theCalendarNames)
		set o's sDates to item i of theStartDates
		set o's summaries to item i of theSummaries
		set thisCalendarName to item i of theCalendarNames
		repeat with j from 1 to (count o's sDates)
			set thisStartDate to item j of o's sDates
			if (not ((thisStartDate comes before dateRangeStart) or (thisStartDate comes after dateRangeEnd))) then
				set thisSummary to item j of o's summaries
				set end of o's filteredData to {thisStartDate, thisSummary, thisCalendarName}
			end if
		end repeat
	end repeat
	
	return o's filteredData
end filterToDateRange

-- Sort the filtered data by start date.
on sortByDate(filteredData)
	-- A sort-customisation object which compares the first items of two sublists taken from the list being sorted.
	script custom
		on isGreater(a, b)
			return (beginning of a > beginning of b)
		end isGreater
	end script
	
	CustomShellSort(filteredData, 1, -1, {comparer:custom})
end sortByDate

-- Shell sort. Algorithm: Donald Shell, 1959. AppleScript implementation: Nigel Garvey, 2010.
on CustomShellSort(theList, l, r, customiser)
	script o
		property comparer : me
		property slave : me
		property lst : theList
		
		on shsrt(l, r)
			set step to (r - l + 1) div 2
			repeat while (step > 0)
				slave's setStep(step)
				repeat with j from (l + step) to r
					set v to item j of o's lst
					repeat with i from (j - step) to l by -step
						tell item i of o's lst
							if (comparer's isGreater(it, v)) then
								set item (i + step) of o's lst to it
							else
								set i to i + step
								exit repeat
							end if
						end tell
					end repeat
					set item i of o's lst to v
					slave's rotate(i, j)
				end repeat
				set step to (step / 2.2) as integer
			end repeat
		end shsrt
		
		-- Default comparison and slave handlers for an ordinary sort.
		on isGreater(a, b)
			(a > b)
		end isGreater
		
		on rotate(a, b)
		end rotate
		
		on setStep(a)
		end setStep
	end script
	
	-- Process the input parameters.
	set listLen to (count theList)
	if (listLen > 1) then
		-- Negative and/or transposed range indices.
		if (l < 0) then set l to listLen + l + 1
		if (r < 0) then set r to listLen + r + 1
		if (l > r) then set {l, r} to {r, l}
		
		-- Supplied or default customisation scripts.
		if (customiser's class is record) then set {comparer:o's comparer, slave:o's slave} to (customiser & {comparer:o, slave:o})
		
		-- Do the sort.
		o's shsrt(l, r)
	end if
	
	return -- nothing.
end CustomShellSort

-- Create a new TextEdit document with text derived from the gathered data.
on composeText(filteredData)
	tell application "TextEdit"
		-- Make a new document, with a minimal text so that we can discover the name of its font.
		set newDoc to (make new document with properties {text:" "})
		set baseFont to font of newDoc's text
		-- This ASSUMES that an equivalent bold font exists and that its name is the same as the plain font with " bold" appended.
		set boldFont to baseFont & " bold"
		-- Dummy text no longer needed.
		set newDoc's text to ""
		activate
	end tell
	
	if (filteredData is {}) then
		-- If no events have been discovered in the date range, print that fact.
		tell application "TextEdit" to make new paragraph at end of newDoc's text with data "No events found in this period." with properties {font:boldFont}
	else
		-- Otherwise print the event details.
		set currentCalendarDate to "" -- The calendar date currently being processed. (None yet.)
		repeat with i from 1 to (count filteredData)
			-- Get the data for an event from the list of filtered data.
			set {{date string:thisCalendarDate, hours:thisStartTimeH, minutes:thisStartTimeM}, thisSummary, thisCalendar} to item i of filteredData
			-- If the calendar date is different from the one we've been processing up till now, output an empty line and the new date string to TextEdit.
			if (thisCalendarDate is not currentCalendarDate) then
				tell application "TextEdit"
					make new paragraph at end of newDoc's text with data linefeed with properties {font:baseFont}
					make new paragraph at end of newDoc's text with data (thisCalendarDate & linefeed) with properties {font:boldFont}
				end tell
				-- Make the new date the one currently being processed.
				set currentCalendarDate to thisCalendarDate
			end if
			-- Create a 24-hour time string from the hours and minutes of the start date.
			tell (10000 + thisStartTimeH * 100 + thisStartTimeM) as text to set thisStartTime to text 2 thru 3 & ":" & text 4 thru 5
			-- Output the entry for this event to TextEdit.
			tell application "TextEdit" to make new paragraph at end of newDoc's text with data ("\"" & thisCalendar & "\" calendar: " & thisStartTime & " " & thisSummary & linefeed) with properties {font:baseFont}
		end repeat
	end if
end composeText

on main()
	say "Getting data from Calendar. It may take a while."
	tell application "Calendar" to set {{theStartDates, theSummaries}, theCalendarNames} to {{start date, summary} of events, name} of calendars
	set {dateRangeStart, dateRangeEnd} to getDateRange()
	set filteredData to filterToDateRange(theStartDates, theSummaries, theCalendarNames, dateRangeStart, dateRangeEnd)
	sortByDate(filteredData)
	composeText(filteredData)
end main

main()

Fantastic! Thank you, Sir NG! And a more than fair point about my overstatement of the similarity :lol:

This works perfectly - and, as always, I will learn a lot by having a close look at it. Many thanks.

It’s certainly much faster, but it’s also more accurate. The basic problem with using Calendar.app is that the start and end dates for repeating events are the start and end dates set when the event was created. So if you have a repeating event that falls within your time frame, scripts using Calendar.app won’t catch it (unless it’s the first time).

But you don’t actually have to write any ASObjC – all you need is CalendarLib (for 10.9+) or CalendarLib EC (for 10.11 only). So you’d start your script like this:

use script "CalendarLib EC" -- put this at the top of your scripts; use "CalendarLib" for pre-10.11 compatability
use scripting additions

You’d have a handler like this:


on fetchEventsStarting:dateRangeStart ending:dateRangeEnd
	set theStore to fetch store
	set theCals to fetch calendars {} cal type list {} event store theStore -- all calendars
	set theEvents to fetch events starting date dateRangeStart ending date dateRangeEnd searching cals theCals event store theStore
	set theFilteredData to {}
	repeat with anEvent in theEvents
		set theInfo to (event info for event anEvent)
		set end of theFilteredData to {event_start_date of theInfo, event_summary of theInfo, calendar_name of theInfo}
	end repeat
	return theFilteredData
end fetchEventsStarting:ending:

And your main() would be like this:

on main()
	set {dateRangeStart, dateRangeEnd} to getDateRange()
	set filteredData to my fetchEventsStarting:dateRangeStart ending:dateRangeEnd
	composeText(filteredData)
end main

You can then remove filterToDateRange(), sortByDate() and sortByDate().

You can get the CalendarLibs here:

www.macosxautomation.com/applescript/apps/Script_Libs.html

Thanks Shane. That’s great. :cool:

Here’s an actual manifestation of your suggestions:

-- Faster version using ASObjC.
-- Requires Shane Stanley's CalendarLib library. (Use the EC version with El Capitan.)
-- <[url=http://www.macosxautomation.com/applescript/apps/Script_Libs.html]www.macosxautomation.com/applescript/apps/Script_Libs.html[/url]>.

-- Doesn't require Calendar.app to be running.
-- Includes the expression dates of repeating events.
-- Tested with CalendarLIb EC and TextEdit 1.11 (new documents defaulting to RTF) in Mac OS 10.11.2.

use script "CalendarLib EC"
use scripting additions

-- Shane's handler to get the event data using his library.
on fetchEventsStarting:dateRangeStart ending:dateRangeEnd
	set theStore to fetch store
	set theCals to fetch calendars {} cal type list {} event store theStore -- all calendars
	set theEvents to fetch events starting date dateRangeStart ending date dateRangeEnd searching cals theCals event store theStore
	set theFilteredData to {}
	repeat with anEvent in theEvents
		set theInfo to (event info for event anEvent)
		set end of theFilteredData to {event_start_date of theInfo, event_summary of theInfo, calendar_name of theInfo}
	end repeat
	return theFilteredData
end fetchEventsStarting:ending:

-- Ask the user for the range of dates to be covered.
on getDateRange()
	set today to (current date)
	set d1 to today's short date string
	set d2 to short date string of (today + 6 * days)
	
	set dateRange to text returned of (display dialog "Enter the required date range:" default answer d1 & " - " & d2)
	set dateRangeStart to date (text from word 1 to word 3 of dateRange)
	set dateRangeEnd to date (text from word -3 to word -1 of dateRange)
	set dateRangeEnd's time to days - 1 -- Sets the last date's time to 23:59:59, the last second of the range.
	
	return {dateRangeStart, dateRangeEnd}
end getDateRange

-- Create a new TextEdit document with text derived from the gathered data.
on composeText(filteredData)
	tell application "TextEdit"
		-- Make a new document, with a minimal text so that we can discover the name of its font.
		set newDoc to (make new document with properties {text:" "})
		set baseFont to font of newDoc's text
		-- This ASSUMES that an equivalent bold font exists and that its name is the same as the plain font with " bold" appended.
		set boldFont to baseFont & " bold"
		-- Dummy text no longer needed.
		set newDoc's text to ""
		activate
	end tell
	
	if (filteredData is {}) then
		-- If no events have been discovered in the date range, print that fact.
		tell application "TextEdit" to make new paragraph at end of newDoc's text with data "No events found in this period." with properties {font:boldFont}
	else
		-- Otherwise print the event details.
		set currentCalendarDate to "" -- The calendar date currently being processed. (None yet.)
		repeat with i from 1 to (count filteredData)
			-- Get the data for an event from the list of filtered data.
			set {{date string:thisCalendarDate, hours:thisStartTimeH, minutes:thisStartTimeM}, thisSummary, thisCalendar} to item i of filteredData
			-- If the calendar date is different from the one we've been processing up till now, output an empty line and the new date string to TextEdit.
			if (thisCalendarDate is not currentCalendarDate) then
				tell application "TextEdit"
					make new paragraph at end of newDoc's text with data linefeed with properties {font:baseFont}
					make new paragraph at end of newDoc's text with data (thisCalendarDate & linefeed) with properties {font:boldFont}
				end tell
				-- Make the new date the one currently being processed.
				set currentCalendarDate to thisCalendarDate
			end if
			-- Create a 24-hour time string from the hours and minutes of the start date.
			tell (10000 + thisStartTimeH * 100 + thisStartTimeM) as text to set thisStartTime to text 2 thru 3 & ":" & text 4 thru 5
			-- Output the entry for this event to TextEdit.
			tell application "TextEdit" to make new paragraph at end of newDoc's text with data ("\"" & thisCalendar & "\" calendar: " & thisStartTime & " " & thisSummary & linefeed) with properties {font:baseFont}
		end repeat
	end if
end composeText

on main()
	set {dateRangeStart, dateRangeEnd} to getDateRange()
	say "This script will finish before you can say \"Jack ."
	set filteredData to my fetchEventsStarting:dateRangeStart ending:dateRangeEnd
	composeText(filteredData)
	say ". Robinson\"!"
end main

main()

I’ve noticed this morning that Shane’s adaption, as well as returning occurrences of repeating events, returns single events which are in progress during the specified period. I have a January sale noted in one of my calendars which began last Friday (before the default date range if the script’s run today) and ends this coming Sunday (after it). Shane’s handler includes this event in its result and the script goes on to make an entry for it in TextEdit under last Friday’s date. This gives the user the impression of a bug in the script, but it should probably be regarded as a feature not catered for under the script’s original specification. A really posh implementation would need to catch multi-day events and indicate them in some specified way in the text output.

Nigel,

If you mark your sale as all-day (not technically correct, I know), you can filter it out by inserting this after the fetch events line:

set theEvents to filter events event list theEvents without runs all day

Thanks Shane. But that of course excludes all all-day events, which may not be desired. As I said, the script needs to be able to recognise multi-day events now and to have a policy about what to do with them. Ideally too, it shouldn’t display start times with all-day events.

Here’s an attempt to address these points. Unfortunately, I’ve had to reintroduce sorting handlers ” an insertion sort seeming preferable this time:

-- Faster version using ASObjC.
-- Requires Shane Stanley's CalendarLib library. (Use the EC version with El Capitan.)
-- <[url=http://www.macosxautomation.com/applescript/apps/Script_Libs.html]www.macosxautomation.com/applescript/apps/Script_Libs.html[/url]>.

-- Doesn't require Calendar.app to be running.
-- Includes the expression dates of repeating events.
-- Now has a policy for the handling of all-day and multi-day events.
-- Tested with CalendarLIb EC and TextEdit 1.11 (new documents defaulting to RTF) in Mac OS 10.11.2.

use script "CalendarLib EC"
use scripting additions

-- Shane's handler to get the event data using his library, modified for special handling of all-day and multi-day events.
on fetchEventsStarting:dateRangeStart ending:dateRangeEnd
	set theStore to (fetch store)
	set theCals to (fetch calendars {} cal type list {} event store theStore) -- all calendars
	set theEvents to (fetch events starting date dateRangeStart ending date dateRangeEnd searching cals theCals event store theStore)
	set theFilteredData to {}
	repeat with anEvent in theEvents
		-- Besides the start date, summary, and calendar of each event, get its end date and time zone too.
		set {event_start_date:thisStartDate, event_end_date:thisEndDate, event_time_zone:thisTimeZone, event_summary:thisSummary, calendar_name:thisCalendarName} to (event info for event anEvent)
		
		-- If a returned event starts before the date range entered by the user, its start and end dates straddle the beginning of the range. Use the range's start date in this case instead of the returned one.
		set eventAlreadyStarted to (thisStartDate comes before dateRangeStart)
		if (eventAlreadyStarted) then set thisStartDate to dateRangeStart
		-- If the event's an all-day one (no time zone), it ends at 00:00:00 the following day. Change this to 23:59:59 on the event day.
		if (thisTimeZone is missing value) then set thisEndDate to thisEndDate - 1
		-- If the event ends after the specified date range, substitute the last date in the range for the returned end date.
		if (thisEndDate comes after dateRangeEnd) then set thisEndDate to dateRangeEnd
		-- Store the finalised start date, 'already started' flag, time zone, summary, and calendar name. If a multi-day event, make separate entries for each date occupied in the range.
		repeat until (thisStartDate comes after thisEndDate)
			set end of theFilteredData to {thisStartDate, eventAlreadyStarted, thisTimeZone, thisSummary, thisCalendarName}
			set thisStartDate to thisStartDate + days
			set eventAlreadyStarted to true
		end repeat
	end repeat
	-- Since there may afterwards be additional entries for multi-day events, re-sort the entries by start date.
	sortByStartDate(theFilteredData)
	
	return theFilteredData
end fetchEventsStarting:ending:

-- Sort the filtered data by start date.
on sortByStartDate(filteredData)
	-- Comparison object for a customisable sort. Compares the first items of two passed lists.
	script byFirstListItem
		on isGreater(a, b)
			return (beginning of a > beginning of b)
		end isGreater
	end script
	
	CustomInsertionSort(filteredData, 1, -1, {comparer:byFirstListItem})
end sortByStartDate

-- Customisable insertion sort. Algorithm: unknown author. AppleScript implementation: Arthur J. Knapp and Nigel Garvey, 2003. Revised by NG, 2010.
on CustomInsertionSort(theList, l, r, customiser)
	script o
		property comparer : me
		property slave : me
		property lst : theList
		
		on isrt(l, r)
			set u to item l of o's lst
			repeat with j from (l + 1) to r
				set v to item j of o's lst
				if (comparer's isGreater(u, v)) then
					set here to l
					set item j of o's lst to u
					repeat with i from (j - 2) to l by -1
						tell item i of o's lst
							if (comparer's isGreater(it, v)) then
								set item (i + 1) of o's lst to it
							else
								set here to i + 1
								exit repeat
							end if
						end tell
					end repeat
					set item here of o's lst to v
					slave's rotate(here, j)
				else
					set u to v
				end if
			end repeat
		end isrt
		
		on isGreater(a, b)
			(a > b)
		end isGreater
		
		on rotate(a, b)
		end rotate
	end script
	
	set listLen to (count theList)
	if (listLen > 1) then
		if (l < 0) then set l to listLen + l + 1
		if (r < 0) then set r to listLen + r + 1
		if (l > r) then set {l, r} to {r, l}
		
		if (customiser's class is record) then set {comparer:o's comparer, slave:o's slave} to (customiser & {comparer:o, slave:o})
		
		o's isrt(l, r)
	end if
	
	return -- nothing.
end CustomInsertionSort

-- Ask the user for the range of dates to be covered.
on getDateRange()
	set today to (current date)
	set d1 to today's short date string
	set d2 to short date string of (today + 6 * days)
	
	set dateRange to text returned of (display dialog "Enter the required date range:" default answer d1 & " - " & d2)
	set dateRangeStart to date (text from word 1 to word 3 of dateRange)
	set dateRangeEnd to date (text from word -3 to word -1 of dateRange)
	set dateRangeEnd's time to days - 1 -- Sets the last date's time to 23:59:59, the last second of the range.
	
	return {dateRangeStart, dateRangeEnd}
end getDateRange

-- Create a new TextEdit document with text derived from the gathered data.
on composeText(filteredData)
	tell application "TextEdit"
		-- Make a new document, with a minimal text so that we can discover the name of its font.
		set newDoc to (make new document with properties {text:" "})
		set baseFont to font of newDoc's text
		-- This ASSUMES that an equivalent bold font exists and that its name is the same as the plain font with " bold" appended.
		set boldFont to baseFont & " bold"
		-- Dummy text no longer needed.
		set newDoc's text to ""
		activate
	end tell
	
	if (filteredData is {}) then
		-- If no events have been discovered in the date range, print that fact.
		tell application "TextEdit" to make new paragraph at end of newDoc's text with data "No events found in this period." with properties {font:boldFont}
	else
		-- Otherwise print the event details.
		set currentCalendarDate to "" -- The calendar date currently being processed. (None yet.)
		repeat with i from 1 to (count filteredData)
			-- Get the data for an event from the list of filtered data.
			set {{date string:thisCalendarDate, hours:thisStartTimeH, minutes:thisStartTimeM}, eventAlreadyStarted, thisTimeZone, thisSummary, thisCalendar} to item i of filteredData
			-- If the calendar date is different from the one we've been processing till now, output an empty line and the new date string to TextEdit.
			if (thisCalendarDate is not currentCalendarDate) then
				tell application "TextEdit"
					make new paragraph at end of newDoc's text with data linefeed with properties {font:baseFont}
					make new paragraph at end of newDoc's text with data (thisCalendarDate & linefeed) with properties {font:boldFont}
				end tell
				-- Make the new date the one currently being processed.
				set currentCalendarDate to thisCalendarDate
			end if
			if (eventAlreadyStarted) then -- Continuation of multi-day event.
				set thisStartTime to " (already started)"
			else if (thisTimeZone is missing value) then -- All-day event.
				set thisStartTime to ""
			else -- Timed event.
				-- Create a 24-hour time string from the hours and minutes of the start date.
				tell (10000 + thisStartTimeH * 100 + thisStartTimeM) as text to set thisStartTime to " at " & text 2 thru 3 & ":" & text 4 thru 5
			end if
			-- Output the entry for this event to TextEdit.
			tell application "TextEdit" to make new paragraph at end of newDoc's text with data ("\"" & thisCalendar & "\" calendar: " & thisSummary & thisStartTime & linefeed) with properties {font:baseFont}
		end repeat
	end if
end composeText

on main()
	set {dateRangeStart, dateRangeEnd} to getDateRange()
	say "This script will finish before you can say \"Jack ."
	set filteredData to my fetchEventsStarting:dateRangeStart ending:dateRangeEnd
	composeText(filteredData)
	say ". Robinson\"!"
end main

main()