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 string $type * * @return string */ function jvbRenderHours(int $ID, string $type = ''):string { $cache = Cache::for('hours_display', WEEK_IN_SECONDS)->connect('taxonomy')->connect('post')->connect('user'); $cached = $cache->get($ID); if ($cached !== false) { return $cached; } $meta = match($type){ 'post' => Meta::forPost($ID), 'term' => Meta::forTerm($ID), 'user' => Meta::forUser($ID), default => false }; if (!$meta) { $meta = jvbGetMeta($ID); } if (!$meta) { return ''; } $hours = $meta->get('hours'); $byAppt = $meta->get('by_appointment'); $walkins = $meta->get('walkins'); $out = ''; if (!empty($hours) && jvbHasOperatingHours($hours)) { $out = jvbGetFormattedHours($hours, true, true); } else { $out = '
Hours not available
'; } // Add appointment and walk-in information $notes = []; if ($byAppt) { $notes[] = 'By appointment only'; } if ($walkins) { $notes[] = 'Walk-ins welcome'; } if (!empty($notes)) { $out .= '' . implode(' • ', $notes) . '
'; } $cache->set($ID, $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 * @throws DateInvalidTimeZoneException */ 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[] = 'Hours may be different on holidays'; } if (!empty($notes)) { $output .= '' . implode(' ', $notes) . '
'; } } 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 'Hours not available
'; } $html = "