16

I'm trying to calculate if the current time is within the opening hours of a restaurant.

This question has been asked a lot on Stackoverflow, but I haven't found one that can account for the problems I am having. Also, would be nice to see idea on a better way to do this.

Currently it breaks if the day is closed (Sunday in this example) or if it's 1am on "Saturday" (so technically 1am Sunday morning). I have a feeling I'll have to change the way the data is stored to account for after midnight, but I'm trying to work with what I have for now. It's a problem, because most restaurants list their opening times for a given day as 5pm - 2am, not 5pm - 12am, 12am - 2am.

Anyway, here is what I have. Please tell me a better way to do it.

I have times stored like this:

$times = array(
    'opening_hours_mon' => '9am - 8pm',
    'opening_hours_tue' => '9am - 2am',
    'opening_hours_wed' => '8:30am - 2am',
    'opening_hours_thu' => '5:30pm - 2am',
    'opening_hours_fri' => '8:30am - 11am',
    'opening_hours_sat' => '9am - 3pm, 5pm - 2am',
    'opening_hours_sun' => 'closed'
);

This is the code I'm using now:

// Get the right key for today
$status = 'open';
$now = (int) current_time( 'timestamp' );
$day = strtolower( date('D', $now) );
$string = 'opening_hours_'.$day;

$times = $meta[$string][0]; // This should be a stirng like '6:00am - 2:00am' or even '6:00am - 11:00am, 1:00pm to 11:00pm'.

// Does it contain a '-', if not assume it's closed.
$pos = strpos($times, '-');
if ($pos === false) {       
    $status = 'closed';
} else {

    // Maybe a day has multiple opening times?
    $seating_times = explode(',', $times);
    foreach( $seating_times as $time ) {

        $chunks = explode('-', $time);
        $open_time = strtotime($chunks[0]);
        $close_time = strtotime($chunks[1]);

        // Calculate if now is between range of open and closed
        if(($open_time <= $now) && ($now <= $close_time)) {
            $status = 'open';
            break;
        } else {
            $status = 'closed';             
        }

    }

}

NOTE: current_time('timestamp',0) is a WordPress function.

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
Drew Baker
  • 14,154
  • 15
  • 58
  • 97
  • 1
    Are flexible with how you store the times array? – user20232359723568423357842364 Feb 04 '13 at 17:56
  • Ideally not, as I have 50+ restaurants stored already as described. But maybe the only way is to reformat the stored times... – Drew Baker Feb 04 '13 at 17:58
  • 3
    @DrewBaker All the more reason to change storage format and 'manually' update the current 50+ data... – zaf Feb 04 '13 at 18:00
  • If a restaurant specifies its times as Fri 8pm to 1am, then internally you could convert this to Fri 8pm-Sat 1am, splitting it into two parts if necessary. – halfer Feb 04 '13 at 19:44
  • 2
    I haven't seen much activity from you on the answers already given ... are you reviewing them? – Ja͢ck Oct 23 '13 at 15:55
  • Hey Jack, yeah I'm reviewing them. There are some great ideas in here. It's interesting because so many of them are different, it's taking me a little to compare them all. Plus I like to give bounty questions a full 6 days to run before accepting something. You never know what might come up! – Drew Baker Oct 23 '13 at 21:00
  • That's true. Have you looked at [my answer](http://stackoverflow.com/a/19515515/1338292) too? If it's not good, I'll probably remove it :) – Ja͢ck Oct 25 '13 at 12:43

11 Answers11

3

Here is my object-oriented solution, based on the usage of the PHP DateTime class (available since the 5.2 version):

<?php 

class Restaurant {
    private $cw;
    private $times = array();
    private $openings = array();

    public function __construct(array $times) {
        $this->times = $times;
        $this->setTimes(date("w") ? "this" : "last");
        //print_r($this->openings);       // Debug
    }

    public function setTimes($cw) {
        $this->cw = $cw;
        foreach ($this->times as $key => $val) {
            $t = array();
            $buf = strtok($val, ' -,');
            for ($n = 0; $buf !== FALSE; $n++) {
                try {
                    $d = new DateTime($buf);
                    $d->setTimestamp(strtotime(substr($key, -3)." {$this->cw} week {$buf}"));
                    if ($n && ($d < $t[$n-1])) {
                        $d->add(new DateInterval('P1D'));
                    }
                    $t[] = $d;
                } catch (Exception $e) {
                    break;
                }
                $buf = strtok(' -,');
            }
            if ($n % 2) {
                throw new Exception("Invalid opening time: {$val}");
            } else {
                $this->openings[substr($key, -3)] = $t;
            }
        }
    }

    public function isOpen() {
        $cw = date("w") ? "this" : "last";
        if ($cw != $this->cw) {
            $this->setTimes($cw);
        }
        $d = new DateTime('now');
        foreach ($this->openings as $wd => $t) {
            $n = count($t);
            for ($i = 0; $i < $n; $i += 2) {
                if (($d >= $t[$i]) && ($d <= $t[$i+1])) {
                    return(TRUE);
                }
            }
        }
        return(FALSE);
    }
}

$times = array(
    'opening_hours_mon' => '9am - 8pm',
    'opening_hours_tue' => '9am - 2am',
    'opening_hours_wed' => '8:30am - 2am',
    'opening_hours_thu' => '9am - 3pm',
    'opening_hours_fri' => '8:30am - 11am',
    'opening_hours_sat' => '9am - 3pm, 5pm - 2am',
    'opening_hours_sun' => 'closed'
);

try {
    $r = new Restaurant($times);
    $status = $r->isOpen() ? 'open' : 'closed';
    echo "status=".$status.PHP_EOL;
} catch (Exception $e) {
    echo $e->getMessage().PHP_EOL;
}

?>

As you can see, the constructor builds an internal form (the openings array of DateTime objects), which is then used with a simple comparison in the isOpen method to check if at the time of the call the restaurant is opened or closed.

You'll also notice that I've used the DateTime:add method to calculate tomorrow's date, instead of adding 86400 (24*60*60) to the current date timestamp, to avoid problems with DST time shifts.
Proof of concept:

<?php

ini_set("date.timezone", "Europe/Rome");
echo "date.timezone = ".ini_get("date.timezone").PHP_EOL;

$d1 = strtotime("2013-10-27 00:00:00");
$d2 = strtotime("2013-10-28 00:00:00");
// Expected: 86400, Result: 90000
echo "Test #1: ".($d2 - $d1).PHP_EOL;
// Expected: 2013-10-28 00:00:00, Result: 2013-10-27 23:00:00
echo "Test #2: ".date("Y-m-d H:i:s", $d1 + 86400).PHP_EOL;

$d1 = strtotime("2014-03-30 00:00:00");
$d2 = strtotime("2014-03-31 00:00:00");
// Expected: 86400, Result: 82800
echo "Test #3: ".($d2 - $d1).PHP_EOL;
// Expected: 2014-03-30 00:00:00, Result: 2014-03-29 23:00:00
echo "Test #4: ".date("Y-m-d H:i:s", $d2 - 86400).PHP_EOL;

?>

Which gives the following results:

date.timezone = Europe/Rome
Test #1: 90000
Test #2: 2013-10-27 23:00:00
Test #3: 82800
Test #4: 2014-03-29 23:00:00

Therefore, it seems that one day not always has 86400 seconds; at least not twice a year...

  • 1
    Very interesting results with calculating 1 day forward. An good note on being aware of DST. – Drew Baker Oct 23 '13 at 21:03
  • This is looking like the best answer to me, followed by the one by Uours. I like this because it is compact, although it has a level of sophistication to it that might be above some programers (but I can't hold that against it). Does anyone see any issues with this? Seems to work well for me, providing the timezone is set correct. – Drew Baker Oct 25 '13 at 03:58
  • Doesn't seem to work for opening hours like monday 5pm - 2am. it will say closed for tuesday 1am while it should be open. – Daniel P Oct 26 '13 at 11:59
  • @DanielP You're right; thank you for pointing it out. I've slightly modified the code, so it should be fixed now. –  Oct 26 '13 at 20:22
  • Wasn't sure at first but if you have 5pm - 2am on sunday and it's 1am monday it will also say closed while it's supposed to be open. I am also wondering what would happen if the construct is called sunday (let's say right before midnight) and that 'isOpen' is called a little later while it's monday. I think it might be wise to include something where the construct is called again if for some reason the day has changed. – Daniel P Oct 26 '13 at 23:15
  • If you set all times to 6:30am - 12:00am and the current time is 5:30pm, then it gives this error: "Undefined offset: 1 at line = if (($d >= $t[$i]) && ($d <= $t[$i+1])) {" – Drew Baker Oct 27 '13 at 00:32
  • @DanielP It should be fixed now (at least, I hope so...). –  Oct 27 '13 at 10:48
  • @DrewBaker I've tried to set all times to `6:30am - 12:00am` and I don't get any error. Could you please provide your sample `$times` array? –  Oct 27 '13 at 10:55
  • Still has the problem with Sunday `'opening_hours_sun' => '9am - 2am'` and testing for 1am Monday gives me closed. This is due to the fact that the dates are calculated from Monday to Sunday and if Monday is used you have no previous day. Try setting up your data from Sunday to Sunday since opening hours of each day also can depend on the previous day. – Daniel P Oct 27 '13 at 12:55
  • Yeah it now gives me an error saying the time is invalid. I setup a PHPFiddle here: http://phpfiddle.org/main/code/qxb-xh6 – Drew Baker Oct 28 '13 at 00:54
  • @DrewBaker you have a invalid/invisible character right after the `-` (hex value 9D). Use your arrow keys move cursor past the `-` and you will see. Try retyping instead of copy/pasting ;) – Daniel P Oct 28 '13 at 01:44
  • @Daniel P good spotting! Totally strange, no idea how that got in there! Fixed and working! – Drew Baker Oct 28 '13 at 16:18
  • If it is now 7:00pm and the period is "8:00am - 3:00pm, 5:30pm - 11:00pm" it says closed. – Drew Baker Nov 01 '13 at 02:09
2

Suppose that, instead of such an array, we had another with the following kind of entries:

Array ( [from] => 1382335200 [to] => 1382374800 )

The from and to values are timestamps, calculated by projecting your array's information to the current (running) week.

Then, in order to check if the restaurant was open right now, we would have to do something as simple as:

$slots=..... /* calculate time slots array */
$status='closed';
$rightnow=time();
foreach($slots as $slot)
  if($rightnow<=$slot['to'])
    {
    if($rightnow>=$slot['from']) $status='open';
    break;
    }
echo "The restaurant is <strong>$status</strong> right now<br>";

Given a weekday, in the form of mon, tue, wed etc and two strings that define a time range, for example 8:30am and 3:15pm, the following function will return the corresponding time slot, as described above:

function get_time_slot($weekday,$fromtime,$totime)
  {
  $from_ts=strtotime("this week $weekday $fromtime");
  $to_ts=strtotime("this week $weekday $totime");
  if($to_ts<$from_ts)
    {
    $to_ts=strtotime("this week $weekday +1 day $totime");
    if($to_ts>strtotime("next week midnight")) 
      $to_ts=strtotime("this week mon $totime");
    }
  return array('from'=>$from_ts,'to'=>$to_ts);
  }

strtotime() can work miracles, huh? Notice that, if the end of the timeslot turns out to be earlier than the start, we assume that it refers to the next day, and we re-calculate it as such.

EDIT: At first, I naively thought I'd correct it by adding a day's worth of seconds. That wasn't exactly accurate, since manipulating a timestamp doesn't preserve DST information. So if a timeslot included a day shift (midnight) and also a DST shift, it would give inaccurate results by an hour. Using strtotime() all over again, with the same argument plus a day, would set it straight.

yaEDIT: Another bug (hopefully the last one) fixed: When a restaurant is open on Sundays until after midnight, $to_time should wrap to this week's monday, same time. Phew!

Now, in order to transform your array, you would need to do:

$slots=array();
foreach($times as $key=>$entry)
  {
  list(,,$dow)=explode('_',$key);
  foreach(explode(',',$entry) as $d)
    {
    $arr=explode('-',$d);
    if(count($arr)==2) $slots[]=get_time_slot($dow,$arr[0],$arr[1]);
    }
  }

Here's a little phpfiddle to demonstrate this.


EDIT: Motivated by the "conciseness" discussion in another answer, I thought I'd give my "compact" version. Using the exact same logic, it boils down to the following:
$status='closed';
$rightnow=time();
foreach($times as $key=>$entry)
  {
  list(,,$dow)=explode('_',$key);
  foreach(explode(',',$entry) as $d)
    if(count($arr=explode('-',$d))==2)
      {
      $from_ts=strtotime("this week $dow {$arr[0]}");
      $to_ts=strtotime("this week $dow {$arr[1]}");
      if($to_ts<$from_ts) $to_ts=strtotime("this week $dow +1 day {$arr[1]}");
        {
        $to_ts=strtotime("this week $dow +1 day {$arr[1]}");
        if($to_ts>strtotime("next week midnight")) 
          $to_ts=strtotime("this week mon {$arr[1]}");
        }
      if($rightnow<=$to_ts)
        {
        if($rightnow>=$from_ts) $status='open';
        break 2; // break both loops
        }
      }
  }
echo "<hr>The restaurant is <strong>$status</strong> right now<br>";

However, I myself still prefer the original version. Besides the obvious benefits of having a function, the $slots array could very well be cached and re-used, making relevant calculations much easier than parsing the original data all over again.

geomagas
  • 3,230
  • 1
  • 17
  • 27
  • Interesting, but I can't change the way the data is formatted. Or I'd prefer not, because it will be user generated or imported from yelp. – Drew Baker Oct 23 '13 at 21:05
  • @DrewBaker: Yes, that's the whole idea. You transform the data _only for the calculation_, in a separate array `$slots`. Other than that, your data stay as they lay. This is exactly for the purpose of receiving the data the way you do, without any suggestion to change your existing application logic. – geomagas Oct 24 '13 at 06:41
  • I think you make a lot of unnecessary work - you're check all diapasons, but us interesting particular day – CreatoR Oct 24 '13 at 11:18
  • @CreatoR: "A lot" compared to what? And I don't check all weekdays, if that's what you mean. Only up to the point where I still have a chance to make `$status` equal to `open`. The overhead is insignificant for such data, if you can call it an overhead at all. – geomagas Oct 24 '13 at 11:33
  • @geomagas I don't call it overhead data, but your script will check all weekdays in Sunday, less in Saturday. Your "chance" will be only in Monday and end checking in the beginning. Imagine if database contains very big numbers of restaurants and in Sunday were real hell for your script :) I just call it an unnecessary action when we have particular day. – CreatoR Oct 24 '13 at 11:48
  • @CreatoR: What are you talking about? This is a check for a single restaurant. If the database contains data for multiple ones, I assume we already have _isolated_ the one we are interested in. So where's the "hell"? If you cache the `$slots` array, which by the way could be cached for _all requests made within up to a week_, then you would only have an average of 7 _int_ comparisons per request, in the _worst_ case that a restaurant would have 2 timeslots every single day! Compared to running the orig. array all over again, even when targeting weekdays, _this is optimal in the long run_. – geomagas Oct 24 '13 at 12:00
  • @CreatoR: I've decided to prove the above, so that there's no doubt about the efficiency of the solution. Since you avoid comparing my answer to yours, but you certainly imply so, I took the liberty to benchmark each solution. The results: [My implementation is around seven times faster](http://phpfiddle.org/main/code/hus-y2t)! So I rest my case. Oh, BTW, I discovered, in the process, that your solution doesn't quite work. But I'll comment on your answer about that. – geomagas Oct 24 '13 at 15:12
  • @geomagas I said in generally: less unnecessary action - more performance. About your test: you're right - need convert work hours ones and than using 100000 times, but I said about many restaurants (100000 if you want) and for calculation you need transform each of their hours. Here my [truly test of performance](http://phpfiddle.org/main/code/379-xcn) and my solution 5 times faster than yours (I saw your comment about not working my solution, I'll check it tomorrow, thanks) – CreatoR Oct 24 '13 at 22:36
  • @CreatoR: No this is wrong. You are **purposely manipulating my code to make it look bad** (there's obviously no need to re-create the slots array at each iteration, _for heaven's sake!!!_). You are trying to **mislead**, and that's just **mean**. People using your ways should never have touched a keyboard in their lives. – geomagas Oct 24 '13 at 23:13
2

Weeks timings array

$times = array(
    'opening_hours_mon' => '9am - 8pm',
    'opening_hours_tue' => '9am - 2am',
    'opening_hours_wed' => '8:30am - 2am',
    'opening_hours_thu' => '9am - 3pm',
    'opening_hours_fri' => '8:30am - 11am',
    'opening_hours_sat' => '9am - 3pm, 5pm - 2am',
    'opening_hours_sun' => 'closed'
);

Usage Example :

ini_set( "date.timezone", "Pacific/Auckland" ); // Make sure correct timezone is set

echo ( isOpen( $times ) ? 'Open' : 'Closed' );    

echo ( isOpen( $times ,"10am" ) ? 'Open' : 'Closed' );

Function definition :

/*
 *  First parameter : Weeks timings as array
 *  Second parameter : Time to check as string
 *  Return value : boolean
 */
function isOpen( $times ,$timeToCheck = 'now' )
{
    $timeToCheckAsUnixTimestamp = strtotime( $timeToCheck );

    $yesterdayTimes = $todayTimes = '';
    //  Find yesterday's times and today's times
    foreach( $times as $day => $timeRange )
    {
        $yesterdayTimes = ( stripos( $day ,date( "D" ,time() - 60 * 60 * 24 ) ) !== false ? $timeRange : $yesterdayTimes );
        $todayTimes = ( stripos( $day ,date( "D" ) ) !== false ? $timeRange : $todayTimes );
    }
    //  Handle closed
    if( strcasecmp( $todayTimes ,'closed' ) == 0 ) return false;
    if( strcasecmp( $yesterdayTimes ,'closed' ) == 0 ) $yesterdayTimes = '12am - 12am';
    //  Process and check with yesterday's timings
    foreach( explode( ',' ,$yesterdayTimes ) as $timeRanges )
    {
        list( $from ,$to ) = explode( '-' ,$timeRanges );
        list( $fromAsUnixTimestamp ,$toAsUnixTimestamp ) = array( strtotime( $from .' previous day' ) ,strtotime( $to .' previous day'  ) );
        $toAsUnixTimestamp = ( $toAsUnixTimestamp < $fromAsUnixTimestamp ? strtotime( $to ) : $toAsUnixTimestamp );
        if( $fromAsUnixTimestamp <= $timeToCheckAsUnixTimestamp and $timeToCheckAsUnixTimestamp <= $toAsUnixTimestamp ) return true;
    }
    //  Process and check with today's timings
    foreach( explode( ',' ,$todayTimes ) as $timeRanges )
    {
        list( $from ,$to ) = explode( '-' ,$timeRanges );
        list( $fromAsUnixTimestamp ,$toAsUnixTimestamp ) = array( strtotime( $from ) ,strtotime( $to ) );
        $toAsUnixTimestamp = ( $toAsUnixTimestamp < $fromAsUnixTimestamp ? strtotime( $to .' next day' ) : $toAsUnixTimestamp );
        if( $fromAsUnixTimestamp <= $timeToCheckAsUnixTimestamp and $timeToCheckAsUnixTimestamp <= $toAsUnixTimestamp ) return true;
    }
    return false;
}
Uours
  • 2,517
  • 1
  • 16
  • 21
  • This answer is very complex, I worry it would be very hard to maintain compared to some of the other solutions here. – Drew Baker Oct 23 '13 at 21:06
  • I have tested it up to an extent and seems to work flawlessly but sure it is lengthy and looks complex . Probably the lengthiest solution ! – Uours Oct 23 '13 at 22:41
  • Updated with far shorter and less complex version . – Uours Oct 25 '13 at 03:35
  • Just tired it now (8:40pm) on a restaurant listed as 5:30pm - 2am and it showed it incorrectly as closed. – Drew Baker Oct 25 '13 at 03:41
  • 1
    Hmmm , it worked for me for that timings . Just in case , have you modified the timezone to your timezone ? – Uours Oct 25 '13 at 03:49
  • OK, that seemed to work. This is my new favorite answer! Still testing the one using the DateTime class. – Drew Baker Oct 25 '13 at 03:53
  • I like this because it is compact, doesn't need helper functions. It's between this and the DateTime answer from Enzino. – Drew Baker Oct 25 '13 at 04:01
  • Cool thanks ... After my initial lengthiest solution , I tried to think in what minimal steps it can be achieved . I haven't compared it with others solutions though but glad that you thought it is compact . Cheers , ... – Uours Oct 25 '13 at 04:09
  • Tried is now (6pm) on a time of 6:30am - 12:00am and is says it was closed. Also got a "Undefined offset: 1" in the last foreach when a place was closed today (sunday) – Drew Baker Oct 28 '13 at 01:01
  • `6pm` on a day with open time as `6:30am - 12:00am` showed me as `Open` ! I have corrected the `"Undefined offset: 1"` issue which is related to the `Closed` day(s) . – Uours Oct 29 '13 at 00:42
1

This is probably not the most efficient, but it should work well for your the problem at hand:

$times = array(
    'opening_hours_mon' => '9am - 8pm',
    'opening_hours_tue' => '9am - 2am',
    'opening_hours_wed' => '8:30am - 2am',
    'opening_hours_thu' => '9am - 3pm',
    'opening_hours_fri' => '8:30am - 11am',
    'opening_hours_sat' => '9am - 3pm, 5pm - 2am',
    'opening_hours_sun' => 'closed'
);

var_dump(is_open($times, strtotime('sun 1am'))); // true

Here's the first function, simple in design; it uses a grid of opening and closing times and determines whether the given time matches any of the ranges:

function is_open($times, $now)
{
    $today = strtotime('today', $now);

    $grid = get_time_grid($times);
    $today_name = strtolower(date('D', $today));
    $today_seconds = $now - $today;

    foreach ($grid[$today_name] as $range) {
        if ($today_seconds >= $range[0] && $today_seconds < $range[1]) {
            return true;
        }
    }

    return false;
}

This function builds the actual grid; if a range ending comes before its corresponding start it will create two ranges, one for each day that's being spanned.

function get_time_grid($times)
{
    static $next_day = array(
        'mon' => 'tue', 'tue' => 'wed', 'wed' => 'thu',
        'thu' => 'fri', 'fri' => 'sat', 'sat' => 'sun',
        'sun' => 'mon'
    );
    static $time_r = '(\d{1,2}(?::\d{2})?(?:am|pm))';

    $today = strtotime('today');
    $grid = [];

    foreach ($times as $key => $schedule) {
        $day_name = substr($key, -3);
        // match all time ranges, skips "closed"
        preg_match_all("/$time_r - $time_r/", $schedule, $slots, PREG_SET_ORDER);
        foreach ($slots as $slot) {
            $from = strtotime($slot[1], $today) - $today;
            $to = strtotime($slot[2], $today) - $today;

            if ($to < $from) { // spans two days
                $grid[$day_name][] = [$from, 86400];
                $grid[$next_day[$day_name]][] = [0, $to];
            } else { // normal range
                $grid[$day_name][] = [$from, $to];
            }
        }
    }

    return $grid;
}

There are only a few comments in the code, but I hope you can follow what's being done. Let me know if you need any clarification.

Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
1

I have done something similar in the past but took a totally different approach. I stored my opening hours in a separate table.

CREATE TABLE `Openinghours`
(
    `OpeninghoursID` int, // serial
    `RestaurantID` int, // foreign key
    `Dayofweek` int, // day of week : 0 (for Sunday) through 6 (for Saturday)
    `Opentime` int, // time of day when restaurant opens (in seconds)
    `Closetime` int // time of day when restaurant closes (in seconds)
);

If a restaurant has more than one opening period per day you can just add 2 records (or more id needed). The nice thing of using such a table is that you can simply query to see which restaurants are open.

$day = date('w');
$now = time()-strtotime("00:00");
$query = "Select `RestaurantID` from `Openinghours` where `Dayofweek` = ".$day." and `Opentime` <= ".$now." and `Closetime` > ".$now;

Another beauty of using such a system is that you can adapt you query to get different results like for example : what restaurants are open now and stay open for at least another hour (no point going to a restaurant a few minutes before closing)

$day = date('w');
$now = time()-strtotime("00:00");
$query = "Select `RestaurantID` from `Openinghours` where `Dayofweek` = ".$day." and `Opentime` <= ".$now." and `Closetime` > ".($now+3600);

Of course it needs reformatting your current data but it brings nice features.

Daniel P
  • 471
  • 3
  • 9
1

Here is another solution without needing to reformat your data.

$times = array(
    'opening_hours_mon' => '9am - 8pm',
    'opening_hours_tue' => '9am - 2am',
    'opening_hours_wed' => '8:30am - 2am',
    'opening_hours_thu' => '5:30pm - 2am',
    'opening_hours_fri' => '8:30am - 11am',
    'opening_hours_sat' => '9am - 3pm, 5pm - 2am',
    'opening_hours_sun' => 'closed'
);

function compileHours($times, $timestamp) {
    $times = $times['opening_hours_'.strtolower(date('D',$timestamp))];
    if(!strpos($times, '-')) return array();
    $hours = explode(",", $times);
    $hours = array_map('explode', array_pad(array(),count($hours),'-'), $hours);
    $hours = array_map('array_map', array_pad(array(),count($hours),'strtotime'), $hours, array_pad(array(),count($hours),array_pad(array(),2,$timestamp)));
    end($hours);
    if ($hours[key($hours)][0] > $hours[key($hours)][1]) $hours[key($hours)][1] = strtotime('+1 day', $hours[key($hours)][1]);
    return $hours;
}

function isOpen($now, $times) {
    $open = 0; // time until closing in seconds or 0 if closed
    // merge opening hours of today and the day before
    $hours = array_merge(compileHours($times, strtotime('yesterday',$now)),compileHours($times, $now)); 

    foreach ($hours as $h) {
        if ($now >= $h[0] and $now < $h[1]) {
            $open = $h[1] - $now;
            return $open;
        } 
    }
    return $open;
}

$now = strtotime('7:59pm');
$open = isOpen($now, $times);

if ($open == 0) {
    echo "Is closed";
} else {
    echo "Is open. Will close in ".ceil($open/60)." minutes";
}

?>

I ran several test and it seems to work as expected taking all aspects that I could think off. let me know if you find a problem with this one. I know the approach looks a little nasty but I wanted to use simple functions only (well except for the tricky part with array_map) and keep it as short as possible.

Daniel P
  • 471
  • 3
  • 9
  • 1
    This does seem very concise. I'll test it out. – Drew Baker Oct 26 '13 at 18:23
  • For testing purpose you can adjust the line `$now = time();` and if you want a human readable output of the array just add `foreach ($hours as $h) { echo date("d H:i:s",$h[0]).' - '.date("d H:i:s",$h[1]).'
    '; }` at the end.
    – Daniel P Oct 26 '13 at 22:08
  • Hey @Daniel P could you include some code about how to call this function? I'm not sure how to get a "open" or "closed" status out of this. – Drew Baker Oct 28 '13 at 16:20
  • Slightly changed the code to make the functionality more obvious (I hope). The returned value of the function `isOpen` is equal to 0 when closed and when open it will reflect the number of seconds left until closing, this can be handy to make people aware that it's near closing. – Daniel P Oct 28 '13 at 21:15
0

If you use a database for this why you didn't use datetime for this.

Sample:

sunday 14:28, saturday 1:28

You can split this two part and compare them, in string time (part 2). You can use strtotime to convert string time to timestamp and compare it.

Sample:

$date = "sunday 14:28"; 
echo $stamp = strtotime($date);

Output:

1360492200

Like this code:

$Time="sunday  14:28 , saturday 1:28";
$tA=explode(",",$Time);
$start=strtotime($tA[0]);
$end=strtotime($tA[1]);
$now=time();
if($now>$start and $now<$end){
   echo "is open";
}else{
   echo "is close";
}

But you have a problem with update them you can do this.

halfer
  • 19,824
  • 17
  • 99
  • 186
mohammad mohsenipur
  • 3,218
  • 2
  • 17
  • 22
0

Here is my solution:

INPUT DATA:

$meta = array(
   'opening_hours_mon' => '9am - 8pm',
   'opening_hours_tue' => '9am - 2am',
   'opening_hours_wed' => '8:30am - 2am',
   'opening_hours_thu' => '9am - 3pm',
   'opening_hours_fri' => '8:30am - 11am',
   'opening_hours_sat' => '9am - 3pm, 5pm - 2am',
   'opening_hours_sun' => 'closed'

);

current_time( 'timestamp' ) (as said author) analog of time() in WordPress
And solution:

    $now = (int) current_time( 'timestamp' );
    $day = strtolower(date('D', $now));
    $yesterday = strtolower(date('D', strtotime('-1 day')));
    $days = array(
        'yesterday' => $meta['opening_hours_'.$yesterday],
        'today' => $meta['opening_hours_'.$day],
    );
    $status = false;
    foreach($days as $when=>$times)
    {
        $parts = explode(',',$times);
        foreach($parts as $p)
        {
            if($p == 'closed')
                break;
            else{
                list($b,$e) = explode('-',$p);
                $b = strtotime("$when $b");
                $e = strtotime("$when $e");
                if($b > $e)
                    $e += strtotime("$when $e +1 day");;
                if($b <= $now && $now <= $e)
                {
                    $status =true;
                    break;
                }
            }
        }
    }

FOR TESTING:
you may change first 3 lines to followings:

$now = (int) strtotime('today 3:00am');
$day = strtolower(date('D', $now));
$yesterday = strtolower(date('D', strtotime('yesterday 3:00am')));
CreatoR
  • 1,654
  • 10
  • 14
  • This answer seems so much more concise than the rest. Does anyone see any issues with this? – Drew Baker Oct 23 '13 at 20:59
  • I'll test it and it's works :) You can try is this way: you can set custom `$yesterday = strtolower(date('D', strtotime('-1 day')));` and insert the code `$now = strtotime('-2 days 6:00pm');` for manual setting current time. – CreatoR Oct 23 '13 at 21:27
  • @DrewBaker Unless I'm missing something, the code doesn't work. current_time is an undefined function, $meta is an undefined array, $days is empty, and it is currently telling me it's closed when it should be open. Not sure if CreatoR forgot to include some more code? – BadHorsie Oct 24 '13 at 09:32
  • $meta = $times from author's post, `current_time` - is function of Wordpress. I update post and explain how you may test it – CreatoR Oct 24 '13 at 09:53
  • 1
    @DrewBaker: Yes, a minor one: Adding 24*3600 seconds doesn't accurately render to the correct timestamp, in the rare case of a timeslot containing a midnight _and_ a DST shift. I've just figured this out myself, and edited my answer accordingly -- hope this helps everyone! – geomagas Oct 24 '13 at 10:15
  • @CreatoR +1 on this answer. It works very well. I think it's the most concise answer, bar the potential DST issue mentioned by the other guys. – BadHorsie Oct 24 '13 at 10:21
  • Thanks for geomagas found problem and BadHorsie for testing. You are right! I'm fully forget about DST. I correct solution – CreatoR Oct 24 '13 at 11:08
  • Glad I could help. And an offtopic tip: Don't forget to prepend usernames with `@`, so that the user is notified about your response. Cheers! – geomagas Oct 24 '13 at 11:14
  • I just found out: The changes you made broke it. I tested it with `$now = strtotime('2013-10-26 07:14:57pm');` and `$yesterday = strtolower(date('D', strtotime('2013-10-25 07:14:57pm')));` and it gave me `false`... [Here you go](http://phpfiddle.org/main/code/qnc-rz6). – geomagas Oct 24 '13 at 15:15
0

Solution without change of time stored format

<?php
    $times = array(
        'opening_hours_mon' => '9am - 8pm',
        'opening_hours_tue' => '5pm - 2am',
        'opening_hours_wed' => '8:30am - 2am',
        'opening_hours_thu' => '9am - 3pm',
        'opening_hours_fri' => '8:30am - 11am',
        'opening_hours_sat' => '9am - 3pm, 5pm - 2am',
        'opening_hours_sun' => 'closed'
    );

    //$time_go = '13:00';
    $time_go = date('H:i');

    //$day_go = 1; //monday
    $day_go = (date('N') - 1);

    if(Are_they_open($time_go, $day_go, $times)){
        echo 'jep';
    }
    else{
        echo 'nope';
    }

    function Are_they_open($time_go, $day_go, $times){
        // the magic
        $times = array_values($times);
        $day_go = explode(',', $times[$day_go]);
        $time_go = hh_mm_toSeconds($time_go);

        foreach($day_go as $time){

            if((!$time) || ($time == 'closed')){
                return false;
            }

            $time = explode(' - ', $time);
            $time_open = hh_mm_toSeconds(date('H:i', strtotime($time[0])));
            $time_close = hh_mm_toSeconds(date('H:i', strtotime($time[1])));

            if(($time_go > $time_open) && ($time_go < $time_close)){
                return true;
            }
            elseif (($time_open > $time_close) || ($time_go > $time_open)){
                return true;
            }

        }

        return false;
    }

    function hh_mm_toSeconds($str_time){
        sscanf($str_time, "%d:%d", $hours, $minutes);
        return ($hours * 3600) + ($minutes * 60);
    }
?>

Solution where you change Time format

$times = array(
    1 => array(
        array('07:00', '17:00')
    ),
    2 => array(
        array('07:00', '14:30'),
        array('15:00', '20:00')
    ),
    3 => array(
        array('07:00', '17:00')
    ),
    4 => false, //closed
    5 => array(
        array('07:00', '17:00'),
        array('20:00', '24:00')
    ),
    6 => array(
        array('00:00', '03:00'),
        array('07:00', '17:00'),
        array('20:00', '24:00')
    ),
    7 => array(
        array('00:00', '03:00')
    ),
);
botenvouwer
  • 4,334
  • 9
  • 46
  • 75
  • Normalization, concept, reasoning, logic or however you want to say it. If you do not agree with me then come with some arguments. – botenvouwer Oct 22 '13 at 10:28
  • 1
    The OP poses a straightforward, non-ambiguously stated problem, unlike a quite large amount of questions. If _you_ think the question doesn't live up to your answer, _you_ will have to come up with something more convincing than "_your algorithm is wrong because I don't like the format of your data_". – geomagas Oct 22 '13 at 10:46
  • 2
    @sirwilliam You are trying to force the user to think like a programmer. OP has already said that it is not ideal to change the format of the data. It may also be the case that the opening times were entered as they would be displayed to customers (possibly by a non-programmer), so you cannot expect that user to be entering '09:00:00 - 11:59:59' or whatever, because it doesn't fit your code. The data is not ideally formatted, we all know that, but your solution should fit the problem at hand, not the other way around. – BadHorsie Oct 22 '13 at 12:54
  • @BadHorse Well if I had to made a similar system I would let the user fill in 11:30 to 1:45 And then reformat that in to 11:30 to 24:00 next day 00:00 to 1:45. Sometimes we have to change the way we think to make working processes more efficient. But thats just my opinion. Have you actually tested my solution and then I mean the code I wrote for DrewBaker. It works. – botenvouwer Oct 22 '13 at 14:28
0

Pass Your array as argument to this function and You will get true for open and false for closed at current time. It is simple straightforward function. I just check today opening times and if necessary yesterday without unnecessary looping through all week days. Maybe it could be improved a bit, but it works and is not complicated.

function isOpen($times) {
    $times = array_values($times); //we will use numeric indexes
    $now = new DateTime();
    $day = $now->format('N'); 
    $day--; //days are counted 1 to 7 so we decrement it to match indexes
    $period = $times[$day];
    if($period!='closed') {
        $opening = explode('-', $period);
        $open = new DateTime($opening[0]);
        $close = new DateTime($opening[1]);
        if($close<$open) {
            //it means today we close after midnight, it is tomorrow
            $close->add(new DateInterval('P1D'));
        }
        if($open<$now && $now<$close) {
            //we are open
            return true;
        }
    }
    if($period=='closed' || $now<$open) {
        //now we check if we still open since yesterday
        $day = $day==0 ? 6 : $day-1;
        $period = $times[$day];
        if($period=='closed') return false;
        $opening = explode(' - ', $period);
        $open = new DateTime($opening[0]);
        $close = new DateTime($opening[1]);
        if($close<$open) {
        //it means yesterday we closed after midnight
            if($now<$close) {
                //we are before closing time
                return true;
            }
        }
    }
    return false;
}
Gustek
  • 3,680
  • 2
  • 22
  • 36
0

Maybe I'm not understanding this completely, but going through a basic loop, maybe this could work:

if( ($date('D') == "3" ) )
{
    $open = "8";
    $close = "17";
    $now = time();

    if( ($now > $open) and ($now < $close) )
    {
        echo "open";
    }
    else
    {
        echo "closed";
    }
}

Perhaps not the best way to go around it since this would not include holidays and such, and requires a few conditional statements, but I think this could work. Well, not optimal, but always worked for me.

davewoodhall
  • 998
  • 3
  • 18
  • 43