I've always used org-mode for managing and displaying my calendar, but
it's worth noting that Emacs comes with a built-in calendar that
already offers a lot of functionalities and customizations. Its
documentation sits within the Emacs manual (C-h i m Emacs RET g
Calendar/Diary RET
).
Among other things, the Emacs calendar comes with a lot of
predefined holidays that can be displayed in the calendar buffer (by
pressing x
) or listed using e.g. calendar-holidays
. Dates of
holidays are computed based on the value of several lists such as
holiday-general-holidays
, holiday-islamic-holidays
or
holiday-christian-holidays
. It is possible to customize these, but
the calendar offer two variables holiday-local-holidays
and
holiday-other-holidays
that are meant to be customized.
Let's see how we can customize the calendar to display the dates for public holidays in the United Kingdom. One potential difficulty is that, in the UK, public holidays that fall inside a weekend are pushed back to the first next non-holiday week day. But with the power of Lisp in our hands, I'm not to worried..
1. Setting UK bank holidays
Disable the default holidays. I'm not interested in most of them, and the few Christian holidays relevant when living in the UK are computed further below.
(setq holiday-general-holidays nil
holiday-bahai-holidays nil
holiday-hebrew-holidays nil
holiday-christian-holidays nil
holiday-islamic-holidays nil
holiday-oriental-holidays nil)
Next we define the holiday list for the UK bank holidays:
- Christmas
- Boxing Day
- New Year's day
- Good Friday
- Easter Monday
- Early May bank holiday
- Spring bank holiday
If any of the above fall on a Saturday of Sunday, a substitute bank holiday day is applied on the next non-holiday week day. It's usually the next Monday, except when Boxing Day or Christmas day falls on a Sunday, in which case the substitute falls on a Tuesday. In this case Monday is already a holiday, or substitute holiday.
Below are two functions find-next-weekday
and set-double-holiday
that deal with finding the final date for a bank holiday,
substituting to the next non-holiday weekday if required:
(defun is-weekend (date) "Evaluates to t or nil whether of not DATE falls in a weekend. DATE is a list of the form (MONTH DAY YEAR)" (or (equal (calendar-day-of-week date) 6) (equal (calendar-day-of-week date) 0))) (defun add-one-day (date) "Evaluates to (MONTH DAY YEAR) for the day following DATE. DATE should also be provided in the (MONTH DAY YEAR) format" (calendar-gregorian-from-absolute (+ (calendar-absolute-from-gregorian date) 1))) (defun find-next-weekday (date) "Evaluate to (MONTH DAY YEAR) for the first next weekday after or including DATE. If DATE corresponds to a Saturday or Sunday, then evaluates to following Monday. If DATE is a weekday, then evaluates to DATE. This function is useful to compute bank holidays in the United Kingdom, which are pushed to first following non-bank holiday weekday (ususally Monday) if the bank holiday falls inside a weekend. Caveat: do not use function to compute the substitute day for a Sunday bank holiday that follows a Saturday bank holiday (e.g. boxing day on a Sunday) or the substitute day for a Saturday bank holiday t hat precedes a Monday bank holiday (e.g. Christmas day on a Saturday). See functions `set-boxing-day` and `set-christmas-day` for dealing with these special cases." (let ((next-day-date (add-one-day date))) (if (is-weekend date) (find-next-weekday next-day-date) date))) (defun set-double-holiday (date) "Evaluates to the (potentially substitute) date for a holiday inside a holiday pair (e.g. Christmas Day followed by Boxing Day). If DATE is a Saturday, then substitute day is next Monday as usual. If DATE is a Sunday, then substitute day is next Tuesday, because next Monday is already a bank holiday (Boxing Day) or substitute day (Christmas)." (cond ((equal (calendar-day-of-week date) 6) (find-next-weekday date)) ((equal (calendar-day-of-week date) 0) (add-one-day (find-next-weekday date))) (t date)))
Functions find-next-weekday
and set-double-holiday
are used in
conjunction with holiday-sexp
like so:
(holiday-sexp '(set-double-holiday (list 12 25 year)) "Christmas day bank holiday") (holiday-sexp '(set-double-holiday (list 12 26 year)) "Boxing day bank holiday") (holiday-sexp '(find-next-weekday (list 01 01 year)) "New Year's Day bank holiday")
The Spring bank holiday is almost always set on the last Monday of May. But in the case of a royal Jubilee year this holiday in usually moved further in June to pair it with the Jubilee bank holiday. A royal Jubilee occurs on the 10th and 25th anniversaries of the current British monarch's accession to the throne. Not all the anniversaries are associated a bank holiday however, and it seems that this is left to the discretion of the government and/or royal family. Even though she accessed the throne in February, Queen Elizabeth II's Jubilee are always celebrated in June at a date that seems to be confirmed only a year before.
That is to say that Jubilee dates and corresponding Spring bank holiday cannot be computed years in advance. As a result, I simply do not set the Spring bank holiday if the current year is a Jubilee year.
(defun is-royal-jubilee (accession-year year) "Evaluates to t or nil depending on whether or not year YEAR is a royal jubilee year for current British monarch who accessed the throne in year ACCESSION-YEAR. Jubilees occur on the 10th and 25th anniversaries of the accession year. For instance Queen Elizabeth II's silver jubilee was held in 1977, 25 years after her accession in 1952. This function is useful to prevent the setting of the Spring bank holiday in case of a jubilee year. In this case the Spring bank holiday is usually pushed from its usual date (last Monday of May) to later in June, to pair it with the jubilee bank holiday." (or (zerop (% (- displayed-year accession-year) 10)) (zerop (% (- displayed-year accession-year) 25))))
The current British monarch is Queen Elizabeth II who accessed the throne in 1952, so
(if (not (is-royal-jubilee 1952 displayed-year)) (holiday-float 5 1 -1 "Spring bank holiday"))
Overall, we set the list of holiday forms for the UK:
(setq tl/holiday-uk-holidays '((holiday-float 8 1 -1 "Summer bank holiday") (holiday-sexp '(set-double-holiday (list 12 25 year)) "Christmas day bank holiday") (holiday-sexp '(set-double-holiday (list 12 26 year)) "Boxing day bank holiday") (holiday-sexp '(find-next-weekday (list 01 01 year)) "New Year's Day bank holiday") (holiday-easter-etc -2 "Good Friday") (holiday-easter-etc 0 "Easter Monday") (holiday-float 5 1 1 "Early May bank holiday") (if (not (is-royal-jubilee 1952 displayed-year)) (holiday-float 5 1 -1 "Spring bank holiday"))))
Lastly, we set the upcoming Platinum Jubilee bank holiday and corresponding Spring bank holiday is a special list of holiday forms
(setq tl/holiday-exceptional-uk-holidays '((if (equal displayed-year 2022) (holiday-fixed 6 2 "Spring bank holiday")) (if (equal displayed-year 2022) (holiday-fixed 6 3 "Platinum Jubilee bank holiday"))))
Finally, we set the value of the holiday-local-holidays
variable
for the holidays to appear in the calendar.
(setq holiday-local-holidays (append
tl/holiday-uk-holidays
tl/holiday-exceptional-uk-holidays))
2. Fête des Mères et Fête des Pères
I want to add a shorter example of customizing the calendar, to
illustrate how the built-in functions holiday-float
,
holiday-sexp
and similar can be used to compare holiday dates for
a given year.
I'd like to have the French Mothers' Day and Fathers' Day appear in my calendar. In France, Mother's Day is planned for the last Sunday of May, unless this conflicts with Pentecost (Whitsunday) in which case it is pushed to the first Sunday of June.
The date for Pentecost can be computed using the built-in function
holiday-easter-etc
. The following decides on the date for
Mothers' Day based on whether or not the last Sunday of May is the
Pentecost day.
(if (equal (holiday-easter-etc 49 "string") (holiday-float 5 0 -1 "string")) (holiday-float 6 0 1 "Fête des Mères (repoussé après Pentecôte)") (holiday-float 5 0 -1 "Fête des Mères"))
Because this function is supposed to be eval
'd when building the
holiday list, it expects an argument string
for a description.
When comparing dates we just set it to an arbitrary string "string"
.
In France, Fathers' Day is the third Sunday of June. Combining this
with Mothers' Day above, we set the variable holiday-other-holidays
.
(setq holiday-other-holidays '((if (equal (holiday-easter-etc 49 "string") (holiday-float 5 0 -1 "string")) (holiday-float 6 0 1 "Fête des Mères (repoussé après Pentecôte)") (holiday-float 5 0 -1 "Fête des Mères")) (holiday-float 6 0 3 "Fête des Pères")))