6 min read
Calendar Builder

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:

  1. Uses less code
  2. Has fewer edge cases
  3. Shows clear intent
  4. Still validates dates through datetime
  5. 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')

Close