npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

panchang-ts

v5.4.0

Published

Pure TypeScript Hindu Panchang calculations. Tithi, Nakshatra, Yoga, Karana, Vara, and more. Offline-first, React Native compatible.

Readme

panchang-ts

npm version license

Pure TypeScript Hindu Panchang (almanac), Jyotish, and Birth Chart calculations. Zero runtime dependencies. Works offline in React Native (Hermes), Node.js, and browsers.

Fast (~0.16 ms trimmed, ~0.30 ms full) · Typed (full TypeScript) · Offline (pure JS math) · 9,207 tests across 146 files

📖 Full documentation: dharmagya.app/docs/panchang-ts This README covers install, quick start, and the 5.3 → 5.4, 5.1 → 5.2, 5.0 → 5.1 and 4.x → 5 migrations in full, plus a per-feature quick reference. The complete reference (every option, result field, table format, accuracy bound and performance note) lives on the docs site.

A Go port of the same engine ships from the same repository, held to this package's exact arithmetic by a parity harness. See the repository if you need it.


Install

npm install panchang-ts
# or: pnpm add panchang-ts / yarn add panchang-ts

Quick Start

import { getDailyPanchang } from 'panchang-ts';

const result = getDailyPanchang(
  new Date(2025, 0, 14),                      // January 14, 2025
  { latitude: 23.1765, longitude: 75.7885 },  // Ujjain, India
  { timezone: 330 },                          // IST = UTC+5:30 = 330 minutes
);
// → DailyPanchangResult | null. Null only when no sunrise-to-sunrise day starts on that date
//   (polar latitudes, or an offset far from local solar time). Narrow with `if (!result) return;`.

console.log(result!.angas.tithis[0].name);          // "Krishna Chaturdashi"
console.log(result!.angas.nakshatras[0].name);      // "Mrigashira"
console.log(result!.angas.vara.name);               // "Mangalawara"
console.log(result!.calendar.chandramasa.name);        // "Magha"
console.log(result!.calendar.samvat.vikramSamvat);     // 2081

Reading Output Times

Every Date in a result is a real instant: .getTime() is the correct epoch millisecond. Every instant has a *Local companion: an offset-carrying ISO 8601 string, which is what you want for display.

result!.sun.rise;            // Date: 2025-01-14T01:39:44.172Z (the actual moment)
result!.sun.riseLocal;       // "2025-01-14T07:09:44.172+05:30"
result!.inauspicious.rahuKalam.start;    // Date
result!.inauspicious.rahuKalam.startLocal;

// Just the wall clock:
result!.sun.riseLocal.slice(11, 16);   // "07:09"

// Anything else works too, because the Date is genuinely correct:
new Intl.DateTimeFormat('en-IN', { timeZone: 'Asia/Kolkata', timeStyle: 'short' })
  .format(result!.sun.rise);           // "7:09 am"
Temporal.Instant.from(result!.sun.riseLocal);

For an instant you derive yourself, formatInZone renders it the same way:

import { formatInZone } from 'panchang-ts';
const noon = new Date((result!.sun.rise.getTime() + result!.sun.set.getTime()) / 2);
formatInZone(noon, result!.timezone.offsetMinutes);  // "2025-01-14T12:27:31.086+05:30"

Changed in v5. This is the breaking change most likely to affect you. Through 4.x every published Date was the true instant shifted by the UTC offset, and the README told you to read it back with getUTC*. That worked only as long as you did nothing else with the value: JSON.stringify emitted a wrong instant labelled Z, Intl with a timeZone rendered 12:39 pm for an 07:09 am sunrise, and any comparison, diff, database write, date-fns or Temporal call was off by the offset.

Migration is mechanical: x.getUTCHours() → read xLocal, or format the instant. See Upgrading from 4.x.

moon.rise / moon.set can be null: the Moon occasionally does not rise or set on a given calendar day, which is normal. moon.riseLocal / moon.setLocal are null exactly when they are.

The result is grouped

DailyPanchangResult has seven groups plus a handful of top-level fields. Through 4.x it was ~50 flat fields; the groups are what tell you where to look.

| Group | Holds | |---|---| | sun | rise / set / nextRise (+ *Local), day and night lengths, the Sun's siderealLongitude and nakshatra | | moon | rise / set (+ *Local), the Moon's siderealLongitude and rashi | | angas | the five limbs: tithis, nakshatras, yogas, karanas, vara | | calendar | masa (solar), chandramasa (lunar), samvat | | muhurtas | abhijit, brahma, vijaya, godhuli, nishita, amritKala, madhyahna, pratahSandhya, sayahnaSandhya, doGhati | | inauspicious | rahuKalam, gulikaKalam, yamaganda, durMuhurta, varjyam, bhadra, gandaMula, panchaka, panchakaInfo, panchakaRahita | | periods | choghadiya, hora, gowri |

Top level: date, location, timezone, ayanamsa, specialYogas, anandadiYoga, festivals, eclipse, chandraBalam, tarabala.

Nothing is optional. Every field is always present. A value that does not apply is null; a collection that does not apply is []. That holds whether the reason is the domain (no Bhadra window today) or your options (you did not pass janmaRashi, so chandraBalam is null). The result shape never depends on what you passed.

getInstantPanchang uses the same group names for the subset an instant can answer: sun, moon, angas, calendar, inauspicious. There is no muhurtas or periods, because those are properties of a Hindu day.

getDailyPanchang vs getInstantPanchang

| Use case | Use | |---|---| | Daily calendar, festivals, muhurtas, time-slots (Choghadiya/Hora/Gowri), eclipses with sutak | getDailyPanchang | | Single-moment snapshot ("what's active right now?") or birth-chart casting | getInstantPanchang |

getInstantPanchang emits a festivals field but only checks rule predicates at the given instant: it skips canonical-time refinements (madhyahna / pradosha / nishita / chandrodaya), transit-based Sankranti, and Smarta/Vaishnava Ekadashi split. For reliable festival dating, use getDailyPanchang.

If you have no location

location is required and stays required. Nothing here silently guesses where you are, because a panchang computed for the wrong place does not look wrong: it returns a complete, plausible result with some dates off by one.

There are three reference frames, and a result is always in one of them:

| frame | point | when | |---|---|---| | practical | the location you passed | you have real coordinates. The only frame correct for a real user | | traditional | Ujjain 23.1765N, 75.7885E | the classical madhya rekha of the Surya Siddhanta. The default | | modern | Central Station 23.1833N, 82.5E | the 1955 Calendar Reform Committee reference for the Rashtriya Panchang |

Why the frame matters at all: a tithi, nakshatra, yoga or karana ends at one instant worldwide. Only the civil day it gets labelled with depends on your sunrise, along with everything else rise/set derived (rahu kalam, choghadiya, pradosha, nishita, moonrise anchors).

import { getDailyPanchang, resolveLocation, IST_OFFSET_MINUTES } from 'panchang-ts';

// maybeLocation may be undefined; traditional (Ujjain) is the default frame
const { location, reference } = resolveLocation(maybeLocation);
const day = getDailyPanchang(date, location, { timezone: IST_OFFSET_MINUTES });

if (reference !== 'practical') {
  // say so, the way a printed panchang names the city it was computed for
  console.info(`Computed for the ${reference} reference, not your location.`);
}

Pass 'modern' as the second argument to fall back to the Central Station instead, or use referenceLocation('traditional' | 'modern') if you only want the point. 'practical' is reported, never selected: passing a location is what makes a result practical.

Price the fallbacks before you rely on them. Measured over 730 days against 48 Indian cities, the tithi at the reference's sunrise differs from the city's own on up to 5.9% of days for Ujjain and 4.0% for the Central Station. Ujjain is the better of the two for most users (population-weighted 1.18% against 1.80%, since India's metros lie west of the IST meridian) and the worse in the tail. Outside India both degrade without limit.

IST_OFFSET_MINUTES (330) is exact for the Central Station, whose longitude is the IST meridian. For Ujjain it is the civil clock rather than its local mean time, which is 303.2 minutes; the 26.8-minute gap is what the classical deshantara correction exists to close.

A half-filled location throws rather than being completed from a reference point, and { latitude: 0, longitude: 0 } is treated as the real place it is, not as missing.

Note that the shipped static tables (panchang-ts/festivals, /eclipses, /muhurta) are pre-computed for Varanasi, which is a fourth point again.


Upgrading to 5.4

Correctness fixes from a full TypeScript and Go audit, festival rules brought in line with the reference almanac and its competitors, a corrected Yoni koota table, one new export and two new festival keys. Nothing is removed or renamed and no signature changes, but many published values move, so read the first sections before upgrading if you cache or diff output. The Go module (source/go/v5) carries the same changes byte for byte.

Festival days now follow the reference almanac

Every rule below was checked against published dates: the reference almanac first, then AstroSage and other almanacs, with the classical rule (Nirnaya Sindhu, Dharma Sindhu) as the tie-breaker. The dates relied on are pinned in testdata/almanac/almanac-festival-selection-2026-09.json and almanac-festival-rules-2026-09-nakshatra-solar.json. Over 5 cities and 1900 to 2100 every once-a-year festival now fires exactly once per year and every monthly one at most once per paksha or month; before, most of them were listed on two consecutive days in some years. Two exceptions remain while their rules are deferred (below): Phagli still takes every day whose sunrise holds Purnima (two days in 2033, none in 2018 at Delhi), and a paksha whose Trayodashi touches no pradosha window has no Pradosh.

  • A tithi current at two sunrises (vriddhi): the first day, as the reference publishes (Anant Chaturdashi 2018-09-23, Ghatasthapana 2015-10-13), except the Tritiya vratas (Hariyali, Kajari and Hartalika Teej, Gangaur) and Jagannath Rath Yatra, which take the second (Hartalika Teej 2006-08-27). Ugadi, Gudi Padwa, Hanuman Jayanti, Navaratri, Durga Ashtami, Guru, Sharad, Kartika and Vat Savitri Purnima, Mahalaya Amavasya and others lose their second-day duplicate.
  • Festivals chosen by a kala use the reference's windows (madhyahna the third fifth of daytime, aparahna the fourth, pradosha sunset to sunset + night/5, nishita the 8th night muhurta). The day whose window the tithi covers most decides Ganesh and Vinayaka Chaturthi (madhyahna), Dhanteras, Diwali and Parashurama Jayanti (pradosha) and Govardhan Puja (daytime), the earlier day winning a tie or when both days' windows are full. Masik and Maha Shivaratri follow the reference's nishita ladder, and Dussehra its aparahna ladder with the Shravana rule. Rama Navami (madhyahna), Bhai Dooj and Vat Savitri Amavasya (aparahna) take the last day whose window the tithi touches, Narak Chaturdashi the first day whose arunodaya it touches, and Vasant Panchami the first day on which Panchami has begun by two fifths of the daytime. Vinayaka Chaturthi in December 2025 at Delhi drops its 12-23 duplicate and keeps 12-24.
  • Raksha Bandhan, Yajur Upakarma, Nag Panchami and Akshaya Tritiya take the first sunrise day on which the tithi lasts at least 3 muhurtas, else the day it begins. Akshaya Tritiya 2020 at Delhi drops its 04-25 duplicate and keeps 04-26.
  • Pradosh vrat is the day whose pradosha window (sunset to sunset + night/5) overlaps Trayodashi most; it reproduces every reference date for Delhi 2025 and 2027 (Ravi Pradosh 2025-02-09 was missing).
  • Holika Dahan is a new festival key, holika_dahan, on the evening the reference's Bhadra ladder chooses; holi is now the next day (Rangwali Holi), as the reference publishes it. That holds for getDailyPanchang, the listings and the tables. getInstantPanchang reports by the tithi at the instant, so an instant in Phalguna Purnima lists holika_dahan and holi together, and it never lists karthigai_deepam (it reports masik_karthigai on that day).
  • Chhath is anchored on the Shashthi sunrise day with Nahay Khay, Kharna and Usha Arghya at -2, -1 and +1 days; Maha Navami takes the first day whose sunset less two muhurtas follows the start of Navami (2015-10-21, 2027-10-08); Saddula Bathukamma moves to Durgashtami; Varamahalakshmi is the Friday in the 7 days ending on the Shravana Purnima sunrise day.
  • Masik Karthigai is one day per Krittika transit (the first day Krittika holds at sunset, else at sunrise); it fired on two consecutive days in about three months of four. Karthigai Deepam is a new festival key, karthigai_deepam, on the Karthigai-month Krittika day nearest the full moon, for regions all and tamil-nadu. On that day those two regions list it in place of masik_karthigai; every other region still lists masik_karthigai, so a table built for all and filtered by region afterwards loses that month's Masik Karthigai outside Tamil Nadu.
  • Onam is exactly one day a year: the Thiruvonam of Chingam, the first of two Chingam sunrises, the day holding it when it holds no sunrise, and the later of two Thiruvonams in one Chingam (at Kochi, 2024-09-15 and 2013-09-16).
  • Shravan Somvar, Mangala Gauri, Kartik Somvar and Magha Shanivar follow masaSystem: the default purnimanta gives the North Indian Mondays (Sawan Somwar 2025: 07-14, 07-21, 07-28, 08-04), amanta the southern ones as before, region nepal the solar month. This changes the default output: a caller that never sets masaSystem now gets the North Indian dates.
  • Deferred for lack of a reference capture: a Pradosh fallback when Trayodashi touches no pradosha window, Phagli, and the Guru Purnima short-udaya refinement.

Yoni koota and Pathu Porutham Yoni

Yoni koota (computeAshtakoot) now uses the published five-level table (4 same animal, 3 friendly, 2 neutral, 1 unfriendly, 0 enemy); the old one never awarded 3 and matched no published chakra. The table is the chakra of Mahidhar Sharma's tika on Muhurta Chintamani, as carried by Frawley and the Jagannatha Hora port PyJHora; the sources are in testdata/charts/yoni-koota-references.json. 550 of the 1,296 valid pairs of natal Moons change total (372 by -1, 166 by +1, 12 by +2). Pathu Porutham Yoni (computePathuPorutham) is now the Tamil pass/fail test, failing only on the seven enemy pairs, and no longer reads the Ashtakoot score; its description reads, for example, "horse ↔ snake, not enemies".

Day-valued results west of UTC, and the Odisha new year

computeSankrantisForYear (SankrantiEvent.date) and the solar branch of getHinduNewYear (Tamil Nadu, Kerala, Punjab, West Bengal, Assam) now return the UTC midnight that falls within the local day, like every other day-valued result, so formatting them in the location's zone gives the right day (Puthandu 2025 in New York read as April 12). At UTC and east of it, IST included, every value is byte-identical. getHinduNewYear(year, 'odisha') returns Pana Sankranti, the Mesha Sankranti day, instead of Chaitra Shukla Pratipada: the transit's civil date, or, when the transit falls later than 0.315 of the way through the night after sunset, the date of the sunrise that ends that night. That cutoff is fitted to the reference almanac's Bhubaneswar dates (2024-04-13 for a 21:15 transit, 2028-04-14 for 21:47); no textbook Odia rule reproduces them. The cutoff is a fraction of the local night, so the date depends on the location (2028 is 04-13 at Delhi and 04-14 at Bhubaneswar), and it can fall a day before SankrantiEvent.date, which moves a transit after sunset to the next day (Bhubaneswar 2024: 04-13 and 04-14). The festival listings are not part of this change: computeFestivalsForYear returns the local midnight of each day and computeFestivalsInRange the range start's local time of day, and both read correctly in the location's zone.

Values that move

  • Brahma Muhurta (muhurtas.brahma, computeBrahmaMuhurta) is now the 14th of the 15 night muhurtas, from two night muhurtas to one before sunrise (about 96 to 48 minutes at an equinox). It used daylight/30, a 24 minute window ending 24 minutes before sunrise. The daily panchang measures the night from sunset to the next sunrise, so the window's midpoint is exactly Pratah Sandhya's start, which the reference almanac captures in testdata confirm; they hold no Brahma Muhurta value of their own.
  • Year and range listings in zones with daylight saving (computeFestivalsForYear, computeFestivalsInRange, computeAuspiciousDatesForYear, computeAuspiciousDatesInRange, computeMoonPhasesForYear, computeEclipsesForYear) now step civil days in the zone. They used a fixed 24 hour step from an offset taken on 1 July, so in America and Europe they skipped the spring-forward day, listed the fall-back day twice, included the previous 31 December and dropped the requested one, and stamped winter dates at 23:00 of the previous day (Diwali 2026 in New York came back as 2026-11-09T04:00Z, 23:00 on 8 November in the zone and 9 November at the July offset; it is now 2026-11-08T05:00Z, local midnight). Numeric offsets and zones without daylight saving, IST included, are byte-identical.
  • Festivals that went missing: Sankashti Chaturthi when Chaturthi touches no moonrise (about 7% of Krishna pakshas, for example Delhi 2025-10-10), Ugadi, Gudi Padwa and Navaratri when a kshaya Pratipada follows an adhika month, the Smarta Ekadashi of a vriddha Ekadashi followed by a kshaya Dwadashi, and Onam in adhika Bhadrapada years (2012, 2096). Across 9 cities over 1900 to 2100 these four fixes add 1,677 entries and remove or move none (the rule changes above restore further dates of their own, and do move and remove entries).
  • getHinduNewYear on a kshaya Chaitra Shukla Pratipada now returns the containing day, the day the library's own Ugadi falls on and the reference publishes (2026-03-19, was 2026-03-20).
  • convertHinduToGregorian now finds Adhika Chaitra Krishna-paksha dates in purnimanta mode.
  • computeEclipsesInRange / computeEclipsesForYear select by the eclipse's peak, not its syzygy, so an eclipse near a range or year boundary is listed exactly once.
  • computeEkadashiDatesForYear covers the local calendar year exactly: a fast on local 31 December moves from the next year's list into its own (Pune 1912 loses 1912-01-01, which is the 1911-12-31 fast). The values themselves are unchanged: each is still the UTC midnight that falls within the local day of the fast, so format it in the location's zone.
  • Years 1900 and 2100 now work at every offset in the year listings and table builders. The festival, moon-phase, eclipse and auspicious-date listings, the lunar getHinduNewYear, convertHinduToGregorian and the table builders reject years 0 to 99 with INVALID_DATE instead of silently reading them as 1900 to 1999. computeEkadashiDatesForYear and computeSankrantisForYear still take any year, so 99 now means the year 99 rather than 1999, and the solar getHinduNewYear returns null for such a year (5.3 returned the 1999 date).
  • Daily panchang edges: a civil day with no sunrise at a polar transition returns null instead of the next day's panchang, and getInstantPanchang returns null for an instant whose Hindu day has no sunrise to start it; moon.set is no longer taken from the next civil day on a day with no moonrise; one eclipse no longer shows on two consecutive days when sunrise falls between syzygy and peak; a solar eclipse whose peak is below the horizon is described by its deepest visible phase (so its description says it is visible while visibleFromLocation, which is about the peak, stays false); computeEndTimes: false no longer changes specialYogas; the Bhadra boundary is no longer about a minute off.
  • Dashas: Ashtottari and Yogini antardashas of the birth mahadasha are now those of the full mahadasha clipped at birth, as Vimshottari's already were; Narayan { duration: 'variable' } gives 12 years to a sign whose lord occupies it (it gave 0, or 1 for an exalted lord); sub-periods now end exactly at their parent's end (they drifted by up to 8 ms).
  • Shadbala: Nathonatha Bala is continuous across sunrise and sunset (it jumped by 60 virupas); cached Shadbala and Bhava Bala totals move by up to about 34 virupas in the pinned charts, and more for a birth just after sunrise in a long night; the Varshaphala year lord can change with them.
  • Sade Sati no longer skips a retrograde re-entry when finding nextArcStart, which could be about 21 years late.
  • Kemadruma no longer counts the Sun, consistent with Sunapha and Anapha.
  • computeVarjyam returns null when the index is not the nakshatra in force at sunrise, instead of a meaningless window (usually a few seconds, occasionally a plausible-looking hour).
  • Placidus-KP houses between about 66.2 and 66.56 degrees of latitude now return cusps instead of PLACIDUS_DIVERGED.

New export

  • computeVimshottariPratyantarIn(mahaDasha, antardasha): the pratyantars of an antardasha split over its full length and clipped, which is what the birth antardasha needs. computeVimshottariPratyantar is unchanged.

Input handling

Only inputs that used to hang, crash with a raw TypeError, or return garbage:

  • An Invalid Date, or an instant beyond the rise and set solver's range, throws INVALID_DATE instead of hanging getSunrise and friends forever.
  • An unknown house system, divisional, graha, yoga type or node-aspect mode, or a vara outside 0 to 6, throws PanchangError INVALID_INPUT. Before, these crashed with a TypeError, returned an Invalid Date window, or (yoga type, node-aspect mode) were silently ignored. computeVimshottariPratyantar with an unknown lord throws INVALID_INPUT too (a plain Error before), and the sidereal-longitude helpers throw INVALID_DATE for an Invalid Date (5.3 returned NaN).
  • An Invalid Date in getUpcomingSolarEclipse or getUpcomingLunarEclipse (5.3: null) or in computePlanetaryPositions (5.3: NaN longitudes) throws INVALID_DATE. A non-integer or out-of-range timezoneOffsetMinutes in buildEclipsesTable or buildMoonPhasesTable throws INVALID_TIMEZONE; 5.3 built the table.
  • An unknown table language throws a RangeError in TypeScript (INVALID_INPUT in Go); 5.3 built the festivals table anyway and crashed with a TypeError in the other two builders.
  • One relaxation: janmaRashi: null or janmaNakshatra: null in getDailyPanchang now means the same as leaving it out (5.3 threw a RangeError).
  • A Moon longitude outside [0, 360) is wrapped into it by all three moon-longitude dashas; NaN or Infinity throws INVALID_INPUT.
  • formatInZone renders any integer offset correctly and throws INVALID_TIMEZONE for a fractional one (5.3 printed +00:undefined).
  • computePrashnaChart and computeKpCuspalSubLords keep their KP defaults when an option is passed as undefined.

Documented, not changed: an omitted timezone falls back to the host's zone in untyped JavaScript; the Go port reports INVALID_TIMEZONE instead, and accepts the zone names "" (UTC) and "Local" (the host), which TypeScript rejects.

Faster, with no output changes

The full parity documents, every table file and every golden are byte-identical before and after. Measured on the same machine, alternating builds, medians:

| call | TypeScript | Go | |---|---|---| | getDailyPanchang, a year of days | 1.4x | 1.3x | | getDailyPanchang, a year in America/New_York | 1.5x | 1.4x | | getInstantPanchang, a year of instants | 1.5x | 1.4x | | computeFestivalsForYear | 1.8x | 1.5x | | buildMuhurtaTable, one year | 2.2x | 2.0x | | convertHinduToGregorian | 6.5x | 5.0x | | getUpcomingEclipses | 4.6x | 3.9x | | computeSadeSati | 18x | 16x | | computeKpCuspalSubLords / computeBhava | 12x | 38x | | a birth-chart bundle | 2.2x | 2.5x |

Most of the causes were repeated work: the same new-moon searches and planet positions recomputed across days and charts (now kept in bounded caches keyed by every input, so no result depends on which calls ran before it), house cusps computing nine planet positions they never read, the converters and table builders building whole days to read four labels, Sade Sati sampling Saturn at steps where it cannot change sign, and a timezone formatter rebuilt on every call. One was not: in TypeScript, the sine and cosine behind every series evaluation passed their reduced argument through module-level variables, and now keep it in locals with the same operations in the same order. That change alone gives the chart primitives their whole speedup and most of the gain on a distinct day and in the Ekadashi and Sankranti listings; the caches give nearly all of it on a repeated day.


Upgrading to 5.2

No type breaks and no removed exports. What changes is output: a set of dates that were wrong, or missing entirely, are now right. If you cache festival dates, diff them against a rebuild before you ship, and key on festivals[].key rather than on the label, because 27 user-visible strings changed punctuation in both en and hi.

Festivals that used to vanish for a whole year now appear

A tithi that begins and ends between two consecutive sunrises is current at no sunrise. A rule keyed on the sunrise tithi matched it on no day of the year, so the festival disappeared from that year entirely. Ugadi and Gudi Padwa were absent from 2026 at every location; Navaratri 2027, Gangaur 2025, both Teejes, Govardhan Puja, Bhai Dooj, Anant Chaturdashi, Kartika Purnima and Chhath Kharna were missing at some cities and present at others. The Hindu day that wholly contains the tithi now claims it, which is what the reference almanac publishes.

Measured over eight Indian cities across 2020 to 2030: 87 dates added, and no existing date moves. Every addition belongs to a city and year where the festival previously had no date at all, so nothing is rescheduled.

Four festivals are deliberately left out, because the reference does not put them on the containing day: Narak Chaturdashi and Chhath Usha Arghya, which it publishes on the day the tithi ends, and Holi and Phagli, which are anchored to pradosha.

Three festivals move to a different date

These are the ones to diff for. They are corrections, not additions.

| festival | was | now | |---|---|---| | Vat Savitri Amavasya | roughly 30 days late, every year | 7 of 7 checked years match the reference | | Rig Upakarma | 9 of 13 checked years right | 13 of 13 | | Sama Upakarma | 4 of 13 checked years right | 13 of 13 |

Vat Savitri Amavasya was filed under the wrong lunar month. The vrat falls on purnimanta Jyeshtha Amavasya, and an amavasya ends its amanta month, so the two namings sit a month apart; the rule had been selecting the following new moon since the festival was added. It is now also anchored to aparahna rather than sunrise, with the later day taking a span that reaches aparahna twice. vat_savitri_purnima is unaffected and still correct, because a purnima sits inside the amanta month it names.

Both Upakarma rules matched on the nakshatra at sunrise and nothing else. Sama Upakarma fired a second time in six of thirteen years, on the Bhadrapada Krishna Amavasya that carries Hasta again, and was a day late in five more. Rig Upakarma emitted two dates when its nakshatra spanned two sunrises (2019, 2029), a date in the wrong paksha when the nakshatra slipped into Krishna (2020), and nothing at all in the one year it fell outside Shukla paksha entirely (2022). Both now emit exactly one date in all 88 city-years of the eight-city sweep. Sama Upakarma is anchored to aparahna and is therefore longitude sensitive: in 2026 it is the 13th of September at Kolkata and the 12th elsewhere, which is what the reference publishes.

Numbers that changed

getUpcomingSolarEclipse did not check that an eclipse ended after the instant it was asked to search from, so range walkers could return the same eclipse repeatedly and drop others. computeEclipsesInRange(2000 to 2011, Varanasi) returned 277 entries of which 249 were one 2007 partial; it now returns 32 distinct entries. Shadbala Kala Bala is now anchored to the preceding sunrise rather than the following one, moving 1009 of 2016 sampled births. Narayan dasha durations exclude adjacent signs from rasi drishti. Gulika and Mandi use a day/night test that matches their own definition.


Upgrading to 5.1, part 2

Two deliberate type breaks, both correcting fields that did not match the reference almanac (the project's parity oracle), in the same style as part 1's varjyam break.

inauspicious.durMuhurta is now DurMuhurtaPeriod[]

- const [dm1, dm2] = r.inauspicious.durMuhurta;
+ r.inauspicious.durMuhurta.forEach((dm) => show(dm)); // 1-2 windows
+ // dm.segment is 'day' or 'night' (only Tuesday carries a night window)

The old table emitted two day windows every day and matched the almanac on none of the seven weekdays. The corrected classical Muhurta-Chintamani table gives one window on Sunday and Wednesday, two elsewhere, and Tuesday's second window falls at night (the 7th of the 15 sunset→sunrise muhurtas), so the exactly-two-day-windows tuple could not survive. Verified against 58 consecutive almanac day-pages across two cities; a full almanac week is pinned.

muhurtas.amritKala is now TimePeriod[]

- if (r.muhurtas.amritKala) show(r.muhurtas.amritKala);
+ r.muhurtas.amritKala.forEach(show);   // [] when the day has none

Amrit Kala shares Varjyam's architecture (the almanac prints them from the same frame): each window anchors at its nakshatra's own start, offset by a per-nakshatra count of nakshatra-elastic ghatikas, spans exactly 4 such ghatikas, and belongs to the Hindu day its start falls in (0-2 windows per day). The old sunrise-anchored single window disagreed with the almanac by up to ~16 h. computeAmritKala is replaced by computeAmritKalaWindows(sunriseUtc, nextSunriseUtc, getMoon).

Ekadashi splits: Smarta first, Vaishnava second

On the days the almanac prints an Ekadashi twice, the earlier day is the Smarta fast and the later one the Vaishnava fast. The almanac says so in prose on every Ekadashi date-time page. Two corrections bring the library in line:

  // Dashami-viddha day (e.g. Rama Ekadashi, 2027-10-25)
- ['vaishnava_ekadashi', 'smarta_ekadashi' /* deferred */, 'ekadashi']
+ ['smarta_ekadashi', 'ekadashi']          // vaishnava_ekadashi is tomorrow

  // First day of a vriddha Dwadashi (e.g. 2026-08-24)
- []
+ ['vaishnava_ekadashi']

Both the Dashami-viddha and the vriddha-Dwadashi (Pakshavardhini) splits now match the almanac across every pair it publishes in 2024-2028. getDailyPanchang callers that keyed off smarta_ekadashi / vaishnava_ekadashi on split days will see the two swap places; computeEkadashiDatesForYear is unchanged. Custom locale packs need the renamed viddha description keys.

Regional solar new years land on their own days

vishu, baisakhi and pohela_boishakh no longer share Puthandu's day. Each keys off the Mesha transit moment its own way, so in 2027 Vishu and Pohela Boishakh fall on April 15 while Puthandu falls on April 14, and in 2028 Vaisakhi falls on April 13 while the rest fall on April 14. getHinduNewYear(year, region, …) follows the same per-region rules.

Several other value-level corrections ride along without shape changes: dur muhurta ordinals, night choghadiya names, Bhadra vasa (now Moon-rashi keyed, with a piecewise vasa segment list on BhadraInfo), night-transit Sankranti dates (+ a new moment field on SankrantiEvent), kshaya-Dwadashi Ekadashi advance, Vijayadashami/Karva Chauth/Janmashtami kala rules, the Kali Yuga year boundary, scoreMuhurta special-yoga parity, eastern- longitude Gulika/Mandi, and the opt-in Ashtakavarga reductions.


Upgrading to 5.1, part 1 (from 5.0)

One deliberate type break, two corrected dasha tables, and one opt-in flag.

inauspicious.varjyam is now TimePeriod[]

- if (r.inauspicious.varjyam) show(r.inauspicious.varjyam);
+ r.inauspicious.varjyam.forEach(show);   // [] when the day has none

5.0 evaluated only the nakshatra active at sunrise and dropped the second Varjyam window printed panchangs show on transition days. 5.1 publishes every window, in start order, under the almanac's attribution rule: a window belongs to the Hindu day its start falls in (one that begins before sunrise and runs past it is yesterday's), and window instants are unclamped, so an end can land after next sunrise. Validated window-for-window against a 61-day reference-almanac sweep (Aug-Sep 2026, two full nakshatra cycles): 62/62 match.

Two value-level corrections ride along: Mula carries a second tyajya spell (elapsed ghatikas 20 and 56; the reference almanac, ProKerala and B.V. Raman's Muhurta concur), so Mula days now emit the window 5.0 missed; and the standalone computeVarjyam primitive returns the earliest of a nakshatra's spells overlapping the day. New export: computeVarjyamWindows(sunriseUtc, nextSunriseUtc, getMoon).

Yogini and Ashtottari starting lords were wrong, now classical

  • Yogini used nakshatraIndex % 8, off by three Yoginis for every birth. Now the classical Devi-Bhagavata formula: (1-based janma nakshatra + 3) mod 8, remainder 1 = Mangala … 0 = Sankata, so Ashwini → Bhramari, Pushya → Dhanya. Verified against published worked examples and PyJHora.
  • Ashtottari used a years-proportional split of the zodiac from a Krittika anchor, matching no source. Now the classical Ardradi group table (malefics rule four nakshatras each, benefics three; Sun = Ardra…Ashlesha, Venus = Krittika…Mrigashira; exported as ASHTOTTARI_NAKSHATRA_GROUPS), with the balance from the elapsed fraction of the group. Verified against PyJHora and Maitreya 8, which agree on every output.

Both functions keep their signatures; recorded outputs from 5.0 will differ and should be discarded.

Opt-in Gana-dosha cancellation in computeAshtakoot

computeAshtakoot(boy, girl, { ganaCancellation: true });

Default output is byte-identical to 5.0 (the reference almanac's published 36-guna table applies no Gana cancellation, and reference-almanac parity stays the default standard). With the flag raised, a doshic Gana score (≤ 1) is restored to the full 6 when the two Moons' sign lords are the same graha or mutual naisargika friends, recorded in cancellations: the condition set attested across independent pandit corpora; weaker ones are documented on AshtakootOptions and deliberately not encoded.


Upgrading from 4.x

Three changes move numbers that 4.x produced, and one option is gone.

Lahiri ayanamsa corrected by +38″

The library's Lahiri constant sat 38 arcseconds behind the reference almanac's: it used 23.853211° at J2000 (the widely-repeated 23° 51′ 11.6″ figure) where the almanac computes 23.863801°. The replacement was solved from the almanac's own published values across 1950-2050, which agree on it to within 0.01″, a century-wide baseline, so the precession polynomial is pinned too, not just the epoch constant. Every sidereal output moves with it:

| Output | Effect | |---|---| | Nakshatra end-times | ~69 s later than 4.x (carries the ayanamsa once) | | Yoga end-times | ~129 s later than 4.x (carries it twice) | | Planetary longitudes, rashi, pada, lagna, divisionals, dashas | shifted +0.0106° | | Tithi / karana end-times | unchanged: Moon − Sun cancels the ayanamsa | | Raman / KP / True Chitra / Thirukanitham | moved by the same +38″; their offsets from Lahiri are preserved |

Worst-case end-time drift vs the almanac dropped from 131 s to 60 s, and the sign split by ayanamsa exposure (nakshatra and yoga early, tithi and karana late) is gone. If you have snapshot tests or cached charts from 4.x, expect them to need re-pinning.

ΔT now uses measurement, so every published time moves ~6 s

4.x took ΔT (TT − UT) entirely from Espenak-Meeus. Its post-2005 branches are an extrapolation published in 2006, and Earth's rotation did not follow it. By 2026 the model reads about 5.9 s high, drifting a further ~0.6 s each year. Because this library reports times, that lands directly on published values.

v5 takes ΔT from the leap-second chain (32.184 + (TAI − UTC), exact, and within the 0.9 s band leap seconds maintain) wherever ΔT has actually been measured, and resumes Espenak-Meeus beyond it carrying the offset it had accrued. Against JPL Horizons the measured era now agrees to 0.005 s at every decade from 1980, where 4.x was seconds out.

| Output | Effect | |---|---| | Tithi / nakshatra / yoga / karana end-times | ~5.7 s later for 2025 dates, growing with the model's drift | | Sankranti and other transit instants | same shift: it is one uniform correction, not per-element | | Sunrise / sunset / moonrise / moonset | barely moved: the error scales against the 15°/hr sky rotation | | Dates a panchang element is filed under | unchanged except where a transit sits within seconds of sunrise |

That last row is the one to know about. computeSankrantisForYear publishes a date, and the date is decided by whether the transit precedes sunrise. The 2025 Tula Sankranti at Reykjavik is such a case: it still falls on Oct 16, but its margin narrowed from 7.1 s to 1.4 s. Locations at high latitude with a transit near sunrise are where a day could flip.

Instant-mode vara was wrong after ~19:00

getInstantPanchang located sunrise by searching forward from date − 12 h. For an evening instant that start point is already past the morning's sunrise, so it found tomorrow's and rolled the weekday back a day. Any query after roughly 7 pm returned the previous vara, and with it the wrong Rahu Kalam, Gulika Kalam, Yamaganda, Choghadiya, Hora, Anandadi yoga and special yogas. getDailyPanchang was never affected. If you cached instant-mode results from 4.x for evening timestamps, discard them.

precision removed

precision: 'standard' | 'high' and the Precision type no longer exist. Element transitions are now solved by secant iteration, which converges to the root rather than stopping at a fixed tolerance, so there is nothing left for the option to select, and the tighter setting no longer buys anything. Removing it from your options object is the whole migration; leaving it in is a type error, not a silent no-op.

Sunrise is single-valued per location-day

Solar rise/set is computed from a canonical anchor and cached per location-day, so it no longer depends on which instant the caller happened to start searching from. Values shift by ≤108 ms vs 4.x, and two calls for the same day now agree exactly instead of differing by up to 109 ms. Windows derived proportionally from the day length (Varjyam, Bhadra, the slot systems) move by a little more than that. This removes an inconsistency rather than introducing an approximation: 4.x returned a different sunrise depending on which caller asked.

Moonrise and moonset are single-valued per location-day

The same treatment sunrise received, now applied to the Moon. getMoonrise / getMoonset resolve through a canonical per-UTC-day cache, so an event has one timestamp no matter which caller asks or from which instant they searched. Published moon.rise / moon.set shift by ≤182 ms vs 4.x. Nothing else in the result moves, verified over 11,520 daily results across six locations and three centuries: zero changes to any index, name, boolean, festival date or other timestamp.

read* for tables, compute* for the engine

getFestivalsForYear read a pre-built table; getFestivalsInRange ran the engine. Two near-identical names, completely different inputs and semantics. v5 settles one convention across all four families (festivals, eclipses, moon phases and muhurta):

| 4.x | v5 | what it does | |---|---|---| | getFestivalsForYear | readFestivalsForYear | reads a table | | getFestivalsForDate | readFestivalsForDate | reads a table | | getFestivalsYearRange | readFestivalsYearRange | reads a table | | getEclipsesForYear / ForDate / YearRange | readEclipsesForYear / … | reads a table | | getMoonPhasesForYear / ForDate / YearRange | readMoonPhasesForYear / … | reads a table | | n/a | readMuhurtaForYear / ForDate / YearRange | new: reads a table | | n/a | readBestMuhurtaDays | new: top-scoring days | | getFestivalsInRange | computeFestivalsInRange | runs the engine | | getEclipsesInRange | computeEclipsesInRange | runs the engine | | getMoonPhasesInRange | computeMoonPhasesInRange | runs the engine | | findAuspiciousDates | computeAuspiciousDatesInRange | runs the engine | | getEkadashiDatesForYear | computeEkadashiDatesForYear | runs the engine | | getSankrantisForYear | computeSankrantisForYear | runs the engine |

Every 4.x name still works: they are deprecated aliases pointing at the same functions, kept through v5. Nothing breaks today; the old names will go in v6.

New single-year entry points, the shape most callers reach for first:

computeFestivalsForYear(2027, location, { timezone: 330 });
computeEclipsesForYear(2027, location, { timezone: 330 });
computeMoonPhasesForYear(2027, { timezone: 330 });
computeAuspiciousDatesForYear(2027, vivahRule, location, { timezone: 330 });

buildMuhurtaTable + the panchang-ts/muhurta subpath

Festivals, eclipses and moon phases each had a builder and an engine-free reader; muhurta had neither, so finding auspicious dates meant running the full engine on device for every query. v5 completes the family:

// build once (build time, or first launch), then persist the JSON
import { buildMuhurtaTable, vivahRule } from 'panchang-ts';
const table = buildMuhurtaTable({
  rule: vivahRule, location: DELHI, timezoneOffsetMinutes: 330,
  startYear: 2026, endYear: 2031,
});

// read it back with no astronomy code in the bundle (~1.7 KB)
import { readBestMuhurtaDays } from 'panchang-ts/muhurta';
readBestMuhurtaDays(table, 5);   // top 5 days, highest score first

Only days that pass the rule are stored unless you pass includeFailures: true.

Built tables are dictionary-encoded and carry key

build*Table now emits a _dict of unique entries with each day holding indices, rather than repeating every localized string at every occurrence. A 10-year festival table goes from 315 KB to 89.9 KB (28.5%); a 10-year moon-phase table from 106 KB to 29.7 KB (28.0%). Resolved output is identical, verified across every year and both locales.

Gzip already hid most of this on the wire, so the win is parse time and resident memory, which is the constraint that actually bites on Hermes.

Festival table entries also gained the stable key the engine has been returning since 4.x, so a table is no longer both larger and less useful than engine output.

Tables you already cached still read. The reader detects the format, so a v1 table built with 4.x keeps working; only key is unavailable from it, and comes back as ''.

Published Dates are real instants: the flagship change

- result.sunrise.getTime()      // NOT when sunrise happened (off by the UTC offset)
- result.sunrise.getUTCHours()  // the documented 4.x idiom
+ result.sun.rise.getTime()     // correct epoch ms
+ result.sun.riseLocal          // "2025-01-14T07:09:44.172+05:30"

Applies to every Date in DailyPanchangResult and to every TimePeriod: sun.rise, sun.set, sun.nextRise, moon.rise, moon.set, all the muhurtas and inauspicious periods, all four slot systems, the element startTime/endTime arrays, the eclipse contacts and sutak window. Each gains a *Local companion: sun.riseLocal, inauspicious.rahuKalam.startLocal, angas.tithis[0].endTimeLocal, and so on.

| you had | you now write | |---|---| | r.sun.rise.getUTCHours() | r.sun.riseLocal.slice(11, 13) | | `${h}:${m}` from getUTC* | r.sun.riseLocal.slice(11, 16) | | r.inauspicious.rahuKalam.start.getUTCHours() | r.inauspicious.rahuKalam.startLocal.slice(11, 13) | | r.angas.tithis[0].endTime for display | r.angas.tithis[0].endTimeLocal | | a Date you derived yourself | formatInZone(d, r.timezone.offsetMinutes) |

getInstantPanchang results carry no *Local fields: that call takes no timezone, so there is no zone to render a wall clock in.

Cost: rendering the strings adds ~0.04 ms per daily panchang, invisible on a cold call and ~20% of a fully cached warm one. The release warms to 0.17 ms, against published 4.3.1's 6.20 ms.

result.timezone is now an object

- result.timezone            // 330
+ result.timezone            // { offsetMinutes: 330, zone: 'Asia/Kolkata' }
+ result.timezone.offsetMinutes

options.timezone has always accepted number | string, but the result carried only a number, so passing 'America/New_York' produced a result that could not say which zone produced it. zone is present only when you passed a zone name.

The DST limit, now stated explicitly. The offset is resolved once per call from a reference date, so a Hindu day containing a DST transition is computed at a single offset throughout. Correct for almost every day; on the one or two transition days a year, times after the jump are shifted by its size. 4.x documented this as a blanket "DST resolves automatically", which was not the whole truth.

The result object is grouped

DailyPanchangResult had ~50 flat top-level fields mixing five categories. v5 sorts them into seven groups. See The result is grouped for the full table. This is a large break, and it lands in the same release as the Date change on purpose: migrating both at once is one pass over your read sites, not two.

Every rename, in full:

| 4.x | v5 | |---|---| | sunrise / sunset / nextSunrise | sun.rise / sun.set / sun.nextRise | | sunriseLocal / sunsetLocal / nextSunriseLocal | sun.riseLocal / sun.setLocal / sun.nextRiseLocal | | dayDurationMinutes / nightDurationMinutes | sun.dayDurationMinutes / sun.nightDurationMinutes | | dinamanaMinutes / ratrimanaMinutes | sun.dinamanaMinutes / sun.ratrimanaMinutes | | siderealSunAtSunrise | sun.siderealLongitude | | suryaNakshatra | sun.nakshatra | | moonrise / moonset | moon.rise / moon.set | | moonriseLocal / moonsetLocal | moon.riseLocal / moon.setLocal | | siderealMoonAtSunrise | moon.siderealLongitude | | chandraRashi | moon.rashi | | tithis / nakshatras / yogas / karanas / vara | angas.* (same names) | | masa / chandramasa / samvat | calendar.* (same names) | | abhijitMuhurta / brahmaMuhurta / vijayaMuhurta | muhurtas.abhijit / muhurtas.brahma / muhurtas.vijaya | | godhuliMuhurta / nishitaMuhurta | muhurtas.godhuli / muhurtas.nishita | | amritKala / madhyahna / pratahSandhya / sayahnaSandhya | muhurtas.* (same names) | | doGhatiMuhurta | muhurtas.doGhati | | rahuKalam / gulikaKalam / yamaganda / durMuhurta | inauspicious.* (same names) | | varjyam / bhadra / gandaMula / panchaka / panchakaRahita | inauspicious.* (same names) | | choghadiya / hora | periods.choghadiya / periods.hora | | gowriPanchangam | periods.gowri | | eclipse.magnitude (disc area) | eclipse.obscuration: same value; the new eclipse.magnitude is the diameter fraction catalogues publish, and is negative for a penumbral lunar eclipse |

Unmoved: date, location, timezone, ayanamsa, specialYogas, anandadiYoga, festivals, eclipse, chandraBalam, tarabala.

For getInstantPanchang: tithi / nakshatra / yoga / karana / vara → angas.*; siderealSun → sun.siderealLongitude; siderealMoon → moon.siderealLongitude; suryaNakshatra → sun.nakshatra; chandraRashi → moon.rashi; chandramasa / samvat → calendar.*; panchaka / gandaMula → inauspicious.*.

One rule for "not applicable": always present, null or []

4.x used three conventions and you could not predict which you would get: | null for bhadra / varjyam / eclipse, ?-optional for chandraBalam / tarabala, and an empty array for panchakaRahita / festivals. v5 has one rule: every field is always present, a value that does not apply is null, and a collection that does not apply is []. (Since 5.1, varjyam is a collection and follows the [] arm; see Upgrading from 5.0.)

- if ('chandraBalam' in r) …        // 4.x: field absent without janmaRashi
- r.chandraBalam!.house             // and the `!` was mandatory
+ if (r.chandraBalam !== null) …    // v5: always present, null when unasked
+ r.chandraBalam?.house

Only chandraBalam and tarabala changed behaviour; everything else already followed the rule. toBeUndefined()-style checks against them become toBeNull().

suryaNakshatra is typed as a nakshatra, not a rashi

Now published as sun.nakshatra. Its index has always been 0..26 (Ashwini … Revati). Its type said RashiInfo, documented "0 = Mesha … 11 = Meena", so anyone indexing a 12-element rashi array by it got silent garbage for two thirds of the year. The runtime value is unchanged; the type is now NakshatraIndexInfo and TypeScript will point at the misuse.

_debug removed

DailyPanchangResult._debug was declared in the published type and written nowhere in the library. It never carried data. If you referenced it, it was always undefined.

Alias fields documented rather than removed

sun.dinamanaMinutes / sun.dayDurationMinutes and sun.ratrimanaMinutes / sun.nightDurationMinutes are the same numbers under classical and English names. Both pairs stay (consumers use both vocabularies) and the types now say plainly that they are aliases, never independently computed.

calendar.chandramasa keeps its casing beside moon.rashi and sun.nakshatra. Renaming it would break every consumer for a casing preference. The type now documents that calendar.masa is the solar month and calendar.chandramasa the lunar one, which was previously left to guesswork.

Additive, but worth knowing

  • EclipseInfo / EclipseSubtype are now exported. 4.x shipped getUpcomingLunarEclipse and friends without the type they return.
  • festivals[].key: stable, language-independent festival id. Match on this, never on name.
  • bhadra.locationName: localized display name; bhadra.location stays the machine-readable key.
  • MuhurtaScore.factors: structured scoring inputs alongside English reasons.
  • BirthChart.byPlanet: the nine placements keyed by graha.
  • eclipse.description is now localized. Under language: 'hi' it was previously emitted in English, including inside festivals[].description.
  • sections on getDailyPanchang: opt into a narrower, cheaper call. See Performance.

Features at a Glance

| Category | Features | |---|---| | Pancha Anga | Tithi, Nakshatra, Yoga, Karana, Vara, with all intra-day transitions | | Lunar Calendar | Chandra Masa (Purnimanta + Amanta), Adhika (leap) detection, Vikram + Shaka Samvat | | Solar Calendar | Saura Masa, Surya Nakshatra, Sankranti (transit-based) | | Sun & Moon | Sunrise, Sunset, Moonrise, Moonset (Meeus apparent-upper-limb), Chandra Rashi | | Auspicious Muhurta | Brahma, Abhijit, Vijaya, Godhuli, Nishita, Madhyahna, Pratah/Sayahna Sandhya, Amrit Kala | | Inauspicious Periods | Rahu Kalam, Gulika Kalam, Yamaganda, Dur Muhurta, Varjyam, Ganda Mula, Bhadra Kala, Panchaka | | Time-Slot Systems | Choghadiya, Gowri Panchangam (Tamil "Nalla Neram"), Hora, Do Ghati, Panchaka Rahita | | Special Yogas | Anandadi (28-cycle), Amrit Siddhi, Sarvartha Siddhi, Ravi/Guru Pushya, Dwi-/Tripushkar, Jwalamukhi, Aadal, Vidaal, Ravi | | Festivals (80+) | Ekadashi (Smarta/Vaishnava split), Pradosha, Sankranti, classical (Diwali/Holi/Shivaratri…), regional across 21 states + Nepal | | Eclipses | Solar/lunar detection, subtype, magnitude, horizon visibility, sutak window | | Planetary Positions | All 9 grahas (sidereal) with rashi, nakshatra, pada, retrograde; mean or true Rahu/Ketu | | Dashas | Vimshottari (3-level), Ashtottari, Yogini, Chara, Narayan | | Personal Transits | Chandra Balam, Tarabala (9-cycle), Sade Sati | | Birth Chart | Lagna, Bhava under 3 house systems, D1/D2/D3/D7/D9/D10/D12/D30, Planetary Dignity | | Compatibility & Doshas | Ashtakoot (36-point), Pathu Porutham (Tamil 10-fold), Mangal, Kaal Sarp (12 subtypes), Pitru | | Strength & Aspects | Drishti, Shadbala (6-fold), Ashtakavarga (Bhinna + Sarva, with reductions), Bhava Bala, Argala | | Yogas & Karakas | 25 named yogas (with cancellations), 7- and 8-Karaka Jaimini | | Annual & Sensitive | Varshaphala (Tajik + 27 Sahams), Tithi Pravesha, Arudha padas, Hora/Ghati/Bhava/Sripati lagnas, Upagrahas | | KP & Prashna | KP sub-lord at any longitude, Placidus-KP cuspal sub-lords, KP significators, Prashna chart | | Muhurta Engine | Configurable scoring + 13 stock occasions (vivah, griha pravesh, namakarana, …) | | Calendar Conversion | Gregorian↔Hindu, Kali Yuga year, Hindu New Year, yearly Ekadashi/Sankranti/festival listings | | Localization | English + Hindi (Devanagari) | | Configuration | 5 ayanamsas (Lahiri, Raman, KP, True Chitrapaksha, Thirukanitham), 2 masa systems, 3 house systems |


Used By


Feature Reference

A quick tour with runnable snippets. Each section links to its full page on the docs site: every option, field, and caveat lives there.

Pancha Anga & the daily result

📖 Daily Panchang →

const r = getDailyPanchang(date, location, { timezone: 330 })!;

r.angas.tithis.forEach(t => console.log(t.name, t.paksha, t.endTime));
r.angas.vara.name;                     // "Mangalawara"
r.calendar.chandramasa.isAdhika;       // true during leap months
r.muhurtas.brahma;                     // TimePeriod | null, and 9 more muhurtas
r.inauspicious.rahuKalam;              // { start, end }, and 9 more windows
r.periods.choghadiya.day[0].name;      // 16 Choghadiya + Gowri + 24 Hora slots
r.anandadiYoga.name; r.specialYogas;   // Anandadi + Amrit/Sarvartha Siddhi, …

// Single-instant snapshot:
import { getInstantPanchang } from 'panchang-ts';
const i = getInstantPanchang(new Date(), location)!;
console.log(i.angas.tithi.name, i.angas.nakshatra.name);

Festivals (80+)

📖 Festivals →

r.festivals.forEach(f => console.log(f.key, f.name, f.type));
// `name` is localized, so match on `key`, never on `name`:
const hasDiwali = r.festivals.some(f => f.key === 'diwali');

// Scope regional variants: 21 state slugs + 'nepal' + 'all' (default)
getDailyPanchang(jan14, chennai, { timezone: 330, region: 'tamil-nadu' });

// Pre-computed table: build once, cache the JSON, read engine-free.
import { buildFestivalsTable } from 'panchang-ts';
import { readFestivalsForYear, readFestivalsForDate } from 'panchang-ts/festivals';

No table ships with the package: festival dates are observer-dependent, so you build one for your users' location and years (npm run festivals:gen is a worked example).

Eclipses & Moon Phases

📖 Eclipses & Moon Phases →

if (r.eclipse) {
  r.eclipse.kind; r.eclipse.subtype;       // 'solar'|'lunar', 'partial'|'total'|…
  r.eclipse.obscuration;                   // disc AREA covered, 0..1
  r.eclipse.magnitude;                     // catalogue DIAMETER fraction
  r.eclipse.sutakStart; r.eclipse.sutakEnd;
}

import { getUpcomingSolarEclipse, computeMoonPhasesInRange } from 'panchang-ts';
getUpcomingSolarEclipse(new Date(), loc, 365);
computeMoonPhasesInRange(start, end);      // precise new/quarter/full instants

// Engine-free tables: panchang-ts/eclipses and panchang-ts/moon-phases

Muhurta Engine

📖 Muhurta Engine →

import { scoreMuhurta, computeAuspiciousDatesInRange, vivahRule } from 'panchang-ts';

const s = scoreMuhurta(new Date('2026-05-12'), DELHI, vivahRule, { timezone: 330 });
s.score;            // 0..100; passes when ≥ 50
s.factors;          // structured, stable codes: localize/filter on these
s.reasons;          // diagnostic English

computeAuspiciousDatesInRange(vivahRule, start, end, DELHI, { timezone: 330 });

13 stock rules (vivah, griha pravesh, namakarana, …) or your own pure-data MuhurtaRule. Vara × Tithi yogas (Siddha, Amrita, Dagdha, …) are scored jointly. Pre-compute a table with buildMuhurtaTable and read it back through panchang-ts/muhurta (~1.7 KB, no astronomy code).

Planetary Positions & Birth Charts

📖 Birth Charts →

import {
  computePlanetaryPositions, computeLagna, computeBhava,
  computeRashiChart, computeNavamsa, computeDivisionalChart, computeDignity,
} from 'panchang-ts';

const g = computePlanetaryPositions(new Date(), 'lahiri');
g.jupiter.rashi.name; g.jupiter.nakshatra.pada; g.saturn.isRetrograde;

const d1 = computeRashiChart(birth, loc);       // houseSystem: whole-sign | equal | placidus-kp
d1.byPlanet.Mars.house;                          // keyed lookup, no linear scan
const d9 = computeNavamsa(birth, loc);           // + D2/D3/D7/D10/D12/D30
computeDignity('Mars', 9);                       // 'exalted'

Dashas & Personal Transits

📖 Dashas & Transits →

import {
  computeVimshottariDashaFromBirth, computeVimshottariPratyantar,
  computeAshtottariDasha, computeYoginiDasha, computeCharaDasha,
  computeNarayanDasha, computeSadeSati,
} from 'panchang-ts';

const vim = computeVimshottariDashaFromBirth(birth, 'lahiri');   // 3-level
computeSadeSati(natalMoonRashiIndex, new Date());
// Daily transits: pass janmaRashi / janmaNakshatra to getDailyPanchang
// and read r.chandraBalam / r.tarabala.

Strength, Yogas & Karakas

📖 Strength, Yogas & Karakas →

import {
  computeAspects, computeShadbala, computeBhavaBala,
  computeAshtakavarga, computeYogas, computeJaiminiKarakas,
} from 'panchang-ts';

computeShadbala(birth, loc);                        // 6-fold, in Virupas
computeAshtakavarga(d1, { reductions: true });      // Bhinna + Sarva + Sodhana
computeYogas(d1);                                   // ~25 named, with bhanga
computeJaiminiKarakas(d1, { variant: '8-jaimini' });

Compatibility & Doshas

📖 Matching & Doshas →

import {
  computeAshtakoot, computePathuPorutham,
  computeMangalDosha, computeMangalCompatibility, computeKaalSarp, computePitruDosha,
} from 'panchang-ts';

computeAshtakoot({ rashi: 4, nakshatra: 9 }, { rashi: 0, nakshatra: 1 });
// → { totalScore: 0..36, koots: KootScore[8], cancellations: string[] }
// Opt-in Gana-dosha cancellation (default off, preserves almanac 36-guna parity):
computeAshtakoot(boy, girl, { ganaCancellation: true });

computeMangalCompatibility(boyChart, girlChart);   // Manglik is a PAIRWISE verdict
computeKaalSarp(d1);                               // 12 subtypes by Rahu's house

Annual Charts, Sensitive Points, KP & Prashna

📖 Annual Charts → · KP & Prashna →

import {
  computeVarshaphala, computeTithiPravesha, computeArudhas,
  computeHoraLagna, computeGhatiLagna, computeBhavaLagna, computeSripatiLagna,
  computeUpagrahas, computeArgala,
  computeKpSubLord, computeKpCuspalSubLords, computeKpSignificators,
  computePrashnaChart,
} from 'panchang-ts';

const v = computeVarshaphala(birth, 30, loc);      // Tajik + Muntha + 27 Sahams
computeTithiPravesha(birth, 30, loc);              // natal tithi's Sun-Moon separation returns
computeKpSubLord(45.5).subLord;                    // 243 sub-divisions
computePrashnaChart(questionTime, querentLoc);     // horary, Placidus-KP default

Calendar Conversion

📖 Calendar Conversion →

import {
  convertGregorianToHindu, convertHinduToGregorian,
  getKaliYugaYear, getHinduNewYear,
  computeEkadashiDatesForYear, computeSankrantisForYear,
} from 'panchang-ts';

convertHinduToGregorian(
  { vikramSamvat: 2083, masaIndex: 0, paksha: 'shukla', pakshaTithi: 9 },
  DELHI, { timezone: 330 },
);                                                  // → Date[] (Rama Navami VS 2083)
getHinduNewYear(2026, 'tamil-nadu', DELHI, { timezone: 330 });  // region-aware

Localization & Configuration

📖 Options & Localization →

const hi = getDailyPanchang(date, loc, { timezone: 330, language: 'hi' })!;
hi.angas.tithis[0].name;       // "कृष्ण चतुर्दशी"
hi.angas.vara.englishName;     // "Tuesday", englishName always English

// All options: timezone (number | IANA string), ayanamsa (5), language (en|hi),
// masaSystem (purnimanta|amanta), region, computeEndTimes, sections,
// janmaRashi, janmaNakshatra.

Machine-readable keys never change with language: match on festival.key, bhadra.location, eclipse.kind, factors[].code; render name / locationName / description / reasons.

Types & Exports

📖 Types & Exports →: the key interfaces (TithiInfo, FestivalInfo, EclipseInfo, GrahaPosition, …) and the complete export list of the main entry and the four engine-free subpaths (panchang-ts/festivals, /eclipses, /moon-phases, /muhurta).


React Native / Hermes

Works with Expo and bare React Native (Hermes engine). Pass timezone as a number: IANA strings need Intl, which older Hermes versions lack.

Two-pass rendering pattern for smooth UI:

import { getDailyPanchang } from 'panchang-ts';
import { InteractionManager } from 'react-native';

// Pass 1 (cheapest useful result): elements, slots, muhurtas (~0.25 ms).
const fast = getDailyPanchang(date, location, {
  timezone: 330,
  sections: [],
  computeEndTimes: false,
});
setState(fast);

// Pass 2 (background): everything (~0.41 ms).
InteractionManager.runAfterInteractions(() => {
  setState(getDailyPanchang(date, location, { timezone: 330 }));
});

Accuracy

📖 Full accuracy notes →

9,207 tests across 146 files, including fixtures cross-verified against reference panchang calculations spanning 2025-2026 across 10 Indian cities plus New York, London, Sydney, Dubai, Singapore (diaspora fixtures cover DST on America/New_York).

| Element | Accuracy | |---|---| | Sunrise / Sunset | ≤29 s observed vs reference minute-midpoint (±45 s tolerance) | | Moonrise / Moonset | Meeus apparent-upper-limb (refraction + parallax); ~3-5 min vs simpler-horizon authorities is expected | | Tithi / Nakshatra / Yoga / Karana names | Exact match vs reference | | Tithi / Nakshatra / Yoga / Karana end-times | ≤60 s vs the reference almanac across all 20 audited comparisons | | Ayanamsa (Lahiri) | Reproduces the reference almanac's published value to ~0.01″ across 1950-2050 | | Planetary positions (Sun-Saturn) | ±0.02° sidereal | | Planetary positions (Rahu/Ketu) | ≤0.5° mean node, ≤0.6° true node (typical) | | Lagna sidereal longitude | Cross-checked against Jagannath Hora reference charts | | D1 / D9 house placement | Exact match vs reference for 9-graha placement | | Ashtakoot total | ±1 point per pair across 30+ matched pairs | | Varjyam windows | count + position vs the reference almanac over a 61-day / two-nakshatra-cycle sweep, ≤2 min (62/62 windows) | | Sade Sati arc start/end | ±1-2 days vs authoritative ephemerides |

Festival dating follows the reference al