Guide/Script: Repeating iCal events on specific day of the month or nearest workday

Unfortunately iCal doesn't have any built-in feature for repeating an event "on the Xth of the month or the nearest workday preceding it". That would be a very useful feature because you might have to do do something related to your work on the workday on or nearest the 25th, for instance.

Luckily, it's actually possible to do this by exploiting some tricks in the iCal calendar format. I figured out those tricks and wrapped them inside a script that accepts an input "template" calendar file and outputs a file that will repeat on the Xth of every month or the nearest workday.

I decided to upload it here to place it in the public domain in a secure home.

I often need this feature and I don't feel like remembering all the boring details, so I wrote the following code. It is a small program that will accept an iCal calendar file as input and output a file with the desired repeating pattern.

I am not here to teach newbies how to use it but I tried to make it self-explanatory and included instructions. I place it here so that it goes into the public domain of the WWW. Hopefully people download and share it to give it a safe home somewhere on the internet. Enjoy.

Oh and if any Apple engineers are reading, you are free to take this method and implement it officially in the iCal GUI. It would be a very nice feature and deserves being part of the program.

repeatMonthlyOnWorkdayAtOrNear.sh:

Code:

#!/bin/bash
# Exploits tricks in the iCal specification to repeat an event on your desired day or the latest preceding workday.
# Basically, we give iCal a choice of 3 dates (the desired day and the two days before it), and then tell iCal to iterate backwards through the list until it finds a regular workday.
# CAVEAT:
# The iCal specification does not allow monthly recurring events to be placed in the preceding month, so if the user provided a desired day of 1 or 2, we cannot fall back to the last day(s) of the previous month in the case that the 1st/2nd days of the current month were both the weekend.
# So, instead we kludge a workaround as follows: If they want day 2, we'll find the earliest workday by checking the 2nd, 1st or 3rd day in that order. If they want day 1, we'll find the earliest workday by checking the 1st, 2nd or 3rd day in that order.
# This means that for day 1 or 2, you might get a match one or two days after your desired day. Nothing can be done about that, unfortunately. However, if you want any day from 3 or above, everything works as intended and it will search the desired day and the two truly preceding days.
# USAGE:
# * Save this script as a file called repeatMonthlyOnWorkdayAtOrNear.sh on your desktop.
# * Open a Terminal and navigate to the desktop and make the script executable with:
# chmod +x repeatMonthlyOnWorkdayAtOrNear.sh
# * Create an event in iCal on whatever day you want its first occurrence to be (this has no bearing on when it will be repeating so pick the day you truly want the first instance to happen on). Set up its alarm times and so on as desired. Finally set it up to "Repeat: Every month".
# Now click and drag the event from iCal to your desktop to export it, and make sure to delete the whole blueprint event (along with its repeats) from iCal.
# Next, run this script over the calendar file, telling it what day you want it to repeat at/near. For instance, day 15:
# ./repeatMonthlyOnWorkdayAtOrNear.sh YourCalendarEventFileName.ical 15
# Now just double click the processed file to import it back into iCal and you'll have an event that starts on the day of your manual first occurrence, and then repeats every month on the Xth day or the nearest workday.
# input variables
ical_file="$1"
desired_day="$2"
# validate the input variables
if [[ ! "$desired_day" =~ ^[0-9]+$ ]] || [ "$desired_day" -lt 1 ] || [ "$desired_day" -gt 31 ]; then
echo "Please provide a valid day in the range 1-31."
echo "Syntax: $0 <iCal file> <desired day>"
exit 1
fi
if [ ! -f "$ical_file" ]; then
echo "The input file you provided does not exist."
echo "Syntax: $0 <iCal file> <desired day>"
exit 1
fi
# we will be looking for the unmodified default monthly repeat rule
search_for="RRULE:FREQ=MONTHLY;INTERVAL=1"
# we will replace it with an event that finds the exact day or the earliest workday before it
replace_with="RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYMONTHDAY="
# if the desired day is 29 or higher, it will not exist in every month (particularly February), so we'll have to prepend a condition that also covers days 26, 27 and 28 to make sure that all months work as intended regardless of how many days they contain
# the reason we go all the way back to the 26th is to cover the case of february ending with both saturday and sunday in which case the earliest possible workday, a friday, would be found on the 26th
if [ "$desired_day" -ge 29 ]; then
replace_with="${replace_with}26,27,28,"
fi
# now generate the desired search condition.
# if the desired day is 1 or 2, we hit a situation where both day 1 and 2 of the month might be the weekend, and the iCal spec doesn't allow us to say "if both day 1 and 2 are the weekend, wrap back around to the earliest workday in the PRECEDING month".
# so, we'll have to kludge a fix that involves checking the first 3 days of the month. this means that if the user desires day 1 or 2, the event could end up as far back as on day 3 if day 1/2 aren't workdays.
if [ "$desired_day" -eq 2 ]; then
# they want day 2, so we will match the 2nd day if it's a workday, otherwise checks first day, otherwise checks 3rd day
replace_with="${replace_with}3,1,2,"
elif [ "$desired_day" -eq 1 ]; then
# they want day 1, so we will match the first day if it's a workday, otherwise checks 2nd day, otherwise checks 3rd day
replace_with="${replace_with}3,2,1,"
else
# they want a day in the range 3-31, so generate the condition by inserting the desired day and the two days before it (in ascending order)
for (( i=$((desired_day - 2)); i<=$desired_day; i++ )); do
# only output the day if it wasn't already covered by "26,27,28" above
if [ "$desired_day" -le 28 ] || [ "$i" -ge 29 ]; then
this_day=$i
replace_with="${replace_with}${this_day},"
fi
done
fi
# remove the superfluous trailing comma
replace_with="${replace_with%?}"
# finish off the replacement string by adding the condition that makes iCal search backwards through the provided dates until it finds a workday
replace_with="${replace_with};BYSETPOS=-1"
# alright now search for the original monthly repeat line and insert the replacement
sed -i ".bak" -e "s/${search_for}/${replace_with}/" "$ical_file"
# check whether the file has actually changed; if not, remove new file, name the backup back to the original name, and warn the user that no match was found
if diff "$ical_file" "${ical_file}.bak" > /dev/null; then
# the files are the same (no substitution took place)
rm "$ical_file"
mv "${ical_file}.bak" "$ical_file"
echo "Unable to find the default monthly repeat instruction in calendar file; has the file already been modified?"
exit 1
else
# the files are different (success!)
echo "Successfully updated calendar file to repeat monthly on day ${desired_day} or the nearest available workday."
echo "Backup of original file saved to ${ical_file}.bak"
exit 0
fi

Usage:

Save the above code as a file called repeatMonthlyOnWorkdayAtOrNear.sh on your desktop.

Open a Terminal and navigate to the desktop and make the script executable with:

Code:

chmod +x repeatMonthlyOnWorkdayAtOrNear.sh

Create an event in iCal on whatever day you want its first occurrence to be (this has no bearing on when it will be repeating so pick the day you truly want the first instance to happen on). Set up its alarm times and so on as desired. Finally set it up to "Repeat: Every month".

Now click and drag the event from iCal to your desktop to export it, and make sure to delete the whole blueprint event (along with its repeats) from iCal.

Next, run this script over the calendar file, telling it what day you want it to repeat at/near. For instance, day 15:

Code:

./repeatMonthlyOnWorkdayAtOrNear.sh YourCalendarEventFileName.ical 15

Now just double click the processed file to import it back into iCal and you'll have an event that starts on the day of your manual first occurrence, and then repeats every month on the Xth day or the nearest workday.