content: -- 1.
  -- Set start date or due date in terms of each other or themselves
  -- Define Start Date in terms of  self or due date +/-  N days/weeks
  -- Define Due Date in terms of  self or start date +/-  N days/weeks
  -- 2.
  -- Set start or due dates using a variety of relative and absolute expressions
  
  property pTitle : "Set relative (Start|Due) dates"
  property pVer : "Ver .037"
  
  -- 	Copyright © 2011, Robin Trew
  --  All rights reserved.
  -- 	
  -- 	Redistribution and use in source and binary forms, with or without modification, 
  -- 	are permitted provided that the following conditions are met:
  -- 	
  -- 		- Redistributions of source code must retain the above copyright notice, 
  -- 		  this list of conditions and the following disclaimer.
  -- 		- Redistributions in binary form must reproduce the above copyright notice, 
  -- 		  this list of conditions and the following disclaimer in the documentation 
  -- 		  and/or other materials provided with the distribution.
  -- 		
  -- 	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
  -- 	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
  -- 	THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
  -- 	IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 
  -- 	ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  -- 	 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
  -- 	 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
  -- 	 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
  -- 	 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  
  property pstrSample : short date string of ((current date) + (14 * days))
  property pstrEnter : "Enter command:"
  property pstrSyntax : "NOTE: 'start' and 'due' below can be abbreviated to 'sd' and 'dd', or just 's' and 'd')
  
  DEFER OR BRING AHEAD:
  	start=start+7d
  	due=due-1w
  DEFINE START OR DUE IN TERMS OF EACH OTHER:
  	due=start+2w
  	start=due-5d
  SIMPLE RELATIVE OR ABSOLUTE SETTINGS:
  	start=<sep>
  	start=tomorrow
  	start=today+2w
  	due=now+2d
  	due=" & pstrSample & "
  DEFAULTS
  	Expressions with no left-hand side 
  	are interpreted as references to due dates.
  	Interval strings with no units are interpreted
  	as a number of days.
  	
  Separate multiple commands with commas or semi-colons
  e.g.
  	s=today+10d, d=s+2w
  	d=" & pstrSample & ", s=d-7
  	
  or, to simply delete dates:
  	s=clear, d=clear"
  
  property pCancel : "Cancel"
  property pHelp : "Help"
  property pGrowlURL : "http://growl.info/"
  
  -- SCRIPT BEHAVIOUR OPTIONS
  property pblnUseDialog : false -- always use a display dialog rather than just a Growl notification
  property plngGrowlNamedItems : 5 -- Max no. of selected tasks/projects to name in Growl report. (With longer lists Growl will just give a count).
  
  on handle_string(strCmd)
  	Apply2SeldTasks(strCmd)
  end handle_string
  
  on run
  	-- CHECK WHETHER THERE ARE ANY SELECTED TASKS OR PROJECTS
  	tell application id "OFOC"
  		tell front document window of front document
  			if (count of (selected trees of content where class of its value is not item and class of its value is not folder)) < 1 then
  				if (count of (selected trees of sidebar where class of its value is not item and class of its value is not folder)) < 1 then return
  			end if
  		end tell
  		
  		-- TRY TO GET A COMMAND STRING
  		set {strCmd, strSyntax} to {"", "Enter command:"}
  		repeat while strCmd = ""
  			activate
  			tell (display dialog strSyntax default answer "" buttons {pCancel, pHelp, "Set date(s)"} default button 3 with title pTitle & space & pVer)
  				set {strCmd, strButton} to {text returned, button returned}
  			end tell
  			
  			-- ALLOW USER TO TOGGLE HELP ON AND OFF
  			if strButton = pHelp then
  				if strSyntax ≠ pstrSyntax then
  					set strSyntax to pstrSyntax
  				else
  					set strSyntax to pstrEnter
  				end if
  			end if
  		end repeat
  		
  		-- PROCESS ANY COMMAND STRING
  		if strCmd ≠ "" then my Apply2SeldTasks(strCmd)
  	end tell
  end run
  
  on Apply2SeldTasks(strCmd)
  	if strCmd = "" then return
  	
  	-- CHECK THAT THERE ARE SELECTED TASKS
  	tell application id "OFOC"
  		-- SHOW HELP, IF REQUESTED
  		if my PatternMatch(strCmd, "^(\\?|help)") > 0 then
  			activate
  			display dialog pstrSyntax buttons "OK" default button 1 with title pTitle
  			return
  		end if
  		
  		tell front document
  			tell front document window
  				set lstTasks to value of (selected trees of content where class of its value is not item and class of its value is not folder)
  				set lngTasks to (count of lstTasks)
  				if lngTasks < 1 then
  					set lstTasks to value of (selected trees of sidebar where class of its value is not item and class of its value is not folder)
  					set lngTasks to (count of lstTasks)
  					if lngTasks < 1 then return
  				end if
  			end tell
  			
  			-- 	GET THE DEFAULT DUE TIME FOR THE ACTIVE OMNIFOCUS DOCUMENT
  			set strDefaultDueTime to (value of setting id "DefaultDueTime")
  			set lstParts to my split("[^0-9]", strDefaultDueTime)
  			if (count of lstParts) ≠ 3 then error "DefaultDueTime could not be parsed: " & strDefaultDueTime
  			set {strHours, strMins, strSecs} to lstParts
  			set lngDefaultDueTime to (strHours * 3600) + (strMins * 60) + (strSecs as integer)
  		end tell
  	end tell
  	
  	-- PARSE THE COMMAND(S)
  	set lstCmds to ParseCmd(strCmd)
  	if lstCmds = {} then return
  	
  	-- LOAD RELATIVE DATES LIBRARY
  	set libOFDates to load script ((path to me) as string) & "Contents:Resources:" & "OF_DateLib.scpt" as alias
  	
  	-- PROCESS THE COMMANDS, WHILE BUILDING A LOG
  	set {lngDue, lngStart} to {0, 0}
  	
  	-- GET DATE/TIMES FOR NOW, THE START OF THE TODAY, AND THE DEFAULT DUE TIME FOR TODAY
  	set {dteNow, dteTodayStart, dteTodayDue} to {current date, current date, current date}
  	set time of dteTodayStart to 0
  	set time of dteTodayDue to lngDefaultDueTime -- The setting under OmniFocus > Preferences > Default time for due dates
  	
  	set strCMDLog to ""
  	tell application id "OFOC"
  		repeat with oCmd in lstCmds
  			set {strUnParsed, blnReflexive, blnReadStart, blnWriteStart, strExpression} to oCmd
  			if strUnParsed ≠ "" then
  				set strCMDLog to strCMDLog & quoted form of strUnParsed & " could not be parsed as start or due" & return
  			else
  				if blnReflexive then
  					-- CALCULATE THE REQUIRED OFFSET AS A NUMBER OF SECONDS
  					set dteNew to libOFDates's DatePlus(dteNow, strExpression)
  					if dteNew is not missing value then
  						set lngSecs to dteNew - dteNow
  						
  						if lngSecs ≠ 0 then
  							-- APPLY THE OFFSET AS REQUIRED
  							if blnReadStart then
  								if blnWriteStart then -- ADJUST START DATES BY SPECIFIED AMOUNT OF TIME
  									repeat with oTask in lstTasks
  										set dteStart to start date of oTask
  										if dteStart is missing value then set dteStart to dteTodayStart
  										set start date of oTask to (dteStart) + lngSecs
  										set lngStart to lngStart + 1
  									end repeat
  								else -- SET DUE DATES IN RELATION TO START DATES
  									repeat with oTask in lstTasks
  										set dteStart to start date of oTask
  										if dteStart is missing value then
  											set dteStart to dteTodayStart
  											set start date of oTask to dteTodayStart
  											set due date of oTask to dteStart + lngSecs + lngDefaultDueTime
  										else
  											if time of dteStart = 0 then
  												set due date of oTask to (dteStart + lngSecs + lngDefaultDueTime)
  											else
  												set due date of oTask to (dteStart + lngSecs)
  											end if
  										end if
  										set lngDue to lngDue + 1
  									end repeat
  								end if
  							else -- SET START DATES IN RELATION TO DUE DATES
  								if blnWriteStart then
  									if lngSecs > 0 then
  										activate
  										tell (display dialog "This would set the start date later than the due date:" & return & ¬
  											strCmd buttons {"Skip", "Confirm procrastination"} default button "Skip" with title pTitle)
  											set blnSkip to button returned = "Skip"
  										end tell
  									else
  										set blnSkip to false
  									end if
  									
  									if blnSkip then
  										set strCMDLog to strCMDLog & "Skipped setting of Start later than Due" & return
  									else
  										repeat with oTask in lstTasks
  											set dteDue to due date of oTask
  											if dteDue is missing value then
  												set due date of oTask to dteTodayDue
  												set dteStart to dteTodayStart + lngSecs
  											else
  												set dteStart to (dteDue + lngSecs)
  											end if
  											set time of dteStart to 0 -- begin at midnight, by default
  											set start date of oTask to dteStart
  											
  											set lngStart to lngStart + 1
  										end repeat
  									end if
  								else -- ADJUST DUE DATES BY A SPECIFIED AMOUNT OF TIME
  									repeat with oTask in lstTasks
  										set dteDue to due date of oTask
  										if dteDue is missing value then set dteDue to dteTodayDue
  										set due date of oTask to (dteDue) + lngSecs
  										set lngDue to lngDue + 1
  									end repeat
  								end if
  							end if
  						else
  							set strCMDLog to strCMDLog & "No change" & return
  						end if
  					else
  						set strCMDLog to strCMDLog & quoted form of strExpression & " could not be parsed as a date." & return
  					end if
  				else
  					-- APPLY ONE DATE TO ALL THE SELECTED TASKS
  					if strExpression ≠ "clear" then
  						set dteNew to libOFDates's DateExpression(strExpression)
  						if (dteNew is not missing value) then
  							tell dteNew
  								if not blnWriteStart then if its time = 0 then set its time to lngDefaultDueTime
  								set strCMDLog to strCMDLog & strExpression & " = " & short date string & space & rich text 1 thru 5 of time string & return
  							end tell
  							if blnWriteStart then
  								repeat with oTask in lstTasks
  									set start date of oTask to dteNew
  								end repeat
  								set lngStart to lngStart + lngTasks
  							else
  								repeat with oTask in lstTasks
  									set due date of oTask to dteNew
  								end repeat
  								set lngDue to lngDue + lngTasks
  							end if
  						else
  							set strCMDLog to strCMDLog & quoted form of strExpression & " could not be parsed as a date" & return
  						end if
  					else -- CLEAR START OR DUE DATES FROM SELECTED TASKS
  						set strCMDLog to strCMDLog
  						if blnWriteStart then
  							repeat with oTask in lstTasks
  								set start date of oTask to missing value
  							end repeat
  							set strCMDLog to strCMDLog & my pl("start date", lngTasks) & " cleared" & return
  						else
  							repeat with oTask in lstTasks
  								set due date of oTask to missing value
  							end repeat
  							set strCMDLog to strCMDLog & my pl("due date", lngTasks) & " cleared" & return
  						end if
  					end if
  				end if
  			end if
  		end repeat
  	end tell
  	
  	-- REPORT RESULTS TO THE USER
  	set strLog to BuildLog(lstTasks, lngTasks, strCmd, strCMDLog, lngDue, lngStart)
  	
  	if pblnUseDialog or (lngDue + lngStart) < 1 then
  		tell application id "OFOC"
  			activate
  			display dialog strLog buttons "OK" default button 1 with title pTitle
  		end tell
  	else
  		Notify(pTitle, strLog) -- use Growl if installed (otherwise fall back to display dialog)
  	end if
  end Apply2SeldTasks
  
  on BuildLog(lstTasks, lngTasks, strCmd, strCMDLog, lngDue, lngStart)
  	set strLog to pl("item", lngTasks) & " selected"
  	if lngTasks ≤ plngGrowlNamedItems then
  		set strLog to strLog & ":" & return
  		set {dlm, my text item delimiters} to {my text item delimiters, space}
  		repeat with i from 1 to lngTasks
  			set strName to (name of item i of lstTasks)
  			if (count of words of strName) > 4 then set strName to ((words 1 thru 4 of strName) as string) & " ..."
  			set strLog to strLog & "• " & strName & return
  		end repeat
  		set my text item delimiters to dlm
  		set strLog to strLog & return
  	else
  		set strLog to strLog & return & return
  	end if
  	set strLog to strLog & strCmd & return & return
  	if strCMDLog ≠ "" then set strLog to strLog & "(" & text 1 thru -2 of strCMDLog & ")" & return & return
  	if lngStart > 0 then set strLog to strLog & pl("start date", lngStart) & " set" & return
  	if lngDue > 0 then set strLog to strLog & pl("due date", lngDue) & " set" & return
  	return strLog
  end BuildLog
  
  -- {{blnReflexive, blnReadStart, blnWriteStart, strExpression}, ...}
  on ParseCmd(strCmds)
  	-- NORMALISE ANY COMMAS TO SEMI-COLON COMMAND SEPARATORS
  	set strText to Replace(strCmds, ",", ";")
  	
  	-- DIVIDE THE STRING INTO SEPARATE COMMANDS
  	set {dlm, my text item delimiters} to {my text item delimiters, ";"}
  	set lstExpressions to text items of strText
  	
  	-- BUILD A LIST OF PARSED COMMANDS
  	set lstCmd to {}
  	
  	repeat with oCmd in lstExpressions
  		set strUnParsed to ""
  		
  		-- DIVIDE THE EXPRESSION INTO LEFT AND RIGHT SIDES
  		set my text item delimiters to "="
  		set lstSides to text items of oCmd
  		if (count of lstSides) > 1 then
  			set {strLeft, strRight} to {my Trim(item 1 of lstSides), my Trim(item 2 of lstSides)}
  			
  			-- DETERMINE WHETHER DATE TO BE SET/ADJUSTED IS START OR DUE
  			set blnWriteStart to (my PatternMatch(strLeft, "^(s|sd|start|start date)$")) > 0
  			
  			-- IF LEFT HAND SIDE UNRECOGNIZABLE, FAIL THE PARSE ??
  			if not blnWriteStart then
  				if (my PatternMatch(strLeft, "^(d|dd|due|due date)$")) < 1 then set strUnParsed to strLeft
  			end if
  		else
  			-- IF NO LEFT HAND SIDE, ASSUME DUE DATE BY DEFAULT
  			set {strLeft, strRight} to {"due", my Trim(item 1 of lstSides)}
  			set {blnReadStart, blnWriteStart} to {false, false}
  		end if
  		
  		-- DETERMINE WHETHER THE EXPRESSION IS REFLEXIVE (BASED ON OWN START|DUE)
  		-- (Assume that a bare interval string is a reflexive reference to a due date)
  		set lngReflexEnd to my PatternMatch(strRight, "^(s|d|sd|dd|start|due|\\+\\d*$|\\-\\d*$|\\d*$)")
  		set blnReflexive to (lngReflexEnd > 0)
  		
  		-- IF REFLEXIVE,  ARE WE READING THE START DATE OR THE DUE DATE ?
  		if blnReflexive then
  			set blnReadStart to not (my PatternMatch(strRight, "^(d|dd|due|\\+|\\-|\\d)")) > 0
  		else
  			set blnReadStart to false
  		end if
  		
  		-- APPEND, WITH DATE EXPRESSION, TO COMMAND LIST
  		if length of strRight > 1 then
  			if (my PatternMatch(strRight, "^(\\+|\\-|\\d)")) = 0 then
  				set strRight to text (lngReflexEnd + 1) thru -1 of strRight
  			end if
  		end if
  		set end of lstCmd to {strUnParsed, blnReflexive, blnReadStart, blnWriteStart, strRight}
  	end repeat
  	set my text item delimiters to dlm
  	return lstCmd
  end ParseCmd
  
  -- "1 widget" or "3 widgets"
  on pl(str, lng)
  	if lng > 1 then
  		(lng as string) & space & str & "s"
  	else
  		(lng as string) & space & str
  	end if
  end pl
  
  on Replace(str, strFind, strReplace)
  	do shell script "echo " & quoted form of str & " | sed -e 's/" & strFind & "/" & strReplace & "/g'"
  end Replace
  
  (* Returns position of last character of matched pattern *)
  on PatternMatch(strText, strPattern)
  	try
  		(do shell script "echo " & quoted form of strText & " | perl -ne 'if (m/(" & strPattern & ")/) {print \"$+[1]\"}'") as integer
  	on error
  		0
  	end try
  end PatternMatch
  
  on Trim(strText)
  	do shell script "echo " & quoted form of strText & " | perl -pe 's/^\\s+//; s/\\s+$//'"
  end Trim
  
  -- REPORT RESULTS TO USER ( BY DEFAULT THROUGH GROWL - IF INSTALLED )
  on Notify(strTitle, strReport)
  	set strGrowlPath to ""
  	try
  		tell application "Finder" to tell (application file id "GRRR") to set strGrowlPath to POSIX path of (its container as alias) & name
  	end try
  	set blnInstalled to (strGrowlPath ≠ "")
  	--set blnInstalled to false
  	
  	-- IF INSTALLED, THEN IS IT RUNNING ?
  	if blnInstalled then
  		tell application id "sevs"
  			set blnGrowlRunning to exists (application process "GrowlHelperApp")
  			
  			-- IF NOT RUNNING THEN TRY TO WAKE IT UP ...
  			if not blnGrowlRunning then
  				do shell script "open " & quoted form of strGrowlPath & " ; sleep .5"
  				set blnGrowlRunning to exists (application process "GrowlHelperApp")
  			end if
  			
  			if blnGrowlRunning then
  				tell application "Growl"
  					register as application "houthakker scripts" all notifications {strTitle} default notifications {strTitle} icon of application "OmniFocus"
  					notify with name strTitle title strTitle application name "houthakker scripts" description strReport
  				end tell
  			else
  				-- IF NO REPORT HAS BEEN MADE THROUGH GROWL, REPORT THRU DIALOG
  				set strReport to "(Growl not running)" & return & return & strReport
  				tell application id "sevs"
  					activate
  					display dialog strReport buttons {"OK"} default button 1 with title pTitle
  				end tell
  			end if
  		end tell
  	else
  		-- IF GROWL IS NOT INSTALLED , REPORT THROUGH DIALOG
  		set strReport to "(Growl not installed)" & return & return & strReport
  		tell application id "sevs"
  			activate
  			tell (display dialog strReport buttons {pGrowlURL, "OK"} default button 2 with title pTitle)
  				if button returned = pGrowlURL then tell me to do shell script "open " & quoted form of pGrowlURL
  			end tell
  		end tell
  	end if
  end Notify
  
  on split(strPattern, strString)
  	paragraphs of (do shell script "perl -e 'print join(\"" & return & "\", split(/" & strPattern & "/,\"" & strString & "\"));'")
  end split
  
  