Business Days

Up to now we have dealt with date operations that do not take into consideration one of the key features, holidays. Whether its working around weekends or the UK's bank holidays, operations involving holidays (or equivalently business days) is essential.

As such the RDate package provides the construct to allow you to work with holiday calendars, without tying you to a specific implementation.

Note

RDates will never provide an explicit implementation of the calendar system, but do check out HolidayCalendars which will get released soon which builds rule-based calendar systems.

Before we walk through how this is integrated into the RDate language, we'll look at how calendars are modelled.

Calendars

A calendar defines whether a given day is a holiday. To implement a calendar you need to inherit from RDates.Calendar and define the following methods.

RDates.is_holidayFunction
is_holiday(calendar::Calendar, date::Dates.Date)::Bool

Determine whether the date requested is a holiday for this calendar or not.

source
RDates.holidaysFunction
holidays(calendar::Calendar, from::Dates.Date, to::Dates.Date)::Vector{Bool}

Get the vector of each day and whether its a holiday or not, inclusive of from and to.

source

Calendars also come with a number of helpful wrapper methods

RDates.holidaycountFunction
holidaycount(calendar::Calendar, from::Dates.Date, to::Dates.Date)::Int

Get the number of holidays in the calendar between two dates (inclusive)

source
RDates.bizdaycountFunction
bizdaycount(calendar::Calendar, from::Dates.Date, to::Dates.Date)::Int

Get the number of business days in the calendar between two dates (inclusive)

source

RDate provides some primitive calendar implementations to get started with

RDates.JointCalendarType
JointCalendar(calendars::Vector{Calendar}) <: Calendar

A grouping of calendars, for which it is a holiday if it's marked as a holiday for any of the underlying calendars.

By default addition of calendars will generate a joint calendar for you.

source
RDates.CachedCalendarType
CachedCalendar(cal::Calendar)

Creating a wrapping calendar that will cache the holidays lazily as retrieved for a given year, rather than loading them in one go.

source

Calendar Manager

To access calendars within the relative date library, we use a calendar manager. It provides the interface to access calendars based on their name, a string identifier.

A calendar manager must inherit from RDates.CalendarManager and implement the following

RDates.calendarMethod
calendar(calendarmgr::CalendarManager, names::Vector)::Calendar

Given a set of calendar names, request the calendar manager to retrieve the associated calendar that supports the union of them.

source

RDates provides some primitive calendar manager implementations to get started with

RDates.NullCalendarManagerType
NullCalendarManager()

The most primitive calendar manager, that will return an error for any request to get a calendar. The default calendar manager that is available when applying rdates using the + operator, without an explicit calendar manager.

source
RDates.SimpleCalendarManagerType
SimpleCalendarManager()
SimpleCalendarManager(calendars::Dict{String, Calendar})

A basic calendar manager which just holds a reference to each underlying calendar, by name, and will generate a joint calendar if multiple names are requested.

To set a calendar on this manager then use setcalendar!

julia> mgr = SimpleCalendarManager()
julia> setcalendar!(mgr, "WEEKEND", WeekendCalendar())
WeekendCalendar()

If you want to set a cached wrapping of a calendar then use setcachedcalendar!

julia> mgr = SimpleCalendarManager()
julia> setcachedcalendar!(mgr, "WEEKEND", WeekendCalendar())
CachedCalendar(WeekendCalendar())

Examples

julia> mgr = SimpleCalendarManager()
julia> setcalendar!(mgr, "WEEKEND", WeekendCalendar())
julia> is_holiday(calendar(mgr, ["WEEKEND"]), Date(2019,9,28))
true
source

When you just add a Date and an RDate then we'll use the NullCalendarManager() by default. To pass a calendar manager to the rdate when it's been applied, use the apply function.

RDates.applyFunction
apply(rdate::RDate, date::Dates.Date, calendarmgr::CalendarManager)::Dates.Date

The application of an rdate to a specific date, given an explicit calendar manager.

Examples

julia> cal_mgr = SimpleCalendarManager()
julia> setcalendar!(cal_mgr, "WEEKEND", WeekendCalendar())
julia> apply(rd"1b@WEEKEND", Date(2021,7,9), cal_mgr)
2021-07-12
source
apply(rdate::RDate, date::Dates.Date)::Dates.Date

The application of an rdate to a specific date, without an explicit calendar manager. This will use the NullCalendarManager().

source
apply(rounding::HolidayRoundingConvention, date::Dates.Date, calendar::Calendar)::Dates.Date

Apply the holiday rounding to a given date. There is no strict requirement that the resolved date will not be a holiday for the given date.

source

Calendar Adjustments

Now that we have a way for checking whether a given day is a holiday and can use your calendar manager, let's introduce calendar adjustments.

These allow us to apply a holiday calendar adjustment, after a base rdate has been applied. To support this we need to introduce the concept of Holiday Rounding.

Holiday Rounding Convention

The holiday rounding convention provides the details on what to do if we fall on a holiday.

RDates.HolidayRoundingNBDType
HolidayRoundingNBD()

Move to the next business day when a date falls on a holiday.

Examples

julia> cal_mgr = SimpleCalendarManager()
julia> setcalendar!(cal_mgr, "WEEKEND", WeekendCalendar())
julia> apply(RDates.CalendarAdj("WEEKEND", rd"0d", RDates.HolidayRoundingNBD()), Date(2021,7,10), cal_mgr)
2021-07-12
julia> apply(rd"0d@WEEKEND[NBD]", Date(2021,7,10), cal_mgr)
2021-07-12
source
RDates.HolidayRoundingPBDType
HolidayRoundingPBD()

Move to the previous business day when a date falls on a holiday.

Examples

julia> cal_mgr = SimpleCalendarManager()
julia> setcalendar!(cal_mgr, "WEEKEND", WeekendCalendar())
julia> apply(RDates.CalendarAdj("WEEKEND", rd"0d", RDates.HolidayRoundingPBD()), Date(2021,7,10), cal_mgr)
2021-07-09
julia> apply(rd"0d@WEEKEND[PBD]", Date(2021,7,10), cal_mgr)
2021-07-09
source
RDates.HolidayRoundingNBDSMType
HolidayRoundingNBDSM()

Move to the next business day when a date falls on a holiday, unless the adjusted date would be in the next month, then go the previous business date instead.

Examples

julia> cal_mgr = SimpleCalendarManager()
julia> setcalendar!(cal_mgr, "WEEKEND", WeekendCalendar())
julia> apply(RDates.CalendarAdj("WEEKEND", rd"0d", RDates.HolidayRoundingNBDSM()), Date(2021,7,10), cal_mgr)
2021-07-12
julia> apply(rd"0d@WEEKEND[NBDSM]", Date(2021,7,10), cal_mgr)
2021-07-12
julia> apply(rd"0d@WEEKEND[NBDSM]", Date(2021,7,31), cal_mgr)
2021-07-30
source
RDates.HolidayRoundingPBDSMType
HolidayRoundingPBDSM()

Move to the previous business day when a date falls on a holiday, unless the adjusted date would be in the previous month, then go the next business date instead.

Examples

julia> cal_mgr = SimpleCalendarManager()
julia> setcalendar!(cal_mgr, "WEEKEND", WeekendCalendar())
julia> apply(RDates.CalendarAdj("WEEKEND", rd"0d", RDates.HolidayRoundingPBDSM()), Date(2021,7,10), cal_mgr)
2021-07-09
julia> apply(rd"0d@WEEKEND[PBDSM]", Date(2021,7,10), cal_mgr)
2021-07-09
julia> apply(rd"0d@WEEKEND[NBDSM]", Date(2021,8,1), cal_mgr)
2021-08-02
source
RDates.HolidayRoundingNRType
HolidayRoundingNR()

No rounding, so just give back the same date even though it's a holiday. Uses "NR" for short hand.

Examples

julia> cal_mgr = SimpleCalendarManager()
julia> setcalendar!(cal_mgr, "WEEKEND", WeekendCalendar())
julia> apply(RDates.CalendarAdj("WEEKEND", rd"0d", RDates.HolidayRoundingNR()), Date(2021,7,10), cal_mgr)
2021-07-10
julia> apply(rd"0d@WEEKEND[NR]", Date(2021,7,10), cal_mgr)
2021-07-10
source
RDates.HolidayRoundingNearestType
HolidayRoundingNearest()

Move to the nearest business day, with the next one taking precedence in a tie. This is commonly used for U.S. calendars which will adjust Sat to Fri and Sun to Mon for their fixed date holidays.

Examples

julia> cal_mgr = SimpleCalendarManager()
julia> setcalendar!(cal_mgr, "WEEKEND", WeekendCalendar())
julia> apply(RDates.CalendarAdj("WEEKEND", rd"0d", RDates.HolidayRoundingNearest()), Date(2021,7,10), cal_mgr)
2021-07-09
julia> apply(rd"0d@WEEKEND[NEAR]", Date(2021,7,10), cal_mgr)
2021-07-09
julia> apply(rd"0d@WEEKEND[NEAR]", Date(2021,7,11), cal_mgr)
2021-07-12
source

Now we have everything we need to define calendar adjustments and business days

RDates.CalendarAdjType
CalendarAdj(calendars, rdate::RDate, rounding::HolidayRoundingConvention)

Apply a calendar adjustment to an underlying rdate, applying an appropriate convention if our final date falls on a holiday.

The calendars can only use alphanumeric characters, plus /, - and .

In the string-form, you can apply a calendar adjustment using the @ character and provide | separated calendar names to apply it on. The convention is finally provided in square brackets using its string-form name.

Examples

julia> cal_mgr = SimpleCalendarManager()
julia> setcalendar!(cal_mgr, "WEEKEND", WeekendCalendar())
julia> apply(rd"1d", Date(2019,9,27), cal_mgr)
2019-09-28
julia> apply(rd"1d@WEEKEND[NBD]", Date(2019,9,27), cal_mgr)
2019-09-30
julia> apply(rd"2m - 1d", Date(2019,7,23), cal_mgr)
2019-09-22
julia> apply(rd"(2m - 1d)@WEEKEND[PBD]", Date(2019,7,23), cal_mgr)
2019-09-20
source
RDates.BizDaysType
BizDays(days::Int64, calendars)
BizDays(days::BizDayZero)

It can be handy to work in business days at times, rather than calendar days. This allows us to move forwards or backwards n days.

julia> cal_mgr = SimpleCalendarManager()
julia> setcalendar!(cal_mgr, "WEEKEND", WeekendCalendar())
julia> apply(RDates.BizDays(1, "WEEKEND"), Date(2021,7,9), cal_mgr)
2021-07-12
julia> apply(rd"1b@WEEKEND", Date(2021,7,9), cal_mgr)
2021-07-12
julia> apply(RDates.BizDays(-10, "WEEKEND"), Date(2021,7,9), cal_mgr)
2021-06-25

If the date falls on a holiday, then it is first moved forward (or backwards) to a valid business day.

julia> apply(RDates.BizDays(1, "WEEKEND"), Date(2021,7,10), cal_mgr)
2021-07-13

For zero business days, we could either want to move forwards or backwards. As such we provide BizDayZero which can be used to provide each. By default, 0b will move forward

julia> apply(RDates.BizDays(RDates.BizDayZero(:next), "WEEKEND"), Date(2021,7,10), cal_mgr)
2021-07-12
julia> apply(RDates.BizDays(RDates.BizDayZero(:prev), "WEEKEND"), Date(2021,7,10), cal_mgr)
2021-07-09
julia> apply(rd"0b@WEEKEND", Date(2021,7,10), cal_mgr)
2021-07-12
julia> apply(rd"-0b@WEEKEND", Date(2021,7,10), cal_mgr)
2021-07-09
source