A form at the bank, a school’s admission form or an exam board’s website asks for your date of birth in AD. Your birth certificate gives it in BS: 6 Kartik 2066, say. Nepal lives with two calendars, Bikram Sambat (BS) for the government, schools and festivals, and AD for most of the world, and every day someone has to turn a date in one into the other.
You might expect a short formula to do it: BS is about 56 years and 8 months ahead of AD, so subtract. The widget below does it properly. Pick a BS date and it gives the AD date; under it, the table it used.
Look at the squares for a few years. What do you think: can a formula, with no table, turn every BS date into the right AD date?
Pick an answer to see why.
You need lists and dictionaries in Python (Grade 9). Python’s datetime (Grade 10) is explained where it is used.
Try it
Convert a few dates in the widget, then look at the table more closely.
- Your own birthday. Set the year, month and day of your birth in BS and read the AD date and its day of the week.
- Asar. Move the year one step at a time and watch Asar’s square (the third). It has 31 or 32 days, and the 32s don’t come every fourth year or in any pattern you can count on.
- The same day, other years. 6 Kartik 2083 is Friday 23 October 2026. Move to 2081: 6 Kartik 2081 was 22 October 2024, not 23 October, because 2081 began on 13 April. A BS year doesn’t always begin on the same AD day, and the months inside it don’t always add up the same either.
- Years after 2083. Their squares are dashed. Nobody has fixed those months yet: the lengths shown are a guess, and the real calendar may differ.
- AD. Press AD. Every year’s squares are the same, except February’s: 28 days, or 29 in a leap year.
How it works
The sun keeps the months
Each BS month starts when the sun enters a new sign of the zodiac. Seen from the Earth, the sun seems to move slowly round the sky against the stars, once a year. The circle it follows is cut into twelve equal parts of 30° each, the signs (rashi): Mesh, Brish, Mithun and so on. The moment the sun enters a sign is its sankranti (Mesh Sankranti, Makar Sankranti), and a month begins on the day it falls on, or the next day. Baishakh is the sun’s month in Mesh, Jestha in Brish, Asar in Mithun.
So the length of a month is how long the sun takes to cross one sign. That isn’t the same for every sign. The Earth’s path round the sun is not quite a circle: it is closest to the sun in early January and moves fastest then, and farthest in early July, when it moves slowest. So the sun seems to cross a sign quickest in the winter months, Poush and Magh, in about 29½ days, and slowest in Asar and Shrawan, in about 31½.
A month has to be a whole number of days, though, and the sun can enter a sign at any time of day or night. Whether a month gets 29 days or 30, or 31 or 32, depends on the hour of its sankranti and of the next one. Those hours shift a little every year (a year by the sun is about 365 days and 6 hours), so the lengths shift too.
Fixed to the stars, not the seasons
There is one more twist. The signs are fixed to the stars (sidereal, from the Latin for star): Mesh begins at the same place among the stars every year. The seasons follow a slightly shorter year, from one spring equinox to the next. A year by the stars is 365.256363 days; a year of seasons, the tropical year, is 365.242189 days, 20 minutes and 24.6 seconds shorter, because the Earth’s axis slowly turns like a spinning top’s (the precession of the equinoxes). So BS’s new year creeps later against the seasons, about a day every 70 years. You will see it in your own program’s numbers (Going further still).
Who fixes the table
Working out the sankrantis, and the festivals and tithis that hang on the moon, is the work of the almanac makers (panchang, पञ्चाङ्ग). In Nepal, the Nepal Panchanga Nirnayak Bikas Samiti (the Nepal Calendar Determination Committee, under the Ministry of Culture, Tourism and Civil Aviation) approves which almanacs and wall calendars may be published. For 2083 BS it approved 18 almanacs, which went on sale from 2 Falgun 2082 (14 February 2026), about two months before the year began. That is how far ahead the calendar is fixed: until the almanacs are out, nobody can be sure how long next year’s months are.
The AD calendar needs only a rule
Compare the calendar most of the world uses. Since 1582 (Pope Gregory XIII’s reform, which is why it is called the Gregorian calendar), every month has a fixed length, and one rule says which years have a 29th of February: a year divisible by 4 is a leap year, except a year divisible by 100, unless it is also divisible by 400. So 2024 and 2000 were leap years and 1900 wasn’t. That rule makes 97 leap years in every 400, and its average year is 365.2425 days, close to the tropical year. Any AD date can be worked out from the rule alone, for any year since then and far into the future, with no table and no committee.
Why can’t a converter work out next year’s BS months for itself?
Pick an answer to see why.
Program it
All the programs use the same table, the one in the widget: the number of days in every month from 2000 to 2090 BS, in a file on this page, bs_table.py.
MONTHS is a dictionary: its keys are the years and each value is a list of the 12 month lengths, Baishakh first. So MONTHS[2083][2] is the length of Asar 2083: 32. START is the AD date of 1 Baishakh 2000, the one date the converter knows for sure.
Step 1: a converter from the table
Python’s datetime module knows AD dates. date(1943, 4, 14) is 14 April 1943, and adding timedelta(days=n) to it gives the date n days later, with all the AD months and leap years taken care of. So converting a BS date means only this: count how many days it is after 1 Baishakh 2000, and add them.
from datetime import date, timedeltafrom bs_table import NAMES, START, MONTHS def bs_to_ad(year, month, day): days = 0 for y in range(2000, year): # every whole year before it days = days + sum(MONTHS[y]) for m in range(month - 1): # the months before it, this year days = days + MONTHS[year][m] days = days + day - 1 # and the days before it, this month return date(*START) + timedelta(days=days) for y, m, d in [(2000, 1, 1), (2083, 1, 1), (2083, 7, 6), (2075, 6, 2)]: print(f'{d} {NAMES[m - 1]} {y} BS is', bs_to_ad(y, m, d).strftime('%A %d %B %Y'))Output
1 Baishakh 2000 BS is Wednesday 14 April 19431 Baishakh 2083 BS is Tuesday 14 April 20266 Kartik 2083 BS is Friday 23 October 20262 Asoj 2075 BS is Tuesday 18 September 2018date(*START) unpacks the three numbers of START into date(1943, 4, 14), and strftime('%A %d %B %Y') writes a date out with its weekday and month names. 6 Kartik 2083 is the date Grade 10’s library example gives, 23 October 2026. And 2 Asoj 2075, the day Nepal’s Privacy Act was certified, is 18 September 2018 in the Law Commission’s copy: your converter agrees.
Step 2: a formula from the averages
Now try to do without the table. The simplest formula uses the average length of each month. This program works out the averages from the table’s fixed years, 2000 to 2083, makes a formula from them, and tests it on every single day of those years, against the table:
from datetime import date, timedeltafrom bs_table import MONTHS, START years = range(2000, 2084) # the years already fixedaverage = [sum(MONTHS[y][m] for y in years) / len(years) for m in range(12)]year_length = sum(average)print('Average months:', [round(a, 2) for a in average])print('Average year:', round(year_length, 4), 'days') def formula(year, month, day): days = (year - 2000) * year_length + sum(average[:month - 1]) + day - 1 return date(*START) + timedelta(days=round(days)) right = wrong = worst = 0real = date(*START) # the real AD date, a day at a timefor y in years: for m in range(1, 13): for d in range(1, MONTHS[y][m - 1] + 1): off = (formula(y, m, d) - real).days if off == 0: right = right + 1 else: wrong = wrong + 1 worst = max(worst, abs(off)) real = real + timedelta(days=1)print(f'{right + wrong:,} days: {right:,} right, {wrong:,} wrong, at worst by {worst} day')Output
Average months: [30.93, 31.42, 31.42, 31.7, 31.02, 30.45, 29.89, 29.48, 29.62, 29.14, 29.82, 30.36]Average year: 365.25 days30,681 days: 24,443 right, 6,238 wrong, at worst by 1 daySubtracting two dates gives a timedelta, and its .days is the difference in days. The formula is never more than a day out, but it is a day out on about one date in five. That is fine for “about when?”, and useless for a birth date on a passport form. And this is a formula made from the table: without it you wouldn’t even have the averages.
Step 3: the day of the week, by a rule
The week is the one thing both calendars share: 1 Baishakh 2083 and 14 April 2026 are the same Tuesday. Once a BS date is turned into AD, its weekday follows from AD’s rule. In papers from 1882 to 1886, Christian Zeller published a way to work it out with whole-number arithmetic, known as Zeller’s congruence:
from datetime import date, timedelta DAYS = ['Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'] def zeller(year, month, day): if month < 3: # January and February count as months 13 and 14 month = month + 12 # of the year before year = year - 1 k = year % 100 # the year within its century j = year // 100 # the century h = (day + 13 * (month + 1) // 5 + k + k // 4 + j // 4 - 2 * j) % 7 return DAYS[h] print('14 April 2026 was a', zeller(2026, 4, 14)) # Check it on every day from 1943 to 2034 against Python's own datetimed = date(1943, 4, 14)checked = wrong = 0while d < date(2034, 4, 14): if zeller(d.year, d.month, d.day) != d.strftime('%A'): wrong = wrong + 1 checked = checked + 1 d = d + timedelta(days=1)print(f'{checked:,} days checked, {wrong} wrong')Output
14 April 2026 was a Tuesday33,238 days checked, 0 wrongWhy move January and February to the end of the year before? Because the leap day, 29 February, then comes last, and the months from March on always have the same lengths. 13 * (month + 1) // 5 counts the days the months before have pushed the weekday along (most months are 30 or 31 days, 2 or 3 more than four whole weeks). k + k // 4 does the same for the years of the century so far (a year is 52 weeks and a day, a leap year one day more), and j // 4 - 2 * j for the whole centuries before (a century moves the weekday on 5 days, which is the same as 2 back once you take the remainder by 7, and one more every 400 years). That is the whole leap-year rule in one line, and it works for any date since 1582. A rule like that is exactly what BS doesn’t have.
Step 4: can the sun predict the table?
The months follow the sun, and modern astronomy knows where the sun is to a fraction of a second. So try it: could you make the table yourself? The file sun.py on this page has the moment the sun entered each sign from 2070 to 2084 BS, in Nepal time, worked out with the astronomy library astropy. First, how long did the sun take over each month of 2083?
from datetime import datetimefrom sun import SUNfrom bs_table import NAMES, MONTHS def moment(text): return datetime.strptime(text, '%Y-%m-%d %H:%M') year = 2083starts = [moment(t) for t in SUN[year]] + [moment(SUN[year + 1][0])]for m in range(12): length = (starts[m + 1] - starts[m]).total_seconds() / 86400 # in days print(f'{NAMES[m]:9} the sun: {length:5.2f} days the table: {MONTHS[year][m]}')Output
Baishakh the sun: 30.87 days the table: 31Jestha the sun: 31.27 days the table: 31Asar the sun: 31.45 days the table: 32Shrawan the sun: 31.34 days the table: 31Bhadra the sun: 30.99 days the table: 31Asoj the sun: 30.50 days the table: 31Kartik the sun: 30.00 days the table: 30Mangsir the sun: 29.62 days the table: 29Poush the sun: 29.45 days the table: 30Magh the sun: 29.54 days the table: 29Falgun the sun: 29.87 days the table: 30Chaitra the sun: 30.35 days the table: 30datetime.strptime reads a date and time from text in the pattern given, and .total_seconds() / 86400 turns the time between two moments into days. The sun is quickest in Poush, Magh and Mangsir, under 29⅔ days, and slowest in Asar, nearly 31½, as How it works said. But look at Asar: 31.45 days by the sun, 32 in the table. And Poush: 29.45 by the sun, yet 30 days long. A month’s length in days depends on which days its two sankrantis fall on, not only on how long the sun takes.
So the real question is the hour. If the sun enters a sign in the early morning, that day is surely the new month’s first. If it enters late at night, the month starts the next day. Somewhere between is the cut-off. This program tries cut-offs every two hours and counts how many of the 168 months from 2070 to 2083 each one gets right:
from datetime import datetime, date, timedeltafrom sun import SUNfrom bs_table import START, MONTHS # The real first day of every month up to 2083, from the tablefirst = {}day = date(*START)for y in range(2000, 2084): for m in range(12): first[(y, m)] = day day = day + timedelta(days=MONTHS[y][m]) def rule(sun_time, hour): """The month starts on the sun's day if the sun gets there before `hour`, else the next day.""" if sun_time.hour < hour: return sun_time.date() return sun_time.date() + timedelta(days=1) for hour in range(0, 25, 2): right = 0 for y in range(2070, 2084): for m in range(12): sun_time = datetime.strptime(SUN[y][m], '%Y-%m-%d %H:%M') if rule(sun_time, hour) == first[(y, m)]: right = right + 1 print(f'before {hour:2}:00 {right} of 168 months right')Output
before 0:00 60 of 168 months rightbefore 2:00 77 of 168 months rightbefore 4:00 89 of 168 months rightbefore 6:00 102 of 168 months rightbefore 8:00 121 of 168 months rightbefore 10:00 132 of 168 months rightbefore 12:00 145 of 168 months rightbefore 14:00 134 of 168 months rightbefore 16:00 137 of 168 months rightbefore 18:00 138 of 168 months rightbefore 20:00 126 of 168 months rightbefore 22:00 122 of 168 months rightbefore 24:00 108 of 168 months rightThe dictionary first uses a pair, (y, m), as its key: a tuple can be a key because it can’t change. The best cut-off, noon, gets 145 months of 168 right, and no cut-off gets them all. Modern astronomy and a simple rule come close to the table but don’t make it. Perhaps the almanacs start the signs from a slightly different point, or work out the sun by older methods, or decide the day by another rule; whatever the reason, the table is what the approved almanacs say. The only way to know it is to wait for the almanacs, and copy their months into a table.
What your program shows
- The months follow the sun: from 29.45 days in Poush to 31.45 in Asar in 2083, quickest when the Earth is nearest the sun.
- No formula gets them all. One made from the table’s own averages is a day out on 6,238 of 30,681 days; one tuned to sixty-one years still gets 32 of the next 276 months wrong.
- Not even the sun does. Modern positions of the sun with the best fixed cut-off get 145 of 168 months.
- AD needs only a rule: Zeller’s arithmetic and the leap-year rule agreed with Python on all 33,238 days.
So every converter, in every phone and on every website, carries a table, and someone has to add next year’s row when the almanacs come out. A converter is only as right as its table.
Going further still
The new year creeps later
The table says on which AD date each BS year began. Count them:
A calendar of the sun with a rule
India’s national calendar, used officially from 22 March 1957, also has twelve months with the same names as BS’s, but it was designed to need no table: its months have fixed lengths (Chaitra 30 days, or 31 in a leap year; Baishakh to Bhadra 31; Asoj to Falgun 30), its leap years are the Gregorian ones, and it follows the seasons, not the stars. The committee that designed it, led by the physicist Meghnad Saha, chose exactly what BS doesn’t have: a rule.
An open question: should a calendar follow the sky as it really is, and need a table that people fix every year, or follow a rule that anyone can work out but that drifts a little from the sky? Every calendar in the world is one of these choices, and there is no answer that is right for everyone.
Challenges
Three challenges, each checked on the page: write your function in the program above the checker (press Edit), then press Check my program. The examples show what your function should give; there are more tests you can’t see.
★ Which day of the year?
Write day_of_year(lengths, month, day): lengths is one year’s list of 12 month lengths, Baishakh first. Give back which day of that year the date is: 1 Baishakh is day 1, 2 Baishakh day 2, 1 Jestha day 1 plus Baishakh’s length, and so on.
def day_of_year(lengths, month, day): # Give back the day's number in its year (1 Baishakh is 1). return 0 print(day_of_year([31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30], 7, 6))★★ From AD to BS
Write ad_to_bs(months, first, start, ad), the converter the other way. months is a list of years, each a list of 12 month lengths, the first for the BS year first; start is the AD date of 1 Baishakh of that year as [year, month, day]; ad is an AD date in the same form. Give back the BS date as [year, month, day]. You may assume the date is inside the table.
from datetime import date def ad_to_bs(months, first, start, ad): # Give back [year, month, day] in BS. return [first, 1, 1] print(ad_to_bs([[31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30], [31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30]], 2082, [2025, 4, 14], [2026, 10, 23]))★★★ The same wall calendar
An old AD calendar can be used again in a later year that starts on the same weekday and has the same leap year: 2015’s calendar works for 2026. Can a BS wall calendar be reused too? Write same_calendar(months, first, weekday, year): months and first as in ★★, and weekday the day of the week of 1 Baishakh in the year first (0 for Sunday up to 6 for Saturday). Give back the first year after year whose calendar is exactly the same: the same 12 month lengths, and 1 Baishakh on the same weekday. Give back None if no later year in the table has one.
def same_calendar(months, first, weekday, year): # Give back the next year with the same calendar as `year`, or None. return None print(same_calendar([[30, 31, 31, 31, 31, 30, 30, 30, 29, 30, 30, 31], [31, 31, 31, 31, 30, 30, 30, 30, 30, 29, 30, 31], [30, 31, 31, 31, 31, 30, 30, 30, 29, 30, 30, 31]], 2000, 3, 2000))Questions
Why does every BS converter carry a table of month lengths?
Pick an answer to see why.
A converter’s table goes to 2090 BS. What can you say about its months for 2088?
Pick an answer to see why.
Which year is a leap year in the Gregorian calendar?
Pick an answer to see why.
Explain why the BS months in winter (Poush, Magh) are shorter than those in summer (Asar, Shrawan).
Answer
In Zeller’s rule, January and February are counted as months 13 and 14 of the year before. Why does that help?
Answer
A friend’s app converts BS dates with a table that hasn’t been updated for two years. Which dates might it get wrong, and which will it still get right? Use what happened to nepali-datetime’s table for 2083.
Answer
India’s national calendar has fixed month lengths and Gregorian leap years. Give one advantage and one disadvantage of such a calendar compared with BS.
Answer
Where to go next
- Grade 9, Dictionaries: the dictionaries the table and the counting programs are built from.
- Grade 10, Dates, and a lucky draw: Python’s
datetime, which knows every AD month and leap year. - Grade 10, Libraries from everywhere: installing nepali-datetime on your own computer.
- How Can a Computer Never Lose at Tic-Tac-Toe?: another program that finds out something certain by trying every case.
