__( 'Sunday', 'a1-tools' ), 'mon' => __( 'Monday', 'a1-tools' ), 'tue' => __( 'Tuesday', 'a1-tools' ), 'wed' => __( 'Wednesday', 'a1-tools' ), 'thu' => __( 'Thursday', 'a1-tools' ), 'fri' => __( 'Friday', 'a1-tools' ), 'sat' => __( 'Saturday', 'a1-tools' ), ); } /** Abbreviations, for a group that spans a range. */ function a1tools_hours_day_abbrevs() { return array( 'sun' => __( 'Sun', 'a1-tools' ), 'mon' => __( 'Mon', 'a1-tools' ), 'tue' => __( 'Tue', 'a1-tools' ), 'wed' => __( 'Wed', 'a1-tools' ), 'thu' => __( 'Thu', 'a1-tools' ), 'fri' => __( 'Fri', 'a1-tools' ), 'sat' => __( 'Sat', 'a1-tools' ), ); } /** * Comparison key for one day's value. * * Only ever used to decide whether two days are "the same" — the value shown * on screen is always the original string, so a site that writes * "8:00 AM – 10:00 PM" keeps its en dash even though this folds it to a * hyphen for matching. * * @param string $raw Stored value for one day. * @return string Normalised key; '' means closed. */ function a1tools_hours_compare_key( $raw ) { $v = trim( (string) $raw ); if ( '' === $v ) { return ''; } // En/em dash and the various unicode hyphens all mean the same range. $v = str_replace( array( "\xE2\x80\x93", "\xE2\x80\x94", "\xE2\x80\x92", "\xE2\x88\x92" ), '-', $v ); $v = preg_replace( '/\s+/', ' ', $v ); $v = strtolower( $v ); // "Closed", "closed", "CLOSED" and an empty value are one state. if ( 'closed' === $v ) { return ''; } return $v; } /** * Collapse a seven-day schedule into display groups. * * @param array $hours Day key ('mon'…) => stored value. Missing = closed. * @param string $closed_text Label for a closed group. * @param bool $hide_closed Drop closed groups entirely (applied AFTER * grouping, so hiding never merges what was split). * @return array */ function a1tools_group_operating_hours( $hours, $closed_text = 'Closed', $hide_closed = false ) { $order = a1tools_hours_day_order(); $names = a1tools_hours_day_names(); $abbrevs = a1tools_hours_day_abbrevs(); $keys = array(); $raws = array(); foreach ( $order as $d ) { $raw = isset( $hours[ $d ] ) ? (string) $hours[ $d ] : ''; $raws[ $d ] = $raw; $keys[ $d ] = a1tools_hours_compare_key( $raw ); } // Runs of adjacent days sharing a comparison key. $runs = array(); foreach ( $order as $d ) { $k = $keys[ $d ]; if ( $runs && end( $runs )['key'] === $k ) { $runs[ count( $runs ) - 1 ]['days'][] = $d; continue; } $runs[] = array( 'key' => $k, 'days' => array( $d ) ); } // One boundary merge: Saturday's run may join Sunday's when they match, so a // Sat+Sun weekend reads as "Sat – Sun" instead of two identical rows at // opposite ends of the list. // // Capped at three days, which is the spec's "only when the resulting // chronological label remains unambiguous" made concrete. A range label is // read forward from its first day, so a SHORT wrap is obvious ("Sat – Sun" // is plainly two days) while a long one hides which day was left out: // Sun–Tue plus Thu–Sat merges to "Thu – Tue", and a reader has to work // backwards to discover Wednesday is the exception. Three rows that each // say what they mean beat two that need decoding, so past three days the // runs stay separate. // // The cap also makes the all-seven case impossible, where "Sat – Sat" // would have been a lie about a full week. $max_wrap_days = 3; if ( count( $runs ) > 1 ) { $first = $runs[0]; $last = $runs[ count( $runs ) - 1 ]; if ( $first['key'] === $last['key'] && 'sun' === $first['days'][0] && 'sat' === $last['days'][ count( $last['days'] ) - 1 ] && count( $first['days'] ) + count( $last['days'] ) <= $max_wrap_days ) { array_pop( $runs ); array_shift( $runs ); // Saturday-side days lead so the label reads in wrap order, and the // merged run goes LAST: it now starts on Saturday, which is after // every remaining run, so appending keeps the list in chronological // order by start day. Leaving it in Sunday's old slot printed // "Sat – Sun" above "Mon – Fri" — a week beginning at the weekend. $runs[] = array( 'key' => $first['key'], 'days' => array_merge( $last['days'], $first['days'] ), ); } } $groups = array(); foreach ( $runs as $run ) { $is_closed = ( '' === $run['key'] ); if ( $hide_closed && $is_closed ) { continue; } $days = $run['days']; $first = $days[0]; $last = $days[ count( $days ) - 1 ]; $label = ( 1 === count( $days ) ) ? $names[ $first ] : $abbrevs[ $first ] . ' – ' . $abbrevs[ $last ]; $groups[] = array( 'label' => $label, // The original stored string, never the normalised one. 'time' => $is_closed ? $closed_text : $raws[ $first ], 'is_closed' => $is_closed, 'days' => $days, ); } return $groups; }