cft

Detecting new and shifting public holidays with Ruby

In the last few years I've run into a couple of cases where public holidays weren't occuring where they usually would, and even a case of a new public holidays happening. In this post we take a look at a Ruby gem that can help define when holidays occur.


user

Anton Ivanopoulos

a year ago | 5 min read

Our app has a few features that involve patient communications, specifically around not sending those communications out when there’s a public holiday (when a clinic might be closed and unable to handle incoming calls).

Countries have different holidays, but that also applies to regions and cities. Because people from all across the country use our app, we account for that. We'll detect if there’s a holiday or not for a given set of users based on location, which will affect those comms going out.

In the last couple of years, some interesting cases had confused customers because holidays started behaving weirdly, sending out things when we weren’t expecting to or not sending things when we should.

We use the holidays gem for detecting holidays on given dates, so I’ll go over what we ran into and how some previously-unused gem features helped us.

tl;dr

  • We had a case of an existing public holiday shifting from when it would usually occur, and we needed to account for the new date.
  • We had a case of a new, one-time public holiday that we needed to define ahead of time.

The Basics

The holidays gem uses a series of yaml definition files that list all of the holidays in a given region. An example of one of these definitions is this one for New Year's Day:

months:

1:

- name: New Year's Day

regions: [au, au_nsw, au_vic, au_act, au_sa, au_wa, au_nt, au_qld]

mday: 1

observed: to_monday_if_weekend(date)

There are a few parts here:

  • You specify the month the holiday occurs.
  • You can define more granular regions that observe the holiday. In this case, New Year's Day applies to all states in Australia and the whole AU region.
  • You can define the day of the month it occurs on (you can also specify the week number it occurs in and the day of that week)
  • You can also define functions to handle the definition. In this case, we need to move the observed date to the following Monday if the holiday falls on a weekend. You can use a function to handle everything if things are particularly complex.

Then when you want to check if there's a holiday on a certain date, you can check it in the following way:

=> [{:name => 'ANZAC Day',...}]

Holidays that don't want to sit still

One of the interesting gotchas from the past couple of years with COVID is having to handle holidays that shift around due to extenuating circumstances.

In this case, we had support tickets from confused Brisbane customers asking us why things weren't sending as expected on a random Wednesday in August. This holiday had a series of interesting cases from 2020–2021. In 2020, it got moved to a Friday when it usually occurs on a Wednesday, and in 2021 it was moved to a later date (October sometime). On this most recent occurrence in 2022, it was a case of the holiday being detected by holidays on the wrong Wednesday in August.

Taking a bit of a closer look, we found the holiday defined as follows:

8:

- name: Ekka

regions: [au_qld_brisbane]

week: -3

wday: 3

It turns out that this mostly works, but, is actually a little too simple for how the holiday actually occurs. As taken from the QLD public holidays page:

The Royal National Agricultural (RNA) Show Day (Brisbane only) is held on the Wednesday during the RNA Show period. The RNA Show commences on the first Friday in August, unless the first Friday is prior to 5 August, then it commences on the second Friday of August.

So the holiday has a bit of a dynamic aspect not currently being taken into account. On a recent post, I had someone comment that they had used my post as a launchpad for making a change in an open source project, and that inspired me to go and sort this out myself. It ended up being a pretty simple change (my first proper OSS contribution, hell yeah), but essentially changes the holiday definition to:

- name: Ekka

regions: [au_qld_brisbane]

function: qld_brisbane_ekka_holiday(year)

With an accompanying function (I found writing this ruby function in yaml to be pretty funky but we got there):

qld_brisbane_ekka_holiday:

# https://www.qld.gov.au/recreation/travel/holidays/public

# The Ekka holiday occurs on the Wednesday during the RNA Show perido, the RNA show occurs on the first Friday of August, unless that's prior to August 5, then it occurs on the second.

arguments: year

ruby: |

first_friday = Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 8, :first, :friday)

if first_friday < 5

second_friday = Date.civil(year, 8, Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 8, :second, :friday))

second_friday + 5 # The next Wednesday

else

Date.civil(year, 8, first_friday) + 5

end

While putting this together, I found it pretty cool that you also define the tests in the yaml file. The development flow has you changing the branch that the holidays/definitions submodule inside the holidays/holidays repo is using, running make commands to generate the functions and tests, and then running those tests.

- given:

date: "2019-08-14"

regions: ["au_qld_brisbane"]

expect:

name: "Ekka"

- given:

date: "2022-08-10"

regions: ["au_qld_brisbane"]

expect:

name: "Ekka"

I added some extra tests for 2023/24, so we're ready to rock when next year rolls around.

New public holidays

With the recent passing of Her Majesty Queen Elizabeth II, Australia announced that it would have a Nation Day of Mourning on 22 September and that it would be a public holiday.

The holidays gem allows you to load in custom holidays, so we opted to go that route in the short term. We added a new initializer to load in a custom holidays.yml file that contained the definition (shout out to Alex for putting this together):

Holidays.load_custom("#{__dir__}/holidays.yaml")

# Custom holiday definitions for the holidays gem (https://github.com/holidays/holidays)

#

# See https://github.com/holidays/definitions/blob/master/doc/SYNTAX.md

---

months:

9:

- name: National Day of Mourning for Queen Elizabeth II

regions: [au]

mday: 22

year_ranges:

limited: [2022]

Moving forward

This was all pretty straightforward stuff and I definitely don't expect us to be needing to do these kinds of changes often (the definition for the Day of Mourning is already merged into the gem itself), but I really enjoyed getting a better look at the internals of this gem. Holidays are a bit of a hairy beast (especially when you have new ones popping up), but it was interesting to poke at some of the features on offer here that we hadn't really had to play with before.

Some final questions I'm left with:

  • Can you override the definition of an existing holiday with something new?
  • Can you exclude holidays from the bundled definitions? (It seems like an open point of discussion in this issue)

Upvote


user
Created by

Anton Ivanopoulos

Software Engineer at HotDoc, currently working in Platform Enablement.


people
Post

Upvote

Downvote

Comment

Bookmark

Share


Related Articles