I'm trying to write a method that takes an NSDate as input and returns the first day of the week that the date falls in. I've been using NSDateComponents to try and set the year, week of year, and day of week; but I get unexpected behavior. For example :
NSDateComponents* comps = [[NSDateComponents alloc]init];
[comps setYear:2015];
[comps setWeekday:1];
[comps setWeekOfYear:1];
NSLog(#"%#\n%#\n\n", comps, [formatter stringFromDate:[calendar dateFromComponents:comps]]);
Results in:
<NSDateComponents: 0x7fbc24cc9870>
Calendar Year: 2015
Week of Year: 1
Weekday: 1
Dec 27 2016
All strangeness aside, here is my method that's supposed to return the first day of the week from an NSDate:
- (NSDate *)dateAtBeginningOfDayForDate:(NSDate *)inputDate
{
// Use the user's current calendar and time zone
NSCalendar *calendar = [NSCalendar currentCalendar];
NSTimeZone *timeZone = [NSTimeZone systemTimeZone];
[calendar setTimeZone:timeZone];
NSDateComponents *dateComps = [calendar components:NSCalendarUnitYear | NSCalendarUnitWeekOfYear | NSCalendarUnitWeekday fromDate:inputDate];
NSDateComponents* startOfWeek = [[NSDateComponents alloc]init];
// Set the time components manually
[startOfWeek setWeekOfYear:dateComps.weekOfYear];
[startOfWeek setYear:dateComps.year];
[startOfWeek setWeekday:0];
// Convert to date
NSDate *beginningOfDay = [calendar dateFromComponents:startOfWeek];
return beginningOfDay;
}
Follow this Ry’s Objective-C Tutorial...
http://rypress.com/tutorials/objective-c/data-types/dates
Related
I am currently updating an old iOS App that uses NSDateComponents to calculate the start date of the week a given NSDate is in:
NSDate* someDate = [MyDateFactory dateFromDay:31 month:8 year:2017];
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *comps = [calendar components:(NSYearCalendarUnit | NSWeekCalendarUnit) fromDate:someDate];
// For 2017-08-31 comps is now year=2017, week (obsolete)=35
comps.calendar = calendar;
NSDate* weekStartDate = [comps date];
// weekStartDate = 2017-08-27
This works fine, but NSYearCalendarUnit and NSWeekCalendarUnit are both deprecated. Replacing NSYearCalendarUnit with the new NSCalendarUnitYear is no problem. But what it the correct replacemtn for NSWeekCalendarUnit.
It seems that NSWeekCalendarUnit returns the same value (week of year) as the new NSCalendarUnitWeekOfYear. But this does not result in the same weekStartDate:
// ... same as above. Now use new enum values
NSDateComponents *comps = [calendar components:(NSCalendarUnitYear | NSCalendarUnitWeekOfYear) fromDate:someDate];
// For 2017-08-31 comps is now year=2017, week of year=35 <== Same es above
comps.calendar = calendar;
NSDate* weekStartDate = [comps date];
// weekStartDate = 2017-01-01
So using NSCalendarUnitWeekOfYear returns the same week value (week 35), but it seems that this value is not considered when calculating the date from the the NSDateComponents object. 2017-01-01 is obviously not the date week 35 starts at in 2017.
So, how to solve this?
You can use other methods of NSCalendar instead.
To get the day on which weeks start, use calendar.firstWeekday.
Then put this into some date components:
NSDateComponents *search = [[NSDateComponents alloc] init];
search.weekDay = calendar.firstWeekday;
Now you can use the calendar to find the previous date which matches the components:
NSDate *weekStart = [calendar nextDateAfterDate: someDate matchingComponents: search options: NSCalendarSearchBackwards];
I am using https://github.com/ruslanskorb/RSDayFlow to add calendar to my application.
I override the method as
// Prints out the selected date.
- (void)datePickerView:(RSDFDatePickerView *)view didSelectDate:(NSDate *)date {
self.transactionDate = date;
self.inputDateField.text = [[Helper getDateFormatterForClient] stringFromDate:self.transactionDate];
NSLog(#"transactionDate=%#", self.transactionDate);
[self.inputDateField resignFirstResponder];
}
All I want to add is current time HH:MM:SS to the date user selects.
I have no idea how to do this, any help is greatly appreciated
UPDATE
Currently, when the date is selected, I get
2015-01-15 10:34:35.210 myapp-ios[21476:60b] transactionDate=2015-01-15 08:00:00 +0000
If by "current time HH:MM:SS to the date" you mean that you want to replace the selected date's HH:MM:SS with the current HH:MM:SS, you can extract the relevant date components from the selected date, get the current datetime, extract the relevant time components, then add those components back into self.transactionDate, ex:
// Prints out the selected date.
- (void)datePickerView:(RSDFDatePickerView *)view didSelectDate:(NSDate *)date {
self.transactionDate = date;
self.inputDateField.text = [[Helper getDateFormatterForClient] stringFromDate:self.transactionDate];
NSLog(#"transactionDate=%#", self.transactionDate);
[self.inputDateField resignFirstResponder];
// Create an instance of the current calendar
NSCalendar *calendar = [NSCalendar currentCalendar];
// Break self.transactionDate into day, month, and year components
NSDateComponents* dateComponents = [[NSCalendar currentCalendar] components:NSCalendarUnitYear|
NSCalendarUnitMonth|NSCalendarUnitDay fromDate:self.transactionDate];
// Save transaction date with just the day, month, and year
self.transactionDate = [[NSCalendar currentCalendar] dateFromComponents:dateComponents];
// Get the current datetime
NSDate *currentDate = [NSDate date];
// Extract the hour, minute, and second components
NSDateComponents *components = [calendar components:(NSCalendarUnitHour|
NSCalendarUnitMinute|NSCalendarUnitSecond) fromDate:currentDate];
// Add those hour, minute, and second components to
// self.transactionDate
self.transactionDate = [calendar dateByAddingComponents:components
toDate:self.transactionDate options:0];
}
This method Which I have created the in the category of `NSDate'.
+ (NSDate *)getDateWithCurrentTime:(NSDate *)date {
NSCalendar *gregorian = [[NSCalendar alloc]
initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *components =
[gregorian components:(NSCalendarUnitDay |
NSCalendarUnitYear|NSCalendarUnitMonth) fromDate:date];
NSDateComponents *CurrentDateComponent =
[gregorian components:(NSCalendarUnitHour |
NSCalendarUnitMinute|NSCalendarUnitSecond) fromDate:[NSDate date]];
[components setHour:[CurrentDateComponent hour]];
[components setMinute:[CurrentDateComponent minute]];
[components setSecond:[CurrentDateComponent second]];
[components setTimeZone:[NSTimeZone systemTimeZone]];
return [gregorian dateFromComponents:components];
}
I have created a calendar in my app, using the date object this way:
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *weekdayComponents = [gregorian components:(NSDayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit | NSMinuteCalendarUnit)fromDate:[NSDate date]];
NSInteger day = [weekdayComponents day];
NSInteger month = [weekdayComponents month];
NSInteger year = [weekdayComponents year];
m_dateFormatter.dateFormat = #"dd/MM/yyyy";
[gregorian setTimeZone:[NSTimeZone timeZoneWithAbbreviation:#"UTC"]];
NSDateComponents *timeZoneComps=[[NSDateComponents alloc] init];
[timeZoneComps setDay:day];
[timeZoneComps setMonth:month];
[timeZoneComps setYear:year];
[timeZoneComps setHour:00];
[timeZoneComps setMinute:00];
[timeZoneComps setSecond:01];
m_currentDate = [gregorian dateFromComponents:timeZoneComps];
When the user wants to go next month, I highlight the first date of that month. So, in this case, the date will be 1-06-2014,00:00:01.
Here is the code:
- (void)showNextMonth
{
// Move the date context to the next month
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *dateComps = [[NSDateComponents alloc] init];
[dateComps setMonth:1];
m_currentMonthContext =[gregorian dateByAddingComponents:dateComps toDate:m_currentMonthContext options:0];
NSDateComponents *weekdayComponents1 = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit) fromDate:m_currentMonthContext];
NSInteger nextMonth = [weekdayComponents1 month];
NSInteger nextyear = [weekdayComponents1 year];
NSDateComponents *weekdayComponents2 = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit) fromDate:m_currentDate];
NSInteger currentDay = [weekdayComponents2 day];
NSInteger currentMonth = [weekdayComponents2 month];
NSInteger currentYear = [weekdayComponents2 year];
NSInteger selectedDay = 1;
if(nextMonth == currentMonth && nextyear == currentYear)
{
selectedDay = currentDay;
}
NSInteger month = nextMonth;
[gregorian setTimeZone:[NSTimeZone timeZoneWithAbbreviation:#"UTC"]];
NSDateComponents *timeZoneComps=[[NSDateComponents alloc] init];
[timeZoneComps setDay:selectedDay];
[timeZoneComps setMonth:month];
[timeZoneComps setYear:nextyear];
[timeZoneComps setHour:00];
[timeZoneComps setMinute:00];
[timeZoneComps setSecond:01];
m_currentMonthContext =[gregorian dateFromComponents:timeZoneComps];
[self createCalendar];
}
When m_currentMonthContext is calculated on the second to last line of the above method, its value is 1-06-2014,00:00:01.
createCalendar implementation:
-(void)createCalendar
{
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *weekdayComponents = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit)fromDate:m_currentMonthContext];
NSInteger month = [weekdayComponents month];
NSInteger year = [weekdayComponents year];
}
Here I get month as 5 and year as 2014, but the date is 1-06-2014. This happens only in US time zone, in all other time zones it is working fine.
So I want to know how to handle timezones effectively, or in other sense, how to make sure that NSDate does not change even if time zone changes.
The proximate cause is that the time zone is not consistently set on the calendar when calculating dates and date components. Sometimes you set the time zone to UTC, and sometimes not, which is going to cause inconsistencies, as sometimes offsets for local time will be applied, and sometimes not.
In detail, in your situation, m_currentMonthContext is an NSDate which represents the UTC time one second after midnight on June 1st, 2014. In your createCalendar method, you create a calendar that is the local time of the user, and calculate the components for such a date. In all time zones in the US, it is still the month of May one second after midnight on June 1st, 2014 UTC. An example in code, that can be run in isolation:
NSCalendar *utcCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
[utcCalendar setTimeZone:[NSTimeZone timeZoneWithAbbreviation:#"UTC"]];
NSCalendar *localCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDate *june = [NSDate dateWithTimeIntervalSince1970:1401580801];
NSDateComponents *utcComponents = [utcCalendar components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:june];
NSDateComponents *localComponents = [localCalendar components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:june];
NSLog(#"utc : %#", utcComponents);
NSLog(#"local: %#", localComponents);
Here in MDT time zone, this logs:
utc :
Calendar Year: 2014
Month: 6
Leap month: no
Day: 1
local:
Calendar Year: 2014
Month: 5
Leap month: no
Day: 31
To recap, you're keeping a date in memory that's been calculated to represent a certain calendar date in UTC time, and then calculating the calendar date in the user's local time, but it seems you have an incorrect expectation that calendars for different time zones will interpret the same date the same way.
So, what to do? Your example is pretty complex, but it seems there's no need at all to store date components sometimes in UTC time zone and sometimes not - be consistent. Now, it also seems to me that you can be much much simpler in your code if you just want to find the first day of the next month.:
NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *comps = [cal components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:[NSDate date]];
[comps setMonth:[comps month] + 1];
[comps setDay:1];
I tested this with December 15th, 2014, and it worked to create January 1st, 2015 in my local time. Hopefully that is consistent behavior.
To sum up - it's very likely a bug to not use a consistent calendar for your date component calculations. Sometimes having UTC and sometimes local is going to cause you nightmares. It seems like you should always calculate in local time, but I don't know the whole context of your application so can't make a blanket statement for that. Also, it should be safe to not rely on incessant conversions between dates and date components, and instead have the date component be your source of truth. That is, I mean it seems convoluted to convert date components to dates always to store in instance variables, but then to immediately convert the dates back into date components every time they're used - it seems better to just work with date components as much as possible.
From the comment, I hope I understand your question correctly. You can try this code:-
NSDate * nowDate = [NSDate date];
NSLog(#"nowDate: %#",nowDate);
NSDateFormatter *df = [NSDateFormatter new];
[df setDateFormat:#"dd/MM/yyyy HH:mm"];
df.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:[NSTimeZone localTimeZone].secondsFromGMT];
NSString *localDate = [df stringFromDate:nowDate];
NSLog(#"localDate: %#", localDate);
Output:
2014-05-24 23:03:06.205 TestTimeZone[10214:60b] nowDate: 2014-05-24
15:03:06 +0000
2014-05-24 23:03:06.209 TestTimeZone[10214:60b] localDate: 24/05/2014
23:03
[NSDate date] always return GMT+0 date, no matter where is your timezone. May be just use this? At the same time I used NSDateFormatter to set to my local date based on my laptop. You can try to change to a few different timezones on your mac while running the above code on simulator. [NSDate date] might be just what you need.
I've been looking for a way to format the following NSDate object:
19 Feb 2013
Like that
18-25 Feb 2013
The 19 occurs within the week between the 18th and the 25th of Feb.
I couldn't an easy method to do so, is there build in functionality in to the NSDateFormater? should I implement it myself?
I don't think there is built in functionality in NSDateFormatter to do this. However, Apple has an example of how to get the NSDate values for the first and last days of a the week given a date. Here's their example on getting the Sunday of the current week:
NSDate *today = [[NSDate alloc] init];
NSCalendar *gregorian = [[NSCalendar alloc]
initWithCalendarIdentifier:NSGregorianCalendar];
// Get the weekday component of the current date
NSDateComponents *weekdayComponents = [gregorian components:NSWeekdayCalendarUnit
fromDate:today];
/*
Create a date components to represent the number of days to subtract from the current date.
The weekday value for Sunday in the Gregorian calendar is 1, so subtract 1 from the number of days to subtract from the date in question. (If today is Sunday, subtract 0 days.)
*/
NSDateComponents *componentsToSubtract = [[NSDateComponents alloc] init];
[componentsToSubtract setDay: 0 - ([weekdayComponents weekday] - 1)];
NSDate *beginningOfWeek = [gregorian dateByAddingComponents:componentsToSubtract
toDate:today options:0];
/*
Optional step:
beginningOfWeek now has the same hour, minute, and second as the original date (today).
To normalize to midnight, extract the year, month, and day components and create a new date from those components.
*/
NSDateComponents *components =
[gregorian components:(NSYearCalendarUnit | NSMonthCalendarUnit |
NSDayCalendarUnit) fromDate: beginningOfWeek];
beginningOfWeek = [gregorian dateFromComponents:components];
Since Sunday is not the beginning of the week in all locales, they also show how to get the beginning of the week as defined by the calendar's locale:
NSDate *today = [[NSDate alloc] init];
NSDate *beginningOfWeek = nil;
BOOL ok = [gregorian rangeOfUnit:NSWeekCalendarUnit startDate:&beginningOfWeek
interval:NULL forDate: today];
e.g. 01.10.2010 is friday => 27.09.2010 is monday.
I have no idea how to manage this one. btw: how can I calculate with dates?
For time/date calculations use NSDateComponents.
Listing 2 Getting the Sunday in the current week
NSDate *today = [[NSDate alloc] init];
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
// Get the weekday component of the current date
NSDateComponents *weekdayComponents = [gregorian components:NSWeekdayCalendarUnit fromDate:today];
/*
Create a date components to represent the number of days to subtract from the current date.
The weekday value for Sunday in the Gregorian calendar is 1, so subtract 1 from the number of days to subtract from the date in question. (If today's Sunday, subtract 0 days.)
*/
NSDateComponents *componentsToSubtract = [[NSDateComponents alloc] init];
[componentsToSubtract setDay: 0 - ([weekdayComponents weekday] - 1)];
NSDate *beginningOfWeek = [gregorian dateByAddingComponents:componentsToSubtract toDate:today options:0];
/*
Optional step:
beginningOfWeek now has the same hour, minute, and second as the original date (today).
To normalize to midnight, extract the year, month, and day components and create a new date from those components.
*/
NSDateComponents *components =
[gregorian components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit)
fromDate: beginningOfWeek];
beginningOfWeek = [gregorian dateFromComponents:components];
In later versions, there is a smarter way:
NSCalendar *cal = [NSCalendar currentCalendar];
[cal setFirstWeekday:2]; //2 is monday. 1:Sunday .. 7:Saturday don't set it, if user's locale should determine the start of a week
NSDate *now = [NSDate date];
NSDate *monday;
[cal rangeOfUnit:NSWeekCalendarUnit // we want to have the start of the week
startDate:&monday // we will write the date object to monday
interval:NULL // we don't care for the seconds a week has
forDate:now]; // we want the monday of today's week
If you actually change the weekday that represents the start of the week (Sunday vs. Monday), you should change it back after this snippet.