@smcv/opening-hours 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Yuri Sementsov
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 ADDED
@@ -0,0 +1,314 @@
1
+ # Opening Hours
2
+
3
+ A TypeScript library for managing and querying business opening hours, heavily inspired by [Spatie's Opening Hours PHP library](https://github.com/spatie/opening-hours).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @smcv/opening-hours
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Usage
14
+
15
+ ```typescript
16
+ import { OpeningHours } from '@smcv/opening-hours';
17
+
18
+ const openingHours = OpeningHours.create({
19
+ monday: ['09:00-12:00', '13:00-18:00'],
20
+ tuesday: ['09:00-12:00', '13:00-18:00'],
21
+ wednesday: ['09:00-12:00'],
22
+ thursday: ['09:00-12:00', '13:00-18:00'],
23
+ friday: ['09:00-12:00', '13:00-20:00'],
24
+ saturday: ['09:00-12:00', '13:00-16:00'],
25
+ sunday: [],
26
+ exceptions: {
27
+ '2025-12-25': [], // Closed on Christmas
28
+ '2025-11-11': ['09:00-12:00'], // Different hours on this day
29
+ },
30
+ });
31
+
32
+ // Check if open on a specific day
33
+ console.log(openingHours.isOpenOn('monday')); // true
34
+ console.log(openingHours.isOpenOn('sunday')); // false
35
+
36
+ // Check if open at a specific date and time
37
+ const dateTime = new Date('2025-11-05 15:00:00');
38
+ console.log(openingHours.isOpenAt(dateTime)); // false (Wednesday afternoon)
39
+
40
+ // Check if open on Christmas
41
+ const christmas = new Date('2025-12-25 10:00:00');
42
+ console.log(openingHours.isOpenAt(christmas)); // false (exception)
43
+ ```
44
+
45
+ ### Get Opening Hours for a Day
46
+
47
+ ```typescript
48
+ const monday = openingHours.forDay('monday');
49
+ console.log(monday.toString()); // '09:00-12:00, 13:00-18:00'
50
+
51
+ // Get all time ranges as objects
52
+ const timeRanges = monday.getTimeRanges();
53
+ timeRanges.forEach(range => {
54
+ console.log(`Open from ${range.getStart()} to ${range.getEnd()}`);
55
+ });
56
+ ```
57
+
58
+ ### Get Opening Hours for a Specific Date
59
+
60
+ `forDate()` respects exceptions, unlike `forDay()` which only looks at the weekly schedule:
61
+
62
+ ```typescript
63
+ const christmas = openingHours.forDate(new Date('2025-12-25'));
64
+ console.log(christmas.toString()); // 'Closed'
65
+
66
+ const normalMonday = openingHours.forDate(new Date('2025-11-03'));
67
+ console.log(normalMonday.toString()); // '09:00-12:00, 13:00-18:00'
68
+ ```
69
+
70
+ ### Get Opening Hours for the Week
71
+
72
+ ```typescript
73
+ const week = openingHours.forWeek();
74
+ Object.entries(week).forEach(([day, dayHours]) => {
75
+ console.log(`${day}: ${dayHours.toString()}`);
76
+ });
77
+ ```
78
+
79
+ ### Array and String Representation
80
+
81
+ ```typescript
82
+ // Get opening hours as an array
83
+ const hoursArray = openingHours.toArray();
84
+ console.log(hoursArray);
85
+ // Output: [['Mon...Fri', '9AM-5PM'], ['Sat', '9AM-1PM', '2PM-4PM'], ['Sun', 'Closed']]
86
+
87
+ // Get opening hours as a formatted string
88
+ const hoursString = openingHours.toString();
89
+ console.log(hoursString);
90
+ // Output:
91
+ // Mon...Fri 9AM-5PM
92
+ // Sat 9AM-1PM 2PM-4PM
93
+ // Sun Closed
94
+ ```
95
+
96
+ Days with identical schedules are grouped (`Mon...Fri`). Each row begins with the day(s) followed by time slots, or `Closed` for days with no hours.
97
+
98
+ #### Display Options
99
+
100
+ Both `toArray()` and `toString()` accept a `DisplayOptions` object:
101
+
102
+ | Option | Type | Default | Description |
103
+ |--------|------|---------|-------------|
104
+ | `locale` | `string` | `'en-US'` | BCP 47 locale for day names |
105
+ | `weekday` | `'narrow'` \| `'short'` \| `'long'` | `'short'` | Day name format |
106
+ | `firstDayOfWeek` | `'monday'` \| `'sunday'` | `'monday'` | First day of the week |
107
+ | `dayRangeSeparator` | `string` | `'...'` | Separator between grouped days |
108
+ | `timeRangeSeparator` | `string` | `'-'` | Separator within a time range |
109
+ | `timeRangesSeparator` | `string` | `', '` | Separator between multiple time ranges |
110
+ | `timeFormat` | `string` | auto | Format string (`H:i`, `gA`, etc.) |
111
+ | `closedText` | `string` | `'Closed'` | Text shown for closed days |
112
+
113
+ #### Internationalization
114
+
115
+ ```typescript
116
+ // French day names, full format
117
+ const frenchHours = openingHours.toString({ locale: 'fr', weekday: 'long' });
118
+ // lundi...vendredi 09:00-17:00
119
+ // samedi 09:00-13:00 14:00-18:00
120
+ // dimanche Fermé
121
+
122
+ // Spanish, abbreviated (default)
123
+ const spanishHours = openingHours.toString({ locale: 'es' });
124
+ // lun...vie 09:00-17:00
125
+ // sáb 09:00-13:00 14:00-18:00
126
+ // dom Cerrado
127
+
128
+ // Narrow (single-letter)
129
+ const narrow = openingHours.toString({ weekday: 'narrow' });
130
+ ```
131
+
132
+ #### First Day of Week
133
+
134
+ ```typescript
135
+ // Sunday first (US convention)
136
+ const sundayFirst = openingHours.toString({ firstDayOfWeek: 'sunday' });
137
+ // Output:
138
+ // Sun Closed
139
+ // Mon...Fri 9AM-5PM
140
+ // Sat 9AM-1PM 2PM-4PM
141
+ ```
142
+
143
+ ### Exceptions
144
+
145
+ ```typescript
146
+ // Get all exceptions
147
+ const exceptions = openingHours.getExceptions();
148
+ Object.entries(exceptions).forEach(([date, hours]) => {
149
+ console.log(`${date}: ${hours.toString()}`);
150
+ });
151
+ ```
152
+
153
+ ### Current Open Range
154
+
155
+ ```typescript
156
+ const now = new Date();
157
+ const range = openingHours.currentOpenRange(now);
158
+
159
+ if (range) {
160
+ console.log(`Open since ${openingHours.currentOpenRangeStart(now)?.toLocaleTimeString()}`);
161
+ console.log(`Closes at ${openingHours.currentOpenRangeEnd(now)?.toLocaleTimeString()}`);
162
+ } else {
163
+ console.log('Currently closed');
164
+ }
165
+ ```
166
+
167
+ ### Next and Previous Ranges
168
+
169
+ ```typescript
170
+ const now = new Date();
171
+
172
+ // Next opening window
173
+ const nextStart = openingHours.nextOpenRangeStart(now);
174
+ const nextEnd = openingHours.nextOpenRangeEnd(now);
175
+ console.log(`Next open: ${nextStart?.toLocaleString()} – ${nextEnd?.toLocaleString()}`);
176
+
177
+ // Next moment the business opens / closes
178
+ const nextOpen = openingHours.nextOpen(now);
179
+ const nextClose = openingHours.nextClose(now);
180
+
181
+ // Previous opening window
182
+ const prevStart = openingHours.previousOpenRangeStart(now);
183
+ const prevEnd = openingHours.previousOpenRangeEnd(now);
184
+ ```
185
+
186
+ ### Time Differences
187
+
188
+ ```typescript
189
+ const start = new Date('2025-01-01 08:00:00');
190
+ const end = new Date('2025-01-05 18:00:00');
191
+
192
+ console.log(openingHours.diffInOpenHours(start, end));
193
+ console.log(openingHours.diffInOpenMinutes(start, end));
194
+ console.log(openingHours.diffInOpenSeconds(start, end));
195
+
196
+ console.log(openingHours.diffInClosedHours(start, end));
197
+ console.log(openingHours.diffInClosedMinutes(start, end));
198
+ console.log(openingHours.diffInClosedSeconds(start, end));
199
+ ```
200
+
201
+ ### Day Ranges
202
+
203
+ ```typescript
204
+ const openingHours = OpeningHours.create({
205
+ 'monday...friday': ['09:00-17:00'],
206
+ saturday: ['09:00-13:00'],
207
+ sunday: [],
208
+ });
209
+ ```
210
+
211
+ ### Overnight Ranges
212
+
213
+ ```typescript
214
+ const nightClub = OpeningHours.create({
215
+ friday: ['22:00-04:00'],
216
+ saturday: ['22:00-04:00'],
217
+ overflow: true, // required for overnight ranges
218
+ });
219
+
220
+ const saturdayNight = new Date('2025-01-04 02:00:00'); // 2 AM Saturday
221
+ console.log(nightClub.isOpenAt(saturdayNight)); // true
222
+ ```
223
+
224
+ ### Schema.org Integration
225
+
226
+ Create from schema.org structured data:
227
+
228
+ ```typescript
229
+ const hours = OpeningHours.createFromStructuredData([
230
+ {
231
+ '@type': 'OpeningHoursSpecification',
232
+ opens: '09:00:00Z',
233
+ closes: '17:00:00Z',
234
+ dayOfWeek: [
235
+ 'https://schema.org/Monday',
236
+ 'https://schema.org/Friday',
237
+ ],
238
+ },
239
+ ]);
240
+ ```
241
+
242
+ Export to schema.org format:
243
+
244
+ ```typescript
245
+ const structured = openingHours.asStructuredData();
246
+ console.log(JSON.stringify(structured, null, 2));
247
+
248
+ // With timezone offset in the output
249
+ const structuredWithTz = openingHours.asStructuredData('America/New_York');
250
+ ```
251
+
252
+ ## Advanced Usage
253
+
254
+ ### Custom Data in Time Ranges
255
+
256
+ Associate arbitrary data with each time range via generics:
257
+
258
+ ```typescript
259
+ type ShiftData = { shift: string };
260
+
261
+ const openingHours = OpeningHours.create<ShiftData>({
262
+ monday: [
263
+ { hours: '09:00-12:00', shift: 'morning' },
264
+ { hours: '13:00-18:00', shift: 'afternoon' },
265
+ ],
266
+ });
267
+
268
+ const ranges = openingHours.forDay('monday').getTimeRanges();
269
+ console.log(ranges[0]?.getData()?.shift); // 'morning'
270
+ ```
271
+
272
+ ### Dynamic Exceptions
273
+
274
+ Use a function to compute exceptions at runtime:
275
+
276
+ ```typescript
277
+ const openingHours = OpeningHours.create({
278
+ monday: ['09:00-17:00'],
279
+ exceptions: {
280
+ // First Monday of each month has different hours
281
+ firstMondayFunc: (date: Date) => {
282
+ if (date.getDay() === 1 && date.getDate() <= 7) {
283
+ return ['10:00-15:00'];
284
+ }
285
+ return [];
286
+ },
287
+ },
288
+ });
289
+ ```
290
+
291
+ ### Timezone Support
292
+
293
+ Specify the organization's local timezone so that `isOpenAt` and related methods evaluate dates in that timezone regardless of the caller's local time:
294
+
295
+ ```typescript
296
+ const openingHours = OpeningHours.create({
297
+ monday: ['09:00-17:00'],
298
+ timezone: 'America/New_York',
299
+ });
300
+
301
+ const dateTime = new Date('2025-01-15T12:30:00Z'); // UTC input
302
+ console.log(openingHours.isOpenAt(dateTime)); // evaluated in America/New_York
303
+ ```
304
+
305
+ Valid values are IANA timezone identifiers: `America/New_York`, `Europe/London`, `Asia/Tokyo`, `Australia/Sydney`, etc.
306
+
307
+ ## Requirements
308
+
309
+ - Node.js 20.19+
310
+ - TypeScript 4.5+ (optional peer dependency)
311
+
312
+ ## License
313
+
314
+ MIT