Given a date (for example today 4 Feb 2014), I would like to get a list of NSDate instances that go from Monday 27 Jan 2014 to Sunday 2 Mar 2014.
Basically all the dates in the current month, plus the dates from the last week of the previous month, and the first week of the next month, if they share the same week with some of the dates in the current month.
How can I achieve this?
I can think of a way to obtain this (pseudo-code below), but it's way too long and complicated. Is there any simpler way, like a method that the SDK provides to short cut?
Extract the month component from [NSDate date] (today)
Construct the first day of the month
Calculate its weekday
If it's Wednesday (3rd day of the week) then add today-1, today-2 to the list and so on
Repeat from step 2, but with the last day of the month
Also to all the elitists out there, just because I'm not posting any code, doesn't mean this is not a coding question. The problem is real (construct a calendar grid of a month), and finding the right algorithm/method before coding is much better than playing around and manually do the maths with NSDate and NSCalendar (very error prone, as I will need to take into account all the weird cases). I figure many people have already encountered this same problem and if they could share some pointers, great. If you don't want to answer, no need to reply.
I'm going to post very general code. You are going to need to make it specific to your needs.
//Setup the calendar object
NSCalendar *calendar = [NSCalendar currentCalendar];
//Get the current date's month
NSUInteger month = [dateComponents month];
//Create an NSDate for the first and last day of the month
NSDateComponents *comp = [calendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:[NSDate date]];
[comp setDay:1];
NSDate *firstOfMonth = [calendar dateFromComponents:comp];
[comp setMonth:[comp month]+1];
[comp setDay:0];
NSDate *lastOfMonth = [calendar dateFromComponents:comp];
//Now get the first and last week number from there
NSUInteger unitFlags = NSWeekCalendarUnit;
//Create a date component object from today's date.
NSDateComponents *firstDateComponents = [calendar components:unitFlags
fromDate:firstOfMonth // for current date
options:0];
NSDateComponents *lastDateComponents = [calendar components:unitFlags
fromDate:lastOfMonth // for current date
options:0];
NSUInteger firstWeek = [firstDateComponents week];
NSUInteger lastWeek = [lastDateComponents week];
Now that you have the first and last weeks you can start at the first day of the first week and go through the last day of the last week to set up your calendar. Good luck.
Swift 5:
extension Date {
var startOfMonth: Date {
let calendar = Calendar(identifier: .gregorian)
let components = calendar.dateComponents([.year, .month], from: self)
return calendar.date(from: components)!
}
var endOfMonth: Date {
var components = DateComponents()
components.month = 1
components.second = -1
return Calendar(identifier: .gregorian).date(byAdding: components, to: startOfMonth)!
}
}
let date = Date()
let firstWeek = Calendar.current.dateComponents([.weekOfYear], from: date.startOfMonth)
let lastWeek = Calendar.current.dateComponents([.weekOfYear], from: date.endOfMonth)
Related
I'm trying to test one of my methods can compare two times correctly. It works fine unless I add a timezone to my component as follow:
_calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *components = [[NSDateComponents alloc] init];
components.second = 31;
components.minute = 21;
components.hour = 5;
components.day = 30;
components.month = 3;
components.year = 2016;
components.timeZone = [NSTimeZone timeZoneWithName:#"UTC"];
_mar30_2016Morning = [_calendar dateFromComponents:components];
components.second = 01;
components.minute = 00;
components.hour = 22;
components.day = 30;
components.month = 3;
components.year = 2016;
components.timeZone = [NSTimeZone timeZoneWithName:#"UTC"];
_mar30_2016Evening = [_calendar dateFromComponents:components];
//Test two dates are equal
-(void) testCompareWithoutTimeTo
{
XCTAssertEqual([_mar30_2016Morning compareWithoutTimeTo:_mar30_2016Evening],NSOrderedSame);
}
Firs way:
-(NSComparisonResult) compareWithoutTimeTo:(NSDate *) otherDate {
NSDate *this = [self copy];
NSDate *that = [otherDate copy];
return [[NSCalendar currentCalendar] compareDate:this toDate:that toUnitGranularity:NSCalendarUnitDay];;
}
Second way:
NSCalendar* calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
[[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitDay startDate:&this interval:NULL forDate:this];
[[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitDay startDate:&that interval:NULL forDate:that];
NSComparisonResult result = [this compare:that];
These dates are in same day and it must return same but it is returning it is different days?!
This is quite confusing and not very well documented.
#RobMayoff told you what was wrong, but it bears further explanation.
Here's my understanding of how it works. You create NSDateComponents and give them a time zone. You then convert them to NSDates, which discards the time zone info and expresses those dates as an offset from the iOS epoch time, ALWAYS in UTC. (NSDate objects don't have a time zone They represent an instant in time anywhere on Earth.)
Finally, you ask a calendar object to compare the 2 dates. The calendar object converts the provided dates to its time zone before doing the comparison. If the 2 dates fall on different days in the calendar's current time zone, you'll get unequal days.
The solution is to create a calendar and set it's time zone to UTC before doing the comparison.
So simply add a second line to the code you posted:
_calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
_calendar.timeZone = [NSTimeZone timeZoneWithName:#"UTC"]; //This is the fix
EDIT:
As rmaddy points out in his comment, there's no requirement that you compare dates in UTC. You just need to make sure you create the dates and compare them using the same time zone. Since you create your input dates using NSDateComponents with a time zone of UTC, you need to compare them with a calendar that is also set to UTC.
You're not setting the time zone of the calendar, so it's using the device's time zone setting, which is probably not UTC. Those two NSDate values fall on different calendar days in many time zones.
The common question on stackoverflow is how to get the Day of Year from a date but how to you get the date from the Day of Year?
I'm using the following code to generate the Day of Year but how to I do the converse?
// Calculate Day of the Year
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSUInteger dayOfYear = [gregorian ordinalityOfUnit:NSDayCalendarUnit inUnit:NSYearCalendarUnit forDate:[NSDate date]];
setDayOfYear = dayOfYear;
return setDayOfYear;
Assuming you know the year you want and the actual time of the date doesn't matter and you have the correct locale and time zones set up, you can just convert the day of the year and year from components back into a date. Again, you may need to decide what your application needs to do about the time component and you have to make sure that the day and year actually make sense for the calendar, but assuming the gregorian calendar you listed in your example, and the 200th day of 2013 (19 July, 2013), you could so something like this:
NSDateComponents* components = [[NSDateComponents alloc] init];
[components setDay:200];
[components setYear:2013];
NSDate* july19_2013 = [gregorian dateFromComponents:components];
If I were to run this, I'd get a date that's 19 July, 2013 # 07:00 since my current locale is in a time zone equiv of America/Los_Angeles and I didn't configure the calendar.
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 need to calculate the weekday for a given date, however, depending on the calendar a week can star on Monday and somwhere on Sunday
so i wanted to set it, to start on Monday, using
[[NSCalendar currentCalendar] setFirstWeekday:2];
however, the calculation outcome is the same
{
[[NSCalendar currentCalendar] setFirstWeekday:1];
NSDateComponents *weekdayComponents = [[NSCalendar currentCalendar] components:(NSDayCalendarUnit | NSWeekdayCalendarUnit) fromDate:date];
NSInteger weekday = [weekdayComponents weekday] - 1;
NSLog(#"%d", weekday);
}
{
[[NSCalendar currentCalendar] setFirstWeekday:2];
NSDateComponents *weekdayComponents = [[NSCalendar currentCalendar] components:(NSDayCalendarUnit | NSWeekdayCalendarUnit) fromDate:date];
NSInteger weekday = [weekdayComponents weekday] - 1;
NSLog(#"%d", weekday);
}
returns same numbers, but why?
The behavior you see is correct. The weekday component is not affected by the firstWeekday property. If you have a date representing a sunday it will always be a sunday wether you start your week on that sunday or on monday. What this should affect is the week number in the week property of your date components.
I believe you need to use the ordinalityOfUnit:inUnit:forDate: method rather than attempting to extract the date components. So something like this:
NSUInteger weekday = [[NSCalendar currentCalendar] ordinalityOfUnit:NSWeekdayCalendarUnit inUnit:NSWeekCalendarUnit forDate:date];
Basically that call is asking for the day (NSWeekdayCalendarUnit) in the week (NSWeekCalendarUnit) for the given date.
If that doesn't work as is, you may need to create your own calendar, rather than trying to modifying the first week day on the currentCalendar.
For example:
NSCalendarIdentifier calendarIdentifier = [[NSCalendar currentCalendar] calendarIdentifier];
NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:calendarIdentifier] autorelease];
[calendar setFirstWeekday:2];
Then use that new calendar object rather than [NSCalendar currentCalendar] in the ordinalityOfUnit:inUnit:forDate: call.
A useful function to get corrected weekday number for different firstWeekday cases
Swift 3.0
func dayOfWeek(day: Int, month: Int, year: Int) -> Int {
let calendar = Calendar.current
let dateComponents = DateComponents(year: year, month: month, day: day)
guard let date = calendar.date(from: dateComponents) else {
fatalError("Can't create date form specified date components")
}
var weekday = calendar.component(.weekday, from: date)
//handling the case when calendar starts from Monday: firstWeekday == 2
if calendar.firstWeekday == 2 {
weekday = (weekday == 1) ? 7 : (weekday - 1)
}
return weekday
}
How can I find out the Date of Monday if I pass the week number. For example, I need the Monday of the Week 24 of the year
Fill out a NSDateComponents object with the information you have. Then you can ask the current NSCalendar what date corresponds to those components.
NSDateComponents *components = [NSDateComponents new];
components.weekday = 2;
components.weekOfYear = 24;
components.year = 2012;
NSDate *date = [[NSCalendar currentCalendar] dateFromComponents: components];
The weekday property starts with Sunday = 1.
If you are on OS X 10.7 or newer you should use the property yearForWeekOfYear instead of year.