Skip to content
Documentation · all sections

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'

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.

regionrule20272028
tamil-naduThe Sankranti observance dayApr 14Apr 14
punjabThe civil day containing the transitApr 14Apr 13
keralaFirst sunrise at or after the transitApr 15Apr 14
west-bengalThe day after the transit’s civil dayApr 15Apr 14
odishaPana 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 nightApr 14Apr 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)

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'

Each 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().

FormReturned byExample
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 itconvertHinduToGregorian, getHinduNewYear, computeEkadashiDatesForYear, SankrantiEvent.dateNew York, Mesha Sankranti 2025: 2025-04-14T00:00:00.000Z, which is 20:00 EDT on 13 April
The local midnight of the daycomputeFestivalsForYear (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 daycomputeFestivalsInRange (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'

Going 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.

FieldMeaning
samvat.vikramSamvatVikram Samvat year (e.g. 2081 during early 2025)
samvat.shakaSamvatShaka 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.