- Dharmagya
- panchang docs
- Calendar Conversion
Calendar Conversion
panchang · v5.4.0 · MIT
Gregorian ↔ Hindu date conversion, Kali Yuga year, region-aware Hindu New Year, and yearly Ekadashi / Sankranti / festival listings.
Gregorian ↔ Hindu
These calls move a date between the civil calendar and Hindu calendar coordinates. Both read the calendar at sunrise, so you pass a location and a timezone offset with every call.
import {
convertGregorianToHindu, convertHinduToGregorian,
getKaliYugaYear, getHinduNewYear, computeSamvat,
} from 'panchang-ts';
const DELHI = { latitude: 28.6139, longitude: 77.2090 };
const opts = { timezone: 330 };
// Gregorian → Hindu, read at sunrise
const h = convertGregorianToHindu(new Date('2026-04-15'), DELHI, opts);
h.masaName; h.paksha; h.pakshaTithi; h.vikramSamvat; // Vaishakha krishna 13 2083
// Hindu → Gregorian. Zero dates (kshaya) or two (adhika/vriddhi) both happen.
const dates = convertHinduToGregorian(
{ vikramSamvat: 2083, masaIndex: 0, paksha: 'shukla', pakshaTithi: 9 },
DELHI, opts,
);
dates.map(d => d.toISOString()); // ['2026-03-27T00:00:00.000Z']
getKaliYugaYear(new Date('2026-04-01')); // 5127
getHinduNewYear(2026, 'tamil-nadu', DELHI, opts); // Puthandu, 2026-04-14 (null if none)
const s = computeSamvat(new Date('2026-04-15'));
s.vikramSamvat; s.shakaSamvat; s.vikramSamvatsara; // 2083 1948 'Siddharthi'package main
import (
"fmt"
"time"
"github.com/ishankgupta95/panchang/source/go/v5/panchang"
"github.com/ishankgupta95/panchang/source/go/v5/types"
)
func main() {
s := panchang.New()
delhi := types.GeoLocation{Latitude: 28.6139, Longitude: 77.2090}
opts := types.ConvertOptions{Timezone: panchang.OffsetMinutes(330)}
// Gregorian -> Hindu, read at sunrise
h, err := s.ConvertGregorianToHindu(
time.Date(2026, 4, 15, 0, 0, 0, 0, time.UTC), delhi, opts)
if err != nil {
panic(err)
}
fmt.Println(h.MasaName, h.Paksha, h.PakshaTithi, h.VikramSamvat)
// Vaishakha krishna 13 2083
// Hindu -> Gregorian. Zero dates (kshaya) or two (adhika/vriddhi) both happen.
dates, _ := s.ConvertHinduToGregorian(types.HinduDateCoords{
VikramSamvat: 2083, MasaIndex: 0, Paksha: "shukla", PakshaTithi: 9,
}, delhi, opts)
for _, d := range dates {
fmt.Println(d.ISOString()) // 2026-03-27T00:00:00.000Z
}
ky, _ := s.GetKaliYugaYear(time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(ky) // 5127
// second result is ok: false means the region has no new year that year
ny, ok, _ := s.GetHinduNewYear(2026, types.RegionTamilNadu, delhi, opts)
fmt.Println(ny.ISOString(), ok) // 2026-04-14T00:00:00.000Z true
sv, _ := s.ComputeSamvat(time.Date(2026, 4, 15, 0, 0, 0, 0, time.UTC))
fmt.Println(sv.VikramSamvat, sv.ShakaSamvat, sv.VikramSamvatsara)
// 2083 1948 Siddharthi
}convertHinduToGregorian gives you an array, not one date. A Hindu date can land on no civil day at all (kshaya) or on two of them (adhika or vriddhi), so check the length before you read an element. In TypeScript, HinduCalendarCoords and ConvertOptions are exported if you want to type your own call sites.
That includes the Krishna paksha of an Adhika Chaitra in purnimanta mode, the default, which 5.3 never found. Chaitra Krishna Saptami of VS 2086 at Delhi returns 2029-04-05 and 2030-03-25, and convertGregorianToHindu reads the first of them back as Adhika Chaitra. 5.3 returned only the second, so the round trip failed. Pass adhikaOnly: true to keep just the adhika date.
The solar regions do not share one day
Mesha Sankranti is a single instant, but the regions anchored to it pick their calendar day from that instant in five different ways. In the same year they fall on two different dates, the transit's civil day or the day after, so pass the region you actually mean.
| region | rule | 2027 | 2028 |
|---|---|---|---|
tamil-nadu | The Sankranti observance day | Apr 14 | Apr 14 |
punjab | The civil day containing the transit | Apr 14 | Apr 13 |
kerala | First sunrise at or after the transit | Apr 15 | Apr 14 |
west-bengal | The day after the transit’s civil day | Apr 15 | Apr 14 |
odisha | Pana Sankranti: the transit’s civil day, or the next sunrise’s day once the transit is more than 0.315 of the way through the night | Apr 14 | Apr 13 |
import { getHinduNewYear, computeSankrantisForYear, formatInZone } from 'panchang-ts';
const DELHI = { latitude: 28.6139, longitude: 77.2090 };
const opts = { timezone: 330 };
const regions = ['tamil-nadu', 'punjab', 'kerala', 'west-bengal', 'odisha'] as const;
// a new year is a day value: read its date in the zone (see "Dates that name a day")
for (const year of [2027, 2028]) {
for (const r of regions) {
console.log(year, r, formatInZone(getHinduNewYear(year, r, DELHI, opts)!, 330).slice(0, 10));
}
}
// the transit itself is Mesha Sankranti, rashi 0
for (const year of [2027, 2028]) {
const mesha = computeSankrantisForYear(year, DELHI, opts).find(s => s.rashi === 0)!;
console.log(year, 'transit', mesha.moment.toISOString());
}
// 2027 transit 2027-04-14T10:03:48.954Z = Apr 14, 15:33 IST (afternoon)
// 2028 transit 2028-04-13T16:18:19.730Z = Apr 13, 21:48 IST (after sunset)s := panchang.New()
delhi := types.GeoLocation{Latitude: 28.6139, Longitude: 77.2090}
opts := types.ConvertOptions{Timezone: panchang.OffsetMinutes(330)}
regions := []types.FestivalRegion{
types.RegionTamilNadu,
types.RegionPunjab,
types.RegionKerala,
types.RegionWestBengal,
types.RegionOdisha,
}
// a new year is a day value: read its date in the zone (see "Dates that name a day")
for _, year := range []int{2027, 2028} {
for _, r := range regions {
d, _, _ := s.GetHinduNewYear(year, r, delhi, opts)
fmt.Println(year, r, panchang.FormatInZone(d.Time(), 330)[:10])
}
}
// the transit itself is Mesha Sankranti, rashi 0
for _, year := range []int{2027, 2028} {
events, _ := s.ComputeSankrantisForYear(year, delhi,
types.YearlyListingOptions{Timezone: panchang.OffsetMinutes(330)})
for _, e := range events {
if e.Rashi == 0 {
fmt.Println(year, "transit", e.Moment.ISOString())
}
}
}
// 2027 transit 2027-04-14T10:03:48.954Z = Apr 14, 15:33 IST (afternoon)
// 2028 transit 2028-04-13T16:18:19.730Z = Apr 13, 21:48 IST (after sunset)The Sankranti observance day is the reference almanac's rule. A transit during daylight keeps its own civil day. A transit between sunset and the next sunrise is observed on the next day. The same per-region rules produce the puthandu, vishu, baisakhi and pohela_boishakh festival entries, so for those four regions the festival list and getHinduNewYear always agree, read in the location's zone. Both are checked against the reference almanac's per-region date pages for 2025 to 2029.
Yearly listings
These four calls list a whole year, or any span you give them, in one go. In Go each has a twin ending in Context — ComputeFestivalsForYearContext and its siblings — that takes a context.Context as its first argument and stops early once that context is cancelled or its deadline passes. The plain form runs to completion.
import {
computeEkadashiDatesForYear, computeSankrantisForYear,
computeFestivalsForYear, computeFestivalsInRange,
} from 'panchang-ts';
const DELHI = { latitude: 28.6139, longitude: 77.2090 };
const opts = { timezone: 330 };
const ek = computeEkadashiDatesForYear(2026, DELHI, opts);
ek.length; ek[0].toISOString(); // 24 '2026-01-14T00:00:00.000Z'
const sk = computeSankrantisForYear(2026, DELHI, opts);
sk.length; sk[0].rashiName; sk[0].moment.toISOString();
// 12 'Makara' '2026-01-14T09:44:20.228Z'
const fy = computeFestivalsForYear(2026, DELHI, opts);
fy.length; fy[0].festival.key; fy[0].date.toISOString();
// 253 'pradosha' '2025-12-31T18:30:00.000Z'
// festival dates are LOCAL midnight, so the
// UTC ISO of an IST date reads a day earlier
const rng = computeFestivalsInRange(
new Date('2026-10-01'), new Date('2026-11-30'), DELHI, opts);
rng.length; rng[0].festival.key; // 52 'vaishnava_ekadashi's := panchang.New()
delhi := types.GeoLocation{Latitude: 28.6139, Longitude: 77.2090}
opts := types.YearlyListingOptions{Timezone: panchang.OffsetMinutes(330)}
// errors dropped for brevity; every call returns one
ek, _ := s.ComputeEkadashiDatesForYear(2026, delhi, opts)
fmt.Println(len(ek), ek[0].ISOString()) // 24 2026-01-14T00:00:00.000Z
sk, _ := s.ComputeSankrantisForYear(2026, delhi, opts)
fmt.Println(len(sk), sk[0].RashiName, sk[0].Moment.ISOString())
// 12 Makara 2026-01-14T09:44:20.228Z
fy, _ := s.ComputeFestivalsForYear(2026, delhi, opts)
fmt.Println(len(fy), fy[0].Festival.Key, fy[0].Date.ISOString())
// 253 pradosha 2025-12-31T18:30:00.000Z
// festival dates are LOCAL midnight, so the UTC ISO of an IST date
// reads a day earlier
start := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2026, 11, 30, 0, 0, 0, 0, time.UTC)
rng, _ := s.ComputeFestivalsInRange(start, end, delhi, opts)
fmt.Println(len(rng), rng[0].Festival.Key) // 52 vaishnava_ekadashiEach one narrows the work to what it needs, so it beats looping day by day. computeEkadashiDatesForYear reads only the tithi at sunrise and takes about 16 ms for a year. computeSankrantisForYear checks one solar longitude per day and then bisects the 12 transits, about 2.8 ms for a year. See Performance for the rest of the numbers.
Yearly eclipse listings (computeEclipsesForYear, getUpcomingEclipses) live on Eclipses & Moon Phases.
Which days a year covers
A ...ForYear listing covers the local calendar year in timezone, 1 January to 31 December, each civil date once. The festival listings step one civil day at a time in the zone, so in a zone with daylight saving such as 'America/New_York' the spring-forward and fall-back days are listed like any other.
That is new in 5.4. Before, the listings stepped a fixed 24 hours from the offset in force on 1 July, so they skipped the spring-forward day, listed the fall-back day twice, began on the previous 31 December, dropped the requested one, and stamped winter days at 23:00 instead of midnight, which a UTC reading took for the next date. New York's Diwali 2026, on 8 November, came back as 2026-11-09T04:00:00.000Z, 23:00 EST on the 8th; it is now 2026-11-08T05:00:00.000Z, local midnight. This part of 5.4 does not touch numeric offsets or zones without daylight saving, IST included.
computeEkadashiDatesForYear covers the local year exactly too. A fast on local 31 December is listed in its own year: Pune's fast of 31 December 1911 used to head the 1912 list as 1912-01-01, and now closes the 1911 list.
Years 1900 and 2100 work at every offset. In 5.3 a local year that spilled past either end of the supported span threw, so computeFestivalsForYear(1900, …) failed east of UTC and 2100 failed west of it. Outside 1900 to 2100, computeFestivalsForYear and the Chaitra regions of getHinduNewYear throw INVALID_DATE, and since 5.4 that includes years 0 to 99, which 5.3 silently read as 1900 to 1999. convertHinduToGregorian, which takes a Vikram Samvat year, throws INVALID_DATE when the days it would search fall outside 1900 to 2100. computeEkadashiDatesForYear, computeSankrantisForYear and the solar regions of getHinduNewYear do not check the year at all, so 99 means the year 99, not 1999 (getHinduNewYear(99, 'tamil-nadu', …) finds no transit and returns null).
Dates that name a day
Several calls return a Date that stands for a civil day, not a moment. It comes in one of three forms. Each gives the right day when you format it in the location's timezone, so always read them there, never with toISOString() or getUTCDate().
| Form | Returned by | Example |
|---|---|---|
| The UTC midnight that falls within the local day: the date’s own UTC midnight at or east of UTC, the next one west of it | convertHinduToGregorian, getHinduNewYear, computeEkadashiDatesForYear, SankrantiEvent.date | New York, Mesha Sankranti 2025: 2025-04-14T00:00:00.000Z, which is 20:00 EDT on 13 April |
| The local midnight of the day | computeFestivalsForYear (FestivalDay.date) | Delhi, Akshaya Tritiya 2020: 2020-04-25T18:30:00.000Z, which is 00:00 IST on 26 April |
| The range start’s local time of day, on each day | computeFestivalsInRange (FestivalDay.date) | Delhi, from new Date('2026-10-01'): 05:30 IST on every day listed. Start at a local midnight to get local midnights. |
5.4 moved SankrantiEvent.date and the solar regions of getHinduNewYear onto the UTC-midnight form, joining the rest. In 5.3 they were the date's own UTC midnight at every offset, which west of UTC reads one day early in the zone: New York's Puthandu 2025 came back as 2025-04-13T00:00:00.000Z, 12 April in New York, a day before the transit itself. At UTC and east of it, IST included, every value is what it was, and SankrantiEvent.moment, the transit instant, did not change anywhere. See Upgrading 5.3 → 5.4.
import { computeSankrantisForYear, getHinduNewYear, computeFestivalsForYear } from 'panchang-ts';
const NY = { latitude: 40.7128, longitude: -74.0060 };
const opts = { timezone: 'America/New_York' };
const day = (d: Date) => // YYYY-MM-DD in the zone
new Intl.DateTimeFormat('en-CA', { timeZone: opts.timezone }).format(d);
const mesha = computeSankrantisForYear(2025, NY, opts).find(s => s.rashi === 0)!;
mesha.moment.toISOString(); // '2025-04-13T22:01:18.441Z' the transit, 18:01 EDT
mesha.date.toISOString(); // '2025-04-14T00:00:00.000Z' first form
day(mesha.date); // '2025-04-13'
day(getHinduNewYear(2025, 'tamil-nadu', NY, opts)!); // '2025-04-13'
const fy = computeFestivalsForYear(2025, NY, { ...opts, region: 'tamil-nadu' });
const puthandu = fy.find(f => f.festival.key === 'puthandu')!;
puthandu.date.toISOString(); // '2025-04-13T04:00:00.000Z' local midnight
day(puthandu.date); // '2025-04-13's := panchang.New()
ny := types.GeoLocation{Latitude: 40.7128, Longitude: -74.0060}
tz := panchang.Zone("America/New_York")
zone, _ := time.LoadLocation("America/New_York")
day := func(d types.JSDate) string { return d.Time().In(zone).Format("2006-01-02") }
events, _ := s.ComputeSankrantisForYear(2025, ny, types.YearlyListingOptions{Timezone: tz})
for _, e := range events {
if e.Rashi == 0 {
fmt.Println(e.Moment.ISOString(), e.Date.ISOString(), day(e.Date))
}
}
// 2025-04-13T22:01:18.441Z 2025-04-14T00:00:00.000Z 2025-04-13
ny25, _, _ := s.GetHinduNewYear(2025, types.RegionTamilNadu, ny, types.ConvertOptions{Timezone: tz})
fmt.Println(day(ny25)) // 2025-04-13
fy, _ := s.ComputeFestivalsForYear(2025, ny,
types.YearlyListingOptions{Timezone: tz, Region: types.RegionTamilNadu})
for _, f := range fy {
if f.Festival.Key == "puthandu" {
fmt.Println(f.Date.ISOString(), day(f.Date))
}
}
// 2025-04-13T04:00:00.000Z 2025-04-13Going the other way, convertGregorianToHindu reads the civil day its argument falls on in the zone. West of UTC, pass an instant inside the day you mean, such as a value from the calls above. In New York, new Date('2025-04-13') is the evening of 12 April, and converts as Saturday, Chaitra Purnima.
Samvat
The daily result carries the era years, and there are standalone helpers for the same values.
| Field | Meaning |
|---|---|
samvat.vikramSamvat | Vikram Samvat year (e.g. 2081 during early 2025) |
samvat.shakaSamvat | Shaka Samvat year (e.g. 1946) |
getKaliYugaYear(date) | Kali Yuga year (e.g. 5127) |
computeSamvat is the helper behind calendar.samvat on the daily result, which reads it at the day's sunrise. On its own it turns over at the instant of the Chaitra new moon, so on the day of that new moon the two can differ: at Delhi on 1 April 2022, computeSamvat at 12:00 IST gives 2079 while the daily result gives 2078. Pass the day's sun.rise to get the daily result's year.
