panchang-ts
v5.4.0
Published
Pure TypeScript Hindu Panchang calculations. Tithi, Nakshatra, Yoga, Karana, Vara, and more. Offline-first, React Native compatible.
Maintainers
Readme
panchang-ts
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-tsQuick 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); // 2081Reading 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
Datewas the true instant shifted by the UTC offset, and the README told you to read it back withgetUTC*. That worked only as long as you did nothing else with the value:JSON.stringifyemitted a wrong instant labelledZ,Intlwith atimeZonerendered 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()→ readxLocal, 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;holiis now the next day (Rangwali Holi), as the reference publishes it. That holds forgetDailyPanchang, the listings and the tables.getInstantPanchangreports by the tithi at the instant, so an instant in Phalguna Purnima listsholika_dahanandholitogether, and it never listskarthigai_deepam(it reportsmasik_karthigaion 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 regionsallandtamil-nadu. On that day those two regions list it in place ofmasik_karthigai; every other region still listsmasik_karthigai, so a table built foralland 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),amantathe southern ones as before, regionnepalthe solar month. This changes the default output: a caller that never setsmasaSystemnow 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 intestdataconfirm; 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 as2026-11-09T04:00Z, 23:00 on 8 November in the zone and 9 November at the July offset; it is now2026-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).
getHinduNewYearon 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).convertHinduToGregoriannow finds Adhika Chaitra Krishna-paksha dates in purnimanta mode.computeEclipsesInRange/computeEclipsesForYearselect by the eclipse's peak, not its syzygy, so an eclipse near a range or year boundary is listed exactly once.computeEkadashiDatesForYearcovers 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,convertHinduToGregorianand the table builders reject years 0 to 99 withINVALID_DATEinstead of silently reading them as 1900 to 1999.computeEkadashiDatesForYearandcomputeSankrantisForYearstill take any year, so 99 now means the year 99 rather than 1999, and the solargetHinduNewYearreturnsnullfor such a year (5.3 returned the 1999 date). - Daily panchang edges: a civil day with no sunrise at a polar transition returns
nullinstead of the next day's panchang, andgetInstantPanchangreturnsnullfor an instant whose Hindu day has no sunrise to start it;moon.setis 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 whilevisibleFromLocation, which is about the peak, staysfalse);computeEndTimes: falseno longer changesspecialYogas; 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.
computeVarjyamreturnsnullwhen 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.computeVimshottariPratyantaris 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_DATEinstead of hanginggetSunriseand friends forever. - An unknown house system, divisional, graha, yoga type or node-aspect mode, or a vara outside 0
to 6, throws
PanchangErrorINVALID_INPUT. Before, these crashed with aTypeError, returned an Invalid Date window, or (yoga type, node-aspect mode) were silently ignored.computeVimshottariPratyantarwith an unknown lord throwsINVALID_INPUTtoo (a plainErrorbefore), and the sidereal-longitude helpers throwINVALID_DATEfor an Invalid Date (5.3 returnedNaN). - An Invalid Date in
getUpcomingSolarEclipseorgetUpcomingLunarEclipse(5.3:null) or incomputePlanetaryPositions(5.3:NaNlongitudes) throwsINVALID_DATE. A non-integer or out-of-rangetimezoneOffsetMinutesinbuildEclipsesTableorbuildMoonPhasesTablethrowsINVALID_TIMEZONE; 5.3 built the table. - An unknown table language throws a
RangeErrorin TypeScript (INVALID_INPUTin Go); 5.3 built the festivals table anyway and crashed with aTypeErrorin the other two builders. - One relaxation:
janmaRashi: nullorjanmaNakshatra: nullingetDailyPanchangnow means the same as leaving it out (5.3 threw aRangeError). - A Moon longitude outside [0, 360) is wrapped into it by all three moon-longitude dashas; NaN or
Infinity throws
INVALID_INPUT. formatInZonerenders any integer offset correctly and throwsINVALID_TIMEZONEfor a fractional one (5.3 printed+00:undefined).computePrashnaChartandcomputeKpCuspalSubLordskeep their KP defaults when an option is passed asundefined.
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 noneAmrit 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 none5.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 firstOnly 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.offsetMinutesoptions.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?.houseOnly 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/EclipseSubtypeare now exported. 4.x shippedgetUpcomingLunarEclipseand friends without the type they return.festivals[].key: stable, language-independent festival id. Match on this, never onname.bhadra.locationName: localized display name;bhadra.locationstays the machine-readable key.MuhurtaScore.factors: structured scoring inputs alongside Englishreasons.BirthChart.byPlanet: the nine placements keyed by graha.eclipse.descriptionis now localized. Underlanguage: 'hi'it was previously emitted in English, including insidefestivals[].description.sectionsongetDailyPanchang: 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
- dharmagya.app: Daily Panchang and Hindu calendar (Play Store)
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
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+)
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
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-phasesMuhurta 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
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
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
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
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 houseAnnual 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 defaultCalendar 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-awareLocalization & Configuration
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
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
