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.
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_holiday
— Functionis_holiday(calendar::Calendar, date::Dates.Date)::Bool
Determine whether the date requested is a holiday for this calendar or not.
RDates.holidays
— Functionholidays(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.
Calendars also come with a number of helpful wrapper methods
RDates.holidaycount
— Functionholidaycount(calendar::Calendar, from::Dates.Date, to::Dates.Date)::Int
Get the number of holidays in the calendar between two dates (inclusive)
RDates.bizdaycount
— Functionbizdaycount(calendar::Calendar, from::Dates.Date, to::Dates.Date)::Int
Get the number of business days in the calendar between two dates (inclusive)
RDate provides some primitive calendar implementations to get started with
RDates.NullCalendar
— TypeNullCalendar()
A holiday calendar for which there is never a holiday. sigh
RDates.WeekendCalendar
— TypeWeekendCalendar()
A calendar which will mark every Saturday and Sunday as a holiday
RDates.JointCalendar
— TypeJointCalendar(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.
RDates.CachedCalendar
— TypeCachedCalendar(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.
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.calendar
— Methodcalendar(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.
RDates provides some primitive calendar manager implementations to get started with
RDates.NullCalendarManager
— TypeNullCalendarManager()
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.
RDates.SimpleCalendarManager
— TypeSimpleCalendarManager()
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
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.apply
— Functionapply(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
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()
.
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.
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.HolidayRoundingNBD
— TypeHolidayRoundingNBD()
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
RDates.HolidayRoundingPBD
— TypeHolidayRoundingPBD()
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
RDates.HolidayRoundingNBDSM
— TypeHolidayRoundingNBDSM()
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
RDates.HolidayRoundingPBDSM
— TypeHolidayRoundingPBDSM()
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
RDates.HolidayRoundingNR
— TypeHolidayRoundingNR()
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
RDates.HolidayRoundingNearest
— TypeHolidayRoundingNearest()
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
Now we have everything we need to define calendar adjustments and business days
RDates.CalendarAdj
— TypeCalendarAdj(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
RDates.BizDays
— TypeBizDays(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