Skip to content

Working hours ​

ts
import { createWorkCalendar } from 'temporal-workdays';

const support = createWorkCalendar({
  hours: [
    ['09:00', '12:00'],
    ['13:00', '17:00'],
  ],
});

const ticket = Temporal.ZonedDateTime.from('2026-11-06T16:00[America/New_York]'); // a Friday
const due = support.addWorkingTime(ticket, { hours: 4 }); // a 4-hour SLA
due.toString(); // → '2026-11-09T12:00:00-05:00[America/New_York]'

const spent = support.workingTimeBetween(ticket, Temporal.ZonedDateTime.from('2026-11-09T14:30[America/New_York]'));
spent.toString(); // → 'PT5H30M'
spent.total('hours'); // → 5.5

support.isWorkingTime(Temporal.ZonedDateTime.from('2026-11-09T12:30[America/New_York]')); // → false
support.rollForwardToWorkingTime(Temporal.PlainDateTime.from('2026-11-07T10:00')).toString(); // → '2026-11-09T09:00:00'

Hours can differ by day of the week, and special hours replace them on given dates:

ts
import { createWorkCalendar } from 'temporal-workdays';

const shop = createWorkCalendar({
  hours: { default: [['09:00', '18:00']], 5: [['09:00', '14:00']] }, // Fridays until 14:00
  specialHours: {
    '2026-12-24': [['09:00', '12:00']], // half day
    '2026-12-31': [], // closed
  },
});

shop.getWorkingHours(Temporal.PlainDate.from('2026-12-04')); // → [['09:00', '14:00']]
shop.getWorkingHours(Temporal.PlainDate.from('2026-12-24')); // → [['09:00', '12:00']]

specialHours can also be a function, (date, regularHours) => hours | undefined.

Overnight shifts ​

A range whose end is before its start runs into the next morning. The shift belongs to the day it starts, so a Friday night shift is worked even though Saturday is a weekend day.

ts
import { createWorkCalendar } from 'temporal-workdays';

const nights = createWorkCalendar({ hours: [['22:00', '06:00']] });

// Saturday 03:00 is inside Friday's shift.
nights.isWorkingTime(Temporal.PlainDateTime.from('2026-11-07T03:00')); // → true
// 7 hours finish Friday's shift, and the next shift starts on Monday night.
nights.addWorkingTime(Temporal.PlainDateTime.from('2026-11-06T23:00'), { hours: 8 }).toString(); // → '2026-11-09T23:00:00'
nights.getWorkingHours(Temporal.PlainDate.from('2026-11-06')); // → [['22:00', '06:00']]

Pausing the clock ​

Support SLAs usually stop while a ticket waits on the customer. Pass those periods to workingTimeBetween as pauses, then add the time that is left to get the deadline.

ts
import { createWorkCalendar } from 'temporal-workdays';

const support = createWorkCalendar({ hours: [['09:00', '17:00']] });

const opened = Temporal.PlainDateTime.from('2026-11-09T15:00'); // a Monday
const now = Temporal.PlainDateTime.from('2026-11-10T15:00');
const waitingOnCustomer = [
  [Temporal.PlainDateTime.from('2026-11-10T09:00'), Temporal.PlainDateTime.from('2026-11-10T13:00')],
];

const used = support.workingTimeBetween(opened, now, { pauses: waitingOnCustomer });
used.toString(); // → 'PT4H'

const deadline = support.addWorkingTime(now, Temporal.Duration.from({ hours: 8 }).subtract(used));
deadline.toString(); // → '2026-11-11T11:00:00'

Pauses can overlap each other and the ends of the range. Each working minute is left out at most once.

Edge cases ​

  • Periods include their start and exclude their end. In 09:00 to 17:00, 17:00 is not working time. Adding time that ends exactly at the end of a period returns that end (16:00 plus 1 hour is 17:00), and subtracting to the start of a period returns that start.
  • Starting outside working hours, counting begins at the next working period.
  • With ZonedDateTime, time is measured as it really passes. A 09:00 to 17:00 day is 8 hours, but 00:00 to 24:00 on the day clocks go forward is 23. Times that daylight saving skips move forward, as with Temporal's 'compatible' option.
  • Weekends and holidays have no working hours. Special hours replace everything else for their date.

Released under the MIT License.