temporal-workdays 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -3
- package/README.md +144 -82
- package/dist/index.cjs +816 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +229 -28
- package/dist/index.d.ts +229 -28
- package/dist/index.js +809 -143
- package/dist/index.js.map +1 -1
- package/package.json +31 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Working hours**: `isWorkingTime`, `addWorkingTime`, `subtractWorkingTime`, `workingTimeBetween` (returns a
|
|
8
|
+
`Temporal.Duration`), `rollForwardToWorkingTime`, `rollBackwardToWorkingTime`, `getWorkingHours`.
|
|
9
|
+
Hours per day of week with breaks, a default, and `specialHours` per date (table or function).
|
|
10
|
+
`ZonedDateTime` measures exact elapsed time across DST changes.
|
|
11
|
+
- `createWorkCalendar({ hours })` returns a `WorkTimeCalendar` with the working-time methods.
|
|
12
|
+
- **`fromDateHolidays(holidays, { types })`**: holidays and partial-day closures from the `date-holidays` package,
|
|
13
|
+
independent of the machine's time zone.
|
|
14
|
+
- One options object can now be shared by every function (day-level functions accept `hours`/`specialHours`).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Invalid input is rejected with clearer errors: unknown option names, non-object options, a bare string where an
|
|
19
|
+
array is expected, 5-digit years (use the ISO `+YYYYYY` form), and unsafe integer amounts.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `workdaysBetween` returned `-0` instead of `0` for empty backward ranges.
|
|
24
|
+
- `WorkCalendar#subtractWorkdays` accepted numeric strings such as `'2'`.
|
|
25
|
+
- The README's `date-holidays` example gave wrong answers outside the US time zones; replaced by `fromDateHolidays`.
|
|
26
|
+
|
|
3
27
|
## 0.1.0
|
|
4
28
|
|
|
5
|
-
- Initial release: `isWorkday`, `isWeekend`, `isHoliday`, `addWorkdays`, `subtractWorkdays`,
|
|
6
|
-
`
|
|
7
|
-
`eachWorkday`, `createWorkCalendar`.
|
|
29
|
+
- Initial release: `isWorkday`, `isWeekend`, `isHoliday`, `addWorkdays`, `subtractWorkdays`, `nextWorkday`,
|
|
30
|
+
`previousWorkday`, `rollForward`, `rollBackward`, `workdaysBetween`, `eachWorkday`, `createWorkCalendar`.
|
package/README.md
CHANGED
|
@@ -1,142 +1,204 @@
|
|
|
1
1
|
# temporal-workdays
|
|
2
2
|
|
|
3
|
-
Business
|
|
4
|
-
Add and subtract workdays, count workdays between dates, and handle custom weekends, holidays and make-up working days.
|
|
3
|
+
Business days and business hours for the JavaScript [Temporal API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal).
|
|
5
4
|
|
|
6
|
-
- **
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
5
|
+
- **Workdays** — add/subtract workdays, count them, list them; custom weekends, holidays and make-up days.
|
|
6
|
+
- **Working hours** — "add 4 business hours", working time between two moments, breaks, half days, per-date hours. Exact across DST changes.
|
|
7
|
+
- **Holiday data** — plug in [`date-holidays`](https://www.npmjs.com/package/date-holidays) (200+ countries), time-zone safe.
|
|
8
|
+
- **Built for Temporal** — `PlainDate`, `PlainDateTime` and `ZonedDateTime` in, the same type out. Works with native Temporal, `temporal-polyfill` or `@js-temporal/polyfill`; no global `Temporal` needed, at runtime or in your types.
|
|
9
|
+
- **Small and strict** — zero dependencies, tree-shakeable (workdays alone ≈ 1.8 kB, everything ≈ 5.5 kB min+brotli), ESM + CommonJS, strict TypeScript types, clear errors instead of silently wrong dates.
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
npm install temporal-workdays
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Workdays
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import { addWorkdays,
|
|
18
|
+
import { addWorkdays, isWorkday, workdaysBetween } from 'temporal-workdays';
|
|
19
19
|
|
|
20
20
|
const friday = Temporal.PlainDate.from('2026-11-06');
|
|
21
21
|
|
|
22
|
-
addWorkdays(friday, 1).toString();
|
|
23
|
-
addWorkdays(friday, -5).toString();
|
|
24
|
-
isWorkday(Temporal.PlainDate.from('2026-11-07')); // false
|
|
22
|
+
addWorkdays(friday, 1).toString(); // → '2026-11-09'
|
|
23
|
+
addWorkdays(friday, -5).toString(); // → '2026-10-30'
|
|
24
|
+
isWorkday(Temporal.PlainDate.from('2026-11-07')); // → false
|
|
25
|
+
workdaysBetween(Temporal.PlainDate.from('2026-11-02'), Temporal.PlainDate.from('2026-11-09')); // → 5
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Holidays, other weekends and make-up working days:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createWorkCalendar } from 'temporal-workdays';
|
|
32
|
+
|
|
33
|
+
const calendar = createWorkCalendar({
|
|
34
|
+
weekend: [5, 6], // Friday + Saturday (ISO: 1 = Monday … 7 = Sunday)
|
|
35
|
+
holidays: ['2026-03-20', '12-25'], // one date, and every Dec 25
|
|
36
|
+
extraWorkdays: ['2026-03-28'], // a Saturday that is a working day
|
|
37
|
+
});
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
); // 5
|
|
39
|
+
calendar.isWorkday(Temporal.PlainDate.from('2026-03-20')); // → false
|
|
40
|
+
calendar.isWorkday(Temporal.PlainDate.from('2026-03-28')); // → true
|
|
41
|
+
calendar.nextWorkday(Temporal.PlainDate.from('2026-03-19')).toString(); // → '2026-03-22'
|
|
30
42
|
```
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
`createWorkCalendar` validates and parses the options once. Every function also takes the options as its last
|
|
45
|
+
argument, e.g. `addWorkdays(date, 3, { holidays })`.
|
|
46
|
+
|
|
47
|
+
## Working hours
|
|
33
48
|
|
|
34
49
|
```ts
|
|
35
50
|
import { createWorkCalendar } from 'temporal-workdays';
|
|
36
51
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'
|
|
41
|
-
'12-25', // every year on Dec 25
|
|
42
|
-
Temporal.PlainMonthDay.from('01-01'),
|
|
52
|
+
const support = createWorkCalendar({
|
|
53
|
+
hours: [
|
|
54
|
+
['09:00', '12:00'],
|
|
55
|
+
['13:00', '17:00'],
|
|
43
56
|
],
|
|
44
|
-
extraWorkdays: ['2026-03-28'], // a Saturday that is a working day
|
|
45
57
|
});
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
const ticket = Temporal.ZonedDateTime.from('2026-11-06T16:00[America/New_York]'); // Friday
|
|
60
|
+
const due = support.addWorkingTime(ticket, { hours: 4 }); // SLA: 4 business hours
|
|
61
|
+
due.toString(); // → '2026-11-09T12:00:00-05:00[America/New_York]'
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
const spent = support.workingTimeBetween(ticket, Temporal.ZonedDateTime.from('2026-11-09T14:30[America/New_York]'));
|
|
64
|
+
spent.toString(); // → 'PT5H30M'
|
|
65
|
+
spent.total('hours'); // → 5.5
|
|
53
66
|
|
|
54
|
-
|
|
67
|
+
support.isWorkingTime(Temporal.ZonedDateTime.from('2026-11-09T12:30[America/New_York]')); // → false
|
|
68
|
+
support.rollForwardToWorkingTime(Temporal.PlainDateTime.from('2026-11-07T10:00')).toString(); // → '2026-11-09T09:00:00'
|
|
69
|
+
```
|
|
55
70
|
|
|
56
|
-
|
|
71
|
+
Hours per day of week, and special hours for specific dates:
|
|
57
72
|
|
|
58
73
|
```ts
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
import { createWorkCalendar } from 'temporal-workdays';
|
|
75
|
+
|
|
76
|
+
const shop = createWorkCalendar({
|
|
77
|
+
hours: { default: [['09:00', '18:00']], 5: [['09:00', '14:00']] }, // Fridays until 14:00
|
|
78
|
+
specialHours: {
|
|
79
|
+
'2026-12-24': [['09:00', '12:00']], // half day
|
|
80
|
+
'2026-12-31': [], // closed
|
|
81
|
+
},
|
|
62
82
|
});
|
|
83
|
+
|
|
84
|
+
shop.getWorkingHours(Temporal.PlainDate.from('2026-12-04')); // → [['09:00', '14:00']]
|
|
85
|
+
shop.getWorkingHours(Temporal.PlainDate.from('2026-12-24')); // → [['09:00', '12:00']]
|
|
63
86
|
```
|
|
64
87
|
|
|
65
|
-
|
|
88
|
+
`specialHours` can also be a function `(date, regularHours) => hours | undefined`.
|
|
89
|
+
|
|
90
|
+
How the edges work:
|
|
91
|
+
|
|
92
|
+
- Periods are `[start, end)`: in `09:00–17:00`, 17:00 is not working time. Adding time that ends exactly at the end
|
|
93
|
+
of a period returns that end (`16:00 + 1h` → `17:00`), and subtracting to a start returns that start.
|
|
94
|
+
- Starting outside working hours, counting begins at the next working period.
|
|
95
|
+
- `ZonedDateTime` measures real elapsed time: a 09:00–17:00 day is still 8 hours, but `00:00–24:00` on a
|
|
96
|
+
spring-forward day is 23. Wall times skipped by DST move forward, like Temporal's `'compatible'`.
|
|
97
|
+
- Weekends and holidays have no working hours. Special hours override everything for their date.
|
|
98
|
+
|
|
99
|
+
## Holidays from `date-holidays`
|
|
66
100
|
|
|
67
101
|
```ts
|
|
68
102
|
import Holidays from 'date-holidays';
|
|
103
|
+
import { createWorkCalendar, fromDateHolidays } from 'temporal-workdays';
|
|
69
104
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
const bavaria = createWorkCalendar({
|
|
106
|
+
...fromDateHolidays(new Holidays('DE', 'BY'), { types: ['public', 'bank'] }),
|
|
107
|
+
hours: [['09:00', '17:00']],
|
|
73
108
|
});
|
|
109
|
+
|
|
110
|
+
bavaria.isWorkday(Temporal.PlainDate.from('2026-12-25')); // → false
|
|
111
|
+
bavaria.getWorkingHours(Temporal.PlainDate.from('2026-12-24')); // → [['09:00', '14:00']]
|
|
74
112
|
```
|
|
75
113
|
|
|
76
|
-
|
|
114
|
+
- `types` picks which holiday types close the business (default `['public']`).
|
|
115
|
+
- Days a holiday covers completely are holidays; partly covered days keep reduced hours (Christmas Eve from 14:00).
|
|
116
|
+
- Answers don't depend on the machine's time zone. (Calling `date-holidays`' own `isHoliday(new Date(…))` does:
|
|
117
|
+
run in India, it says US Thanksgiving is not a holiday.)
|
|
118
|
+
- `temporal-workdays` doesn't depend on `date-holidays`; install it yourself if you use this.
|
|
77
119
|
|
|
78
|
-
|
|
120
|
+
To combine it with your own holidays, wrap the predicate:
|
|
79
121
|
|
|
80
122
|
```ts
|
|
81
|
-
|
|
82
|
-
|
|
123
|
+
import Holidays from 'date-holidays';
|
|
124
|
+
import { fromDateHolidays, isWorkday } from 'temporal-workdays';
|
|
125
|
+
|
|
126
|
+
const us = fromDateHolidays(new Holidays('US'));
|
|
127
|
+
const companyDays = ['2026-12-24'];
|
|
128
|
+
const options = { ...us, holidays: (date) => us.holidays(date) || companyDays.includes(date.toString()) };
|
|
129
|
+
|
|
130
|
+
isWorkday(Temporal.PlainDate.from('2026-12-24'), options); // → false
|
|
131
|
+
isWorkday(Temporal.PlainDate.from('2026-11-26'), options); // → false
|
|
83
132
|
```
|
|
84
133
|
|
|
85
134
|
## API
|
|
86
135
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
136
|
+
`T` is the Temporal type you pass in, and is also what you get back.
|
|
137
|
+
|
|
138
|
+
### Workdays — accept `PlainDate`, `PlainDateTime` or `ZonedDateTime`
|
|
139
|
+
|
|
140
|
+
| Function | Returns |
|
|
141
|
+
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| `isWorkday(date)` / `isWeekend(date)` / `isHoliday(date)` | `boolean`. `extraWorkdays` always count as workdays. |
|
|
143
|
+
| `addWorkdays(date, n)` / `subtractWorkdays(date, n)` | `T`, `n` workdays away. `n = 0` returns `date`; from a non-workday, `+1` is the next workday. |
|
|
144
|
+
| `nextWorkday(date)` / `previousWorkday(date)` | `T`, the nearest workday strictly after / before. |
|
|
145
|
+
| `rollForward(date)` / `rollBackward(date)` | `T`: `date` if it is a workday, else the next / previous one. |
|
|
146
|
+
| `workdaysBetween(start, end)` | `number` of workdays in `(start, end]`; negative if `end < start`. `workdaysBetween(d, addWorkdays(d, n)) === n` |
|
|
147
|
+
| `eachWorkday(start, end)` | A generator of every workday in `[start, end]`, as `T`. |
|
|
148
|
+
|
|
149
|
+
### Working time — accept `PlainDateTime` or `ZonedDateTime`, need `hours`
|
|
150
|
+
|
|
151
|
+
| Function | Returns |
|
|
152
|
+
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
153
|
+
| `isWorkingTime(dateTime)` | `boolean` |
|
|
154
|
+
| `addWorkingTime(dateTime, duration)` / `subtractWorkingTime(dateTime, duration)` | `T`. `duration` is a `Temporal.Duration` or `{ hours, minutes, … }` (no days/weeks/months). |
|
|
155
|
+
| `workingTimeBetween(start, end)` | `Temporal.Duration` balanced up to hours (`PT27H30M`); negative if `end < start`. |
|
|
156
|
+
| `rollForwardToWorkingTime(dateTime)` / `rollBackwardToWorkingTime(dateTime)` | `T`: `dateTime` if it is working time, else the next period start / previous period end. |
|
|
157
|
+
| `getWorkingHours(date)` | The periods of that date, e.g. `[['09:00', '17:00']]`. |
|
|
158
|
+
|
|
159
|
+
### Calendars and adapters
|
|
160
|
+
|
|
161
|
+
| Function | Returns |
|
|
162
|
+
| --------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
163
|
+
| `createWorkCalendar(options)` | All of the above bound to `options`. Working-time methods are included when `hours` is given. |
|
|
164
|
+
| `fromDateHolidays(holidays, { types })` | `{ holidays, specialHours }` to spread into options. |
|
|
102
165
|
|
|
103
166
|
### Options
|
|
104
167
|
|
|
105
|
-
| Option
|
|
106
|
-
|
|
|
107
|
-
| `weekend`
|
|
108
|
-
| `holidays`
|
|
109
|
-
| `extraWorkdays` |
|
|
168
|
+
| Option | Type | Default |
|
|
169
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
|
|
170
|
+
| `weekend` | ISO days of week, `1` (Mon) … `7` (Sun) | `[6, 7]` |
|
|
171
|
+
| `holidays` | `'YYYY-MM-DD'`, `'MM-DD'` (yearly), `Temporal.PlainDate`, `Temporal.PlainMonthDay`, or `(date) => boolean` | none |
|
|
172
|
+
| `extraWorkdays` | `'YYYY-MM-DD'` or `Temporal.PlainDate`: workdays even on a weekend or holiday | none |
|
|
173
|
+
| `hours` | `[[start, end], …]` for every workday, or `{ 1: […], …, 7: […], default: […] }`. Times are `'HH:MM[:SS[.f]]'` or `Temporal.PlainTime`; `'24:00'` may end a period | required for working time |
|
|
174
|
+
| `specialHours` | `{ 'YYYY-MM-DD': [[start, end], …] }` (`[]` = closed), or `(date, regularHours) => hours \| undefined` | none |
|
|
110
175
|
|
|
111
|
-
Invalid input
|
|
112
|
-
|
|
113
|
-
the functions throw a `RangeError` instead of looping forever.
|
|
176
|
+
Invalid input throws a `RangeError` or `TypeError` that says what is wrong, including unknown option names.
|
|
177
|
+
A configuration without any workdays or working time throws instead of looping forever.
|
|
114
178
|
|
|
115
179
|
## Requirements
|
|
116
180
|
|
|
117
|
-
|
|
181
|
+
A Temporal implementation: native in Firefox 139+, Chrome 144+ and Node.js 26+; elsewhere (Safari/iOS, older Node) use
|
|
182
|
+
a polyfill:
|
|
118
183
|
|
|
119
|
-
|
|
120
|
-
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
import { Temporal } from 'temporal-polyfill';
|
|
125
|
-
```
|
|
184
|
+
```js
|
|
185
|
+
import 'temporal-polyfill/global'; // installs a global Temporal
|
|
186
|
+
// or, without touching globals:
|
|
187
|
+
import { Temporal } from 'temporal-polyfill';
|
|
188
|
+
```
|
|
126
189
|
|
|
127
|
-
|
|
190
|
+
Non-ISO calendars (`hebrew`, `japanese`, …) work; weekends and holidays are matched on the ISO date and results keep
|
|
191
|
+
the input's calendar. Node.js 18+.
|
|
128
192
|
|
|
129
193
|
## Migrating from moment-business-days / luxon-business-days
|
|
130
194
|
|
|
131
|
-
| moment-business-days
|
|
132
|
-
|
|
|
133
|
-
| `moment(d).businessAdd(3)` | `addWorkdays(d, 3)` |
|
|
134
|
-
| `moment(d).
|
|
135
|
-
| `moment(d).
|
|
136
|
-
| `moment(
|
|
137
|
-
| `moment
|
|
138
|
-
| `moment(a).businessDiff(moment(b))` | `workdaysBetween(b, a)` (check the endpoint convention for your case) |
|
|
139
|
-
| `moment.updateLocale('us', { holidays, holidayFormat, workingWeekdays })` | `createWorkCalendar({ holidays, weekend })` |
|
|
195
|
+
| moment-business-days | temporal-workdays |
|
|
196
|
+
| ---------------------------------------------------------- | ------------------------------------------------------- |
|
|
197
|
+
| `moment(d).businessAdd(3)` / `.businessSubtract(3)` | `addWorkdays(d, 3)` / `subtractWorkdays(d, 3)` |
|
|
198
|
+
| `moment(d).isBusinessDay()` | `isWorkday(d)` |
|
|
199
|
+
| `moment(d).nextBusinessDay()` / `.prevBusinessDay()` | `nextWorkday(d)` / `previousWorkday(d)` |
|
|
200
|
+
| `moment(a).businessDiff(moment(b))` | `workdaysBetween(b, a)` (check the endpoint convention) |
|
|
201
|
+
| `moment.updateLocale('us', { holidays, workingWeekdays })` | `createWorkCalendar({ holidays, weekend })` |
|
|
140
202
|
|
|
141
203
|
## License
|
|
142
204
|
|