Emacs diary mode

I recently started using Emacs diary mode again, probably a couple of decades or so since I first played with it. It does a lot of things I like, but there are a couple of things that I wanted that didn't seem to be obvously available, and also a couple of things that, although easy, weren't quite as obvious as I'd hoped from the documentation.

As a start, here is some code to put into your .emacs to give you UK holidays. I intend to add more bits and pieces here over time.

Roy Badami <roy@gnomon.org.uk>

UK holidays

Here is some code to add to your .emacs to give you a more UK-centric set of holidays.

It redefines general-holidays to be more relevent to the UK.

It computes a full list of bank holidays in England and Wales and other notable dates, though is currently rather Anglocentric. I hope to fix this in a future version, but in the mean time this should hopefully still serve as a useful starting point for UK Emacs-users situated outside England.

Of particular note it includes the extra bank holidays that are added if Christmas Day, Boxing Day or New Year's Day falls on a weekend, and also computes the correct UK dates for Mother's Day and Father's Day (which, of course, differ from the US dates used by default by Emacs).

The US federal holidays (that don't duplicate ours) are provided in other-holidays so you don't lose the major US notable dates—if you have no interest in these then simply omit them.

It also redefines christian-holidays to remove those holidays that duplicate holidays important enough to be included in the UK definition of general-holidays. This isn't ideal, but I can't see an obvious way round it, given a number of holiday-related functions don't make any effort to remove duplicate holidays.

N.B. This code relies on the Emacs 22 definition of holiday-easter-etc—if you need Emacs 21 code give me a shout.

This is quite long, but you don't need to understand it if you don't want to. Just put it in your .emacs and forget about it!

;;UK public holidays, and other UK notable dates.
(setq general-holidays
      '((holiday-fixed 1 1 "New Year's Day")
	(holiday-new-year-bank-holiday)
	(holiday-fixed 2 14 "Valentine's Day")
	(holiday-fixed 3 17 "St. Patrick's Day")
	(holiday-fixed 4 1 "April Fools' Day")
	(holiday-easter-etc -47 "Shrove Tuesday")
	(holiday-easter-etc -21 "Mother's Day")
	(holiday-easter-etc -2 "Good Friday")
	(holiday-easter-etc 0 "Easter Sunday")
	(holiday-easter-etc 1 "Easter Monday")
	(holiday-float 5 1 1 "Early May Bank Holiday")
	(holiday-float 5 1 -1 "Spring Bank Holiday")
	(holiday-float 6 0 3 "Father's Day")
	(holiday-float 8 1 -1 "Summer Bank Holiday")
	(holiday-fixed 10 31 "Halloween")
	(holiday-fixed 12 24 "Christmas Eve")
	(holiday-fixed 12 25 "Christmas Day")
	(holiday-fixed 12 26 "Boxing Day")
	(holiday-christmas-bank-holidays)
	(holiday-fixed 12 31 "New Year's Eve")))

;;Major US holidays
(setq other-holidays
      '((holiday-float 1 1 3 "Martin Luther King Day")
	(holiday-float 2 1 3 "President's Day")
	(holiday-float 5 1 -1 "Memorial Day")
	(holiday-fixed 7 4 "Independence Day")
	(holiday-float 9 1 1 "Labor Day")
	(holiday-float 10 1 2 "Columbus Day")
	(holiday-fixed 11 11 "Veteran's Day")
	(holiday-float 11 4 4 "Thanksgiving")))

;;N.B. It is assumed that 1 January is defined with holiday-fixed -
;;this function only returns any extra bank holiday that is allocated
;;(if any) to compensate for New Year's Day falling on a weekend.
;;
;;Where 1 January falls on a weekend, the following Monday is a bank
;;holiday.
(defun holiday-new-year-bank-holiday ()
  (let ((m displayed-month)
	(y displayed-year))
    (increment-calendar-month m y 1)
    (when (<= m 3)
      (let ((d (calendar-day-of-week (list 1 1 y))))
	(cond ((= d 6)
	       (list (list (list 1 3 y)
			   "New Year's Day Bank Holiday")))
	      ((= d 0)
	       (list (list (list 1 2 y)
			   "New Year's Day Bank Holiday"))))))))

;;N.B. It is assumed that 25th and 26th are defined with holiday-fixed -
;;this function only returns any extra bank holiday(s) that are
;;allocated (if any) to compensate for Christmas Day and/or Boxing Day
;;falling on a weekend.
;;
;;Christmas day is always 25 December; beyond that there is no
;;entirely consistent practice.  We proceed as follows:
;;
;;Traditionally, Boxing day was the first day after Christmas, not
;;including Sundays (i.e. if Christmas fell on a Saturday, Boxing Day
;;was Monday 27th) however we follow modern practice here and always
;;regard Boxing Day as 26 December (which, as noted above, is never
;;returned by this function).
;;
;;Generally the extra bank holiday is allocated on the first available
;;day that would otherwise have been a working day.  However in the
;;case where we need to allocate two additional bank holidays -
;;i.e. where Christmas Day falls on the Saturday, there is some
;;confusion as to how to proceed.  We allocate the Boxing Day Bank Holiday
;;to the Monday, since this is the historic date of Boxing Day in this
;;case, and allocate the Christmas Day Bank Holiday to the following day.
;;
;;This is consistent with the way that the 'substitute days' were
;;allocated in the list of bank holidays on the Department of Trade
;;and Industry in the recent past, although they don't use the any
;;specific names for these holidays.
;;
;;The latest list on the direct.gov.uk web site is not consistent with
;;this practice, however, allocating the substitute days for Christmas
;;Day and Boxing Day in the other order in 2010.  However this list
;;also manages to allocate them in order in 2011 (where Christmas Day
;;falls on a Sunday), therefore placing the substitute holiday for
;;Christmas Day _on_ Boxing Day, and then the substitute holiday for
;;Boxing Day on the following day.  I'm not at all sure this isn't a
;;mistake.
;;
;;In any case, this is largely academic as there is no dispute over
;;which days are public holidays, only what to call them - so unless
;;you care deeply just ignore the issue and use the function as
;;supplied.
(defun holiday-christmas-bank-holidays ()
  (let ((m displayed-month)
	(y displayed-year))
    (increment-calendar-month m y -1)
    (when (>= m 10)
      (let ((d (calendar-day-of-week (list 12 25 y))))
	(cond ((= d 5)
	       (list (list (list 12 28 y)
			   "Boxing Day Bank Holiday")))
	      ((= d 6)
	       (list (list (list 12 27 y)
			   "Boxing Day Bank Holiday")
		     (list (list 12 28 y)
			   "Christmas Day Bank Holiday")))
	      ((= d 0)
	       (list (list (list 12 27 y)
			   "Christmas Day Bank Holiday"))))))))

;;Comment out the Christian holidays that also have secular
;;significance in the UK (Shrove Tuesday, Good Friday, Easter Sunday,
;;Christmas) as EMACS doesn't remove duplicates holidays.  These
;;holidays are included in the UK redefinition of general-holidays
;;(where Chistmas is listed as Christmas Day).
(setq christian-holidays
      '((if all-christian-calendar-holidays
	    (holiday-fixed 1 6 "Epiphany"))
;	(holiday-easter-etc 0 "Easter Sunday")
;	(holiday-easter-etc -2 "Good Friday")
	(holiday-easter-etc -46 "Ash Wednesday")
	(if all-christian-calendar-holidays
	    (holiday-easter-etc -63 "Septuagesima Sunday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc -56 "Sexagesima Sunday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc -49 "Shrove Sunday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc -48 "Shrove Monday"))
;	(if all-christian-calendar-holidays
;	    (holiday-easter-etc -47 "Shrove Tuesday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc -14 "Passion Sunday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc -7 "Palm Sunday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc -3 "Maundy Thursday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc 35 "Rogation Sunday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc 39 "Ascension Day"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc 49 "Pentecost (Whitsunday)"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc 50 "Whitmonday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc 56 "Trinity Sunday"))
	(if all-christian-calendar-holidays
	    (holiday-easter-etc 60 "Corpus Christi"))
	(if all-christian-calendar-holidays
	    (holiday-greek-orthodox-easter))
	(if all-christian-calendar-holidays
	    (holiday-fixed 8 15 "Assumption"))
	(if all-christian-calendar-holidays
	    (holiday-advent 0 "Advent"))
;	(holiday-fixed 12 25 "Christmas")
	(if all-christian-calendar-holidays
	    (holiday-julian 12 25 "Eastern Orthodox Christmas"))))

Recurring events with exceptions

Added 11 April 2009

Emacs diary mode allows you to enter recurring or repeating events very easily using S-expression diary entries. e.g. to create a diary entry on 7 January 2009, repeating every 14 days thereafter, you can write the following. (This assumes European date format; if you're using the default North American date format you would need to swap the day and month):

%%(diary-cyclic 14 7 1 2009) Company meeting.

But what happens if there are exceptions? I was expecting that, like most calendaring programs, Emacs diary mode would have special mechanisms to handle such exceptions—but the documentation didn't seem to mention any.

After a short while I realised that the reason is that there is no need for any special mechanisms; you can achieve this with some very simple LISP code.

The thing to understand is that S-expression diary entries are evaluated for each day and return a boolean to say whether or not they match that day—there's a little bit more to it than that but the details aren't relevent here. Essentially they're predicates on some dynamically bound variables that you don't need to worry about the details of. But the important thing is that they are booleans so you can use and, or and not to construct all kinds of exceptions.

Let's say that you want to modify the above to show there is no meeting on 4 Feburary 2009 you could write the following (all examples in European date order):

%%(and (diary-cyclic 14 7 1 2009)
       (not (diary-date 4 2 2009))) Company meeting

If you wanted to exclude 18 March 2009 as well you could write:

%%(and (diary-cyclic 14 7 1 2009)
       (not (or (diary-date 4 2 2009)
                (diary-date 18 3 2009)))) Company meeting

Actually, if you don't do LISP and just want a recipe to memorise, you might prefer the following, although personally I find the above more natural.

%%(and (diary-cyclic 14 7 1 2009)
       (not (diary-date 4 2 2009))
       (not (diary-date 18 3 2009))) Company meeting

That's probably all most people really, absolutely, need but if you are comfortable with LISP syntax you can do so much more, so let's try something a little more complicated.

Let's say I have a weekly departmental meeing on Mondays, starting 5 January 2009. However the meeting on 23 February has been rescheduled for 24th, and from 20 April 2009 to 11 May 2009 (inclusive) there are no meetings. You can write:

%%(or (and (diary-cyclic 7 5 1 2009)
           (not (or (diary-date 23 2 2009)
                    (diary-block 20 4 2009 11 5 2009))))
      (diary-date 24 2 2009)) Departmental meeting

Here we use diary-block to exclude a range of dates. We also wrap the whole thing up in an or to allow us to add in additional dates—in this case the rescheduled meeting on 24 February 2009—that don't fit the pattern.

So, as you see, using S-expressions to define diary events is incredibly flexible. The reason there's no special facility, as such, for defining exceptions to recurring events is that there's absolutely no need for one!

It's just a pity the documentation doesn't make this more obvious...