Generating birthday calendars
I recently switched from using Nextcloud for managing contacts and calendars to Radicale. Radicale is much easier to configure and maintain than a Nextcloud instance but there was one feature missing: automatically generating birthday calendar events from the contants. Since both the vCard and iCalendar formats are plain-text it shouldn't be too difficult to generate them using a script.
Reading through the Wikipedia article for vCard, I saw a very useful table showing all available vCard properties. For vCard 3+ the formatted name (FN) property is required so that can be reliably used as the name of the calendar event. The other property of interest is of course the birthday date (BDAY). Looking at some of the vCard files for my contacts I noticed that the date of the BDAY property occasionally has dash separating its components. Something interesting I noticed is that for contacts whose birth year isn't known, it's set to 1604. It would be interesting to learn why.
For the iCalendar format the Wikipedia article wasn't that helpful but the RFC was. The online validator was invaluable in testing the resulting files.
After a little trial and error I came up with the following shell script:
#!/bin/sh
# SPDX-FileCopyrightText: 2022-2023 Sotiris Papatheodorou
# SPDX-License-Identifier: CC0-1.0
set -eu
vcard_to_bday() {
awk '
BEGIN { FS = ":"; OFS = "\t" }
/^FN/ { name = $2 }
/^BDAY/ {
bday = $2
gsub("-", "", bday)
# Replace missing years (1604) with 1900 to avoid using
# the Julian calendar.
gsub("^1604", "1900", bday)
}
/^END:VCARD/ { if (name && bday) print name, bday }
' "$1"
}
# Usage: bday_to_vcal2 NAME BDAY UID
bday_to_vcal2() {
cat <<- EOF
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Sotiris Papatheodorou//${progname:-}//EN
BEGIN:VEVENT
UID:$3
SUMMARY:$1 🎂
DTSTART;VALUE=DATE:$2
DURATION:P1D
RRULE:FREQ=YEARLY
DTSTAMP:$(date -u '+%Y%m%dT%H%M%SZ')
BEGIN:VALARM
DESCRIPTION:$1 🎂
ACTION:DISPLAY
TRIGGER:PT0S
END:VALARM
END:VEVENT
END:VCALENDAR
EOF
}
vcard_to_calendar() {
data=$(vcard_to_bday "$1")
if [ -z "$data" ]
then
return
fi
name=$(printf '%s' "$data" | cut -f 1)
bday=$(printf '%s' "$data" | cut -f 2)
uid=$(uuidgen --md5 --namespace '@oid' --name "$progname$name$bday")
bday_to_vcal2 "$name" "$bday" "$uid" > "$2/$uid.ics"
}
progname=${0##*/}
if [ "$#" -ne 2 ]
then
cat <<- EOF >&2
Usage: ${progname} INDIR OUTDIR
Generate birthday calendars inside OUTDIR for
all contacts inside INDIR and its subdirectories.
EOF
exit 2
fi
mkdir -p "$2"
rm -f "$2"/*.ics
find "$1" -type f -name '*.vcf' | while IFS= read -r file
do
vcard_to_calendar "$file" "$2"
done
I created a new calendar from the Radicale web interface, set the script to run on a daily cron job and that was it!
Sotiris 2022/04/04
Updates
- 2022/04/07: Added a reminder to the generated calendar events.