A small custom calendar maker build for my grandfather. He wanted to be able to programatically create his yearly, printable calendar from a list of birthdays etc in excel. rather than manually transferring in photoshop or Canva.com every year.
This likely seems incredibly simple for someone who has programmed lots. That’s kinda why I like it, it uses very few python libraries, it’s incredibly fast and small. There’s no bloated JS frameworks or Nextjs, it runs on a random vps with minimal config, no vercel.
It was kinda nice as even this site or even a single landing page I built for [Redacted] are significantly larger for no apparent reason? Why didn’t I use plain css and js. Likely because I thought it was easier. But this took less than 2 hours to get online.
Makes me want to give HTMX a go.
Some notes from it.
The Clever Bits
Smart Date Handling Here’s how the date handling evolved from complex to basic.
First Try: Regex Overload
I started with the programmer’s reflex - regex patterns for every case:
def load_events(self, csv_file):
recurring_pattern = r'^(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01])$'
specific_pattern = r'^(\d{4})-(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01])$'
with open(csv_file, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
date_str = row['date'].strip()
if re.match(recurring_pattern, date_str):
month, day = map(int, re.match(recurring_pattern, date_str).groups())
# Handle recurring dates...
elif re.match(specific_pattern, date_str):
year, month, day = map(int, re.match(specific_pattern, date_str).groups())
# Handle specific dates...
It worked, but demanded extensive testing for edge cases like leading zeros and different separators. Classic overengineering.
Next: Datetime Parse
Then I thought datetime could handle everything:
def load_events(self, csv_file):
with open(csv_file, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
date_str = row['date'].strip()
try:
date = datetime.strptime(date_str, '%Y-%m-%d').date()
self.specific_events[date].append(event)
except ValueError:
temp_date = datetime.strptime(date_str, '%m-%d')
month, day = temp_date.month, temp_date.day
# Handle recurring...
Cleaner, but still excessive. Splitting “12-25” into month and day doesn’t need datetime’s parsing power.
Final: String Length
Then I noticed something obvious - the formats identify themselves by length:
def load_events(self, csv_file):
with open(csv_file, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
date_str = row['date'].strip()
if len(date_str) == 5: # MM-DD format = recurring
month, day = map(int, date_str.split('-'))
self.recurring_events[month][day].append(event)
else: # YYYY-MM-DD = one-time event
date = datetime.strptime(date_str, '%Y-%m-%d').date()
self.specific_events[date].append(event)
Now when grandpa writes:
date,event
12-25,Christmas
2024-01-01,New Year Party
The system immediately knows Christmas repeats yearly while the New Year Party happens once.
The final version:
- Uses less code
- Has fewer edge cases
- Shows clear intent
- Still validates dates through datetime
- Needs no extra validation beyond length
Sometimes the clever solution isn’t about adding complexity - it’s about recognizing when your problem has a simpler shape than you first imagined. String length as a discriminator only emerged after writing those complex versions first. It’s a reminder that code often improves through deletion rather than addition.
Holiday Calculations Calculating moving holidays taught me a lesson about readability versus cleverness. Here’s how the solution evolved.
First Try: Calendar Math
Initially, I tried to calculate everything from scratch:
def get_irish_holidays(year):
holidays = {}
# Calculate Easter using Gauss formula
a = year % 19
b = year % 4
c = year % 7
k = year // 100
p = (13 + 8 * k) // 25
q = k // 4
M = (15 - p + k - q) % 30
N = (4 + k - q) % 7
d = (19 * a + M) % 30
e = (2 * b + 4 * c + 6 * d + N) % 7
easter_day = 22 + d + e
easter_month = 3
if easter_day > 31:
easter_day -= 31
easter_month = 4
easter = date(year, easter_month, easter_day)
holidays[easter] = "Easter Sunday"
# Calculate first Mondays with manual iteration
may_first = date(year, 5, 1)
days_until_monday = (7 - may_first.weekday()) % 7
holidays[may_first + timedelta(days=days_until_monday)] = "May Bank Holiday"
It worked but was dense and error-prone. Reading the code told you nothing about the holidays.
Next: Library Functions
Then I discovered dateutil’s relativedelta:
def get_irish_holidays(year):
holidays = {}
# Fixed dates
holidays[date(year, 1, 1)] = "New Year's Day"
holidays[date(year, 3, 17)] = "St. Patrick's Day"
# Calculate Easter with helper
easter = calculate_easter(year)
holidays[easter] = "Easter Sunday"
holidays[easter + timedelta(days=1)] = "Easter Monday"
holidays[easter - timedelta(days=2)] = "Good Friday"
# First Mondays with relativedelta
for month in [5, 6]:
first = date(year, month, 1)
monday = first + relativedelta(weekday=MO(1))
holidays[monday] = f"{calendar.month_name[month]} Bank Holiday"
Better, but mixing timedelta and relativedelta made intentions unclear.
Final: Clear Intent
The winning version uses consistent tools and clear naming:
def get_irish_holidays(year):
holidays = {}
# Fixed dates first
holidays[date(year, 1, 1)] = "New Year's Day"
holidays[date(year, 3, 17)] = "St. Patrick's Day"
# Easter-based holidays
easter = easter(year) # Using built-in calculator
holidays[easter] = "Easter Sunday"
holidays[easter + relativedelta(days=1)] = "Easter Monday"
holidays[easter - relativedelta(days=2)] = "Good Friday"
# First Mondays
holidays[date(year, 5, 1) + relativedelta(weekday=MO(+1))] = "May Bank Holiday"
holidays[date(year, 6, 1) + relativedelta(weekday=MO(+1))] = "June Bank Holiday"
Now the code tells a story: Fixed dates are obvious, Easter-based holidays move relative to Easter, and bank holidays are always first Mondays. The complexity of calculating Easter is hidden behind a library call, and relativedelta makes the “first Monday” logic read naturally.
Why It’s Efficient
PDF Generation
Using ReportLab directly instead of more abstracted libraries gives us precise control:
def generate_calendar(self, year, output_file):
doc = SimpleDocTemplate(
output_file,
pagesize=self.page_size,
rightMargin=10*mm,
leftMargin=10*mm,
topMargin=15*mm,
bottomMargin=15*mm
)
Flask Without Complexity
The entire web interface is just a few routes:
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
# Handle file upload or CSV text
if 'file' in request.files:
file = request.files['file']
# Process CSV
elif csv_text := request.form.get('csv_text'):
# Process pasted CSV
# Generate and return PDF
return send_file(pdf_path, as_attachment=True)
return render_template('index.html')