I'm trying to get the ordinal number of a week using NSCalendar so that I can calculate the number of weeks between two dates, however the method I'm using is demonstrating some weird behaviour.
I'm expecting a new week to begin every Sunday at 00:00:00, but instead it seems to happen at 23:58:45. I've tried changing the firstWeekday property of the calendar but that doesn't have any effect.
Example Code (note: 2014-03-09 is a Sunday)
- (void)testTimeWeeksBegins
{
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
// Make NSDates
NSDateComponents *comps = [[NSDateComponents alloc] init];
comps.year = 2014;
comps.month = 3;
comps.day = 9;
comps.hour = 23;
comps.minute = 58;
comps.second = 44;
NSDateComponents *compsToAdd = [[NSDateComponents alloc] init];
compsToAdd.second = 1;
NSDate *date23_58_44 = [calendar dateFromComponents:comps];
NSDate *date23_58_45 = [calendar dateByAddingComponents:compsToAdd toDate:date23_58_44 options:0];
// Calculate ordinality of week on both dates
NSUInteger ord23_58_44 = [calendar ordinalityOfUnit:NSWeekOfYearCalendarUnit inUnit:NSEraCalendarUnit forDate:date23_58_44];
NSUInteger ord23_58_45 = [calendar ordinalityOfUnit:NSWeekOfYearCalendarUnit inUnit:NSEraCalendarUnit forDate:date23_58_45];
// Output
NSLog(#"Date is %# and week number is %d", date23_58_44, ord23_58_44);
NSLog(#"Date is %# and week number is %d", date23_58_45, ord23_58_45);
}
Output
Date is 2014-03-09 23:58:44 +0000 and week number is 105043
Date is 2014-03-09 23:58:45 +0000 and week number is 105044
Am I being stupid and missing something obvious or is this a bug? I suppose my workaround would be to use a date with a time after 23:58:45, to ensure no problems in future?
Related
I have to find next Sunday date (NSDate) from device's current date.
I have used below logic:
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *components = [calendar components:NSCalendarUnitWeekday | NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay fromDate:self];
NSUInteger weekdayToday = [components weekday];
NSInteger daysToMonday = (8 - weekdayToday) % 7;
NSDate *weekEndPrev = [self dateByAddingTimeInterval:60*60*24*daysToMonday];
Here, in EST if time is near to 11 PM, I'm getting Monday as Weekend.
Let me know feasible solution. I have tried many options. Nothings works with 2 different timezones.
Thanks in Advance.
You need just to get the number of days you need and then just add them to your current date:
NSCalendar * calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDate * newDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:value toDate:date options:0];
where value - is the number of days to add
Find current week Sunday Date first then add 7 days using NSDateComponents.
NSDate *today = [[NSDate alloc] init];
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *weekdayComponents = [gregorian components:NSCalendarUnitWeekday fromDate:today];
NSDateComponents *componentsToSubtract = [[NSDateComponents alloc] init];
[componentsToSubtract setDay:(0 - ([weekdayComponents weekday] - 1))];
NSDate *sudayCurWeek = [gregorian dateByAddingComponents:componentsToSubtract toDate:today options:0];
NSDateComponents *offsetComponents = [[NSDateComponents alloc] init];
[offsetComponents setDay:7];
NSDate *nextSunday = [gregorian dateByAddingComponents:offsetComponents toDate:sudayCurWeek options:0];
NSLog(#"%#",nextSunday);
You can do this without any math, which is best left to NSCalendar as it handles daylight savings etc. First you need a calendar with the correct time zone. For example for IST:
NSCalendar *gc = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
gc.timeZone = [NSTimeZone timeZoneWithName:#"IST"];
Then you can use nextDateAfterDate:matchingUnit:value:options: to find the start of the next Sunday after a given time. For example:
NSDate *start = ... // the time you wish to start from
NSDate *nextSunday =
[gc nextDateAfterDate:start
matching:UnitNSCalendarUnitWeekday // match based on weekday number
value:1 // Sunday = weekday 1
options:NSCalendarMatchNextTime // read the docs!
];
The returned date, nextSunday, will be the next Sunday at 00:00 in the timezone of the calendar, gc.
HTH
Finally, I got solution..
NSDateComponents *components = [[NSCalendar currentCalendar] componentsInTimeZone:[NSTimeZone timeZoneWithAbbreviation:#"UTC"] fromDate:[NSDate date]]; // Pass date for which you want to find next Sunday
NSUInteger weekdayToday = [components weekday];
NSInteger daysToMonday = (8 - weekdayToday) % 7;
NSDate *weekEndPrev = [[NSDate date] dateByAddingTimeInterval:60*60*24*daysToMonday];
I need to know the NSDate's present in a particular week. I know the weekOfMonth, month and year.
For example, if my weekOfMonth is 0 and month is 2 and the year is 2016. I want to get the dates available in the current week (from Sunday to Saturday). But as the first day of this week (Sunday) falls on the previous month, I need to get the dates from 1-Feb-2016 to 6-Feb-2016 (Monday to Saturday).
NSDateComponents *comp = [[NSDateComponents alloc] init];
comp.month = components.month;
comp.year = components.year;
comp.day = ??; // What should I give here to get the current start date of the week?
Or if I could know the start date and end date of the current week, that would be also helpful.
here's a slightly clunky approach, based on getting the date using dayOfWeek = 0, and then stepping forward until the resultant date is in the same month
int monthTarget = 2;
int dayOfWeekTarget = 0;
int yearTarget = 2016;
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *comp = [[NSDateComponents alloc] init];
[comp setMonth:monthTarget];
[comp setYear:yearTarget];
[comp setWeekOfMonth:dayOfWeekTarget];
[comp setDay:dayOfWeekTarget];
// see what this gives us
NSDate *firstDate = [calendar dateFromComponents:comp];
NSDateComponents *firstDayComponents = [calendar components:NSCalendarUnitMonth | NSCalendarUnitDay fromDate:firstDate];
NSInteger day = [firstDayComponents day];
NSInteger month = [firstDayComponents month];
// move forward if we have to
while (month < monthTarget)
{
dayOfWeekTarget++;
[comp setDay:dayOfWeekTarget];
firstDate = [calendar dateFromComponents:comp];
firstDayComponents = [calendar components:NSCalendarUnitMonth | NSCalendarUnitDay fromDate:firstDate];
day = [firstDayComponents day];
month = [firstDayComponents month];
}
I'm sure you can tidy this up a bit, but the approach is here!
When trying to add a month using the suggested 'correct' method on all the stackoverflow questions I can find, the date I get back always loses an hour due to DST.
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *component = [[NSDateComponents alloc] init];
[component setDay:29];
[component setMonth:3];
[component setYear:2015];
NSDate *date = [gregorian dateFromComponents:component];
NSLog(#"Date: %#", date);
NSDateComponents *monthComponent = [[NSDateComponents alloc] init];
monthComponent.month = 1;
NSDate *newDate = [gregorian dateByAddingComponents:monthComponent toDate:date options:0];
NSLog(#"newDate: %#", newDate);
The output of this code is:
Date: 2015-03-29 00:00:00 +0000
newDate: 2015-04-28 23:00:00 +0000
How can I add a month across a DST change and keep the time at midnight?
Thanks for any help.
Example in response to gnasher729 below:
In the example the first two dates would be set at midnight and the second two would be 23:00. What would be the best practice to implement something like this while keeping all the outputted dates to the desired midnight time?
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *initialDateComponent = [[NSDateComponents alloc] init];
[initialDateComponent setDay:29];
[initialDateComponent setMonth:1];
[initialDateComponent setYear:2015];
NSDate *date = [gregorian initialDateComponent];
NSLog(#"Date: %#", date);
NSDateComponents *monthComponent = [[NSDateComponents alloc] init];
monthComponent.month = 1;
monthComponent.hour = 0;
NSMutableArray *eventAlarmDate = [[NSMutableArray alloc] init];
int numberOfRepeatingMonths = 4;
for(int x = 0; 0 < numberOfRepeatingMonths; x++) {
date = [gregorian dateByAddingComponents:monthComponent toDate:date options:0];
[eventAlarmDate addObject:date];
}
You are actually asking us how to introduce a bug in your code.
The result is correct. Adding one month during a DST change will and must add one hour more or one hour less than full days.
NSDate always displays its date in UTC, which is British time without DST correction. The date calculated is with DST corrections, and it depends on your location. If you are in India, midnight is at 5:30 am UTC (or 18:30 pm UTC on the previous day, not sure).
Before giving downvote, comment the reason
I have created a UIDatePicker with minimum & maximum date values (i.e. 6 months of date picker) Now, i need to get 7 days from selected date. There i need to check the conditions,
If date is today date i need to get 7 days from today onwards
If date is last date (i.e. last date of picker) need to get last 7 days including last day
If date is middle of today's date & last date i need to get last 3 days, next 3 days including today date. And, also while getting last & next 3 days it shouldn't get exceed with picker's date limit.
Here's my code snippet:
- (void)addDays:(NSInteger)range {
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:#"dd-MM-yyyy"];
NSDate *startDate = self.selectedDate;
for (int x = 0; x <= range; x++) {
NSLog(#"%#", [dateFormat stringFromDate:startDate]);
startDate = [startDate dateByAddingTimeInterval:(60 * 60 * 24)];
}
}
- (void)minusDays:(NSInteger)range {
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:#"dd-MM-yyyy"];
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *comps = [NSDateComponents new];
for (NSInteger i=0; i<range; i++) {
comps.day += -1;
NSDate *date = [calendar dateByAddingComponents:comps toDate:self.selectedDate options:0];
NSDateComponents *components = [calendar components:(NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit) fromDate:date];
NSLog(#"%#", [dateFormat stringFromDate:[calendar dateFromComponents:components]]);
}
}
- (void)calculateDateRange {
if ([dateArray count] > 0) {
[dateArray removeAllObjects];
}
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *lastcomponents = [calendar components:(NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit) fromDate:[NSDate date]];
lastcomponents.month += 6;
NSDateFormatter *formatter = [NSDateFormatter new];
[formatter setDateFormat:#"yyyy/MM/dd"];
NSDate *currentDate = [NSDate date];
NSDate *selectedD = self.selectedDate;
NSDate *endDate = [calendar dateFromComponents:lastcomponents];
NSDate *fromDate;
NSDate *toDate;
[calendar rangeOfUnit:NSDayCalendarUnit startDate:&fromDate interval:NULL forDate:currentDate];
[calendar rangeOfUnit:NSDayCalendarUnit startDate:&toDate interval:NULL forDate:selectedD];
NSDateComponents *difference = [calendar components:NSDayCalendarUnit fromDate:fromDate toDate:toDate options:0];
NSInteger first = [difference day];
[calendar rangeOfUnit:NSDayCalendarUnit startDate:&fromDate interval:NULL forDate:selectedD];
[calendar rangeOfUnit:NSDayCalendarUnit startDate:&toDate interval:NULL forDate:endDate];
NSDateComponents *difference2 = [calendar components:NSDayCalendarUnit fromDate:fromDate toDate:toDate options:0];
NSInteger second = [difference2 day];
if ((first == 0 || first < 3) && second > 7) {
[self addDays:7];
} else if (first >= 3 && second > 7) {
[self minusDays:3];
[self addDays:3];
}else if (second == 7 || second < 7) {
[self minusDays:7];
}
}
This is working fine. But, can't get exact last & previous days.
Anyone has idea on this?
My interpretation of your needs
You have a date picker. When a date is picked you need to create a 7 day range around that selected date.
So, if the selected date is 15/11/2014 then you want 3 days either side so...
12/11/2014 - 18/11/2014.
However, the date range cannot exceed the limits of the date picker. So if the minimum date on the date picker is set to 14/11/2014 then (in the above example) the date range would be...
14/11/2014 - 21/11/2014
Even after your additional explanation this is still my interpretation. And my code does exactly this.
Solution
You CANNOT use 60*60*24 to mean one day. This is just wrong. When dealing with dates you should always be using NSDateComponents and NSCalendar.
Also, break down your problem into small steps. There is no reason to do everything in one giant function.
OK I guess you have a datePicker action somewhere so I'd code it like this...
- (void)datePickerDateChanged
{
NSDate *minimumDate = self.datePicker.minimumDate;
NSDate *maximumDate = self.datePicker.maximumDate;
NSDate *selectedDate = self.datePicker.date;
NSDate *startDate;
NSDate *endDate;
if ([self numberOfDaysFromDate:minimumDate toDate:selectedDate] < 3) {
// get 7 days after minimumDate
startDate = minimumDate;
endDate = [self dateByAddingDays:6 toDate:minimumDate];
} else if ([self numberOfDaysFromDate:selectedDate toDate:maximumDate] < 3) {
// get 7 days before maximumDate
startDate = [self dateByAddingDays:-6 toDate:maximumDate];
endDate = maximumDate;
} else {
// get 3 days before and 3 days after selectedDate
startDate = [self dateByAddingDays:-3 toDate:selectedDate];
endDate = [self dateByAddingDays:3 toDate:selectedDate];
}
// Here startDate and endDate define your date range.
}
- (NSDate *)dateByAddingDays:(NSInteger)days toDate:(NSDate *)date
{
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components = [NSDateComponents new];
components.day = days;
return [calendar dateByAddingComponents:components toDate:date options:0];
}
- (NSInteger)numberOfDaysFromDate:(NSDate *)fromDate toDate:(NSDate *)toDate
{
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components = [calendar components:NSDayCalendarUnit fromDate:fromDate toDate:toDate options:0];
// always return positive. We just want magnitude of days.
return components.day > 0 ? components.day : -components.day;
}
This is untested and just a first attempt.
I'm trying to work out the number of days between two dates. Here is how I am doing it:
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
unsigned int calendarFlags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
NSDate *dateToCheck = (self.subscriptionEnd ?: self.trialEnd);
NSLog(#"dateToCheck: %#", dateToCheck);
NSLog(#"current date: %#", [self systemTimeZoneDate]);
NSDateComponents *components = [gregorian components:calendarFlags
fromDate:[self systemTimeZoneDate]
toDate:dateToCheck
options:0];
return [components day] >= 0 ?: 0;
At the time of this writing, NSLog outputted the following:
2014-09-05 22:56:20.054 tweepy[9635:60b] dateToCheck: 2014-10-05 08:02:51 PM +0000
2014-09-05 22:56:20.057 tweepy[9635:60b] current date: 2014-09-05 10:56:20 PM +0000
It is returning a difference of one day because iOS thinks that the day is in YDM format.
Should I be indicating the date format somewhere?
Here is the code of how self.subscriptionEnd is setup:
NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
dateComponents.day = 30;
self.subscriptionEnd = [gregorianCalendar dateByAddingComponents:dateComponents toDate:[self systemTimeZoneDate] options:0];
Here is the code of how self.trialEnd is setup:
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
dateComponents.day = 2;
self.trialEnd = [gregorian dateByAddingComponents:dateComponents toDate:[self systemTimeZoneDate] options:0];
The date format doesn't have anything to do with it. What is happening is the NSDateComponents are giving you what you are asking for, the total difference in years, months, and days. If you want just the days, you need to only provide NSDayCalendarUnit. The docs make this clear:
Some operations can be ambiguous, and the behavior of the computation is calendar-specific, but generally larger components will be computed before smaller components; for example, in the Gregorian calendar a result might be 1 month and 5 days instead of, for example, 0 months and 35 days.