Adjust NSDate by a specified amount

Isn't annoying when you want to adjust a NSDate object by 3 days, or 8 hours, or even by 4 months? You have to instance a NSDateComponents object, edit what you need and then instance a new NSDate object by using a [NSCalendar dateByAddingComponents:toDate:options] method call. In my current app, I have these little adjustments littered all through-out my code. So much so that I decided it was time to make things easier. I just made a simple method that handles this for me:

- (NSDate *)adjustTimeOfDate:(NSDate *)date byAmount:(int)amount  usingPeriod:(AdjustmentPeriod)period;

It allows me to do something like this in my code:

someDateInMyApp = [NSDateCategory adjustTimeOfDate:someDateInMyApp byAmount:2 usingPeriod:AdjustByDay];

I successfully adjusted a NSDate object by two days without having to write 4 lines of code everytime, or without having to figure out how many seconds it takes to increase a NSDate object by for 2 days. I can also go backwards in time:

someDateInMyApp = [NSDateCategory adjustTimeOfDate:someDateInMyApp byAmount:-4 usingPeriod:AdjustByDay];

It makes life so much easier. In order to implement the method, we first need to have a custom enum in place that we can use to select what time frame we want to adjust by. My code was not placed into a NSDate category for my app, but I would recommend that you do that. It seems like the best place for it to go. If you need some help creating a category, check out how I did it in my Understanding Categories post.

My enum goes in my header file like such:

typedef enum adjustmentPeriod {
    AdjustByYear,
    AdjustByMonth,
    AdjustByDay,
    AdjustByHour,
    AdjustByMinute,
    AdjustBySecond
} AdjustmentPeriod;

Next, we define our method in the header file:

- (NSDate *)dateByAdjustingTimeOfDate:(NSDate *)dateToAdjust byAmount:(int)amount usingPeriodOfTime:(AdjustmentPeriod)period;

We are now ready to implement the method. It is really simple, we just instance a blank NSDateComponents and add the amount specified to the correct period of time based on what is provided as an argument. In order to determine it, we will use a switch/case statement.

- (NSDate *)dateByAdjustingTimeOfDate:(NSDate *)dateToAdjust byAmount:(int)amount usingPeriodOfTime:(AdjustmentPeriod)period {
    NSDateComponents *components = [[NSDateComponents alloc] init];

    switch (period) {
        case AdjustByYear:
            components.year = amount;
            break;
        case AdjustByMonth:
            components.month = amount;
            break;
        case AdjustByDay:
            components.day = amount;
            break;
        case AdjustByHour:
            components.hour = amount;
            break;
        case AdjustByMinute:
            components.minute = amount;
            break;
        case AdjustBySecond:
            components.second = amount;
            break;
        default:
            break;
    }

    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    return [calendar dateByAddingComponents:components toDate:dateToAdjust options:0];
}

Now you have a method that can adjust any date. Due to this being a category, you can't add properties (without a bit of grunt work) so I instance the NSCalendar object in the method itself. Due to my app using this method a lot, I chose to put it into a custom class inheriting from NSObject rather than a category so that the NSCalendar object can be a property that is shared by all method calls. Instancing a NSCalendar object is expensive, so if you plan on using this a lot through-out your code, you might want to consider abandoning the category and just sub-class NSObject so you can have a instance variable of NSCalendar to re-use.

If you find that you need to adjust multiple parts of the NSDate, such as the day and hour in one shot, you can modify our method to accept a NSDictionary object. The NSDictionary would have the periods of time you want to edit as the keys and the amount to edit as the value. So you could do something like this:

NSDictionary *adjustmentOptions = @{ @"Year" : @(4), @"Day" : @(12)};

You then loop through your dictionary keys, adjusting each period using a switch/case statement once again. This way, you only create one new NSDate object and not a series of them.

Understanding Categories

What is a Category?

Objective-C has a really cool feature called Categories. In short, a Category gives developers an opportunity to add additional methods to existing classes. Does NSString not have a method you wish it did? You can go ahead and write the method and add it to NSString, without needing the original source code. Apple explains this in their documentation like such:

Any methods that you declare in a category will be available to all instances of the original class, as well as any subclasses of the original class. At runtime, there’s no difference between a method added by a category and one that is implemented by the original class.

One thing you want to watch out for however is properties. You can only add methods to an existing class via Categories; never properties. My tinkering around with them the other day showed that ivars are not allowed either. I'm not sure why this restriction is in place, but that's the way Apple designed them. You can add a property like @property (nonatomic, strong) NSString *foo; and access it without a problem; the compiler might complain a bit, but that can be silenced by adding a @dynamic above your property. All that does however is silence the warnings that your property is not being properly synthesized. Apple's documentation tells us that the class that the category is adding methods to, will not synthesize any properties in the categories. So the issue that comes from this is the inability to track the values associated with our properties, once the code leaves our setter/getter methods. The backing ivar's are never synthesized.

So long story short, don't use properties in your categories. If you need to use a property, extend the class with a class extension or sub-class it.

Putting it into practice

We now know what a category is, but what would you ever need to use one for? Once you get used to how categories work, it's surprisingly easy to find a use for them on a regular basis. For instance, tonight I ran into an issue with my code were I had two NSDate objects that had the same hour and minute, but were off by a few seconds because of the delay in creating the two dates. Since the seconds were irrelevant to what I was doing, I wanted a easy way to zero them out which would allow them to be equal to each other.

The most obvious answer is to use NSDateComponents, which is a real pain. It requires several lines of code that would need to be re-wrote over and over.

@implementation TestViewController
- (void)viewDidLoad
{
    [super viewDidLoad];

    // Get the current date.
    NSDate *date = [NSDate date];
    NSLog(@"Date is %@", date);

    // Only pull the year, month, day, hour and minute. Ignore seconds.
    NSUInteger unitFlags = (NSYearCalendarUnit
                            | NSMonthCalendarUnit
                            | NSDayCalendarUnit
                            | NSHourCalendarUnit
                            | NSMinuteCalendarUnit);

    // Instance our calendar
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    // Filter out what components we want from the date
    NSDateComponents *dateComponents = [calendar components:unitFlags fromDate:date];

    // Adjust the date using our date components
    NSDate *correctedDate = [calendar dateFromComponents:dateComponents];
    NSLog(@"Corrected date is %@", correctedDate);
}
@end

This provides me with the following output, which verifies that the code is indeed, zeroing out the seconds.

[6755:60b] Date is 2013-11-22 08:05:25 +0000

[6755:60b] Corrected date is 2013-11-22 08:05:00 +0000

Now, instead of re-using this code all over the place, I can make a helper method in what ever object is using this. Although, I might want to use this in other classes, or even in another project! So why don't I move the code over to a category instead? Let's do that, using the same code above.

You add a new category file to your project via File->New->File and selecting a Objective-C Category under the Cocoa Touch or Cocoa platforms; depending on if you are developing on iOS or OS X.

Press next and give it a category name of MyDateCategory. For the Category On field, type in NSDate and press Next and save the file to your project folder.

Alright, now we have two new files. A NSDate+MyDateCategory.h header file and a NSDate+MyDateCategory.m implementation file. Let's start off with the header file which should look like this:

#import <Foundation/Foundation.h>

@interface NSDate (MyDateCategory)

@end

You can see that the actual header is an interface for NSDate. Our custom category is shown in parentheses, which lets the compiler know we are adding stuff to the existing NSDate class. We want to add our code to zero out the seconds right? So we need a new instance method which we will call dateWithZeroSeconds.

#import <Foundation/Foundation.h>

@interface NSDate (MyDateCategory)
- (NSDate *)dateWithZeroSeconds;
@end

Our new instance method returns a new NSDate, which will let us just invoke this method and assign our date with the adjusted date. The implementation then would look like this in the .m file.

#import "NSDate+MyDateCategory.h"

@implementation NSDate (MyDateCategory)
- (NSDate *)dateWithZeroSeconds {
    // Get the current date.
    NSLog(@"Date is %@", self);

    // Only pull the year, month, day, hour and minute. Ignore seconds.
    NSUInteger unitFlags = (NSYearCalendarUnit
                            | NSMonthCalendarUnit
                            | NSDayCalendarUnit
                            | NSHourCalendarUnit
                            | NSMinuteCalendarUnit);

    // Instance our calendar
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    // Filter out what components we want from the date
    NSDateComponents *dateComponents = [calendar components:unitFlags fromDate:self];

    // Adjust the date using our date components
    NSDate *correctedDate = [calendar dateFromComponents:dateComponents];
    NSLog(@"Corrected date is %@", correctedDate);

    return correctedDate;
}
@end

It's the same code that we wrote above right? With the exception that now we can use it in any project that has our NSDate+MyCustomDate.h and .m files included. Now one of the important differences here however is to note my use of the keyword self. Since we are adding this method to NSDate, it makes since that I need to get the date components from myself, because I am the date at this point. Not another object, so we ask for the components from self. You'll want to remember that when you are writing a category method, all of the class properties and methods are available to you; just be sure to access them via the self keyword.

So returning to our ViewController, we would use this method like such:

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSDate *date = [NSDate date];
    NSDate *correctedDate = [date dateWithZeroSeconds];
}

And we can shorten this up even more by passing our dateWithZeroSeconds method the returned object from [NSDate date]

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSDate *correctedDate = [[NSDate date] dateWithZeroSeconds];
}

Really nice right? Now anywhere in your code, you can use your custom method just like it was part of a NSDate object, without having to sub-class NSDate!

Now, I said earlier that using the NSDateComponents object is more work than what you really need to do for something like this, so I'm going to revise our Category method with something more elegant.

Like such:

- (NSDate *)dateWithZeroSeconds
{
    NSTimeInterval time = floor([self timeIntervalSinceReferenceDate] / 60.0) * 60.0;
    return  [NSDate dateWithTimeIntervalSinceReferenceDate:time];
}

Now isn't that prettier?

Hopefully this helps those that read it understand Categories in Objective-C.