Noda Time can only represent values within a limited range. This is for pragmatic reasons: while it would be possible to try to model every instant since some estimate of the big bang until some expected "end of time," this would come at a significant cost to the experience of the 99.99% of programmers who just need to deal with dates and times in a reasonably modern (but not far-flung future) era.
The Noda Time 1.x releases supported years between around -27000 and +32000 (in the Gregorian calendar), but had significant issues around the extremes. Noda Time 2.0 has a smaller range, with more clearly-defined behaviour.
The "main" calendar for Noda Time is the Gregorian calendar. (We actually use CalendarSystem.Iso
by default, but that's just the Gregorian calendar with slightly different numbering around centuries;
it doesn't affect ranges.) This is the maximal calendar: a date in any other calendar can always be
converted to the Gregorian calendar.
Additionally, all calendars are restricted to four digit formats, even in year-of-era representations, which avoids ever having to parse 5-digit years. This leads to a Gregorian calendar from 9999 BCE to 9999 CE inclusive, or -9998 to 9999 in "absolute" years. The range of other calendars is determined from this and from natural restrictions (such as not being proleptic).
The date range is always a complete number of years - the range shown below is inclusive at both ends, so every date from the start of the minimum year to the end of the maximum year is valid. (The min/max Gregorian year shows the Gregorian year corresponding to the min/max calendar values; this does not mean that the whole of that Gregorian year can be converted into the relevant calendar.)
Calendar | Min year | Max year | Min year (Gregorian) | Max year (Gregorian) |
---|---|---|---|---|
Gregorian/ISO | -9998 | 9999 | -9998 | 9999 |
Julian | -9997 | 9998 | -9998 | 9999 |
Islamic | 1 | 9665 | 622 | 9999 |
Persian | 1 | 9377 | 622 | 9999 |
Hebrew | 1 | 9999 | -3760 | 6239 |
Coptic | 1 | 9715 | 284 | 9999 |
The range for Instant
is simply the range of the Gregorian calendar, in UTC.
In other words, it covers -9998-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z inclusive.
All of the types in this heading are based on dates in calendars, and they all have ranges based on that
local date (and thus on the calendar). So even if the logical position on the global time-line for a
ZonedDateTime
or OffsetDateTime
is out of the range of Instant
, the value is still valid. For example, consider
this code:
LocalDate earliestDate = new LocalDate(-9998, 1, 1);
OffsetDateTime offsetDateTime = earliestDate.AtMidnight().WithOffset(Offset.FromHours(10));
Here, offsetDateTime
has a local date/time value of -9998-01-01T00:00:00, but it's 10 hours ahead of UTC... giving a logical
instant of -9999-12-31T14:00:00Z... which is out of range. That's fine, and the code above won't throw an exception... unless
you try to convert offsetDateTime
to an Instant
.
Duration
is designed to allow for at least the largest difference in
valid Instant
values in either direction. As such, it needs to cover 631,075,881,599,999,999,999 nanoseconds -
which is just shy of 7,304,119 days. Internally, durations are stored in terms of "day" and "nanosecond within the day" (an
implementation detail to be sure, but one which sometimes affects other decisions).
Additionally, it seems useful to be able to cover the full range of
TimeSpan
, given that Duration
is meant to be the roughly-equivalent
type.
The result is that we have a range of days from -224 to +224-1 - and the nanosecond part means that the
total range is from -224 days inclusive to +224 days exclusive - the largest valid Duration
is 1
nanosecond less than 224 days.
The Years
, Months
, Weeks
, Days
properties are 32-bit integers;
the Hours
, Minutes
, Seconds
, Milliseconds
, Ticks
, Nanoseconds
properties are all 64-bit integers.
Properties are independent of each other, and can take the full range of values for their respective types. "Extreme" periods
may have a very limited (or empty) set of LocalDate
/LocalDateTime
/LocalTime
values that they can be added to or
subtracted from.
Assuming Noda Time is bug free1, we ensure that if you try to create a value outside the valid range for that type (via any normal API calls, or deserialization), an exception of one of the following types will be thrown:
OverflowException
ArgumentOutOfRangeException
ArgumentException
InvalidOperationException
The "best" exception in each case depends very much on the context - but in the aim of code reuse and
performance, we may not always have that context available when the exception is thrown. For example, a call
to OffsetDateTime.ToInstant()
doesn't have any arguments - but internally we may end up calling another method
and passing one or more arguments. If the validation of the "inner" method throws an ArgumentOutOfRangeException
,
that's what you'll see too. It's not ideal, but it's a reasonable balance. You should almost never be catching any
of those exceptions explicitly anyway, so hopefully it's just a matter of being aware that if one of those exceptions
is thrown from the depths of Noda Time, it's probably due to a value being out of range somewhere.
As far as possible, we try to ensure that if you try to perform an operation which can succeed, it will succeed.
This is not guaranteed in all cases, however. For example, consider adding a Period
consisting of -1 year and 365 days
to -9998-01-01. The "logical" result is a no-op, but as Period
addition is performed one unit at a time, it will fail
on the addition of the -1 year.
1 I'm not claiming that it is bug free, but I can't predict what will happen if there are bugs.