Format Dates Calendar.Txt Style

Format your dates Calendar.txt style, everywhere! Like 2024-01-04 w01 Thu.

I printed every day from year 1700 to 2400, using Python 3, Go, PHP, Django templates and 'date'. Then I checked that results match.

day.strftime("%Y-%m-%d w%V %a") # Python
$date->format('Y-m-d \wW D') // PHP
date +"%Y-%m-%d w%V %a" # Linux 'date'

Django Templates and Go are a bit more verbose, read the full programs below. Or download them all (calendartxt-date-2024-01-04.zip, 5.5 MB).

Looking for Calendar.txt? This article is just about date formats. If you want to keep your calendar in a plain text file, head to Calendar.txt

Test output

For each language or command, a test script is generated.

Test script prints each date to standard output.

  • Start date: 1700-01-01 w53 Fri (inclusive)
  • End date: 2400-01-01 w52 Sat (inclusive)

Rationale for the testing period

  • Must be considerably longer than the lifetime of any living person now (2024).
  • Should not go near the edge of clearly defined dates, e.g. dates outside gregorian calendar. This is to avoid irrelevant errors outside the range of real-life usable dates.

Rationale for dates

  • Start date 1700-01-01 w53 Fri
    • 1700 is much later than "Inter gravissimas" bulla in 1582
    • 1700 is over 300 years ago
    • Conveniently, it's on w53 (most years have only 52 weeks)
  • End date 2400-01-01 w52 Sat
    • 2400 is over 300 years into the future

Corrrect output file

The correct output file is dategen-go.txt. 4.7 MB, 255 670 dates, 255 670 lines. SHA256 hash is 343b12bae2b459f0bfe2f676f7350210fae9a19aa54f756cc0a4063098b29b37.

When test output is saved into a file, this is the correct result.

The file ends with a newline "\n". There are no other empty lines. There must be no whitespace at the end of lines.

$ sha256sum out/*
343b12bae2b459f0bfe2f676f7350210fae9a19aa54f756cc0a4063098b29b37  out/dategen-django.txt
343b12bae2b459f0bfe2f676f7350210fae9a19aa54f756cc0a4063098b29b37  out/dategen-go.txt
343b12bae2b459f0bfe2f676f7350210fae9a19aa54f756cc0a4063098b29b37  out/dategen-php.txt
343b12bae2b459f0bfe2f676f7350210fae9a19aa54f756cc0a4063098b29b37  out/dategen-python.txt
343b12bae2b459f0bfe2f676f7350210fae9a19aa54f756cc0a4063098b29b37  out/dategen-sh.txt

As all correct output files are identical, we can further look at any one file

$ wc -l dategen-go.txt 
255670 dategen-go.txt

So it has 255 670 lines, each representing a date.

$ head -1 dategen-go.txt; tail -1 dategen-go.txt
1700-01-01 w53 Fri
2400-01-01 w52 Sat

Debugging incorrect output

If sha256sum does not match, the file is incorrect.

A count of diff lines gives an idea if all lines are bad. If all are bad, you can look at the first line to see if there is a major problem. Also, white space at the end of line is a hard to see candidate.

diff correct.txt new.txt |wc -l

If only some lines are incorrect, check if zero padding in weeks is missing.

In addition to these obvious things, there could be incorrect or weird date calculations.

Full Date Format Programs

Python - dategen.py

#!/usr/bin/python3
# Copyright 2024 Tero Karvinen https://TeroKarvinen.com

from datetime import datetime
from dateutil.relativedelta import relativedelta

start = datetime(1700, 1, 1)
end = datetime(2400, 1, 1)

day = start

while day <= end:
	# "%W" is not ISO8601 week, as it includes week zero (0). 
	# Correct ISO8601 week is "%V", https://bugs.python.org/issue12006 fixed in 2015. 
	print(day.strftime("%Y-%m-%d w%V %a"))
	day += relativedelta(days=+1)

PHP - dategen.php

#!/usr/bin/php
<?php
# Copyright 2024 Tero Karvinen https://TeroKarvinen.com

$format = 'Y-m-d \wW D'; // https://terokarvinen.com/2021/calendar-txt/
$start = new DateTime('1700-01-01');
$end = new DateTime('2400-01-02'); // one day past last date to make it inclusive, 2400-01-01

$interval = new DateInterval('P1D'); // one day
$period = new DatePeriod($start, $interval, $end);

foreach ($period as $date) {
   echo $date->format($format) . "\n";
}
?>

Bash / date - dategen.sh

#!/usr/bin/bash
# Copyright 2024 Tero Karvinen https://TeroKarvinen.com

STARTDATE="1700-01-01" # first date, inclusive, YYYY-MM-DD, e.g. "1700-01-01"
# STARTDATE="2399-12-01" # for testing the last date
ENDDATE="2400-01-02" # one day past last date, YYYY-MM-DD, it's inclusive "2400-01-01"

# OUTFILE="out/dategen-sh-out.txt"

FORMAT="%Y-%m-%d w%V %a"
DATE=$(date -d "$STARTDATE" +"$FORMAT")

# echo "" > "$OUTFILE"

until [ "$ENDDATE" == "$(echo "$DATE"|head -c10)" ]
do
	# echo "$DATE" >> "$OUTFILE"
	echo "$DATE"
	DATE=$(date -d "$DATE + 1 days" +"$FORMAT")
done

Golang Go - dategen-go.go

// Copyright 2020-2024 Tero Karvinen http://TeroKarvinen.com

package main

import (
	"fmt"
	"time"
)

func main() {

	/* Print dates */
	weekdays := []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
	day := time.Date(1700, time.January, 1, 0, 0, 0, 0, time.UTC)
	// day := time.Date(2399, time.December, 1, 0, 0, 0, 0, time.UTC) // for testing
	end := time.Date(2400, time.January, 1, 0, 0, 0, 0, time.UTC)
	for !day.After(end) {
		_, w := day.ISOWeek()
		weekday := weekdays[day.Weekday()]
		fmt.Printf("%v w%02d %s\n", day.Format("2006-01-02"), w, weekday)
		day = day.Add(time.Hour * 24)
	}
}

Django Templates - dategen-django.py

Django Template solution is not that pretty.

#!/usr/bin/python3
# Copyright 2024 Tero Karvinen https://TeroKarvinen.com

# sudo apt-get install python3-django
# django-admin --version # prints "3.2.19"

from datetime import datetime
from dateutil.relativedelta import relativedelta

from django.template import Template, Context
from django.conf import settings
import django

## Set up Single file Django

settings.configure(TEMPLATES=[
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'APP_DIRS': False, # we have no apps
    },
])
django.setup()

## Print dates

start = datetime(1700, 1, 1)
# end = datetime(1700, 1, 18)
end = datetime(2400, 1, 1)

day = start

while day <= end:
	# https://docs.djangoproject.com/en/3.2/ref/templates/builtins/#date picks, dashes added, reordered """
	# - Uses a similar format to PHP’s date() function with some differences.
	# - o ISO-8601 week-numbering year, corresponding to the ISO-8601 week number (W) which uses leap weeks. See Y for the more common year format. '1999'
	# - W SO-8601 week number of year, with weeks starting on Monday. 1, 53
	# """
	# Not the same as in PHP. This works in PHP "Y-m-d \wW D", but lacks week zero padding in Django.
	# It seems that Django template filter "date" does not have a zero padded week number as of 2024-01-04 w01 Thu.
	# To get zero padded ISO week number, 'day | date:"W"' prints week number as a string without padding "2",
	# which is then converted to integer '| add:"0"' and padded with leading zero 'stringformat:"02d"'
	# "Y" seems to be the correct year for ISO8601 dates. The suggested "o" gives nonsensical results.
	t = Template('{{ day | date:"Y-m-d" }} w{{ day | date:"W" | add:"0" | stringformat:"02d"}} {{ day | date:"D" }}') 
	c = Context({'day': day})
	s = t.render(c)
	print(s)

	day += relativedelta(days=+1)

Next Steps

Keep your calendar in a plain text file, head to Calendar.txt.

Adminstrivia

2024-01-11 w02 Thu: fixed some typos.