Programmatically getting the date "next Sunday at 5PM" - objective-c

Edited 07/08/13: Apple has an excellent set of WWDC videos that really helped me understand the various date and time classes in Objective-C, and how to correctly perform time calculations/manipulations with them.
"Solutions to Common Date and Time Challenges" (HD video, SD video, slides (PDF)) (WWDC 2013)
"Performing Calendar Calculations" (SD video, slides (PDF)) (WWDC 2011)
Note: links require a free Apple Developer membership.
I'm writing an app for a friend's podcast. She broadcasts her show live every Sunday at 5PM, and I would like to write some code in my app to optionally schedule a local notification for that time, so that the user is reminded of when the next live show is. How would I go about getting an NSDate object that represents "the next Sunday, at 5 PM Pacific time." (obviously this would have to be converted into whatever timezone the user is using)

First get the current day of the week:
NSDate *now = [NSDate date];
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *dateComponents = [calendar components:NSCalendarUnitWeekday | NSCalendarUnitHour fromDate:now];
NSInteger weekday = [dateComponents weekday];
The Apple docs define a weekday as:
Weekday units are the numbers 1 through n, where n is the number of
days in the week. For example, in the Gregorian calendar, n is 7 and
Sunday is represented by 1.
Next figure out how many days to add to get to the next sunday at 5:
NSDate *nextSunday = nil;
if (weekday == 1 && [dateComponents hour] < 5) {
// The next Sunday is today
nextSunday = now;
} else {
NSInteger daysTillNextSunday = 8 - weekday;
int secondsInDay = 86400; // 24 * 60 * 60
nextSunday = [now dateByAddingTimeInterval:secondsInDay * daysTillNextSunday];
}
To get it at 5:00 you can just change the hour and minute on nextSunday to 5:00. Take a look at get current date from [NSDate date] but set the time to 10:00 am

Related

Given an NSDate, find the last day of fourth prior month

I am trying to calculate an NSDate object based on the current date. If the current date is April 1st, 2015, I need to generate the date, December 31, 2014. If the current date is April 30th, 2015, I STILL need to generate the date, December 31, 2014. If however, it is May 1st, 2015, I need to generate January 31st, 2015. In other words, whatever month I am in, I need the date of the end of the month, from four months ago, regardless of where I am in the current month.
The code I have thus far is:
NSCalendar *theCalendar = [NSCalendar currentCalendar];
NSDateComponents *dayComponent = [[NSDateComponents alloc] init];
[dayComponent setDay:-90];
NSDate *nextDate = [theCalendar dateByAddingComponents:dayComponent toDate:[NSDate date] options:0];
NSLog(#"The date I am getting is: %#", nextDate);
The above code gives me the date value of exactly 90 days prior to the current date, but I need the date to always be the end of the month that is 4 months earlier.
As you've already discovered, you need a starting date and a calendar:
NSDate *startingDate = [NSDate date];
NSCalendar *calendar = [NSCalendar autoupdatingCurrentCalendar];
You'll need the components of the current date but only down to the current month, because you don't care about the specific day within the month:
NSDateComponents *components = [calendar
components:NSCalendarUnitEra | NSCalendarUnitYear | NSCalendarUnitMonth
fromDate:startingDate];
You say you want the last day of the fourth prior month. Since months have different numbers of days, the last day varies depending on the month. But all months have first days, and those first days are always numbered 1. So it's easiest to compute “the last day of the fourth prior month” by first going back three months:
components.month -= 3;
Then, go one day prior to that month:
components.day = -1;
Finally, you need to get clear in your head that an NSDate represents an instant in time, but a day (like “April 1st, 2015”) is an interval of time, starting and ending at specific instants. If you're going to represent a whole day using an NSDate, you're going to be storing one instant within that interval. You don't want to store the first or last instant (which will both be midnights); that causes problems for some days in some time zones. Instead, use noon as your instant:
components.hour = 12;
Now you're ready to ask the calendar for a new NSDate:
NSDate *lastDayOfFourthPriorMonth = [calendar dateFromComponents:components];
You want to get familiar with NSCalendar and NSDateComponents. If you read those two API documents, you will be able to assemble the answer.
Conceptually, you want to use NSCalendar components:fromDate: to get the components (day, month, year) of the current date.
You will then walk that month value back 4 months. Now if that wraps past January, you know you need to determine how much, so that you adjust and stay within months [1..12]. Further, you'll need to decrement the year at that point too.
Knowing the month, you can find the last day of that month through several means; the crudest of which is to maintain your own enum...but there's probably a better way using NSCalendar that will also account for February in leap years.
At the end, you can build the resultant date from the components you've assembled, using the NSCalendar method dateFromComponents:.
See #rob mayoff's excellent answer which is a more concrete realization of this theory and IMO, the correct answer.

Where is the extra 75 seconds coming from?

While writing some unit tests on a Julian Day calculator, I found that dates prior to 2nd December 1847 were being initialised incorrectly by NSDate. They appear to have 75 seconds added on. I haven't been able to find anything pointing to that date (which is well after the Gregorian calendar cutoff). Is it a bug or is there a historic calendar adjustment that I've not come across?
int main(int argc, const char * argv[])
{
#autoreleasepool {
NSCalendar *cal = [NSCalendar currentCalendar];
NSDateComponents *dateComps = [NSDateComponents new];
dateComps.year = 1847;
dateComps.month = 12;
dateComps.day = 1;
NSDate *d1 = [cal dateFromComponents:dateComps];
NSLog(#"d1 = %#", d1);
dateComps = [NSDateComponents new];
dateComps.year = 1847;
dateComps.month = 12;
dateComps.day = 2;
NSDate *d2 = [cal dateFromComponents:dateComps];
NSLog(#"d2 = %#", d2);
}
return 0;
}
Output:
d1 = 1847-12-01 00:01:15 +0000
d2 = 1847-12-02 00:00:00 +0000
According to http://www.timeanddate.com/worldclock/clockchange.html?n=136&year=1847 there was a time shift forward of 75 seconds at that time.
In London, when local time was about to reach 12:00:00 AM on Wednesday December 1, 1847, clocks were advanced to Wednesday, December 1, 1847 12:01:15 AM.
In response to the post by Richard Krajunus, here is some additional information from the zoneinfo database used by most computers to track these kinds of changes:
# From Paul Eggert (1993-11-18):
#
# Howse writes that Britain was the first country to use standard time.
# The railways cared most about the inconsistencies of local mean time,
# and it was they who forced a uniform time on the country.
# The original idea was credited to Dr. William Hyde Wollaston (1766-1828)
# and was popularized by Abraham Follett Osler (1808-1903).
# The first railway to adopt London time was the Great Western Railway
# in November 1840; other railways followed suit, and by 1847 most
# (though not all) railways used London time. On 1847-09-22 the
# Railway Clearing House, an industry standards body, recommended that GMT be
# adopted at all stations as soon as the General Post Office permitted it.
# The transition occurred on 12-01 for the L&NW, the Caledonian,
# and presumably other railways; the January 1848 Bradshaw's lists many
# railways as using GMT. By 1855 the vast majority of public
# clocks in Britain were set to GMT (though some, like the great clock
# on Tom Tower at Christ Church, Oxford, were fitted with two minute hands,
# one for local time and one for GMT). The last major holdout was the legal
# system, which stubbornly stuck to local time for many years, leading
# to oddities like polls opening at 08:13 and closing at 16:13.
# The legal system finally switched to GMT when the Statutes (Definition
# of Time) Act took effect; it received the Royal Assent on 1880-08-02.
#
# In the tables below, we condense this complicated story into a single
# transition date for London, namely 1847-12-01. We don't know as much
# about Dublin, so we use 1880-08-02, the legal transition time.
Sorry I couldn't respond using a comment in that thread; StackOverflow does not deem me worthy of that yet.
Is it a bug or is there a historic calendar adjustment that I've not come across?
There have been a number of times the calendar was ... fixed in the past.
Check the "adoption" sections of the wikipedia articles for Julian and Gregorian calendars.
The NSDate instance should always be showing the correct date for whatever timezone it was initialized with, however.
NSDateComponents are using your local time zone. Try setting the timeZone to UTC?
NSCalendar *cal = [NSCalendar currentCalendar];
NSDateComponents *dateComps = [NSDateComponents new];
dateComps.timeZone = [NSTimeZone timeZoneWithAbbreviation:#"UTC"];
dateComps.year = 1847;
dateComps.month = 12;
dateComps.day = 1;
NSDate *d1 = [cal dateFromComponents:dateComps];
NSLog(#"d1 = %#", d1);
dateComps = [NSDateComponents new];
dateComps.timeZone = [NSTimeZone timeZoneWithAbbreviation:#"UTC"];
dateComps.year = 1847;
dateComps.month = 12;
dateComps.day = 2;
NSDate *d2 = [cal dateFromComponents:dateComps];
NSLog(#"d2 = %#", d2);
[19875:60b] d1 = 1847-12-01 00:00:00 +0000
[19875:60b] d2 = 1847-12-02 00:00:00 +0000

Assuming gregorian calendar, why is adding a time interval of 86,400 (one day) to a date never the answer?

I created a method to calculate the first and last day of week, by adding a time interval of one day (86,400) to a date, multiplied by the required number of days.
A colleague commented "adding multiples of 86,400 is never the answer". Why is this so?
Here's the category method that I created, how can I improve this?
- (NSDate*)firstDayOfWeek
{
return [self dateWithDaysAddedGivenDayOfWeek:#{
#1 : #-6,
#2 : #0,
#3 : #-1,
#4 : #-2,
#5 : #-3,
#6 : #-4,
#7 : #-5
}];
}
- (NSDate*)lastDayOfWeek
{
return [self dateWithDaysAddedGivenDayOfWeek:#{
#1 : #0,
#2 : #6,
#3 : #5,
#4 : #4,
#5 : #3,
#6 : #2,
#7 : #1
}];
}
- (NSDate*)dateWithDaysAddedGivenDayOfWeek:(NSDictionary*)daysToAddGivenDay
{
NSDateComponents* components = [[NSCalendar currentCalendar]
components:NSYearCalendarUnit | NSMonthCalendarUnit | NSWeekCalendarUnit | NSWeekdayCalendarUnit fromDate:self];
NSInteger daysToAdd = [[daysToAddGivenDay objectForKey:#([components weekday])] integerValue];
return [self dateByAddingTimeInterval:daysToAdd * 60 * 60 * 24];
}
Do I need to use a different time interval for the day? (eg 23 hours, 56 minutes, 4.1 seconds)
You cannot assume that days have the same length. Due to daylight saving times they can i.e. be 23, 24 or 25 hours long. It is easy to imagine the trouble it might cause if we assume a day to be exactly 24*60*60 seconds long.
Also your code might be faulty as in different countries and cultures the first day of the week might differ (Sunday vs. Monday). So time and date calculations must be in respect to the calendar and the users locale. It is not simply done by counting seconds.
And we haven't even mentioned more nasty things as leap seconds, politicians changing a states time zone (Russia under Putin, Spain under Franco) or even shifting them across the international date line (refer to Samoa — they did it twice).
So you should trust the framework's tool, nicely explained in WWDC2011: Performing Calendar Calculations.
For the code you posted in the question I would suggest some thing like
NSCalendar *cal = [NSCalendar currentCalendar];
NSDate *now = [NSDate date];
NSDate *startOfTheWeek;
NSDate *endOfWeek;
NSTimeInterval interval;
[cal rangeOfUnit:NSWeekCalendarUnit
startDate:&startOfTheWeek
interval:&interval //<-- interval will hold the length of the time unit,
forDate:now]; // here week, taking DST et al into account
//startOfWeek holds now the first day of the week, according to locale (monday vs. sunday)
endOfWeek = [startOfTheWeek dateByAddingTimeInterval:interval-1];
// holds 23:59:59 of last day in week.
In your case another option would be to skip NSDate, as they are not representing a day but a specific moment in time and work with NSDateComponents. Also explained in the video.
You can't predict how many seconds are in a given day due to a wide variety of factors (leap seconds, DST, etc...). Instead, you should let the system handle this for you using this method on NSCalendar:
- (NSDate *)dateByAddingComponents:(NSDateComponents *)comps toDate:(NSDate *)date options:(NSUInteger)opts
By setting the day to a positive or negative number, you can add or subtract days accurately from a given date.

Number of Tuesdays in the current month in objective c

I'm looking for a way to calculate the number of Tuesdays in the current month in Objective C.
For example if the code ran today (July 16, 2012) it would ouput 5 because there are 5 Tuesdays in the month of July in 2012 (the 3rd, 10th, 17th, 24th, and 31st).
I have seen solutions online for doing it in Excel, but I am struggling translating it to Objective C.
Thanks for your help! Long time, first time.
We're going to need a calendar:
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
Now let's get the current month and year:
NSDate *now = [NSDate date];
NSDateComponents *monthAndYear = [calendar components:NSMonthCalendarUnit | NSYearCalendarUnit fromDate:now];
We can use that to get the first Tuesday of the current month and year:
NSDateComponents *firstTuesdayComponents = [monthAndYear copy];
firstTuesdayComponents.weekday = 3; // Sunday = 1
firstTuesdayComponents.weekdayOrdinal = 1; // First Tuesday
NSDate *firstTuesday = [calendar dateFromComponents:firstTuesdayComponents];
We can also use it to get the first day of next month:
NSDateComponents *firstOfNextMonthComponents = [monthAndYear copy];
firstOfNextMonthComponents.month += 1;
firstOfNextMonthComponents.day = 1;
NSDate *firstOfNextMonth = [calendar dateFromComponents:firstOfNextMonthComponents];
Now we can ask for the number of days between the two dates:
NSDateComponents *differenceComponents = [calendar components:NSDayCalendarUnit fromDate:firstTuesday toDate:firstOfNextMonth options:0];
Most weeks have seven days and a single Tuesday, so we should divide the number of days by 7. If there's a remainder, we should round it up because we started counting from a Tuesday.
int tuesdayCount = (differenceComponents.day + 6) / 7; // Adding 6 makes the integer division round up.
NSLog(#"There are %d Tuesdays in month %d of year %d.", tuesdayCount, (int)monthAndYear.month, (int)monthAndYear.year);
Now let's hop in the time machine to test it:
There are 5 Tuesdays in month 1 of year 2012.
There are 4 Tuesdays in month 2 of year 2012.
There are 4 Tuesdays in month 3 of year 2012.
There are 4 Tuesdays in month 4 of year 2012.
There are 5 Tuesdays in month 5 of year 2012.
There are 4 Tuesdays in month 6 of year 2012.
There are 5 Tuesdays in month 7 of year 2012.
There are 4 Tuesdays in month 8 of year 2012.
There are 4 Tuesdays in month 9 of year 2012.
There are 5 Tuesdays in month 10 of year 2012.
There are 4 Tuesdays in month 11 of year 2012.
There are 4 Tuesdays in month 12 of year 2012.
When looking at the outputs of cal 2012, cal 2011, and cal 2000, I've drawn a few conclusions:
There are either four or five Tuesdays in every month.
To find the months with five Tuesdays, note:
For months with 31 days, the first must fall on a Sunday, Monday, or Tuesday
For months with 30 days, the first must fall on a Monday or Tuesday
For months with 29 days, the first must fall on a Tuesday
Hopefully this is an easier problem to solve.
Using an algorithm based on the same observation sarnold made in his answer, this should do the trick:
- (NSUInteger)numberOfTuesdaysInMonthContainingDate:(NSDate *)date
{
NSCalendar *calendar = [NSCalendar currentCalendar];
NSUInteger numDaysInMonth = [calendar rangeOfUnit:NSDayCalendarUnit inUnit:NSMonthCalendarUnit forDate:date].length;
NSDate *firstDayOfMonth;
NSTimeInterval firstDayOfMonthLength;
if (![calendar rangeOfUnit:NSMonthCalendarUnit startDate:&firstDayOfMonth interval:&firstDayOfMonthLength forDate:date]) {
NSLog(#"Unable to calculate first day of the month for %#", date);
return 0;
}
NSUInteger firstDayOfTheMonthWeekday = [calendar ordinalityOfUnit:NSWeekdayCalendarUnit inUnit:NSWeekCalendarUnit forDate:firstDayOfMonth];
// The following is for Tuesday only, the math is more complex for an abitrary weekday
NSUInteger numTuesdays = 4;
NSUInteger minValidWeekday = 32 - numDaysInMonth;
if (firstDayOfTheMonthWeekday >= minValidWeekday && firstDayOfTheMonthWeekday <= 3) {
numTuesdays = 5;
}
return numTuesdays;
}

Pick a day in a cycle based on a starting date and the current date

An app allows a user to define an arbitrary set of days like so:
Day 1 - Pick flowers
Day 2 - Have coffee
Day 3 - Go swimming
This set of days may have only one entry or it may have many. Suppose that this cycle of days begins on March 23, 2010.
Is there a general algorithm that can determine which day in a cycle corresponds to a given calendar day?
On March 24th, the algorithm should return Day 2, March 25th should be Day 3, March 26th should be Day 1, and so on...
More specifically, I am writing this code for a Cocoa application. So, barring the existence of a more general technique can the Calendar classes help me?
NSCalendar *gregorian = ...;
NSDate *startDate = ...;
NSDate *currentDate = [NSDate date];
NSUInteger unitFlags = NSDayCalendarUnit;
NSDateComponents *components = [gregorian components:unitFlags fromDate:startDate toDate:currentDate options:0];
NSInteger days = [components day];
days now contains the number of days which have elapsed since the start date. This can be used as an index (zero-based) into your array of activities. You can use the modulus operator with the length of the cycle as the dividend to compute indices after the first transit around the cycle.
Calculate the Julian date (or a variant) of the start date and the current date (This part is left as an exercise to the reader). The current day of the cycle is then (currentDate - startDate) % lengthOfCycle + 1.
After typing this out I figured out the answer and it is straightforward.
Calculate the number of days between the starting date and the current date then take that number modulo the total number of days in the cycle and you have the current day. Easy peezy.