NSCalendar Additions
Dates. More than any other data type, the gulf between the initial banality of dates and their true, multifaceted complexity looms terrifyingly large. Combining sub-second precision, overlapping units, geopolitical time zone boundaries, localization differences in both language and grammar, and Daylight Saving shifts and leap year adjustments that literally add and remove whole chunks of time from measured existence, there’s a lot to process.
To embark on any date-heavy task, then, requires a solid understanding of the tools already at your fingertips. Better to use a Foundation
method than to write the n-thousandth version of date
. Are you using NSDate
? Did you specify all the right calendar units? Will your code still work correctly on February 28, 2100?
But here’s the thing: the APIs you’re already using have been holding out on you. Unless you’re digging through release notes and API diffs, you wouldn’t know that over the last few releases of OS X, NSCalendar
has quietly built a powerful set of methods for accessing and manipulating dates, and that the latest release brought them all to iOS.
let calendar = NSCalendar.current Calendar()
NSCalendar *calendar = [NSCalendar current Calendar];
From new ways of accessing individual date components and flexibly comparing dates to powerful date interpolation and enumeration methods, there’s far too much to ignore. Make some room in your calendar and read on for more.
Convenient Component Access
Oh, NSDate
. So practical and flexible, yet so cumbersome when I just. Want to know. What the hour is. NSCalendar
to the rescue!
let hour = calendar.component(.Calendar Unit Hour, from Date: NSDate())
NSInteger hour = [calendar component:NSCalendar Unit Hour from Date:[NSDate date]];
That’s much better. NSCalendar
, what else can you do?
get
: Returns the era, year, month, and day of the given date by reference. PassEra(_:year:month:day:from Date:) nil
/NULL
for any parameters you don’t need.get
: Returns the era, year (for week of year), week of year, and weekday of the given date by reference. PassEra(_:year For Week Of Year:week Of Year:weekday:from Date:) nil
/NULL
for any parameters you don’t need.get
: Returns time information for the given date by reference.Hour(_:minute:second:nanosecond:from Date:) nil
/NULL
, you get the idea.
And just kidding, NSDate
, I take it all back. There are a couple methods for you, too:
components
: Returns anIn Time Zone(_:from Date:) NSDate
instance with components of the given date shifted to the given time zone.Components components(_:from
: Returns the difference between twoDate Components:to Date Components:options:) NSDate
instances. The method will use base values for any components that are not set, so provide at the least the year for each parameter. The options parameter is unused; passComponents nil
/0
.
Date Comparison
While direct NSDate
comparison has always been a simple matter, more meaningful comparisons can get surprisingly complex. Do two NSDate
instances fall on the same day? In the same hour? In the same week?
Fret no more, NSCalendar
has you covered with an extensive set of comparison methods:
is
: ReturnsDate In Today(_:) true
if the given date is today.is
: ReturnsDate In Tomorrow(_:) true
if the given date is tomorrow.is
: ReturnsDate In Yesterday(_:) true
if the given date is a part of yesterday.is
: ReturnsDate In Weekend(_:) true
if the given date is part of a weekend, as defined by the calendar.is
: ReturnsDate(_:in Same Day As Date:) true
if the twoNSDate
instances are on the same day—delving into date components is unnecessary.is
: ReturnsDate(_:equal To Date:to Unit Granularity:) true
if the dates are identical down to the given unit of granularity. That is, two date instances in the same week would return true if used withcalendar.is
, even if they fall in different months.Date(tuesday, equal To Date: thursday, to Unit Granularity: .Calendar Unit Week Of Year) compare
: Returns anDate(_:to Date:to Unit Granularity:) NSComparison
, treating as equal any dates that are identical down to the given unit of granularity.Result date(_:matches
: ReturnsComponents:) true
if a date matches the specific components given.
Date Interpolation
Next up is a set of methods that allows you to find the next date(s) based on a starting point. You can find the next (or previous) date based on an NSDate
instance, an individual date component, or a specific hour, minute, and second. Each of these methods takes an NSCalendar
bitmask parameter that provides fine-grained control over how the next date is selected, particularly in cases where an exact match isn’t found at first.
NSCalendar Options
The easiest option of NSCalendar
is .Search
, which reverses the direction of each search, for all methods. Backward searches are constructed to return dates similar to forward searches. For example, searching backwards for the previous date with an hour
of 11 would give you 11:00, not 11:59, even though 11:59 would technically come “before” 11:00 in a backwards search. Indeed, backward searching is intuitive until you think about it and then unintuitive until you think about it a lot more. That .Search
is the easy part should give you some idea of what’s ahead.
The remainder of the options in NSCalendar
help deal with “missing” time instances. Time can be missing most obviously if one searches in the short window when time leaps an hour forward during a Daylight Saving adjustment, but this behavior can also come into play when searching for dates that don’t quite add up, such as the 31st of February or April.
When encountering missing time, if NSCalendar
is provided, the methods will continue searching to find an exact
match for all components given, even if that means skipping past higher order components. Without strict matching invoked, one of .Match
, .Match
, and .Match
must be provided. These options determine how a missing instance of time will be adjusted to compensate for the components in your request.
In this case, an example will be worth a thousand words:
// begin with Valentine's Day, 2015 at 9:00am
let valentines = cal.date With Era(1, year: 2015, month: 2, day: 14, hour: 9, minute: 0, second: 0, nanosecond: 0)!
// to find the last day of the month, we'll set up a date components instance with
// `day` set to 31:
let components = NSDate Components()
components.day = 31
NSDate *valentines = [calendar date With Era:1 year:2015 month:2 day:14 hour:9 minute:0 second:0 nanosecond:0];
NSDate Components *components = [[NSDate Components alloc] init];
components.day = 31;
Using strict matching will find the next day that matches 31
, skipping into March to do so:
calendar.next Date After Date(valentines, matching Components: components, options: .Match Strictly)
// Mar 31, 2015, 12:00 AM
NSDate *date = [calendar next Date After Date:valentines matching Components:components options:NSCalendar Match Strictly];
// Mar 31, 2015, 12:00 AM
Without strict matching, next
will stop when it hits the end of February before finding a match—recall that the highest unit specified was the day, so the search will only continue within the next highest unit, the month. At that point, the option you’ve provided will determine the returned date. For example, using .Match
will pick the next possible day:
calendar.next Date After Date(valentines, matching Components: components, options: .Match Next Time)
// Mar 1, 2015, 12:00 AM
date = [calendar next Date After Date:valentines matching Components:components options:NSCalendar Match Next Time];
// Mar 1, 2015, 12:00 AM
Similarly, using .Match
will pick the next day, but will also preserve all the units smaller than the given NSCalendar
:
calendar.next Date After Date(valentines, matching Components: components, options: .Match Next Time Preserving Smaller Units)
// Mar 1, 2015, 9:00 AM
date = [calendar next Date After Date:valentines matching Components:components options:NSCalendar Match Next Time Preserving Smaller Units];
// Mar 1, 2015, 9:00 AM
And finally, using .Match
will resolve the missing date by going the other direction, choosing the first possible previous day, again preserving the smaller units:
calendar.next Date After Date(valentines, matching Components: components, options: .Match Previous Time Preserving Smaller Units)
// Feb 28, 2015, 9:00 AM
date = [calendar next Date After Date:valentines matching Components:components options:NSCalendar Match Previous Time Preserving Smaller Units];
// Feb 28, 2015, 9:00 AM
Besides the NDate
version shown here, it’s worth noting that next
has two other variations:
// matching a particular calendar unit
cal.next Date After Date(valentines, matching Unit: .Calendar Unit Day, value: 31, options: .Match Strictly)
// March 31, 2015, 12:00 AM
// matching an hour, minute, and second
cal.next Date After Date(valentines, matching Hour: 15, minute: 30, second: 0, options: .Match Next Time)
// Feb 14, 2015, 3:30 PM
// matching a particular calendar unit
date = [calendar next Date After Date:valentines matching Unit:NSCalendar Unit Day value:31 options:NSCalendar Match Strictly];
// March 31, 2015, 12:00 AM
// matching an hour, minute, and second
date = [calendar next Date After Date:valentines matching Hour:15 minute:30 second:0 options:NSCalendar Match Next Time];
// Feb 14, 2015, 3:30 PM
Enumerating Interpolated Dates
Rather than using next
iteratively, NSCalendar
provides an API for enumerating dates with the same semantics. enumerate
computes the dates that match the given set of components and options, calling the provided closure with each date in turn. The closure can set the stop
parameter to true
, thereby stopping the enumeration.
Putting this new NSCalendar
knowledge to use, here’s one way to list the last fifty leap days:
let leap Year Components = NSDate Components()
leap Year Components.month = 2
leap Year Components.day = 29
var date Count = 0
cal.enumerate Dates Starting After Date(NSDate(), matching Components: leap Year Components, options: .Match Strictly | .Search Backwards)
{ (date: NSDate!, exact Match: Bool, stop: Unsafe Mutable Pointer<Obj CBool>) in
println(date)
if ++date Count == 50 {
// .memory gets at the value of an Unsafe Mutable Pointer
stop.memory = true
}
}
// 2012-02-29 05:00:00 +0000
// 2008-02-29 05:00:00 +0000
// 2004-02-29 05:00:00 +0000
// 2000-02-29 05:00:00 +0000
…
NSDate Components *leap Year Components = [[NSDate Components alloc] init];
leap Year Components.month = 2;
leap Year Components.day = 29;
__block int date Count = 0;
[calendar enumerate Dates Starting After Date:[NSDate date]
matching Components:leap Year Components
options:NSCalendar Match Strictly | NSCalendar Search Backwards
using Block:^(NSDate *date, BOOL exact Match, BOOL *stop) {
NSLog(@"%@", date);
if (++date Count == 50) {
*stop = YES;
}
}];
// 2012-02-29 05:00:00 +0000
// 2008-02-29 05:00:00 +0000
// 2004-02-29 05:00:00 +0000
// 2000-02-29 05:00:00 +0000
…
Working for the Weekend
If you’re always looking forward to the weekend, look no further than our final two NSCalendar
methods:
next
: Returns the starting date and length of the next weekend by reference via the first two parameters. This method will return false if the current calendar or locale doesn’t support weekends. The only relevant option here isWeekend Start Date(_:interval:options:after Date) .Search
. (See below for an example.)Backwards range
: Returns the starting date and length of the weekend containing the given date by reference via the first two parameters. This method returns false if the given date is not in fact on a weekend or if the current calendar or locale doesn’t support weekends.Of Weekend Start Date(_:interval:containing Date)
Localized Calendar Symbols
As if all that new functionality wasn’t enough, NSCalendar
also provides access to a full set of properly localized calendar symbols, making possible quick access to the names of months, days of the week, and more. Each group of symbols is further enumerated along two axes: (1) the length of the symbol and (2) its use as a standalone noun or as part of a date.
Understanding this second attribute is extremely important for localization, since some languages, Slavic languages in particular, use different noun cases for different contexts. For example, a calendar would need to use one of the standalone
variants for its headers, not the month
that are used for formatting specific dates.
For your perusal, here’s a table of all the symbols that are available in NSCalendar
—note the different values for standalone symbols in the Russian column:
en_US | ru_RU | |
---|---|---|
month |
January, February, March… | января, февраля, марта… |
short |
Jan, Feb, Mar… | янв., февр., марта… |
very |
J, F, M, A… | Я, Ф, М, А… |
standalone |
January, February, March… | Январь, Февраль, Март… |
short |
Jan, Feb, Mar… | Янв., Февр., Март… |
very |
J, F, M, A… | Я, Ф, М, А… |
weekday |
Sunday, Monday, Tuesday, Wednesday… | воскресенье, понедельник, вторник, среда… |
short |
Sun, Mon, Tue, Wed… | вс, пн, вт, ср… |
very |
S, M, T, W… | вс, пн, вт, ср… |
standalone |
Sunday, Monday, Tuesday, Wednesday… | Воскресенье, Понедельник, Вторник, Среда… |
short |
Sun, Mon, Tue, Wed… | Вс, Пн, Вт, Ср… |
very |
S, M, T, W… | В, П, В, С… |
AMSymbol |
AM | AM |
PMSymbol |
PM | PM |
quarter |
1st quarter, 2nd quarter, 3rd quarter, 4th quarter | 1-й квартал, 2-й квартал, 3-й квартал, 4-й квартал |
short |
Q1, Q2, Q3, Q4 | 1-й кв., 2-й кв., 3-й кв., 4-й кв. |
standalone |
1st quarter, 2nd quarter, 3rd quarter, 4th quarter | 1-й квартал, 2-й квартал, 3-й квартал, 4-й квартал |
short |
Q1, Q2, Q3, Q4 | 1-й кв., 2-й кв., 3-й кв., 4-й кв. |
era |
BC, AD | до н. э., н. э. |
long |
Before Christ, Anno Domini | до н.э., н.э. |
Note: These same collections are also available via
NSDate
.Formatter
Your Weekly Swiftification
It’s becoming something of a feature here at NSHipster to close with a slightly Swift-ified version of the discussed API. Even in this brand-new set of NSCalendar
APIs, there are some sharp edges to be rounded off, replacing Unsafe
parameters with more idiomatic tuple return values.
With a useful set of NSCalendar
extensions (gist here), the component accessing and weekend finding methods can be used without in-out variables. For example, getting individual date components from a date is much simpler:
// built-in
var hour = 0
var minute = 0
calendar.get Hour(&hour, minute: &minute, second: nil, nanosecond: nil, from Date: NSDate())
// swiftified
let (hour, minute, _, _) = calendar.get Time From Date(NSDate())
As is fetching the range of the next weekend:
// built-in
var start Date: NSDate?
var interval: NSTime Interval = 0
let success = cal.next Weekend Start Date(&start Date, interval: &interval, options: nil, after Date: NSDate())
if success, let start Date = start Date {
println("start: \(start Date), interval: \(interval)")
}
// swiftified
if let next Weekend = cal.next Weekend After Date(NSDate()) {
println("start: \(next Weekend.start Date), interval: \(next Weekend.interval)")
}
So take that, complicated calendrical math. With these new additions to NSCalendar
, you’ll have your problems sorted out in no time.