shift-to-ical / shift_to_ical.py
Tal
Fix timezone offsets by using timedelta objects instead of strings
7ebef8d
import re
from datetime import datetime, timedelta
from icalendar import Calendar, Event, Timezone, TimezoneStandard, TimezoneDaylight, vRecur
import pytz
import uuid
import gradio as gr
import os
# Define the shift pattern to match: DD.MM. Day ShiftCode HH.MM - HH.MM
shift_pattern = re.compile(
r'(\d{2}\.\d{2})\.?\s+' # Date (DD.MM or DD.MM.)
r'[A-Za-z]{2}\s+' # Day abbreviation (ignored)
r'([A-Za-z0-9]+)\s+' # Shift code (allow lowercase)
r'(\d{2}\.\d{2})\s+-\s+(\d{2}\.\d{2})' # Time range
)
def create_shift_calendar(shift_data, year=datetime.now().year):
# Split data into lines and remove empty lines
lines = [line.strip() for line in shift_data.split('\n') if line.strip()]
# Create a new calendar with specified format
cal = Calendar()
cal.add('prodid', '-//Gemini AI//Google Calendar Creator//EN')
cal.add('version', '2.0')
cal.add('calscale', 'GREGORIAN')
cal.add('method', 'PUBLISH')
cal.add('x-wr-calname', 'Work Shifts')
cal.add('x-wr-timezone', 'Europe/Helsinki')
# Add timezone component for Europe/Helsinki
tz = Timezone()
tz.add('tzid', 'Europe/Helsinki')
# Standard time (UTC+2)
std = TimezoneStandard()
std.add('tzname', 'EET')
std.add('dtstart', datetime(1970, 10, 25, 3, 0, 0))
std.add('tzoffsetto', timedelta(hours=2))
std.add('tzoffsetfrom', timedelta(hours=3))
std.add('rrule', vRecur(freq='YEARLY', bymonth=10, byweekday=['SU'], bysetpos=-1))
tz.add_component(std)
# Daylight saving time (UTC+3)
dst = TimezoneDaylight()
dst.add('tzname', 'EEST')
dst.add('dtstart', datetime(1970, 3, 29, 2, 0, 0))
dst.add('tzoffsetto', timedelta(hours=3))
dst.add('tzoffsetfrom', timedelta(hours=2))
dst.add('rrule', vRecur(freq='YEARLY', bymonth=3, byweekday=['SU'], bysetpos=-1))
tz.add_component(dst)
cal.add_component(tz)
# Get the timezone object
helsinki_tz = pytz.timezone('Europe/Helsinki')
for line in lines:
match = shift_pattern.match(line)
if not match:
print(f"Warning: Could not parse line '{line}'")
continue
try:
# Extract components from the match groups
date_str = match.group(1) # First group: date (DD.MM)
shift_code = match.group(2) # Second group: shift code
start_time_str = match.group(3) # Third group: start time
end_time_str = match.group(4) # Fourth group: end time
# Create datetime objects and localize to Helsinki time
start_dt_naive = datetime.strptime(
f"{date_str}.{year} {start_time_str.replace('.', ':')}",
"%d.%m.%Y %H:%M"
)
end_dt_naive = datetime.strptime(
f"{date_str}.{year} {end_time_str.replace('.', ':')}",
"%d.%m.%Y %H:%M"
)
# Localize to Helsinki timezone and convert to UTC
start_dt_helsinki = helsinki_tz.localize(start_dt_naive)
end_dt_helsinki = helsinki_tz.localize(end_dt_naive)
start_dt_utc = start_dt_helsinki.astimezone(pytz.utc)
end_dt_utc = end_dt_helsinki.astimezone(pytz.utc)
# Create event with UTC times
event = Event()
event.add('summary', f'Work Shift: {shift_code}')
event.add('dtstart', start_dt_utc)
event.add('dtend', end_dt_utc)
event.add('description', f'Shift code: {shift_code}')
event.add('uid', str(uuid.uuid4()))
event.add('dtstamp', datetime.utcnow())
# Add event to calendar
cal.add_component(event)
except ValueError as e:
print(f"Warning: Could not parse date/time on line '{line}'. Error: {e}")
continue
return cal.to_ical() # Return the calendar data
def generate_ical(shift_data):
"""
Takes raw shift data, generates an iCalendar file, and returns the path to the file.
"""
if not shift_data:
# Gradio needs a file path to be returned, so create an empty file.
with open("shifts_empty.ics", "w") as f:
pass
return "shifts_empty.ics"
ics_data = create_shift_calendar(shift_data)
# Define the output file path
file_path = "shifts.ics"
# Write the iCalendar data to the file
with open(file_path, 'wb') as f:
f.write(ics_data)
return file_path
# Create the Gradio interface
demo = gr.Interface(
fn=generate_ical,
inputs=gr.Textbox(
lines=15,
label="Paste Shift Data Here",
placeholder="Example:\n01.07.\tTi\tA5\t07.30 - 15.42\n02.07.\tKe\tI2\t13.00 - 21.00\n..."
),
outputs=gr.File(label="Download iCalendar File"),
title="Shift to iCalendar Converter",
description="Paste your shift data in the format 'DD.MM. Day ShiftCode HH.MM - HH.MM' (one shift per line). The script will generate a downloadable .ics file that you can import into your calendar application.",
allow_flagging="never",
)
if __name__ == "__main__":
demo.launch()