EvanLovely
5/17/2014 - 11:08 PM

New reminder from Launchbar

--Script for setting Reminders for LaunchBar and Alfred
--For Alfred, Applescript must NOT be set to run in Background otherwise date parsing does not work
--For LaunchBar, place the script in ~/Library/Scripts/LaunchBar
--by Michelle L. Gill, 10/07/2012
--Inspired by https://gist.github.com/3187630
--A related Alfred version 2 workflow can be found here: https://github.com/mlgill/alfred-workflow-create-reminder

--Changes
--02/01/2013 * Fixed an issue with setting the time when the hour is 12 and AM/PM (12-hour clock) is used
--            * Removed the ability to set seconds for the time since Reminders doesn't recognize them
--02/02/2013 * Fixed a regression in setting the date when only the hour is present
--           * Added the ability to return information from the created reminder as a string for error checking.
--           * This returned string can be sent to Growl (if installed) to post a notification. For Alfred, the 
--        option to post the returned string via Growl has been commented out by default
--        since the forthcoming version of Alfred will use this script via workflows and 
--        they have their own method of notification posting. If you're using alfred version 1,
--        uncomment the appropriate line below. Note that posting notifications via Mountain Lion's
--        Notification Center is not possible without incorporating additional tools (an application).
--        I **DO NOT** plan to add notification center support to this script for this reason. (However,
--        notification support can be used within the workflows in Alfred 2.

--Notes on valid reminders
--Order of the text must be "title #note $list @date time" but note
--List is optional if a default is set within the script
--Time must be included, but date can be omitted and is assumed to be today
--Time can be in 12- or 24-hour format, and 24-hour format is assumed if am/pm are absent
--Time can be either just the hour or colon separated (hour:minute)
--Numerical dates are listed as month/day/year and year isn't required
--Relative dates such as "Today" and "Tomorrow" can be used, as well as days of the week
--Relative dates are case instensitive and can be abbreviated, i.e. "mon" for "Monday"
--If the day of the week is entered as "Sunday" and today is Sunday, 7 days from today will be returned
--"Next" can be added to relative dates return an additional seven days from the appropriate day of the week */

--Examples of valid reminders:
--Buy Milk #note about milk $Personal @10/7/2012 5:00 PM
--Buy Milk #note about Milk @10/7/2012 5:00 PM
--Buy Milk $Personal @10/7/2012 5:00PM
--Buy Milk @10/7/2012 5:00 PM
--Buy Milk @10/7/12 5:00pm
--Buy Milk @10/7/12 5pm
--Buy Milk @10/7 5pm
--Buy Milk @Tomorrow 5pm
--Buy Milk @tomorrow 5pm
--Buy Milk @tom 5pm
--Buy Milk @Sunday 5pm   	    this returns 10/14/2012 if today is Sunday, 10/7/2012
--Buy Milk @sunday 5pm   	    this returns 10/14/2012 if today is Sunday, 10/7/2012
--Buy Milk @sun 5pm   	        this returns 10/14/2012 if today is Sunday, 10/7/2012
--Buy Milk @Next Sunday 5pm	this returns 10/21/2012 if today is Sunday, 10/7/2012
--Buy Milk @next sun 5pm	    this returns 10/21/2012 if today is Sunday, 10/7/2012
--Buy Milk @today 17		    this assumes time is in 24-hour format (i.e. 5pm)
--Buy Milk @today 5		    this also assumes time is in 24-hour format (i.e. 5am)
--Buy Milk @5pm               this also assumes time is in 12-hour format
--Buy Milk @5		            this also assumes time is in 24-hour format (i.e. 5am)

--set this to the name of the default Reminders list
property myDefaultList : "Personal"

-- for Alfred
on alfred_script(str)
	set theResultString to my parse_reminder_string(str)
	-- uncomment the next line to post a growl notification for Alfred version 1
	-- post_growl_notification(theResultString)
end alfred_script

-- for LaunchBar
on handle_string(str)
	set theResultString to my parse_reminder_string(str)
	-- post growl notification if available
	post_growl_notification(theResultString)
end handle_string


-- subroutines
on parse_reminder_string(str)
	--separate the time string and get the value
	set arrayWithDate to my theSplit(str, "@")
	if arrayWithDate's length > 1 then
		set theDate to my parseDate(getArrayValue(arrayWithDate, 2))
	end if
	
	--separate the Reminders list name if present
	set arrayWithListName to my theSplit(getArrayValue(arrayWithDate, 1), "$")
	if arrayWithListName's length > 1 then
		set listName to my getArrayValue(arrayWithListName, 2) as string
	else
		set listName to myDefaultList
	end if
	
	--separate the note for the reminder if present
	set arrayWithBody to my theSplit(getArrayValue(arrayWithListName, 1), "#")
	if arrayWithBody's length > 1 then
		set reminderBody to my getArrayValue(arrayWithBody, 2)
	else
		set reminderBody to ""
	end if
	
	set reminderName to getArrayValue(arrayWithBody, 1)
	
	tell application "Reminders"
		try
			set listName to list listName
			set newReminder to make new reminder with properties {name:reminderName, container:listName, body:reminderBody, remind me date:theDate}
			set theReminderIDString to id of newReminder as string
			set theResultString to my get_reminder_data(theReminderIDString)
		on error
			--set newReminder to make new reminder with properties {name:reminderName, container:listName, body:reminderBody}
			set theResultString to "There was an error creating the reminder."
		end try
	end tell
	return theResultString
end parse_reminder_string

on theSplit(theString, theDelim)
	set oldDelimiters to AppleScript's text item delimiters
	set AppleScript's text item delimiters to theDelim
	set theArray to every text item of theString
	set theTrimmedArray to {}
	repeat with currentSplitString in theArray
		if currentSplitString as string is not equal to "" then
			set currentTrimmedString to trim(currentSplitString)
			copy currentTrimmedString to the end of theTrimmedArray
		end if
	end repeat
	set AppleScript's text item delimiters to oldDelimiters
	return theTrimmedArray
end theSplit

on getArrayValue(array, location)
	return item location in array
end getArrayValue

on parseDate(theDateStr)
	-- todo: parsing pseudo-natural language date and time with applescript 
	-- is a pain and rife with special cases (bugs). should convert this
	-- subroutine to python and use a real natural language parser.
	
	set theDate to current date
	set time of theDate to 0
	
	-- parse the time --
	-- applescript's text parsing seems very promiscuous
	-- e.g. if letters end up part of the date, they are simply ignored rather than 
	--       producing an error. this is a lot of work to check for everything, so 
	--       it hasn't been implemented yet
	
	-- first check for am/pm
	set theDateStrOrig to theDateStr
	set theDateStr to getArrayValue(theSplit(theDateStr, "am"), 1) as string
	set theDateStr to getArrayValue(theSplit(theDateStr, "AM"), 1) as string
	
	if theDateStrOrig is not in theDateStr then
		set usesAM to true
	else
		set usesAM to false
	end if
	
	set theDateStrOrig to theDateStr
	set theDateStr to getArrayValue(theSplit(theDateStr, "pm"), 1) as string
	set theDateStr to getArrayValue(theSplit(theDateStr, "PM"), 1) as string
	
	if theDateStrOrig is not in theDateStr then
		set usesPM to true
	else
		set usesPM to false
	end if
	
	-- todo: should throw an error if usesPM and usesAM end up being true
	
	-- split the string into date and time
	set theDateTimeArray to theSplit(theDateStr, " ")
	
	-- get the time, which is the last element of theDateTimeArray
	set theTimeStr to getArrayValue(theDateTimeArray, -1) as string
	set theTimeArray to theSplit(theTimeStr, ":")
	
	-- get the time variables
	set theHours to item 1 of theTimeArray
	
	if theTimeArray's length > 1 then
		set theMinutes to item 2 of theTimeArray
	else
		set theMinutes to 0
	end if
	
	-- determine how to adjust the hours for am/pm
	if (theHours as integer = 12) and usesAM then
		set hoursAdjust to -12
	else if (theHours as integer ≠ 12) and usesPM then
		set hoursAdjust to 12
	else
		set hoursAdjust to 0
	end if
	
	-- calculate the time
	set time of theDate to (time of theDate) + (theHours + hoursAdjust) * 3600
	set time of theDate to (time of theDate) + theMinutes * 60
	
	-- now parse the date if present --
	if theDateTimeArray's length > 1 then
		set theDateArray to items 1 thru -2 of theDateTimeArray
		set theDateArrayLen to the count of items in theDateArray
		
		-- first see if this is a relative date involving "next"
		if theDateArrayLen = 2 then
			set theRelativeDateStr to getArrayValue(theDateArray, 1) as string
			set theWeekdayStr to getArrayValue(theDateArray, 2) as string
			
			if "next" is in theRelativeDateStr then
				-- setting forward 8 days which prevents one week from today being returned for "next sunday" if today is sunday
				set time of theDate to (time of theDate) + (8 * 24 * 3600)
			end if
			
			set theDate to getWeekday(theDate, theWeekdayStr)
		else
			-- look for backslashes to decide if this date is relative or numerical
			set theDateArraySplit to theSplit(getArrayValue(theDateArray, 1), "/")
			set theDateArraySplitLen to the count of items in theDateArraySplit
			
			if theDateArraySplitLen = 1 then
				-- relative date without "next", i.e. today, tomorrow, monday, etc.
				set theRelativeDayStr to getArrayValue(theDateArraySplit, 1) as string
				
				if "tod" is in theRelativeDayStr then
					set theDate to theDate
				else if "tom" is in theRelativeDayStr then
					set time of theDate to (time of theDate) + (24 * 3600)
				else
					-- this prevents today's date from being returned if current day of week is entered
					set time of theDate to (time of theDate) + (24 * 3600)
					set theDate to getWeekday(theDate, theRelativeDayStr)
				end if
			else
				-- numerical date
				set month of theDate to (item 1 of theDateArraySplit) as integer
				set day of theDate to (item 2 of theDateArraySplit) as integer
				try
					set theNewYear to (item 3 of theDateArraySplit) as integer
					if theNewYear < 100 then
						set theCurrentYear to (year of theDate) as integer
						set theNewYear to theNewYear + ((theCurrentYear / 100) as integer) * 100
					end if
					set year of theDate to theNewYear
				end try
			end if
		end if
	end if
	
	return theDate
end parseDate

on get_reminder_data(reminderIDString)
	tell application "Reminders"
		-- get the reminder reference
		set theReminder to reminder id reminderIDString
		-- get the reminder properties
		set theRemProps to properties of theReminder
		-- get the list properties
		set theListProps to properties of container of theReminder
		-- title, due date, note from reminder properties
		set theTitle to name of theRemProps
		set theDueDate to due date of theRemProps
		set theNote to body of theRemProps
		-- list name from list properties
		set theListName to name of theListProps
	end tell
	-- create a string with info
	set theString to "Created Reminder '" & theTitle & "' on list '" & theListName & "' due on " & theDueDate
	if theNote is not "" then
		set theString to theString & " with note '" & theNote & "'"
	end if
	set theString to theString & "."
	return theString
end get_reminder_data

on post_growl_notification(theString)
	-- crap that Growl needs before it will post a reminder
	set allNotificationsList to {"Create Reminder Result"}
	set enabledNotificationsList to {"Create Reminder Result"}
	
	try
		tell application "Growl"
			register as application ¬
				"Create Reminder" all notifications allNotificationsList ¬
				default notifications enabledNotificationsList ¬
				icon of application "Reminders"
			notify with name ¬
				"Create Reminder Result" title ¬
				"Create Reminder Result" description ¬
				theString application name "Create Reminder"
		end tell
	end try
end post_growl_notification

on getWeekday(theDate, theWeekdayStr)
	repeat until theWeekdayStr is in ((weekday of theDate) as string)
		set time of theDate to (time of theDate) + (24 * 3600)
	end repeat
	return theDate
end getWeekday

on trim(someText)
	-- trimming subroutine from: http://macscripter.net/viewtopic.php?id=18519
	repeat until someText does not start with " "
		set someText to text 2 thru -1 of someText
	end repeat
	
	repeat until someText does not end with " "
		set someText to text 1 thru -2 of someText
	end repeat
	
	return someText
end trim