AppleScript to create list of Events over a specified date range

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

Unfortunately that’s not so. Suppose on Monday morning I add a lunch event, and set it to repeat five times. If I run the fetch events part of the script for, say, Wednesday, it will include the lunch event, which I think is what is wanted. But the start date of the lunch will still be on Monday (mercifully, so will the end date).

That’s not what I see here. If I create a lunch event on any day and set it to repeat five times (ie. on five consecutive days), your library returns data for each repeat occurring on a day covered by the script, individually dated as per the day covered. Only events (single or repeat instances) whose start and end dates straddle the start of the period are returned as having start dates preceding it. (The comment you quoted is misleadingly phrased. I’ll correct it when I post this.)

Ah, I didn’t realise that using fetch events also mean the returned events had their start/end dates changed appropriately – I was looking at that part in Calendar.app, which was still showing the original date.

Don’t miss that sale :slight_smile:

I didn’t. :wink:

After stumbling across this gem of a thread, I began trying to tweak Nigel’s first iteration of the script as it was serving my needs rather nicely with the exception that it does not print start/end times.

Really though, I’m not sure which is easier to use (and frankly I’m a bit lost on how to implement it). My only needs are that it saves to a text file a list of events in a given date range, with their start datetimes and end datetimes (no need for pretty formatting or entries listed under each day).

Hi raulium. Welcome to MacScripter.

As you’ll have picked up from the above discussion, the first version of my script was written for a specific requirement and is very slow even at that. However, it’s quite easy to patch for what you say you need and ” to give you a reply in a reasonable time ” the version below is the result. It includes the start dates/times and end dates/ times and saves the text to a file on the desktop instead of showing it in a TextEdit document. Since it has to fetch the events’ end dates as well as the summaries and start dates, it takes half as long again as the original! Edit the property at the top of the script so that the text is the name of your calendar.

I’ll try and sort out a faster (and hopefully better written) version at more leisure and when I know it does what you want.

property calendarName: "Home" -- Edit as required.

-- 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, dateRange} -- Return dateRange too to use in the file name.
end getDateRange

-- Return the start dates and summaries which are in the given date range.
on filterToDateRange(theStartDates, theEndDates, theSummaries, dateRangeStart, dateRangeEnd)
	set {eventDatesInRange, endDatesInRange, 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 endDatesInRange to item i of theEndDates
			set end of eventSummariesInRange to item i of theSummaries
		end if
	end repeat
	
	return {eventDatesInRange, endDatesInRange, eventSummariesInRange}
end filterToDateRange

-- Sort both the start-date and summary lists by start date.
on sortByDate(eventDatesInRange, endDatesInRange, eventSummariesInRange)
	-- A sort-customisation object for sorting the summary list AND the end date list in parallel with the start date list.
	script custom
		property endDates : endDatesInRange
		property summaries : eventSummariesInRange
		
		on swap(i, j)
			tell item i of my endDates
				set item i of my endDates to item j of my endDates
				set item j of my endDates to it
			end tell
			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, endDatesInRange, eventSummariesInRange)
	set txt to ""
	set gap to linefeed & linefeed
	
	set matchCount to (count eventDatesInRange)
	if (matchCount is 0) then return "No events found between the specfied dates."
	repeat with i from 1 to matchCount
		-- The dates are automatically coerced to text by concatentation to it.
		set txt to txt & item i of eventDatesInRange & (("-" & item i of endDatesInRange) & (tab & item i of eventSummariesInRange & gap))
	end repeat
	
	return text 1 thru -3 of txt
end composeText

on main()
	tell application "Calendar" to set {theStartDates, theEndDates, theSummaries} to {start date, end date, summary} of events of calendar calendarName
	
	set {dateRangeStart, dateRangeEnd, dateRangeEntered} to getDateRange()
	set {eventDatesInRange, endDatesInRange, eventSummariesInRange} to filterToDateRange(theStartDates, theEndDates, theSummaries, dateRangeStart, dateRangeEnd)
	sortByDate(eventDatesInRange, endDatesInRange, eventSummariesInRange)
	set txt to composeText(eventDatesInRange, endDatesInRange, eventSummariesInRange)
	
	set fileName to "Events " & dateRangeEntered & ".txt"
	set filePath to (path to desktop as text) & fileName
	
	set fRef to (open for access file filePath with write permission)
	try
		set eof fRef to 0
		write txt as «class utf8» to fRef
		close access fRef
	on error errMsg
		close access fRef
		display dialog errMsg buttons {"OK"} default button 1
		error number -128
	end try
	display dialog "Data saved to file "" & fileName & "" on the desktop"
end main

main()

Thank you kindly. I’m very keen in automating a lot of my life, and I’m quickly realizing that applescript will be one of my best of friends – so I’m happy to have found this place.

Now I’m seeing this manifest itself, I’m appreciating the inclusion of CalendarLib in the prior post. It would be nice to have the individual occurrence of the event show up in the output list (the fact it’s recurring is more info than I need for my purposes). Everything else is exactly as I requested, so your efforts are appreciated.

OK. See how this goes. This time, the calendar name property to edit to your requirements is calendarList, which must be a list containing the relevant calendar name(s).

-- 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.
-- This version simply (!) lists events one per line and saves the resulting text to a file on the user's desktop.
-- Tested with CalendarLIb EC in Mac OS 10.11.5.

use script "CalendarLib EC"
use scripting additions

property calendarList : {"Home", "Work"} -- Edit as required. An empty list means all calendars.

main()

on main()
	set {dateRangeStart, dateRangeEnd} to getDateRange()
	set dataList to my fetchEventsStarting:dateRangeStart ending:dateRangeEnd
	set eventText to composeText(dataList)
	set fileName to "Event list " & short date string of dateRangeStart & "-" & short date string of dateRangeEnd & ".txt"
	saveToFile(eventText, fileName)
	set userReaction to button returned of (display dialog "The event information has been saved to a file called “" & fileName & "” on your desktop." with icon note buttons {"Reveal", "Open", "Thanks!"} default button 3)
	if (userReaction is "Reveal") then
		tell application "Finder"
			activate
			reveal file fileName of desktop
		end tell
	else if (userReaction is "Open") then
		tell application "Finder" to open file fileName of desktop
	end if
end main

-- 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
	-- 'fetch store', 'fetch calendars', and 'fetch events' are from CalendarLib.
	set theStore to (fetch store)
	set theCals to (fetch calendars calendarList cal type list {} event store theStore)
	-- It's come to light that the system method which fetches the events won't get more than four years of them at a time. CalendarLib will no doubt be updated to work round this in time, but here's a work-round anyway.
	set theEvents to {}
	set fromDate to dateRangeStart
	set toDate to fromDate - 1
	set justUnderFourYears to (365 * 4) * days
	repeat until (fromDate comes after dateRangeEnd)
		set toDate to toDate + justUnderFourYears
		if (toDate comes after dateRangeEnd) then set toDate to dateRangeEnd
		set theEvents to theEvents & (fetch events starting date fromDate ending date toDate searching cals theCals event store theStore)
		set fromDate to fromDate + justUnderFourYears
	end repeat
	
	script o
		property dataList : {}
	end script
	
	repeat with anEvent in theEvents
		-- Get the start date, end date, time zone, and summary of each returned event.
		set {event_start_date:thisStartDate, event_end_date:thisEndDate, event_time_zone:thisTimeZone, event_summary:thisSummary} to (event info for event anEvent) -- CalendarLib.
		-- If a returned event starts before the date range entered by the user, note the fact and set its start date to that of the range.
		set startedBeforePeriod to (thisStartDate comes before dateRangeStart)
		if (startedBeforePeriod) then set thisStartDate to dateRangeStart
		-- With an all-day event (no associated time zone), nudge its end time forward by a second to 23:59:59 on the event day.
		set allDayEvent to (thisTimeZone is missing value)
		if (allDayEvent) then set thisEndDate to thisEndDate - 1
		-- Store the start date, 'already started' flag, end date, 'all-day event' flag, and summary. If a multi-day event, add discrete entries for each date it occupies in the range.
		repeat until ((thisStartDate comes after thisEndDate) or (thisStartDate comes after dateRangeEnd))
			set end of o's dataList to {thisStartDate, startedBeforePeriod, thisEndDate, allDayEvent, thisSummary}
			set thisStartDate to thisStartDate + days
			set thisStartDate's time to 0
			set startedBeforePeriod to true
		end repeat
	end repeat
	
	-- Sort the entries by start date.
	sortByStartDate(o's dataList)
	
	return o's dataList
end fetchEventsStarting:ending:

-- Sort the filtered data by start date.
on sortByStartDate(dataList)
	-- 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(dataList, 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

-- Derive a text from the gathered data.
on composeText(dataList)
	-- dataList = list of {{thisStartDate, startedBeforePeriod, thisEndDate, allDayEvent, thisSummary}, {thisStartDate, …
	if (dataList is {}) then
		return "No events found in this period."
	else
		script o
			property workList : dataList
		end script
		repeat with i from 1 to (count dataList)
			-- Get the data for an event/day from the list of filtered data.
			set {{date string:thisCalendarStartDate, hours:thisStartTimeH, minutes:thisStartTimeM}, startedBeforeToday, {date string:thisCalendarEndDate, hours:thisEndTimeH, minutes:thisEndTimeM}, allDayEvent, thisSummary} to item i of o's workList
			set endsAfterToday to (thisCalendarEndDate is not thisCalendarStartDate)
			
			-- Begin the entry for the event/day with a suitable expression of its start date.
			set thisEntry to thisCalendarStartDate
			if (startedBeforeToday) then
				set thisEntry to thisEntry & " (already started, ends "
			else if (not allDayEvent) then
				tell (10000 + thisStartTimeH * 100 + thisStartTimeM) as text to set thisStartTime to space & text 2 thru 3 & ":" & text 4 thru 5
				set thisEntry to thisEntry & thisStartTime & "-"
			else if (endsAfterToday) then
				set thisEntry to thisEntry & "-"
			end if
			-- If appropriate, append a suitable expression of the end time.
			if (endsAfterToday) then
				set thisEntry to thisEntry & thisCalendarEndDate
			else if (startedBeforeToday) then
				set thisEntry to thisEntry & "today"
			end if
			if (not allDayEvent) then
				tell (10000 + thisEndTimeH * 100 + thisEndTimeM) as text to set thisEndTime to text 2 thru 3 & ":" & text 4 thru 5
				if ((startedBeforeToday) or (endsAfterToday)) then set thisEndTime to space & thisEndTime
				set thisEntry to thisEntry & thisEndTime
			end if
			if (startedBeforeToday) then set thisEntry to thisEntry & ")"
			-- Append a tab and the event's summary.
			set thisEntry to thisEntry & (tab & thisSummary)
			-- Store this entry.
			set item i of o's workList to thisEntry
		end repeat
		
		-- When all the entries are ready, coerce to a single text with linefeeds and return the result.
		set astid to AppleScript's text item delimiters
		set AppleScript's text item delimiters to linefeed
		set outputText to o's workList as text
		set AppleScript's text item delimiters to astid
		
		return outputText
	end if
end composeText

-- Save the text to a file on the desktop.
on saveToFile(txt, fileName)
	set filePath to (path to desktop as text) & fileName
	set fRef to (open for access file filePath with write permission)
	try
		set eof fRef to 0
		write txt as «class utf8» to fRef
		close access fRef
	on error errMsg
		close access fRef
		display dialog errMsg with icon stop buttons {"OK"} default button 1
		error number -128
	end try
end saveToFile

Edits: Minor tidying up of script code and comments, minor bug fixes, work-round for just discovered “four year” bug in the system method CalendarLib uses to get the events.
27th October 2018: Characters corrupted in a later MacScripter BBS software update restored to the originals.

Came across this wonderful thread today and it was very helpful. Thank you Nigel & Shane!

My use case is to filter out the resulting list of events to only those which are named “Weekly Check (K/T)” because I need to send just the dates and times of those events to my supervisor.

Would anyone be able to provide an additional tweak?

Hi. Welcome to MacScripter.

Ideally, the script in post #28 would be rewritten to accommodate filtering by various user-entered criteria. But as a crude patch for your own requirement, you could simply insert this line …

set theEvents to (filter events event list theEvents event summary "Weekly Check (K/T)") -- ADDED LINE.

… between the first ‘end repeat’ and the ‘script o’ in the ‘fetchEventsStarting:dateRangeStart ending:dateRangeEnd’ handler.

Works like a dream! Thank you.

Old thread, but hoping someone can point me in the right direction. Nigel Garvey’s script in post 28 functions perfectly (and, I did not install Shane Stanley’s CalendarLib because I couldn’t find it anywhere).

My question: If you include multiple calendars, the list comes out by date with all calendars. How would I change (or add to the script) to have the list come out by Calendar, then date? So, if I have five calendars I’d end up with five lists, by date.

Hi @Homer712.

My script in post 28 relies on Shane’s library, so your statement doesn’t make sense. :thinking:

Anyway, Shane’s libraries are now here on the Late Night Software (ie. Script Debugger) site. Here’s a version of the script which includes the calendar names and sorts the events first on those and then on the start dates:

-- Faster version using ASObjC.
-- Requires Shane Stanley's CalendarLib library. (Use the EC version with El Capitan or later.)
-- <https://latenightsw.com/freeware/>.

-- 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.
-- This version simply (!) lists events one per line and saves the resulting text to a file on the user's desktop.
-- Tested with CalendarLIb EC in Mac OS 15.1.1.

use script "CalendarLib EC"
use scripting additions

property calendarList : {"Home", "Work"} -- Edit as required. An empty list means all calendars.

main()

on main()
	set {dateRangeStart, dateRangeEnd} to getDateRange()
	set dataList to my fetchEventsStarting:dateRangeStart ending:dateRangeEnd
	set eventText to composeText(dataList)
	set fileName to "Event list " & short date string of dateRangeStart & "-" & short date string of dateRangeEnd & ".txt"
	saveToFile(eventText, fileName)
	set userReaction to button returned of (display dialog "The event information has been saved to a file called “" & fileName & "” on your desktop." with icon note buttons {"Reveal", "Open", "Thanks!"} default button 3)
	if (userReaction is "Reveal") then
		tell application "Finder"
			activate
			reveal file fileName of desktop
		end tell
	else if (userReaction is "Open") then
		tell application "Finder" to open file fileName of desktop
	end if
end main

-- 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
	-- 'fetch store', 'fetch calendars', and 'fetch events' are from CalendarLib.
	set theStore to (fetch store)
	set theCals to (fetch calendars calendarList cal type list {} event store theStore)
	-- It's come to light that the system method which fetches the events won't get more than four years of them at a time. CalendarLib will no doubt be updated to work round this in time, but here's a work-round anyway.
	set theEvents to {}
	set fromDate to dateRangeStart
	set toDate to fromDate - 1
	set justUnderFourYears to (365 * 4) * days
	repeat until (fromDate comes after dateRangeEnd)
		set toDate to toDate + justUnderFourYears
		if (toDate comes after dateRangeEnd) then set toDate to dateRangeEnd
		set theEvents to theEvents & (fetch events starting date fromDate ending date toDate searching cals theCals event store theStore)
		set fromDate to fromDate + justUnderFourYears
	end repeat
	
	script o
		property dataList : {}
	end script
	
	repeat with anEvent in theEvents
		-- Get the start date, end date, time zone, and summary of each returned event.
		set {calendar_name:thisCalendarName, event_start_date:thisStartDate, event_end_date:thisEndDate, event_time_zone:thisTimeZone, event_summary:thisSummary} to (event info for event anEvent) -- CalendarLib.
		-- If a returned event starts before the date range entered by the user, note the fact and set its start date to that of the range.
		set startedBeforePeriod to (thisStartDate comes before dateRangeStart)
		if (startedBeforePeriod) then set thisStartDate to dateRangeStart
		-- With an all-day event (no associated time zone), nudge its end time forward by a second to 23:59:59 on the event day.
		set allDayEvent to (thisTimeZone is missing value)
		if (allDayEvent) then set thisEndDate to thisEndDate - 1
		-- Store the start date, 'already started' flag, end date, 'all-day event' flag, and summary. If a multi-day event, add discrete entries for each date it occupies in the range.
		repeat until ((thisStartDate comes after thisEndDate) or (thisStartDate comes after dateRangeEnd))
			set end of o's dataList to {thisCalendarName, thisStartDate, startedBeforePeriod, thisEndDate, allDayEvent, thisSummary}
			set thisStartDate to thisStartDate + days
			set thisStartDate's time to 0
			set startedBeforePeriod to true
		end repeat
	end repeat
	
	-- Sort the entries by start date.
	sortByCalendarAndStartDate(o's dataList)
	
	return o's dataList
end fetchEventsStarting:ending:

-- Sort the filtered data by start date.
on sortByCalendarAndStartDate(dataList)
	-- Comparison object for a customisable sort. Compares the first items of two passed lists.
	script byFirstListItem
		on isGreater(a, b)
			return ((a's beginning > b's beginning) or ((a's beginning = b's beginning) and (a's second item > b's second item)))
		end isGreater
	end script
	
	CustomInsertionSort(dataList, 1, -1, {comparer:byFirstListItem})
end sortByCalendarAndStartDate

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

-- Derive a text from the gathered data.
on composeText(dataList)
	-- dataList = list of {{thisCalendarName, thisStartDate, startedBeforePeriod, thisEndDate, allDayEvent, thisSummary}, {thisStartDate, …
	if (dataList is {}) then
		return "No events found in this period."
	else
		script o
			property workList : dataList
		end script
		repeat with i from 1 to (count dataList)
			-- Get the data for an event/day from the list of filtered data.
			set {thisCalendarName, {date string:thisCalendarStartDate, hours:thisStartTimeH, minutes:thisStartTimeM}, startedBeforeToday, {date string:thisCalendarEndDate, hours:thisEndTimeH, minutes:thisEndTimeM}, allDayEvent, thisSummary} to item i of o's workList
			set endsAfterToday to (thisCalendarEndDate is not thisCalendarStartDate)
			
			-- Begin the entry for the event/day with the calendar name and a suitable expression of the start date.
			set thisEntry to thisCalendarName & tab & thisCalendarStartDate
			if (startedBeforeToday) then
				set thisEntry to thisEntry & " (already started, ends "
			else if (not allDayEvent) then
				tell (10000 + thisStartTimeH * 100 + thisStartTimeM) as text to set thisStartTime to space & text 2 thru 3 & ":" & text 4 thru 5
				set thisEntry to thisEntry & thisStartTime & "-"
			else if (endsAfterToday) then
				set thisEntry to thisEntry & "-"
			end if
			-- If appropriate, append a suitable expression of the end time.
			if (endsAfterToday) then
				set thisEntry to thisEntry & thisCalendarEndDate
			else if (startedBeforeToday) then
				set thisEntry to thisEntry & "today"
			end if
			if (not allDayEvent) then
				tell (10000 + thisEndTimeH * 100 + thisEndTimeM) as text to set thisEndTime to text 2 thru 3 & ":" & text 4 thru 5
				if ((startedBeforeToday) or (endsAfterToday)) then set thisEndTime to space & thisEndTime
				set thisEntry to thisEntry & thisEndTime
			end if
			if (startedBeforeToday) then set thisEntry to thisEntry & ")"
			-- Append a tab and the event's summary.
			set thisEntry to thisEntry & (tab & thisSummary)
			-- Store this entry.
			set item i of o's workList to thisEntry
		end repeat
		
		-- When all the entries are ready, coerce to a single text with linefeeds and return the result.
		set astid to AppleScript's text item delimiters
		set AppleScript's text item delimiters to linefeed
		set outputText to o's workList as text
		set AppleScript's text item delimiters to astid
		
		return outputText
	end if
end composeText

-- Save the text to a file on the desktop.
on saveToFile(txt, fileName)
	set filePath to (path to desktop as text) & fileName
	set fRef to (open for access file filePath with write permission)
	try
		set eof fRef to 0
		write txt as «class utf8» to fRef
		close access fRef
	on error errMsg
		close access fRef
		display dialog errMsg with icon stop buttons {"OK"} default button 1
		error number -128
	end try
end saveToFile

You’re absolutely correct about having the library installed. I had started a thread here: Printing iCal Events

Where in the first post, I referenced this very thread. I’m going to blame it on advancing age :rofl:

Thank you for the revised script.