Picking the nearest 3 dates out of a list - ios

I am working on a TV-guide app, and am trying to get the nearest 3 dates from an NSArray with NSDictionary's. So far so good, but I have been trying to figure out how I can do this the best way using as little memory as possible and with as little code (hence decreasing the likelihood of bugs or crashes). The array is already sorted.
I have a dictionary with all the channels shows for one day. The dictionary withholds an NSDate (called date).
Lets say a channel has 8 shows and the time is now 11:45. show #3 started at 11:00 and ends at 12:00, show #4 starts at 12:00 and ends at 13:00, show #5 at 13:00 to 14:00 ect.
How could I fetch show #3 (which started in the past!), #4 and #5 the fastest (memory wise) and easiest from my array of dictionaries?
Currently I am doing a for loop fetching each dictionary, and then comparing the dictionaries date with the current date. And thats where I am stuck. Or maybe I just have a brain-fag.
My current code (after a while of testing different things):
- (NSArray*)getCommingProgramsFromDict:(NSArray*)programs amountOfShows:(int)shows
{
int fetched = 0;
NSMutableArray *resultArray = [[NSMutableArray alloc] init];
NSDate *latestDate = [NSDate date];
for (NSDictionary *program in programs)
{
NSDate *startDate = [program objectForKey:#"date"];
NSLog(#"Program: %#", program);
switch ([latestDate compare:startDate]) {
case NSOrderedAscending:
NSLog(#"latestDate is older, meaning the show starts in the future from latestDate");
// do something
break;
case NSOrderedSame:
NSLog(#"latestDate is the same as startDate");
// do something
break;
case NSOrderedDescending:
NSLog(#"latestDate is more recent, meaning show starts in the past");
// do something
break;
}
// Now what?
}
return resultArray;
}
I am writing it for iOS 5, using ARC.

After your EDIT and explanation, here is another answer, hopefully fitting your question better.
The idea is to find the index of the show that is next (startDate after now). Once you have it, it will be easy to get the show at the previous index (on air) and the 2 shows after it.
NSUInteger indexOfNextShow = [arrayOfShows indexOfObjectPassingTest:^BOOL(id program, NSUInteger idx, BOOL *stop) {
NSDate* startDate = [program objectForKey:#"date"];
return ([startDate timeIntervalSinceNow] > 0); // startDate after now, so we are after the on-air show
}];
At that stage, indexOfNextShow contains the index of the show in your NSArray that will air after the current show. Thus what you want according to your question is objects at index indexOfNextShow-1 (show on air), indexOfNextShow (next show) and indexOfNextShow+1 (show after the next).
// in practice you should check the range validity before doing this
NSIndexSet* indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(indexOfNextShow-1,3)];
NSArray* onAirShowAnd2Next = [arrayOfShows objectsAtIndexes:indexes];
Obviously in practice you should add some verifications (like indexOfNextShow being >0 before trying to access object at index indexOfNextShow-1 and indexOfNextShow+1 not being past the total number of shows in your array).
The advantage of this is that since your array of shows is sorted by startDate already, indexOfObjectPassingTest: returns the first object passing the test, and stop iterating as soon as it has found the right object. So this is both concise, easy-to-read code and relatively efficient.
.

I'm not sure I understood your model structure, you have an NSArray of shows, each show being a NSDictionary holding the NSDate of the show along with other info, right?
One idea then is to sort this NSArray of show according to the distance between the start time of the show and now.
NSArray* shows = ... // your arraw of NSDictionaries representing each show
NSArray* sortedShows = [shows sortedArrayUsingComparator:^(id show1, id show2) {
NSTimeInterval ti1 = fabs([[show1 objectForKey:#"startDate"] timeIntervalSinceNow]);
NSTimeInterval ti2 = fabs([[show2 objectForKey:#"startDate"] timeIntervalSinceNow]);
return (NSComparisonResult)(ti1-ti2);
}];
Then of course it is easy at that point to only take the 3 first shows of the sortedShows array.
If I've misunderstood your model structure, please edit your question to specify it, but I'm sure you can adapt my code to fit your model then

The question asks for the "fastest (memory wise)". Are you looking for the fastest or the most memory/footprint conscious? With algorithms there is often a space vs. time tradeoff so as you make it faster, you typically do it by adding indexes and other lookup data structures which increase the memory footprint.
For this problem the straight forward implementation would be to iterate through each channel and each item comparing each against the top 3 held in memory. But that could be slow.
With additional storage, you could have an additional array which indexes into time slots (one per 15 minutes granularity good enough?) and then daisy chain shows off of those time slots. Given the current time, you could index straight into the current times slot and then look up the next set of shows. The array would have pointers to the same objects that the dictionaries are pointing to. That's an additional data structure to optimize one specific pattern of access but it does it at a cost - more memory.
That would increase your foot print but would be very fast since it's just an array index offset.
Finally, you could store all your shows in a sqlite database or CoreData and solve your problem with one query. Let the sql engine do the hard work. that wold also keep your memory foot print reasonable.
Hope that sparks some ideas.
EDIT:
A crude example showing how you can construct a look table - an array with slots for every 15 minutes. It's instant to jump to the current time slot since it's just an array offset. Then you walk the absolute number of walks - the next three and you're out. So, it's an array offset with 3 iterations.
Most of the code is building date - the lookup table, finding the time slot and the loop is trivial.
NSInteger slotFromTime(NSDate *date)
{
NSLog(#"date: %#", date);
NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:(NSHourCalendarUnit | NSMinuteCalendarUnit) fromDate:date];
NSInteger hour = [dateComponents hour];
NSInteger minute = [dateComponents minute];
NSInteger slot = (hour * 60 + minute)/15;
NSLog(#"slot: %d", (int)slot);
return slot;
}
int main (int argc, const char * argv[])
{
// An array of arrays - the outer array is an index of 15 min time slots.
NSArray *slots[96];
NSDate *currentTime = [NSDate date];
NSInteger currentSlot = slotFromTime(currentTime);
// populate with shows into the next few slots for demo purpose
NSInteger index = currentSlot;
NSArray *shows1 = [NSArray arrayWithObjects:#"Seinfeld", #"Tonight Show", nil];
slots[++index] = shows1;
NSArray *shows2 = [NSArray arrayWithObjects:#"Friends", #"Jurassic Park", nil];
slots[++index] = shows2;
// find next three -jump directly to the current slot and only iterate till we find three.
// we don't have to iterate over the full data set of shows
NSMutableArray *nextShow = [[NSMutableArray alloc] init];
for (NSInteger currIndex = currentSlot; currIndex < 96; currIndex++)
{
NSArray *shows = slots[currIndex];
if (shows)
{
for (NSString *show in shows)
{
NSLog(#"found show: %#", show);
[nextShow addObject:show];
if ([nextShow count] == 3)
break;
}
}
if ([nextShow count] == 3)
break;
}
return 0;
}
This outputs:
2011-10-01 17:48:10.526 Craplet[946:707] date: 2011-10-01 21:48:10 +0000
2011-10-01 17:48:10.527 Craplet[946:707] slot: 71
2011-10-01 17:48:14.335 Craplet[946:707] found show: Seinfeld
2011-10-01 17:48:14.336 Craplet[946:707] found show: Tonight Show
2011-10-01 17:48:21.335 Craplet[946:707] found show: Friends

Related

Time difference with NSDateComponents

I'm building an app whereas I need to calculate the time between two NSDates.
I've subclassed NSObject and named it "MyObject" (simplified for the sake of this question).
It holds these two properties
#property (nonatomic, strong) NSDate *startDate;
#property (nonatomic, strong) NSDate *endDate;
I have an array storing x number of MyObjects, which I loop through like so
NSTimeInterval totalInterval = 0;
for (MyObject *currentObject in _listOfItems)
{
totalInterval += [currentObject.endDate timeIntervalSinceDate:currentObject.startDate];
}
I'm using totalInterval to summarise the length of all the "spans" combined. I'm doing this by setting up 2 NSDates, simply for the sake of calculating the difference.
NSDate *date1 = [[NSDate alloc] init];
NSDate *date2 = [[NSDate alloc] initWithTimeInterval:totalInterval sinceDate:date1];
I'm setting up these flags:
unsigned int unitFlags = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitDay | NSCalendarUnitMonth;
And return the result as NSDateComponent like so:
return [sysCalendar components:unitFlags fromDate:date1 toDate:date2 options:0];
My question: When I used the units above, I had the wrong result - namely 11h 20m when the real answer is about 83h. I learned that I was needlessly specifying the NSCalendarUnitDay and NSCalendarUnitMonth units. After reading the docs, I read that "There is no need to specify any more components than those in which you are interested" and I removed the two needlessly specified units. This gave me the right result, but how come? Why would it report the wrong result by simply specifying more units?
Thank you!
The calculation will always begin with the largest units that you've asked for. So if you ask for days and hours, but then only inspect hours, you will see the actual total number of hours modulo 24. This applies to any date decomposition that NSCalendar performs.
Why would it report the wrong result by simply specifying more units?
The NSDateComponents represents all of the time between the two dates (300.000 seconds in your case), specified in a number of days (3), hours (11) and minutes (11).
Asking for just seconds will get you seconds, asking for seconds and days will give you just those. Asking just for days would give you the full number of days, so it would be 3.
Note that NSDateComponents can either represent a date (point time), or a time interval.
This flexibility gives you the ability to create some pretty cool time-based components.

NSDecimalNumber for finances

I'm building an app that deals with money and I've been using floating point arithmetic up until now, but I've learned that it's better to use NSDecimalNumber.
I want to make sure that I've understood it correctly, so here goes:
Imagine some worker, earning 20.57$/hour. This information is provided by the user. I did it like this before:
#property (nonatomic) float hourlyRate;
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
NSNumber *hourlyRate = [numberFormatter numberFromString:self.rateTextField.text];
settingsObject.hourlyRate = [hourlyRate floatValue];
But I've now changed it to:
#property (nonatomic) NSDecimalNumber *hourlyRate;
settingsObject.hourlyRate = (NSDecimalNumber *)[NSDecimalNumber decimalNumberWithString:self.rateTextField.text locale:[NSLocale currentLocale]];
Is this the correct way to read NSDecimalNumbers from string?
Say this person enters a workplace at 10:01. I save that information like so:
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:#"start"];
Once the person is finished, the start time is read from NSUserDefaults like so:
NSDate *start = [[NSUserDefaults standardUserDefaults] objectForKey:#"start"];
The duration is calculated like so:
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:start] / 3600;
NSDecimalNumber *earned = [settingsObject.hourlyRate decimalNumberByMultiplyingBy:[[NSDecimalNumber alloc] initWithFloat:interval]];
is this the correct and most efficient way, while keeping precision?
Thanks!
Is this the correct way to read NSDecimalNumbers from string?
Yes. However, you shouldn't need to cast the result. So it should just look like this:
settingsObject.hourlyRate = [NSDecimalNumber decimalNumberWithString:self.rateTextField.text locale:[NSLocale currentLocale]];
And the NSLocale part isn't necessary, unless you're using a non-current locale:
settingsObject.hourlyRate = [NSDecimalNumber decimalNumberWithString:self.rateTextField.text];
I don't think this function likes nil values, so you would want to make sure that rateTextField has a non-nil value first:
if ([self.rateTextField hasText]) {
settingsObject.hourlyRate = [NSDecimalNumber decimalNumberWithString:self.rateTextField.text];
}
else {
settingsObject.hourlyRate = [NSDecimalNumber zero];
}
is this the correct and most efficient way, while keeping precision?
Yes, your conversion of the interval to an NSDecimal number and then the multiplication looks good to me.
First you need an answer for this question:
How accurate do you want to be - or do you need to be?
Will your app generate invoices, or will it serve as the basis for invoices? If yes, you may have to stay away from floating point numbers. You may also have to think about how duration is actually used in calculations. There may be regulations for certain business areas or professions, which define how to measure / invoice time.
Not really accurate:
If you do not need to be really accurate, I would continue using floating numbers until you hit obstacles.
Accurate:
1. Money:
Store money as integers as cents.
2. Time:
Determine time slots and store them as integers (maybe minutes).
3. Calculations:
Calculate using integers. Never use floats.
Details:
When you have an input like 25.07 (I used a different number than your 20.57 to explain cents rendering) store it as 2507. Do your calculations in cents. For displaying convert to $ or $/hrs. For example:
int amount = 2507;
int amountDollars = amount / 100;
int amountCents = amount % 100;
NSString *output = [NSString stringWithFormat:#"%d.%02d $/hrs", amountDollars, amountCents];
NSLog(#"output = %#", output);
// prints:
// output = 25.07 $/hrs
For time chose a suitable base unit (minute / second / five minutes / ten minutes / half an hour?) and store that as an integer.
Lets assume you invoice in units of 10 minutes. You probably then bill the next 10 minutes, when they are begun. So if 11 minutes are in the record, you have to calculate 20 minutes. Similar issues exist for minutes - assume these values:
Actual Time | "Integer Time"
10:09:59 | 10:09
10:10:01 | 10:10
Difference:
00:00:02 | 00:01
If you just use actual time using floating points the first difference amounts to almost nothing - the second amounts to one minute. If your times are shown in the report as 10:09 and 10:10, people will wonder, why in the first case the amount is almost nothing.
Doing financial and time calculations is hard if you need to be really accurate. So it boils down to this: First find out, how accurate you need to be. (Note that "accurate" may have weird meanings and implications when entering the realm of regulations, laws etc.)

Function to return open, closed or closing from Hours of day

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.

Notify when device date enters a date range on iOS [duplicate]

I am interested in creating a method to find if the current date falls between certain times on any given day. It is for a scheduling program, so I want to find what event is occurring at the current time. Every day has the same set of times: 820-900, 900-940, 940-1020, etc. Since this must be done on any given day I do not know how to create an NSDate with a certain time. I think this might be done with NSTimeInterval but I am not sure how to instantiate that.
This isn't perfect, but you could use [NSDate compare:] to check your date against both boundaries:
NSDate *firstDate = ...
NSDate *secondDate = ...
NSDate *myDate = [NSDate date];
switch ([myDate compare:firstDate]) {
case NSOrderedAscending:
NSLog(#"myDate is older");
// do something
break;
case NSOrderedSame:
NSLog(#"myDate is the same as firstDate");
// do something
break;
case NSOrderedDescending:
NSLog(#"myDate is more recent");
// do something
break;
}
switch ([myDate compare:secondDate]) {
case NSOrderedAscending:
NSLog(#"myDate is older");
// do something
break;
case NSOrderedSame:
NSLog(#"myDate is the same as secondDate");
// do something
break;
case NSOrderedDescending:
NSLog(#"myDate is more recent");
// do something
break;
}
Or more briefly:
BOOL between = NO;
if (([myDate compare:firstDate] == NSOrderedDescending) &&
([myDate compare:secondDate] == NSOrderedAscending)) {
between = YES;
}
I'm sure there's a better way to do complex date comparison, but this should work.
The easiest way to do this would be to use -timeIntervalSinceReferenceDate to turn each of your dates into an NSTimeInterval typed value, which is really just a double.
NSTimeInterval rightNow = [[NSDate date] timeIntervalSinceReferenceDate];
From there, determining if a date is in between any given two dates is just a matter of simple numeric comparisons.
If you need to convert from a string representation of a date to an NSDate instance to then retrieve a time interval, use NSDateFormatter.
If you need to create a date from known date components, use NSCalendar. (i.e. you know the year is 2010, the month is 4 and the day is 12, you can use NSCalendar's components to generate an NSDate instance via the -dateFromComponents: method.).
As Ben indicated, NSCalendar's component interface can also be used to suss out the hour & minute to determine if it is in a range (which would seemingly be an atypical usage in that most events don't happen every day at the same time... but... sure... there are reasons to do that, too!)
Take a look at -[NSCalendar components:fromDate:]. It will let you 'decompose' a date into its various components, and then you can, in this instance, look at the hour and minute.

Parse.com always returns a maximum of 100 records, same having "limit = 1000"

I have an iOS app that receives data from the PARSE.COM.
How did not know nothing about 'parse.com' , I used the tutorial "http://www.raywenderlich.com/15916/how-to-synchronize-core-data-with-a-web-service-part-1".
The synchronization occurs only from the server to the device (iOS), and one time the object is added to the device, it should not be inserted again.
Turns out I got 131 objects in a class and 145 in another, but the Parse.com always returns me the first 100 items, even those already are in the device (iOS).
The worst thing is that in my code I have a variable "limit" in "request" that should work, but does not work for me.
I need your help, please ...
Code:
- (NSMutableURLRequest *)GETRequestForAllRecordsOfClass:(NSString *)className updatedAfterDate:(NSDate *)updatedDate
{
NSMutableURLRequest *request = nil;
NSDictionary *paramters = nil;
if (updatedDate) {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"yyyy-MM-dd'T'HH:mm:ss.'999Z'"];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:#"GMT"]];
NSString *jsonString = [NSString
stringWithFormat:#"{\"updatedAt\":{\"$gte\":{\"__type\":\"Date\",\"iso\":\"%#\"}}}",
[dateFormatter stringFromDate:updatedDate]];
//That's line of 'paramters' is from original tutorial Raywenderlich
// paramters = [NSDictionary dictionaryWithObject:jsonString forKey:#"where"];
//This line was add for the autors of tutorial in a comment from your blog, and he say that has work, but not for me =(
paramters = #{#"where" : jsonString, #"limit" : #(1000)};
}
request = [self GETRequestForClass:className parameters:paramters];
return request;
}​
The print of variable "request" after this method is this:
URL: https://api.parse.com/1/classes/Substancia?where=%7B%22updatedAt%22%3A%7B%22%24gte%22%3A%7B%22__type%22%3A%22Date%22%2C%22iso%22%3A%222014-09-23T02%3A13%3A01.999Z%22%7D%7D%7D&limit=1000
Why do same having the variable "LIMIT = 1000", the parse.com every returns me 100 items?
And even that returns 100 items, why do in the next time he does the "request" he it does not catch the next 100 since the other 100 registers earlier have already been entered?
Can anyone help me?
(Answering here since I don't have enough reputation to comment.)
For the limit=1000 not seeming to work: perhaps the "where" clause (i.e. constraining to items with updatedAt value >= 2014-09-23T02:13:01.999Z) is limiting results to less than 1000?
(To Ian's point) There is a 'skip' parameter that tells the API how many items to skip ahead, for pagination. i.e. limit=100&skip=100 to see page 2.
I'm not sure, but I think this is what you're looking for. A great solution on how to retrieve all the objects from Parse instead of the max limit i.e. 1000.

Resources