This should be simple, but it's proving challenging for me. I'd like to know the best approach to calculating the difference in seconds between [NSDate date] and a future event x seconds from that time. There are several different types of event, and each event may occur several times a day, and at different times, depending what day of the week it happens to be.
What I am trying to do is have the user select an event type from a picker and then set an alarm in Notification Center for the next occurrence of that event based on their selection. I have everything working fine, except for the seconds calculation.
So, for example, let's say it's 9am on a Monday. I'd like to determine how many seconds it would be between now and a user selected event that regularly occurs every Tuesday, Thursday, and Saturday at 10am, 4pm, and 11pm on each day, or on Sunday at 1pm. How would you approach this most efficiently?
When you're talking about a time or date like "next Thursday at 1 PM", that's information that only makes sense in the context of a calendar. NSDate is not going to provide you with much help. It would perhaps be more appropriately named NSPointInTime. It's just a number of seconds that have passed from some earlier, arbitrary reference point in time. It has no notion of weekdays, ante/post meridiem, or even hour of the day.
The two objects that do know about those sorts of thing are NSDateComponents and NSCalendar. Working together, they can create an NSDate from a specification like "next Thursday at 1PM".
You can decompose any date into components using -[NSCalendar components:fromDate:], and you can then use other NSDateComponents objects to perform arithmetic on the individual pieces of information. Find the weekday of today, for example, and its difference from Thursday. Then use -[NSCalendar dateByAddingComponents:toDate:options:] to create a new date based on that offset.
#interface NSCalendar (NextWeekday)
- (NSInteger)maxWeekday;
- (NSDate *)dateFromComponents:(NSDateComponents *)comps
forNextWeekday:(NSInteger)weekday
atHour:(NSInteger)hour;
#end
#implementation NSCalendar (NextWeekday)
- (NSInteger)maxWeekday
{
return [self maximumRangeOfUnit:NSWeekdayCalendarUnit].length;
}
- (NSDate *)dateFromComponents:(NSDateComponents *)comps
forNextWeekday:(NSInteger)weekday
atHour:(NSInteger)hour
{
NSInteger diff = weekday - [comps weekday];
if( diff < 0 ){
diff += [self maxWeekday];
}
NSDateComponents * weekdayOffset = [NSDateComponents new];
[weekdayOffset setWeekday:diff];
[comps setHour:hour];
return [self dateByAddingComponents:weekdayOffset
toDate:[self dateFromComponents:comps]
options:0];
}
#end
#define GREGORIAN_THURSDAY 5
int main(int argc, const char * argv[])
{
#autoreleasepool {
NSCalendar * cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents * wednesday = [NSDateComponents new];
[wednesday setDay:3];
[wednesday setWeekday:4];
[wednesday setMonth:6];
[wednesday setYear:2013];
NSDateComponents * friday = [NSDateComponents new];
[friday setDay:5];
[friday setWeekday:6];
[friday setMonth:6];
[friday setYear:2013];
NSDateComponents * now = [cal components:NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit|NSWeekdayCalendarUnit
fromDate:[NSDate date]];
NSDateComponents * lastSatOfDecember = [NSDateComponents new];
[lastSatOfDecember setDay:29];
[lastSatOfDecember setWeekday:7];
[lastSatOfDecember setMonth:12];
[lastSatOfDecember setYear:2012];
NSLog(#"From Wednesday: %#", [cal dateFromComponents:wednesday
forNextWeekday:GREGORIAN_THURSDAY
atHour:13]);
NSLog(#"From Friday: %#", [cal dateFromComponents:friday
forNextWeekday:GREGORIAN_THURSDAY
atHour:13]);
NSLog(#"From now: %#", [cal dateFromComponents:now
forNextWeekday:GREGORIAN_THURSDAY
atHour:13]);
NSLog(#"Crossing over the year: %#", [cal dateFromComponents:lastSatOfDecember
forNextWeekday:GREGORIAN_THURSDAY
atHour:13]);
}
return 0;
}
Related
In my App, When i select a day in the datepicker it should automatically calculate and display 4 other days and also it should notify on the corresponding days. For Eg. if i enter 19-08-2014, it should calculate and display the 3rd day, the 7th day, the 14th day and the 21st day from 19-08-2014(the day i entered in datepicker).
How would i achieve this. Any help would be greatly appreciated.
I have added my current code for your reference. This doesnt serve my purpose.
Kindly Help.
(IBAction)save:(UIButton *)sender {
NSDate *pickerDate = [self.picker date];
UILocalNotification *localNotif = [[UILocalNotification alloc]init];
//localNotif.alertBody = _enterText.text;
localNotif.alertBody = #"Please Take Your Rabipur Dosage";
localNotif.fireDate = pickerDate;
localNotif.timeZone = [NSTimeZone defaultTimeZone];
localNotif.soundName = (UILocalNotificationDefaultSoundName);
localNotif.applicationIconBadgeNumber = 1;
//localNotif.repeatInterval = NSDayCalendarUnit;
[[UIApplication sharedApplication]scheduleLocalNotification:localNotif];
[self dismissViewControllerAnimated:YES completion:nil];
}
Create an NSDateComponents object with the date difference you want to add, and add it to pickerDate via NSCalendar -dateByAddingComponents:toDate:options. Then create your notification based on the resulting date.
In this case, something like this:
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *offsetComponents = [[NSDateComponents alloc] init];
[offsetComponents setDay:3];
NSDate *reminderDate = [gregorian dateByAddingComponents:offsetComponents toDate:pickerDate options:0];
Repeat for 7th, 14th, and 21st day.
I am making an app that calculates salaries based on the years somebody is working for a firm. The user enter 2 dates, the first one is the day he started working (let's say 1/12/2010) and the second is the end date (let's say 1/02/2012). So far i know the total days he worked but i want to be more specific in order to calculate that he worked:
30 days in 2010 * 30$ per day = 90$
365 days in 2011 * 35$ per day = 12775$
32 days in 2012 * 40$ per day = 1280$
It's the first time i am working with dates and calendars and as you can guess i am totally unfamiliar with them.
Try this code :
-(NSInteger)daysBetweenTwoDates:(NSDate *)fromDateTime andDate:(NSDate*)toDateTime{
NSDate *fromDate;
NSDate *toDate;
NSCalendar *calendar = [NSCalendar currentCalendar];
[calendar rangeOfUnit:NSDayCalendarUnit startDate:&fromDate interval:NULL forDate:fromDateTime];
[calendar rangeOfUnit:NSDayCalendarUnit startDate:&toDate interval:NULL forDate:toDateTime];
NSDateComponents *difference = [calendar components:NSDayCalendarUnit fromDate:fromDate toDate:toDate options:0];
return [difference day]+1;//+1 as if start and end both date are same, so 1 day worked.
}
- (IBAction)calculate:(id)sender {
NSDate *startDate=[self.startDate dateValue];//taking from datepicker
NSDate *endDate=[self.endDate dateValue];//taking from datepicker
NSTimeZone *gmt=[NSTimeZone timeZoneWithAbbreviation:#"GMT"];
NSDateFormatter *dateFormatterYYYY=[NSDateFormatter new];
[dateFormatterYYYY setTimeZone:gmt];
[dateFormatterYYYY setDateFormat:#"YYYY"];
NSDateFormatter *dateFormatterDDMMYYYY=[NSDateFormatter new];
[dateFormatterDDMMYYYY setDateFormat:#"dd/mm/YYYY"];
[dateFormatterDDMMYYYY setTimeZone:gmt];
//1. startDate to 31/12/StartDateYear
//2. loop to endDateYear
//3. 01/01/endDateYear to endDate
/* step 1 */
//find 31/12/StartDateYear
NSLog(#"YYYY ; %#",[dateFormatterYYYY stringFromDate:startDate]);
NSString *lastDateOfStartYearString=[NSString stringWithFormat:#"31/12/%#",[dateFormatterYYYY stringFromDate:startDate]];
NSDateFormatter *dateFormatddMMyyyy=[NSDateFormatter new];
[dateFormatddMMyyyy setDateFormat:#"dd'/'MM'/'yyyy"];
NSDate *lastDateOfStartYear=[dateFormatddMMyyyy dateFromString:lastDateOfStartYearString];
//no. of days
NSInteger year=[[dateFormatterYYYY stringFromDate:startDate]integerValue];
NSInteger days=[self daysBetweenTwoDates:startDate andDate:lastDateOfStartYear];
//add in dictionary
NSMutableDictionary *daysForYearDictionary=[NSMutableDictionary new];
[daysForYearDictionary setValue:#(days) forKey:[NSString stringWithFormat:#"%ld",year]];
/* step 2*/
NSInteger nextYearAfterStart=[[dateFormatterYYYY stringFromDate:startDate]integerValue]+1;
NSInteger previousYearBeforeEnd=[[dateFormatterYYYY stringFromDate:endDate]integerValue]-1;
for (NSInteger yearCounter=nextYearAfterStart; yearCounter<=previousYearBeforeEnd; yearCounter++) {
NSString *firstDateString=[NSString stringWithFormat:#"1/1/%ld",yearCounter];
NSDate *firstDate=[dateFormatddMMyyyy dateFromString:firstDateString];
NSString *lastDateString=[NSString stringWithFormat:#"31/12/%ld",yearCounter];
NSDate *lastDate=[dateFormatddMMyyyy dateFromString:lastDateString];
//no. of days
days=[self daysBetweenTwoDates:firstDate andDate:lastDate];
//add in dictionary
[daysForYearDictionary setValue:#(days) forKey:[NSString stringWithFormat:#"%ld",yearCounter]];
}
/* step 3 */
//find 1/1/EndDateYear
NSString *firstDateOfStartYearString=[NSString stringWithFormat:#"1/1/%#",[dateFormatterYYYY stringFromDate:endDate]];
NSDate *firstDateOfEndYear=[dateFormatddMMyyyy dateFromString:firstDateOfStartYearString];
//no. of days
year=[[dateFormatterYYYY stringFromDate:endDate]integerValue];
days=[self daysBetweenTwoDates:firstDateOfEndYear andDate:endDate];
//add in dictionary
[daysForYearDictionary setValue:#(days) forKey:[NSString stringWithFormat:#"%ld",year]];
//printing
for (NSString *yearStr in daysForYearDictionary) {
NSLog(#"Year : %#, Days Worked : %#",yearStr,[daysForYearDictionary valueForKey:yearStr]);
}
}
Given an arbitrary date, I need to find the date of the first day of the next week of the month. Note that it is not as simple as adding 7 days to the current date because the last week of the month may be less than 7 days. Here is the code I'm using now:
NSCalendar* calendar = [NSCalendar currentCalendar];
NSDateComponents* components = [calendar components:NSWeekOfMonthCalendarUnit|NSYearCalendarUnit|NSMonthCalendarUnit|NSWeekCalendarUnit|NSDayCalendarUnit fromDate:currentDate];
NSLog(#"week of month: %ld", [components weekOfMonth]);
[components setWeekOfMonth:[components weekOfMonth] + 1];
NSLog(#"new week of month: %ld", [components weekOfMonth]); //week of month is now 2
[components setWeekday:1];
NSDate *nextWeek = [calendar dateFromComponents:components];
As an example, currentDate is set to 2012-10-01. In this example nextWeek is always 2012-10-01. It appears that sending setWeekOfMonth: does not increment the other date components in the NSDateComponents object. Do I have the wrong date components configured, or is setWeekOfMonth: not supposed to work like that?
So.... Let's start with an NSDate and an NSCalendar:
NSDate *date = ...;
NSCalendar *cal = [NSCalendar currentCalendar];
Figure out what day of the week that date is:
NSInteger weekdayOfDate = [cal ordinalityOfUnit:NSWeekdayCalendarUnit inUnit:NSWeekCalendarUnit forDate:date];
NSInteger numberOfDaysToStartOfCurrentWeek = weekdayOfDate - 1;
Let's move that date into the next week:
NSDateComponents *oneWeek = [[NSDateComponents alloc] init];
[oneWeek setWeek:1]; // add one week
[oneWeek setDay:-numberOfDaysToStartOfCurrentWeek]; // ... and subtract a couple of days to get the first day of the week
NSDate *startOfNextWeek = [cal dateByAddingComponents:oneWeek toDate:date options:0];
At this point, you have a date that points to the first day of the next week. Now we should verify that it's still in the same month:
NSDateComponents *monthOfNextWeek = [cal components:NSMonthCalendarUnit fromDate:startOfNextWeek];
NSDateComponents *monthOfThisWeek = [cal components:NSMonthCalendarUnit fromDate:date];
if ([monthOfNextWeek month] != [monthOfThisWeek month]) {
// the first day of the next week is not in the same month as the start date
}
When I run this, I get that the start of next week is 30 Dec 2012 (a Sunday, because my calendar's weeks start on Sunday).
If, however, I want the first day of the week to start on Monday, I can preface this code with:
[cal setFirstWeekday:2]; // the first day of the week is the second day (ie, Monday, because Sunday = 1)
If I do this, then the startOfNextWeek results in 31 Dec 2012.
Will this be a valid answer :
NSDateFormatter *dateFormatter=[NSDateFormatter new];
[dateFormatter setLocale:[[NSLocale alloc]initWithLocaleIdentifier:#"en_US_POSIX"]];
[dateFormatter setDateFormat:#"EEE"];
NSDate *date=[NSDate date];
NSString *dateString=[dateFormatter stringFromDate:date];
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar];
NSDateComponents *components = [NSDateComponents new];
[calendar setFirstWeekday:1];//1 for Mon
NSDate *newDate=date;
while (![dateString isEqualToString:#"Mon"]) {
components.day = 1;
newDate = [calendar dateByAddingComponents:components toDate:newDate options:0];
dateString=[dateFormatter stringFromDate:newDate];
}
NSLog(#"Upcoming week Date=%#",newDate);
I am working with a schedule that specifies times of day, such as 10:30 AM. I do not know the dates, however. I'm going to store these as values in a NSDictionary and would like to deal with them in a straightforward way.
I can't use NSDate, since I don't have a date. At least, not in a straightforward way.
The other way that seems obvious is NSTimeInterval, but that looks like it's probably a source of very subtle errors. I'm thinking in particular of daylight savings time (which is on my mind this week for some reason!).
Other than that, the only things that really spring to mind are keeping it in a formatted string or encoding it in a NSNumber (like hours * 60 + minutes). Both of which would work out fine, of course, but seem like I'm inventing a square wheel where I'm sure there's already a round one somewhere.
What's the least against the grain way of dealing with raw times using Cocoa?
The short answer is that there's no built-in mechanism for storing time-of-day, so just use whatever is most convenient to you. I've used strings in the past using my own encoding and parsing code, (e.g., "22:00") because they're easy to read and debug, but there's nothing wrong with storing seconds or minutes past midnight as you suggest. Just remember that you'll have to do the math yourself.
How ever you do it, you will need separate year, month, day, hour, minute, and second values so that you can construct an NSDate from NSDateComponents, using NSCalendar's -dateFromComponents: method.
And as others have said, you cannot set the time-of-day by adding hours and minutes to an existing NSDate because if you cross a DST boundary you won't get the value you expect. (However, I assume you can still add day and month components without worrying about DST)
So, I guess there's no simple inbuilt way of doing this. It also looks like the only way to get Cocoa to build the time I expect on DST boundaries is with strings.
So for posterity, it looks like I'll be using something like this.
Test harness:
//
// TimeTest.m
//
#import <Foundation/Foundation.h>
#import "Utility.h"
id utility;
void testTime( NSTimeInterval time ) {
id gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar] autorelease];
id oneDay = [[[NSDateComponents alloc] init] autorelease];
[oneDay setDay: 1];
id thisDay = [gregorian dateFromComponents: [gregorian components: (NSEraCalendarUnit | NSYearCalendarUnit)
fromDate: [NSDate date]]];
for (NSInteger dayIdx = 0; dayIdx < 365; ++dayIdx ) {
NSDate *dateTime = [utility timeInSeconds: time
onDate: thisDay];
NSLog( #"%#", dateTime );
thisDay = [gregorian dateByAddingComponents: oneDay
toDate: thisDay
options: 0];
}
}
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
utility = [[[Utility alloc] init] autorelease];
testTime( ((10 * 60.0) + 0.0) * 60.0 );
testTime( ((9 * 60.0) + 30.0) * 60.0 );
[pool drain];
return 0;
}
Utility header:
//
// Utility.h
//
#import <Foundation/Foundation.h>
#interface Utility : NSObject {
NSCalendar *gregorian;
NSDateFormatter *dateWithoutTimeFormatter, *dateWithTimeFormatter;
}
- (NSDate *)timeInHours: (NSInteger)hours
minutes: (NSInteger)minutes
seconds: (NSInteger)seconds
onDate: (NSDate *)inDate;
- (NSDate *)timeInSeconds: (NSTimeInterval)inTime
onDate: (NSDate *)inDate;
#end
Utility implementation:
//
// Utility.m
//
#import "Utility.h"
#interface Utility()
#property (nonatomic, readwrite, retain) NSCalendar *gregorian;
#property (nonatomic, readwrite, retain) NSDateFormatter *dateWithoutTimeFormatter, *dateWithTimeFormatter;
#end
#implementation Utility
#synthesize gregorian, dateWithoutTimeFormatter, dateWithTimeFormatter;
- (NSDate *)timeInHours: (NSInteger)hours
minutes: (NSInteger)minutes
seconds: (NSInteger)seconds
onDate: (NSDate *)inDate;
{
id timeStr = [[NSMutableString alloc] initWithFormat: #"%02d:%02d:%02d", hours, minutes, seconds];
id dateStr = [dateWithoutTimeFormatter stringFromDate: inDate];
id dateTimeStr = [[NSString alloc] initWithFormat: #"%# %#", dateStr, timeStr];
[timeStr release];
id dateTime = [dateWithTimeFormatter dateFromString: dateTimeStr];
[dateTimeStr release];
return dateTime;
}
- (NSDate *)timeInSeconds: (NSTimeInterval)inTime
onDate: (NSDate *)inDate;
{
NSAssert1( inTime < 24.0 * 3600.0, #"Time %f must be less than 24hrs", inTime );
double temp = inTime;
int hours = rintf(floor( temp / 3600.0 ));
temp -= ( hours * 3600 );
int minutes = rintf(floorf( temp / 60.0 ));
temp -= ( minutes * 60 );
int seconds = rintf( temp );
return [self timeInHours: hours
minutes: minutes
seconds: seconds
onDate: inDate];
}
- (id)init;
{
if (( self = [super init] )) {
self.gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar] autorelease];
self.dateWithoutTimeFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateWithoutTimeFormatter setDateFormat: #"yyyy-MM-dd"];
self.dateWithTimeFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateWithTimeFormatter setDateFormat: #"yyyy-MM-dd HH:mm:ss"];
}
return self;
}
- (void)dealloc;
{
self.gregorian = nil;
self.dateWithoutTimeFormatter = nil;
self.dateWithTimeFormatter = nil;
[super dealloc];
}
#end
Why bother with a separate unit for this? Well, I've written enough date formatting code to know that constructing NSCalendar and NSDateFormatter on the fly utterly kills performance.
You can use NSDateComponents to store only the components you need (hour and minutes for example), and then use NSCalendar dateByAddingComponents:toDate:options: to create an absolute date reference, when you need using the serialized components and a base date.
Is there a better way to do this?
-(NSDate *)getMidnightTommorow {
NSCalendarDate *now = [NSCalendarDate date];
NSCalendarDate *tomorrow = [now dateByAddingYears:0 months:0 days:1 hours:0 minutes:0 seconds:0];
return [NSCalendarDate dateWithYear:[tomorrow yearOfCommonEra]
month:[tomorrow monthOfYear]
day:[tomorrow dayOfMonth]
hour:0
minute:0
second:0
timeZone:[tomorrow timeZone]];
}
Note that I always want the next midnight, even if it happens to be midnight when I make that call, however if it happens to be 23:59:59, I of course want the midnight that is coming in one second.
The natural language functions seem flaky, and I'm not sure what Cocoa would do if I pass 32 in the "day" field. (If that'd work I could drop the [now dateByAddingYears:...] call)
From the documentation:
Use of NSCalendarDate strongly
discouraged. It is not deprecated yet,
however it may be in the next major OS
release after Mac OS X v10.5. For
calendrical calculations, you should
use suitable combinations of
NSCalendar, NSDate, and
NSDateComponents, as described in
Calendars in Dates and Times
Programming Topics for Cocoa.
Following that advice:
NSDate *today = [NSDate date];
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *components = [[NSDateComponents alloc] init];
components.day = 1;
NSDate *tomorrow = [gregorian dateByAddingComponents:components toDate:today options:0];
[components release];
NSUInteger unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
components = [gregorian components:unitFlags fromDate:tomorrow];
components.hour = 0;
components.minute = 0;
NSDate *tomorrowMidnight = [gregorian dateFromComponents:components];
[gregorian release];
[components release];
(I'm not sure offhand if this is the most efficient implementation, but it should serve as a pointer in the right direction.)
Note: In theory you can reduce the amount of code here by allowing a date components object with values greater than the range of normal values for the component (e.g. simply adding 1 to the day component, which might result in its having a value of 32). However, although dateFromComponents: may tolerate out-of-bounds values, it's not guaranteed to. You're strongly encouraged not to rely on it.
Nope - it'll be the same way you use to find midnight today.
NSCalendar *gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
NSDate *tomorrow = [NSDate dateWithTimeIntervalSinceNow:(24 * 60 * 60)];
NSDateComponents *components = [gregorian components:(NSYearCalendarUnit |
NSMonthCalendarUnit |
NSDayCalendarUnit)
fromDate:tomorrow];
NSDate *midnight = [gregorian dateFromComponents:components];
[NSDate dateWithNaturalLanguageString:#"midnight tomorrow"];
Convert your current date and time to a Unix date (seconds since 1970) or DOS style (since 1980), then add 24 hours and convert it back. Then reset the hours, minutes and seconds to zero to get to midnight.
You could try this way:
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *comps = [[NSDateComponents alloc] init];
[comps setDay:1];
NSDate *tomorrow = [calendar dateByAddingComponents:comps toDate:[NSDate date] options:0]; //it gives us tomorrow with current time
NSDate *midnight = [calendar startOfDayForDate:tomorrow]; //here we get next midnight
It is also easy to retrieve the seconds interval if needed to set up an NSTimer:
double intervalToMidnight = midnight.timeIntervalSinceNow;