pes18fan > open posts/ad_to_bs.html

converting AD to Bikram Sambat

In Nepal, the Bikram Sambat calendar sees very common use. Therefore, conversions between AD and BS are done often, and a lot of the time people have no idea what is going on under the hood to get it done. That included me, until yesterday when I made the decision to create a little terminal app called ncal, which is a very basic, small calendar for BS. It looks like this:

ncal running in WezTerm

I plan on adding a few more features and probably work on a mobile app for a Nepali calendar to learn mobile development as well as to finally have a non-bloated Nepali calendar app for once.

Anyways, making this little program gave me some insight into how the conversion is done, and it’s not pretty, I promise you.

The process I utilized is one that is used to convert BS to AD, documented in this repository and all I did was reverse it for my purposes.

initial steps

The thing is, the Bikram Sambat is a lunisolar calendar with a really complicated set of properties that make determining the length of each month ridiculously difficult mathematically, unlike AD where each month has a fixed number of days (barring February in a leap year). I’m not sure if its impossible or not, but regardless I didn’t want to put in way too much work for a simple project like this. Perhaps I’ll try to figure out a way to do it sometime, but not for now.

So, the only option left is to use a lookup table, which includes the length of each month in a set of specific years. For ncal, this set is from the year 2000 to 2090.

A lookup table is also required to get the English date that corresponds to the first day of each of the Nepali years that we’ve selected in this set. Data for this was only available from 2000 to 2090, so even though the month length set was available upto 2100, I only took the set from 2000 to 2090. The month lengths being available do mean that I can calculate the first days myself, which I might do in the future.

getting the year

This part is the easiest. Just take the Gregorian year and add 56 or 57 to it.

nep_year = if gregorian < Globals.first_day[gregorian.year + 57]
             year + 56
           else
             year + 57
           end

Whether you add 56 or 57 is based on whether the Gregorian date is before or after the Nepali new year for its corresponding year. For instance, say we want to convert the date 2024-1-1 to BS. Adding 57 to its year yields 2081, and the English date for the first day of that year is 2024-4-13. Obviously, January 1st is before the 13th of April of 2024, thus our Nepali year is 2024 + 56 = 2080. And if we were to provide, say, 2024-5-5 as the date, we’d get the correct answer of 2081 as the year since May 5th is after April 13th.

getting the month and day

This part is a bit trickier. First, we get the English date for the first day of the Nepali year we just found out:

first_day_of_nep_year = Globals.first_day[nep_year]

Then, we find the number of days between the input date and this first date.

# converted to Int32 since the value may come out as either Int32 or Int64
gregorian_day_gap = (gregorian - first_day_of_nep_year).days.to_i32

Now, loop over the big hash table with the years as the keys and an array of the month lengths as the values. For each month, subtract the number of days of that month from your gregorian_day_gap. Do this till this number is less than the number of days in the current month.

Globals.month_length[nep_year].each_with_index do |days, i|
  if gregorian_day_gap < days
    nep_month = i + 1
    nep_day = gregorian_day_gap + 1
    break
  end

  gregorian_day_gap -= days
end

I’m adding 1 to the nep_month since the nep_month is a zero-based index. I’m doing the same for the nep_day since humans generally don’t call the first day of some month as the “0th”.

All that’s left is to package this data neatly in any form you like. My code here is inside of a private function of a class called NepaliDate and is called by two of the three constructors used to create a NepaliDate. It returns the data to them as a NamedTuple, which in Crystal is basically a tuple with names for each field.

conclusion

Well, that’s it! A convoluted process for sure and not one I would recommend trying to figure out at 12 AM, but in the end it works!