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.
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!!")
- Kazimierz M. Borkowski, "The tropical year and solar calendar", The Journal of the Royal Astronomical Society of Canada 85/3 (June 1991) 121–130.
- The repository for main functions. http://github.com/hadilq/persian-calendar-important-functions/blob/master/persianCalendar.py.