#! /usr/local/bin/python3

# CGI script to compute the Gregorian solar and lunar calendars
# of any given year

# See <URL: http://www.madore.org/~david/misc/calendar.html >

# This version: 2023-07-03 (2023-L07-15)

# Previously: 2003-11-08 (2003-L11-14)

# This file is in the public domain.

# Author: David A. Madore <david+www@madore.org>

import sys
import cgi
import html
import time
import os

# Return the epact of any given Gregorian year.
def epact(year):
    a = year%19
    k = year//100
    p = (8*k+13)//25
    q = k//4
    M = 8 + p + q + 29*k
    d = (11*a+M)%30
    return d

# Find the month of a given day in the year table.
def findmonth(day,yeartab):
    i = 0
    while i<len(yeartab)-1:
        if day>=yeartab[i] and day<yeartab[i+1]: break
        i += 1
    return i

# Name a year
def nameyear(year):
    if year <= 0:
        return ("~%d" % (1-year))
    else:
        return ("%d" % year)

# Name a solar month.
solarnametab = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
def namesolarmonth(month,year):
    yr = year
    mn = month-2
    while mn<0:
        yr -= 1
        mn += 12
    while mn>=12:
        yr += 1
        mn -= 12
    return (mn, solarnametab[mn] + " " + nameyear(yr))

# Name a lunar month.
lunarnametab = ["Terminus", "Lipidus", "Venuch", "Amber", "Pook", "Jupe", "Tibery", "Claudy", "Septil", "Octil", "Novil", "Decil", "Mercuary"]
def namelunarmonth(month,year):
    yr = year
    mn = month-2
    while mn<0:
        yr -= 1
        mn += 13
    while mn>=13:
        yr += 1
        mn -= 13
    return (mn, lunarnametab[mn] + " " + nameyear(yr))

# Return "even", "odd" or "mer"
def parity(n):
    if n == 12: return "mer"
    elif n % 2 == 0: return "odd" # Months are internally numbered from 0...
    else: return "even"

# Die with HTTP 400 error.
def die400(msg):
    sys.stdout.write("Content-Type: text/plain\nStatus: 400 Bad Request\n\n"+msg)
    sys.exit(1)

# Print HTTP headers.
def announce():
    sys.stdout.write("Content-Type: text/html\n\n")

# CGI magick...
form = cgi.FieldStorage()
yearstring = form.getvalue("year", "NOW")
if type(yearstring) is type([]): # This cgi module REALLY SUCKS!!!
    yearstring = yearstring[0]
if not type(yearstring) is type(""): die400("Bad year parameter.")
if yearstring == "NOW":
    year = time.localtime()[0]
else:
    try:
        if yearstring[0]=="~":
            year = 1-int(yearstring[1:])
        else:
            year = int(yearstring)
    except ValueError: die400("Invalid year parameter.")
announce()

# Now the real calendar stuff!

# indiction is Roman indiction
indiction = ((year+2)%15)+1
# gn is golden number
gn = (year%19)+1
ngn = ((year+1)%19)+1
pgn = ((year-1)%19)+1
# ept is epact
ept = epact(year)
nept = epact(year+1)
pept = epact(year-1)
# embol indicates whether year is embolismic
if ept >= 16 and ept <= 24+(gn>=12):
    embol = 1
elif ept >= 12 and ept <= 15:
    embol = not ( nept >= 16 and nept <= 24+(ngn>=12) )
else:
    embol = 0
if pept >= 16 and pept <= 24+(pgn>=12):
    pembol = 1
elif pept >= 12 and pept <= 15:
    pembol = not ( ept >= 16 and ept <= 24+(gn>=12) )
else:
    pembol = 0
# Whether year is centennial, solar leap, lunar leap
centennial = (year%100 == 0)
sleap = (year%4 == 0) and (not centennial or year%400 == 0)
lleap = (year%4 == 0) and (not centennial or not ((year//100)%25) in [2,5,8,11,14,18,21,24])
psleap = ((year-1)%4 == 0) and (not ((year-1)%100 == 0) or (year-1)%400 == 0)
# dow is day of week of March 5th, where 0=Sunday, 1=Monday, ..., 6=Saturday
dow = (year + year//4 - year//100 + year//400) % 7
# Reference day (0) is a Monday (which Monday hardly matters).
# Compute start of solar months (wrt reference day).
march1 = 2+dow
february1 = march1-(28+sleap)
january1 = february1-31
pdecember1 = january1-31
pnovember1 = pdecember1-30
april1 = march1+31
may1 = april1+30
june1 = may1+31
july1 = june1+30
august1 = july1+31
september1 = august1+31
october1 = september1+30
november1 = october1+31
december1 = november1+30
njanuary1 = december1+31
nfebruary1 = njanuary1+31
solaryeartab = [pnovember1,pdecember1,january1,february1,march1,april1,may1,june1,july1,august1,september1,october1,november1,december1,njanuary1,nfebruary1]
# Compute start of lunar months (wrt reference day).
if ept <= 24+(gn>=12): amber1 = march1+30-ept
else: amber1 = march1+60-ept
venuch1 = amber1-30
lipidus1 = venuch1-(29+lleap)
terminus1 = lipidus1-(30-(gn==1 and not pembol))
if pembol:
    pmercuary1 = terminus1-(30-(gn==1))
    pdecil1 = pmercuary1-29
else:
    pdecil1 = terminus1-29
    pmercuary1 = terminus1 # Fictitious value (month does not exist)
pook1 = amber1+29
jupe1 = pook1+30
tibery1 = jupe1+29
claudy1 = tibery1+30
septil1 = claudy1+29
octil1 = septil1+30
novil1 = octil1+29
decil1 = novil1+30
mercuary1 = decil1+29
if embol:
    nterminus1 = mercuary1+(30-(ngn==1))
    nlipidus1 = nterminus1+30
else:
    nterminus1 = mercuary1
    nlipidus1 = nterminus1+(30-(ngn==1))
lunaryeartab = [pdecil1,pmercuary1,terminus1,lipidus1,venuch1,amber1,pook1,jupe1,tibery1,claudy1,septil1,octil1,novil1,decil1,mercuary1,nterminus1,nlipidus1]
# A few other remarkable days
startdayu = min(january1,terminus1)
startday = startdayu - startdayu%7
stopdayu = max(njanuary1,nterminus1)
stopday = stopdayu + (7-stopdayu)%7
isostart = january1+3 - (january1+3)%7
isonstart = njanuary1+3 - (njanuary1+3)%7
pjanuary1 = january1 - (365+psleap)
isopstart = pjanuary1+3 - (pjanuary1+3)%7
if amber1+13 >= march1+20: paschalmoon = amber1+13
else: paschalmoon = pook1+13
easter = paschalmoon+1 + (5-paschalmoon)%7
eastermonth = findmonth(easter,solaryeartab)
(_,eastermonthname) = namesolarmonth(eastermonth,year)
easterlunarmonth = findmonth(easter,lunaryeartab)
(_,easterlunarmonthname) = namelunarmonth(easterlunarmonth,year)
christmas = december1+24
pchristmas = pdecember1+24

# Now do the printing
sys.stdout.write("""<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta charset="utf-8" />
""")
sys.stdout.write("<title>Gregorian Solar and Lunar Calendar for %s</title>\n" % nameyear(year))
sys.stdout.write("""<style type="text/css">"
/* <![CDATA[ */
h1 { text-align: center; }
#computus { font-size: .83em; }
#computus > dt { font-weight: bold; }
.headrow > th { font-size: .83em; }
.calrow > td { font-family: monospace; }
.feast { font-weight: normal; color: rgb(0,0,255); }
.odd { background: rgb(255,224,224); }
.even { background: rgb(224,255,224); }
.mer { background: rgb(255,255,224); }
/* ]]> */
</style>
""")
sys.stdout.write("</head>\n")
sys.stdout.write("<body>\n")
warning = ""
if year <= 1582: warning = " (proleptic Gregorian calendar)"
sys.stdout.write("<h1>Gregorian Solar and Lunar Calendar for %s%s</h1>\n" % (nameyear(year),warning))
sys.stdout.write("<dl id=\"computus\">\n")
sys.stdout.write("<dt>Roman indiction</dt><dd>%d</dd>\n" % indiction)
sys.stdout.write("<dt>Golden number</dt><dd>%d</dd>\n" % gn)
sys.stdout.write("<dt>Epact</dt><dd>%d</dd>\n" % ept)
if sleap:
    sys.stdout.write("<dt>Dominical letters</dt><dd>%c%c</dd>\n" % (chr(65+(8-dow)%7),chr(65+(7-dow)%7)))
else:
    sys.stdout.write("<dt>Dominical letter</dt><dd>%c</dd>\n" % chr(65+(7-dow)%7))
sys.stdout.write("<dt>Easter</dt><dd>%d %s (%d %s)</dd>\n" % (easter-solaryeartab[eastermonth]+1, eastermonthname, easter-lunaryeartab[easterlunarmonth]+1, easterlunarmonthname))
sys.stdout.write("</dl>\n")
sys.stdout.write("<table border=\"1\">\n")
sys.stdout.write("""<tr class="headrow">
<th colspan="9">Solar</th>
<th><acronym>ISO</acronym></th>
<th colspan="9">Lunar</th>
</tr>
<tr class="headrow">
<th>Month</th>
<th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th><th>Sa</th><th>Su</th>
<th>Month</th>
<th>Week</th>
<th>Month</th>
<th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th><th>Sa</th><th>Su</th>
<th>Month</th>
</tr>
""")
nbweeks = (stopday-startday)//7
row = 0
monday = startday
while row < nbweeks:
    sunday = monday+6
    sys.stdout.write("<tr class=\"calrow\">\n")
    # Print solar week
    month = findmonth(monday,solaryeartab)
    (smonth,monthname) = namesolarmonth(month,year)
    if row == 0 or monday-7 < solaryeartab[month]:
        rows = min(nbweeks-row, (solaryeartab[month+1]-monday+6)//7)
        sys.stdout.write("<td rowspan=\"%d\" class=\"%s\">%s</td>\n" % (rows,parity(smonth),monthname))
    day = monday
    while day<=sunday:
        while day >= solaryeartab[month+1]:
            month += 1
            smonth += 1
            while smonth >= 12: smonth -= 12
        if day == easter or day == christmas or day == pchristmas:
            sys.stdout.write("<td class=\"%s\"><b class=\"feast\">%d</b></td>\n" % (parity(smonth),day-solaryeartab[month]+1))
        else:
            sys.stdout.write("<td class=\"%s\">%d</td>\n" % (parity(smonth),day-solaryeartab[month]+1))
        day += 1
    # month = findmonth(sunday,solaryeartab)
    (smonth,monthname) = namesolarmonth(month,year)
    if row == 0 or sunday-7 < solaryeartab[month]:
        rows = min(nbweeks-row, (solaryeartab[month+1]-sunday+6)//7)
        sys.stdout.write("<td rowspan=\"%d\" class=\"%s\">%s</td>\n" % (rows,parity(smonth),monthname))
    # Print ISO week
    if monday < isostart:
        sys.stdout.write("<td>%s-W%02d</td>\n" % (nameyear(year-1), (monday-isopstart)/7+1))
    elif monday < isonstart:
        sys.stdout.write("<td>%s-W%02d</td>\n" % (nameyear(year), (monday-isostart)/7+1))
    else:
        sys.stdout.write("<td>%s-W%02d</td>\n" % (nameyear(year+1), (monday-isonstart)/7+1))
    # Print lunar week
    month = findmonth(monday,lunaryeartab)
    (lmonth,monthname) = namelunarmonth(month,year)
    if row == 0 or monday-7 < lunaryeartab[month]:
        rows = min(nbweeks-row, (lunaryeartab[month+1]-monday+6)//7)
        sys.stdout.write("<td rowspan=\"%d\" class=\"%s\">%s</td>\n" % (rows,parity(lmonth),monthname))
    day = monday
    while day<=sunday:
        while day >= lunaryeartab[month+1]:
            month += 1
            lmonth += 1
            while lmonth >= 13: lmonth -= 13
        if day == easter or day == christmas or day == pchristmas:
            sys.stdout.write("<td class=\"%s\"><b class=\"feast\">%d</b></td>\n" % (parity(lmonth),day-lunaryeartab[month]+1))
        else:
            sys.stdout.write("<td class=\"%s\">%d</td>\n" % (parity(lmonth),day-lunaryeartab[month]+1))
        day += 1
    # month = findmonth(sunday,lunaryeartab)
    (lmonth,monthname) = namelunarmonth(month,year)
    if row == 0 or sunday-7 < lunaryeartab[month]:
        rows = min(nbweeks-row, (lunaryeartab[month+1]-sunday+6)//7)
        sys.stdout.write("<td rowspan=\"%d\" class=\"%s\">%s</td>\n" % (rows,parity(lmonth),monthname))
    # Conclude
    sys.stdout.write("</tr>\n")
    row += 1
    monday += 7
sys.stdout.write("""<tr class="headrow">
<th>Month</th>
<th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th><th>Sa</th><th>Su</th>
<th>Month</th>
<th>Week</th>
<th>Month</th>
<th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th><th>Sa</th><th>Su</th>
<th>Month</th>
</tr>
<tr class="headrow">
<th colspan="9">Solar</th>
<th><acronym>ISO</acronym></th>
<th colspan="9">Lunar</th>
</tr>
""")
sys.stdout.write("</table>\n")

# Provide a link to reuse this script.
try:
    server_name = os.environ["SERVER_NAME"]
    server_port = os.environ["SERVER_PORT"]
    script_name = os.environ["SCRIPT_NAME"]
    if server_port == "80":
        selfuri = ("http://%s%s" % (server_name, script_name))
    else:
        selfuri = ("http://%s:%s%s" % (server_name, server_port, script_name))
    sys.stdout.write("<form action=\"%s\">\n" % (html.escape(selfuri,1)))
    sys.stdout.write("<p>Display calendar for\n")
    sys.stdout.write("<a href=\"%s?year=%d\">previous year</a>,\n" % (html.escape(selfuri,1), year-1))
    sys.stdout.write("<a href=\"%s?year=%d\">this year</a>,\n" % (html.escape(selfuri,1), year))
    sys.stdout.write("<a href=\"%s?year=%d\">next year</a>,\n" % (html.escape(selfuri,1), year+1))
    sys.stdout.write("<a href=\"%s?year=NOW\">current year</a>,\n" % (html.escape(selfuri,1)))
    sys.stdout.write("or arbitrary year: <input type=\"text\" name=\"year\" />\n")
    sys.stdout.write("<input type=\"submit\" value=\"Show it!\" /></p>\n")
    sys.stdout.write("</form>\n")
except KeyError:
    pass

sys.stdout.write("<p>See <a href=\"http://www.madore.org/~david/misc/calendar.html\">this page</a> for more explanations.</p>\n")
sys.stdout.write("</body>\n")
sys.stdout.write("</html>\n")
