💾 Archived View for sotiris.papatheodorou.xyz › gemlog › 20220404_generating_birthday_calendars.gmi captured on 2024-03-21 at 15:06:37. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-03-20)

-=-=-=-=-=-=-

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

Nextcloud

Radicale

vCard

iCalendar RFC

iCalendar validator

Updates