@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.
@@ -0,0 +1,1009 @@
1
+ (() => {
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ function __accessProp(key) {
7
+ return this[key];
8
+ }
9
+ var __toCommonJS = (from) => {
10
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
11
+ if (entry)
12
+ return entry;
13
+ entry = __defProp({}, "__esModule", { value: true });
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (var key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(entry, key))
17
+ __defProp(entry, key, {
18
+ get: __accessProp.bind(from, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ __moduleCache.set(from, entry);
23
+ return entry;
24
+ };
25
+ var __moduleCache;
26
+ var __returnValue = (v) => v;
27
+ function __exportSetter(name, newValue) {
28
+ this[name] = __returnValue.bind(null, newValue);
29
+ }
30
+ var __export = (target, all) => {
31
+ for (var name in all)
32
+ __defProp(target, name, {
33
+ get: all[name],
34
+ enumerable: true,
35
+ configurable: true,
36
+ set: __exportSetter.bind(all, name)
37
+ });
38
+ };
39
+
40
+ // src/index.ts
41
+ var exports_src = {};
42
+ __export(exports_src, {
43
+ TimeRange: () => TimeRange,
44
+ Time: () => Time,
45
+ OpeningHoursForDay: () => OpeningHoursForDay,
46
+ OpeningHours: () => OpeningHours,
47
+ Days: () => Days
48
+ });
49
+
50
+ // src/types.ts
51
+ var Days = {
52
+ sunday: 0,
53
+ monday: 1,
54
+ tuesday: 2,
55
+ wednesday: 3,
56
+ thursday: 4,
57
+ friday: 5,
58
+ saturday: 6
59
+ };
60
+
61
+ // src/classes/Time.ts
62
+ class Time {
63
+ hour;
64
+ minute;
65
+ constructor(hour, minute = 0) {
66
+ this.hour = hour;
67
+ this.minute = minute;
68
+ }
69
+ static fromString(timeString) {
70
+ const isoMatch = timeString.match(/^(\d{2}):(\d{2})(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?$/);
71
+ if (isoMatch) {
72
+ const hour2 = Number.parseInt(isoMatch[1], 10);
73
+ const minute2 = Number.parseInt(isoMatch[2], 10);
74
+ return new Time(hour2, minute2);
75
+ }
76
+ const parts = timeString.split(":").map(Number);
77
+ const hour = parts[0] || 0;
78
+ const minute = parts[1] || 0;
79
+ return new Time(hour, minute);
80
+ }
81
+ static fromDate(date) {
82
+ return new Time(date.getHours(), date.getMinutes());
83
+ }
84
+ getHour() {
85
+ return this.hour;
86
+ }
87
+ getMinute() {
88
+ return this.minute;
89
+ }
90
+ format(format = "H:i") {
91
+ const replacements = {
92
+ H: this.hour.toString().padStart(2, "0"),
93
+ G: this.hour.toString(),
94
+ h: (this.hour % 12 || 12).toString().padStart(2, "0"),
95
+ g: (this.hour % 12 || 12).toString(),
96
+ i: this.minute.toString().padStart(2, "0"),
97
+ a: this.hour < 12 ? "am" : "pm",
98
+ A: this.hour < 12 ? "AM" : "PM"
99
+ };
100
+ return format.replace(/[HGhgiaA]/g, (match) => replacements[match] || match);
101
+ }
102
+ toString() {
103
+ return this.format();
104
+ }
105
+ isBefore(time) {
106
+ if (this.hour < time.hour) {
107
+ return true;
108
+ }
109
+ if (this.hour === time.hour && this.minute < time.minute) {
110
+ return true;
111
+ }
112
+ return false;
113
+ }
114
+ isAfter(time) {
115
+ if (this.hour > time.hour) {
116
+ return true;
117
+ }
118
+ if (this.hour === time.hour && this.minute > time.minute) {
119
+ return true;
120
+ }
121
+ return false;
122
+ }
123
+ isEqual(time) {
124
+ return this.hour === time.hour && this.minute === time.minute;
125
+ }
126
+ addMinutes(minutes) {
127
+ const date = new Date(0, 0, 0, this.hour, this.minute);
128
+ date.setMinutes(date.getMinutes() + minutes);
129
+ return Time.fromDate(date);
130
+ }
131
+ toMinutesSinceMidnight() {
132
+ return this.hour * 60 + this.minute;
133
+ }
134
+ diffInMinutes(time) {
135
+ return Math.abs(this.toMinutesSinceMidnight() - time.toMinutesSinceMidnight());
136
+ }
137
+ toDate(baseDate = new Date) {
138
+ const date = new Date(baseDate);
139
+ date.setHours(this.hour, this.minute, 0, 0);
140
+ return date;
141
+ }
142
+ }
143
+
144
+ // src/classes/OpeningHoursForDay.ts
145
+ class OpeningHoursForDay {
146
+ timeRanges;
147
+ overflow;
148
+ displayOptions;
149
+ data;
150
+ constructor(timeRanges = [], overflow = false, displayOptions, data) {
151
+ this.timeRanges = timeRanges;
152
+ this.overflow = overflow;
153
+ this.displayOptions = displayOptions;
154
+ this.data = data;
155
+ }
156
+ isOpenAt(time) {
157
+ return this.timeRanges.some((range) => range.containsTime(time, this.overflow));
158
+ }
159
+ isOpenAtDateTime(dateTime) {
160
+ const time = Time.fromDate(dateTime);
161
+ return this.isOpenAt(time);
162
+ }
163
+ isOpenDuringDay() {
164
+ return this.timeRanges.length > 0;
165
+ }
166
+ getTimeRanges() {
167
+ return this.timeRanges;
168
+ }
169
+ getData() {
170
+ return this.data;
171
+ }
172
+ getCurrentOpenRange(time) {
173
+ for (const range of this.timeRanges) {
174
+ if (range.containsTime(time, this.overflow)) {
175
+ return range;
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+ getCurrentOpenRangeAtDateTime(dateTime) {
181
+ const time = Time.fromDate(dateTime);
182
+ return this.getCurrentOpenRange(time);
183
+ }
184
+ equals(openingHoursForDay) {
185
+ if (this.timeRanges.length !== openingHoursForDay.timeRanges.length) {
186
+ return false;
187
+ }
188
+ for (let i = 0;i < this.timeRanges.length; i++) {
189
+ const thisRange = this.timeRanges[i];
190
+ const otherRange = openingHoursForDay.getTimeRanges()[i];
191
+ if (!thisRange || !otherRange) {
192
+ return false;
193
+ }
194
+ const thisStart = thisRange.getStart();
195
+ const otherStart = otherRange.getStart();
196
+ const thisEnd = thisRange.getEnd();
197
+ const otherEnd = otherRange.getEnd();
198
+ if (!thisStart.isEqual(otherStart) || !thisEnd.isEqual(otherEnd)) {
199
+ return false;
200
+ }
201
+ }
202
+ return true;
203
+ }
204
+ formatTimeRange(timeRange, options = {}) {
205
+ const {
206
+ timeRangeSeparator = "-",
207
+ timeFormat,
208
+ locale = "en-US"
209
+ } = {
210
+ ...this.displayOptions,
211
+ ...options
212
+ };
213
+ const defaultFormat = locale === "en-US" ? "gA" : "H:i";
214
+ const format = timeFormat || defaultFormat;
215
+ const startStr = timeRange.getStart().format(format);
216
+ const endStr = timeRange.getEnd().format(format);
217
+ let result = startStr + timeRangeSeparator + endStr;
218
+ const data = timeRange.getData();
219
+ if (data && typeof data.note === "string" && data.note.length > 0) {
220
+ result += ` (${data.note})`;
221
+ }
222
+ return result;
223
+ }
224
+ toString(options = {}) {
225
+ if (this.timeRanges.length === 0) {
226
+ const { closedText = "Closed" } = {
227
+ ...this.displayOptions,
228
+ ...options
229
+ };
230
+ const note = this.data?.note;
231
+ return (typeof note === "string" && note.length > 0 ? note : null) ?? closedText;
232
+ }
233
+ const mergedOptions = { ...this.displayOptions, ...options };
234
+ const { timeRangesSeparator = ", " } = mergedOptions;
235
+ return this.timeRanges.map((range) => this.formatTimeRange(range, mergedOptions)).join(timeRangesSeparator);
236
+ }
237
+ }
238
+
239
+ // src/classes/TimeRange.ts
240
+ class TimeRange {
241
+ start;
242
+ end;
243
+ data;
244
+ constructor(start, end, data) {
245
+ this.start = start;
246
+ this.end = end;
247
+ this.data = data;
248
+ }
249
+ static fromString(rangeString, data) {
250
+ const parts = rangeString.split("-").map((part) => part.trim());
251
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
252
+ throw new Error(`Invalid time range format: ${rangeString}`);
253
+ }
254
+ const [startStr, endStr] = parts;
255
+ const start = Time.fromString(startStr);
256
+ const end = Time.fromString(endStr);
257
+ return new TimeRange(start, end, data);
258
+ }
259
+ getStart() {
260
+ return this.start;
261
+ }
262
+ getEnd() {
263
+ return this.end;
264
+ }
265
+ getData() {
266
+ return this.data;
267
+ }
268
+ containsTime(time, overflow = false) {
269
+ if (overflow && this.spansOvernight()) {
270
+ return time.isAfter(this.start) || time.isBefore(this.end) || time.isEqual(this.start) || time.isEqual(this.end);
271
+ }
272
+ return (time.isAfter(this.start) || time.isEqual(this.start)) && (time.isBefore(this.end) || time.isEqual(this.end));
273
+ }
274
+ spansOvernight() {
275
+ return this.end.isBefore(this.start);
276
+ }
277
+ overlaps(timeRange, overflow = false) {
278
+ const thisSpansOvernight = this.spansOvernight();
279
+ const otherSpansOvernight = timeRange.spansOvernight();
280
+ if (thisSpansOvernight === otherSpansOvernight) {
281
+ if (overflow && thisSpansOvernight) {
282
+ return true;
283
+ }
284
+ return this.containsTime(timeRange.getStart(), overflow) || this.containsTime(timeRange.getEnd(), overflow) || timeRange.containsTime(this.start, overflow) || timeRange.containsTime(this.end, overflow);
285
+ }
286
+ if (thisSpansOvernight) {
287
+ return this.containsTime(timeRange.getStart(), overflow) || this.containsTime(timeRange.getEnd(), overflow);
288
+ }
289
+ return timeRange.containsTime(this.start, overflow) || timeRange.containsTime(this.end, overflow);
290
+ }
291
+ durationInMinutes(overflow = false) {
292
+ if (overflow && this.spansOvernight()) {
293
+ const minutesUntilMidnight = 24 * 60 - this.start.toMinutesSinceMidnight();
294
+ const minutesAfterMidnight = this.end.toMinutesSinceMidnight();
295
+ return minutesUntilMidnight + minutesAfterMidnight;
296
+ }
297
+ return this.end.toMinutesSinceMidnight() - this.start.toMinutesSinceMidnight();
298
+ }
299
+ toString() {
300
+ return `${this.start.toString()}-${this.end.toString()}`;
301
+ }
302
+ }
303
+
304
+ // src/classes/OpeningHours.ts
305
+ class OpeningHours {
306
+ config;
307
+ schedule;
308
+ exceptions;
309
+ filters;
310
+ timezone;
311
+ overflow;
312
+ displayOptions;
313
+ constructor(config) {
314
+ const {
315
+ locale,
316
+ weekday,
317
+ firstDayOfWeek,
318
+ dayRangeSeparator,
319
+ timeRangeSeparator,
320
+ timeFormat,
321
+ timeRangesSeparator,
322
+ showTimezone,
323
+ timeZoneName,
324
+ closedText,
325
+ ...configOptions
326
+ } = config;
327
+ this.displayOptions = {
328
+ locale,
329
+ weekday,
330
+ firstDayOfWeek,
331
+ dayRangeSeparator,
332
+ timeRangeSeparator,
333
+ timeFormat,
334
+ timeRangesSeparator,
335
+ showTimezone,
336
+ timeZoneName,
337
+ closedText
338
+ };
339
+ this.config = configOptions;
340
+ this.schedule = new Map;
341
+ this.exceptions = new Map(Object.entries(this.config.exceptions || {}));
342
+ this.filters = this.config.filters || [];
343
+ this.timezone = this.config.timezone;
344
+ this.overflow = this.config.overflow || false;
345
+ this.initializeSchedule();
346
+ }
347
+ initializeSchedule() {
348
+ for (const [day, schedule] of Object.entries(this.config)) {
349
+ if (day in Days) {
350
+ this.schedule.set(day, this.normalizeTimeRanges(schedule));
351
+ }
352
+ }
353
+ for (const [dayRange, schedule] of Object.entries(this.config)) {
354
+ if (dayRange.includes("...")) {
355
+ const [startDay, endDay] = dayRange.split("...");
356
+ if (typeof startDay === "string" && typeof endDay === "string" && startDay in Days && endDay in Days) {
357
+ const startIndex = Days[startDay];
358
+ const endIndex = Days[endDay];
359
+ for (let i = startIndex;i <= endIndex; i++) {
360
+ const currentDay = Object.keys(Days).find((key) => Days[key] === i);
361
+ if (currentDay && !this.schedule.has(currentDay)) {
362
+ this.schedule.set(currentDay, this.normalizeTimeRanges(schedule));
363
+ }
364
+ }
365
+ }
366
+ }
367
+ }
368
+ }
369
+ normalizeTimeRanges(schedule) {
370
+ if (!Array.isArray(schedule)) {
371
+ return [schedule];
372
+ }
373
+ return schedule;
374
+ }
375
+ isMetadataOnly(item) {
376
+ if (typeof item === "string")
377
+ return false;
378
+ return !item.hours && !item.time;
379
+ }
380
+ filterActualRanges(schedule) {
381
+ return schedule.filter((item) => !this.isMetadataOnly(item));
382
+ }
383
+ extractDataFromSchedule(schedule) {
384
+ for (const item of schedule) {
385
+ if (this.isMetadataOnly(item) && typeof item !== "string") {
386
+ const {
387
+ time: _t,
388
+ hours: _h,
389
+ ...rest
390
+ } = item;
391
+ return rest;
392
+ }
393
+ }
394
+ return;
395
+ }
396
+ getDataForDate(date) {
397
+ const dateString = this.formatDate(date);
398
+ const exception = this.exceptions.get(dateString);
399
+ if (Array.isArray(exception)) {
400
+ return this.extractDataFromSchedule(exception);
401
+ }
402
+ return;
403
+ }
404
+ convertToTimeRange(timeRangeData) {
405
+ if (typeof timeRangeData === "string") {
406
+ return TimeRange.fromString(timeRangeData);
407
+ }
408
+ const timeStr = timeRangeData.time || timeRangeData.hours || "";
409
+ const data = { ...timeRangeData };
410
+ data.time = undefined;
411
+ data.hours = undefined;
412
+ return TimeRange.fromString(timeStr, data);
413
+ }
414
+ getDayName(date) {
415
+ const dayIndex = date.getDay();
416
+ return Object.keys(Days).find((key) => Days[key] === dayIndex);
417
+ }
418
+ formatDate(date) {
419
+ return date.toISOString().split("T")[0];
420
+ }
421
+ getTimeRangesForDate(date) {
422
+ const dateString = this.formatDate(date);
423
+ if (this.exceptions.has(dateString)) {
424
+ const exception = this.exceptions.get(dateString);
425
+ if (typeof exception === "function") {
426
+ return exception(date).map((tr) => this.convertToTimeRange(tr));
427
+ }
428
+ if (Array.isArray(exception)) {
429
+ return this.filterActualRanges(exception).map((tr) => this.convertToTimeRange(tr));
430
+ }
431
+ return [];
432
+ }
433
+ const filterRanges = [];
434
+ for (const filter of this.filters) {
435
+ const ranges = filter(date);
436
+ filterRanges.push(...ranges.map((tr) => this.convertToTimeRange(tr)));
437
+ }
438
+ if (filterRanges.length > 0) {
439
+ return filterRanges;
440
+ }
441
+ const dayName = this.getDayName(date);
442
+ const daySchedule = this.schedule.get(dayName) || [];
443
+ return daySchedule.map((tr) => this.convertToTimeRange(tr));
444
+ }
445
+ static create(config) {
446
+ return new OpeningHours(config);
447
+ }
448
+ isOpenOn(day) {
449
+ const timeRanges = this.schedule.get(day) || [];
450
+ return timeRanges.length > 0;
451
+ }
452
+ isOpenAt(dateTime) {
453
+ const adjustedDateTime = this.toTimezone(dateTime);
454
+ return this.forDate(adjustedDateTime).isOpenAtDateTime(adjustedDateTime);
455
+ }
456
+ isClosedAt(dateTime) {
457
+ return !this.isOpenAt(dateTime);
458
+ }
459
+ forDay(day) {
460
+ const timeRanges = this.schedule.get(day) || [];
461
+ const ranges = timeRanges.map((tr) => this.convertToTimeRange(tr));
462
+ return new OpeningHoursForDay(ranges, this.overflow, this.displayOptions);
463
+ }
464
+ forWeek() {
465
+ const result = {};
466
+ for (const day in Days) {
467
+ result[day] = this.forDay(day);
468
+ }
469
+ return result;
470
+ }
471
+ forDate(date) {
472
+ const adjustedDate = this.toTimezone(date);
473
+ const timeRanges = this.getTimeRangesForDate(adjustedDate);
474
+ const data = this.getDataForDate(adjustedDate);
475
+ return new OpeningHoursForDay(timeRanges, this.overflow, this.displayOptions, data);
476
+ }
477
+ getExceptions() {
478
+ const result = {};
479
+ for (const [dateString, exception] of this.exceptions.entries()) {
480
+ const date = new Date(dateString);
481
+ if (typeof exception === "function") {
482
+ const ranges = exception(date).map((tr) => this.convertToTimeRange(tr));
483
+ result[dateString] = new OpeningHoursForDay(ranges, this.overflow, this.displayOptions);
484
+ } else if (Array.isArray(exception)) {
485
+ const ranges = this.filterActualRanges(exception).map((tr) => this.convertToTimeRange(tr));
486
+ const data = this.extractDataFromSchedule(exception);
487
+ result[dateString] = new OpeningHoursForDay(ranges, this.overflow, this.displayOptions, data);
488
+ }
489
+ }
490
+ return result;
491
+ }
492
+ diffInOpenHours(startDate, endDate) {
493
+ return this.diffInOpenMinutes(startDate, endDate) / 60;
494
+ }
495
+ diffInOpenMinutes(startDate, endDate) {
496
+ let start = this.toTimezone(startDate);
497
+ let end = this.toTimezone(endDate);
498
+ if (start > end) {
499
+ const temp = start;
500
+ start = end;
501
+ end = temp;
502
+ }
503
+ let openMinutes = 0;
504
+ const currentDate = new Date(start);
505
+ while (currentDate < end) {
506
+ const nextDay = new Date(currentDate);
507
+ nextDay.setDate(nextDay.getDate() + 1);
508
+ nextDay.setHours(0, 0, 0, 0);
509
+ const endOfSegment = new Date(Math.min(nextDay.getTime(), end.getTime()));
510
+ openMinutes += this.calculateOpenMinutesForTimespan(currentDate, endOfSegment);
511
+ currentDate.setTime(nextDay.getTime());
512
+ }
513
+ if (this.hasExceptionBetween(start, end)) {
514
+ const fullDayMinutes = this.forDate(end).getTimeRanges().reduce((sum, range) => sum + range.durationInMinutes(this.overflow), 0);
515
+ const startOfEnd = new Date(end);
516
+ startOfEnd.setHours(0, 0, 0, 0);
517
+ const partialEnd = this.calculateOpenMinutesForTimespan(startOfEnd, end);
518
+ openMinutes += fullDayMinutes - partialEnd;
519
+ }
520
+ return openMinutes;
521
+ }
522
+ calculateOpenMinutesForTimespan(start, end) {
523
+ const openingHoursForDay = this.forDate(start);
524
+ if (!openingHoursForDay.isOpenDuringDay()) {
525
+ return 0;
526
+ }
527
+ let openMinutes = 0;
528
+ const timeRanges = openingHoursForDay.getTimeRanges();
529
+ for (const range of timeRanges) {
530
+ const rangeStart = range.getStart().toDate(start);
531
+ const rangeEnd = range.getEnd().toDate(start);
532
+ if (range.spansOvernight() && this.overflow) {
533
+ rangeEnd.setDate(rangeEnd.getDate() + 1);
534
+ }
535
+ if (rangeEnd <= start || rangeStart >= end) {
536
+ continue;
537
+ }
538
+ const overlapStart = new Date(Math.max(rangeStart.getTime(), start.getTime()));
539
+ const overlapEnd = new Date(Math.min(rangeEnd.getTime(), end.getTime()));
540
+ openMinutes += (overlapEnd.getTime() - overlapStart.getTime()) / (60 * 1000);
541
+ }
542
+ return openMinutes;
543
+ }
544
+ hasExceptionBetween(start, end) {
545
+ const current = new Date(start);
546
+ current.setDate(current.getDate() + 1);
547
+ current.setHours(0, 0, 0, 0);
548
+ const endDate = new Date(end);
549
+ endDate.setHours(0, 0, 0, 0);
550
+ while (current < endDate) {
551
+ if (this.exceptions.has(this.formatDate(current))) {
552
+ return true;
553
+ }
554
+ current.setDate(current.getDate() + 1);
555
+ }
556
+ return false;
557
+ }
558
+ diffInOpenSeconds(startDate, endDate) {
559
+ return this.diffInOpenMinutes(startDate, endDate) * 60;
560
+ }
561
+ diffInClosedHours(startDate, endDate) {
562
+ return this.diffInClosedMinutes(startDate, endDate) / 60;
563
+ }
564
+ diffInClosedMinutes(startDate, endDate) {
565
+ const totalMinutes = Math.abs(endDate.getTime() - startDate.getTime()) / (60 * 1000);
566
+ const openMinutes = this.diffInOpenMinutes(startDate, endDate);
567
+ return totalMinutes - openMinutes;
568
+ }
569
+ diffInClosedSeconds(startDate, endDate) {
570
+ return this.diffInClosedMinutes(startDate, endDate) * 60;
571
+ }
572
+ currentOpenRange(dateTime) {
573
+ const adjustedDateTime = this.toTimezone(dateTime);
574
+ return this.forDate(adjustedDateTime).getCurrentOpenRangeAtDateTime(adjustedDateTime);
575
+ }
576
+ currentOpenRangeStart(dateTime) {
577
+ const adjustedDateTime = this.toTimezone(dateTime);
578
+ const range = this.forDate(adjustedDateTime).getCurrentOpenRangeAtDateTime(adjustedDateTime);
579
+ if (!range) {
580
+ return null;
581
+ }
582
+ const result = range.getStart().toDate(adjustedDateTime);
583
+ if (this.overflow) {
584
+ const midnight = new Date(adjustedDateTime);
585
+ midnight.setHours(0, 0, 0, 0);
586
+ if (result < midnight && adjustedDateTime >= midnight) {
587
+ result.setDate(result.getDate() - 1);
588
+ }
589
+ }
590
+ return this.timezone ? this.fromTimezone(result) : result;
591
+ }
592
+ currentOpenRangeEnd(dateTime) {
593
+ const adjustedDateTime = this.toTimezone(dateTime);
594
+ const range = this.forDate(adjustedDateTime).getCurrentOpenRangeAtDateTime(adjustedDateTime);
595
+ if (!range) {
596
+ return null;
597
+ }
598
+ const result = range.getEnd().toDate(adjustedDateTime);
599
+ if (this.overflow && range.spansOvernight()) {
600
+ result.setDate(result.getDate() + 1);
601
+ }
602
+ return this.timezone ? this.fromTimezone(result) : result;
603
+ }
604
+ findNextOpenRange(dateTime) {
605
+ let searchDate = this.toTimezone(dateTime);
606
+ let comparisonTime = new Date(searchDate);
607
+ for (let i = 0;i < 14; i++) {
608
+ const dayHours = this.forDate(searchDate);
609
+ const ranges = dayHours.getTimeRanges();
610
+ for (const range of ranges) {
611
+ const start = range.getStart().toDate(searchDate);
612
+ const end = range.getEnd().toDate(searchDate);
613
+ if (this.overflow && range.spansOvernight()) {
614
+ end.setDate(end.getDate() + 1);
615
+ }
616
+ if (end <= comparisonTime) {
617
+ continue;
618
+ }
619
+ if (start > comparisonTime) {
620
+ return { range, date: new Date(searchDate) };
621
+ }
622
+ }
623
+ searchDate = new Date(searchDate);
624
+ searchDate.setDate(searchDate.getDate() + 1);
625
+ searchDate.setHours(0, 0, 0, 0);
626
+ comparisonTime = searchDate;
627
+ }
628
+ return null;
629
+ }
630
+ findPreviousOpenRange(dateTime) {
631
+ let searchDate = this.toTimezone(dateTime);
632
+ let comparisonTime = new Date(searchDate);
633
+ for (let i = 0;i < 14; i++) {
634
+ const dayHours = this.forDate(searchDate);
635
+ const ranges = dayHours.getTimeRanges();
636
+ for (let j = ranges.length - 1;j >= 0; j--) {
637
+ const range = ranges[j];
638
+ if (!range)
639
+ continue;
640
+ const start = range.getStart().toDate(searchDate);
641
+ const end = range.getEnd().toDate(searchDate);
642
+ if (this.overflow && range.spansOvernight()) {
643
+ end.setDate(end.getDate() + 1);
644
+ }
645
+ if (start <= comparisonTime && end > comparisonTime) {
646
+ return { range, date: new Date(searchDate) };
647
+ }
648
+ if (end < comparisonTime) {
649
+ return { range, date: new Date(searchDate) };
650
+ }
651
+ }
652
+ searchDate = new Date(searchDate);
653
+ searchDate.setDate(searchDate.getDate() - 1);
654
+ searchDate.setHours(0, 0, 0, 0);
655
+ comparisonTime = new Date(searchDate);
656
+ comparisonTime.setHours(23, 59, 59, 999);
657
+ }
658
+ return null;
659
+ }
660
+ nextOpenRange(dateTime) {
661
+ const result = this.findNextOpenRange(dateTime);
662
+ return result ? result.range : null;
663
+ }
664
+ nextOpenRangeStart(dateTime) {
665
+ const result = this.findNextOpenRange(dateTime);
666
+ if (!result) {
667
+ return null;
668
+ }
669
+ const start = result.range.getStart().toDate(result.date);
670
+ return this.timezone ? this.fromTimezone(start) : start;
671
+ }
672
+ nextOpenRangeEnd(dateTime) {
673
+ const result = this.findNextOpenRange(dateTime);
674
+ if (!result) {
675
+ return null;
676
+ }
677
+ const end = result.range.getEnd().toDate(result.date);
678
+ if (this.overflow && result.range.spansOvernight()) {
679
+ end.setDate(end.getDate() + 1);
680
+ }
681
+ return this.timezone ? this.fromTimezone(end) : end;
682
+ }
683
+ nextOpen(dateTime) {
684
+ return this.nextOpenRangeStart(dateTime);
685
+ }
686
+ nextClose(dateTime) {
687
+ if (this.isOpenAt(dateTime)) {
688
+ return this.currentOpenRangeEnd(dateTime);
689
+ }
690
+ return this.nextOpenRangeEnd(dateTime);
691
+ }
692
+ previousOpenRange(dateTime) {
693
+ const result = this.findPreviousOpenRange(dateTime);
694
+ return result ? result.range : null;
695
+ }
696
+ previousOpenRangeStart(dateTime) {
697
+ const result = this.findPreviousOpenRange(dateTime);
698
+ if (!result) {
699
+ return null;
700
+ }
701
+ const start = result.range.getStart().toDate(result.date);
702
+ return this.timezone ? this.fromTimezone(start) : start;
703
+ }
704
+ previousOpenRangeEnd(dateTime) {
705
+ const result = this.findPreviousOpenRange(dateTime);
706
+ if (!result) {
707
+ return null;
708
+ }
709
+ const end = result.range.getEnd().toDate(result.date);
710
+ if (this.overflow && result.range.spansOvernight()) {
711
+ end.setDate(end.getDate() + 1);
712
+ }
713
+ return this.timezone ? this.fromTimezone(end) : end;
714
+ }
715
+ static from(config) {
716
+ return new OpeningHours(config);
717
+ }
718
+ static createFromStructuredData(inputData, displayOptions = {}) {
719
+ let data = inputData;
720
+ if (typeof data === "string") {
721
+ try {
722
+ data = JSON.parse(data);
723
+ } catch (_e) {
724
+ throw new Error("Invalid JSON string for structured data");
725
+ }
726
+ }
727
+ if (!Array.isArray(data)) {
728
+ throw new Error("Structured data must be an array of OpeningHoursSpecification");
729
+ }
730
+ const config = {};
731
+ const exceptions = {};
732
+ for (const item of data) {
733
+ const opens = item.opens;
734
+ const closes = item.closes;
735
+ if (!opens || !closes) {
736
+ continue;
737
+ }
738
+ const startStr = Time.fromString(opens).toString();
739
+ const endStr = Time.fromString(closes).toString();
740
+ const timeRange = `${startStr}-${endStr}`;
741
+ if (item.validFrom && item.validThrough) {
742
+ const fromDate = new Date(item.validFrom);
743
+ const throughDate = new Date(item.validThrough);
744
+ const current = new Date(fromDate);
745
+ while (current <= throughDate) {
746
+ const dateStr = current.toISOString().split("T")[0];
747
+ if (!exceptions[dateStr]) {
748
+ exceptions[dateStr] = [];
749
+ }
750
+ exceptions[dateStr]?.push(timeRange);
751
+ current.setDate(current.getDate() + 1);
752
+ }
753
+ continue;
754
+ }
755
+ if (item.dayOfWeek) {
756
+ const days = Array.isArray(item.dayOfWeek) ? item.dayOfWeek : [item.dayOfWeek];
757
+ for (const day of days) {
758
+ let dayName;
759
+ if (typeof day === "string" && day.includes("schema.org/")) {
760
+ const parts = day.split("/");
761
+ const lastPart = parts[parts.length - 1];
762
+ dayName = lastPart.toLowerCase();
763
+ } else {
764
+ dayName = day.toLowerCase();
765
+ }
766
+ if (dayName in Days) {
767
+ if (!config[dayName]) {
768
+ config[dayName] = [];
769
+ }
770
+ config[dayName].push(timeRange);
771
+ }
772
+ }
773
+ }
774
+ }
775
+ if (Object.keys(exceptions).length > 0) {
776
+ config.exceptions = exceptions;
777
+ }
778
+ return new OpeningHours({ ...config, ...displayOptions });
779
+ }
780
+ formatISOTime(time, timezone) {
781
+ const hh = time.getHour().toString().padStart(2, "0");
782
+ const mm = time.getMinute().toString().padStart(2, "0");
783
+ const ss = "00";
784
+ if (timezone) {
785
+ try {
786
+ const formatter = new Intl.DateTimeFormat("en", {
787
+ timeZone: timezone,
788
+ timeZoneName: "longOffset"
789
+ });
790
+ const parts = formatter.formatToParts(new Date);
791
+ const offsetPart = parts.find((part) => part.type === "timeZoneName");
792
+ if (offsetPart?.value.match(/GMT[+-]\d{2}:\d{2}/)) {
793
+ const offset = offsetPart.value.replace("GMT", "");
794
+ return `${hh}:${mm}:${ss}${offset}`;
795
+ }
796
+ } catch (_error) {}
797
+ return `${hh}:${mm}:${ss}`;
798
+ }
799
+ return `${hh}:${mm}:${ss}Z`;
800
+ }
801
+ asStructuredData(timezone) {
802
+ const usedTimezone = timezone || this.timezone || null;
803
+ const result = [];
804
+ const dayMap = {};
805
+ for (const day in Days) {
806
+ const dayRanges = this.forDay(day).getTimeRanges();
807
+ for (const range of dayRanges) {
808
+ const key = range.toString();
809
+ if (!dayMap[key]) {
810
+ dayMap[key] = [];
811
+ }
812
+ dayMap[key].push(range);
813
+ }
814
+ }
815
+ for (const [timeRangeStr, ranges] of Object.entries(dayMap)) {
816
+ const days = [];
817
+ if (ranges.length === 0) {
818
+ continue;
819
+ }
820
+ const timeRange = ranges[0];
821
+ for (const day in Days) {
822
+ const dayRanges = this.forDay(day).getTimeRanges();
823
+ if (dayRanges.some((r) => r.toString() === timeRangeStr)) {
824
+ days.push(`https://schema.org/${day.charAt(0).toUpperCase() + day.slice(1)}`);
825
+ }
826
+ }
827
+ if (days.length > 0) {
828
+ result.push({
829
+ "@type": "OpeningHoursSpecification",
830
+ dayOfWeek: days,
831
+ opens: this.formatISOTime(timeRange.getStart(), usedTimezone),
832
+ closes: this.formatISOTime(timeRange.getEnd(), usedTimezone)
833
+ });
834
+ }
835
+ }
836
+ for (const [dateStr, openingHours] of Object.entries(this.getExceptions())) {
837
+ const ranges = openingHours.getTimeRanges();
838
+ if (ranges.length === 0) {
839
+ result.push({
840
+ "@type": "OpeningHoursSpecification",
841
+ validFrom: dateStr,
842
+ validThrough: dateStr,
843
+ opens: "00:00:00Z",
844
+ closes: "00:00:00Z"
845
+ });
846
+ } else {
847
+ for (const range of ranges) {
848
+ result.push({
849
+ "@type": "OpeningHoursSpecification",
850
+ validFrom: dateStr,
851
+ validThrough: dateStr,
852
+ opens: this.formatISOTime(range.getStart(), usedTimezone),
853
+ closes: this.formatISOTime(range.getEnd(), usedTimezone)
854
+ });
855
+ }
856
+ }
857
+ }
858
+ return result;
859
+ }
860
+ toTimezone(date) {
861
+ if (!this.timezone) {
862
+ return new Date(date);
863
+ }
864
+ try {
865
+ return new Date(date.toLocaleString("en-US", { timeZone: this.timezone }));
866
+ } catch (_e) {
867
+ console.warn(`Invalid timezone: ${this.timezone}. Using local timezone.`);
868
+ return new Date(date);
869
+ }
870
+ }
871
+ fromTimezone(date) {
872
+ if (!this.timezone) {
873
+ return new Date(date);
874
+ }
875
+ try {
876
+ const inv = new Date(date.toLocaleString("en-US", { timeZone: this.timezone }));
877
+ const diff = inv.getTime() - date.getTime();
878
+ return new Date(date.getTime() - diff);
879
+ } catch (_e) {
880
+ console.warn(`Invalid timezone: ${this.timezone}. Using local timezone.`);
881
+ return new Date(date);
882
+ }
883
+ }
884
+ getScheduleStringForDay(day) {
885
+ const timeRanges = this.getTimeRangesForDayOfWeek(day);
886
+ return timeRanges.map((tr) => tr.toString()).join(",");
887
+ }
888
+ getTimeRangesForDayOfWeek(day) {
889
+ const schedule = this.schedule.get(day) || [];
890
+ return schedule.map((tr) => this.convertToTimeRange(tr));
891
+ }
892
+ formatDayRange(days, options = {}) {
893
+ const { locale = "en-US", weekday = "short", dayRangeSeparator = "..." } = options;
894
+ const first = this.formatDayName(days[0], locale, weekday);
895
+ if (days.length === 1) {
896
+ return first;
897
+ }
898
+ const last = this.formatDayName(days[days.length - 1], locale, weekday);
899
+ return `${first}${dayRangeSeparator}${last}`;
900
+ }
901
+ formatTimeRange(timeRange, options = {}) {
902
+ const {
903
+ timeRangeSeparator = "-",
904
+ timeFormat,
905
+ locale = "en-US"
906
+ } = {
907
+ ...this.displayOptions,
908
+ ...options
909
+ };
910
+ const defaultFormat = locale === "en-US" ? "gA" : "H:i";
911
+ const format = timeFormat || defaultFormat;
912
+ const startStr = timeRange.getStart().format(format);
913
+ const endStr = timeRange.getEnd().format(format);
914
+ let result = startStr + timeRangeSeparator + endStr;
915
+ const data = timeRange.getData();
916
+ if (data && typeof data.note === "string" && data.note.length > 0) {
917
+ result += ` (${data.note})`;
918
+ }
919
+ return result;
920
+ }
921
+ toArray(options = {}) {
922
+ const mergedOptions = { ...this.displayOptions, ...options };
923
+ const {
924
+ locale = "en-US",
925
+ weekday = "short",
926
+ firstDayOfWeek = "monday",
927
+ showTimezone = !!this.timezone,
928
+ timeZoneName = "shortGeneric",
929
+ closedText = "Closed",
930
+ dayRangeSeparator = "..."
931
+ } = mergedOptions;
932
+ const tz = showTimezone ? this.getTimezoneName(locale, timeZoneName) : "";
933
+ const orderedDays = firstDayOfWeek === "sunday" ? ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"] : [
934
+ "monday",
935
+ "tuesday",
936
+ "wednesday",
937
+ "thursday",
938
+ "friday",
939
+ "saturday",
940
+ "sunday"
941
+ ];
942
+ const groups = [];
943
+ let currentGroup = [];
944
+ let currentSchedule = "";
945
+ for (const day of orderedDays) {
946
+ const schedule = this.getScheduleStringForDay(day);
947
+ if (currentGroup.length === 0 || schedule === currentSchedule) {
948
+ currentGroup.push(day);
949
+ } else {
950
+ groups.push(currentGroup);
951
+ currentGroup = [day];
952
+ }
953
+ currentSchedule = schedule;
954
+ }
955
+ if (currentGroup.length > 0) {
956
+ groups.push(currentGroup);
957
+ }
958
+ const result = [];
959
+ for (const group of groups) {
960
+ const dayRange = this.formatDayRange(group, {
961
+ locale,
962
+ weekday,
963
+ dayRangeSeparator
964
+ });
965
+ const day = group[0];
966
+ const timeRanges = this.getTimeRangesForDayOfWeek(day);
967
+ if (timeRanges.length === 0) {
968
+ result.push([dayRange, closedText]);
969
+ continue;
970
+ }
971
+ const formatted = timeRanges.map((tr) => this.formatTimeRange(tr, mergedOptions));
972
+ if (tz && formatted.length > 0) {
973
+ formatted[formatted.length - 1] += ` ${tz}`;
974
+ }
975
+ result.push([dayRange, ...formatted]);
976
+ }
977
+ return result;
978
+ }
979
+ formatDayName(day, locale = "en-US", weekday = "short") {
980
+ const dayIndex = Days[day];
981
+ const date = new Date(2023, 0, 1 + dayIndex);
982
+ return new Intl.DateTimeFormat(locale, { weekday }).format(date);
983
+ }
984
+ getTimezoneName(locale, timeZoneName = "shortGeneric") {
985
+ const zone = this.timezone;
986
+ if (!zone) {
987
+ return "";
988
+ }
989
+ const formatter = new Intl.DateTimeFormat(locale, {
990
+ timeZone: zone,
991
+ timeZoneName
992
+ });
993
+ const parts = formatter.formatToParts(new Date);
994
+ const tzPart = parts.find((p) => p.type === "timeZoneName");
995
+ return tzPart?.value || "";
996
+ }
997
+ toString(options = {}) {
998
+ const mergedOptions = { ...this.displayOptions, ...options };
999
+ const { timeRangesSeparator = ", " } = mergedOptions;
1000
+ return this.toArray(mergedOptions).map((row) => {
1001
+ if (row.length <= 1)
1002
+ return row.join("");
1003
+ const [dayPart, ...timeRanges] = row;
1004
+ return `${dayPart} ${timeRanges.join(timeRangesSeparator)}`;
1005
+ }).join(`
1006
+ `);
1007
+ }
1008
+ }
1009
+ })();