bs

package module
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2026 License: MIT Imports: 7 Imported by: 0

README

go-bs

test Go Reference Go Report Card

A small, dependency-free Go library for converting dates between Gregorian (AD) and Bikram Sambat (BS), the calendar used in Nepal.

✓ 122 BS years covered (1979-2100)
✓ 1,464 BS months represented
✓ Every supported BS date tested
✓ AD → BS → AD round-trip tested
✓ BS → AD → BS round-trip tested
✓ Zero runtime dependencies
✓ Zero network requests

Features

  • ADToBS / BSToAD conversion
  • Supports BS years 1979–2100 inclusive
  • Strict date validation against real BS month lengths (not just shape)
  • Date arithmetic and comparison (AddDays, DaysBetween, Before/After, ...)
  • Layout-based formatting and Nepali-digit conversion
  • Calendar-grid helpers for building calendar UIs (MonthCalendar, ...)
  • Drop-in JSON encoding and database/sql support (Date implements encoding.TextMarshaler/TextUnmarshaler and driver.Valuer/sql.Scanner)
  • Zero runtime dependencies
  • Timezone-safe: conversion is based on calendar date, not time-of-day
  • Table-driven calendar data, verified against a live source where possible (see docs/calendar-data.md)
  • Exhaustively tested: every one of the 44,562 supported days round-trips in both directions

Installation

go get github.com/suprimkhatri77/go-bs

Usage

Convert AD to BS:

package main

import (
	"fmt"
	"log"
	"time"

	bs "github.com/suprimkhatri77/go-bs"
)

func main() {
	ad := time.Date(2026, time.September, 22, 0, 0, 0, 0, time.UTC)

	d, err := bs.ADToBS(ad)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(d) // 2083-06-06
}

Convert BS to AD:

d, err := bs.NewDate(2083, 6, 6)
if err != nil {
	log.Fatal(err)
}

ad, err := bs.BSToAD(d)
if err != nil {
	log.Fatal(err)
}

fmt.Println(ad.Format("2006-01-02")) // 2026-09-22

Validate a BS date without converting it:

if !bs.IsValid(2080, 2, 32) {
	fmt.Println("not a real Bikram Sambat date")
}

Date arithmetic and comparison:

d, _ := bs.NewDate(2083, 6, 15)

next, _ := d.AddDays(10)
fmt.Println(next) // 2083-06-25

fmt.Println(d.Before(next)) // true

weekday, _ := d.DayOfWeek()
fmt.Println(weekday) // Thursday

name, _ := d.MonthName()
fmt.Println(name) // Ashwin

diff, _ := bs.DaysBetween(d, next)
fmt.Println(diff) // 10

start, _ := d.StartOfMonth()
end, _ := d.EndOfMonth()
fmt.Println(start, end) // 2083-06-01 2083-06-31

Parsing:

d, err := bs.Parse("2083-06-15")
if err != nil {
	log.Fatal(err)
}

Formatting and Nepali digits:

d, _ := bs.NewDate(2083, 6, 6)

s, _ := d.Format("dddd, MMMM D, YYYY")
fmt.Println(s) // Tuesday, Ashwin 6, 2083

fmt.Println(bs.ToNepaliDigits(d.String())) // २०८३-०६-०६

name, _ := d.MonthNameNepali()
fmt.Println(name) // असोज

Building a calendar UI:

weeks, _ := bs.MonthCalendar(2083, 6) // [][]*bs.Date, Sunday-first, nil-padded

for _, week := range weeks {
	for _, day := range week {
		if day == nil {
			fmt.Print("   ") // no day of this month in this cell
		} else {
			fmt.Printf("%2d ", day.Day)
		}
	}
	fmt.Println()
}

TodayBS, MustParse, NextMonth/PreviousMonth, and Age:

today, _ := bs.TodayBS()
fmt.Println(today) // 2083-06-06 (whatever "today" is when this runs)

birth := bs.MustParse("2060-06-15")

next, _ := birth.NextMonth()
fmt.Println(next) // 2060-07-15

years, months, days, _ := bs.Age(birth, today)
fmt.Println(years, months, days) // 22 11 22

API overview

const (
	MinBSYear = 1979
	MaxBSYear = 2100
)

type Date struct {
	Year, Month, Day int
}

func NewDate(year, month, day int) (Date, error)
func Parse(s string) (Date, error)    // "YYYY-MM-DD"
func MustParse(s string) Date         // panics instead of erroring
func TodayBS() (Date, error)

func (d Date) Valid() bool
func (d Date) String() string // "YYYY-MM-DD"
func (d Date) MonthName() (string, error)
func (d Date) MonthNameNepali() (string, error)
func (d Date) Format(layout string) (string, error) // e.g. "YYYY-MM-DD"

func (d Date) Before(other Date) bool
func (d Date) After(other Date) bool
func (d Date) Equal(other Date) bool
func Compare(a, b Date) int // -1, 0, 1

func (d Date) AddDays(n int) (Date, error)
func (d Date) SubDays(n int) (Date, error)
func (d Date) NextDay() (Date, error)
func (d Date) PreviousDay() (Date, error)
func (d Date) NextMonth() (Date, error)     // clamps to target month's last day
func (d Date) PreviousMonth() (Date, error) // clamps to target month's last day
func (d Date) DayOfWeek() (time.Weekday, error)
func DaysBetween(a, b Date) (int, error)
func Age(birthBS, todayBS Date) (years, months, days int, err error)

func (d Date) StartOfMonth() (Date, error)
func (d Date) EndOfMonth() (Date, error)
func (d Date) StartOfYear() (Date, error)
func (d Date) EndOfYear() (Date, error)
func (d Date) DayOfYear() (int, error)

func ADToBS(t time.Time) (Date, error)
func BSToAD(d Date) (time.Time, error)

func IsValid(year, month, day int) bool
func IsSupportedBSYear(year int) bool
func DaysInMonth(year, month int) (int, error)
func DaysInYear(year int) (int, error)
func MonthName(month int) (string, error)
func MonthNameNepali(month int) (string, error)
func ToNepaliDigits(s string) string
func FromNepaliDigits(s string) string

func FirstWeekdayOfMonth(year, month int) (time.Weekday, error)
func WeeksInMonth(year, month int) (int, error)
func MonthCalendar(year, month int) ([][]*Date, error) // Sunday-first, nil-padded

func (d Date) MarshalText() ([]byte, error)  // encoding.TextMarshaler; same as String
func (d *Date) UnmarshalText(data []byte) error // encoding.TextUnmarshaler; same as Parse
func (d Date) Value() (driver.Value, error)  // database/sql/driver.Valuer
func (d *Date) Scan(value any) error         // database/sql.Scanner

var (
	ErrInvalidYear      error
	ErrInvalidMonth     error
	ErrInvalidDay       error
	ErrInvalidFormat    error
	ErrOutOfRange       error
	ErrInvalidDateOrder error
)

All errors support errors.Is, e.g. errors.Is(err, bs.ErrInvalidDay).

Date.Month is 1-based: 1 is Baisakh, 12 is Chaitra.

MonthCalendar's weeks run Sunday through Saturday, matching how Nepali calendars are conventionally laid out (Hamro Patro included).

Supported range

  • Bikram Sambat: 1979–2100 (MinBSYearMaxBSYear), inclusive.
  • The corresponding Gregorian range is 1922-04-13 to 2044-04-13, derived from the verified calendar data rather than assumed.

Most BS calendar libraries stop around this range because that's roughly the edge of what Nepal's calendar authorities have officially published in advance. MaxBSYear is expected to move out further (realistically not before around BS 2095) once more official data exists.

Accuracy

BS month lengths are not computed from a formula — they follow the officially published Nepali calendar and are stored as a static, table-driven dataset. That dataset was cross-checked against multiple existing open-source implementations and, where possible, against a live calendar source, rather than copied from a single upstream project as-is. See docs/calendar-data.md for the sources, the verification method, and a documented known limitation (BS years 1979–1999 could not be checked against a live source and rely on two library sources agreeing with each other).

Timezone behavior

ADToBS looks only at the Year, Month and Day of the given time.Time — its time-of-day and location are ignored, and the same Gregorian calendar date always converts to the same BS date, regardless of which timezone the time.Time is expressed in.

Testing

go test ./...
go test -race ./...
go vet ./...

TestExhaustiveBSRoundTrip and TestExhaustiveADRoundTrip convert every one of the 44,562 supported days in both directions and confirm the round trip is exact, rather than relying on a sample.

Contributing

See CONTRIBUTING.md.

License

MIT — see LICENSE.

Documentation

Overview

Code generated by tools/calendar-generator. DO NOT EDIT.

Regenerate with: go run ./tools/calendar-generator -out data.go See docs/calendar-data.md for sources and verification method.

Package bs converts calendar dates between the Gregorian (AD) calendar and the Bikram Sambat (BS) calendar used in Nepal, for BS years MinBSYear through MaxBSYear inclusive.

BS month lengths are not derived from a formula: they follow Nepal's officially published calendar and are stored as a verified, table-driven dataset (see docs/calendar-data.md for sources). The package has no runtime dependencies.

Conversion operates on calendar dates (year, month, day), not timestamps. ADToBS considers only the Year, Month and Day components of the given time.Time and ignores time-of-day and location.

Index

Constants

View Source
const (
	MinBSYear = 1979
	MaxBSYear = 2100
)

MinBSYear and MaxBSYear are the inclusive bounds of the BS years supported by this package.

Variables

View Source
var (
	// ErrInvalidYear is returned when a BS year is outside MinBSYear..MaxBSYear.
	ErrInvalidYear = errors.New("bs: invalid year")

	// ErrInvalidMonth is returned when a BS month is not in the range 1..12.
	ErrInvalidMonth = errors.New("bs: invalid month")

	// ErrInvalidDay is returned when a BS day is not a valid day of the given
	// month (either out of the generic 1..32 bound, or greater than the
	// actual number of days in that month/year).
	ErrInvalidDay = errors.New("bs: invalid day")

	// ErrOutOfRange is returned when an AD date falls outside the Gregorian
	// range corresponding to MinBSYear..MaxBSYear.
	ErrOutOfRange = errors.New("bs: date outside supported range")

	// ErrInvalidFormat is returned when a string passed to Parse is not
	// shaped like "YYYY-MM-DD".
	ErrInvalidFormat = errors.New("bs: invalid date format")

	// ErrInvalidDateOrder is returned when a function expecting dates in a
	// particular order (e.g. Age's birth date before its reference date)
	// gets them the wrong way round.
	ErrInvalidDateOrder = errors.New("bs: invalid date order")
)

Sentinel errors returned by this package. Use errors.Is to check for them; wrapped errors include additional context via fmt.Errorf's %w verb.

Functions

func Age added in v0.5.0

func Age(birthBS, todayBS Date) (years, months, days int, err error)

Age computes the calendar age from birthBS to todayBS (see TodayBS), as years, months and days such that adding that many years, months and days to birthBS lands on todayBS. It returns an error wrapping ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if either date is invalid, or ErrInvalidDateOrder if birthBS is after todayBS.

func BSToAD

func BSToAD(d Date) (time.Time, error)

BSToAD converts a Bikram Sambat date to the corresponding Gregorian calendar date, returned as a time.Time at UTC midnight. It returns an error wrapping ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if d is not a real, supported Bikram Sambat date.

func Compare added in v0.2.0

func Compare(a, b Date) int

Compare compares two dates and returns -1 if a is before b, 0 if they're equal, and 1 if a is after b. It compares fields directly and does not require either date to be valid.

func DaysBetween added in v0.2.0

func DaysBetween(a, b Date) (int, error)

DaysBetween returns the number of calendar days from a to b: positive if b is after a, negative if b is before a, zero if they're equal. It returns an error wrapping ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if either date is invalid.

func DaysInMonth

func DaysInMonth(year, month int) (int, error)

DaysInMonth returns the number of days in the given Bikram Sambat month. It returns ErrInvalidYear or ErrInvalidMonth if year or month is out of range.

func DaysInYear

func DaysInYear(year int) (int, error)

DaysInYear returns the total number of days in the given Bikram Sambat year. It returns ErrInvalidYear if year is out of range.

func FirstWeekdayOfMonth added in v0.4.0

func FirstWeekdayOfMonth(year, month int) (time.Weekday, error)

FirstWeekdayOfMonth returns the day of the week that day 1 of the given Bikram Sambat month falls on. It returns an error wrapping ErrInvalidYear or ErrInvalidMonth if year or month is out of range.

func FromNepaliDigits added in v0.3.0

func FromNepaliDigits(s string) string

FromNepaliDigits replaces every Devanagari digit (०-९) in s with its ASCII equivalent (e.g. "२०८३" becomes "2083"). Other characters, including existing ASCII digits, are copied through unchanged.

func IsSupportedBSYear

func IsSupportedBSYear(year int) bool

IsSupportedBSYear reports whether year is within MinBSYear..MaxBSYear.

func IsValid

func IsValid(year, month, day int) bool

IsValid reports whether year, month and day form a real Bikram Sambat calendar date within the supported range (MinBSYear..MaxBSYear).

func MonthCalendar added in v0.4.0

func MonthCalendar(year, month int) ([][]*Date, error)

MonthCalendar returns a week-by-week grid of the given Bikram Sambat month, for building calendar UIs. Each returned week is exactly 7 cells (Sunday through Saturday); a nil cell means no day of this month falls in that slot (padding at the start of the first week and/or the end of the last week). Every date from day 1 to the month's last day appears exactly once, in order. It returns an error wrapping ErrInvalidYear or ErrInvalidMonth if year or month is out of range.

func MonthName

func MonthName(month int) (string, error)

MonthName returns the English name of the given 1-based Bikram Sambat month (1 is Baisakh, 12 is Chaitra).

func MonthNameNepali added in v0.3.0

func MonthNameNepali(month int) (string, error)

MonthNameNepali returns the Devanagari name of the given 1-based Bikram Sambat month (1 is Baisakh, 12 is Chaitra).

func ToNepaliDigits added in v0.3.0

func ToNepaliDigits(s string) string

ToNepaliDigits replaces every ASCII digit (0-9) in s with its Devanagari equivalent (e.g. "2083" becomes "२०८३"). Non-digit characters, including punctuation and existing Devanagari digits, are copied through unchanged.

func WeeksInMonth added in v0.4.0

func WeeksInMonth(year, month int) (int, error)

WeeksInMonth returns the number of calendar-grid rows needed to display the given Bikram Sambat month, with weeks running Sunday through Saturday and the month's own days aligned under their weekday (i.e. the same row count MonthCalendar returns). It returns an error wrapping ErrInvalidYear or ErrInvalidMonth if year or month is out of range.

Types

type Date

type Date struct {
	Year  int
	Month int
	Day   int
}

Date represents a Bikram Sambat calendar date. Month is 1-based, where 1 is Baisakh and 12 is Chaitra. A Date is not guaranteed to be valid unless it was constructed with NewDate or returned by this package.

func ADToBS

func ADToBS(t time.Time) (Date, error)

ADToBS converts a Gregorian calendar date to Bikram Sambat. Only the Year, Month and Day components of t are used; time-of-day and location are ignored. It returns an error wrapping ErrOutOfRange if t falls outside the Gregorian range corresponding to MinBSYear..MaxBSYear.

func MustParse added in v0.5.0

func MustParse(s string) Date

MustParse is like Parse but panics if s cannot be parsed instead of returning an error. It's meant for cases like package-level variable initialization with a literal, known-good date string — most callers should use Parse.

func NewDate

func NewDate(year, month, day int) (Date, error)

NewDate constructs a Date and validates it. It returns an error wrapping ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if the given components do not form a real Bikram Sambat calendar date in the supported range.

func Parse

func Parse(s string) (Date, error)

Parse parses a Bikram Sambat date in "YYYY-MM-DD" format (the same format Date.String produces). It returns an error wrapping ErrInvalidFormat if s is not shaped like a date, or ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if it's shaped correctly but not a real supported date.

func TodayBS added in v0.5.0

func TodayBS() (Date, error)

TodayBS returns the current date in Nepal (Nepal Standard Time, UTC+05:45) converted to Bikram Sambat.

It deliberately does not use the calling process's local timezone: on a server configured for UTC (a common default for cloud VMs and containers), "today" would otherwise be wrong for roughly 5h45m of every day — the window after midnight has passed in Nepal but before it's passed in UTC.

It returns an error wrapping ErrOutOfRange if today's date in Nepal falls outside the supported range (i.e. this code is running before MinBSYear or after MaxBSYear's corresponding Gregorian date).

func (Date) AddDays

func (d Date) AddDays(n int) (Date, error)

AddDays returns the date n calendar days after d (or before, if n is negative). It returns an error wrapping ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if d is not itself a valid date, or ErrOutOfRange if the result falls outside the supported range.

func (Date) After

func (d Date) After(other Date) bool

After reports whether d is chronologically after other. It compares fields directly and does not require either date to be valid.

func (Date) Before

func (d Date) Before(other Date) bool

Before reports whether d is chronologically before other. It compares fields directly and does not require either date to be valid.

func (Date) DayOfWeek

func (d Date) DayOfWeek() (time.Weekday, error)

DayOfWeek returns the day of the week d falls on, derived through its equivalent Gregorian date. It returns an error wrapping ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if d is not a valid date.

func (Date) DayOfYear added in v0.2.0

func (d Date) DayOfYear() (int, error)

DayOfYear returns d's 1-based ordinal day within its year (1 for Baisakh 1, up to 365 or 366 for the last day of Chaitra). It returns an error wrapping ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if d is not a valid date.

func (Date) EndOfMonth added in v0.2.0

func (d Date) EndOfMonth() (Date, error)

EndOfMonth returns the last day of d's month. It returns an error wrapping ErrInvalidYear or ErrInvalidMonth if d's year or month is invalid.

func (Date) EndOfYear added in v0.2.0

func (d Date) EndOfYear() (Date, error)

EndOfYear returns the last day of Chaitra of d's year. It returns an error wrapping ErrInvalidYear if d's year is invalid.

func (Date) Equal

func (d Date) Equal(other Date) bool

Equal reports whether d and other represent the same calendar date.

func (Date) Format added in v0.3.0

func (d Date) Format(layout string) (string, error)

Format renders d according to layout, replacing recognized tokens:

YYYY  4-digit year, e.g. "2083"
YY    2-digit year, e.g. "83"
MMMM  full month name, e.g. "Ashwin"
MM    2-digit month, zero-padded
M     month, no leading zero
DD    2-digit day, zero-padded
D     day, no leading zero
dddd  full weekday name, e.g. "Thursday"
ddd   3-letter weekday abbreviation, e.g. "Thu"

Any other character in layout (including punctuation and spaces) is copied through unchanged. It returns an error wrapping ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if d is not a valid date.

func (Date) MarshalText added in v0.6.0

func (d Date) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler, rendering d the same way as String ("YYYY-MM-DD"). Because encoding/json prefers TextMarshaler over a struct's default field-by-field encoding, this is what makes a Date field serialize as the string "2083-06-06" instead of {"Year":2083,"Month":6, "Day":6} — and it also makes Date usable as a map key, and with encoding/gob, encoding/csv and url.Values, all for free.

Like String, MarshalText does not validate d; an invalid or zero-value Date still marshals, matching time.Time's behavior.

func (Date) MonthName

func (d Date) MonthName() (string, error)

MonthName returns the English name of d's month (e.g. "Baisakh").

func (Date) MonthNameNepali added in v0.3.0

func (d Date) MonthNameNepali() (string, error)

MonthNameNepali returns the Devanagari name of d's month.

func (Date) NextDay added in v0.5.0

func (d Date) NextDay() (Date, error)

NextDay returns the day after d. It returns the same errors as AddDays.

func (Date) NextMonth added in v0.5.0

func (d Date) NextMonth() (Date, error)

NextMonth returns the date one Bikram Sambat month after d, in the same day-of-month, clamped to the target month's last day if it's shorter (e.g. day 31 in a 30-day month becomes day 30, rather than rolling over into the following month). It returns an error wrapping ErrInvalidYear, ErrInvalidMonth or ErrInvalidDay if d is not itself a valid date, or ErrInvalidYear if the result falls outside the supported range.

func (Date) PreviousDay added in v0.5.0

func (d Date) PreviousDay() (Date, error)

PreviousDay returns the day before d. It returns the same errors as AddDays.

func (Date) PreviousMonth added in v0.5.0

func (d Date) PreviousMonth() (Date, error)

PreviousMonth returns the date one Bikram Sambat month before d, in the same day-of-month, clamped to the target month's last day if it's shorter. It returns the same errors as NextMonth.

func (*Date) Scan added in v0.6.0

func (d *Date) Scan(value any) error

Scan implements database/sql.Scanner, so a Date can be read directly out of a database/sql row. It accepts:

  • nil, which resets d to the zero Date
  • string or []byte in "YYYY-MM-DD" form (as written by Value)
  • time.Time, whose Year/Month/Day are taken literally as BS components (not run through ADToBS) — this is what most drivers hand back for a native DATE/DATETIME column, and taking the components as-is is what makes that round-trip symmetric with Value's string encoding.

Any other source type, or a string/[]byte not shaped like a date, returns an error.

func (Date) StartOfMonth added in v0.2.0

func (d Date) StartOfMonth() (Date, error)

StartOfMonth returns the first day (day 1) of d's month. It returns an error wrapping ErrInvalidYear or ErrInvalidMonth if d's year or month is invalid.

func (Date) StartOfYear added in v0.2.0

func (d Date) StartOfYear() (Date, error)

StartOfYear returns Baisakh 1 of d's year. It returns an error wrapping ErrInvalidYear if d's year is invalid.

func (Date) String

func (d Date) String() string

String returns d formatted as "YYYY-MM-DD".

func (Date) SubDays added in v0.2.0

func (d Date) SubDays(n int) (Date, error)

SubDays returns the date n calendar days before d (or after, if n is negative). It returns the same errors as AddDays.

func (*Date) UnmarshalText added in v0.6.0

func (d *Date) UnmarshalText(data []byte) error

UnmarshalText implements encoding.TextUnmarshaler, parsing the same "YYYY-MM-DD" shape Parse accepts. It returns the same errors Parse does.

func (Date) Valid

func (d Date) Valid() bool

Valid reports whether d is a real Bikram Sambat calendar date in the supported range.

func (Date) Value added in v0.6.0

func (d Date) Value() (driver.Value, error)

Value implements database/sql/driver.Valuer, so a Date can be passed directly as a query argument to database/sql. It encodes the same way as String and MarshalText ("YYYY-MM-DD").

Value does not validate d and never returns an error; an invalid or zero-value Date still writes its (nonsensical) string form rather than failing silently as NULL. Use a *Date (nil for NULL) if you need nullable storage — Value is not called on a nil *Date.

Directories

Path Synopsis
tools
calendar-generator command
Command calendar-generator produces the ../../data.go calendar dataset for package bs (github.com/suprimkhatri77/go-bs).
Command calendar-generator produces the ../../data.go calendar dataset for package bs (github.com/suprimkhatri77/go-bs).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL