Persian Calendar

This book meant to collect data about Jalali/Persian Calendar, which is currently used by Iranian. I wish others help me to complete the historical information, but what I want to share is some functions about how to calculate the leap years of this calendar.

Leap Years

The year was computed from the vernal w:equinox, which is take 365.24219 days (The actual value is 365.2422464 days). In order to evaluate the length of one year, Khayyam made a 2820-year cycle rule to find the leap years. Leap years have 366 days and others have 365 days. Here we explain the rule and write its algorithm. The 2820-year cycle is divided into 21 subcycles of 128 years each, and a 132-year subcycle at the end of each 2820-year cycle. A 128-year subcycle consists of a 29-year sub-subcycle, followed by 3 sub-subcycles of 33 years each. Finally, the 132-year subcycle consists of one sub-subcycle of 29 years, followed by two 33-year sub-subcycles and a final sub-subcycle of 37 years. The years are numbered within each cycle. Writing n for the number of a year within a cycle, this year is a leap year if n > 1 and n mod 4 = 1. This algorithm in python3 programming language is

# This is the implementation of Khayyam rules. year is an integer parameter.
def isLeapYearReal(year):
# The 2820-year cycle includes 21 128-year subcycles, and a 132-year subcycle
cycle2820 = ((21,128),(1,132))
# The 128-year subcycle includes a 29-year sub-subcycles, and three 33-year sub-subcycle
cycle128  = ((1,29),(3,33))
cycle132  = ((1,29),(2,33),(1,37))
cycle29   = ((1,5),(6,4))
cycle33   = ((1,5),(7,4))
cycle37   = ((1,5),(8,4))

if year > 0:
realYear = (year + 37) % 2820   # realYear includes zero
elif year < 0:
# 38 years separating the beginning of the 2820-year cycle from Hejira
realYear = (year + 38) % 2820
else:
return None                     # There is no zero year!!

wi = whereIs(cycle2820, realYear)   # find what subcycle of 2820-year cycle includes the realYear
if(wi == 128):                   # if realYear is inside of 128-year subcycle
wi1 = whereIs(cycle128, wi)  # find what subcycle of 128-cycle includes the wi
if(wi1 == 29):               # if realYear is inside of 29-year sub-subcycle
wi2 = whereIs(cycle29, wi1)
if wi2 == wi2 - 1:    # if wi2 mod wi2 becomes wi2 - 1 (wi2 is 4 or 5)
return True
elif(wi1 == 33):             # if realYear is inside of 33-year sub-subcycle
wi2 = whereIs(cycle33, wi1)
if wi2 == wi2 - 1:
return True

elif(wi == 132):                 # if realYear is inside of 132-year subcycle
wi1 = whereIs(cycle132, wi)
if(wi1 == 29):
wi2 = whereIs(cycle29, wi1)
if wi2 == wi2 - 1:
return True
elif(wi1 == 33):
wi2 = whereIs(cycle33, wi1)
if wi2 == wi2 - 1:
return True
elif(wi1 == 37):
wi2 = whereIs(cycle37, wi1)
if wi2 == wi2 - 1:
return True
return False

def whereIs(cycle, year):            # a function to find what subcycle includes the year
y = year
# for example p is (21,128), which means this cycle have 21 of 128-year subcycles
for p in cycle:
if y < p*p:            # if y is inside one of subcycles
# p is the length of subcycle
# y % p is y mod p, which gives the position of y inside one of ps
return (p, y % p)
y -= p*p               # if y is not inside of p subcycle prepare for next subcycle

where 38 represents the years separating the beginning of the 2820-year cycle from Hejira - the year of Mohammed's flight from Mecca to Medina, corresponding to 621-622 AD, which the Jalali panel of scientists chose as the first year of the Iranian calendar. As you can see this algorithm is too long and slow. To improve the calculation here is an extrapolation of above function

# a function to extrapolate leap years just like isLeapYearReal(year)
def isLeapYear(year):
a = 0.025                     # a and b are two parameters. which are tuned
b = 266
if year > 0:
# 38 days is the difference of epoch to 2820-year cycle
leapDays0 = ((year + 38) % 2820)*0.24219 + a  # 0.24219 ~ extra days of one year
leapDays1 = ((year + 39) % 2820)*0.24219 + a
elif year < 0:
leapDays0 = ((year + 39) % 2820)*0.24219 + a
leapDays1 = ((year + 40) % 2820)*0.24219 + a
else:
# In case of using isLeapYear(year - 1) as last year. Look FixedDate function
return True

frac0 = int((leapDays0 - int(leapDays0))*1000)    # the fractions of two consecutive days
frac1 = int((leapDays1 - int(leapDays1))*1000)

# 242 fraction, which is the extra days of one year, can happened twice inside
# a 266 interval so we have to check two consecutive days
if frac0 <= b and frac1 > b : # this year is a leap year if the next year wouldn't be a leap year
return True
else:
return False

where a and b are two parameters, which are tuned. Another function that is so useful in programming is how to extrapolate the days that passed from the epoch (FARVARDIN 1, 1) to the first day of each year (FARVARDIN 1, year).

# find the interval in days between FARVARDIN 1 of this year and the first one
def FixedDate(year):
if year > 0:
realYear = year - 1   # realYear includes zero
elif year < 0:
realYear = year
else:
return None           # There is no zero year!!

cycle = (realYear + 38) % 2820                  # cycle is (realYear + 38) mod 2820
base = int( (realYear + 38) / 2820)
if realYear + 38 < 0: base -= 1
days = 1029983 * base                           # 1029983 is the total days of one 2820-year cycle
days += int((cycle - 38) * 365.24219) + 1
if cycle - 38 < 0: days -= 1
extra = cycle * 0.24219                         # 0.24219 ~ extra days of one year
frac = int((extra - int(extra))*1000)           # frac is the fraction of extra days
if isLeapYear(year - 1) and frac <= 202:        # 202 is a tuned parameter
days += 1

return days

There is no limitation for these functions as long as Kayyam rules are correct. To become convinced anyone can use this test function.

def test():
days = 1                                         # The first day of calendar, FARVARDIN 1, 1
for year in range(1,2850):
# check if the estimated function is the same as the real one
if isLeapYear(year) != isLeapYearReal(year):
print("wrong!!")

if FixedDate(year) != days:
print("wrong!!")

if isLeapYear(year):                         # add 366 days for leap years
days += 366
else:
days += 365

days = 1                                         # The first day of calendar, FARVARDIN 1, 1
for year in range(-1,-2850,-1):                  # do the same for negative years
if isLeapYear(year) != isLeapYearReal(year):
print("wrong!!")

if isLeapYear(year):
days -= 366
else:
days -= 365

if FixedDate(year) != days:
print("wrong!!")
1. Kazimierz M. Borkowski, "The tropical year and solar calendar", The Journal of the Royal Astronomical Society of Canada 85/3 (June 1991) 121–130.
2. http://www.ortelius.de/kalender/pers_en.php
3. The repository for main functions. http://github.com/hadilq/persian-calendar-important-functions/blob/master/persianCalendar.py.