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

edit

The year was computed from the vernal w:equinox, which is take 365.24219 days (The actual value is 365.2422464 days)[1]. 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.[2] This algorithm in python3 programming language is[3]

# 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[0] == 128):                   # if realYear is inside of 128-year subcycle 
        wi1 = whereIs(cycle128, wi[1])  # find what subcycle of 128-cycle includes the wi[1]
        if(wi1[0] == 29):               # if realYear is inside of 29-year sub-subcycle 
            wi2 = whereIs(cycle29, wi1[1])
            if wi2[1] == wi2[0] - 1:    # if wi2[1] mod wi2[0] becomes wi2[0] - 1 (wi2[0] is 4 or 5)
                return True
        elif(wi1[0] == 33):             # if realYear is inside of 33-year sub-subcycle 
            wi2 = whereIs(cycle33, wi1[1])
            if wi2[1] == wi2[0] - 1:
                return True

    elif(wi[0] == 132):                 # if realYear is inside of 132-year subcycle 
        wi1 = whereIs(cycle132, wi[1])
        if(wi1[0] == 29):
            wi2 = whereIs(cycle29, wi1[1])
            if wi2[1] == wi2[0] - 1:
                return True
        elif(wi1[0] == 33):
            wi2 = whereIs(cycle33, wi1[1])
            if wi2[1] == wi2[0] - 1:
                return True
        elif(wi1[0] == 37):
            wi2 = whereIs(cycle37, wi1[1])
            if wi2[1] == wi2[0] - 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[0]*p[1]:            # if y is inside one of subcycles
            # p[1] is the length of subcycle
            # y % p[1] is y mod p[1], which gives the position of y inside one of p[1]s
            return (p[1], y % p[1])  
        y -= p[0]*p[1]               # if y is not inside of p[1] 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.[4] 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.
  4. http://www.aitotours.com/aboutiran/14/iranian-calendar/default.aspx