@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 +21 -0
- package/README.md +314 -0
- package/dist/index.d.ts +420 -0
- package/dist/index.global.d.ts +420 -0
- package/dist/index.global.js +1009 -0
- package/dist/index.js +966 -0
- package/package.json +70 -0
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
|