Convert a ISO-formatted date to a decimal date, accounting for negative years (BCE) and for very large dates (-1000000-01-01).
The returned decimal date is the year plus a decimal portion indicating "how far along" it was into the year on 12 noon of that day. For example, January 1 would be 0.5 days out of 365 so would have a decimal portion of 0.00136986.
This treats the Gregorian calendar as proleptic, continuing with leap years every fourth year (except centuries, except-except 4th centuries) into positive and negative infinity. As such, approximately every fourth year year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
will have 366 days, as we are accustomed today.
import decimaldate
print( decimaldate.iso2dec('2000-02-28') ) # 2000.15984
print( decimaldate.iso2dec('540-01-31') ) # 540.08333 note the small decimal portion, since Jan 1 is closer to 0 origin when CE
print( decimaldate.iso2dec('-540-01-31') ) # -539.91667 note the large decimal portion, since Jan 1 is further from 0 origin when BCE
print( decimaldate.iso2dec('1900-02-29') ) # error, this would not have been a leap year
print( decimaldate.dec2iso(1999.0013700) ) # 1999-01-01
print( decimaldate.dec2iso(1999.497260) ) # 1999-07-01
print( decimaldate.dec2iso(-550.9164383) ) # -0551-01-31 negative year, large decimal portion because January is further from 0 origin
print( decimaldate.dec2iso(-550.0835617) ) # -0551-12-01 negative year, small decimal portion because December is closer to 0 origin
This follows ISO 8601 in that year 0000 is 1 BCE, -0001 is 2 BCE, and so on. Expect negative dates to seem off by 1.
// positive dates are what you expect
print( decimaldate.iso2dec('2000-01-01') ) # 2000.001366
print( decimaldate.dec2iso(2000.001366) ) # 2000-01-01
// off by 1: 0 = 1, -1 = -2, and so on
print( decimaldate.iso2dec('-2000-01-01') ) # -1999.998634
print( decimaldate.dec2iso(-2000.998634) ) # -2001-01-01
// but it unpacks the same
print( decimaldate.dec2iso(decimaldate.iso2dec('-1000-06-30')) ) # -1000-06-30
The Gregorian calendar has no year 0, and the morning after Dec 31 of 1 BCE would be Jan 1 of 1 CE.
On a number line from BCE to CE, the value 0 would appear at the cusp between December 31 1 BCE (0000-12-31) and January 1 1 CE (0001-01-01).
Decimaldate can be thought of as an offset on that number line.
- +0.25 would be 3 months forward into 1 CE (early April)
- +1.5 would be a year and a half forward from 0, so early July of 2 CE
- -0.5 would be half a year backward into 1 BCE (early October)
- -1.5 would be a year and a half backward from 0, so early July of 2 BCE
However, decimaldate shifts the origin by 1 year to make positive dates look more intuitive. While it is mathematically correct that +2022.9 is November 2023, people reading decimal dates visually just didn't like the numbers looking like that. As such, +1 is added to decimal dates.
true decimal | decimaldate | iso | comment |
---|---|---|---|
-1.998633 | -0.998633 | -0001-01-01 | first day of 2 CE, most negative (highest decimal portion) day of the year |
-1.001366 | -0.001366 | -0001-12-31 | the last day of 2 BCE, least negative (lowest decimal portion) day of the year |
-0.998633 | 0.001367 | 0000-01-01 | first day of 1 BCE, most negative (highest decimal portion) day of the year |
-0.5 | 0.5 | 0000-07-02 | middle of 1 BCE, 6 months before the 0 origin of Jan 1 1 CE |
-0.001366 | 0.998634 | 0000-12-31 | the last day of 1 BCE, least negative (lowest decimal portion) day of the year |
0 | 1 | cusp | the cusp between Dec 31 1 BCE (0000-12-31) and Jan 1 1 CE (0001-01-01) |
+0.001369 | +1.001369 | 0001-01-01 | the first day of 1 CE, least positive day of the year |
+0.5 | +1.5 | 0001-07-01 | middle of 1 CE, 6 months after the 0 origin of Jan 1 1 CE |
+0.998631 | +1.998631 | 0001-12-31 | last day of 1 CE, most positive day of the year |
+1.001369 | +2.001369 | 0002-01-01 | the first day of 2 CE, least positive day of the year |
+1.998631 | +2.998631 | 0002-12-31 | last day of 2 CE, most positive day of the year |
At OpenHistoricalMap, for the purpose of filtering vector tiles, we needed a method of converting dates into a number which could be unequivocally compared as >=
and <=
.
- Dates in ISO 8601 string format such as 2000-01-01 fall flat when dealing with BCE dates, e.g. -2500-01-01 is greater than -2499-12-31
- We need support outside the range of the Unix epoch (1900-2039) and earlier than that of the Julian calendar (-4713-01-01).
- Existing libraries do not support dates outside of their range. Dates prior to 0 J are out of range in Python and in PostgreSQL, and are silently (erroneously) converted to 0 J by PHP. Underlying C libraries using
struct tm
do not work outside of Unix epoch range.
- Existing libraries do not support dates outside of their range. Dates prior to 0 J are out of range in Python and in PostgreSQL, and are silently (erroneously) converted to 0 J by PHP. Underlying C libraries using
Effectively, this means we had to create our own implementation of the R decimal_date()
function, without recourse to underlying libraries.
As such, the technique chosen here is to convert the specified date into a decimal year, without recourse to the underlying date/time libraries.
- Dates are supplied in ISO 8601-like format and support positive and negative years, e.g. -2000-01-01 and 2000-01-01 and +2000-01-01
- The Gregorian calendar is treated as proleptic: 365 or 366 says, February 29 existing only when
year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
- The returned decimal value represents the year, plus a decimal portion indicating "how far along" the year is at 12 noon on the given day. Keep in mind that since years vary between 365 and 336 days' length, the decimal "value" of a date may vary between years:
- Example: 1999 has 365 days, so 12 noon on January 1 would be 1999.00136986 and on December 31 would be 1999.99863014
- Example: 2000 has 366 days, so 12 noon on January 1 would be 2000.00136612 and on December 31 would be 2000.99863388
- In the case of negative years (BCE dates) the decimal portion is "inverted" into days from December 31, since December of a BCE year is closer to the 0 mark.
- Example: Dec 31 2000 BCE is -2000.00136612 and Jan 1 2000 BCE is -2000.99863388