Julian Day Number Algorithms
Potential Confusions
The following functions only calculate what I call the Julian serial day number; that is, they are a mapping of dates to integers. They do not take in to account time of day or the time zone like what I call astronomical Julian day numbers which include a decimal part.
A number indicating the number in a year is also sometimes called a Julian day; that is not what these algorithms calculate, though, given the output of these algorithms is easy to convert between dates and those type of Julian days.
Finally, Julian day numbers are sometimes called Julian dates, but in my usage a Julian date is a date expressed in the Julian calendar.
Handling of BCE Years
As discussed at BC or BCE Not Negative, historians use what is called historical year numbering in which 1 CE is considered to be immediately preceeded by 1 BCE without a year zero, but this creates an off-by-one problem for calculations involving year intervals crossing the CE/BCE boundary. So astronomers usually use what is called astronomical year numbering in which year 1 is considered to be preceeded by year 0, then by year -1, etc.; while this means that astronomers have years offset by one from historians prior to 1 CE, this makes calculating intervals computable by simple subtraction not requiring an off-by-one correction when crossing the boundary from negative years to positive years.
Problems With the Div and Mod Operators With Negative Operands
Almost all modern computer languages implement an integer division and modulus operators -- abbreviated "div" and "mod", respectively, here. In short, imagine dividing with a remainder; it is not true that computers can not do this: div gives the integer part of the division and mod gives the remainder. (Python, the language used in the examples here, uses // for div and % for mod.) All computer languages agree on the values given by their div and mod operators when both operands are positive (or the first operand is 0 and the second operand is positive); however, when one or both of the operands in negative then they disagree. Refer to the table below for the behavior in Python and C.
| x | x div 4 | x mod 4 | ||
|---|---|---|---|---|
| Python | C | Python | C | |
| 12 | 3 | 3 | 0 | 0 |
| 11 | 2 | 2 | 3 | 3 |
| 10 | 2 | 2 | 2 | 2 |
| 9 | 2 | 2 | 1 | 1 |
| 8 | 2 | 2 | 0 | 0 |
| 7 | 1 | 1 | 3 | 3 |
| 6 | 1 | 1 | 2 | 2 |
| 5 | 1 | 1 | 1 | 1 |
| 4 | 1 | 1 | 0 | 0 |
| 3 | 0 | 0 | 3 | 3 |
| 2 | 0 | 0 | 2 | 2 |
| 1 | 0 | 0 | 1 | 1 |
| 0 | 0 | 0 | 0 | 0 |
| -1 | -1 | (-)0 | 3 | -1 |
| -2 | -1 | (-)0 | 2 | -2 |
| -3 | -1 | (-)0 | 1 | -3 |
| -4 | -1 | -1 | 0 | 0 |
| -5 | -2 | -1 | 3 | -1 |
| -6 | -2 | -1 | 2 | -2 |
| -7 | -2 | -1 | 1 | -3 |
| -8 | -2 | -2 | 0 | 0 |
While I have indicated some zeros as negative zeros in the C column, I do that to indicate the logic not because there are positive and negative zeros. Note how the pattern started in the non-negative numbers persists in to the negative numbers in Python: the mod repeats downward 3, 2, 1, 0; and the div is consistently divided to to groups of four; four 2's, then four 1's, then four 0's, then four -1's, etc. This is the behavior one wants in the calendar algorithms. Unfortunately, not all languages provide it: C does not. Because of the way the algorithms work, when the date before 0 March 01, a negative number will be fed to at least one div or mod operator.
Unlike the Easter case, where I ignored the problem of what happens when a language like C gets fed a negative year in the algorithm; I can't ignore it here; some people will want to calculate Julian day numbers for dates before 1 CE. So I give alternate algorithms that avoid ever feeding negative operands to the div and mod operators. While some people use tricks like (year%4 + 4)%4 in C, I chose to treat the div and mod operators as undefined for negative operands, except in the first simplest program; this guarantees that even if the language does something other than the Python or C case with negative operands (raises an error, perhaps), that my algorithm will still work.
The Programs
All of my programs in Python must start with:
#!usr/bin/python3
from enum import Enum
class Caltype(Enum):
greg=1
jul=2
class Date(object):
def __init__(self, year, month, day):
self.year=year
self.month=month
self.day=day
return
def __repr__(self):
return ("Date("+str(self.year)+","+str(self.month)+","+str(self.day)+")")This code is not part of the fundamental algorithm. It does set up the flags for the Gregorian and Julian calendars.
Python Compatable div and mod Code
The following code assumes that div and mod work as they do in Python or that one never passes dates before 0 March 01 or Julian Day numbers that would result in dates before 0 March 01.
def cal2jd(date,caltype=Caltype.greg):
year=date.year
month=date.month
day=date.day
if (month<3):
year=year-1
month=month+12
jd=365*year+year//4
jd=jd+(153*(month+1))//5+day-123
if caltype==Caltype.greg:
jd=jd-year//100+year//400
jd=jd+1721120
else:
jd=jd+1721118
return jd
def jd2cal(jd,caltype=Caltype.greg):
if caltype==Caltype.greg:
day=jd-1721120
a=(4*day+3)//146097
day=day+a-a//4
else:
day=jd-1721118
year=(4*day+3)//1461
day=day-(1461*year)//4
month=(5*day+2)//153
day=day-(153*month+2)//5
day=day+1
month=month+3
if (month>12):
month=month-12
year=year+1
return Date(year,month,day)The previous code works in Python for all dates including those before 0 March 01 and negative Julian day numbers. The following functions are unnecessary in Python; they are provided for translation in to languages that do not guarantee that div and mod work as they do in Python or in which integers might overflow.
Python Incompatable div and mod Error-check Only
I recommend putting in a test for errors if one is working in a language which does not guarantee Python behavior for the div and mod operators with a negative first operand. This as Python code looks like this:
def cal2jd(date,caltype=Caltype.greg):
year=date.year
month=date.month
day=date.day
if (month<3):
year=year-1
month=month+12
if year<0:
raise ValueError("Too early.")
jd=365*year+year//4
jd=jd+(153*(month+1))//5+day-123
if caltype==Caltype.greg:
jd=jd-year//100+year//400
jd=jd+1721120
else:
jd=jd+1721118
return jd
def jd2cal(jd,caltype=Caltype.greg):
if caltype==Caltype.greg:
day=jd-1721120
a=(4*day+3)//146097
day=day+a-a//4
else:
day=jd-1721118
if day<0:
raise ValueError("Too early.")
year=(4*day+3)//1461
day=day-(1461*year)//4
month=(5*day+2)//153
day=day-(153*month+2)//5
day=day+1
month=month+3
if (month>12):
month=month-12
year=year+1
return Date(year,month,day)Python Incompatable div and mod (Static Offset Method)
I have two alternate code possibilities if one is interested in working with dates before 0 March 01 and can't guarantee the behavior of the mod and div operators with negative numbers will be as in Python. The first is the offset method. This can be used when one only needs a limited range of negative years.
- Determine the earliest date that one needs correct results for; eg, -4712 January 01.
- Take the latest date such that the year is a mulitple of 400 and the day is March 01; eg, -4800 March 01.
- Take the absolute value of that year and divide it by 400 and call that number cycles; eg, 12.
- Then you will need to calculate three constants:
- c1=cycles*400; eg, 4800
- c2=1721120 - 146097*cycles; eg, -32044
- c3=1721118 - 146100*cycles; eg, -32082
Then use this code substituting in the constants you have calculated:
def cal2jd(date,caltype=Caltype.greg):
year=date.year
month=date.month
day=date.day
year=year+c1
if (month<3):
year=year-1
month=month+12
if year<0:
raise ValueError("Too early.")
jd=365*year+year//4
jd=jd+(153*(month+1))//5+day-123
if caltype==Caltype.greg:
jd=jd-year//100+year//400
jd=jd+c2
else:
jd=jd+c3
return jd
def jd2cal(jd,caltype=Caltype.greg):
if caltype==Caltype.greg:
day=jd-c2
a=(4*day+3)//146097
day=day+a-a//4
else:
day=jd-c3
if day<0:
raise ValueError("Too early.")
year=(4*day+3)//1461
day=day-(1461*year)//4
month=(5*day+2)//153
day=day-(153*month+2)//5
day=day+1
month=month+3
if (month>12):
month=month-12
year=year+1
year=year-c1
return Date(year,month,day)Python Incompatable div and mod (Dynamic Offset Method)
Suppose that one is unable or unwilling to give a limit on the earliest date one might possibly need but still can't guarantee the Python-like behavior for the div and mod operators. Then one needs this code:
def cal2jd(date,caltype=Caltype.greg):
year=date.year
month=date.month
day=date.day
if year<0:
if caltype==Caltype.greg:
cycles=-((-year)//400)-1
if (-year)%400==0:
cycles=cycles+1
year=(400-((-year)%400))%400
else: ## caltype==Caltype.jul
cycles=-((-year)//4)-1
if (-year)%4==0:
cycles=cycles+1
year=(4-((-year)%4))%4
else:
cycles=0
if (month<3):
year=year-1
month=month+12
jd=365*year+year//4
jd=jd+(153*(month+1))//5+day-123
if caltype==Caltype.greg:
jd=jd-year//100+year//400
jd=jd+1721120+146097*cycles
else:
jd=jd+1721118+1461*cycles
return jd
def jd2cal(jd,caltype=Caltype.greg):
if caltype==Caltype.greg:
day=jd-1721120
if day<0:
cycles=-((-day)//146097)-1
if (-day)%146097==0:
cycles=cycles+1
day=(146097-((-day)%146097))%146097
else:
cycles=0
a=(4*day+3)//146097
day=day+a-a//4
else: ## caltype==Caltype.jul
day=jd-1721118
if day<0:
cycles=-((-day)//1461)-1
if (-day)%1461==0:
cycles=cycles+1
day=(1461-((-day)%1461))%1461
else:
cycles=0
year=(4*day+3)//1461
day=day-(1461*year)//4
month=(5*day+2)//153
day=day-(153*month+2)//5
day=day+1
month=month+3
if (month>12):
month=month-12
year=year+1
if caltype==Caltype.greg:
year=year+400*cycles
else:
year=year+4*cycles
return Date(year,month,day)Preventing overflows
If one is using a language where integers can overflow (eg, C), the value of 4*day+3 in line year=(4*day+3)//1461 where the final result would not; this isn't the only point of possible overflow either. If one wants to prevent this (which may not be necessary depending on the maximum date one expects to have to handle), then the following code is what you need:
def cal2jd(date,caltype=Caltype.greg):
year=date.year
month=date.month
day=date.day
if caltype==Caltype.greg:
if year<0:
cycles=-((-year)//400)-1
if (-year)%400==0:
cycles=cycles+1
year=(400-((-year)%400))%400
else:
cycles=year//400
year=year%400
else: ## caltype==Caltype.jul
if year<0:
cycles=-((-year)//4)-1
if (-year)%4==0:
cycles=cycles+1
year=(4-((-year)%4))%4
else:
cycles=year//4
year=year%4
if (month<3):
year=year-1
month=month+12
jd=365*year+year//4
jd=jd+(153*(month+1))//5+day-123
if caltype==Caltype.greg:
jd=jd-year//100+year//400
jd=jd+1721120+146097*cycles
else:
jd=jd+1721118+1461*cycles
return jd
def jd2cal(jd,caltype=Caltype.greg):
if caltype==Caltype.greg:
day=jd-1721120
if day<0:
cycles=-((-day)//146097)-1
if (-day)%146097==0:
cycles=cycles+1
day=(146097-((-day)%146097))%146097
else:
cycles=day//146097
day=day%146097
a=(4*day+3)//146097
day=day+a-a//4
else: ## caltype==Caltype.jul
day=jd-1721118
if day<0:
cycles=-((-day)//1461)-1
if (-day)%1461==0:
cycles=cycles+1
day=(1461-((-day)%1461))%1461
else:
cycles=day//1461
day=day%1461
year=(4*day+3)//1461
day=day-(1461*year)//4
month=(5*day+2)//153
day=day-(153*month+2)//5
day=day+1
month=month+3
if (month>12):
month=month-12
year=year+1
if caltype==Caltype.greg:
year=year+400*cycles
else:
year=year+4*cycles
return Date(year,month,day)What about the "Revised Julian"/Eastern Orthodox calendar?
I cover that in Julian Day Algorithms for Non-standard Calendars.