Jake Vanderwerf
2025-11-04 42fa8304ddb811b0f725f245130f70c0f5e86a6c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
<?php
 
use JVBase\managers\CacheManager;
 
if (!defined('ABSPATH')) {
    exit;
}
 
/**
 * @return string
 */
function jvbLastMonth():string
{
    $first_of_this_month = strtotime(date('Y-m-01'));
 
    // Subtract one day to get the last day of the previous month
    $last_of_prev_month = strtotime('-1 day', $first_of_this_month);
 
    // Return the month name of that date
    return date('F', $last_of_prev_month);
}
 
/**
 * Format a date in relative time (e.g., "2 days ago")
 * @param string $dateStr - Date string or DateTime object
 * @return string - Formatted relative time
 */
function jvbFormatTimeAgo(string $dateStr):string
{
    // Convert input to DateTime if it's a string
    if (is_string($dateStr)) {
        $date = new DateTime($dateStr);
    } elseif ($dateStr instanceof DateTime) {
        $date = $dateStr;
    } else {
        return '';
    }
 
    $now = new DateTime();
    $diff = $now->getTimestamp() - $date->getTimestamp();
 
    $seconds = floor($diff);
    $minutes = floor($seconds / 60);
    $hours = floor($minutes / 60);
    $days = floor($hours / 24);
    $weeks = floor($days / 7);
    $months = floor($days / 30);
 
    // Within the last 24 hours - show hours ago
    if ($hours < 24) {
        if ($hours === 0) {
            return $minutes === 0 ? 'Just now' : sprintf('%d %s ago', $minutes, $minutes === 1 ? 'minute' : 'minutes');
        }
        return sprintf('%d %s ago', $hours, $hours === 1 ? 'hour' : 'hours');
    }
 
    // Within the last 7 days - show days ago
    if ($days < 7) {
        return sprintf('%d %s ago', $days, $days === 1 ? 'day' : 'days');
    }
 
    // Within the last 4 weeks - show week-based format
    if ($weeks < 4) {
        if ($weeks === 1) {
            return 'Last week';
        }
        return 'A few weeks ago';
    }
 
    // Within the last 2 months - show "Last month"
    if ($months < 2) {
        return 'Last month';
    }
 
    // For anything older, show the full date
    return $date->format('F j, Y');
}
 
 
/**
 * @param string $time
 *
 * @return string
 */
function jvbFormat12HourTime(string $time):string
{
    if (!$time) {
        return '';
    }
    // Split the time into hours and minutes
    list($hours, $minutes) = explode(':', $time);
 
    // Convert to integers
    $hours = (int)$hours;
    $minutes = (int)$minutes;
 
    // Determine AM/PM
    $ampm = ($hours >= 12) ? 'pm' : 'am';
 
    // Convert to 12-hour format
    $hours = $hours % 12;
    if ($hours === 0) {
        $hours = 12;
    }
 
    // Format the final string
    if ($minutes === 0) {
        return $hours . $ampm; // No minutes needed for whole hours (e.g., 10am)
    } else {
        return $hours . ':' . sprintf('%02d', $minutes) . $ampm; // Include minutes (e.g., 5:30pm)
    }
}
 
/**
 * @param string $start
 * @param string $end
 * @param bool $shortened
 *
 * @return string
 */
function formatRange(string $start, string $end, bool $shortened = false):string
{
    // Create a formatting function
    $formatDay = function ($day) use ($shortened) {
        return ucfirst($shortened ? substr($day, 0, 3) : $day);
    };
 
    // Apply formatting to both days
    $formattedStart = $formatDay($start);
    $formattedEnd = $formatDay($end);
 
    // Return single day or range as appropriate
    return ($start === $end) ? $formattedStart : "$formattedStart-$formattedEnd";
}
 
/**
 * @param int $ID
 * @param JVBase\Meta\MetaManager $meta
 *
 * @return string
 */
function jvbRenderHours(int $ID, JVBase\Meta\MetaManager $meta):string
{
    $cache = CacheManager::for('hours-'.$ID, WEEK_IN_SECONDS);
    $key = 'hours_display';
    $cached = $cache->get($key);
 
    if ($cached !== false) {
        return $cached;
    }
 
    if (!$meta) {
        if (term_exists((int)$ID)) {
            $type = 'term';
        } elseif (get_post_status((int)$ID)) {
            $type = 'post';
        } else {
            $type = 'user';
        }
        $meta = new JVBase\meta\MetaManager($ID, $type);
    }
 
    $hours = $meta->getValue('hours');
    $byAppt = $meta->getValue('by_appointment');
    $walkins = $meta->getValue('walkins');
 
    $out = '';
 
    if (!empty($hours) && jvbHasOperatingHours($hours)) {
        $out = jvbGetFormattedHours($hours, true, true);
    } else {
        $out = '<p class="no-hours">Hours not available</p>';
    }
 
    // Add appointment and walk-in information
    $notes = [];
    if ($byAppt) {
        $notes[] = 'By appointment only';
    }
    if ($walkins) {
        $notes[] = 'Walk-ins welcome';
    }
 
    if (!empty($notes)) {
        $out .= '<p class="hours-notes"><small>' . implode(' • ', $notes) . '</small></p>';
    }
 
    $cache->set($key, $out);
    return $out;
}
 
/**
 * @param array $daysList
 * @param $short
 *
 * @return string
 */
function jvbCondenseDayRange(array $daysList, $short = false):string
{
    if (count($daysList) === 7 || $daysList[0] === 'daily') {
        return 'Daily';
    }
    // Define the complete list of days in order
    $allDays = jvbFullWeekdays();
 
    // Create a mapping of day names to their indices
    $dayIndices = array_flip($allDays);
 
    // Split the input into individual days and normalize
    $days = array_map('trim', $daysList);
    $days = array_map('strtolower', $days);
    if ($short) {
    }
 
    // Sort the days according to their position in the week
    usort($days, function ($a, $b) use ($dayIndices) {
        return $dayIndices[$a] - $dayIndices[$b];
    });
 
    // Find consecutive ranges
    $ranges = [];
    $rangeStart = null;
    $rangeEnd = null;
 
    foreach ($days as $i => $day) {
        if ($rangeStart === null) {
            $rangeStart = $day;
            $rangeEnd = $day;
        } elseif ($dayIndices[$day] == $dayIndices[$rangeEnd] + 1) {
            // This day is consecutive to the previous one
            $rangeEnd = $day;
        } else {
            // This day breaks the sequence, save the current range
            $ranges[] = formatRange($rangeStart, $rangeEnd, $short);
            $rangeStart = $day;
            $rangeEnd = $day;
        }
    }
 
    // Add the last range
    if ($rangeStart !== null) {
        $ranges[] = formatRange($rangeStart, $rangeEnd, $short);
    }
 
    // Join the ranges with commas
    return implode(', ', $ranges);
}
 
/**
 * @param string $range wed-thur or wednesday-thursday
 *
 * @return string
 */
function jvbExpandDayRange(string $range):string
{
    if ($range === 'daily') {
        return 'daily';
    }
    // Define the complete list of days in order
    $allDays = jvbFullWeekdays();
    $shortDays = ['mon', 'tue', 'wed', 'thu', 'fri','sat','sun'];
    // Split the range into start and end days
    [$startDay, $endDay] = explode('-', strtolower($range));
    error_log('Start Day: '.print_r($startDay, true));
    error_log('End Day: '.print_r($endDay, true));
    $days = (in_array($startDay, $allDays)) ?
        $allDays :
        ((in_array($startDay, $shortDays)) ? $shortDays : false);
    error_log('Final Days: '.print_r($days, true));
    if (!$days) {
        return '';
    }
    // Find the positions of start and end days
    $startIndex = array_search(strtolower($startDay), $days);
    $endIndex = array_search(strtolower($endDay), $days);
 
    // Handle case where end comes before start in the week (wrapping around)
    if ($startIndex > $endIndex) {
        $endIndex += 7;
    }
 
    // Extract the days in the range
    $result = [];
    for ($i = $startIndex; $i <= $endIndex; $i++) {
        $result[] = $allDays[$i % 7];
    }
    if (count($result) === 7) {
        $result = ['daily'];
    }
 
    // Join with commas and return
    return implode(',', $result);
}
 
/**
 * @return array
 */
function jvbFullWeekdays():array
{
    return ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
}
 
 
/**
 * Check if business is currently open based on stored hours
 *
 * @param array $hours_data Array of hours data from wp_option
 * @param string $timezone Timezone string (default: 'America/Edmonton')
 * @return bool True if currently open, false if closed
 */
function jvbIsCurrentlyOpen($hours_data = null, $timezone = 'America/Edmonton') {
    // Get hours data if not provided
    if ($hours_data === null) {
        $hours_data = get_option(BASE.'hours', []);
    }
 
    // Return false if no hours data
    if (empty($hours_data) || !is_array($hours_data)) {
        return false;
    }
 
    // Set timezone for current time
    $current_time = new DateTime('now', new DateTimeZone($timezone));
    $current_day = strtolower($current_time->format('l')); // monday, tuesday, etc.
    $current_time_string = $current_time->format('H:i'); // 24-hour format
 
    // Get today's hours
    $today_hours = $hours_data[$current_day] ?? [];
 
    // Return false if closed today
    if (empty($today_hours) || !($today_hours['open'] ?? false)) {
        return false;
    }
 
    $open_time = $today_hours['time_opens'] ?? '';
    $close_time = $today_hours['time_closes'] ?? '';
 
    if (!$open_time || !$close_time) {
        return false;
    }
 
    // Handle different time formats (with or without seconds)
    $open_time = date('H:i', strtotime($open_time));
    $close_time = date('H:i', strtotime($close_time));
 
    // Check if current time is within operating hours
    if ($close_time > $open_time) {
        // Normal case: opens and closes on same day
        return ($current_time_string >= $open_time && $current_time_string <= $close_time);
    } else {
        // Handles overnight hours (e.g., 22:00 to 02:00)
        return ($current_time_string >= $open_time || $current_time_string <= $close_time);
    }
}
 
/**
 * Check if current time is between two specified times
 *
 * @param string $start_time Start time in H:i format (e.g., '10:00')
 * @param string $end_time End time in H:i format (e.g., '15:15')
 * @param string $timezone Timezone string (default: 'America/Edmonton')
 * @return bool True if current time is within range, false otherwise
 */
function jvbIsTimeBetween($start_time=null, $end_time=null, $timezone = 'America/Edmonton') {
    if (!$start_time && !$end_time) {
        $hours = get_option(BASE.'today_hours');
        $start_time = $hours['time_start'];
        $end_time = $hours['time_end'];
    }
 
    // Get current time in specified timezone
    $current_time = new DateTime('now', new DateTimeZone($timezone));
    $current_time_string = $current_time->format('H:i');
 
    // Normalize time formats (handle with or without seconds)
    $start_time = date('H:i', strtotime($start_time));
    $end_time = date('H:i', strtotime($end_time));
 
    // Check if current time is within range
    if ($end_time > $start_time) {
        // Normal case: same day (e.g., 10:00 to 15:15)
        return ($current_time_string >= $start_time && $current_time_string <= $end_time);
    } else {
        // Overnight case: spans midnight (e.g., 22:00 to 02:00)
        return ($current_time_string >= $start_time || $current_time_string <= $end_time);
    }
}
 
/**
 * Get next opening time for business
 *
 * @param array $hours_data Day-based hours data
 * @param string $timezone Timezone string
 * @return string|null Next opening time description or null if never opens
 */
function jvbGetNextOpeningTime(array $hours_data, string $timezone = 'America/Edmonton'): ?string {
    if (!jvbHasOperatingHours($hours_data)) {
        return null;
    }
 
    $current_time = new DateTime('now', new DateTimeZone($timezone));
    $weekdays = jvbFullWeekdays();
 
    // Check next 7 days
    for ($i = 0; $i < 7; $i++) {
        $check_date = clone $current_time;
        $check_date->modify("+{$i} day");
        $day_name = strtolower($check_date->format('l'));
 
        $day_data = $hours_data[$day_name] ?? [];
        if (empty($day_data) || !($day_data['open'] ?? false)) {
            continue;
        }
 
        $open_time = $day_data['time_opens'] ?? '';
        if (!$open_time) {
            continue;
        }
 
        // If it's today, make sure we haven't passed opening time
        if ($i === 0) {
            $today_open = DateTime::createFromFormat('H:i', $open_time, new DateTimeZone($timezone));
            $today_open->setDate(
                $current_time->format('Y'),
                $current_time->format('m'),
                $current_time->format('d')
            );
 
            if ($current_time >= $today_open) {
                continue; // Already passed today's opening time
            }
        }
 
        // Format the result
        if ($i === 0) {
            return 'Opens today at ' . jvbFormat12HourTime($open_time);
        } elseif ($i === 1) {
            return 'Opens tomorrow at ' . jvbFormat12HourTime($open_time);
        } else {
            $day_formatted = ucfirst($day_name);
            return "Opens {$day_formatted} at " . jvbFormat12HourTime($open_time);
        }
    }
 
    return null;
}
 
 
/**
 * Check if business has any operating hours
 *
 * @param array $hours_data Day-based hours data
 * @return bool True if has any open days, false if always closed
 */
function jvbHasOperatingHours(array $hours_data): bool {
    foreach ($hours_data as $day_data) {
        if (!empty($day_data) && ($day_data['open'] ?? false)) {
            return true;
        }
    }
    return false;
}
 
/**
 * Get formatted hours for display with additional context
 *
 * @param array $hours_data Day-based hours data
 * @param bool $include_notes Include additional notes like "By appointment" etc.
 * @param bool $short Use short day names
 * @return string Complete formatted hours display
 */
function jvbGetFormattedHours(array $hours_data, bool $include_notes = true, bool $short = true): string {
    $condensed_hours = jvbRenderCondensedHours($hours_data, $short, 'list-none');
 
    $output = $condensed_hours;
 
    if ($include_notes) {
        $notes = [];
 
        // Check for special conditions (you can extend this based on your needs)
        $has_weekend_hours = !empty($hours_data['saturday']) || !empty($hours_data['sunday']);
        $has_all_days = true;
 
        foreach (jvbFullWeekdays() as $day) {
            if (empty($hours_data[$day]) || !($hours_data[$day]['open'] ?? false)) {
                $has_all_days = false;
                break;
            }
        }
 
        if (!$has_all_days) {
            $notes[] = '<small><i>Hours may be different on holidays</i></small>';
        }
 
        if (!empty($notes)) {
            $output .= '<p>' . implode(' ', $notes) . '</p>';
        }
    }
 
    return $output;
}
 
 
/**
 * Render condensed hours as HTML list
 *
 * @param array $hours_data Day-based hours data
 * @param bool $short Use short day names
 * @param string $class_name CSS class for the list
 * @return string HTML formatted hours list
 */
function jvbRenderCondensedHours(array $hours_data, bool $short = true, string $class_name = 'hours-list'): string {
    $condensed = jvbCondenseHours($hours_data, $short);
 
    if (empty($condensed)) {
        return '<p class="no-hours">Hours not available</p>';
    }
 
    $html = "<ul class=\"{$class_name}\">";
    foreach ($condensed as $hours_line) {
        $html .= "<li>{$hours_line}</li>";
    }
    $html .= "</ul>";
 
    return $html;
}
 
/**
 * Create a unique signature for hours to compare identical time slots
 *
 * @param array $day_data Single day's hours data
 * @return string Unique signature for the hours
 */
function jvbGetHoursSignature(array $day_data): string {
    if (empty($day_data) || !($day_data['open'] ?? false)) {
        return 'closed';
    }
 
    $opens = $day_data['time_opens'] ?? '';
    $closes = $day_data['time_closes'] ?? '';
 
    // Normalize times to ensure consistent comparison
    $opens = date('H:i', strtotime($opens));
    $closes = date('H:i', strtotime($closes));
 
    return "{$opens}-{$closes}";
}
 
/**
 * Group consecutive days with identical hours
 *
 * @param array $hours_data Day-based hours data
 * @return array Grouped hours with consecutive days
 */
function jvbGroupIdenticalHours(array $hours_data): array {
    $weekdays = jvbFullWeekdays();
    $groups = [];
    $current_group = null;
 
    foreach ($weekdays as $day) {
        $day_data = $hours_data[$day] ?? [];
 
        // Skip closed days (empty arrays or open = false)
        if (empty($day_data) || !($day_data['open'] ?? false)) {
            // Finalize current group if we hit a closed day
            if ($current_group !== null) {
                $groups[] = $current_group;
                $current_group = null;
            }
            continue;
        }
 
        $hours_signature = jvbGetHoursSignature($day_data);
 
        // Start new group or continue existing one
        if ($current_group === null) {
            // Start new group
            $current_group = [
                'time_opens' => $day_data['time_opens'] ?? '',
                'time_closes' => $day_data['time_closes'] ?? '',
                'signature' => $hours_signature,
                'days' => [$day]
            ];
        } elseif ($current_group['signature'] === $hours_signature) {
            // Same hours, add to current group
            $current_group['days'][] = $day;
        } else {
            // Different hours, finalize current group and start new one
            $groups[] = $current_group;
            $current_group = [
                'time_opens' => $day_data['time_opens'] ?? '',
                'time_closes' => $day_data['time_closes'] ?? '',
                'signature' => $hours_signature,
                'days' => [$day]
            ];
        }
    }
 
    // Don't forget the last group
    if ($current_group !== null) {
        $groups[] = $current_group;
    }
 
    return $groups;
}
 
/**
 * Condense hours data into readable format with consecutive day ranges
 *
 * @param array $hours_data Day-based hours data from new structure
 * @param bool $short Use short day names (Mon vs Monday)
 * @return array Array of condensed hours strings
 */
function jvbCondenseHours(array $hours_data, bool $short = true): array {
    if (empty($hours_data)) {
        return [];
    }
 
    // Group identical hours together
    $grouped_hours = jvbGroupIdenticalHours($hours_data);
 
    // Convert groups to readable format
    $condensed = [];
    foreach ($grouped_hours as $group) {
        $days_display = jvbCondenseDayRange($group['days'], $short);
        $open_time = jvbFormat12HourTime($group['time_opens']);
        $close_time = jvbFormat12HourTime($group['time_closes']);
 
        $condensed[] = "{$days_display} {$open_time} - {$close_time}";
    }
 
    return $condensed;
}