Dates & Times in AppleScripts

The idea is that when date of the August event actually arrives, the alarm attached to the event automatically runs the script again (more precisely, an invisible helper application that’s part of the iCal software, but which runs independently of iCal itself, reacts to the stored alarm data and runs the script at the required time) to create the September event, and so on. Sorry my explanation wasn’t clear.

Also, the script writes own its location into the alarm so that the alarm can find it next time. For this to work, the script has to be saved as an application in the location where you want to keep it before it’s run for the first time.

Do let me know if there are any problems, or if it’s not quite what you need. :slight_smile:

Thanks again Nigel. I got it now. There are two more things I need to sort with this script:

  1. I had preferred that the event gets populated throughout the calendar rather than just the first month. It worked very well according to the way you designed it. In fact, I quite like the way it automatically creates next month’s event when the alarm goes off (it’s quite cool actually) but I guess I’m used to seeing recurrent events actually appearing in the subsequent months when I create them. I’m thinking that in some other events where I will deploy this script, it is important that I see the event appear in subsequent months in case this recurrent event is a meeting or something like that and I need to be sure I don’t schedule another appointment which clashes with the timing.

  2. I sync-ed this over to my iPhone and as you probably already know, it doesn’t quite work in iPhone, that is, iPhone doesn’t run scripts. So the alarm came and went and that’s about it. While I could get it back to the iPhone every time I sync it, I would prefer that it works in iPhone because I use my iPhone a lot more than iCal.

So what I’m hoping for is a script that will sort of permanently create all these first weekday events inside iCal as normal recurrent events and then when I sync it over to iPhone, I’ll get all these dates populated into iPhone’s calendar as recurrent events. I can then manipulate them as normal recurrent events - like delete a single occurrence or the whole series. So the script is a run-once kind of script.

I’m sorry for this long posting but I am a little disappointed with Apple that such a function in iCal needs such a workaround. I used to use Outlook and Entourage and probably got pampered by these apps.

Hi, Kedeb.

Sorry I’ve been so long getting back. I’m having a busy week and also ran into some hitherto unsuspected iCal peculiarities that needed to be checked out before I posted the second script below.

A “recurring event” is just a single event with a recurrence rule, which iCal interprets “on the fly” to display the subsequent reiterations in the calendar. The iCalendar recurrence rule for the first non-(Saturday-or-Sunday) of every month would be “FREQ=MONTHLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1”. But although iCal observes many recurrence types that can’t be set in its GUI, the BYSETPOS rule part isn’t one of them (at least, up to iCal 2.0.5). So a “recurring event” of the type you want isn’t possible in iCal.

The easiest thing with AppleScript would be to create a separate event for each month ” though of course it’s only sensible to do this for a certain distance ahead. Five years’ worth of reminders would require sixty events:

-- Adjust these properties to your own requirements.
property theSummary : "On-line payments" -- Event legend.
property calendarNameOrNumber : -1 -- The calendar name or index number.
property numberOfMonths : 60 -- Number of months in the period to cover.

set nextEventDate to (current date)
set nextEventDate's time to 0

repeat numberOfMonths times
	-- Get the 1st day of next month.
	tell nextEventDate + (32 - (nextEventDate's day)) * days to set nextEventDate to it + (1 - (its day)) * days
	-- If it's a Saturday or a Sunday, advance to the following Monday.
	set w to nextEventDate's weekday
	if (w is Saturday) then
		set nextEventDate to nextEventDate + 2 * days
	else if (w is Sunday) then
		set nextEventDate to nextEventDate + days
	end if
	-- If it's the New Year bank holiday, adjust accordingly.
	if (nextEventDate's month is January) then
		if (w is Friday) then
			set nextEventDate to nextEventDate + 3 * days
		else
			set nextEventDate to nextEventDate + days
		end if
	end if
	
	-- Make an all-day event in iCal for the calculated day.
	tell application "iCal" to make new event at end of events of calendar calendarNameOrNumber with properties {summary:theSummary, start date:nextEventDate, allday event:true}
end repeat

An alternative would be to create an event that recurred on the first day of every month, then go through the calendar, manually dragging iterations that fell on a Saturday or Sunday to the following Monday. (This can’t easily be done with AppleScript.) Again, it’s only sensible to do it for a certain amount of time ahead, but the “detached events” so created would be relatively few in number. For the five years beginning next month, there’d be 17 detached events (or 20 if you were also avoiding New Year bank holidays) plus the original recurring one. “Detached events” are linked to specific iteration times of a recurring event and are expressed instead of the iterations that would otherwise have appeared at those times. iCal 2.0.5 hides them from AppleScript.

An AppleScriptable variation on this would be to delete (or “exclude”) iterations that fell on the offending days and create completely independent events for the alternative days. The number of independent events would be the same as the number of detached events with the previous method.

The iCal peculiarities I mentioned affect this script:

  1. With both iCal 1.5.5 and iCal 2.0.5, if a recurring event is specified to iterate n times, and n > 1, and the event’s an all-day event, then iCal only expresses (n - 1) iterations. I haven’t done anything about this bug in the script.
  2. iCal returns index references to events. But I find that iCal 2.0.5 cavalierly reindexes events on the fly if it feels like it, rendering event references in variables obsolete. The script therefore sets up a filter-by-UID reference for the recurring event so that it can be identified when the time comes to exclude its unwanted iteration dates.
  3. The events the script creates, although testable with AppleScript, don’t become visible in iCal (2.0.5) until I quit it and reopen it. The script therefore arranges for this to happen.
-- Adjust these properties to your own requirements.
property theSummary : "On-line payments" -- Event legend.
property calendarNameOrNumber : -1 -- The calendar name or index number.
property numberOfMonths : 60 -- Number of months in the period to cover.

-- Get the first day of the month after a given date.
on firstOfNextMonth(now)
	tell now + (32 - (now's day)) * days to return it + (1 - (its day)) * days
end firstOfNextMonth

-- If a 1st-of-month date falls during a weekend or is New Year's Day, get the next working day.
on nudge(thisDate)
	set w to thisDate's weekday
	if (w is Saturday) then
		set thisDate to thisDate + 2 * days
	else if (w is Sunday) then
		set thisDate to thisDate + days
	end if
	if (thisDate's month is January) then
		if (w is Friday) then
			set thisDate to thisDate + 3 * days
		else
			set thisDate to thisDate + days
		end if
	end if
	
	return thisDate
end nudge

set startDate to nudge(firstOfNextMonth(current date))
set startDate's time to 0

-- Create a recurring all-day event that repeats on the first of every month for the set number of months.
-- Get a filter-by-UID reference to it for reliability in iCal 2.0.5.
tell application "iCal"
	set rootEventUID to uid of (make new event at end of events of calendar calendarNameOrNumber with properties {start date:startDate, summary:theSummary, allday event:true})
	set rootEvent to a reference to (first event of calendar calendarNameOrNumber whose uid is rootEventUID)
	set rootEvent's recurrence to "FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1;COUNT=" & numberOfMonths
end tell

-- Trace the recurrence sequence, looking out for weekend and New Year dates.
set recurrenceDate to startDate
set excludedDates to {}
repeat (numberOfMonths - 1) times
	-- If the next recurrence date falls during a weekend or is New Year's Day, append it
	-- to the excluded-date list and create an alternative event for the next working day.
	set recurrenceDate to firstOfNextMonth(recurrenceDate)
	if (recurrenceDate's weekday is in {Saturday, Sunday}) or (recurrenceDate's month is January) then
		set end of excludedDates to recurrenceDate
		set recurrenceDate to nudge(recurrenceDate)
		tell application "iCal" to make new event at end of events of calendar calendarNameOrNumber with properties {start date:recurrenceDate, summary:theSummary, allday event:true}
	end if
end repeat

-- Set the recurring event's 'excluded dates' to the excluded-date list to stop those particular recurrence
-- instances being expressed, then quit and reactivate iCal to make the changes visible in the calendar (in iCal 2.0.5).
tell application "iCal"
	set rootEvent's excluded dates to excludedDates
	quit
end tell
tell application "System Events" to repeat while (application process "iCal" exists)
	delay 0.2
end repeat
tell application "iCal" to activate

Thanks again Nigel. I really appreciate the help you are giving me. I’m at a conference now myself but I just wanted to quickly drop a line to say thank you. It’s going to take a while for me to digest all that. Thanks again.

I am a wannabe scripter, have been for years… I DO like to dabble, though.

Here’s what I want to do: I want to place in a signature on my outgoing emails, a little countdown timer. (such as countdown to my vacation, etc.)

Is applescript even what I need? I found SignatureProfiler which will allow me to place applescripts or even html code into my signatures. Now I’m trying to figure out how to do the timer thing.
Am I in the right place?
Thank you.
Dee Dee

This whole thread is VERY intriguing but, unfortunately, I am too limited in my AppleScript knowledge to fully grasp all the intricacies of the coercion of dates.

Currently, I am trying to coerce a string from an astrology program (“Io Sprite”) and add either a day, month or year to advance or regress by any of these intervals.

I can get the LocalDate (app class) out as a string but adding or subtracting from it and and putting it back in the InfoCardWindow is eluding me.

Any ideas or help would be greatly appreciated.

~Roc

Give us an example of the string date you want to coerce.

I suggest you use this simpler way of getting a week number:

--According to ISO 8601 the week 1 of a year is the one with 4 Jan in it: (http://en.wikipedia.org/wiki/ISO_8601#Week_dates)

on weekNumber from inputDate

--First get the number of days since 4 Jan, then divide that by seven to get the number of full weeks passed. Finally round the number up to get the week currently in progress.

	round ((inputDate) - (date ("4 Jan" & year of inputDate))) / days / 7 rounding up
end weekNumber

Hi, solidfox.

Thanks for posting your suggestion. However, there are a few issues with it.

Firstly, the ‘date “”’ construction only works where the user’s Date and Time preferences are compatible with the format of the date string given. As from Snow Leopard, ‘date (“4 Jan” & year of inputDate)’ will error for users in the USA.

Secondly, your calculation works directly from 4th January, not from the beginning of the week in which it occurs.

Thirdly, you need to make provision for when the input date comes before 4th January and for when it’s one of the last three days of December and might be in the first week of the following year.

on weekNumber from inputDate against weekStart
	-- Get a known date in the past with the week-start weekday.
	-- NB. This was compiled in Snow Leopard, where AppleScript dates are Julian rather than Gregorian before some date in the 1500s. With Leopard or earlier, use date "Monday 6 January 1000".
	set baseDate to (date "Monday 1 January 1000 00:00:00") + (offset of (text 1 thru 2 of (weekStart as text)) in "MoTuWeThFrSaSu") div 2 * days
	
	-- Get 4th January in the year of the input date.
	copy inputDate to Jan4
	set Jan4's day to 4
	set Jan4's month to January
	-- If the input date's in the last three days of December, initially try with 4th January the following year.
	if (inputDate's month is December) and (inputDate's day > 28) then set Jan4's year to (Jan4's year) + 1
	
	-- Derive the beginning of the week of the 4th January date.
	set yearStart to Jan4 - (Jan4 - baseDate) mod weeks
	-- If the input date comes before this, it's counted as being in the previous year.
	if (inputDate comes before yearStart) then
		set Jan4's year to (Jan4's year) - 1
		set yearStart to Jan4 - (Jan4 - baseDate) mod weeks
	end if
	
	-- Derive and return the relevant week number for the input date.
	return (inputDate - yearStart + weeks) div weeks
end weekNumber

weekNumber from (current date) against Monday -- or "Monday"

I am trying to add twenty five minutes to the current time and then display the result in QuickSilver. How would I add time to the current time?

I am also not sure why QuickSilver is not posting my results.


--http://www.devdaily.com/apple/applescript-current-time-example-script-format

say "It will be " & getTimeInHoursAndMinutes() & " in 25 minutes" using "Alex"

on getTimeInHoursAndMinutes()
	-- Get the "hour"
	set timeStr to time string of (current date)
	set Pos to offset of ":" in timeStr
	set theHour to characters 1 thru (Pos - 1) of timeStr as string
	set timeStr to characters (Pos + 1) through end of timeStr as string
	
	-- Get the "minute"
	set Pos to offset of ":" in timeStr
	set theMin to characters 1 thru (Pos - 1) of timeStr as string
	set timeStr to characters (Pos + 1) through end of timeStr as string
	
	--Get "AM or PM"
	set Pos to offset of " " in timeStr
	set theSfx to characters (Pos + 1) through end of timeStr as string
	
	return (theHour & ":" & theMin & " " & theSfx) as string
	
end getTimeInHoursAndMinutes


set today to date string of (current date)
get today

tell application "Quicksilver" to show large type (getTimeInHoursAndMinutes())
end

This works but I don’t want to display the date at all.

tell application "Quicksilver" to show large type (current date) as string
end

This will display just the date,

set today to date string of (current date)
get today

tell application "Quicksilver" to show large type (get today)
end

Now how do I do just the time or at least add 25 minutes to the current time and include AM/PM and leave out the seconds?

Browser: Safari 531.21.10
Operating System: Mac OS X (10.6)

use seconds without units:

set TS to time string of ((current date) + (25 * 60))

or the word minutes:

set TS to time string of ((current date) + (25 * minutes))

then:

tell application "Quicksilver" to show large type TS

and by the way, when you call a handler from your script from a tell block you must include the word “my” in front of the handler call – Quicksilver doesn’t know about the handler.

That change makes your script work (without the last “end” you have in it). You do don’t use an “end” statement following a one line tell. You can see from above that there’s a much simpler way than you used.

--http://www.devdaily.com/apple/applescript-current-time-example-script-format

say "It will be " & getTimeInHoursAndMinutes() & " in 25 minutes" using "Alex"

on getTimeInHoursAndMinutes()
	-- Get the "hour"
	set timeStr to time string of (current date)
	set Pos to offset of ":" in timeStr
	set theHour to characters 1 thru (Pos - 1) of timeStr as string
	set timeStr to characters (Pos + 1) through end of timeStr as string
	
	-- Get the "minute"
	set Pos to offset of ":" in timeStr
	set theMin to characters 1 thru (Pos - 1) of timeStr as string
	set timeStr to characters (Pos + 1) through end of timeStr as string
	
	--Get "AM or PM"
	set Pos to offset of " " in timeStr
	set theSfx to characters (Pos + 1) through end of timeStr as string
	
	return (theHour & ":" & theMin & " " & theSfx) as string
	
end getTimeInHoursAndMinutes


set today to date string of (current date)
get today

tell application "Quicksilver" to show large type (my getTimeInHoursAndMinutes())

Wonderful thank you Adam that was very helpful. This seems like a complicated way to get the seconds to not be included and show AM and PM but I am glad that I have something that works. If anyone knows of a simpler way I’d love to hear about it.

I am also guessing as Stephan mentioned it probably wouldn’t work on everyones system and would depend on their time settings.

--http://www.devdaily.com/apple/applescript-current-time-example-script-format

say "It will be " & getTimeInHoursAndMinutes() & " in 25 minutes" using "Alex"

on getTimeInHoursAndMinutes()
	-- Get the "hour"
	set timeStr to time string of (current date)
	set Pos to offset of ":" in timeStr
	set theHour to characters 1 thru (Pos - 1) of timeStr as string
	set timeStr to characters (Pos + 1) through end of timeStr as string
	
	-- Get the "minute"
	set Pos to offset of ":" in timeStr
	set theMin to characters 1 thru (Pos - 1) of timeStr as string
	set timeStr to characters (Pos + 1) through end of timeStr as string
	
	--Get "AM or PM"
	set Pos to offset of " " in timeStr
	set theSfx to characters (Pos + 1) through end of timeStr as string
	
	return (theHour & ":" & (theMin + 25) & " " & theSfx) as string
	
end getTimeInHoursAndMinutes


set today to date string of (current date)
get today

tell application "Quicksilver" to show large type (my getTimeInHoursAndMinutes())

Oh darn I just ran the script again and realized it now says 10:83AM. That worked fine when the time was between 00-30.

Thanks again Adam!

Yeah. You need to add the twenty-five minutes before extracting the time string. :slight_smile:

By the way, lines like:

set theHour to characters 1 thru (Pos - 1) of timeStr as string

. would be better rendered as:

set theHour to text 1 thru (Pos - 1) of timeStr

It’s a more efficient, one-step process and doesn’t depend on the current state of AppleScript’s text item delimiters.

I think you could get away with using the ‘words’ of the time string, which would give you a shorter handler:

on getTimeInHoursAndMinutes(aDate)
	set ts to time string of aDate
	
	set hm to text 1 thru word 2 of ts
	if ((count ts's words) is 4) then set hm to hm & space & word 4 of ts
	
	return hm
end getTimeInHoursAndMinutes

say "It will be " & getTimeInHoursAndMinutes((current date) + 25 * minutes) & " in 25 minutes" using "Alex"

--tell application "Quicksilver" to show large type (my getTimeInHoursAndMinutes())

There are two other units that come in handy when performing business or scheduling calculations. Weeks and Months. Sometimes it’s useful to determine how many weeks or months have elapsed between two particular dates. I’ve also used this in document backup scripts for determining when to trigger weekly & monthly backups.

Nigel I just wanted to say thanks again for your help on this, I use what you helped me with a couple dozen times daily and find it very useful!

I recently installed a pulse water meter. With each 0.1 gallons use a pulse is generated. The pulses are recorded in a variable in Indigo, the home automation system.

I’d like to be able to log the data as current day, prior day, current week, prior week, current month and the preceeding 12 months, possibly going back to a couple of years. Obviously I need to create the relevant variables to store the data, but I’m not sure where to begin to get the data properly sorted. Any advice? I looked thru the thread and did not see a direct example.

Thanks!!!

I think this does what you have in mind:


set timeStr to time string of ((current date) + 25 * 60) -- you add seconds
tell application "Quicksilver" to show large type (get timeStr)

or in shorter form:


tell application "Quicksilver" to show large type (time string of ((current date) + 25 * 60))

thanks for the quick reply. i downloaded Quicksilver and ran the script in the editor. This is what it produced:

If I can ask, why do I want to do this in Quicksilver when i am trying to just partition a data stream from Indigo, that will stay in Indigo variables? I was thinking I would just accumulate the pulses and then segregate them into bins on a schedule.

what about something like this?

I think I figured it out. Each “current variable”, for instance Today, This Week and This Month, are incremented each time the water meter clicks via a single trigger. Every day at 12:01 AM a schedule runs the following script:


set {year:y, month:m, day:d, weekday:w} to (current date)


--Today and Yesterday. Second line in each sequence resets the current variable to ""
set value of variable "WaterUse_2_Yesterday" to value of variable "WaterUse_1_Today" as string

set value of variable "WaterUse_1_Today" to ""


--This week and last week. Only runs on Monday morning. 
if w is Monday then
	set value of variable "WaterUse_4_LastWeek" to value of variable "WaterUse_3_ThisWeek"
	set value of variable "WaterUse_2_Yesterday" to ""
end if


--This month and last month. Only runs on first of month.
if d is 1 then
	set value of variable "WaterUse_6_LastMonth" to value of variable "WaterUse_5_ThisMonth"
	set waterUseThisMonth to value of variable "WaterUse_5_ThisMonth"
	set value of variable "WaterUse_5_ThisMonth" to ""
end if


--Prior Month historical data
if d is 1 then
	if m is January then
		set value of variable "WaterUse_18_Dec" to waterUseThisMonth
	end if
	
	if m is February then
		set value of variable "WaterUse_7_Jan" to waterUseThisMonth
	end if
	
	if m is March then
		set value of variable "WaterUse_8_Feb" to waterUseThisMonth
	end if
	
	if m is April then
		set value of variable "WaterUse_9_Mar" to waterUseThisMonth
	end if
	
	if m is May then
		set value of variable "WaterUse_10_Apr" to waterUseThisMonth
	end if
	
	if m is June then
		set value of variable "WaterUse_11_May" to waterUseThisMonth
	end if
	
	if m is July then
		set value of variable "WaterUse_12_Jun" to waterUseThisMonth
	end if
	
	if m is August then
		set value of variable "WaterUse_13_Jul" to waterUseThisMonth
	end if
	
	if m is September then
		set value of variable "WaterUse_14_Aug" to waterUseThisMonth
	end if
	
	if m is October then
		set value of variable "WaterUse_15_Sep" to waterUseThisMonth
	end if
	
	if m is November then
		set value of variable "WaterUse_16_Oct" to waterUseThisMonth
	end if
	
	if m is December then
		set value of variable "WaterUse_17_Nov" to waterUseThisMonth
	end if
	
end if
			

Honestly, this is probably not very elegant. If anyone can show me how to write this in a less “brute force” way, I’d appreciate seeing how it’s done.

Hi hamw.

Is your query about manipulating AppleScript dates or about scripting Indigo? If the latter, it should be posted in the “AppleScript | Mac OS X” forum rather than appended to this article about dates. And since it’s possible that not many people will have experience of Indigo, you might include some information to lessen any confusion your scripts could cause. For instance, does Indigo have ‘variable’ elements with ‘value’ and ‘name’ properties?

Assuming your script in the previous post works, and that ‘variables’ are indeed elements in Indigo’s scripting dictionary, the following should also work. It makes use of an aspect of AppleScript dates. :wink:


set {year:y, month:m, day:d, weekday:w} to (current date)


--Today and Yesterday. Second line in each sequence resets the current variable to ""
set value of variable "WaterUse_2_Yesterday" to value of variable "WaterUse_1_Today"
set value of variable "WaterUse_1_Today" to ""


--This week and last week. Only runs on Monday morning. 
if w is Monday then
	set value of variable "WaterUse_4_LastWeek" to value of variable "WaterUse_3_ThisWeek"
	set value of variable "WaterUse_2_Yesterday" to ""
end if


--This month and last month. Only runs on first of month.
if d is 1 then
	set m to m as integer -- Coerce the AppleScript month to the equivalent month number.
	set namedLastMonthVariableName to item m of {"WaterUse_18_Dec", "WaterUse_7_Jan", "WaterUse_8_Feb", "WaterUse_9_Mar", "WaterUse_10_Apr", "WaterUse_11_May", "WaterUse_12_Jun", "WaterUse_13_Jul", "WaterUse_14_Aug", "WaterUse_15_Sep", "WaterUse_16_Oct", "WaterUse_17_Nov"}

	set waterUseThisMonth to value of variable "WaterUse_5_ThisMonth"
	set value of variable "WaterUse_6_LastMonth" to waterUseThisMonth
	set value of variable namedLastMonthVariableName to waterUseThisMonth
	set value of variable "WaterUse_5_ThisMonth" to ""
end if