temporal-workdays 0.0.0-stage → 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 ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
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
+
27
+ ## 0.1.0
28
+
29
+ - Initial release: `isWorkday`, `isWeekend`, `isHoliday`, `addWorkdays`, `subtractWorkdays`, `nextWorkday`,
30
+ `previousWorkday`, `rollForward`, `rollBackward`, `workdaysBetween`, `eachWorkday`, `createWorkCalendar`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akash Gupta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,205 @@
1
- # Temporary Holding Version
1
+ # temporal-workdays
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Business days and business hours for the JavaScript [Temporal API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal).
4
+
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
+
11
+ ```sh
12
+ npm install temporal-workdays
13
+ ```
14
+
15
+ ## Workdays
16
+
17
+ ```ts
18
+ import { addWorkdays, isWorkday, workdaysBetween } from 'temporal-workdays';
19
+
20
+ const friday = Temporal.PlainDate.from('2026-11-06');
21
+
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
+ });
38
+
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'
42
+ ```
43
+
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
48
+
49
+ ```ts
50
+ import { createWorkCalendar } from 'temporal-workdays';
51
+
52
+ const support = createWorkCalendar({
53
+ hours: [
54
+ ['09:00', '12:00'],
55
+ ['13:00', '17:00'],
56
+ ],
57
+ });
58
+
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]'
62
+
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
66
+
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
+ ```
70
+
71
+ Hours per day of week, and special hours for specific dates:
72
+
73
+ ```ts
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
+ },
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']]
86
+ ```
87
+
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`
100
+
101
+ ```ts
102
+ import Holidays from 'date-holidays';
103
+ import { createWorkCalendar, fromDateHolidays } from 'temporal-workdays';
104
+
105
+ const bavaria = createWorkCalendar({
106
+ ...fromDateHolidays(new Holidays('DE', 'BY'), { types: ['public', 'bank'] }),
107
+ hours: [['09:00', '17:00']],
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']]
112
+ ```
113
+
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.
119
+
120
+ To combine it with your own holidays, wrap the predicate:
121
+
122
+ ```ts
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
132
+ ```
133
+
134
+ ## API
135
+
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. |
165
+
166
+ ### Options
167
+
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 |
175
+
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.
178
+
179
+ ## Requirements
180
+
181
+ A Temporal implementation: native in Firefox 139+, Chrome 144+ and Node.js 26+; elsewhere (Safari/iOS, older Node) use
182
+ a polyfill:
183
+
184
+ ```js
185
+ import 'temporal-polyfill/global'; // installs a global Temporal
186
+ // or, without touching globals:
187
+ import { Temporal } from 'temporal-polyfill';
188
+ ```
189
+
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+.
192
+
193
+ ## Migrating from moment-business-days / luxon-business-days
194
+
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 })` |
202
+
203
+ ## License
204
+
205
+ MIT