What do the years 144,683 AD and 5,828,963 AD have in common? They break some of Apple’s iOS and OS X code, as do their BC counterparts discussed below. From an app development perspective, you most likely won’t care - but if your curiousity is piqued, read on.
Instances of NSDate are simple wrappers around a double precision floating point value. That value represents the number of seconds before or after the reference date – the first instant of 1 January 2001, GMT.
NSDate has a lot of friends: including NSCalendar, NSDateComponents, NSDateFormatter, NSLocale and NSTimeZone. (The corresponding Core Foundation entities are CFDate, CFCalendar, CFDateFormatter, CFLocale and CFTimeZone)
While NSDate and its friends behave nicely in a restricted range around 2001, they can give unexpected results when pushed too hard.
The Safe Zone
If you only handle dates and times between 144,683 BC and 144,683 AD everything works just fine. Continue reading if you go outside those bounds or are just plain curious.
The Demo App
We’ve posted an Xcode project for a demo app on Github here. The app uses Newton-like iterative methods to find the last valid values that can be formatted correctly.
NSDate distantPast and distantFuture
You may not agree with Apple’s definition of distantPast and distantFuture. These methods produce dates that are not all that distant, depending on how you think about it.
|distantPast||December 30, 1 BC|
|distantFuture||January 1, 4001 AD|
Limits on NSDateFormatter
Class and instance methods of NSDateFormatter are used to convert NSDate instances into NSString instances, and vice versa. These only operate effectively on a restricted range of NSDate values.
|Method||Lower Limit||Upper Limit|
|dateFromString||1 Jan 144,683 BC||1 Jan 144,684 AD|
|stringFromDate||20 Sep 5,838,270 BC||20 Dec 5,828,963 AD|
The conversion to NSDate from an NSString has a much more restricted range than the reverse.
Attempting to convert outside the above limits results in nil NSDate instances or zero-length NSString instances.
Here’s some code that illustrates the behavior.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Limit on NSDate description
The -description method available on NSDate appears to be subject to the same limitations as the stringFromDate method of NSDateFormatter.
In theory, NSDate can represent dates with integral number of seconds up to around 253 = 9,007,199,254,740,992 seconds = approximately 285,426,782 years based on the double precision representation of time. If we round the time to the nearest second before processing, that would be true.
Starting from the current date and time, the demo app adds seconds in powers of 2, then subtracts the same number of seconds, examining how much error is introduced by these two operations. The results will vary depending upon the starting point.
If you play around with the demo app, you will see that NSDate behaves reliably at the predicted limit in the future. Testing the equivalent distance in the past is left as an exercise for the reader.
A Note on NSNumberFormatter
In the course of that testing, we found that NSNumberFormatter exhibits a small bug. Starting at 250 it does not correctly convert NSNumbers stored as doubles to strings, even though the C printf format %lf correctly prints the double, as does the NSNumber -description method. See the sample app for a working proof.
Limit on NSCalendar and NSDateComponents
Constructing an NSDate from NSDateComponents appears to be subject to the same limits as the NSDateFormatter -dateFromString method: you can construct a date correctly on December 19 in the year 5,828,953, but not in the following year.
Interestingly enough, the failure is different than NSDateFormatter: rather than getting a nil, you get an NSDate with the previous year.
Julian to Gregorian
Prior to October 15, 1582 the now standard Gregorian calendar did not exist. There is currently a bug in Apple’s documentation.
The Date and Time Programming Guide for iOS is outdated (as of February 11, 2015).
The guide states:
NSCalendar models the transition from the Julian to Gregorian calendar in October 1582. During this transition, 10 days were skipped. This means that October 15, 1582 follows October 4, 1582. All of the provided methods for calendrical calculations take this into account, but you may need to account for it when you are creating dates from components. Dates created in the gap are pushed forward by 10 days. For example October 8, 1582 is stored as October 18, 1582.
The true behavior seems to be that of a proleptic Gregorian calendar, in which the Gregorian calendar is simply extended backwards in time. This is a more logical way to operate, leaving the somewhat tricky management of Julian calendar dates to the programmer.
We demonstrate the behavior of the calendar prior to October 15, 1582 in the sample app, using dateFromComponents for the Gregorian calendar to get the desired dates.
We note in passing that the Gregorian calendar was adopted many years later than 1582 by many countries. The United Kingdom and its possessions in 1752, the Soviet Union in 1918. You must be careful with recorded dates during the interval between 1582 and final adoption of the Gregorian calendar in the country of interest.
Parse.com is our very favorite back-end service for iOS. It’s well designed, efficient, and inexpensive. You can easily create objects with columns of type “Date” which provide transparent support for NSDate objects. But there’s a catch: Parse won’t store BC dates correctly, and while it will store dates before 100 AD correctly, it won’t display them correctly in the Core data browser.
We’ve filed a bug report with Parse, so they will probably fix this eventually.
Work-around - instead of persisting an NSDate object, use the timeIntervalSinceReferenceDate instance method to obtain a double and save that as a number in Parse. Upon retrieving that number, convert it back to an NSDate by using the dateWithTimeIntervalSinceReferenceDate class method.
Modern dates and times are displayed relative to time zones. Both iOS and OS X use the Time Zone Database (sometimes known as ‘tz’) to accurately represent time as it would appear on clocks in time zones around the world.
Prior to the initial adoption of Railway time in the United Kingdom (1847), the United States (1883) and other countries at varying dates, most cities used Local Mean Time based on their longitude east of Greenwich.
The sample app generates a report for a selection of time zones. This report shows the changes over time due to the initial adoption of time zones and the later adoption of daylight savings time, depending upon jurisdiction. The maintainers of the tz database note that information pre-1970 is not always precise. The report for each time zone starts in 1846, prior to the introduction of Railway time in the United Kingdom.
Because we only examine three specific dates, January 4, July 4, and December 4, the dates on which time zone behavior appears to change in the sample app are not exact.
More on calendars here.
Info on the Time Zone Database here.
Interesting note on the Time Zone Database here.
Info on British time here.