Banner Image

ICS to JSON

Mar 30, 2025

Tags:

I was in need to convert an iCalendar / ics file to a json file, the result can be found here.
PS: The attempt to create the script started by asking AI, honestly, not yet very helpful.

#!/bin/bash

# Usage ./ics_to_json.sh google.ics data/calendar.json 
# script to convert an ics / ical / iCalendar to a json file
# dos2unix needs to be installed to execute this script
# it can be removed if the ics file has unix format but this often is not the case

# Check if ICS file is provided
if [ -z "$1" ]; then
    echo "Usage: $0 <ics_file> [output_file]"
    exit 1
fi

ICS_FILE="$1"
OUTPUT_FILE="${2:-events.json}"  # Default to "events.json" if not specified
SORTED_FILE="$(dirname "$OUTPUT_FILE")/sorted_$(basename "$OUTPUT_FILE")"

# Check if file exists
if [ ! -f "$ICS_FILE" ]; then
    echo "Error: File '$ICS_FILE' not found!"
    exit 1
fi

# Convert line endings if necessary
echo "Converting to unix format, just to make sure line endings are fine..."
dos2unix "$ICS_FILE" 2>/dev/null

# Extract events and convert dates directly in awk
# dates are converted to german format here
# if statement in scropt can be extended to include further fields into the output
echo "Extracting start, end and summary from ics..."

awk -v output="$OUTPUT_FILE" '

function convert_date(ics_date) {
    if (ics_date ~ /^[0-9]{8}T[0-9]{6}Z$/) {
        # Timestamp format: YYYYMMDDTHHMMSSZ
        cmd = "date -u -d \"" substr(ics_date, 1, 4) "-" substr(ics_date, 5, 2) "-" substr(ics_date, 7, 2) " " substr(ics_date, 10, 2) ":" substr(ics_date, 12, 2) ":" substr(ics_date, 14, 2) "\" +\"%d.%m.%Y %H:%M\""
    } else if (ics_date ~ /^[0-9]{8}$/) {
        # All-day event format: YYYYMMDD
        cmd = "date -u -d \"" substr(ics_date, 1, 4) "-" substr(ics_date, 5, 2) "-" substr(ics_date, 7, 2) "\" +\"%d.%m.%Y\""
    } else {
        return ics_date  # Return the original date if it doesnt match expected formats
    }

    cmd | getline formatted_date
    close(cmd)

    # If its an all-day event, append " 00:00" to the formatted date
    if (ics_date ~ /^[0-9]{8}$/) {
        return formatted_date " 00:00"
    }

    return formatted_date
}

BEGIN {
    print "[" > output
    event_count = 0
}

{
    if ($0 ~ /^BEGIN:VEVENT/) {
        if (event_count > 0) {
            print "  }," >> output  # Print closing brace with comma for previous event
        }
        print "  {" >> output  # Start a new event
        event_count++
        first_field = 1  # To track if its the first field in the event
    }
    if ($0 ~ /^SUMMARY:/) {
        sub(/^SUMMARY:/, "", $0)
        if (!first_field) {
            print "," >> output  # Print comma before the next field if its not the first
        }
        print "    \"summary\": \"" $0 "\"" >> output
        first_field = 0  # Mark that weve printed the first field
    }
        if ($0 ~ /^LOCATION:/) {
        sub(/^LOCATION:/, "", $0)
        if (!first_field) {
            print "," >> output  # Print comma before the next field if its not the first
        }
        print "    \"location\": \"" $0 "\"" >> output
        first_field = 0  # Mark that weve printed the first field
    }

    if ($0 ~ /^DTSTART(;VALUE=DATE)?/) {
        split($0, arr, ":")
        if (!first_field) {
            print "," >> output  # Print comma before the next field if its not the first
        }
        print "    \"start\": \"" convert_date(arr[2]) "\"" >> output
        first_field = 0  # Mark that weve printed the first field
    }
    if ($0 ~ /^DTEND(;VALUE=DATE)?/) {
        split($0, arr, ":")
        if (!first_field) {
            print "," >> output  # Print comma before the next field if its not the first
        }
        print "    \"end\": \"" convert_date(arr[2]) "\"" >> output
        first_field = 0  # Mark that weve printed the first field
    }
}

END {
    if (event_count > 0) {
        print "  }" >> output  # Print the closing brace for the last event
    }
    print "]" >> output  # Print the closing bracket for the array
}' "$ICS_FILE"


# Sort JSON by start date, still reflecting german format
echo "Sort by start date..."
jq 'sort_by(.start | strptime("%d.%m.%Y %H:%M"))' "$OUTPUT_FILE" > "$SORTED_FILE"
# cat sorted_"$OUTPUT_FILE"

# Replace original output with sorted version
mv "$SORTED_FILE" "$OUTPUT_FILE"

# Output final sorted JSON
#cat "$OUTPUT_FILE"

echo "File written to <<< '$OUTPUT_FILE' >>>"