Working calendar helpers
The exported calendar API: `createWorkingCalendar`, `CALENDAR_DAYS`, `WorkingCalendar`
A WorkingCalendar is the day unit date arithmetic counts in. The default, CALENDAR_DAYS,
counts all seven days, so arithmetic through it is plain calendar arithmetic.
Working calendar describes what the workingCalendar prop changes in
the chart.
import {
CALENDAR_DAYS,
createWorkingCalendar,
type WorkingCalendar,
type WorkingCalendarOptions,
} from '@jaeungkim/gantt-chart';These methods take and return Dayjs values from dayjs, a runtime dependency of the package.
@jaeungkim/gantt-chart does not re-export dayjs, so pass instances from dayjs itself, in
UTC mode.
createWorkingCalendar
/** A calendar that skips weekends and holidays. */
export function createWorkingCalendar(
options: WorkingCalendarOptions = {}
): WorkingCalendar| Parameter | Type | Required | Meaning |
|---|---|---|---|
options | WorkingCalendarOptions | no | Defaults to {}, which is a Monday to Friday week with no holidays |
createWorkingCalendar takes the same non-working day definition the chart shades with. The
chart's holidays prop also accepts objects and date ranges, and expands them into the plain
YYYY-MM-DD strings this function takes.
The returned calendar always has skipsNonWorkingDays: true, even when the configuration leaves
every day working.
WorkingCalendarOptions
export interface WorkingCalendarOptions {
/** Working weekdays as UTC day numbers, 0 = Sunday (default Mon-Fri) */
workingWeekdays?: number[];
/** Non-working dates as UTC `YYYY-MM-DD` strings */
holidays?: string[];
}| Option | Type | Default | Meaning |
|---|---|---|---|
workingWeekdays | number[] | [1, 2, 3, 4, 5] (Mon to Fri) | UTC day numbers, 0 = Sunday. A day not in the array is non-working |
holidays | string[] | [] | UTC YYYY-MM-DD strings, matched by exact string equality against date.format('YYYY-MM-DD') |
CALENDAR_DAYS
/** The default calendar: every day counts, so arithmetic through it is plain calendar arithmetic. */
export const CALENDAR_DAYS: WorkingCalendarCALENDAR_DAYS is one shared instance rather than a factory. The chart uses it while
workingCalendar is off. Pass it when your own code takes a calendar argument and the caller
wants plain calendar days.
WorkingCalendar
/** Day arithmetic that can skip non-working days. */
export interface WorkingCalendar {
/** False for the default calendar (every day counts) */
readonly skipsNonWorkingDays: boolean;
isWorkingDay(date: Dayjs): boolean;
/** Moves `days` days forward (or backward), skipping non-working days */
addDays(date: Dayjs, days: number): Dayjs;
/** Days from `from` to `to`, counted the same way `addDays` moves. Signed. */
daysBetween(from: Dayjs, to: Dayjs): number;
/** The date itself when it is a working day, otherwise the next one (time of day kept) */
snapForward(date: Dayjs): Dayjs;
}The core has one day unit, defined by this object. Turning the working calendar on
replaces the object and changes nothing else. skipsNonWorkingDays lets a caller take the plain
arithmetic path instead of testing the calendar day by day.
Members
| Member | Signature | CALENDAR_DAYS | Working calendar |
|---|---|---|---|
skipsNonWorkingDays | readonly boolean | false | true |
isWorkingDay | (date: Dayjs) => boolean | Always true | true when the day passes the weekday and holiday check |
addDays | (date: Dayjs, days: number) => Dayjs | date.add(days, 'day') | Steps one calendar day at a time and counts down only on working days. The time of day is kept |
daysBetween | (from: Dayjs, to: Dayjs) => number | to.startOf('day').diff(from.startOf('day'), 'day') | Walks day by day and counts only the working days it lands on. The starting day is never counted |
snapForward | (date: Dayjs) => Dayjs | Returns date unchanged | The next working day at or after date, time of day preserved |
daysBetween compares at day granularity. It calls startOf('day') on both ends, so two times
on the same date are 0 apart.
addDays and daysBetween are exact inverses from a working day, so
daysBetween(from, addDays(from, n)) is n. Moving backwards from a non-working day the round
trip loses a step, because daysBetween never counts the day it starts from. The identity also
needs a span inside the walk limit and a calendar with at least one working day. Both limits are
under Constraints below.
Worked values
In June 2025 the 2nd is a Monday, the 6th a Friday, the 7th and 8th the weekend, and the 9th the
next Monday. workweek is createWorkingCalendar(). withHoliday is
createWorkingCalendar({ holidays: ['2025-06-09'] }). Dates are written as strings for brevity.
Each one stands for dayjs.utc(...).
| Call | Result |
|---|---|
workweek.addDays('2025-06-06', 1) | 2025-06-09 |
workweek.addDays('2025-06-02', 10) | 2025-06-16 |
workweek.addDays('2025-06-09', -1) | 2025-06-06 |
workweek.addDays('2025-06-06T14:30', 1) | 2025-06-09T14:30 |
workweek.daysBetween('2025-06-02', '2025-06-09') | 5 |
workweek.daysBetween('2025-06-06', '2025-06-08') | 0 (Fri to Sun) |
workweek.daysBetween('2025-06-07', '2025-06-09') | 1 (from Sat) |
workweek.daysBetween('2025-06-09', '2025-06-02') | -5 |
workweek.snapForward('2025-06-07T09:00') | 2025-06-09T09:00 |
withHoliday.addDays('2025-06-06', 1) | 2025-06-10 |
withHoliday.daysBetween('2025-06-06', '2025-06-13') | 4 |
CALENDAR_DAYS.daysBetween('2025-06-02', '2025-06-09') | 7 |
Example
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { createWorkingCalendar } from '@jaeungkim/gantt-chart';
dayjs.extend(utc);
const calendar = createWorkingCalendar({ holidays: ['2025-06-09'] });
calendar.addDays(dayjs.utc('2025-06-06'), 1).toISOString(); // 2025-06-10T00:00:00.000ZNothing in the package takes a calendar argument. The chart builds its own from
workingCalendar, workingWeekdays and holidays, so a calendar you construct is for your own
date math. See Working calendar.
Constraints
src/core/calendar.ts imports only the Dayjs type, never dayjs itself. The weekday is
date.day() and a holiday is matched against date.format('YYYY-MM-DD'), so both read in
whatever mode the Dayjs instance you pass is in.
Inside the chart the mode is always UTC. Every date the core hands the calendar is built by
src/core/dates.ts, which uses dayjs.utc. A local-mode Dayjs reads a local weekday and a
local YYYY-MM-DD, so pass UTC-mode instances, as the example above does.
A calendar where nothing is a working day still returns. addDays has a budget of 367
steps per requested day and falls back to plain calendar days when it runs out. snapForward
returns its input unchanged after 366 attempts. An empty workingWeekdays produces wrong
numbers rather than an error.
daysBetween walks one day at a time only up to 18,263 days (about 50 years). Past that it
returns the plain calendar day difference, so on a working calendar the unit silently changes
for spans that long.
daysBetween returns 0, never -0, for a backwards span that contains no working day.
The calendar has no notion of hours. A task from 09:00 to 17:00 on one date measures
daysBetween of 0.
The skip limit (366) and the walk limit (18,263) are internal constants of
src/core/calendar.ts. @jaeungkim/gantt-chart exports neither, and neither can be configured.
The internal factory behind both, build, is not exported either.
WorkingCalendar is a plain interface. A host may implement it directly instead of calling
createWorkingCalendar.
A calendar changes dates only. Bar geometry and timeline ticks take no calendar argument, so a
bar still spans every weekend and holiday it covers. Shading is the separate
showNonWorkingDays prop, described in The timeline.
The calendar the workingCalendar prop selects is read during the bar drag, where snapForward
moves the dragged edge off a non-working day. See Working calendar.