Skip to content

Business Calendars and Roll Conventions

  • The core functions, which every industry uses.

A business day answers “is this a working day here”. The answer depends on the weekend and on the holidays of one place, so every function in this family takes a BusinessCalendar that states both. This guide covers the calendar, the day counting and stepping functions, the roll conventions, and how to merge calendars when a date has to work in two places at once. For the first look at weekdays and business-day arithmetic, see Comparisons and Plain Arithmetic.

A BusinessCalendar is { weekend: number[], holidays: string[], timeZone: string }.

  • The weekend is explicit. It is a list of ISO weekday numbers, because Saturday and Sunday are not universal. Much of the Middle East is Friday and Saturday, and some markets keep a one-day weekend.
  • Holidays are yours to supply. The default entry point of the library bundles no holiday table. Holiday data belongs to a jurisdiction and changes every year, sometimes with days of notice.
  • timeZone records which locality the calendar describes. The business-day functions take and return local dates and never read it. If you hold an instant, reduce it to a local date with floorToZone first.

isBusinessDay, addBusinessDays and subtractBusinessDays all take a calendar as an optional trailing argument. The rest of the family requires one, because there is no default weekend to fall back on: businessDaysBetween, nextBusinessDay, previousBusinessDay, rollDate and mergeCalendars.

businessDaysBetween counts with the start exclusive and the end inclusive. nextBusinessDay and previousBusinessDay are strict: they never return the date you gave them.

import {
businessDaysBetween,
nextBusinessDay,
previousBusinessDay,
rollDate,
mergeCalendars,
isValidBusinessCalendar,
isBusinessDay,
} from "@northguild/gmt";
const nyse = {
weekend: [6, 7],
holidays: ["2024-05-31", "2024-07-04"],
timeZone: "America/New_York",
};
businessDaysBetween("2024-07-01", "2024-07-05", nyse);
// 3 — start exclusive, end inclusive, and 4 July is a holiday
nextBusinessDay("2024-07-03", nyse);
// "2024-07-05" — strictly after, skipping the holiday
previousBusinessDay("2024-07-05", nyse);
// "2024-07-03"

rollDate moves a date onto a working day by an explicit convention. The six conventions are following, modifiedFollowing, preceding, modifiedPreceding, endOfMonth and none.

following and preceding mean on-or-after and on-or-before, so they leave a working day alone. nextBusinessDay and previousBusinessDay are the strict neighbours.

rollDate("2024-05-31", "following", nyse);
// "2024-06-03" — forward past the weekend
rollDate("2024-05-31", "modifiedFollowing", nyse);
// "2024-05-30" — backward instead, because forward leaves May
rollDate("2024-03-15", "endOfMonth", nyse);
// "2024-03-29" — March's last working day; March ends on a Sunday
rollDate("2024-06-01", "none", nyse);
// "2024-06-01" — unadjusted, Saturday or not

mergeCalendars composes jurisdictions. Weekend rules and holidays union, so a date survives only if it is a working day in every input. That is the two-currency intersection that FX settlement needs, and the two-port one that an intermodal move needs.

The merged timeZone is the first calendar’s, because the merged set spans localities that may disagree. An empty list has no calendar to return, so mergeCalendars([]) is null.

const london = { weekend: [6, 7], holidays: ["2024-05-06"], timeZone: "Europe/London" };
const both = mergeCalendars([nyse, london]);
// { weekend: [6, 7], holidays: ["2024-05-06", "2024-05-31", "2024-07-04"], timeZone: "America/New_York" }
// timeZone is the first calendar's; the merged set spans localities that may disagree
isBusinessDay("2024-05-06", both);
// false — a UK holiday closes the merged calendar too
mergeCalendars([]);
// null

isValidBusinessCalendar and isValidRollConvention narrow a candidate. When a function returns its sentinel, they let you tell a misconfigured calendar or contract term apart from bad date input.