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

src/core/calendar.ts
/** A calendar that skips weekends and holidays. */
export function createWorkingCalendar(
  options: WorkingCalendarOptions = {}
): WorkingCalendar
ParameterTypeRequiredMeaning
optionsWorkingCalendarOptionsnoDefaults 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

src/core/calendar.ts
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[];
}
OptionTypeDefaultMeaning
workingWeekdaysnumber[][1, 2, 3, 4, 5] (Mon to Fri)UTC day numbers, 0 = Sunday. A day not in the array is non-working
holidaysstring[][]UTC YYYY-MM-DD strings, matched by exact string equality against date.format('YYYY-MM-DD')

CALENDAR_DAYS

src/core/calendar.ts
/** The default calendar: every day counts, so arithmetic through it is plain calendar arithmetic. */
export const CALENDAR_DAYS: WorkingCalendar

CALENDAR_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

src/core/calendar.ts
/** 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

MemberSignatureCALENDAR_DAYSWorking calendar
skipsNonWorkingDaysreadonly booleanfalsetrue
isWorkingDay(date: Dayjs) => booleanAlways truetrue when the day passes the weekday and holiday check
addDays(date: Dayjs, days: number) => Dayjsdate.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) => numberto.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) => DayjsReturns date unchangedThe 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(...).

CallResult
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.000Z

Nothing 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.