Is there an easy way to calculate dates according to a criteria with NSCalendar and/or NSCalendarComponents? I've been looking at the documentation for a while but it seems a bit complicated. For example, I need
"Wednesday 6:00pm"s for the next 15 even-numbered weeks (including this one if it's even numbered and not past that date/time yet.)
Here is a method for determining which Wednesday to start with and then getting the next 14 even week Wednesdays. This doesn't take the time portion of your question into account, but you can use similar methods to handle the time.
+ (NSArray *)nextFifteenEvenNumberedWeekWednesdays:(NSDate *)startingDate
{
static const NSInteger Wednesday = 4;
static const NSInteger OneDay = 60 * 60 * 24;
NSCalendar *calender = [NSCalendar currentCalendar];
NSDateComponents *startingDateComponents = [calender components:(NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitWeekday | NSCalendarUnitWeekOfYear)
fromDate:startingDate];
// Go to the closest Wednesday
NSInteger daysToWednesday = Wednesday - startingDateComponents.weekday;
if (startingDateComponents.weekday > Wednesday) {
// We already passed wednesday, so go to the next wednesday
if (startingDateComponents.weekOfYear % 2 == 0) {
// If this date is past Wednesday but is on an even week, then moving to the next wednesday
// will be an odd week. So skip forward one week.
startingDate = [startingDate dateByAddingTimeInterval:7 * OneDay];
}
daysToWednesday = 7 + daysToWednesday; // get the next Wednesday
}
startingDate = [startingDate dateByAddingTimeInterval:daysToWednesday * OneDay]; // Move to wednesday
NSMutableArray *wednesdays = [NSMutableArray array];
for (NSInteger i = 0; i < 15; i++) {
// Add every other wednesday
[wednesdays addObject:[startingDate dateByAddingTimeInterval:14 * i * OneDay]];
}
return [wednesdays copy];
}
I ended up answering my own question. It's really easy when you know -[NSCalendar dateBySettingUnit:value:ofDate:options:] auto-increments forward to the next matching date.
NSCalendar *calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
NSDate *date = [NSDate date];
// find the next Wednesday 6 o'clock
date = [calendar dateBySettingUnit:NSCalendarUnitSecond value:0 ofDate:date options:0];
date = [calendar dateBySettingUnit:NSCalendarUnitMinute value:0 ofDate:date options:0];
date = [calendar dateBySettingUnit:NSCalendarUnitHour value:18 ofDate:date options:0];
date = [calendar dateBySettingUnit:NSCalendarUnitWeekday value:4 ofDate:date options:0];
// skip one week if it's not an even week
NSDateComponents *components = [calendar components:NSCalendarUnitWeekOfYear fromDate:date];
date = [calendar dateByAddingUnit:NSCalendarUnitWeekOfYear value:components.weekOfYear % 2 toDate:date options:0];
// 15 weeks of this
for (int i = 0; i < 15; i++)
{
NSDate *testDate = [calendar dateByAddingUnit:NSCalendarUnitWeekOfYear value:i * 2 toDate:date options:0];
NSLog(#"Date: %#", testDate);
}
Related
Can get last weekday in the month through NSDateComponents? For example: last monday in month or last friday in month. etc
Just another way to solve. Find the first day of the next month, then search backwards for Tuesday.
let calendar = NSCalendar.currentCalendar()
let today = NSDate()
let components = calendar.components([.Year, .Month], fromDate: today)
components.month += 1
// If today is 2016-07-12 then nextMonth will be the
// first day of the next month: 2016-08-01
if let nextMonth = calendar.dateFromComponents(components) {
// Search backwards for weekday = Tuesday
let options: NSCalendarOptions = [.MatchPreviousTimePreservingSmallerUnits, .SearchBackwards]
calendar.nextDateAfterDate(nextMonth, matchingUnit: .Weekday, value: 3, options: options)
}
Here's a solution; tested, and fairly robust, I think.
We'll let NSCalendar walk through the month, a day at a time, and pull out all matching weekdays as NSDates. Then you can answer questions like, "The 3rd Wednesday of this month"
I believe the comments are clear about what is happening, and why.
If you need further clarification, I'll be happy to do so.
//What weekday are we interested in? 1 = Sunday . . . 7 = Saturday
NSInteger targetWeekday = 1;
//Using this methodology, GMT timezone is important to set
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
calendar.timeZone = [NSTimeZone timeZoneWithAbbreviation:#"GMT"];
//set the components to the first of the current month
NSDateComponents *startComponents = [calendar components:NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
startComponents.day = 1;
//the enumeration starts "afterDate", so shift the start back one day (86400 seconds) to include the 1st of the month
NSDate *startDate = [[calendar dateFromComponents:startComponents] dateByAddingTimeInterval:-86400];
//the enumeration searches for a match; we'll match at the midnight hour and find every occurance of midnight
NSDateComponents *dayByDay = [[NSDateComponents alloc] init];
dayByDay.hour = 0;
//I've opted to put all matching weekdays of the month into an array, so you can find any instance easily
__block NSMutableArray *foundDates = [NSMutableArray arrayWithCapacity:5];
[calendar enumerateDatesStartingAfterDate:startDate
matchingComponents:dayByDay
options:NSCalendarMatchPreviousTimePreservingSmallerUnits
usingBlock:^(NSDate *date, BOOL exactMatch, BOOL *stop){
NSDateComponents *thisComponents = [calendar components:NSCalendarUnitMonth | NSCalendarUnitWeekday fromDate:date];
//as long as the month stays the same... (or the year, if you wanted to)
if (thisComponents.month == startComponents.month) {
//does this date match our target weekday search?
if (thisComponents.weekday == targetWeekday) {
//then add it to our result array
[foundDates addObject:date];
}
//once the month has changed, we're done
} else {
*stop = YES;
}
}];
//Now, with our search result array, we can find the 1st, last, or any specific occurance of that weekday on that month
NSLog(#"Found these: %#", foundDates);
So, if you only wanted the last one, then just use [foundDates lastObject]
Try this code. This works for me.
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *comps = [gregorian components:NSWeekdayCalendarUnit fromDate:[NSDate date]];
int weekday = [comps weekday];
int lastTues;
int lastSatDay;
if (weekday==1) {
lastTues=5;
lastSatDay=1;
}
else if (weekday==2)
{
lastTues=6;
lastSatDay=2;
}
else if (weekday==3)
{
lastTues=7;
lastSatDay=3;
}
else if (weekday==4)
{
lastTues=1;
lastSatDay=4;
}
else if (weekday==5)
{
lastTues=2;
lastSatDay=5;
}
else if (weekday==6)
{
lastTues=3;
lastSatDay=6;
}
else if (weekday==7)
{
lastTues=4;
lastSatDay=7;
}
NSDate *lastTuesDay = [[NSDate date] addTimeInterval:-3600*24*(lastTues)];
NSDate *lastSaturday = [[NSDate date] addTimeInterval:-3600*24*(lastSatDay)];
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!
I am trying to determine if the current date is in fact three days or less from the end of the month. In other words, if I am in August, then I would like to be alerted if it is the 28,29,30, or 31st. If I am in February, then I would like to be notified when it is the 25,26,27, or 28 (or even 29). In the case of a leap year, I would be alerted from 26th onwards.
My problem is that I am not sure how to perform such a check so that it works for any month. Here is my code that I have thus far:
-(BOOL)monthEndCheck {
NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
NSInteger day = [components day];
NSInteger month = [components month];
NSInteger year = [components year];
if (month is 3 days or less from the end of the month for any month) {
return YES;
} else {
return NO;
}
}
Because there are months with 28, 30, and 31 days, I would like a dynamic solution, rather than creating a whole series of if/else statements for each and every condition. Is there a way to do this?
This is how you get the last day of the month:
NSDate *curDate = [NSDate date];
NSCalendar* calendar = [NSCalendar currentCalendar];
NSDateComponents* comps = [calendar components:NSYearCalendarUnit|NSMonthCalendarUnit|NSWeekCalendarUnit|NSWeekdayCalendarUnit fromDate:curDate]; // Get necessary date components
// set last of month
[comps setMonth:[comps month]+1];
[comps setDay:0];
NSDate *tDateMonth = [calendar dateFromComponents:comps];
NSLog(#"%#", tDateMonth);
Source: Getting the last day of a month
EDIT (another source): How to retrive Last date of month give month as parameter in iphone
Now you can simply count from the current date.
If < 3 do whatever you wanted to do.
Maybe something like this:
NSTimeInterval distanceBetweenDates = [date1 timeIntervalSinceDate:date2];
double timeInSecondsFor3Days = 280000; //Better use NSDateComponents here!
NSInteger hoursBetweenDates = distanceBetweenDates / timeInSecondsFor3Days;
However I did not test that^^
EDIT: Thanks to Aaron. Do NSDateComponents to calculate the time for three days instead!
First you have to compute the start of the current day (i.e. today at 00.00).
Otherwise, the current day will not count as a full day when computing the
difference between today and the start of the next month.
NSDate *now = [NSDate date];
NSCalendar *cal = [NSCalendar currentCalendar];
NSDate *startOfToday;
[cal rangeOfUnit:NSCalendarUnitDay startDate:&startOfToday interval:NULL forDate:now];
Computing the start of the next month can be done with rangeOfUnit:...
(using a "statement expression" to be fancy :)
NSDate *startOfNextMonth = ({
NSDate *startOfThisMonth;
NSTimeInterval lengthOfThisMonth;
[cal rangeOfUnit:NSCalendarUnitMonth startDate:&startOfThisMonth interval:&lengthOfThisMonth forDate:now];
[startOfThisMonth dateByAddingTimeInterval:lengthOfThisMonth];
});
And finally the difference in days:
NSDateComponents *comp = [cal components:NSCalendarUnitDay fromDate:startOfToday toDate:startOfNextMonth options:0];
if (comp.day < 4) {
// ...
}
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?
I am looking to display the amount of months from an NSDate object.
//Make Date Six Months In The Future
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *sixMonthsFromNow = [[NSDateComponents alloc] init];
[sixMonthsFromNow setMonth:6];
NSDate *finishDate = [calendar dateByAddingComponents:sixMonthsFromNow toDate:[NSDate date] options:0]; //Six Months Time
//Display
NSCalendarUnit requiredFormat = NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *dateComponents = [calendar components:requiredFormat fromDate:[NSDate date] toDate:finishDate options:0];
NSLog(#"%d months %d days %d hours %d minutes %d seconds", [dateComponents month], [dateComponents day], [dateComponents hour], [dateComponents minute], [dateComponents second]);
This outputs: 5 months 29 days 23 hours 59 minutes 59 seconds
Which is great, but I only wish to display the amount of months.
If I limit to only months:
//Make Date Six Months In The Future
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *sixMonthsFromNow = [[NSDateComponents alloc] init];
[sixMonthsFromNow setMonth:6];
NSDate *finishDate = [calendar dateByAddingComponents:sixMonthsFromNow toDate:[NSDate date] options:0]; //Six Months Time
//Display
NSCalendarUnit requiredFormat = NSMonthCalendarUnit;
NSDateComponents *dateComponents = [calendar components:requiredFormat fromDate:[NSDate date] toDate:finishDate options:0];
NSLog(#"%d months", [dateComponents month]);
This outputs: 5 months
Although this is technically correct, I would like to round the amount of days to make it output six months.
Is there an easy way to achieve this effect? I noticed there wasn't a rounding property on NSDateComponents. Will I have to manually check the amount of days and decide to round up?
My end goal is to not only limit the rounding effect to months, this should be able to round hours to days if i only supplied: NSDayCalendarUnit
The following method could to what you want.
The idea is that after computing the (rounded down) number of calendar units
between start date and end date, add both that amount and one more to the start date
and check which one is closer to the end date:
#interface NSCalendar (MyCategory)
-(NSInteger)roundedUnit:(NSCalendarUnit)unit fromDate:(NSDate *)fromDate toDate:(NSDate *)toDate;
#end
#implementation NSCalendar (MyCategory)
-(NSInteger)roundedUnit:(NSCalendarUnit)unit fromDate:(NSDate *)fromDate toDate:(NSDate *)toDate
{
// Number of units between the two dates:
NSDateComponents *comps = [self components:unit fromDate:fromDate toDate:toDate options:0];
NSInteger value = [comps valueForComponent:unit];
// Add (value) units to fromDate:
NSDate *date1 = [self dateByAddingComponents:comps toDate:fromDate options:0];
// Add (value + 1) units to fromDate:
[comps setValue:(value + 1) forComponent:unit];
NSDate *date2 = [self dateByAddingComponents:comps toDate:fromDate options:0];
// Now date1 <= toDate < date2. Check which one is closer,
// and return the corresponding value:
NSTimeInterval diff1 = [toDate timeIntervalSinceDate:date1];
NSTimeInterval diff2 = [date2 timeIntervalSinceDate:toDate];
return (diff1 <= diff2 ? value : value + 1);
}
#end
And you would use it as
NSInteger months = [calendar roundedUnit:NSMonthCalendarUnit fromDate:[NSDate date] toDate:finishDate];
The code uses utility methods for NSDateComponents from
https://github.com/henrinormak/NSDateComponents-HNExtensions/blob/master/README.md:
- (void)setValue:(NSInteger)value forComponent:(NSCalendarUnit)unit;
- (NSInteger)valueForComponent:(NSCalendarUnit)unit;
These methods are new in OS X 10.9, but not available in iOS 7.
As the other commenter pointed out, you're calling NSDate twice, and the second date is slightly later than the first. Use the same date object twice and you won't get rounding errors.
As for how to round:
I don't think there is any rounding built into NSCalendars calendrical calculation methods.
You need to decide what that means to you. You might round at the halfway point. In that case, you could ask the calendar how many days there are in the current month, using the rangeOfUnit:inUnit:forDate method, and then add half that many days to the end date, then ask for the month. If you only want to "round up within a week, then only add a week to the end date before asking for the number of months difference.
I've got a similar problem:
let fmt1 = NSDateComponentsFormatter()
fmt1.allowedUnits = .CalendarUnitYear |
.CalendarUnitMonth |
.CalendarUnitDay
let dc1 = NSDateComponents()
dc1.month = 1
dc1.day = 15
// This occasionally outputs "1 month, 14 days" instead of
// "1 month, 15 days" when the formatter is constrained to
// years, months and days. If formatter is allowed to use
// all units, the output is "1 month, 14 days, 23 hours,
// 59 minutes, 59 seconds".
fmt1.stringFromDateComponents(dc1)
This can be fixed by specifying a positive amount of seconds and combining it with
setting maximumUnitCount formatter parameter to a fixed value:
let fmt2 = NSDateComponentsFormatter()
fmt2.allowedUnits = .CalendarUnitYear |
.CalendarUnitMonth |
.CalendarUnitDay
fmt2.maximumUnitCount = 2
fmt2.collapsesLargestUnit = true
let dc2 = NSDateComponents()
dc2.month = 1
dc2.day = 15
dc2.second = 10
// This always outputs "1 month, 15 days"
fmt2.stringFromDateComponents(dc2)
This fix will probably work in your case too: just assign some positive amount of seconds to NSDateComponents before calling dateByAddingComponents:
[sixMonthsFromNow setMonth: 6];
[sixMonthsFromNow setSecond: 10];