I have a bunch of hours of operation I am I want to determine whether the store is open, closed or is closing in 30,29,28,27... minutes I am doing this in Xcode/ Objectic-C. Now I have to do this for lets say 50 different hours of operation. I have made a function that does this but it is not very efficient and involves a lot of if-else statements. Here is a sample hours of operation
Monday - Thursday
7:30am - Midnight
Friday
7:30am - 10:00pm
Saturday
9:00am - 10:00pm
Sunday
9:00am - Midnight
And here is my function and how I handle it
-(BOOL) dateAndTime:(NSDate*)date getStartDay:(NSInteger)startDay getStartHour:(NSInteger)startHour getStartMin:(NSInteger)startMin getEndDay:(NSInteger)endDay getEndHour:(NSInteger)endHour getEndMin:(NSInteger)endMin{
NSCalendar *calendar = [NSCalendar currentCalendar];
const NSCalendarUnit units = NSWeekdayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit;
NSDateComponents *comps = [calendar components:units fromDate:date];
if (comps.weekday == 1) {
comps.weekday = 7;
}
else comps.weekday = comps.weekday - 2;
NSDate *startOfToday;
[[NSCalendar currentCalendar] rangeOfUnit:NSDayCalendarUnit startDate:&startOfToday interval:NULL forDate:date];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
NSTimeZone *timeZone = [NSTimeZone localTimeZone];
[dateFormatter setDateFormat:#"HH:mm"];
[dateFormatter setTimeZone:timeZone];
NSString *dateString = [dateFormatter stringFromDate:date];
NSDate *startDate = [dateFormatter dateFromString:[NSString stringWithFormat:#"%ld:%ld", (long)startHour, (long)startMin]];
NSDate *endDate = [dateFormatter dateFromString:[NSString stringWithFormat:#"%ld:%ld", (long)endHour, (long)endMin]];
NSString *startDateString = [dateFormatter stringFromDate:startDate];
NSString *endDateString = [dateFormatter stringFromDate:endDate];
if ([startDateString compare:dateString] == NSOrderedAscending && [dateString compare:endDateString] == NSOrderedAscending && startDay <= comps.weekday && comps.weekday <= endDay) {
return YES;
}
else return NO;
}
Now I pass in the day, from 0-6 (0 being monday) and then the time in 24-hour time. And then use it like this:
if ([self dateAndTime:date getStartDay:0 getStartHour:7 getStartMin:30 getEndDay:3 getEndHour:23 getEndMin:30] == YES)
text = #"Open";
else if ([self dateAndTime:date getStartDay:0 getStartHour:23 getStartMin:30 getEndDay:3 getEndHour:24 getEndMin:0] == YES)
text = [NSString stringWithFormat:#"Closes in %# min", countdownNumber];
else text = #"Closed";
As you can see to do this for all the days of the week and hours on each day it requires a lot of if statements and is very bad. Just for this one example it requires 8 if-else statements (VERY TIME CONSUMING)
Now the basis of this question is how can I make this much more efficient/ what is a better way to do this, while sill being able to have the countdown for the last 30 minutes?
I have done some research and can't find anything that has a countdown to when something is closing and is efficient.
Here is the full if-else statements for the example if you need it or want to see https://gist.github.com/spennyf/b0b18e31c3e9deaa0455
Thanks for the help and/or advice in advance :)
EDIT
This is at the top of my .m file I have this
#interface HomeTableViewController () {
ShopHours WeekSchedule[];
}
#end
But this gives me the compile error I talked about in the comments, how can I set this variable so it can be used throughout this one .m file? Right now I am just passing it is as an extra parapet for the function which should be fine. :)
And what would be the best way to set up the if statement for if the place is closing in 30 minutes once I have determined that it is open? And could you add the set up for if a place is open for parts of a day/ over midnight.
Thanks for all your help :)
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.
Storing your times as C structures creates a number of memory management headaches. I'd avoid that if you can (and I think you can). Instead, I recommend creating some new classes to help us out here.
First, we should think about these times as "nominal times within a week." By making these nominal times, we can explicitly get rid of DST concerns by saying that "1:30am on Sunday" means "the first instant that we would call 1:30am, no matter what DST transitions might happen to create another 1:30a." That's how shops usually work anyway, but it's important to be precise when thinking about time functions.
The great thing about "nominal times within a week" is that we can start counting minutes from Sunday at midnight, and we know that there will be exactly 60*24*7 (10,080) minutes in the week. We only really care here about minutes, so we just need to keep track of a number between 0 and 10,079. But we must remember that these numbers are subject to modular arithmetic (they "wrap around"). It is meaningless to ask whether Tuesday is before or after Wednesday. Tuesday is before and after Wednesday. But it's meaningful to ask whether Tuesday is between Monday (as a starting point) and Wednesday (as an ending point). We can determine if moving forward from Tuesday we will encounter Wednesday before we encounter Monday. If that's true, it's between. That's how you have to think about modular time.
OK, way too much theory. Let's look at some code (all the code is here). First we'd like a WeekTime to represent some time within a week:
typedef enum {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
} Weekday;
#interface WeekTime : NSObject
#property (nonatomic, readonly) NSInteger minutesSinceStartOfWeek;
- (instancetype)initWithWeekday:(Weekday)weekday hour:(NSInteger)hour minute:(NSInteger)minute;
#end
The impl of this should be obvious so I won't paste it here.
Now we want to think about ranges of nominal week times. We'll make these open ranges, so they include their start time but not their end time. If a place closes at 9p, you want to return "Closed" at 9p, not "Closing in 1m."
#interface WeekTimeRange : NSObject
#property (nonatomic, readonly) WeekTime *start;
#property (nonatomic, readonly) WeekTime *end;
- (instancetype)initWithStart:(WeekTime *)start end:(WeekTime *)end;
- (BOOL)contains:(WeekTime *)time;
#end
Most of this should be obvious, but contains: has one little trick to it. Since we're in modular arithmetic, it's legal for start to be "greater" than end (remembering that "greater" doesn't mean anything). That's easy to deal with:
- (BOOL)contains:(WeekTime *)time {
NSInteger queryMinutes = time.minutesSinceStartOfWeek;
NSInteger startMinutes = self.start.minutesSinceStartOfWeek;
NSInteger endMinutes = self.end.minutesSinceStartOfWeek;
if (startMinutes < endMinutes) {
return (startMinutes <= queryMinutes &&
queryMinutes < endMinutes);
} else {
return (queryMinutes < endMinutes ||
queryMinutes >= startMinutes);
}
}
(In my example I've chosen to make the range where start==end be undefined. If you want to assign some meaning to that, either meaning "all times" or "no times," then you may need to tweak this. Be careful of "all times," though. That really would mean the shop closes and immediately reopens, so you'll get "closing in..." messages. That's why I just made start==end be undefined.)
OK, finally we'll want a few helpers:
WeekTimeRange *WTMakeRange(Weekday startDay, NSInteger startHour, NSInteger startMinute,
Weekday endDay, NSInteger endHour, NSInteger endMinute) {
return [[WeekTimeRange alloc]
initWithStart:[[WeekTime alloc] initWithWeekday:startDay hour:startHour minute:startMinute]
end:[[WeekTime alloc] initWithWeekday:endDay hour:endHour minute:endMinute]];
}
WeekTimeRange *WTFindOpenRangeIncluding(NSArray *ranges, WeekTime *time) {
for (WeekTimeRange *range in ranges) {
if ([range contains:time]) {
return range;
}
}
return nil;
}
NSString *WTStatus(WeekTimeRange *range, WeekTime *requestedTime) {
if (range != nil) {
NSInteger minutesLeft = range.end.minutesSinceStartOfWeek - requestedTime.minutesSinceStartOfWeek;
if (minutesLeft < 0) {
minutesLeft += [WeekTime maxMinute];
}
if (minutesLeft <= 30) {
return [NSString stringWithFormat:#"Closing in %ld minutes", (long)minutesLeft];
} else {
return #"Open";
}
} else {
return #"Closed";
}
}
These shouldn't require much explanation. Let's see it in practice. You already seem to know how to turn dates into their components, so I'm going to just create a WeekTime directly and not worry about converting it from an NSDate.
NSArray *shopHours = #[WTMakeRange(Monday, 7, 30, Tuesday, 0, 0),
WTMakeRange(Tuesday, 7, 30, Wednesday, 0, 0),
WTMakeRange(Wednesday, 7, 30, Thursday, 0, 0),
WTMakeRange(Thursday, 7, 30, Friday, 0, 0),
WTMakeRange(Friday, 7, 30, Friday, 22, 0),
WTMakeRange(Saturday, 9, 0, Saturday, 22, 0),
WTMakeRange(Sunday, 9, 0, Monday, 2, 0)
];
WeekTime *mondayElevenPM = [[WeekTime alloc] initWithWeekday:Monday hour:23 minute:00];
WeekTimeRange *openRange = WTFindOpenRangeIncluding(shopHours, mondayElevenPM);
NSString *result = WTStatus(openRange, mondayElevenPM);
NSLog(#"%#", result);
I haven't tried tons of test cases, but my ad hoc tests seem to work. All the code is in the gist. One case I don't test for is whether your ranges overlap. The above algorithm doesn't care if the ranges are in order, but you may get incorrect "closing in..." messages if the ranges overlap. Resolving that (either by throwing an error, or by merging ranges) should be an pretty easy enhancement.
Instead of having the method dateAndTime with all of those arguments create a custom date object and set the properties you create on that object with values like so:
#interface MyDateModelObject : NSObject
#property (nonatomic, strong) NSDate *startDay;
#property (nonatomic, assign) NSInteger startHour;
And so on...
Turning the dateAndTime method to:
- (BOOL)dateAndTime:(MyDateModelObject *)myDateModelObject
Now getting into dateAndTime's code. This method is doing too much. To follow the Single Responsibility Principle I would remove code like the NSDateFormatter and the date comparison code into their own methods into a utility methods.
With that said I would go further and rename dateAndTime to the following and add your gist code inside it:
- (NSString *)retrieveStoreState:(MyDateModelObject *)date
Now in your code you can create a model object and set it's properties, pass it to retrieveStoreState and make it return just the state based on the logic you have in your gist.
Hopefully this gives you a good direction to go in. If you follow the SRP you should be able to clean your code and spruce your gist up a bit making it more readable.
This question already has answers here:
iOS: Compare two dates
(6 answers)
Closed 9 years ago.
I want to compare two dates.This my code i wrote
NSDate *c_date=[NSDate date];
NSDate *newDate = [c_date dateByAddingTimeInterval:300];
This code is not working?What i am missing?
From NSDate, you can use
- (NSComparisonResult)compare:(NSDate *)anotherDate
You can use
- (NSComparisonResult)compare:(NSDate *)other;
which will yield a
typedef NS_ENUM(NSInteger, NSComparisonResult) {NSOrderedAscending = -1L, NSOrderedSame, NSOrderedDescending};
in your example you're just creating two different NSDate objects with a known NSTimeInterval (300), so there is no comparison.
Use [NSDate timeIntervalSince1970] which will return a simple double value that can be used for comparison just like any other.
NSDate *c_date=[NSDate date];
NSDate *newDate = [c_date dateByAddingTimeInterval:300];
NSTimeInterval c_ti = [c_date timeIntervalSince1970];
NSTimeInterval new_ti = [newDate timeIntervalSince1970];
if (c_ti < new_ti) {
// c_date is before newDate
} else if (c_ti > new_ti) {
// c_date is after newDate
} else {
// c_date and newDate are the same
}
There are also the [NSDate compare:] method, that you might find more convenient.
Here's the thing (well, it might be the thing, it's not completely 100% clear from your question). NSDate represents an interval in seconds since 1st January 1970. Internally, it uses a floating point number (a double in OS X, not sure in iOS). This means that comparing two NSDates for equality is dry hit and miss, actually it's mostly miss.
If you want to make sure one date is within, say, 1/2 a second of another date, try:
fabs([firstDate timeIntervalSinceDate: secondDate]) < 0.5
If you just want both dates to be on the same day, you'll need to muck about with NSCalendar and date components.
See also this SO answer.
https://stackoverflow.com/a/6112384/169346