Try tackling the problem from a different direction. Take the following as a starting point, not everything is explained - if you say don't know what a dictionary is then you should research it.
Looking At The Problem
You have a table of opening and closing times, checking whether your shop is open should be a lookup into this table - just as you would in "real life". To know if the shop is open you need to know the weekday - which tells you which line of your table to consult, and the time - which you compare against the two times in that line of the table.
To represent a table in a program you typically use an array, to represent two associated times - such as the open & close times - you might use a record, object or dictionary etc.
How can you represent a time of day? Well general time & date calculations are complicated, but after the weekday all you need to know is the time in that day, and assuming you are not worried about leap seconds (you're not) you can assume there are 24 hours of 60 minutes each in a day so you can store the time as the number of minutes since midnight - giving you a single number. If you use the number of minutes since midnight to determine whether the shop is open or closed you can avoid complicated date comparisons.
Some Code
As your code already already shows you can use NSCalendar
to obtain the weekeday, hours and minutes of any NSDate
.
How to get the hours and minutes to minutes since midnight, well that is simple arithmetic but you'll want to do it a few times so maybe a simple macro to convert a time in hours and minutes:
#define TO_MINUTES(hour, min) (hour * 60 + min)
How to represent the table of opening times?
Well you could use an NSArray
, indexed by the weekday, where each element is an NSDictionary
containing two key/value pairs - for the open and closing times. However your times are just integers, the number of minutes since midnight, and storing integers in an NSDictionary
requires wrapping them as a NSNumber
objects. (If that doesn't make sense, time to do some research!)
Another approach would be to use a C-style array and structure for your table - this will work quite well as you are only storing integers.
Here is a C structure definition to represent the opening and closing times:
typedef struct
{
NSInteger openTime;
NSInteger closeTime;
} ShopHours;
and with that and the above macro you can easily define a constant array representing the shop hours:
ShopHours WeekSchedule[] =
{
{0, 0}, // index 0 - ignore
{ TO_MINUTES(9, 0), TO_MINUTES(24, 0) }, // index 1 - Sunday
{ TO_MINUTES(7, 30), TO_MINUTES(24, 0) }, // index 2 - Monday
...
{ TO_MINUTES(9, 0), TO_MINUTES(22, 0) }, // index 7 - Saturday
};
In real code you might read this in from a data file, but the above global array will do for now.
(Note that index 0 is ignored - NSDateComponents
number the days starting from 1 for Sunday and arrays (both C-style and NSArray
) are indexed from 0, simply ignoring the zeroth element avoids do - 1
's in your code.)
You already have code to break an NSDate
into NSDateComponents
, using that you can get the weekday and minutes since midnight easily:
NSInteger weekday = comps.weekday;
NSInteger minutes = TO_MINUTES(comps.hour, comps.minute);
use weekday
to index the WeekSchedule
table, and compare minutes
to the two entries and you are done, e.g. is the shop open:
if (minutes >= WeekSchedule[weekday].openTime && minutes <= WeekSchedule[weekday].closeTime)
{
// shop is open...
}
else
{
// shop is closed...
}
You can wrap the above into a method which given a date tells you the state of the shop:
- (NSString *) shopState:(NSDate *)dateAndTime
{
// break out the weekday, hours and minutes...
// your code from the question
NSInteger weekday = comps.weekday;
NSInteger minutes = TO_MINUTES(comps.hour, comps.minute);
if (minutes >= WeekSchedule[weekday].openTime && minutes <= WeekSchedule[weekday].closeTime)
{
// shop is open...
// determine if its closing within 30 mins and return an appropriate string
}
else
{
// shop is closed...
return @"Closed";
}
}
As you'll see this solution is a lot shorter than the approach you took.
HTH
Addendum - Refinements
As Rob Napier has pointed out in comments and in case its not obvious, the above outline of a solution is just that and omits cases such as shops being open over midnight. Here are some things you might want to consider:
Shops open for more than one period per day: Some shops close over lunch, restaurants may open for lunch and evenings, etc. To handle this you need a list of open/close times per day rather than just a single pair. Once you have determined the weekday testing is a matter of iterating over such a list.
Shops open across midnight: This is just a special case of (1), think about it...
Time zones: In your code in the question and the code in this answer it is assumed that the open/close times and the time being tested are all from the same time zone. If you wish to support, say, a person in Canada determining whether a shop in Germany is currently open and can be phoned you need to allow for the time difference.
Daylight Saving Time: This is the corner case Rob mentions in the comments. When a DST change occurs an hour might be skipped or repeat. This is only an issue if you support shops which open/close in that hour - shops which are open right across the period of change need no special handling. NSCalendar
will give the correct hour/min from the time you are testing, you need to handle any adjustments to open/close times. For example consider a shop which closes at 2am, a DST change jumps 2am back to 1am, is the shop open at 1:30am? Yes the first time it comes around, but what about the second? Deciding this is an issue beyond time calculations.
You need to decide whether and how to address these.
Some More Hints
OK, so its Christmas (take that how you like - over eating slowing brains, time for gifts, etc. ;-))
I see you've asked in another question how to create the table dynamically rather than using a static one, so you have that covered.
Let's consider the multiple opening times day & open over midnight:
Arrays of arrays would work, but you could instead just keep an array of opening times for a whole week. E.g. change the TO_MINUTES
macro to take the day number as well and store all the times as the number of minutes since Sunday 0000hrs. Now instead of indexing the array to find the day you iterate or search it - the array is ordered so you could binary search if you wish, but a simple iteration is probably fast enough (how many open/close periods are there in a week?)
By setting the closing time to the next day (1) covers opening over midnight for everything but Sat -> Sun, including closing within 30 min calculations.
To handle Sat -> Sun first split the period into Sat night and Sun morning parts. Add them to your array, they will be the first (early Sunday morning) and last (late Sat night) entries. Now when you go to determine the minutes till closing check if the closing time is Sat midnight (e.g. TO_MINUTES(7, 24, 0)
), if so check if the first entry's opening time is Sunday 0000hrs, if so you want to adjust the closing time to do the 30 min check (add in the length of the first period).
That will handle multiple periods and open over midnight. It doesn't handle DST, shop holidays, etc. - you need to decided how much to handle. For DST use NSTimeZone
to find out when and by how much the times changes (its not always by 1 hour) to figure out the "repeated" and "missing" times - but remember this is only an issue of your shop actually opens/closes during those times.
It's New Year ;-)
Seriously Rob decided to give almost the complete code but using Objective-C objects and a number of methods so I thought I'd add my code for comparison because it raises and interesting issue.
What should be noted first is the similarity, algorithmically the two solutions are close - the wraparound is handled differently but either approach could do it either way so that is not significant.
The difference comes to the choice of data structure - should you use C structures and arrays for something this simple or Objective-C objects? The frameworks themselves have plenty of structure types - e.g. NSRect
et al - there is nothing wrong with using them in Objective-C code. The choice isn't black and white, there is a gray area where either might be suitable, and this problem probably falls in that gray area. So here's the multiple openings times/day solution:
// convenience macro
// day 1 = Sunday, ... 7 = Saturday
#define TO_MINUTES(day, hour, min) ((day * 24 + hour) * 60 + min)
#define WEEK_START TO_MINUTES(1, 0, 0)
#define WEEK_FINISH TO_MINUTES(7, 24, 0)
typedef struct
{ NSInteger openTime;
NSInteger closeTime;
} ShopHours;
// Opening hours
ShopHours WeekSchedule[] =
{ { TO_MINUTES(1, 0, 0), TO_MINUTES(1, 0, 15) }, // Sat night special, part of Sat 11:30pm - Sun 0:15am
{ TO_MINUTES(1, 9, 0), TO_MINUTES(1, 24, 0) }, // Sun 9am - Midnight
{ TO_MINUTES(2, 7, 30), TO_MINUTES(2, 24, 0) }, // Mon 7:30am - Midnight
{ TO_MINUTES(3, 7, 30), TO_MINUTES(3, 24, 0) },
{ TO_MINUTES(4, 7, 30), TO_MINUTES(5, 2, 0) }, // Midweek madness, Wed 7:30am - Thursday 2am
{ TO_MINUTES(5, 7, 30), TO_MINUTES(5, 24, 0) },
{ TO_MINUTES(6, 7, 30), TO_MINUTES(6, 22, 0) }, // Fri 7:30am - 10pm
{ TO_MINUTES(7, 9, 0), TO_MINUTES(7, 22, 0) }, // Sat 9am - 10pm
{ TO_MINUTES(7, 23, 30),TO_MINUTES(7, 24, 0) }, // Sat night special, part of Sat 11:30pm - Sun 0:15am
};
- (NSString *) shopState:(NSDate *)dateAndTime
{ NSCalendar *calendar = [NSCalendar currentCalendar];
const NSCalendarUnit units = NSWeekdayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit;
NSDateComponents *comps = [calendar components:units fromDate:dateAndTime];
NSInteger minutes = TO_MINUTES(comps.weekday, comps.hour, comps.minute);
NSLog(@"%ld (%ld, %ld, %ld)", minutes, comps.weekday, comps.hour, comps.minute);
unsigned periods = sizeof(WeekSchedule)/sizeof(ShopHours);
for (unsigned ix = 0; ix < periods; ix++)
{ if (minutes >= WeekSchedule[ix].openTime)
{ if (minutes < WeekSchedule[ix].closeTime)
{
// shop is open, how long till close time?
NSInteger closeTime = WeekSchedule[ix].closeTime;
// handle Sat -> Sun wraparound
if (closeTime == WEEK_FINISH && WeekSchedule[0].openTime == WEEK_START)
closeTime += WeekSchedule[0].closeTime - WEEK_START;
NSInteger closingIn = closeTime - minutes;
if (closingIn <= 30)
return [NSString stringWithFormat:@"Closes in %ld min", closingIn];
else
return @"Open";
}
}
else // minutes < WeekSchedule[ix].openTime
break;
}
return @"Closed";
}
The DST Issue
I first though this was a non-issue, Rob raised it in comments, and now he thinks its a non-issue, but it isn't - though its somewhat academic maybe.
It is a non-issue if you don't use NSDate
to represent the time being queried, and Rob's solution takes that route.
The original question and the code above does use NSDate
and breaks out the weekday, hour and minute from it as NSDateComponents
. Consider the situation where 2am becomes 1am due to DST change and the shop usually closes at 1:30am. If you start with an NSDate
value before 1am and increment, say by 10min each time, until you get a hour value from components:fromDate:
greater than 2 you'll see values like: 00:50, 01:00, 01:10, ..., 01:50, 01:00, 01:10, ..., 01:50, 02:00, 02:10. Testing each of these times will report the shop as closed for 30 mins after the first 01:30 is passed, then it will re-open for 30 mins until the next one is passed!
This issue only occurs if you start with an NSDate
, if you simple take a weekday/hour/min as your input then it does not occur. Either approach (struct or object) can operate either way, you just have to decide whether it is an issue you wish to address.