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)::BoolDetermine 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)::IntGet the number of holidays in the calendar between two dates (inclusive)
RDates.bizdaycount — Functionbizdaycount(calendar::Calendar, from::Dates.Date, to::Dates.Date)::IntGet 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}) <: CalendarA 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)::CalendarGiven 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))
trueWhen 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.DateThe 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-12apply(rdate::RDate, date::Dates.Date)::Dates.DateThe 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.DateApply 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-12RDates.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-09RDates.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-30RDates.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-02RDates.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-10RDates.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-12Now 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-20RDates.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-25If 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-13For 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