panchang-ts 4.3.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +566 -927
  2. package/dist/calendar/eclipsesTable.cjs +13 -358
  3. package/dist/calendar/eclipsesTable.d.cts +33 -23
  4. package/dist/calendar/eclipsesTable.d.ts +33 -23
  5. package/dist/calendar/eclipsesTable.js +10 -357
  6. package/dist/calendar/festivalsTable.cjs +48 -25335
  7. package/dist/calendar/festivalsTable.d.cts +33 -23
  8. package/dist/calendar/festivalsTable.d.ts +33 -23
  9. package/dist/calendar/festivalsTable.js +45 -25334
  10. package/dist/calendar/moonPhasesTable.cjs +55 -6791
  11. package/dist/calendar/moonPhasesTable.d.cts +33 -21
  12. package/dist/calendar/moonPhasesTable.d.ts +33 -21
  13. package/dist/calendar/moonPhasesTable.js +52 -6790
  14. package/dist/{eclipsesTableTypes-D-tSQMQd.d.cts → eclipsesTableTypes-BFYWRA44.d.cts} +11 -1
  15. package/dist/{eclipsesTableTypes-D-tSQMQd.d.ts → eclipsesTableTypes-BFYWRA44.d.ts} +11 -1
  16. package/dist/festivalsTableTypes-CzDgY4zS.d.cts +100 -0
  17. package/dist/festivalsTableTypes-CzDgY4zS.d.ts +100 -0
  18. package/dist/index.cjs +29098 -5938
  19. package/dist/index.d.cts +1102 -255
  20. package/dist/index.d.ts +1102 -255
  21. package/dist/index.js +29081 -5939
  22. package/dist/moonPhasesTableTypes-9XLibNCZ.d.cts +103 -0
  23. package/dist/moonPhasesTableTypes-9XLibNCZ.d.ts +103 -0
  24. package/dist/muhurta/muhurtaTable.cjs +53 -0
  25. package/dist/muhurta/muhurtaTable.d.cts +47 -0
  26. package/dist/muhurta/muhurtaTable.d.ts +47 -0
  27. package/dist/muhurta/muhurtaTable.js +47 -0
  28. package/dist/muhurtaTableTypes-CgBom297.d.cts +81 -0
  29. package/dist/muhurtaTableTypes-CgBom297.d.ts +81 -0
  30. package/package.json +23 -6
  31. package/dist/festivalsTableTypes-GePaspwd.d.cts +0 -52
  32. package/dist/festivalsTableTypes-GePaspwd.d.ts +0 -52
  33. package/dist/moonPhasesTableTypes-TH0T5rVD.d.cts +0 -56
  34. package/dist/moonPhasesTableTypes-TH0T5rVD.d.ts +0 -56
package/README.md CHANGED
@@ -1,11 +1,17 @@
1
1
  # panchang-ts
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/panchang-ts)](https://www.npmjs.com/package/panchang-ts)
4
+ [![license](https://img.shields.io/npm/l/panchang-ts)](./LICENSE)
4
5
 
5
6
  Pure TypeScript Hindu Panchang (almanac), Jyotish, and Birth Chart calculations.
6
- Zero native dependencies. Works offline in React Native (Hermes), Node.js, and browsers.
7
+ Zero runtime dependencies. Works offline in React Native (Hermes), Node.js, and browsers.
7
8
 
8
- **Fast** (~0.1 ms names-only, ~0.5 ms full) · **Typed** (full TypeScript) · **Offline** (pure JS math) · **8,164 tests across 100 files**
9
+ **Fast** (~0.25 ms trimmed, ~0.41 ms full) · **Typed** (full TypeScript) · **Offline** (pure JS math) · **8,368 tests across 121 files**
10
+
11
+ > 📖 **Full documentation: [dharmagya.app/docs/panchang-ts](https://dharmagya.app/docs/panchang-ts)**
12
+ > This README covers install, quick start, and the 4.x → 5 migration in full, plus a per-feature
13
+ > quick reference. The complete reference — every option, result field, table format, accuracy
14
+ > bound and performance note — lives on the docs site.
9
15
 
10
16
  ---
11
17
 
@@ -31,26 +37,84 @@ const result = getDailyPanchang(
31
37
  // → DailyPanchangResult | null. Null only at polar latitudes where
32
38
  // sunrise can't be computed. Anywhere else, narrow with `if (!result) return;`.
33
39
 
34
- console.log(result!.tithis[0].name); // "Krishna Chaturdashi"
35
- console.log(result!.nakshatras[0].name); // "Mrigashira"
36
- console.log(result!.vara.name); // "Mangalawara"
37
- console.log(result!.chandramasa.name); // "Magha"
38
- console.log(result!.samvat.vikramSamvat); // 2081
40
+ console.log(result!.angas.tithis[0].name); // "Krishna Chaturdashi"
41
+ console.log(result!.angas.nakshatras[0].name); // "Mrigashira"
42
+ console.log(result!.angas.vara.name); // "Mangalawara"
43
+ console.log(result!.calendar.chandramasa.name); // "Magha"
44
+ console.log(result!.calendar.samvat.vikramSamvat); // 2081
39
45
  ```
40
46
 
41
47
  ### Reading Output Times
42
48
 
43
- All `Date` objects are **offset-adjusted** to the requested timezone. Read time
44
- components via `getUTC*` `.getHours()` would use your system zone:
49
+ Every `Date` in a result is a **real instant** `.getTime()` is the correct
50
+ epoch millisecond. Every instant has a `*Local` companion: an offset-carrying
51
+ ISO 8601 string, which is what you want for display.
52
+
53
+ ```typescript
54
+ result!.sun.rise; // Date — 2025-01-14T01:39:44.172Z (the actual moment)
55
+ result!.sun.riseLocal; // "2025-01-14T07:09:44.172+05:30"
56
+ result!.inauspicious.rahuKalam.start; // Date
57
+ result!.inauspicious.rahuKalam.startLocal;
58
+
59
+ // Just the wall clock:
60
+ result!.sun.riseLocal.slice(11, 16); // "07:09"
61
+
62
+ // Anything else works too, because the Date is genuinely correct:
63
+ new Intl.DateTimeFormat('en-IN', { timeZone: 'Asia/Kolkata', timeStyle: 'short' })
64
+ .format(result!.sun.rise); // "7:09 am"
65
+ Temporal.Instant.from(result!.sun.riseLocal);
66
+ ```
67
+
68
+ For an instant you derive yourself, `formatInZone` renders it the same way:
45
69
 
46
70
  ```typescript
47
- const sunrise = result!.sunrise;
48
- const h = sunrise.getUTCHours(); // 7
49
- const m = sunrise.getUTCMinutes(); // 4 → 07:04 local time
71
+ import { formatInZone } from 'panchang-ts';
72
+ const noon = new Date((result!.sun.rise.getTime() + result!.sun.set.getTime()) / 2);
73
+ formatInZone(noon, result!.timezone.offsetMinutes); // "2025-01-14T12:27:31.086+05:30"
50
74
  ```
51
75
 
52
- `moonrise` / `moonset` can be `null` the Moon occasionally does not rise or
53
- set on a given calendar day, which is normal.
76
+ > **Changed in v5 this is the breaking change most likely to affect you.**
77
+ > Through 4.x every published `Date` was the true instant *shifted* by the UTC
78
+ > offset, and the README told you to read it back with `getUTC*`. That worked
79
+ > only as long as you did nothing else with the value: `JSON.stringify` emitted
80
+ > a wrong instant labelled `Z`, `Intl` with a `timeZone` rendered 12:39 pm for
81
+ > an 07:09 am sunrise, and any comparison, diff, database write, date-fns or
82
+ > Temporal call was off by the offset.
83
+ >
84
+ > Migration is mechanical: `x.getUTCHours()` → read `xLocal`, or format the
85
+ > instant. See [Upgrading from 4.x](#upgrading-from-4x).
86
+
87
+ `moon.rise` / `moon.set` can be `null` — the Moon occasionally does not rise or
88
+ set on a given calendar day, which is normal. `moon.riseLocal` / `moon.setLocal`
89
+ are `null` exactly when they are.
90
+
91
+ ### The result is grouped
92
+
93
+ `DailyPanchangResult` has seven groups plus a handful of top-level fields.
94
+ Through 4.x it was ~50 flat fields; the groups are what tell you where to look.
95
+
96
+ | Group | Holds |
97
+ |---|---|
98
+ | `sun` | `rise` / `set` / `nextRise` (+ `*Local`), day and night lengths, the Sun's `siderealLongitude` and `nakshatra` |
99
+ | `moon` | `rise` / `set` (+ `*Local`), the Moon's `siderealLongitude` and `rashi` |
100
+ | `angas` | the five limbs — `tithis`, `nakshatras`, `yogas`, `karanas`, `vara` |
101
+ | `calendar` | `masa` (solar), `chandramasa` (lunar), `samvat` |
102
+ | `muhurtas` | `abhijit`, `brahma`, `vijaya`, `godhuli`, `nishita`, `amritKala`, `madhyahna`, `pratahSandhya`, `sayahnaSandhya`, `doGhati` |
103
+ | `inauspicious` | `rahuKalam`, `gulikaKalam`, `yamaganda`, `durMuhurta`, `varjyam`, `bhadra`, `gandaMula`, `panchaka`, `panchakaInfo`, `panchakaRahita` |
104
+ | `periods` | `choghadiya`, `hora`, `gowri` |
105
+
106
+ Top level: `date`, `location`, `timezone`, `ayanamsa`, `specialYogas`,
107
+ `anandadiYoga`, `festivals`, `eclipse`, `chandraBalam`, `tarabala`.
108
+
109
+ **Nothing is optional.** Every field is always present. A value that does not
110
+ apply is `null`; a collection that does not apply is `[]`. That holds whether
111
+ the reason is the domain (no Bhadra window today) or your options (you did not
112
+ pass `janmaRashi`, so `chandraBalam` is `null`) — the result *shape* never
113
+ depends on what you passed.
114
+
115
+ `getInstantPanchang` uses the same group names for the subset an instant can
116
+ answer: `sun`, `moon`, `angas`, `calendar`, `inauspicious`. There is no
117
+ `muhurtas` or `periods`, because those are properties of a Hindu *day*.
54
118
 
55
119
  ### `getDailyPanchang` vs `getInstantPanchang`
56
120
 
@@ -66,6 +130,327 @@ Ekadashi split. For reliable festival dating, use `getDailyPanchang`.
66
130
 
67
131
  ---
68
132
 
133
+ ## Upgrading from 4.x
134
+
135
+ Three changes move numbers that 4.x produced, and one option is gone.
136
+
137
+ ### Lahiri ayanamsa corrected by +38″
138
+
139
+ The library's Lahiri constant sat 38 arcseconds behind DrikPanchang's — it used
140
+ `23.853211°` at J2000 (the widely-repeated 23° 51′ 11.6″ figure) where Drik
141
+ computes `23.863801°`. The replacement was solved from Drik's own published
142
+ values across 1950–2050, which agree on it to within 0.01″ — a century-wide
143
+ baseline, so the precession polynomial is pinned too, not just the epoch
144
+ constant. Every sidereal output moves with it:
145
+
146
+ | Output | Effect |
147
+ |---|---|
148
+ | Nakshatra end-times | ~69 s later than 4.x (carries the ayanamsa once) |
149
+ | Yoga end-times | ~129 s later than 4.x (carries it twice) |
150
+ | Planetary longitudes, rashi, pada, lagna, divisionals, dashas | shifted +0.0106° |
151
+ | Tithi / karana end-times | unchanged — Moon − Sun cancels the ayanamsa |
152
+ | Raman / KP / True Chitra / Thirukanitham | moved by the same +38″; their offsets from Lahiri are preserved |
153
+
154
+ Worst-case end-time drift vs Drik dropped from 131 s to 60 s, and the sign split
155
+ by ayanamsa exposure — nakshatra and yoga early, tithi and karana late — is gone.
156
+ If you have snapshot tests or cached charts from 4.x, expect them to need
157
+ re-pinning.
158
+
159
+ ### ΔT now uses measurement, so every published time moves ~6 s
160
+
161
+ 4.x took ΔT (TT − UT) entirely from Espenak–Meeus. Its post-2005 branches are an
162
+ extrapolation published in 2006, and Earth's rotation did not follow it — by
163
+ 2026 the model reads about **5.9 s high**, drifting a further ~0.6 s each year.
164
+ Because this library reports *times*, that lands directly on published values.
165
+
166
+ v5 takes ΔT from the leap-second chain (`32.184 + (TAI − UTC)`, exact, and
167
+ within the 0.9 s band leap seconds maintain) wherever ΔT has actually been
168
+ measured, and resumes Espenak–Meeus beyond it carrying the offset it had
169
+ accrued. Against JPL Horizons the measured era now agrees to **0.005 s** at
170
+ every decade from 1980, where 4.x was seconds out.
171
+
172
+ | Output | Effect |
173
+ |---|---|
174
+ | Tithi / nakshatra / yoga / karana end-times | ~5.7 s later for 2025 dates, growing with the model's drift |
175
+ | Sankranti and other transit instants | same shift — it is one uniform correction, not per-element |
176
+ | Sunrise / sunset / moonrise / moonset | barely moved — the error scales against the 15°/hr sky rotation |
177
+ | Dates a panchang element is *filed under* | unchanged except where a transit sits within seconds of sunrise |
178
+
179
+ That last row is the one to know about. `computeSankrantisForYear` publishes a
180
+ date, and the date is decided by whether the transit precedes sunrise. The 2025
181
+ Tula Sankranti at Reykjavik is such a case: it still falls on Oct 16, but its
182
+ margin narrowed from 7.1 s to 1.4 s. Locations at high latitude with a transit
183
+ near sunrise are where a day could flip.
184
+
185
+ ### Instant-mode vara was wrong after ~19:00
186
+
187
+ `getInstantPanchang` located sunrise by searching forward from `date − 12 h`.
188
+ For an evening instant that start point is already past the morning's sunrise,
189
+ so it found *tomorrow's* and rolled the weekday back a day. Any query after
190
+ roughly 7 pm returned the previous vara — and with it the wrong Rahu Kalam,
191
+ Gulika Kalam, Yamaganda, Choghadiya, Hora, Anandadi yoga and special yogas.
192
+ `getDailyPanchang` was never affected. If you cached instant-mode results from
193
+ 4.x for evening timestamps, discard them.
194
+
195
+ ### `precision` removed
196
+
197
+ `precision: 'standard' | 'high'` and the `Precision` type no longer exist.
198
+ Element transitions are now solved by secant iteration, which converges to the
199
+ root rather than stopping at a fixed tolerance, so there is nothing left for the
200
+ option to select — and the tighter setting no longer buys anything. Removing it
201
+ from your options object is the whole migration; leaving it in is a type error,
202
+ not a silent no-op.
203
+
204
+ ### Sunrise is single-valued per location-day
205
+
206
+ Solar rise/set is computed from a canonical anchor and cached per location-day,
207
+ so it no longer depends on which instant the caller happened to start searching
208
+ from. Values shift by ≤108 ms vs 4.x, and two calls for the same day now agree
209
+ exactly instead of differing by up to 109 ms. Windows derived proportionally
210
+ from the day length — Varjyam, Bhadra, the slot systems — move by a little more
211
+ than that. This removes an inconsistency rather than introducing an
212
+ approximation: 4.x returned a different sunrise depending on which caller asked.
213
+
214
+ ### Moonrise and moonset are single-valued per location-day
215
+
216
+ The same treatment sunrise received, now applied to the Moon. `getMoonrise` /
217
+ `getMoonset` resolve through a canonical per-UTC-day cache, so an event has one
218
+ timestamp no matter which caller asks or from which instant they searched.
219
+ Published `moon.rise` / `moon.set` shift by **≤182 ms** vs 4.x. Nothing else in the
220
+ result moves — verified over 11,520 daily results across six locations and three
221
+ centuries: zero changes to any index, name, boolean, festival date or other
222
+ timestamp.
223
+
224
+ ### `read*` for tables, `compute*` for the engine
225
+
226
+ `getFestivalsForYear` read a pre-built table; `getFestivalsInRange` ran the
227
+ engine. Two near-identical names, completely different inputs and semantics. v5
228
+ settles one convention across all four families — festivals, eclipses, moon
229
+ phases and muhurta:
230
+
231
+ | 4.x | v5 | what it does |
232
+ |---|---|---|
233
+ | `getFestivalsForYear` | `readFestivalsForYear` | reads a table |
234
+ | `getFestivalsForDate` | `readFestivalsForDate` | reads a table |
235
+ | `getFestivalsYearRange` | `readFestivalsYearRange` | reads a table |
236
+ | `getEclipsesForYear` / `ForDate` / `YearRange` | `readEclipsesForYear` / … | reads a table |
237
+ | `getMoonPhasesForYear` / `ForDate` / `YearRange` | `readMoonPhasesForYear` / … | reads a table |
238
+ | — | `readMuhurtaForYear` / `ForDate` / `YearRange` | **new** — reads a table |
239
+ | — | `readBestMuhurtaDays` | **new** — top-scoring days |
240
+ | `getFestivalsInRange` | `computeFestivalsInRange` | runs the engine |
241
+ | `getEclipsesInRange` | `computeEclipsesInRange` | runs the engine |
242
+ | `getMoonPhasesInRange` | `computeMoonPhasesInRange` | runs the engine |
243
+ | `findAuspiciousDates` | `computeAuspiciousDatesInRange` | runs the engine |
244
+ | `getEkadashiDatesForYear` | `computeEkadashiDatesForYear` | runs the engine |
245
+ | `getSankrantisForYear` | `computeSankrantisForYear` | runs the engine |
246
+
247
+ **Every 4.x name still works** — they are deprecated aliases pointing at the same
248
+ functions, kept through v5. Nothing breaks today; the old names will go in v6.
249
+
250
+ New single-year entry points, the shape most callers reach for first:
251
+
252
+ ```ts
253
+ computeFestivalsForYear(2027, location, { timezone: 330 });
254
+ computeEclipsesForYear(2027, location, { timezone: 330 });
255
+ computeMoonPhasesForYear(2027, { timezone: 330 });
256
+ computeAuspiciousDatesForYear(2027, vivahRule, location, { timezone: 330 });
257
+ ```
258
+
259
+ ### `buildMuhurtaTable` + the `panchang-ts/muhurta` subpath
260
+
261
+ Festivals, eclipses and moon phases each had a builder and an engine-free
262
+ reader; muhurta had neither, so finding auspicious dates meant running the full
263
+ engine on device for every query. v5 completes the family:
264
+
265
+ ```ts
266
+ // build once (build time, or first launch), then persist the JSON
267
+ import { buildMuhurtaTable, vivahRule } from 'panchang-ts';
268
+ const table = buildMuhurtaTable({
269
+ rule: vivahRule, location: DELHI, timezoneOffsetMinutes: 330,
270
+ startYear: 2026, endYear: 2031,
271
+ });
272
+
273
+ // read it back with no astronomy code in the bundle (~1.7 KB)
274
+ import { readBestMuhurtaDays } from 'panchang-ts/muhurta';
275
+ readBestMuhurtaDays(table, 5); // top 5 days, highest score first
276
+ ```
277
+
278
+ Only days that pass the rule are stored unless you pass `includeFailures: true`.
279
+
280
+ ### Built tables are dictionary-encoded and carry `key`
281
+
282
+ `build*Table` now emits a `_dict` of unique entries with each day holding
283
+ indices, rather than repeating every localized string at every occurrence. A
284
+ 10-year festival table goes from 315 KB to **89.9 KB (28.5%)**; a 10-year
285
+ moon-phase table from 106 KB to **29.7 KB (28.0%)**. Resolved output is
286
+ identical — verified across every year and both locales.
287
+
288
+ Gzip already hid most of this on the wire, so the win is **parse time and
289
+ resident memory**, which is the constraint that actually bites on Hermes.
290
+
291
+ Festival table entries also gained the stable `key` the engine has been
292
+ returning since 4.x, so a table is no longer both larger *and* less useful than
293
+ engine output.
294
+
295
+ **Tables you already cached still read.** The reader detects the format, so a v1
296
+ table built with 4.x keeps working; only `key` is unavailable from it, and comes
297
+ back as `''`.
298
+
299
+ ### Published `Date`s are real instants — the flagship change
300
+
301
+ ```diff
302
+ - result.sunrise.getTime() // NOT when sunrise happened (off by the UTC offset)
303
+ - result.sunrise.getUTCHours() // the documented 4.x idiom
304
+ + result.sun.rise.getTime() // correct epoch ms
305
+ + result.sun.riseLocal // "2025-01-14T07:09:44.172+05:30"
306
+ ```
307
+
308
+ Applies to **every** `Date` in `DailyPanchangResult` and to every `TimePeriod` —
309
+ `sun.rise`, `sun.set`, `sun.nextRise`, `moon.rise`, `moon.set`, all the muhurtas
310
+ and inauspicious periods, all four slot systems, the element
311
+ `startTime`/`endTime` arrays, the eclipse contacts and sutak window. Each gains
312
+ a `*Local` companion: `sun.riseLocal`, `inauspicious.rahuKalam.startLocal`,
313
+ `angas.tithis[0].endTimeLocal`, and so on.
314
+
315
+ | you had | you now write |
316
+ |---|---|
317
+ | `r.sun.rise.getUTCHours()` | `r.sun.riseLocal.slice(11, 13)` |
318
+ | `` `${h}:${m}` `` from `getUTC*` | `r.sun.riseLocal.slice(11, 16)` |
319
+ | `r.inauspicious.rahuKalam.start.getUTCHours()` | `r.inauspicious.rahuKalam.startLocal.slice(11, 13)` |
320
+ | `r.angas.tithis[0].endTime` for display | `r.angas.tithis[0].endTimeLocal` |
321
+ | a `Date` you derived yourself | `formatInZone(d, r.timezone.offsetMinutes)` |
322
+
323
+ `getInstantPanchang` results carry **no** `*Local` fields: that call takes no
324
+ timezone, so there is no zone to render a wall clock in.
325
+
326
+ **Cost:** rendering the strings adds ~0.04 ms per daily panchang — invisible on
327
+ a cold call and ~20% of a fully cached warm one. The release warms to
328
+ **0.17 ms**, against published 4.3.1's **6.20 ms**.
329
+
330
+ ### `result.timezone` is now an object
331
+
332
+ ```diff
333
+ - result.timezone // 330
334
+ + result.timezone // { offsetMinutes: 330, zone: 'Asia/Kolkata' }
335
+ + result.timezone.offsetMinutes
336
+ ```
337
+
338
+ `options.timezone` has always accepted `number | string`, but the result carried
339
+ only a number — so passing `'America/New_York'` produced a result that could not
340
+ say which zone produced it. `zone` is present only when you passed a zone name.
341
+
342
+ **The DST limit, now stated explicitly.** The offset is resolved **once per
343
+ call** from a reference date, so a Hindu day containing a DST transition is
344
+ computed at a single offset throughout. Correct for almost every day; on the one
345
+ or two transition days a year, times after the jump are shifted by its size.
346
+ 4.x documented this as a blanket "DST resolves automatically", which was not the
347
+ whole truth.
348
+
349
+ ### The result object is grouped
350
+
351
+ `DailyPanchangResult` had ~50 flat top-level fields mixing five categories.
352
+ v5 sorts them into seven groups — see [The result is grouped](#the-result-is-grouped)
353
+ for the full table. This is a large break, and it lands in the same release as
354
+ the `Date` change on purpose: migrating both at once is one pass over your read
355
+ sites, not two.
356
+
357
+ Every rename, in full:
358
+
359
+ | 4.x | v5 |
360
+ |---|---|
361
+ | `sunrise` / `sunset` / `nextSunrise` | `sun.rise` / `sun.set` / `sun.nextRise` |
362
+ | `sunriseLocal` / `sunsetLocal` / `nextSunriseLocal` | `sun.riseLocal` / `sun.setLocal` / `sun.nextRiseLocal` |
363
+ | `dayDurationMinutes` / `nightDurationMinutes` | `sun.dayDurationMinutes` / `sun.nightDurationMinutes` |
364
+ | `dinamanaMinutes` / `ratrimanaMinutes` | `sun.dinamanaMinutes` / `sun.ratrimanaMinutes` |
365
+ | `siderealSunAtSunrise` | `sun.siderealLongitude` |
366
+ | `suryaNakshatra` | `sun.nakshatra` |
367
+ | `moonrise` / `moonset` | `moon.rise` / `moon.set` |
368
+ | `moonriseLocal` / `moonsetLocal` | `moon.riseLocal` / `moon.setLocal` |
369
+ | `siderealMoonAtSunrise` | `moon.siderealLongitude` |
370
+ | `chandraRashi` | `moon.rashi` |
371
+ | `tithis` / `nakshatras` / `yogas` / `karanas` / `vara` | `angas.*` (same names) |
372
+ | `masa` / `chandramasa` / `samvat` | `calendar.*` (same names) |
373
+ | `abhijitMuhurta` / `brahmaMuhurta` / `vijayaMuhurta` | `muhurtas.abhijit` / `muhurtas.brahma` / `muhurtas.vijaya` |
374
+ | `godhuliMuhurta` / `nishitaMuhurta` | `muhurtas.godhuli` / `muhurtas.nishita` |
375
+ | `amritKala` / `madhyahna` / `pratahSandhya` / `sayahnaSandhya` | `muhurtas.*` (same names) |
376
+ | `doGhatiMuhurta` | `muhurtas.doGhati` |
377
+ | `rahuKalam` / `gulikaKalam` / `yamaganda` / `durMuhurta` | `inauspicious.*` (same names) |
378
+ | `varjyam` / `bhadra` / `gandaMula` / `panchaka` / `panchakaRahita` | `inauspicious.*` (same names) |
379
+ | `choghadiya` / `hora` | `periods.choghadiya` / `periods.hora` |
380
+ | `gowriPanchangam` | `periods.gowri` |
381
+ | `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 |
382
+
383
+ Unmoved: `date`, `location`, `timezone`, `ayanamsa`, `specialYogas`,
384
+ `anandadiYoga`, `festivals`, `eclipse`, `chandraBalam`, `tarabala`.
385
+
386
+ For `getInstantPanchang`: `tithi` / `nakshatra` / `yoga` / `karana` / `vara` →
387
+ `angas.*`; `siderealSun` → `sun.siderealLongitude`; `siderealMoon` →
388
+ `moon.siderealLongitude`; `suryaNakshatra` → `sun.nakshatra`; `chandraRashi` →
389
+ `moon.rashi`; `chandramasa` / `samvat` → `calendar.*`; `panchaka` / `gandaMula`
390
+ → `inauspicious.*`.
391
+
392
+ ### One rule for "not applicable": always present, `null` or `[]`
393
+
394
+ 4.x used three conventions and you could not predict which you would get —
395
+ `| null` for `bhadra` / `varjyam` / `eclipse`, `?`-optional for `chandraBalam` /
396
+ `tarabala`, and an empty array for `panchakaRahita` / `festivals`. v5 has one
397
+ rule: **every field is always present**, a value that does not apply is `null`,
398
+ and a collection that does not apply is `[]`.
399
+
400
+ ```diff
401
+ - if ('chandraBalam' in r) … // 4.x: field absent without janmaRashi
402
+ - r.chandraBalam!.house // and the `!` was mandatory
403
+ + if (r.chandraBalam !== null) … // v5: always present, null when unasked
404
+ + r.chandraBalam?.house
405
+ ```
406
+
407
+ Only `chandraBalam` and `tarabala` changed behaviour; everything else already
408
+ followed the rule. `toBeUndefined()`-style checks against them become
409
+ `toBeNull()`.
410
+
411
+ ### `suryaNakshatra` is typed as a nakshatra, not a rashi
412
+
413
+ Now published as `sun.nakshatra`. Its `index` has always been 0..26 (Ashwini …
414
+ Revati). Its *type* said `RashiInfo`, documented "0 = Mesha … 11 = Meena", so
415
+ anyone indexing a 12-element rashi array by it got silent garbage for two thirds
416
+ of the year. The runtime value is unchanged; the type is now
417
+ `NakshatraIndexInfo` and TypeScript will point at the misuse.
418
+
419
+ ### `_debug` removed
420
+
421
+ `DailyPanchangResult._debug` was declared in the published type and written
422
+ nowhere in the library. It never carried data. If you referenced it, it was
423
+ always `undefined`.
424
+
425
+ ### Alias fields documented rather than removed
426
+
427
+ `sun.dinamanaMinutes` / `sun.dayDurationMinutes` and `sun.ratrimanaMinutes` /
428
+ `sun.nightDurationMinutes` are the same numbers under classical and English
429
+ names. Both pairs stay — consumers use both vocabularies — and the types now say
430
+ plainly that they are aliases, never independently computed.
431
+
432
+ `calendar.chandramasa` keeps its casing beside `moon.rashi` and `sun.nakshatra`.
433
+ Renaming it would break every consumer for a casing preference. The type now
434
+ documents that `calendar.masa` is the **solar** month and `calendar.chandramasa`
435
+ the **lunar** one, which was previously left to guesswork.
436
+
437
+ ### Additive, but worth knowing
438
+
439
+ - `EclipseInfo` / `EclipseSubtype` are now exported. 4.x shipped
440
+ `getUpcomingLunarEclipse` and friends without the type they return.
441
+ - `festivals[].key` — stable, language-independent festival id. Match on this,
442
+ never on `name`.
443
+ - `bhadra.locationName` — localized display name; `bhadra.location` stays the
444
+ machine-readable key.
445
+ - `MuhurtaScore.factors` — structured scoring inputs alongside English `reasons`.
446
+ - `BirthChart.byPlanet` — the nine placements keyed by graha.
447
+ - `eclipse.description` is now localized. Under `language: 'hi'` it was
448
+ previously emitted in English, including inside `festivals[].description`.
449
+ - `sections` on `getDailyPanchang` — opt into a narrower, cheaper call. See
450
+ [Performance](#performance).
451
+
452
+ ---
453
+
69
454
  ## Features at a Glance
70
455
 
71
456
  | Category | Features |
@@ -104,991 +489,221 @@ Ekadashi split. For reliable festival dating, use `getDailyPanchang`.
104
489
 
105
490
  # Feature Reference
106
491
 
107
- Each daily field is also returned from `getDailyPanchang` if you prefer one call
108
- over per-feature helpers.
492
+ A quick tour with runnable snippets. **Each section links to its full page on
493
+ the docs site** — every option, field, and caveat lives there.
494
+
495
+ ## Pancha Anga & the daily result
109
496
 
110
- ## Pancha Anga
497
+ 📖 [Daily Panchang →](https://dharmagya.app/docs/panchang-ts/daily-panchang)
111
498
 
112
499
  ```typescript
113
500
  const r = getDailyPanchang(date, location, { timezone: 330 })!;
114
501
 
115
- r.tithis.forEach(t => console.log(t.name, t.paksha, t.completionPercentage, t.endTime));
116
- r.nakshatras.forEach(n => console.log(n.name, n.pada, n.endTime));
117
- r.yogas.forEach(y => console.log(y.name, y.endTime));
118
- r.karanas.forEach(k => console.log(k.name, k.type, k.endTime));
119
- console.log(r.vara.name, r.vara.englishName); // "Mangalawara", "Tuesday"
502
+ r.angas.tithis.forEach(t => console.log(t.name, t.paksha, t.endTime));
503
+ r.angas.vara.name; // "Mangalawara"
504
+ r.calendar.chandramasa.isAdhika; // true during leap months
505
+ r.muhurtas.brahma; // TimePeriod | null — and 9 more muhurtas
506
+ r.inauspicious.rahuKalam; // { start, end } — and 9 more windows
507
+ r.periods.choghadiya.day[0].name; // 16 Choghadiya + Gowri + 24 Hora slots
508
+ r.anandadiYoga.name; r.specialYogas; // Anandadi + Amrit/Sarvartha Siddhi, …
120
509
 
121
510
  // Single-instant snapshot:
122
511
  import { getInstantPanchang } from 'panchang-ts';
123
512
  const i = getInstantPanchang(new Date(), location)!;
124
- console.log(i.tithi.name, i.nakshatra.name, i.yoga.name, i.karana.name, i.vara.name);
125
- ```
126
-
127
- ## Lunar & Solar Calendar
128
-
129
- ```typescript
130
- const r = getDailyPanchang(date, loc, { timezone: 330, masaSystem: 'purnimanta' })!;
131
-
132
- r.chandramasa.name; // active system (default: Purnimanta / North Indian)
133
- r.chandramasa.amantaName; // South Indian
134
- r.chandramasa.purnimantaName; // North Indian
135
- r.chandramasa.isAdhika; // true during leap months
136
- r.samvat.vikramSamvat; // 2081
137
- r.samvat.shakaSamvat; // 1946
138
-
139
- r.masa.name; // current solar month (Mesha … Meena)
140
- r.suryaNakshatra.name; // Sun's nakshatra
141
- r.chandraRashi.name; // Moon sign
142
- ```
143
-
144
- ## Sun, Moon & Muhurta
145
-
146
- ```typescript
147
- import { getSunrise, getSunset, getMoonrise, getMoonset } from 'panchang-ts';
148
-
149
- const sunrise = getSunrise(localMidnightUtc, loc);
150
- const sunset = getSunset(sunrise, loc);
151
- const moonrise = getMoonrise(localMidnightUtc, loc); // null on some days (normal)
152
- const moonset = getMoonset(localMidnightUtc, loc);
153
-
154
- // Or read off the daily result:
155
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
156
- r.sunrise; r.sunset; r.moonrise; r.moonset; r.nextSunrise;
157
- r.dayDurationMinutes; r.nightDurationMinutes;
158
-
159
- // Auspicious muhurtas
160
- r.brahmaMuhurta; // two muhurtas before sunrise
161
- r.abhijitMuhurta; // 8th day-muhurta; null on Wednesday (Drik convention)
162
- r.vijayaMuhurta; // 11th day-muhurta
163
- r.godhuliMuhurta; // "cow-dust" sunset muhurta
164
- r.nishitaMuhurta; // midnight muhurta (Shivaratri)
165
- r.madhyahna; // solar noon ±24 min
166
- r.pratahSandhya; // dawn twilight, ends at sunrise
167
- r.sayahnaSandhya; // dusk twilight, starts at sunset
168
- r.amritKala; // nakshatra-specific window (null when nakshatra has none)
169
- ```
170
-
171
- `pratahSandhya` / `sayahnaSandhya` width = `nightDuration / 10` (~62–81 min).
172
-
173
- ## Inauspicious Periods
174
-
175
- ```typescript
176
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
177
-
178
- r.rahuKalam; // { start, end }
179
- r.gulikaKalam;
180
- r.yamaganda;
181
- r.durMuhurta; // two ~48-min windows
182
- r.varjyam; // { start, end } | null
183
- r.gandaMula; // { active, severity: 'mild'|'severe'|null, ... }
184
- r.bhadra; // { start, end, location: 'earth'|'heaven'|'paatal', isActive } | null
185
- r.panchaka; // boolean — Moon in last 5 nakshatras
186
- ```
187
-
188
- ## Time-Slot Systems
189
-
190
- ```typescript
191
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
192
-
193
- // Choghadiya — 8 day + 8 night named, rated slots (Amrit, Kaal, Shubh, Rog, …)
194
- r.choghadiya.day.forEach(s => console.log(s.name, s.qualityName, s.start, s.end));
195
-
196
- // Gowri Panchangam ("Nalla Neram") — 8 day + 8 night Tamil slots
197
- r.gowriPanchangam.day.forEach(s => console.log(s.name, s.qualityName));
198
-
199
- // Hora — 12 day + 12 night planetary hours (Chaldean order)
200
- r.hora.day.forEach(h => console.log(h.planet, h.start, h.end));
201
-
202
- // Do Ghati Muhurta — 15 day + 15 night ~48-min deity-keyed slots (no vara rotation)
203
- r.doGhatiMuhurta.day.forEach(g => console.log(g.name, g.start, g.end));
204
-
205
- // Panchaka Rahita — slices of the day FREE of Panchaka ([] when it pervades)
206
- r.panchakaRahita.forEach(slice => console.log(slice.start, slice.end));
207
- ```
208
-
209
- ## Special Yogas
210
-
211
- ```typescript
212
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
213
-
214
- r.anandadiYoga.name; // 28-cycle name e.g. "Ananda"
215
- r.specialYogas.forEach(y => {
216
- // type: amrit_siddhi | sarvartha_siddhi | ravi_pushya | guru_pushya
217
- // | dwipushkar | tripushkar | jwalamukhi (inauspicious)
218
- // | aadal | vidaal | ravi (Moon-from-Sun nakshatra-distance rules)
219
- console.log(y.name, y.type);
220
- });
513
+ console.log(i.angas.tithi.name, i.angas.nakshatra.name);
221
514
  ```
222
515
 
223
516
  ## Festivals (80+)
224
517
 
225
- Covers Ekadashi (26 variants, Smarta/Vaishnava split via Dashami-viddha; Smarta
226
- fast emits a `deferralDate` for Dwadashi), Pradosha, Sankranti + regional
227
- variants (Pongal, Vishu, Baisakhi, Pohela Boishakh, Bihu, Uttarayan, Lohri…),
228
- canonical-time classical (Janmashtami, Shivaratri, Ganesh Chaturthi, Diwali,
229
- Holi, Raksha Bandhan — Bhadra-aware, Karva Chauth, Akshaya Tritiya…),
230
- regional (Gudi Padwa, Gangaur, Teej variants, Onam, Chhath…), monthly
231
- observances (Masik Shivaratri, Pushya days, Shravan Somvar…).
232
-
233
- ```typescript
234
- r.festivals.forEach(f => {
235
- // type: major | minor | ekadashi | smarta_ekadashi | vaishnava_ekadashi
236
- // | pradosha | sankranti | eclipse
237
- console.log(f.name, f.type, f.deferralDate);
238
- });
239
- ```
240
-
241
- ### Regional scoping
242
-
243
- `region` scopes regional variants to one Indian state. Pan-Indian festivals
244
- emit regardless.
518
+ 📖 [Festivals →](https://dharmagya.app/docs/panchang-ts/festivals)
245
519
 
246
520
  ```typescript
247
- // All regional variants (default):
248
- getDailyPanchang(jan14, chennai, { timezone: 330 })!.festivals.map(f => f.name);
249
- // ["Sankranti","Makar Sankranti","Pongal","Uttarayan","Magh Bihu","Ayyappa Makara Jyothi"]
250
-
251
- // Tamil Nadu only:
252
- getDailyPanchang(jan14, chennai, { timezone: 330, region: 'tamil-nadu' })!
253
- .festivals.map(f => f.name);
254
- // → ["Sankranti","Makar Sankranti","Pongal"]
255
-
256
- // Lohri fires on the Hindu day BEFORE Makara transit, in Punjab/Haryana/Himachal scope:
257
- getDailyPanchang(jan13, amritsar, { timezone: 330, region: 'punjab' })!
258
- .festivals.some(f => f.name === 'Lohri'); // true
259
- ```
260
-
261
- `FestivalRegion` covers 21 Indian states + `'nepal'` + `'all'` (default). The
262
- legacy slugs `'tamil'`, `'bengal'`, `'north-india'` are still accepted and
263
- mapped internally.
264
-
265
- ### Pre-computed table (bundled, India / IST)
521
+ r.festivals.forEach(f => console.log(f.key, f.name, f.type, f.deferralDate));
522
+ // `name` is localized, so match on `key` never on `name`:
523
+ const hasDiwali = r.festivals.some(f => f.key === 'diwali');
266
524
 
267
- If you want festival *dates* without running the engine, import the static
268
- table at `panchang-ts/festivals`. It bundles a rolling **2-years-past /
269
- 5-years-future** window pre-computed against Varanasi (IST). Within India
270
- these dates are essentially universal.
525
+ // Scope regional variants: 21 state slugs + 'nepal' + 'all' (default)
526
+ getDailyPanchang(jan14, chennai, { timezone: 330, region: 'tamil-nadu' });
271
527
 
272
- ```typescript
273
- import {
274
- getFestivalsForYear,
275
- getFestivalsForDate,
276
- FESTIVALS_META,
277
- FESTIVALS_YEAR_RANGE,
278
- } from 'panchang-ts/festivals';
279
-
280
- const yr = FESTIVALS_YEAR_RANGE.start; // e.g. { start: 2024, end: 2031 }
281
- getFestivalsForYear(yr)!.length; // ~150 festival days
282
- const diwali = getFestivalsForYear(yr)!
283
- .find(d => d.festivals.some(f => f.name === 'Diwali'))!.date;
284
- getFestivalsForDate(diwali); // [Narak Chaturdashi, Diwali]
285
- getFestivalsForDate(diwali, 'hi'); // [नरक चतुर्दशी, दिवाली]
286
- FESTIVALS_META.referenceLocation; // "Varanasi"
287
- FESTIVALS_META.languages; // ["en", "hi"]
288
- ```
289
-
290
- This entry point is engine-free — it ships only the JSON + accessors, so
291
- importing it won't pull the calculation engine into your bundle. Both `en`
292
- and `hi` are bundled (names *and* descriptions); pass the locale as the
293
- second argument. Eclipses are excluded here (visibility is location-dependent)
294
- — they ship as their own bundled table at `panchang-ts/eclipses` (see
295
- [Eclipses](#eclipses)).
296
-
297
- ### Festivals outside India — build a location table and cache it
298
-
299
- The bundled table is **IST-only**. Elsewhere (Europe, North America, rest
300
- of world) festival dates can shift by ±1 day, because canonical times
301
- (nishita / pradosha / chandrodaya …) are observer-dependent — and the shift
302
- tracks the timezone offset, not the "region", so a single per-continent
303
- table would mis-date boundary-day festivals.
304
-
305
- For an offline app serving users worldwide, the right pattern is
306
- **compute-once-then-cache for the user's actual location**. Build a
307
- location-specific table with `buildFestivalsTable` (from the main entry —
308
- it uses the engine), persist the returned JSON, then read it back through
309
- the same accessors via their `source` argument:
310
-
311
- ```typescript
528
+ // Pre-computed table: build once, cache the JSON, read engine-free.
312
529
  import { buildFestivalsTable } from 'panchang-ts';
313
- import { getFestivalsForYear, getFestivalsForDate } from 'panchang-ts/festivals';
314
-
315
- // On first use at the user's location (a few seconds on-device — run it in
316
- // the background / chunk by year), then cache `table` to disk/MMKV.
317
- const table = buildFestivalsTable({
318
- location: { latitude: 40.7128, longitude: -74.006 },
319
- timezoneOffsetMinutes: -300, // US Eastern (EST); 0 = UK, 330 = IST
320
- startYear: 2024,
321
- endYear: 2031,
322
- languages: ['en'], // omit hi to halve the size
323
- });
324
-
325
- // Later reads are instant lookups against the cached table:
326
- getFestivalsForYear(2026, 'en', table);
327
- getFestivalsForDate('2026-11-08', 'en', table); // key is in the table's tz
530
+ import { readFestivalsForYear, readFestivalsForDate } from 'panchang-ts/festivals';
328
531
  ```
329
532
 
330
- `buildFestivalsTable` returns the same `FestivalsFile` shape as the bundled
331
- data, so a cached table and the bundled India table are interchangeable as
332
- the `source` argument. India-majority apps can lean on the bundled table for
333
- zero first-load latency and only compute-and-cache for non-IST users.
533
+ No table ships with the package festival dates are observer-dependent, so you
534
+ build one for your users' location and years (`npm run festivals:gen` is a
535
+ worked example).
334
536
 
335
- **Other notes:** Karva Chauth / Dhanteras / Diwali emit with Purnimanta
336
- paksha naming. To regenerate the bundled India table after a registry
337
- change, run `npm run festivals:gen` (rolling window, no constants to edit).
537
+ ## Eclipses & Moon Phases
338
538
 
339
- ## Eclipses
539
+ 📖 [Eclipses & Moon Phases →](https://dharmagya.app/docs/panchang-ts/eclipses-moon-phases)
340
540
 
341
541
  ```typescript
342
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
343
542
  if (r.eclipse) {
344
- r.eclipse.kind; // 'solar' | 'lunar'
345
- r.eclipse.subtype; // 'partial' | 'total' | 'annular' | 'penumbral'
346
- r.eclipse.magnitude; // 0..1 fraction obscured at peak
347
- r.eclipse.visibleFromLocation; // body above horizon at peak?
348
- r.eclipse.start; r.eclipse.peak; r.eclipse.end;
543
+ r.eclipse.kind; r.eclipse.subtype; // 'solar'|'lunar', 'partial'|'total'|…
544
+ r.eclipse.obscuration; // disc AREA covered, 0..1
545
+ r.eclipse.magnitude; // catalogue DIAMETER fraction
349
546
  r.eclipse.sutakStart; r.eclipse.sutakEnd;
350
- // Sutak: 12 h (4 prahara) before solar, 9 h (3 prahara) before lunar
351
547
  }
352
548
 
353
- import { getUpcomingSolarEclipse, getUpcomingLunarEclipse } from 'panchang-ts';
354
- const next = getUpcomingSolarEclipse(new Date(), loc, 365 /* days */);
355
- ```
356
-
357
- ### Pre-computed table (bundled, India / IST)
358
-
359
- Like the festivals table, eclipse data ships as a static, engine-free entry
360
- at `panchang-ts/eclipses` — a rolling **2-years-past / 5-years-future** window
361
- pre-computed against Varanasi (IST). It lists every eclipse **visible from
362
- there during any phase** (so an eclipse already in progress at moon/sunrise or
363
- moon/sunset is included); the `visibleAtPeak` flag tells you whether greatest
364
- eclipse itself is observable. Within India visibility is essentially uniform.
549
+ import { getUpcomingSolarEclipse, computeMoonPhasesInRange } from 'panchang-ts';
550
+ getUpcomingSolarEclipse(new Date(), loc, 365);
551
+ computeMoonPhasesInRange(start, end); // precise new/quarter/full instants
365
552
 
366
- ```typescript
367
- import {
368
- getEclipsesForYear,
369
- getEclipsesForDate,
370
- ECLIPSES_META,
371
- ECLIPSES_YEAR_RANGE,
372
- } from 'panchang-ts/eclipses';
373
-
374
- const e = getEclipsesForYear(2025)![0].eclipses[0];
375
- e.kind; // 'lunar'
376
- e.subtype; // 'total'
377
- e.start; e.peak; e.end; // ISO UTC strings
378
- e.magnitude; // 0..1 obscuration at peak
379
- e.visibleFromLocation; // visible during any phase? (always true in bundled table)
380
- e.visibleAtPeak; // is greatest eclipse itself above the horizon?
381
- e.sutak; // { start, end } — see note below
382
- getEclipsesForDate('2025-09-07', 'hi'); // [पूर्ण चंद्र ग्रहण]
383
- ECLIPSES_META.referenceLocation; // "Varanasi"
384
- ```
385
-
386
- Each entry carries `en` + `hi` text. Solar eclipses report the subtype seen
387
- **locally** (a globally-total eclipse may read `partial` from Varanasi). The
388
- `sutak` window is present only where it applies — all visible solar eclipses
389
- and visible **umbral** (partial/total) lunar eclipses; **penumbral** lunar
390
- eclipses carry no `sutak` and are not religiously observed (drik / pandit
391
- consensus). For full astronomical detail (e.g. eclipses *not* visible in
392
- India), use `getUpcomingEclipses` / `getEclipsesInRange` from the main entry.
393
-
394
- **Outside India:** which eclipses are visible — and thus carry `sutak` —
395
- differs by location. Build and cache a location-specific table with
396
- `buildEclipsesTable` (main entry, uses the engine), then read it back via the
397
- same accessors' `source` argument — the same compute-once-then-cache pattern
398
- as festivals:
399
-
400
- ```typescript
401
- import { buildEclipsesTable } from 'panchang-ts';
402
- import { getEclipsesForYear } from 'panchang-ts/eclipses';
403
-
404
- const table = buildEclipsesTable({
405
- location: { latitude: 51.5074, longitude: -0.1278 },
406
- timezoneOffsetMinutes: 0, // UK / GMT
407
- startYear: 2024,
408
- endYear: 2031,
409
- // visibleOnly: false → also include eclipses below the horizon (no sutak)
410
- });
411
- getEclipsesForYear(2025, 'en', table);
553
+ // Engine-free tables: panchang-ts/eclipses and panchang-ts/moon-phases
412
554
  ```
413
555
 
414
- To regenerate the bundled India table, run `npm run eclipses:gen` (rolling
415
- window, no constants to edit).
416
-
417
- ## Moon Phases
556
+ ## Muhurta Engine
418
557
 
419
- The four principal lunar phases — **new** (Amavasya), **first quarter**,
420
- **full** (Purnima), **last quarter** — as precise instants. (These are the
421
- astronomical quarter moments, distinct from the same-named *tithis*, which are
422
- ~24h windows.)
558
+ 📖 [Muhurta Engine →](https://dharmagya.app/docs/panchang-ts/muhurta)
423
559
 
424
560
  ```typescript
425
- import { getMoonPhasesInRange } from 'panchang-ts';
426
- const phases = getMoonPhasesInRange(new Date('2026-01-01'), new Date('2026-12-31'));
427
- phases.forEach(p => console.log(p.phase, p.time.toISOString())); // ~49 / year
428
- ```
561
+ import { scoreMuhurta, computeAuspiciousDatesInRange, vivahRule } from 'panchang-ts';
429
562
 
430
- ### Pre-computed table (bundled, India / IST)
563
+ const s = scoreMuhurta(new Date('2026-05-12'), DELHI, vivahRule, { timezone: 330 });
564
+ s.score; // 0..100; passes when ≥ 50
565
+ s.factors; // structured, stable codes — localize/filter on these
566
+ s.reasons; // diagnostic English
431
567
 
432
- Same engine-free pattern as festivals and eclipses, at `panchang-ts/moon-phases`
433
- — a rolling **2-years-past / 5-years-future** window. Phases are global
434
- instants; the bundled table maps each onto its **IST** calendar date (so a new
435
- moon at 19:52 UTC on Jan 18 is listed under Jan 19 in India).
436
-
437
- ```typescript
438
- import {
439
- getMoonPhasesForYear,
440
- getMoonPhasesForDate,
441
- MOON_PHASES_META,
442
- MOON_PHASES_YEAR_RANGE,
443
- } from 'panchang-ts/moon-phases';
444
-
445
- getMoonPhasesForYear(2026)!.length; // ~49 phase days
446
- getMoonPhasesForDate('2026-01-03'); // [{ phase: 'full', name: 'Full Moon', ... }]
447
- getMoonPhasesForDate('2026-01-03', 'hi'); // [{ phase: 'full', name: 'पूर्णिमा', ... }]
568
+ computeAuspiciousDatesInRange(vivahRule, start, end, DELHI, { timezone: 330 });
448
569
  ```
449
570
 
450
- Each entry carries `phase`, the phase `time` (ISO UTC), and `en` + `hi` text.
451
- For another timezone, build and cache a table with `buildMoonPhasesTable` (main
452
- entry) and pass it as the accessors' `source` argument it takes only a
453
- `timezoneOffsetMinutes` (no coordinates, since phases are location-independent):
571
+ 13 stock rules (vivah, griha pravesh, namakarana, ) or your own pure-data
572
+ `MuhurtaRule`. Vara × Tithi yogas (Siddha, Amrita, Dagdha, …) are scored
573
+ jointly. Pre-compute a table with `buildMuhurtaTable` and read it back through
574
+ `panchang-ts/muhurta` (~1.7 KB, no astronomy code).
454
575
 
455
- ```typescript
456
- import { buildMoonPhasesTable } from 'panchang-ts';
457
- import { getMoonPhasesForYear } from 'panchang-ts/moon-phases';
458
-
459
- const table = buildMoonPhasesTable({
460
- timezoneOffsetMinutes: -300, // US Eastern
461
- startYear: 2024, endYear: 2031,
462
- });
463
- getMoonPhasesForYear(2026, 'en', table);
464
- ```
576
+ ## Planetary Positions & Birth Charts
465
577
 
466
- Regenerate the bundled India table with `npm run moon-phases:gen`.
467
-
468
- ## Planetary Positions
578
+ 📖 [Birth Charts →](https://dharmagya.app/docs/panchang-ts/birth-chart)
469
579
 
470
580
  ```typescript
471
- import { computePlanetaryPositions, GRAHA_ABBR } from 'panchang-ts';
581
+ import {
582
+ computePlanetaryPositions, computeLagna, computeBhava,
583
+ computeRashiChart, computeNavamsa, computeDivisionalChart, computeDignity,
584
+ } from 'panchang-ts';
472
585
 
473
586
  const g = computePlanetaryPositions(new Date(), 'lahiri');
474
- g.jupiter.rashi.name; // "Dhanu"
475
- g.jupiter.degreeInRashi; // 18.42
476
- g.jupiter.nakshatra.name; // "Purva Ashadha"
477
- g.jupiter.nakshatra.pada; // 3
478
- g.saturn.isRetrograde;
479
- GRAHA_ABBR['Jupiter']; // "Ju"
480
-
481
- // True node (sharper Rahu/Ketu via Meeus periodic correction)
482
- const gT = computePlanetaryPositions(new Date(), 'lahiri', undefined, 'true');
587
+ g.jupiter.rashi.name; g.jupiter.nakshatra.pada; g.saturn.isRetrograde;
588
+
589
+ const d1 = computeRashiChart(birth, loc); // houseSystem: whole-sign | equal | placidus-kp
590
+ d1.byPlanet.Mars.house; // keyed lookup, no linear scan
591
+ const d9 = computeNavamsa(birth, loc); // + D2/D3/D7/D10/D12/D30
592
+ computeDignity('Mars', 9); // 'exalted'
483
593
  ```
484
594
 
485
- ## Dashas
595
+ ## Dashas & Personal Transits
486
596
 
487
- Five classical systems:
597
+ 📖 [Dashas & Transits →](https://dharmagya.app/docs/panchang-ts/dashas)
488
598
 
489
599
  ```typescript
490
600
  import {
491
601
  computeVimshottariDashaFromBirth, computeVimshottariPratyantar,
492
- computeAshtottariDasha, computeYoginiDasha, computeCharaDasha, computeNarayanDasha,
602
+ computeAshtottariDasha, computeYoginiDasha, computeCharaDasha,
603
+ computeNarayanDasha, computeSadeSati,
493
604
  } from 'panchang-ts';
494
605
 
495
- // 1. Vimshottari — 120-year, 9-lord, with 3-level Maha→Antar→Pratyantar.
496
- const vim = computeVimshottariDashaFromBirth(birth, 'lahiri');
497
- const pratyantars = computeVimshottariPratyantar(vim.mahaDashas[0]!.antarDashas[0]!);
498
-
499
- // 2. Ashtottari — 108-year, 8-lord cycle (no Ketu).
500
- const ash = computeAshtottariDasha(birth, moonLon);
501
-
502
- // 3. Yogini — 36-year, 8 yoginis.
503
- const yog = computeYoginiDasha(birth, moonLon);
504
- yog.mahaDashas[0]!.yogini; // 'Dhanya'
505
- yog.mahaDashas[0]!.lord; // 'Jupiter'
506
-
507
- // 4. Chara (Jaimini) — sign-based, 9-8-7 years per modality, forward only.
508
- const cha = computeCharaDasha(birth, loc);
509
-
510
- // 5. Narayan (Jaimini) — sign-based, parity-based direction.
511
- // Vishama-pada lagna {Aries, Taurus, Gemini, Libra, Scorpio, Sag} → forward
512
- // Sama-pada lagna {Cancer, Leo, Virgo, Capricorn, Aquarius, Pisces} → backward
513
- const nar = computeNarayanDasha(birth, loc);
514
- nar.direction; // 'forward' | 'backward'
515
-
516
- // Narayan variable-duration variant (Sanjay Rath):
517
- const narV = computeNarayanDasha(birth, loc, 'lahiri', { duration: 'variable' });
518
- narV.mahaDashas[0]!.years; // 0..12 from rashi-to-lord count (+1 exalt, -1 debil)
606
+ const vim = computeVimshottariDashaFromBirth(birth, 'lahiri'); // 3-level
607
+ computeSadeSati(natalMoonRashiIndex, new Date());
608
+ // Daily transits: pass janmaRashi / janmaNakshatra to getDailyPanchang
609
+ // and read r.chandraBalam / r.tarabala.
519
610
  ```
520
611
 
521
- ## Personal Transits
612
+ ## Strength, Yogas & Karakas
522
613
 
523
- ```typescript
524
- const r = getDailyPanchang(date, loc, {
525
- timezone: 330,
526
- janmaRashi: 3, // 0 = Mesha … 11 = Meena
527
- janmaNakshatra: 0, // 0 = Ashwini … 26 = Revati
528
- })!;
529
- r.chandraBalam!; // { house, quality: 'strong'|'weak', name, englishName }
530
- r.tarabala!; // { taraIndex, name, englishName, quality }
531
-
532
- import { computeSadeSati } from 'panchang-ts';
533
- const ss = computeSadeSati(natalMoonRashiIndex, new Date());
534
- // → { active, phase: 1|2|3|null, currentArcStart, currentArcEnd, nextArcStart }
535
- ```
536
-
537
- ## Birth Chart
538
-
539
- Sidereal Lagna, Bhava under three house systems, D1 + six classical divisional
540
- charts (D2/D3/D7/D9/D10/D12/D30), and Planetary Dignity.
614
+ 📖 [Strength, Yogas & Karakas →](https://dharmagya.app/docs/panchang-ts/strength-yogas)
541
615
 
542
616
  ```typescript
543
617
  import {
544
- computeLagna, computeBhava, computeRashiChart, computeNavamsa,
545
- computeDivisionalChart, computeDignity,
618
+ computeAspects, computeShadbala, computeBhavaBala,
619
+ computeAshtakavarga, computeYogas, computeJaiminiKarakas,
546
620
  } from 'panchang-ts';
547
621
 
548
- const birth = new Date('1995-08-15T05:30:00Z');
549
- const loc = { latitude: 28.6139, longitude: 77.2090 };
550
-
551
- const lagna = computeLagna(birth, loc, 'lahiri', 'en');
552
-
553
- // Bhava — 'whole-sign' (default) | 'equal' | 'placidus-kp'.
554
- // Placidus-KP throws PanchangError('CIRCUMPOLAR') beyond ±66.5°.
555
- const houses = computeBhava(birth, loc, { houseSystem: 'whole-sign' });
556
-
557
- // D1 — full Rashi chart with 9-graha house placement.
558
- const d1 = computeRashiChart(birth, loc, { houseSystem: 'whole-sign' });
559
- d1.planets.find(p => p.planet === 'Jupiter')?.house;
560
- d1.planets.find(p => p.planet === 'Saturn')?.isRetrograde;
561
-
562
- // Divisional charts (D2 Hora, D3 Drekkana, D7 Saptamsa, D9 Navamsa,
563
- // D10 Dasamsa, D12 Dwadasamsa, D30 Trimsamsa).
564
- const d9 = computeNavamsa(birth, loc);
565
- const d10 = computeDivisionalChart(birth, loc, 'D10');
566
- const d30 = computeDivisionalChart(birth, loc, 'D30');
567
-
568
- // Planetary dignity (BPHS Ch.3-4).
569
- computeDignity('Mars', 0); // 'moolatrikona' (Aries)
570
- computeDignity('Mars', 9); // 'exalted' (Capricorn)
571
- computeDignity('Sun', 6); // 'debilitated' (Libra)
622
+ computeShadbala(birth, loc); // 6-fold, in Virupas
623
+ computeAshtakavarga(d1, { reductions: true }); // Bhinna + Sarva + Sodhana
624
+ computeYogas(d1); // ~25 named, with bhanga
625
+ computeJaiminiKarakas(d1, { variant: '8-jaimini' });
572
626
  ```
573
627
 
574
- Birth-chart helpers accept the full ayanamsa set including `'true-chitra'` and
575
- `'thirukanitham'`.
576
-
577
628
  ## Compatibility & Doshas
578
629
 
630
+ 📖 [Matching & Doshas →](https://dharmagya.app/docs/panchang-ts/matching-doshas)
631
+
579
632
  ```typescript
580
633
  import {
581
634
  computeAshtakoot, computePathuPorutham,
582
- computeMangalDosha, computeKaalSarp, computePitruDosha,
635
+ computeMangalDosha, computeMangalCompatibility, computeKaalSarp, computePitruDosha,
583
636
  } from 'panchang-ts';
584
637
 
585
- // Ashtakoot (North Indian, 36-point) Varna, Vashya, Tara, Yoni,
586
- // Graha Maitri, Gana, Bhakoot, Nadi (max 1/2/3/4/5/6/7/8).
587
- const match = computeAshtakoot(
588
- { rashi: 4, nakshatra: 9 },
589
- { rashi: 0, nakshatra: 1 },
590
- );
638
+ computeAshtakoot({ rashi: 4, nakshatra: 9 }, { rashi: 0, nakshatra: 1 });
591
639
  // → { totalScore: 0..36, koots: KootScore[8], cancellations: string[] }
592
640
 
593
- // Opt-in Bhakoot cancellations need extra natal data:
594
- // `lagnaRashi` enables same-lagna-lord + same-7th-lord rules;
595
- // `navamsaRashi` enables the same-Navamsa-lord rule.
596
- const richer = computeAshtakoot(
597
- { rashi: 4, nakshatra: 9, lagnaRashi: 7, navamsaRashi: 2 },
598
- { rashi: 0, nakshatra: 1, lagnaRashi: 1, navamsaRashi: 5 },
599
- );
600
-
601
- // Pathu Porutham (Tamil/Kerala, 10-fold) — binary pass/fail per koot.
602
- // Three vetoes (Yoni, Rajju, Vedha) flip `recommended` regardless of count.
603
- const tp = computePathuPorutham(
604
- { rashi: 4, nakshatra: 9 },
605
- { rashi: 0, nakshatra: 1 },
606
- );
607
- tp.totalPasses; // 0..10
608
- tp.recommended; // no veto + ≥5 passes
609
-
610
- // Doshas
611
- computeMangalDosha(d1);
612
- // Mars in 1/2/4/7/8/12 from Lagna, Moon, AND Venus (Drik rule set).
613
- // Cancellations: Mars in own sign/exalted, conjunct Jup/Moon/Venus,
614
- // or aspected by Jupiter (5/7/9 sign-aspect).
615
- // Severity (anshik/purna) is computed pre-cancellation.
616
-
617
- computeKaalSarp(d1);
618
- // 12 subtypes by Rahu's house: anant, kulik, vasuki, shankhpal, padma,
619
- // mahapadma, takshak, karkotak, shankhachud, ghatak, vishdhar, sheshnag.
620
-
621
- computePitruDosha(d1);
622
- // Pandit-consensus 4-trigger set (rules cited by ≥3 of 6 surveyed
623
- // pandit sources): Sun+Rahu conjunction (any house), Sun+Saturn
624
- // conjunction (any house), Rahu in 9th house, 9th-lord conjunct Rahu.
625
- // Drik panchang publishes no Pitru calculator; minority/expansive
626
- // rules (Sun in 9th alone, Ketu in 4th, 9th lord in dusthana, etc.)
627
- // are intentionally excluded.
641
+ computeMangalCompatibility(boyChart, girlChart); // Manglik is a PAIRWISE verdict
642
+ computeKaalSarp(d1); // 12 subtypes by Rahu's house
628
643
  ```
629
644
 
630
- **Limitations.** Ashtakoot Vashya koot uses single-vashya per rashi.
631
- Bhakoot Parivartana (rashi-lord exchange) cancellation needs per-graha
632
- position data not carried by the `NatalMoon` shape and is not modelled.
645
+ ## Annual Charts, Sensitive Points, KP & Prashna
633
646
 
634
- ## Strength & Aspects
635
-
636
- ```typescript
637
- import {
638
- computeAspects, computeShadbala, computeBhavaBala, computeAshtakavarga,
639
- } from 'panchang-ts';
640
-
641
- // Drishti — every graha aspects the 7th; malefics gain extras
642
- // (Mars 4+8, Jupiter 5+9, Saturn 3+10). Node aspect mode is configurable:
643
- const aspects = computeAspects(d1); // BPHS 7th-only on nodes
644
- const aspExt = computeAspects(d1, { nodeAspects: '5-and-9' }); // KP/BV Raman extension
645
-
646
- // Shadbala — 7 visible grahas, 6 components, in Virupas (60 V = 1 Rupa).
647
- // Sthana = Uchcha + Saptavargaja (D1/D2/D3/D7/D9/D12/D30 dignity sum)
648
- // + Ojha-Yugma (rashi+navamsa parity) + Drekkana (gender decanate).
649
- // Range [0, 420 V]. Dig is directional cusp; Kala = Nathonatha + Paksha;
650
- // Chesta is retrograde-bucket; Naisargika is fixed rank; Drik is weighted aspects.
651
- const bala = computeShadbala(birth, loc);
652
-
653
- // Bhava Bala — 12-bhava strength built on top of Shadbala.
654
- // Per-bhava: { bhavadhipati, dik, drik, sthana, total }.
655
- const bhavaBala = computeBhavaBala(birth, loc);
656
-
657
- // Ashtakavarga — 12-rashi bindu grids (BPHS Ch. 66).
658
- const av = computeAshtakavarga(d1);
659
- av.sarvashtaka; // 12 cells, each 0..56, total 336
660
- av.bhinnashtaka.Jupiter; // 12-cell grid; Jupiter total = 56 (chart-invariant)
661
- // Other invariants: Sun=47, Moon=49, Mars=39, Mercury=54, Venus=52, Saturn=39.
662
-
663
- // Trikona + Ekadhipatya Sodhana reductions (BPHS Ch. 67):
664
- const avR = computeAshtakavarga(d1, { reductions: true });
665
- avR.reduced!.sarvashtaka;
666
- ```
667
-
668
- Rahu and Ketu are not Ashtakavarga receivers or contributors (classical
669
- Parashara scheme).
670
-
671
- ## Yogas & Karakas
672
-
673
- ```typescript
674
- import { computeYogas, computeJaiminiKarakas } from 'panchang-ts';
675
-
676
- // ~25 named yogas — Pancha Mahapurusha (Ruchaka/Bhadra/Hamsa/Malavya/Sasha),
677
- // lunar (Gajakesari, Sunapha, Anapha, Durudhura, Kemadruma), solar
678
- // (Budha-Aditya, Veshi, Vasi, Ubhayachari), Raja (kendra/trikona-lord,
679
- // Dharma-Karmadhipati, Vipareeta, Lakshmi), Dhana (2-11, 5-9, Vasumati),
680
- // Vargottama, Yogakaraka, Neecha Bhanga, Daridra.
681
- const yogas = computeYogas(d1);
682
- // → [{ name, type, reasons[], bhanga?: { applies, reasons[] } }, …]
683
-
684
- // Optional cancellation annotations: 5 Pancha Mahapurusha + Gajakesari
685
- // surface `bhanga` (Sun/Moon conjunct or Jupiter combust/debilitated).
686
- // Neecha Bhanga: dispositor in kendra from Lagna OR Moon; lord-of-
687
- // exaltation-rashi in kendra from Lagna or Moon; mutual exchange;
688
- // dispositor aspecting the debilitated planet.
689
-
690
- // Filter by type / pass D9 for Vargottama:
691
- const d9 = computeNavamsa(birth, loc);
692
- const all = computeYogas(d1, { types: ['raja','dhana'], navamsa: d9 });
693
-
694
- // Jaimini Karakas — Atmakaraka (highest degree-in-rashi) … Darakaraka (lowest).
695
- const k7 = computeJaiminiKarakas(d1); // 7-graha Parashara default
696
- const k8 = computeJaiminiKarakas(d1, { variant: '8-jaimini' }); // adds Rahu (degree reversed),
697
- // inserts Pitrukaraka at 5th
698
- ```
699
-
700
- Yoga and Karaka names are English/transliterated proper nouns and intentionally
701
- **not** locale-resolved.
702
-
703
- ## Annual & Sensitive Layers
647
+ 📖 [Annual Charts →](https://dharmagya.app/docs/panchang-ts/annual-charts) ·
648
+ [KP & Prashna →](https://dharmagya.app/docs/panchang-ts/kp-prashna)
704
649
 
705
650
  ```typescript
706
651
  import {
707
652
  computeVarshaphala, computeTithiPravesha, computeArudhas,
708
653
  computeHoraLagna, computeGhatiLagna, computeBhavaLagna, computeSripatiLagna,
709
654
  computeUpagrahas, computeArgala,
710
- } from 'panchang-ts';
711
-
712
- // Varshaphala — Tajik annual chart for the Nth solar return.
713
- const v = computeVarshaphala(birth, 30, loc);
714
- v.solarReturnInstant;
715
- v.varshaLagna.rashi.name;
716
- v.muntha.rashi; v.muntha.house; // muntha = (natalLagnaRashi + 30) mod 12
717
- v.yearLord; // strongest of 4 candidates by Shadbala
718
- v.sahams.Punya.house;
719
- v.sahams.Vivaha.rashi;
720
- // 27 Sahams: Punya, Vidya, Yasas, Mitra, Karma, Vivaha, Putra, Roga, Marana,
721
- // Rajya, Raja, Bandhu, Dharma, Gnati, Apamrityu, Bhratri, Matri, Pitri, Sama,
722
- // Bandhana, Karyasiddhi, Vyapara, Sastra, Asha, Labha, Susha, Tapas.
723
-
724
- // Tithi Pravesha — annual chart cast when Sun is in natal sidereal sign AND
725
- // Sun-Moon separation equals natal separation. Preserves natal tithi exactly.
726
- const tp = computeTithiPravesha(birth, 30, loc);
727
- tp.natalTithi === tp.praveshTithi; // always true
728
-
729
- // Arudha padas — image/reflection of each bhava. Arudha[0] = Arudha Lagna (AL).
730
- const a = computeArudhas(d1);
731
- a[0]!.bhava; // 1 — AL
732
- a[0]!.arudhaRashi; // 0..11
733
- a[6]!.bhava; // 7 — Darapada (spouse pada)
734
-
735
- // Special lagnas — time-derived sensitive points from sunrise on/before birth.
736
- computeHoraLagna(birth, loc); // 30°/hour (1 rashi/hour)
737
- computeGhatiLagna(birth, loc); // 75°/hour (1 rashi/24 min)
738
- computeBhavaLagna(birth, loc); // 15°/hour (1 rashi/2 hours)
739
- computeSripatiLagna(birth, loc); // = natal lagna (cusp 1)
740
-
741
- // Sripati cusps 2–12 (opt-in) — 4 angular cusps + trisected intermediates.
742
- // Defined at every latitude (unlike Placidus).
743
- const sripati = computeSripatiLagna(birth, loc, 'lahiri', 'en', { includeCusps: true });
744
- sripati.cusps; // number[12] of bhava madhyas; cusps[0/3/6/9] = ASC/IC/DSC/MC
745
-
746
- // Upagrahas — Gulika, Mandi (rising-asc at Saturn segment start/midpoint),
747
- // plus Sun-derived Dhuma, Vyatipata, Parivesha, Indrachapa, Upaketu.
748
- const u = computeUpagrahas(birth, loc);
749
- u.gulika.longitude; u.gulika.rashi; u.gulika.house;
750
-
751
- // Argala (Jaimini) — planets in 2/4/11 from a bhava form Argala (intervention);
752
- // 3/10/12 form Virodhargala (counter). Each planet hits exactly 6 of 12 bhavas.
753
- const arg = computeArgala(d1);
754
- arg[0]!.argala; arg[0]!.virodhargala;
755
-
756
- // Trikonargala (5/9 trine, opt-in) — Ketu reversal: 5th-from → virodhaka,
757
- // 9th-from → source.
758
- const argT = computeArgala(d1, { includeTrikonargala: true });
759
- argT[0]!.trikona!.sources;
760
- argT[0]!.trikona!.virodhakas;
761
- ```
762
-
763
- ## KP & Prashna
764
-
765
- ```typescript
766
- import {
767
655
  computeKpSubLord, computeKpCuspalSubLords, computeKpSignificators,
768
656
  computePrashnaChart,
769
657
  } from 'panchang-ts';
770
658
 
771
- // KP sub-lord at any sidereal longitude (243 sub-divisions across the zodiac,
772
- // proportional to Vimshottari years).
773
- const info = computeKpSubLord(45.5); // 15°30' Taurus
774
- info.signLord; // 'Venus'
775
- info.starLord; // 'Moon'
776
- info.subLord;
777
-
778
- // Cuspal sub-lords (always Placidus-KP — KP's anchor scheme).
779
- const cusps = computeKpCuspalSubLords(birth, loc);
780
- cusps.cusps[0]!.subLord; // ascendant
781
- cusps.cusps[6]!.subLord; // descendant
782
-
783
- // Significators — for each planet, the houses it signifies via the 4-fold KP rule
784
- // (occupant + star-lord-occupant + owner + star-lord-owner).
785
- const sig = computeKpSignificators(d1);
786
- sig.byPlanet.Sun;
787
- sig.byHouse[10];
788
-
789
- // Prashna (horary) chart — cast at question moment from querent's location.
790
- const pchart = computePrashnaChart(
791
- new Date('2026-05-09T14:30:00Z'),
792
- { latitude: 19.0760, longitude: 72.8777 },
793
- );
794
- pchart.lagna.rashi.name;
795
- pchart.bhava.system; // 'placidus-kp' by default (KP horary anchor)
796
- pchart.planets[1]!.house; // Moon — primary mind significator
797
- ```
798
-
799
- Same return shape as a natal `BirthChart`. Pass `{ houseSystem: 'whole-sign' }`
800
- to `computePrashnaChart` for traditional Vedic Prashna.
801
-
802
- ## Muhurta Engine
803
-
804
- ```typescript
805
- import { scoreMuhurta, findAuspiciousDates, vivahRule } from 'panchang-ts';
806
-
807
- const r = scoreMuhurta(new Date('2026-05-12'), DELHI, vivahRule, { timezone: 330 });
808
- // → { date, score: 0..100, passes: boolean, reasons: string[] }
809
-
810
- const dates = findAuspiciousDates(
811
- vivahRule,
812
- new Date('2026-05-01'),
813
- new Date('2026-05-31'),
814
- DELHI,
815
- { timezone: 330 },
816
- ); // MuhurtaDay[] sorted by score desc; full panchang attached
817
-
818
- // Custom rule (pure data, no engine code needed)
819
- const myRule: MuhurtaRule = {
820
- occasion: 'launch_party',
821
- auspiciousVaras: [3, 4, 5],
822
- auspiciousNakshatras: [11, 12, 21],
823
- excludeBhadra: true,
824
- excludeEkadashi: true,
825
- excludeAdhikaMasa: true,
826
- };
659
+ const v = computeVarshaphala(birth, 30, loc); // Tajik + Muntha + 27 Sahams
660
+ computeTithiPravesha(birth, 30, loc); // preserves natal tithi exactly
661
+ computeKpSubLord(45.5).subLord; // 243 sub-divisions
662
+ computePrashnaChart(questionTime, querentLoc); // horary, Placidus-KP default
827
663
  ```
828
664
 
829
- 13 stock rules: vivah, griha pravesh, namakarana, vidyarambh, vahan kharidi,
830
- annaprashan, mundan, upanayanam, karnavedha, aksharabhyasam, seemantham, shop
831
- opening, travel start.
832
-
833
- Scoring: starts at 50; +10 per matching auspicious axis (tithi / nakshatra /
834
- vara / yoga), -15 per inauspicious axis, hard exclusions zero the score.
835
- Special yogas (Amrit Siddhi, Sarvartha Siddhi, Ravi/Guru Pushya) add +5;
836
- Jwalamukhi subtracts -10. Clamped 0..100; `passes: true` when score ≥ 50.
837
-
838
665
  ## Calendar Conversion
839
666
 
667
+ 📖 [Calendar Conversion →](https://dharmagya.app/docs/panchang-ts/calendar-conversion)
668
+
840
669
  ```typescript
841
670
  import {
842
671
  convertGregorianToHindu, convertHinduToGregorian,
843
672
  getKaliYugaYear, getHinduNewYear,
844
- getEkadashiDatesForYear, getSankrantisForYear,
845
- getFestivalsInRange, getUpcomingEclipses, getEclipsesInRange,
673
+ computeEkadashiDatesForYear, computeSankrantisForYear,
846
674
  } from 'panchang-ts';
847
675
 
848
- // Gregorian → Hindu coords at sunrise
849
- const h = convertGregorianToHindu(new Date('2026-04-15'), DELHI, { timezone: 330 });
850
-
851
- // Hindu → Gregorian
852
- const dates = convertHinduToGregorian(
676
+ convertHinduToGregorian(
853
677
  { vikramSamvat: 2083, masaIndex: 0, paksha: 'shukla', pakshaTithi: 9 },
854
678
  DELHI, { timezone: 330 },
855
- ); // → Rama Navami in VS 2083
856
-
857
- getKaliYugaYear(new Date('2026-04-01')); // 5127
858
- getHinduNewYear(2026, 'tamil-nadu', DELHI, { timezone: 330 }); // Puthandu
859
-
860
- getEkadashiDatesForYear(2026, DELHI, { timezone: 330 }); // ~24 Date[]
861
- getSankrantisForYear(2026, DELHI, { timezone: 330 }); // 12 SankrantiEvent[]
862
- getFestivalsInRange(start, end, DELHI, { timezone: 330 }); // FestivalDay[]
863
- getUpcomingEclipses(new Date(), DELHI, 5);
679
+ ); // → Date[] (Rama Navami VS 2083)
680
+ getHinduNewYear(2026, 'tamil-nadu', DELHI, { timezone: 330 }); // region-aware
864
681
  ```
865
682
 
866
- `getHinduNewYear` is region-aware: Tamil Nadu / Kerala / Punjab / Bengal / Assam
867
- use the **solar** (Mesha Sankranti) anchor; elsewhere uses **Chaitra Shukla
868
- Pratipada** (Ugadi / Gudi Padwa / Cheti Chand). When Pratipada is a kshaya
869
- tithi (e.g. Ugadi 2026), falls back to the Amanta-Chaitra-masa boundary.
870
-
871
683
  ## Localization & Configuration
872
684
 
873
- ```typescript
874
- const hi = getDailyPanchang(date, loc, { timezone: 330, language: 'hi' })!;
875
- hi.tithis[0].name; // "कृष्ण चतुर्दशी"
876
- hi.vara.name; // "मंगलवार"
877
- hi.chandramasa.name; // "माघ"
878
- hi.choghadiya.day[0].name; // "अमृत"
879
- hi.vara.englishName; // "Tuesday" — englishName always English
880
-
881
- // All options:
882
- const r = getDailyPanchang(date, loc, {
883
- timezone: 330, // number (UTC offset min) or IANA string
884
- ayanamsa: 'lahiri', // lahiri | raman | krishnamurti | true-chitra | thirukanitham
885
- language: 'en', // en | hi
886
- masaSystem: 'purnimanta', // purnimanta | amanta
887
- region: 'all', // 21 state slugs + 'nepal' + 'all'
888
- computeEndTimes: true, // false → ~5x speedup, names only
889
- precision: 'standard', // standard (15 iter) | high (25 iter)
890
- janmaRashi: undefined, // pass to add r.chandraBalam
891
- janmaNakshatra: undefined, // pass to add r.tarabala
892
- });
893
- ```
894
-
895
- **Timezone.** Number (minutes from UTC, e.g. `330` for IST) or an IANA zone
896
- name (e.g. `'America/New_York'`). IANA strings need `Intl`, which older Hermes
897
- versions lack — pass a number on those targets. DST resolves automatically for
898
- IANA zones.
899
-
900
- ---
901
-
902
- ## Types & Exports
903
-
904
- <details>
905
- <summary><strong>Core, Pancha Anga, Festivals</strong></summary>
906
-
907
- ```typescript
908
- interface GeoLocation { latitude: number; longitude: number; elevation?: number; }
909
- interface TimePeriod { start: Date; end: Date; }
910
-
911
- interface TithiInfo {
912
- index: number; // 0-29
913
- name: string;
914
- paksha: string; // "Shukla"/"Krishna" (en), "शुक्ल"/"कृष्ण" (hi)
915
- number: number; // 1-15 within the paksha
916
- completionPercentage: number;
917
- endTime: Date | null;
918
- }
919
- // NakshatraInfo, YogaInfo, KaranaInfo follow the same pattern.
920
- // DailyTithiInfo extends with startTime + isActiveAtSunrise.
921
-
922
- interface VaraInfo {
923
- index: number; // 0 = Sunday … 6 = Saturday
924
- name: string; // localized (e.g. "Raviwara")
925
- shortName: string;
926
- englishName: string; // always English
927
- }
928
-
929
- interface FestivalInfo {
930
- name: string;
931
- type: 'major' | 'minor' | 'ekadashi' | 'smarta_ekadashi' | 'vaishnava_ekadashi'
932
- | 'pradosha' | 'sankranti' | 'eclipse';
933
- description?: string;
934
- deferralDate?: Date; // Smarta Ekadashi → Dwadashi fast date
935
- }
936
-
937
- type FestivalRegion =
938
- | 'all'
939
- | 'tamil-nadu' | 'kerala' | 'karnataka' | 'andhra-pradesh' | 'telangana'
940
- | 'west-bengal' | 'odisha' | 'assam' | 'bihar' | 'jharkhand'
941
- | 'gujarat' | 'maharashtra' | 'goa' | 'rajasthan'
942
- | 'punjab' | 'haryana' | 'himachal-pradesh' | 'uttarakhand'
943
- | 'uttar-pradesh' | 'madhya-pradesh'
944
- | 'nepal';
945
-
946
- // Legacy slugs accepted (mapped internally): 'tamil' → 'tamil-nadu',
947
- // 'bengal' → 'west-bengal', 'north-india' → 'all'.
948
- ```
949
-
950
- </details>
951
-
952
- <details>
953
- <summary><strong>Eclipses & Bhadra</strong></summary>
685
+ 📖 [Options & Localization →](https://dharmagya.app/docs/panchang-ts/localization)
954
686
 
955
687
  ```typescript
956
- interface EclipseInfo {
957
- kind: 'solar' | 'lunar';
958
- subtype: 'partial' | 'total' | 'annular' | 'penumbral';
959
- start: Date; peak: Date; end: Date;
960
- visibleFromLocation: boolean;
961
- magnitude: number; // [0, 1] at peak
962
- sutakStart: Date; // 12 h pre-solar / 9 h pre-lunar
963
- sutakEnd: Date;
964
- description: string;
965
- }
966
-
967
- interface BhadraInfo {
968
- start: Date; end: Date;
969
- location: 'earth' | 'heaven' | 'paatal'; // 'earth' = malefic for all work
970
- isActive: boolean;
971
- }
972
- ```
973
-
974
- </details>
975
-
976
- <details>
977
- <summary><strong>Jyotish</strong></summary>
978
-
979
- ```typescript
980
- type GrahaName = 'Sun' | 'Moon' | 'Mars' | 'Mercury' | 'Jupiter'
981
- | 'Venus' | 'Saturn' | 'Rahu' | 'Ketu';
982
-
983
- interface GrahaPosition {
984
- planet: GrahaName;
985
- siderealLongitude: number;
986
- rashi: RashiInfo;
987
- degreeInRashi: number;
988
- nakshatra: NakshatraInfo;
989
- isRetrograde: boolean; // always false for Sun/Moon; always true for Rahu/Ketu
990
- }
991
-
992
- type DashaLord = 'Ketu' | 'Venus' | 'Sun' | 'Moon' | 'Mars'
993
- | 'Rahu' | 'Jupiter' | 'Saturn' | 'Mercury';
994
-
995
- interface MahaDasha { lord: DashaLord; startDate: Date; endDate: Date;
996
- years: number; antarDashas: AntarDasha[]; }
997
- interface VimshottariDashaResult {
998
- currentMahaDashaLord: DashaLord;
999
- currentIndex: number;
1000
- mahaDashas: MahaDasha[];
1001
- }
1002
-
1003
- interface ChandraBalamInfo {
1004
- house: number; // 1 = janma rashi; 12 = rashi before janma
1005
- quality: 'strong' | 'weak'; // Shubha houses = 1,3,6,7,10,11
1006
- englishName: string; // "Shubha" | "Ashubha"
1007
- name: string;
1008
- }
688
+ const hi = getDailyPanchang(date, loc, { timezone: 330, language: 'hi' })!;
689
+ hi.angas.tithis[0].name; // "कृष्ण चतुर्दशी"
690
+ hi.angas.vara.englishName; // "Tuesday" englishName always English
1009
691
 
1010
- interface TarabalaInfo {
1011
- taraIndex: number; // 0..8 in 9-tara cycle from janma nakshatra
1012
- englishName: string; // Janma | Sampat | Vipat | Kshema | Pratyari
1013
- // | Sadhaka | Vadha | Mitra | Ati-Mitra
1014
- name: string;
1015
- quality: 'auspicious' | 'inauspicious';
1016
- }
692
+ // All options: timezone (number | IANA string), ayanamsa (5), language (en|hi),
693
+ // masaSystem (purnimanta|amanta), region, computeEndTimes, sections,
694
+ // janmaRashi, janmaNakshatra.
1017
695
  ```
1018
696
 
1019
- </details>
1020
-
1021
- <details>
1022
- <summary><strong>Full export list</strong></summary>
697
+ Machine-readable keys never change with language: match on `festival.key`,
698
+ `bhadra.location`, `eclipse.kind`, `factors[].code` — render `name` /
699
+ `locationName` / `description` / `reasons`.
1023
700
 
1024
- ```typescript
1025
- // Primary entry points
1026
- getDailyPanchang, getInstantPanchang
1027
-
1028
- // Astronomy
1029
- getSunrise, getSunset, getMoonrise, getMoonset
1030
- getSiderealSunLongitude, getSiderealMoonLongitude, getAyanamsa
1031
-
1032
- // Inauspicious / Muhurta
1033
- computeRahuKalam, computeGulikaKalam, computeYamaganda
1034
- computeVarjyam, computeGandaMula, computeAnandadiYoga
1035
- computePanchakaRahita, computeDoGhati, computeGowriPanchangam
1036
- computeAbhijitMuhurta, computeBrahmaMuhurta, computeVijayaMuhurta
1037
- computeGodhuliMuhurta, computeNishitaMuhurta, computeAmritKala
1038
- computeMadhyahna, computePratahSandhya, computeSayahnaSandhya
1039
-
1040
- // Eclipses
1041
- getUpcomingSolarEclipse, getUpcomingLunarEclipse, getEclipseDuringDay
1042
- isEclipseVisibleAnyPhase
1043
-
1044
- // Moon phases (new / quarters / full as precise instants)
1045
- getMoonPhasesInRange
1046
-
1047
- // Jyotish — planets, dashas, transits
1048
- computePlanetaryPositions, GRAHA_ABBR
1049
- computeVimshottariDasha, computeVimshottariDashaFromBirth, computeVimshottariPratyantar
1050
- computeAshtottariDasha, computeYoginiDasha, computeCharaDasha, computeNarayanDasha
1051
- computeChandraBalam, computeTarabala, computeSadeSati
1052
-
1053
- // Jyotish — chart
1054
- computeLagna, computeBhava, computeRashiChart, computeNavamsa, computeDivisionalChart
1055
- computeHoraLagna, computeGhatiLagna, computeBhavaLagna, computeSripatiLagna
1056
- computeDignity
1057
-
1058
- // Jyotish — strength, yogas, sensitive
1059
- computeAspects, computeShadbala, computeBhavaBala, computeAshtakavarga
1060
- computeYogas, computeJaiminiKarakas
1061
- computeVarshaphala, computeTithiPravesha, computeArudhas, computeUpagrahas, computeArgala
1062
-
1063
- // Jyotish — compatibility, doshas
1064
- computeAshtakoot, computePathuPorutham
1065
- computeMangalDosha, computeKaalSarp, computePitruDosha
1066
-
1067
- // KP / Prashna
1068
- computeKpSubLord, computeKpCuspalSubLords, computeKpSignificators
1069
- computePrashnaChart
1070
-
1071
- // Muhurta engine
1072
- scoreMuhurta, findAuspiciousDates, STOCK_MUHURTA_RULES
1073
- vivahRule, grihaPraveshRule, namakaranaRule, vidyarambhRule, vahanKharidiRule
1074
- annaprashanRule, mundanRule, upanayanamRule, karnavedhaRule
1075
- aksharabhyasamRule, seemanthamRule, shopOpeningRule, travelStartRule
1076
-
1077
- // Calendar conversion + yearly listings
1078
- convertGregorianToHindu, convertHinduToGregorian
1079
- getKaliYugaYear, getHinduNewYear, computeSamvat
1080
- getEkadashiDatesForYear, getSankrantisForYear, getFestivalsInRange
1081
- getUpcomingEclipses, getEclipsesInRange
1082
-
1083
- // Static data tables (engine-using runtime builders; bundled JSON at
1084
- // panchang-ts/festivals, panchang-ts/eclipses, panchang-ts/moon-phases)
1085
- buildFestivalsTable, buildEclipsesTable, buildMoonPhasesTable
1086
-
1087
- // Errors
1088
- PanchangError
1089
- ```
701
+ ## Types & Exports
1090
702
 
1091
- </details>
703
+ 📖 [Types & Exports →](https://dharmagya.app/docs/panchang-ts/types) — the key
704
+ interfaces (`TithiInfo`, `FestivalInfo`, `EclipseInfo`, `GrahaPosition`, …) and
705
+ the complete export list of the main entry and the four engine-free subpaths
706
+ (`panchang-ts/festivals`, `/eclipses`, `/moon-phases`, `/muhurta`).
1092
707
 
1093
708
  ---
1094
709
 
@@ -1103,14 +718,15 @@ Two-pass rendering pattern for smooth UI:
1103
718
  import { getDailyPanchang } from 'panchang-ts';
1104
719
  import { InteractionManager } from 'react-native';
1105
720
 
1106
- // Pass 1 — instant, names only (~0.1 ms Node, <100 ms Hermes)
721
+ // Pass 1 — cheapest useful result: elements, slots, muhurtas (~0.25 ms).
1107
722
  const fast = getDailyPanchang(date, location, {
1108
723
  timezone: 330,
724
+ sections: [],
1109
725
  computeEndTimes: false,
1110
726
  });
1111
727
  setState(fast);
1112
728
 
1113
- // Pass 2 — background, full with end-times (~0.5 ms Node, <500 ms Hermes)
729
+ // Pass 2 — background, everything (~0.41 ms).
1114
730
  InteractionManager.runAfterInteractions(() => {
1115
731
  setState(getDailyPanchang(date, location, { timezone: 330 }));
1116
732
  });
@@ -1120,7 +736,9 @@ InteractionManager.runAfterInteractions(() => {
1120
736
 
1121
737
  ## Accuracy
1122
738
 
1123
- 8,164 tests across 100 files, including fixtures cross-verified against reference
739
+ 📖 [Full accuracy notes →](https://dharmagya.app/docs/panchang-ts/accuracy)
740
+
741
+ 8,368 tests across 121 files, including fixtures cross-verified against reference
1124
742
  panchang calculations spanning 2025–2026 across 10 Indian cities plus New York,
1125
743
  London, Sydney, Dubai, Singapore (diaspora fixtures cover DST on
1126
744
  `America/New_York`).
@@ -1130,54 +748,68 @@ London, Sydney, Dubai, Singapore (diaspora fixtures cover DST on
1130
748
  | Sunrise / Sunset | ≤29 s observed vs reference minute-midpoint (±45 s tolerance) |
1131
749
  | Moonrise / Moonset | Meeus apparent-upper-limb (refraction + parallax); ~3–5 min vs simpler-horizon authorities is expected |
1132
750
  | Tithi / Nakshatra / Yoga / Karana names | Exact match vs reference |
1133
- | Tithi / Nakshatra / Yoga / Karana end-times | ±3 min tolerance, max 2.01 min observed |
1134
- | Ayanamsa | ±0.005° vs Swiss Ephemeris |
751
+ | Tithi / Nakshatra / Yoga / Karana end-times | ≤60 s vs Drik across all 20 audited comparisons |
752
+ | Ayanamsa (Lahiri) | Reproduces DrikPanchang's published value to ~0.01″ across 1950–2050 |
1135
753
  | Planetary positions (Sun–Saturn) | ±0.02° sidereal |
1136
- | Planetary positions (Rahu/Ketu, mean node) | ≤0.5° typical; ±2° tolerance |
1137
- | Planetary positions (Rahu/Ketu, true node) | ≤0.6° typical (Meeus periodic correction) |
754
+ | Planetary positions (Rahu/Ketu) | ≤0.5° mean node, ≤0.6° true node (typical) |
1138
755
  | Lagna sidereal longitude | Cross-checked against Jagannath Hora reference charts |
1139
756
  | D1 / D9 house placement | Exact match vs reference for 9-graha placement |
1140
757
  | Ashtakoot total | ±1 point per pair across 30+ matched pairs |
1141
758
  | Sade Sati arc start/end | ±1–2 days vs authoritative ephemerides |
1142
759
 
1143
- **Detection notes.** **Aadal / Vidaal** follow the classical Moon-from-Sun
1144
- nakshatra-distance rule (AstroShastra, HoraSarvam, Ernst Wilhelm), NOT the
1145
- Tamil-Vakya weekday rule used by some online panchangs. **Varjyam** emits the
1146
- sunrise-anchored nakshatra's window only. **Do Ghati Muhurta** does not rotate
1147
- by weekday — the same 30-name deity-keyed sequence applies every day.
760
+ **Festival dating** uses tithi-at-sunrise; a few festivals have authorities on
761
+ other rules (tithi-at-midnight for Janmashtami / Shivaratri / Diwali,
762
+ madhyahna-vyapini for Ganesh Chaturthi edge years) where output can drift
763
+ ±1 day the exact list is
764
+ [documented](https://dharmagya.app/docs/panchang-ts/accuracy#festival-tradeoff).
1148
765
 
1149
- ### Festival Detection Documented Tradeoff
766
+ **Detection conventions:** Aadal / Vidaal follow the classical Moon-from-Sun
767
+ nakshatra-distance rule, not the Tamil-Vakya weekday rule. Varjyam emits the
768
+ sunrise-anchored nakshatra's window only. Do Ghati does not rotate by weekday.
1150
769
 
1151
- The library uses **tithi-at-sunrise** to resolve a festival to a calendar day.
1152
- Some authorities use other classical rules for certain festivals; where those
1153
- rules pick a different day, output can drift ±1 day:
770
+ ---
1154
771
 
1155
- | Alternative rule | Affects |
1156
- |---|---|
1157
- | Tithi-at-midnight | Krishna Janmashtami, Maha Shivaratri, Diwali / Lakshmi Puja |
1158
- | Madhyahna-vyapini | Ganesh Chaturthi (edge years), Akshaya Tritiya 2026 |
1159
- | Kshaya-tithi handling | Ugadi 2026-03-19 (Pratipada is Kshaya) |
772
+ ## Performance
1160
773
 
1161
- Everything else Holi, Ugadi (non-Kshaya years), Rama Navami, Raksha Bandhan,
1162
- Ganesh Chaturthi (normal years), Navaratri, Dussehra, Karva Chauth, Hanuman
1163
- Jayanti — matches the canonical date across 2025 and 2026 fixtures.
774
+ 📖 [Full performance notes →](https://dharmagya.app/docs/panchang-ts/performance)
1164
775
 
1165
- ---
776
+ Measured at Pune, Apple M-series, Node — median of 11 processes. **Distinct
777
+ days** is the calendar-scan cost; **same day repeated** is what a UI
778
+ re-rendering one date sees. Last column is published 4.3.1, benchmarked beside
779
+ this release.
1166
780
 
1167
- ## Performance
781
+ | `getDailyPanchang` call | Distinct days | Same day repeated | 4.3.1 (distinct) |
782
+ |---|---|---|---|
783
+ | Default (all sections + end-times) | **~0.41 ms** | **~0.17 ms** | ~6.06 ms |
784
+ | `sections: []` + `computeEndTimes: false` | ~0.25 ms | ~0.14 ms | n/a |
785
+ | `getInstantPanchang` | ~0.21 ms | ~0.10 ms | ~0.43 ms |
1168
786
 
1169
- | Mode | Node.js | Hermes (budget Android) |
1170
- |---|---|---|
1171
- | Names-only (`computeEndTimes: false`) | ~0.1 ms | <100 ms |
1172
- | Full with end-times | ~0.5 ms | <500 ms |
787
+ **A default day is ~15× cheaper than 4.3.1**, a repeated day ~37×. The levers:
788
+
789
+ - **`sections`** skip the optional ephemeris-backed blocks (`'festivals'`,
790
+ `'eclipse'`, `'moonTimes'`, `'lunarWindows'`). Narrowing is exactly
791
+ output-neutral: every field a narrowed call computes is identical to the full
792
+ call's; omitted sections sit at their documented `null` / `[]`.
793
+ - **`computeEndTimes: false`** — drops the `endTime` transition searches;
794
+ ~5–10% — use it to drop fields you don't want, not to go faster.
1173
795
 
1174
- Birth-chart helpers are independent calling them does not add work to
1175
- `getDailyPanchang`.
796
+ Range helpers narrow internally: `computeEkadashiDatesForYear` **~18 ms/year**
797
+ (4.3.1: ~2,360), `computeFestivalsInRange` **~131 ms/year** (~2,180),
798
+ `computeSankrantisForYear` **~3.3 ms/year** (~154). Solar rise/set events are
799
+ cached process-wide (bounded, 20k entries), which also makes them
800
+ single-valued.
801
+
802
+ One deliberate regression: raw chart primitives (`computeRashiChart`,
803
+ `computeNavamsa`) cost ~0.32 ms vs ~0.10 in 4.3.1 — the price of an
804
+ order-of-magnitude accuracy gain against JPL DE441. `computeShadbala` /
805
+ `computeBhavaBala` went the other way, ~2× faster.
1176
806
 
1177
807
  ---
1178
808
 
1179
809
  ## Error Handling
1180
810
 
811
+ 📖 [Errors & Compatibility →](https://dharmagya.app/docs/panchang-ts/errors)
812
+
1181
813
  ```typescript
1182
814
  import { PanchangError } from 'panchang-ts';
1183
815
 
@@ -1192,7 +824,7 @@ try {
1192
824
  ```
1193
825
 
1194
826
  Error codes: `INVALID_DATE`, `INVALID_LATITUDE`, `INVALID_LONGITUDE`,
1195
- `INVALID_ELEVATION`, `INVALID_TIMEZONE`, `INVALID_AYANAMSA`,
827
+ `INVALID_ELEVATION`, `INVALID_TIMEZONE`, `INVALID_AYANAMSA`, `INVALID_INPUT`,
1196
828
  `TIMEZONE_RESOLUTION_FAILED`, `NO_SUNRISE`, `NO_SUNSET`, `SEARCH_DIVERGED`,
1197
829
  `CIRCUMPOLAR` (Placidus-KP houses above ±66.5°).
1198
830
 
@@ -1209,18 +841,25 @@ for the Moon).
1209
841
 
1210
842
  | Environment | Support |
1211
843
  |---|---|
1212
- | Node.js 18+ | Supported |
844
+ | Node.js 22 (per the package `engines` field) | Supported |
1213
845
  | React Native (Hermes) | Supported (pass `timezone` as number) |
1214
846
  | Expo (managed + bare) | Supported |
1215
847
  | Browser (modern, ESM) | Supported |
1216
848
  | Browser (legacy / IE) | Not supported |
1217
849
 
850
+ The build targets ES2020 (ESM + CJS, full `.d.ts`), has **zero runtime
851
+ dependencies**, and is `sideEffects: false`.
852
+
1218
853
  ---
1219
854
 
1220
855
  ## Acknowledgements
1221
856
 
1222
- [astronomy-engine](https://github.com/cosinekitty/astronomy) by Don Cross — the
1223
- sole runtime dependency. MIT licensed.
857
+ As of v5 the package has **no runtime dependencies** — the ephemeris, ΔT model
858
+ and event searches are the library's own. Two projects still deserve credit:
859
+ [astronomy-engine](https://github.com/cosinekitty/astronomy) by Don Cross (MIT),
860
+ the runtime engine through 4.x and now the dev-time baseline the own ephemeris
861
+ is tested against, and the algorithms of Jean Meeus's *Astronomical Algorithms*,
862
+ which underpin the rise/set and node models.
1224
863
 
1225
864
  ## License
1226
865