panchang-ts 5.0.0 → 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 (2) hide show
  1. package/README.md +163 -1186
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,18 @@
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
9
  **Fast** (~0.25 ms trimmed, ~0.41 ms full) · **Typed** (full TypeScript) · **Offline** (pure JS math) · **8,368 tests across 121 files**
9
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.
15
+
10
16
  ---
11
17
 
12
18
  ## Install
@@ -318,10 +324,8 @@ a `*Local` companion: `sun.riseLocal`, `inauspicious.rahuKalam.startLocal`,
318
324
  timezone, so there is no zone to render a wall clock in.
319
325
 
320
326
  **Cost:** rendering the strings adds ~0.04 ms per daily panchang — invisible on
321
- a cold call (0.7885 → 0.7924 ms) and ~20% of a fully cached warm one
322
- (0.176 → 0.215 ms). Both pairs are the tree measured against itself when the
323
- change landed, mid-Phase-36; the release finally warms to **0.17 ms**, against
324
- published 4.3.1's **6.20 ms**.
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**.
325
329
 
326
330
  ### `result.timezone` is now an object
327
331
 
@@ -485,1109 +489,221 @@ the **lunar** one, which was previously left to guesswork.
485
489
 
486
490
  # Feature Reference
487
491
 
488
- Each daily field is also returned from `getDailyPanchang` if you prefer one call
489
- 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
490
496
 
491
- ## Pancha Anga
497
+ 📖 [Daily Panchang →](https://dharmagya.app/docs/panchang-ts/daily-panchang)
492
498
 
493
499
  ```typescript
494
500
  const r = getDailyPanchang(date, location, { timezone: 330 })!;
495
501
 
496
- r.angas.tithis.forEach(t => console.log(t.name, t.paksha, t.completionPercentage, t.endTime));
497
- r.angas.nakshatras.forEach(n => console.log(n.name, n.pada, n.endTime));
498
- r.angas.yogas.forEach(y => console.log(y.name, y.endTime));
499
- r.angas.karanas.forEach(k => console.log(k.name, k.type, k.endTime));
500
- console.log(r.angas.vara.name, r.angas.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, …
501
509
 
502
510
  // Single-instant snapshot:
503
511
  import { getInstantPanchang } from 'panchang-ts';
504
512
  const i = getInstantPanchang(new Date(), location)!;
505
- console.log(i.angas.tithi.name, i.angas.nakshatra.name, i.angas.yoga.name, i.angas.karana.name, i.angas.vara.name);
506
- ```
507
-
508
- ## Lunar & Solar Calendar
509
-
510
- ```typescript
511
- const r = getDailyPanchang(date, loc, { timezone: 330, masaSystem: 'purnimanta' })!;
512
-
513
- r.calendar.chandramasa.name; // active system (default: Purnimanta / North Indian)
514
- r.calendar.chandramasa.amantaName; // South Indian
515
- r.calendar.chandramasa.purnimantaName; // North Indian
516
- r.calendar.chandramasa.isAdhika; // true during leap months
517
- r.calendar.samvat.vikramSamvat; // 2081
518
- r.calendar.samvat.shakaSamvat; // 1946
519
-
520
- r.calendar.masa.name; // current solar month (Mesha … Meena)
521
- r.sun.nakshatra.name; // Sun's nakshatra
522
- r.moon.rashi.name; // Moon sign
523
- ```
524
-
525
- ## Sun, Moon & Muhurta
526
-
527
- ```typescript
528
- import { getSunrise, getSunset, getMoonrise, getMoonset } from 'panchang-ts';
529
-
530
- const sunrise = getSunrise(localMidnightUtc, loc);
531
- const sunset = getSunset(sunrise, loc);
532
- const moonrise = getMoonrise(localMidnightUtc, loc); // null on some days (normal)
533
- const moonset = getMoonset(localMidnightUtc, loc);
534
-
535
- // Or read off the daily result:
536
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
537
- r.sun.rise; r.sun.set; r.moon.rise; r.moon.set; r.sun.nextRise;
538
- r.sun.dayDurationMinutes; r.sun.nightDurationMinutes;
539
-
540
- // Auspicious muhurtas
541
- r.muhurtas.brahma; // two muhurtas before sunrise
542
- r.muhurtas.abhijit; // 8th day-muhurta; null on Wednesday (Drik convention)
543
- r.muhurtas.vijaya; // 11th day-muhurta
544
- r.muhurtas.godhuli; // "cow-dust" sunset muhurta
545
- r.muhurtas.nishita; // midnight muhurta (Shivaratri)
546
- r.muhurtas.madhyahna; // solar noon ±24 min
547
- r.muhurtas.pratahSandhya; // dawn twilight, ends at sunrise
548
- r.muhurtas.sayahnaSandhya; // dusk twilight, starts at sunset
549
- r.muhurtas.amritKala; // nakshatra-specific window (null when nakshatra has none)
550
- ```
551
-
552
- `muhurtas.pratahSandhya` / `muhurtas.sayahnaSandhya` width =
553
- `sun.nightDurationMinutes / 10` (~62–81 min).
554
-
555
- ## Inauspicious Periods
556
-
557
- ```typescript
558
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
559
-
560
- r.inauspicious.rahuKalam; // { start, end }
561
- r.inauspicious.gulikaKalam;
562
- r.inauspicious.yamaganda;
563
- r.inauspicious.durMuhurta; // two ~48-min windows
564
- r.inauspicious.varjyam; // { start, end } | null
565
- r.inauspicious.gandaMula; // { active, severity: 'mild'|'severe'|null, ... }
566
- r.inauspicious.bhadra; // { start, end, location: 'earth'|'heaven'|'paatal', isActive } | null
567
- r.inauspicious.panchaka; // boolean — Moon in last 5 nakshatras
568
- r.inauspicious.panchakaInfo; // which of the five, and whether it's a dosha
569
- ```
570
-
571
- Panchaka is not one undifferentiated affliction. The tradition names five and
572
- picks between them by **the weekday the spell began on** — so the type belongs
573
- to the spell, not the day, and two days with identical tithi, nakshatra and
574
- vara can carry different ones. A spell begun on a Wednesday or Thursday
575
- (`'samanya'`) carries no named affliction at all:
576
-
577
- ```typescript
578
- const pk = r.inauspicious.panchakaInfo;
579
- if (pk.active && pk.isDosha) {
580
- console.log(pk.name, '— began on vara', pk.onsetVara); // e.g. "Mrityu Panchaka"
581
- }
582
- ```
583
-
584
- ## Time-Slot Systems
585
-
586
- ```typescript
587
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
588
-
589
- // Choghadiya — 8 day + 8 night named, rated slots (Amrit, Kaal, Shubh, Rog, …)
590
- r.periods.choghadiya.day.forEach(s => console.log(s.name, s.qualityName, s.start, s.end));
591
-
592
- // Gowri Panchangam ("Nalla Neram") — 8 day + 8 night Tamil slots
593
- r.periods.gowri.day.forEach(s => console.log(s.name, s.qualityName));
594
-
595
- // Hora — 12 day + 12 night planetary hours (Chaldean order)
596
- r.periods.hora.day.forEach(h => console.log(h.planet, h.start, h.end));
597
-
598
- // Do Ghati Muhurta — 15 day + 15 night ~48-min deity-keyed slots (no vara rotation)
599
- r.muhurtas.doGhati.day.forEach(g => console.log(g.name, g.start, g.end));
600
-
601
- // Panchaka Rahita — slices of the day FREE of Panchaka ([] when it pervades)
602
- r.inauspicious.panchakaRahita.forEach(slice => console.log(slice.start, slice.end));
603
- ```
604
-
605
- ## Special Yogas
606
-
607
- ```typescript
608
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
609
-
610
- r.anandadiYoga.name; // 28-cycle name e.g. "Ananda"
611
- r.specialYogas.forEach(y => {
612
- // type: amrit_siddhi | sarvartha_siddhi | ravi_pushya | guru_pushya
613
- // | dwipushkar | tripushkar | jwalamukhi (inauspicious)
614
- // | aadal | vidaal | ravi (Moon-from-Sun nakshatra-distance rules)
615
- console.log(y.name, y.type);
616
- });
513
+ console.log(i.angas.tithi.name, i.angas.nakshatra.name);
617
514
  ```
618
515
 
619
516
  ## Festivals (80+)
620
517
 
621
- Covers Ekadashi (26 variants, Smarta/Vaishnava split via Dashami-viddha; Smarta
622
- fast emits a `deferralDate` for Dwadashi), Pradosha, Sankranti + regional
623
- variants (Pongal, Vishu, Baisakhi, Pohela Boishakh, Bihu, Uttarayan, Lohri…),
624
- canonical-time classical (Janmashtami, Shivaratri, Ganesh Chaturthi, Diwali,
625
- Holi, Raksha Bandhan — Bhadra-aware, Karva Chauth, Akshaya Tritiya…),
626
- regional (Gudi Padwa, Gangaur, Teej variants, Onam, Chhath…), monthly
627
- observances (Masik Shivaratri, Pushya days, Shravan Somvar…).
518
+ 📖 [Festivals →](https://dharmagya.app/docs/panchang-ts/festivals)
628
519
 
629
520
  ```typescript
630
- r.festivals.forEach(f => {
631
- // key: stable, language-independent id'diwali', 'makar_sankranti',
632
- // type: major | minor | ekadashi | smarta_ekadashi | vaishnava_ekadashi
633
- // | pradosha | sankranti | eclipse
634
- console.log(f.key, f.name, f.type, f.deferralDate);
635
- });
636
-
637
- // `name` is localized, so match on `key` — never on `name`.
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`:
638
523
  const hasDiwali = r.festivals.some(f => f.key === 'diwali');
639
- ```
640
-
641
- `key` is on engine results (`getDailyPanchang`, `getInstantPanchang`,
642
- `computeFestivalsInRange`). Entries read back out of a `buildFestivalsTable` table
643
- carry `name` / `type` / `description` only.
644
-
645
- ### Regional scoping
646
524
 
647
- `region` scopes regional variants to one Indian state. Pan-Indian festivals
648
- emit regardless.
525
+ // Scope regional variants: 21 state slugs + 'nepal' + 'all' (default)
526
+ getDailyPanchang(jan14, chennai, { timezone: 330, region: 'tamil-nadu' });
649
527
 
650
- ```typescript
651
- // All regional variants (default):
652
- getDailyPanchang(jan14, chennai, { timezone: 330 })!.festivals.map(f => f.name);
653
- // → ["Sankranti","Makar Sankranti","Pongal","Uttarayan","Magh Bihu","Ayyappa Makara Jyothi"]
654
-
655
- // Tamil Nadu only:
656
- getDailyPanchang(jan14, chennai, { timezone: 330, region: 'tamil-nadu' })!
657
- .festivals.map(f => f.name);
658
- // → ["Sankranti","Makar Sankranti","Pongal"]
659
-
660
- // Lohri fires on the Hindu day BEFORE Makara transit, in Punjab/Haryana/Himachal scope:
661
- getDailyPanchang(jan13, amritsar, { timezone: 330, region: 'punjab' })!
662
- .festivals.some(f => f.name === 'Lohri'); // true
663
- ```
664
-
665
- `FestivalRegion` covers 21 Indian states + `'nepal'` + `'all'` (default). The
666
- legacy slugs `'tamil'`, `'bengal'`, `'north-india'` are still accepted and
667
- mapped internally.
668
-
669
- ### Pre-computed table — build your own and cache it
670
-
671
- If you want festival *dates* without running the engine in your app, compute a
672
- table once with `buildFestivalsTable`, cache the JSON, and read it back through
673
- the engine-free `panchang-ts/festivals` entry point.
674
-
675
- **The library ships no pre-computed table.** Festival dates are
676
- observer-dependent — canonical times (nishita / pradosha / chandrodaya …) shift
677
- with the timezone offset, so a table built for one place can be ±1 day wrong
678
- elsewhere — and any table baked into the package would also go stale. Building
679
- your own means it is correct for *your* users and covers whatever years you
680
- want.
681
-
682
- ```typescript
683
- import { buildFestivalsTable } from 'panchang-ts'; // uses the engine
684
- import {
685
- readFestivalsForYear,
686
- readFestivalsForDate,
687
- readFestivalsYearRange,
688
- } from 'panchang-ts/festivals'; // engine-free
689
-
690
- // Build once — at your build time, or on first launch in the background.
691
- const table = buildFestivalsTable({
692
- location: { latitude: 25.3176, longitude: 82.9739 }, // Varanasi
693
- timezoneOffsetMinutes: 330, // IST; -300 = US Eastern, 0 = UK
694
- startYear: 2024,
695
- endYear: 2031,
696
- languages: ['en', 'hi'], // drop 'hi' to halve the size
697
- referenceLocation: 'Varanasi',
698
- });
699
- // …persist `table` as JSON (disk / MMKV / your bundler's asset pipeline).
700
-
701
- // Later reads are instant lookups — no engine, no ephemeris.
702
- readFestivalsYearRange(table); // { start: 2024, end: 2031 }
703
- readFestivalsForYear(table, 2026)!.length; // ~150 festival days
704
- const diwali = readFestivalsForYear(table, 2026)!
705
- .find(d => d.festivals.some(f => f.name === 'Diwali'))!.date;
706
- readFestivalsForDate(table, diwali); // [Narak Chaturdashi, Diwali]
707
- readFestivalsForDate(table, diwali, 'hi'); // [नरक चतुर्दशी, दिवाली]
528
+ // Pre-computed table: build once, cache the JSON, read engine-free.
529
+ import { buildFestivalsTable } from 'panchang-ts';
530
+ import { readFestivalsForYear, readFestivalsForDate } from 'panchang-ts/festivals';
708
531
  ```
709
532
 
710
- `panchang-ts/festivals` imports no astronomy code, so a client bundle that only
711
- *reads* a table never pulls in the engine. Keep `buildFestivalsTable` on the
712
- build/server side (or behind a one-time on-device warm-up) and ship only the
713
- JSON.
714
-
715
- Eclipses are excluded here — visibility is location-dependent, so they get their
716
- own table at `panchang-ts/eclipses` (see [Eclipses](#eclipses)).
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).
717
536
 
718
- `npm run festivals:gen` is a worked example of the whole pattern; it writes a
719
- rolling 2-past / 5-future window to `./festivals.json` (or a path you pass).
537
+ ## Eclipses & Moon Phases
720
538
 
721
- **Other notes:** Karva Chauth / Dhanteras / Diwali emit with Purnimanta paksha
722
- naming.
723
-
724
- ## Eclipses
539
+ 📖 [Eclipses & Moon Phases →](https://dharmagya.app/docs/panchang-ts/eclipses-moon-phases)
725
540
 
726
541
  ```typescript
727
- const r = getDailyPanchang(date, loc, { timezone: 330 })!;
728
542
  if (r.eclipse) {
729
- r.eclipse.kind; // 'solar' | 'lunar'
730
- r.eclipse.subtype; // 'partial' | 'total' | 'annular' | 'penumbral'
731
- r.eclipse.obscuration; // 0..1 fraction of the disc AREA covered
732
- r.eclipse.magnitude; // catalogue magnitude — DIAMETER fraction;
733
- // >1 when total, negative when penumbral
734
- r.eclipse.visibleFromLocation; // body above horizon at peak?
735
- 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
736
546
  r.eclipse.sutakStart; r.eclipse.sutakEnd;
737
- // Sutak: 12 h (4 prahara) before solar, 9 h (3 prahara) before lunar
738
547
  }
739
548
 
740
- import { getUpcomingSolarEclipse, getUpcomingLunarEclipse } from 'panchang-ts';
741
- const next = getUpcomingSolarEclipse(new Date(), loc, 365 /* days */);
742
- ```
743
-
744
- ### Pre-computed table — build your own and cache it
549
+ import { getUpcomingSolarEclipse, computeMoonPhasesInRange } from 'panchang-ts';
550
+ getUpcomingSolarEclipse(new Date(), loc, 365);
551
+ computeMoonPhasesInRange(start, end); // precise new/quarter/full instants
745
552
 
746
- Same pattern as festivals: build a table with `buildEclipsesTable`, cache it,
747
- read it back through the engine-free `panchang-ts/eclipses` entry point.
748
-
749
- **No table is bundled.** Which eclipses are visible — and therefore which carry
750
- `sutak` — is location-dependent, so a table is only meaningful for the place it
751
- was built for.
752
-
753
- ```typescript
754
- import { buildEclipsesTable } from 'panchang-ts'; // uses the engine
755
- import {
756
- readEclipsesForYear,
757
- readEclipsesForDate,
758
- readEclipsesYearRange,
759
- } from 'panchang-ts/eclipses'; // engine-free
760
-
761
- const table = buildEclipsesTable({
762
- location: { latitude: 25.3176, longitude: 82.9739 }, // Varanasi
763
- timezoneOffsetMinutes: 330,
764
- startYear: 2024,
765
- endYear: 2031,
766
- languages: ['en', 'hi'],
767
- // visibleOnly: false → also include eclipses below the horizon (no sutak)
768
- });
769
- // …persist `table` as JSON, then:
770
-
771
- readEclipsesYearRange(table); // { start: 2024, end: 2031 }
772
- const e = readEclipsesForYear(table, 2025)![0].eclipses[0];
773
- e.kind; // 'lunar'
774
- e.subtype; // 'total'
775
- e.start; e.peak; e.end; // ISO UTC strings
776
- e.obscuration; // 0..1 disc area covered at peak
777
- e.magnitude; // catalogue magnitude (diameter); >1 total, <0 penumbral
778
- e.visibleFromLocation; // visible during any phase?
779
- e.visibleAtPeak; // is greatest eclipse itself above the horizon?
780
- e.sutak; // { start, end } — see note below
781
- readEclipsesForDate(table, '2025-09-07', 'hi'); // [पूर्ण चंद्र ग्रहण]
553
+ // Engine-free tables: panchang-ts/eclipses and panchang-ts/moon-phases
782
554
  ```
783
555
 
784
- By default a table lists every eclipse **visible from the location during any
785
- phase** (so one already in progress at moon/sunrise or moon/sunset is included);
786
- `visibleAtPeak` tells you whether greatest eclipse itself is observable.
787
-
788
- Solar eclipses report the subtype seen **locally** (a globally-total eclipse may
789
- read `partial` from a given place). The `sutak` window is present only where it
790
- applies — all visible solar eclipses and visible **umbral** (partial/total)
791
- lunar eclipses; **penumbral** lunar eclipses carry no `sutak` and are not
792
- religiously observed (drik / pandit consensus).
793
-
794
- For one-off astronomical detail without building a table, use
795
- `getUpcomingEclipses` / `computeEclipsesInRange` from the main entry.
796
-
797
- `npm run eclipses:gen` is a worked example; it writes a rolling 2-past /
798
- 5-future window to `./eclipses.json` (or a path you pass).
799
-
800
- ## Moon Phases
556
+ ## Muhurta Engine
801
557
 
802
- The four principal lunar phases — **new** (Amavasya), **first quarter**,
803
- **full** (Purnima), **last quarter** — as precise instants. (These are the
804
- astronomical quarter moments, distinct from the same-named *tithis*, which are
805
- ~24h windows.)
558
+ 📖 [Muhurta Engine →](https://dharmagya.app/docs/panchang-ts/muhurta)
806
559
 
807
560
  ```typescript
808
- import { computeMoonPhasesInRange } from 'panchang-ts';
809
- const phases = computeMoonPhasesInRange(new Date('2026-01-01'), new Date('2026-12-31'));
810
- phases.forEach(p => console.log(p.phase, p.time.toISOString())); // ~49 / year
811
- ```
812
-
813
- ### Pre-computed table — build your own and cache it
814
-
815
- Same pattern again, at `panchang-ts/moon-phases`. Phases are **global instants**,
816
- so `buildMoonPhasesTable` takes only a `timezoneOffsetMinutes` (no coordinates)
817
- — the timezone just decides which calendar date each instant lands on (a new
818
- moon at 19:52 UTC on Jan 18 is listed under Jan 19 in IST).
561
+ import { scoreMuhurta, computeAuspiciousDatesInRange, vivahRule } from 'panchang-ts';
819
562
 
820
- ```typescript
821
- import { buildMoonPhasesTable } from 'panchang-ts'; // uses the engine
822
- import {
823
- readMoonPhasesForYear,
824
- readMoonPhasesForDate,
825
- readMoonPhasesYearRange,
826
- } from 'panchang-ts/moon-phases'; // engine-free
827
-
828
- const table = buildMoonPhasesTable({
829
- timezoneOffsetMinutes: 330, // IST; -300 = US Eastern
830
- startYear: 2024,
831
- endYear: 2031,
832
- languages: ['en', 'hi'],
833
- });
834
- // …persist `table` as JSON, then:
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
835
567
 
836
- readMoonPhasesYearRange(table); // { start: 2024, end: 2031 }
837
- readMoonPhasesForYear(table, 2026)!.length; // ~49 phase days
838
- readMoonPhasesForDate(table, '2026-01-03'); // [{ phase: 'full', name: 'Full Moon', … }]
839
- readMoonPhasesForDate(table, '2026-01-03', 'hi'); // [{ phase: 'full', name: 'पूर्णिमा', … }]
568
+ computeAuspiciousDatesInRange(vivahRule, start, end, DELHI, { timezone: 330 });
840
569
  ```
841
570
 
842
- Each entry carries `phase`, the phase `time` (ISO UTC), and `en` + `hi` text.
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).
843
575
 
844
- `npm run moon-phases:gen` is a worked example; it writes a rolling 2-past /
845
- 5-future window to `./moonPhases.json` (or a path you pass).
576
+ ## Planetary Positions & Birth Charts
846
577
 
847
- ## Planetary Positions
578
+ 📖 [Birth Charts →](https://dharmagya.app/docs/panchang-ts/birth-chart)
848
579
 
849
580
  ```typescript
850
- import { computePlanetaryPositions, GRAHA_ABBR } from 'panchang-ts';
581
+ import {
582
+ computePlanetaryPositions, computeLagna, computeBhava,
583
+ computeRashiChart, computeNavamsa, computeDivisionalChart, computeDignity,
584
+ } from 'panchang-ts';
851
585
 
852
586
  const g = computePlanetaryPositions(new Date(), 'lahiri');
853
- g.jupiter.rashi.name; // "Dhanu"
854
- g.jupiter.degreeInRashi; // 18.42
855
- g.jupiter.nakshatra.name; // "Purva Ashadha"
856
- g.jupiter.nakshatra.pada; // 3
857
- g.saturn.isRetrograde;
858
- GRAHA_ABBR['Jupiter']; // "Ju"
859
-
860
- // True node (sharper Rahu/Ketu via Meeus periodic correction)
861
- 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'
862
593
  ```
863
594
 
864
- ## Dashas
595
+ ## Dashas & Personal Transits
865
596
 
866
- Five classical systems:
597
+ 📖 [Dashas & Transits →](https://dharmagya.app/docs/panchang-ts/dashas)
867
598
 
868
599
  ```typescript
869
600
  import {
870
601
  computeVimshottariDashaFromBirth, computeVimshottariPratyantar,
871
- computeAshtottariDasha, computeYoginiDasha, computeCharaDasha, computeNarayanDasha,
602
+ computeAshtottariDasha, computeYoginiDasha, computeCharaDasha,
603
+ computeNarayanDasha, computeSadeSati,
872
604
  } from 'panchang-ts';
873
605
 
874
- // 1. Vimshottari — 120-year, 9-lord, with 3-level Maha→Antar→Pratyantar.
875
- const vim = computeVimshottariDashaFromBirth(birth, 'lahiri');
876
- const pratyantars = computeVimshottariPratyantar(vim.mahaDashas[0]!.antarDashas[0]!);
877
-
878
- // 2. Ashtottari — 108-year, 8-lord cycle (no Ketu).
879
- const ash = computeAshtottariDasha(birth, moonLon);
880
-
881
- // 3. Yogini — 36-year, 8 yoginis.
882
- const yog = computeYoginiDasha(birth, moonLon);
883
- yog.mahaDashas[0]!.yogini; // 'Dhanya'
884
- yog.mahaDashas[0]!.lord; // 'Jupiter'
885
-
886
- // 4. Chara (Jaimini) — sign-based, 9-8-7 years per modality, forward only.
887
- const cha = computeCharaDasha(birth, loc);
888
-
889
- // 5. Narayan (Jaimini) — sign-based, parity-based direction.
890
- // Vishama-pada lagna {Aries, Taurus, Gemini, Libra, Scorpio, Sag} → forward
891
- // Sama-pada lagna {Cancer, Leo, Virgo, Capricorn, Aquarius, Pisces} → backward
892
- const nar = computeNarayanDasha(birth, loc);
893
- nar.direction; // 'forward' | 'backward'
894
-
895
- // Narayan variable-duration variant (Sanjay Rath):
896
- const narV = computeNarayanDasha(birth, loc, 'lahiri', { duration: 'variable' });
897
- narV.mahaDashas[0]!.years; // 0..12 from rashi-to-lord count (+1 exalt, -1 debil)
898
- ```
899
-
900
- ## Personal Transits
901
-
902
- ```typescript
903
- const r = getDailyPanchang(date, loc, {
904
- timezone: 330,
905
- janmaRashi: 3, // 0 = Mesha … 11 = Meena
906
- janmaNakshatra: 0, // 0 = Ashwini … 26 = Revati
907
- })!;
908
- r.chandraBalam; // { house, quality: 'strong'|'weak', name, englishName } — null without janmaRashi
909
- r.tarabala; // { taraIndex, name, englishName, quality } — null without janmaNakshatra
910
-
911
- import { computeSadeSati } from 'panchang-ts';
912
- const ss = computeSadeSati(natalMoonRashiIndex, new Date());
913
- // → { active, phase: 1|2|3|null, currentArcStart, currentArcEnd, nextArcStart }
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.
914
610
  ```
915
611
 
916
- ## Birth Chart
612
+ ## Strength, Yogas & Karakas
917
613
 
918
- Sidereal Lagna, Bhava under three house systems, D1 + six classical divisional
919
- charts (D2/D3/D7/D9/D10/D12/D30), and Planetary Dignity.
614
+ 📖 [Strength, Yogas & Karakas →](https://dharmagya.app/docs/panchang-ts/strength-yogas)
920
615
 
921
616
  ```typescript
922
617
  import {
923
- computeLagna, computeBhava, computeRashiChart, computeNavamsa,
924
- computeDivisionalChart, computeDignity,
618
+ computeAspects, computeShadbala, computeBhavaBala,
619
+ computeAshtakavarga, computeYogas, computeJaiminiKarakas,
925
620
  } from 'panchang-ts';
926
621
 
927
- const birth = new Date('1995-08-15T05:30:00Z');
928
- const loc = { latitude: 28.6139, longitude: 77.2090 };
929
-
930
- const lagna = computeLagna(birth, loc, 'lahiri', 'en');
931
-
932
- // Bhava — 'whole-sign' (default) | 'equal' | 'placidus-kp'.
933
- // Placidus-KP throws PanchangError('CIRCUMPOLAR') beyond ±66.5°.
934
- const houses = computeBhava(birth, loc, { houseSystem: 'whole-sign' });
935
-
936
- // `chart.planets` is the ordered list; `chart.byPlanet` is the same nine
937
- // placements keyed by graha, for direct lookup without a linear scan.
938
- const chart = computeRashiChart(birth, loc);
939
- chart.byPlanet.Mars.house; // instead of chart.planets.find(...)!
940
- chart.planets.map(p => p.rashi); // iterate the list as before
941
-
942
- // D1 — full Rashi chart with 9-graha house placement.
943
- const d1 = computeRashiChart(birth, loc, { houseSystem: 'whole-sign' });
944
- d1.planets.find(p => p.planet === 'Jupiter')?.house;
945
- d1.planets.find(p => p.planet === 'Saturn')?.isRetrograde;
946
-
947
- // Divisional charts (D2 Hora, D3 Drekkana, D7 Saptamsa, D9 Navamsa,
948
- // D10 Dasamsa, D12 Dwadasamsa, D30 Trimsamsa).
949
- const d9 = computeNavamsa(birth, loc);
950
- const d10 = computeDivisionalChart(birth, loc, 'D10');
951
- const d30 = computeDivisionalChart(birth, loc, 'D30');
952
-
953
- // Planetary dignity (BPHS Ch.3-4).
954
- computeDignity('Mars', 0); // 'moolatrikona' (Aries)
955
- computeDignity('Mars', 9); // 'exalted' (Capricorn)
956
- 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' });
957
626
  ```
958
627
 
959
- Birth-chart helpers accept the full ayanamsa set including `'true-chitra'` and
960
- `'thirukanitham'`.
961
-
962
628
  ## Compatibility & Doshas
963
629
 
630
+ 📖 [Matching & Doshas →](https://dharmagya.app/docs/panchang-ts/matching-doshas)
631
+
964
632
  ```typescript
965
633
  import {
966
634
  computeAshtakoot, computePathuPorutham,
967
- computeMangalDosha, computeKaalSarp, computePitruDosha,
635
+ computeMangalDosha, computeMangalCompatibility, computeKaalSarp, computePitruDosha,
968
636
  } from 'panchang-ts';
969
637
 
970
- // Ashtakoot (North Indian, 36-point) Varna, Vashya, Tara, Yoni,
971
- // Graha Maitri, Gana, Bhakoot, Nadi (max 1/2/3/4/5/6/7/8).
972
- const match = computeAshtakoot(
973
- { rashi: 4, nakshatra: 9 },
974
- { rashi: 0, nakshatra: 1 },
975
- );
638
+ computeAshtakoot({ rashi: 4, nakshatra: 9 }, { rashi: 0, nakshatra: 1 });
976
639
  // → { totalScore: 0..36, koots: KootScore[8], cancellations: string[] }
977
640
 
978
- // Opt-in Bhakoot cancellations need extra natal data:
979
- // `lagnaRashi` enables same-lagna-lord + same-7th-lord rules;
980
- // `navamsaRashi` enables the same-Navamsa-lord rule.
981
- const richer = computeAshtakoot(
982
- { rashi: 4, nakshatra: 9, lagnaRashi: 7, navamsaRashi: 2 },
983
- { rashi: 0, nakshatra: 1, lagnaRashi: 1, navamsaRashi: 5 },
984
- );
985
-
986
- // Manglik is a PAIRWISE verdict, not a per-chart one: when both partners are
987
- // Manglik the two afflictions neutralise each other, so the pair is clean
988
- // where a Manglik/non-Manglik pair is not.
989
- const m = computeMangalCompatibility(boyChart, girlChart);
990
- m.afflicted; // false when neither is Manglik AND when both are
991
- m.cancellations; // ['both natives Manglik — mutual cancellation']
992
- m.boy; m.girl; // each native's own MangalDoshaInfo, severity included
993
-
994
- // Pathu Porutham (Tamil/Kerala, 10-fold) — binary pass/fail per koot.
995
- // Three vetoes (Yoni, Rajju, Vedha) flip `recommended` regardless of count.
996
- const tp = computePathuPorutham(
997
- { rashi: 4, nakshatra: 9 },
998
- { rashi: 0, nakshatra: 1 },
999
- );
1000
- tp.totalPasses; // 0..10
1001
- tp.recommended; // no veto + ≥5 passes
1002
-
1003
- // Doshas
1004
- computeMangalDosha(d1);
1005
- // Mars in 1/2/4/7/8/12 from Lagna, Moon, AND Venus (Drik rule set).
1006
- // Cancellations: Mars in own sign/exalted, conjunct Jup/Moon/Venus,
1007
- // or aspected by Jupiter (5/7/9 sign-aspect).
1008
- // Severity (anshik/purna) is computed pre-cancellation.
1009
-
1010
- computeKaalSarp(d1);
1011
- // 12 subtypes by Rahu's house: anant, kulik, vasuki, shankhpal, padma,
1012
- // mahapadma, takshak, karkotak, shankhachud, ghatak, vishdhar, sheshnag.
1013
-
1014
- computePitruDosha(d1);
1015
- // Pandit-consensus 4-trigger set (rules cited by ≥3 of 6 surveyed
1016
- // pandit sources): Sun+Rahu conjunction (any house), Sun+Saturn
1017
- // conjunction (any house), Rahu in 9th house, 9th-lord conjunct Rahu.
1018
- // Drik panchang publishes no Pitru calculator; minority/expansive
1019
- // rules (Sun in 9th alone, Ketu in 4th, 9th lord in dusthana, etc.)
1020
- // are intentionally excluded.
1021
- ```
1022
-
1023
- **Limitations.** Ashtakoot Vashya koot uses single-vashya per rashi.
1024
- Bhakoot Parivartana (rashi-lord exchange) cancellation needs per-graha
1025
- position data not carried by the `NatalMoon` shape and is not modelled.
1026
-
1027
- ## Strength & Aspects
1028
-
1029
- ```typescript
1030
- import {
1031
- computeAspects, computeShadbala, computeBhavaBala, computeAshtakavarga,
1032
- } from 'panchang-ts';
1033
-
1034
- // Drishti — every graha aspects the 7th; malefics gain extras
1035
- // (Mars 4+8, Jupiter 5+9, Saturn 3+10). Node aspect mode is configurable:
1036
- const aspects = computeAspects(d1); // BPHS 7th-only on nodes
1037
- const aspExt = computeAspects(d1, { nodeAspects: '5-and-9' }); // KP/BV Raman extension
1038
-
1039
- // Shadbala — 7 visible grahas, 6 components, in Virupas (60 V = 1 Rupa).
1040
- // Sthana = Uchcha + Saptavargaja (D1/D2/D3/D7/D9/D12/D30 dignity sum)
1041
- // + Ojha-Yugma (rashi+navamsa parity) + Drekkana (gender decanate).
1042
- // Range [0, 420 V]. Dig is directional cusp; Kala = Nathonatha + Paksha;
1043
- // Chesta is retrograde-bucket; Naisargika is fixed rank; Drik is weighted aspects.
1044
- const bala = computeShadbala(birth, loc);
1045
-
1046
- // Bhava Bala — 12-bhava strength built on top of Shadbala.
1047
- // Per-bhava: { bhavadhipati, dik, drik, sthana, total }.
1048
- const bhavaBala = computeBhavaBala(birth, loc);
1049
-
1050
- // Ashtakavarga — 12-rashi bindu grids (BPHS Ch. 66).
1051
- const av = computeAshtakavarga(d1);
1052
- av.sarvashtaka; // 12 cells, each 0..56, total 336
1053
- av.bhinnashtaka.Jupiter; // 12-cell grid; Jupiter total = 56 (chart-invariant)
1054
- // Other invariants: Sun=47, Moon=49, Mars=39, Mercury=54, Venus=52, Saturn=39.
1055
-
1056
- // Trikona + Ekadhipatya Sodhana reductions (BPHS Ch. 67):
1057
- const avR = computeAshtakavarga(d1, { reductions: true });
1058
- avR.reduced!.sarvashtaka;
641
+ computeMangalCompatibility(boyChart, girlChart); // Manglik is a PAIRWISE verdict
642
+ computeKaalSarp(d1); // 12 subtypes by Rahu's house
1059
643
  ```
1060
644
 
1061
- Rahu and Ketu are not Ashtakavarga receivers or contributors (classical
1062
- Parashara scheme).
645
+ ## Annual Charts, Sensitive Points, KP & Prashna
1063
646
 
1064
- ## Yogas & Karakas
1065
-
1066
- ```typescript
1067
- import { computeYogas, computeJaiminiKarakas } from 'panchang-ts';
1068
-
1069
- // ~25 named yogas — Pancha Mahapurusha (Ruchaka/Bhadra/Hamsa/Malavya/Sasha),
1070
- // lunar (Gajakesari, Sunapha, Anapha, Durudhura, Kemadruma), solar
1071
- // (Budha-Aditya, Veshi, Vasi, Ubhayachari), Raja (kendra/trikona-lord,
1072
- // Dharma-Karmadhipati, Vipareeta, Lakshmi), Dhana (2-11, 5-9, Vasumati),
1073
- // Vargottama, Yogakaraka, Neecha Bhanga, Daridra.
1074
- const yogas = computeYogas(d1);
1075
- // → [{ name, type, reasons[], bhanga?: { applies, reasons[] } }, …]
1076
-
1077
- // Optional cancellation annotations: 5 Pancha Mahapurusha + Gajakesari
1078
- // surface `bhanga` (Sun/Moon conjunct or Jupiter combust/debilitated).
1079
- // Neecha Bhanga: dispositor in kendra from Lagna OR Moon; lord-of-
1080
- // exaltation-rashi in kendra from Lagna or Moon; mutual exchange;
1081
- // dispositor aspecting the debilitated planet.
1082
-
1083
- // Filter by type / pass D9 for Vargottama:
1084
- const d9 = computeNavamsa(birth, loc);
1085
- const all = computeYogas(d1, { types: ['raja','dhana'], navamsa: d9 });
1086
-
1087
- // Jaimini Karakas — Atmakaraka (highest degree-in-rashi) … Darakaraka (lowest).
1088
- const k7 = computeJaiminiKarakas(d1); // 7-graha Parashara default
1089
- const k8 = computeJaiminiKarakas(d1, { variant: '8-jaimini' }); // adds Rahu (degree reversed),
1090
- // inserts Pitrukaraka at 5th
1091
- ```
1092
-
1093
- Yoga and Karaka names are English/transliterated proper nouns and intentionally
1094
- **not** locale-resolved.
1095
-
1096
- ## 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)
1097
649
 
1098
650
  ```typescript
1099
651
  import {
1100
652
  computeVarshaphala, computeTithiPravesha, computeArudhas,
1101
653
  computeHoraLagna, computeGhatiLagna, computeBhavaLagna, computeSripatiLagna,
1102
654
  computeUpagrahas, computeArgala,
1103
- } from 'panchang-ts';
1104
-
1105
- // Varshaphala — Tajik annual chart for the Nth solar return.
1106
- const v = computeVarshaphala(birth, 30, loc);
1107
- v.solarReturnInstant;
1108
- v.varshaLagna.rashi.name;
1109
- v.muntha.rashi; v.muntha.house; // muntha = (natalLagnaRashi + 30) mod 12
1110
- v.yearLord; // strongest of 4 candidates by Shadbala
1111
- v.sahams.Punya.house;
1112
- v.sahams.Vivaha.rashi;
1113
- // 27 Sahams: Punya, Vidya, Yasas, Mitra, Karma, Vivaha, Putra, Roga, Marana,
1114
- // Rajya, Raja, Bandhu, Dharma, Gnati, Apamrityu, Bhratri, Matri, Pitri, Sama,
1115
- // Bandhana, Karyasiddhi, Vyapara, Sastra, Asha, Labha, Susha, Tapas.
1116
-
1117
- // Tithi Pravesha — annual chart cast when Sun is in natal sidereal sign AND
1118
- // Sun-Moon separation equals natal separation. Preserves natal tithi exactly.
1119
- const tp = computeTithiPravesha(birth, 30, loc);
1120
- tp.natalTithi === tp.praveshTithi; // always true
1121
-
1122
- // Arudha padas — image/reflection of each bhava. Arudha[0] = Arudha Lagna (AL).
1123
- const a = computeArudhas(d1);
1124
- a[0]!.bhava; // 1 — AL
1125
- a[0]!.arudhaRashi; // 0..11
1126
- a[6]!.bhava; // 7 — Darapada (spouse pada)
1127
-
1128
- // Special lagnas — time-derived sensitive points from sunrise on/before birth.
1129
- computeHoraLagna(birth, loc); // 30°/hour (1 rashi/hour)
1130
- computeGhatiLagna(birth, loc); // 75°/hour (1 rashi/24 min)
1131
- computeBhavaLagna(birth, loc); // 15°/hour (1 rashi/2 hours)
1132
- computeSripatiLagna(birth, loc); // = natal lagna (cusp 1)
1133
-
1134
- // Sripati cusps 2–12 (opt-in) — 4 angular cusps + trisected intermediates.
1135
- // Defined at every latitude (unlike Placidus).
1136
- const sripati = computeSripatiLagna(birth, loc, 'lahiri', 'en', { includeCusps: true });
1137
- sripati.cusps; // number[12] of bhava madhyas; cusps[0/3/6/9] = ASC/IC/DSC/MC
1138
-
1139
- // Upagrahas — Gulika, Mandi (rising-asc at Saturn segment start/midpoint),
1140
- // plus Sun-derived Dhuma, Vyatipata, Parivesha, Indrachapa, Upaketu.
1141
- const u = computeUpagrahas(birth, loc);
1142
- u.gulika.longitude; u.gulika.rashi; u.gulika.house;
1143
-
1144
- // Argala (Jaimini) — planets in 2/4/11 from a bhava form Argala (intervention);
1145
- // 3/10/12 form Virodhargala (counter). Each planet hits exactly 6 of 12 bhavas.
1146
- const arg = computeArgala(d1);
1147
- arg[0]!.argala; arg[0]!.virodhargala;
1148
-
1149
- // Trikonargala (5/9 trine, opt-in) — Ketu reversal: 5th-from → virodhaka,
1150
- // 9th-from → source.
1151
- const argT = computeArgala(d1, { includeTrikonargala: true });
1152
- argT[0]!.trikona!.sources;
1153
- argT[0]!.trikona!.virodhakas;
1154
- ```
1155
-
1156
- ## KP & Prashna
1157
-
1158
- ```typescript
1159
- import {
1160
655
  computeKpSubLord, computeKpCuspalSubLords, computeKpSignificators,
1161
656
  computePrashnaChart,
1162
657
  } from 'panchang-ts';
1163
658
 
1164
- // KP sub-lord at any sidereal longitude (243 sub-divisions across the zodiac,
1165
- // proportional to Vimshottari years).
1166
- const info = computeKpSubLord(45.5); // 15°30' Taurus
1167
- info.signLord; // 'Venus'
1168
- info.starLord; // 'Moon'
1169
- info.subLord;
1170
-
1171
- // Cuspal sub-lords (always Placidus-KP — KP's anchor scheme).
1172
- const cusps = computeKpCuspalSubLords(birth, loc);
1173
- cusps.cusps[0]!.subLord; // ascendant
1174
- cusps.cusps[6]!.subLord; // descendant
1175
-
1176
- // Significators — for each planet, the houses it signifies via the 4-fold KP rule
1177
- // (occupant + star-lord-occupant + owner + star-lord-owner).
1178
- const sig = computeKpSignificators(d1);
1179
- sig.byPlanet.Sun;
1180
- sig.byHouse[10];
1181
-
1182
- // Prashna (horary) chart — cast at question moment from querent's location.
1183
- const pchart = computePrashnaChart(
1184
- new Date('2026-05-09T14:30:00Z'),
1185
- { latitude: 19.0760, longitude: 72.8777 },
1186
- );
1187
- pchart.lagna.rashi.name;
1188
- pchart.bhava.system; // 'placidus-kp' by default (KP horary anchor)
1189
- pchart.planets[1]!.house; // Moon — primary mind significator
1190
- ```
1191
-
1192
- Same return shape as a natal `BirthChart`. Pass `{ houseSystem: 'whole-sign' }`
1193
- to `computePrashnaChart` for traditional Vedic Prashna.
1194
-
1195
- ## Muhurta Engine
1196
-
1197
- ```typescript
1198
- import { scoreMuhurta, computeAuspiciousDatesInRange, vivahRule } from 'panchang-ts';
1199
-
1200
- const r = scoreMuhurta(new Date('2026-05-12'), DELHI, vivahRule, { timezone: 330 });
1201
- // → { date, score: 0..100, passes: boolean,
1202
- // reasons: string[], // diagnostic English
1203
- // factors: MuhurtaFactor[] } // { code, axis, index?, delta } — stable
1204
-
1205
- r.factors.filter(f => f.delta < 0); // what cost the day points
1206
- r.factors.some(f => f.axis === 'exclusion'); // hard-excluded?
1207
-
1208
- const dates = computeAuspiciousDatesInRange(
1209
- vivahRule,
1210
- new Date('2026-05-01'),
1211
- new Date('2026-05-31'),
1212
- DELHI,
1213
- { timezone: 330 },
1214
- ); // MuhurtaDay[] sorted by score desc; full panchang attached
1215
-
1216
- // Custom rule (pure data, no engine code needed)
1217
- const myRule: MuhurtaRule = {
1218
- occasion: 'launch_party',
1219
- auspiciousVaras: [3, 4, 5],
1220
- auspiciousNakshatras: [11, 12, 21],
1221
- bhadra: 'penalize', // 'ignore' | 'penalize' | 'exclude'
1222
- excludeEkadashi: true,
1223
- excludeAdhikaMasa: true,
1224
- };
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
1225
663
  ```
1226
664
 
1227
- **Tithi and vara are scored jointly, not just per-anga.** The classical Vara ×
1228
- Tithi yogas — Siddha, Amrita, Dagdha, Visha, Hutasana, Krakacha, Samvartaka —
1229
- are applied to every rule, so a Rikta tithi landing on a Saturday is partly
1230
- redeemed by Siddha yoga rather than flatly penalised. Set
1231
- `varaTithiYogas: false` for the older per-anga-only scoring, or call
1232
- `computeVaraTithiYogas(vara, tithi)` directly. Where an auspicious and an
1233
- inauspicious yoga both fire — a documented ambiguity in the sources — both are
1234
- surfaced as separate factors and allowed to net out.
1235
-
1236
- `bhadra` defaults to `'ignore'`; the stock rules use `'penalize'`. A whole-day
1237
- `'exclude'` is rarely what you want: Vishti karana sits at fixed positions in
1238
- the tithi cycle, so vetoing the day removes seven tithis outright — among them
1239
- Shukla Ekadashi, which the same sources list as *preferred* for vivah. Read
1240
- `panchang.inauspicious.bhadra` for the window and schedule around it.
1241
- `excludeBhadra: true` still works as an alias for `bhadra: 'exclude'`.
1242
-
1243
- 13 stock rules: vivah, griha pravesh, namakarana, vidyarambh, vahan kharidi,
1244
- annaprashan, mundan, upanayanam, karnavedha, aksharabhyasam, seemantham, shop
1245
- opening, travel start.
1246
-
1247
- ### Pre-computed table — build your own and cache it
1248
-
1249
- Scoring a year of days runs the engine ~365 times. If your app asks the same
1250
- question repeatedly, compute the answer once and ship the JSON — the same
1251
- pattern the festival, eclipse and Moon-phase tables use.
1252
-
1253
- ```typescript
1254
- import { buildMuhurtaTable, vivahRule } from 'panchang-ts';
1255
-
1256
- const table = buildMuhurtaTable({
1257
- rule: vivahRule,
1258
- location: { latitude: 25.3176, longitude: 82.9739 },
1259
- timezoneOffsetMinutes: 330,
1260
- startYear: 2026,
1261
- endYear: 2031,
1262
- referenceLocation: 'Varanasi',
1263
- });
1264
- // persist JSON.stringify(table) — 6 years of vivah dates is ~74 KB
1265
- ```
1266
-
1267
- Read it back through the engine-free `panchang-ts/muhurta` entry (~1.7 KB, no
1268
- astronomy code in your bundle):
1269
-
1270
- ```typescript
1271
- import {
1272
- readMuhurtaForYear,
1273
- readMuhurtaForDate,
1274
- readMuhurtaYearRange,
1275
- readMuhurtaOccasion,
1276
- readBestMuhurtaDays,
1277
- } from 'panchang-ts/muhurta';
1278
-
1279
- const table = JSON.parse(await (await fetch('/muhurta-vivah.json')).text());
1280
-
1281
- readMuhurtaOccasion(table); // 'vivah'
1282
- readMuhurtaYearRange(table); // { start: 2026, end: 2031 }
1283
- readMuhurtaForYear(table, 2026); // MuhurtaTableDay[] — passing days, by date
1284
- readMuhurtaForDate(table, '2026-11-11');
1285
- readBestMuhurtaDays(table, 5); // top 5 across the table, highest first
1286
- ```
1287
-
1288
- Only days that **pass** the rule are stored by default; pass
1289
- `includeFailures: true` to keep every day with its score. Scores are location-
1290
- *and* rule-dependent, so a table built for Varanasi and `vivah` says nothing
1291
- about another place or occasion.
1292
-
1293
- `npm run muhurta:gen` is a worked example script
1294
- ([scripts/generate-muhurta-json.ts](scripts/generate-muhurta-json.ts)):
1295
-
1296
- ```bash
1297
- npm run muhurta:gen -- muhurta-vivah.json vivah
1298
- ```
1299
-
1300
- Scoring: starts at 50; +10 per matching auspicious axis (tithi / nakshatra /
1301
- vara / yoga), -15 per inauspicious axis, hard exclusions zero the score.
1302
- Special yogas (Amrit Siddhi, Sarvartha Siddhi, Ravi/Guru Pushya) add +5;
1303
- Jwalamukhi subtracts -10. Clamped 0..100; `passes: true` when score ≥ 50.
1304
-
1305
- Every scoring input appears in both `reasons` (English prose, diagnostic, not a
1306
- stable format) and `factors` (structured, with a stable `code`). Localize and
1307
- filter on `factors`.
1308
-
1309
- `scoreMuhurta` computes only the sections it actually scores against, so it is
1310
- cheaper than a full `getDailyPanchang`. `computeAuspiciousDatesInRange` does not narrow —
1311
- each returned day carries its complete `panchang` for callers to drill into.
1312
-
1313
665
  ## Calendar Conversion
1314
666
 
667
+ 📖 [Calendar Conversion →](https://dharmagya.app/docs/panchang-ts/calendar-conversion)
668
+
1315
669
  ```typescript
1316
670
  import {
1317
671
  convertGregorianToHindu, convertHinduToGregorian,
1318
672
  getKaliYugaYear, getHinduNewYear,
1319
673
  computeEkadashiDatesForYear, computeSankrantisForYear,
1320
- computeFestivalsInRange, getUpcomingEclipses, computeEclipsesInRange,
1321
674
  } from 'panchang-ts';
1322
675
 
1323
- // Gregorian → Hindu coords at sunrise
1324
- const h = convertGregorianToHindu(new Date('2026-04-15'), DELHI, { timezone: 330 });
1325
-
1326
- // Hindu → Gregorian
1327
- const dates = convertHinduToGregorian(
676
+ convertHinduToGregorian(
1328
677
  { vikramSamvat: 2083, masaIndex: 0, paksha: 'shukla', pakshaTithi: 9 },
1329
678
  DELHI, { timezone: 330 },
1330
- ); // → Rama Navami in VS 2083
1331
-
1332
- getKaliYugaYear(new Date('2026-04-01')); // 5127
1333
- getHinduNewYear(2026, 'tamil-nadu', DELHI, { timezone: 330 }); // Puthandu
1334
-
1335
- computeEkadashiDatesForYear(2026, DELHI, { timezone: 330 }); // ~24 Date[]
1336
- computeSankrantisForYear(2026, DELHI, { timezone: 330 }); // 12 SankrantiEvent[]
1337
- computeFestivalsInRange(start, end, DELHI, { timezone: 330 }); // FestivalDay[]
1338
- getUpcomingEclipses(new Date(), DELHI, 5);
679
+ ); // → Date[] (Rama Navami VS 2083)
680
+ getHinduNewYear(2026, 'tamil-nadu', DELHI, { timezone: 330 }); // region-aware
1339
681
  ```
1340
682
 
1341
- `getHinduNewYear` is region-aware: Tamil Nadu / Kerala / Punjab / Bengal / Assam
1342
- use the **solar** (Mesha Sankranti) anchor; elsewhere uses **Chaitra Shukla
1343
- Pratipada** (Ugadi / Gudi Padwa / Cheti Chand). When Pratipada is a kshaya
1344
- tithi (e.g. Ugadi 2026), falls back to the Amanta-Chaitra-masa boundary.
1345
-
1346
683
  ## Localization & Configuration
1347
684
 
1348
- ```typescript
1349
- const hi = getDailyPanchang(date, loc, { timezone: 330, language: 'hi' })!;
1350
- hi.angas.tithis[0].name; // "कृष्ण चतुर्दशी"
1351
- hi.angas.vara.name; // "मंगलवार"
1352
- hi.calendar.chandramasa.name; // "माघ"
1353
- hi.periods.choghadiya.day[0].name; // "अमृत"
1354
- hi.angas.vara.englishName; // "Tuesday" — englishName always English
1355
-
1356
- // All options:
1357
- const r = getDailyPanchang(date, loc, {
1358
- timezone: 330, // number (UTC offset min) or IANA string
1359
- ayanamsa: 'lahiri', // lahiri | raman | krishnamurti | true-chitra | thirukanitham
1360
- language: 'en', // en | hi
1361
- masaSystem: 'purnimanta', // purnimanta | amanta
1362
- region: 'all', // 21 state slugs + 'nepal' + 'all'
1363
- computeEndTimes: true, // false → skip transition searches
1364
- sections: undefined, // undefined = all; see Performance
1365
- janmaRashi: undefined, // pass to populate r.chandraBalam (else null)
1366
- janmaNakshatra: undefined, // pass to populate r.tarabala (else null)
1367
- });
1368
- ```
1369
-
1370
- **Timezone.** Number (minutes from UTC, e.g. `330` for IST) or an IANA zone
1371
- name (e.g. `'America/New_York'`). IANA strings need `Intl`, which older Hermes
1372
- versions lack — pass a number on those targets. DST resolves automatically for
1373
- IANA zones.
1374
-
1375
- ---
1376
-
1377
- ### Localized vs machine-readable fields
1378
-
1379
- Every user-facing string follows `language`. Where a value is also meaningful to
1380
- code, the two are separate fields — the stable key never changes with language:
1381
-
1382
- | Machine-readable | Localized display |
1383
- |---|---|
1384
- | `festival.key` (`'diwali'`) | `festival.name` (`"दिवाली"`) |
1385
- | `bhadra.location` (`'paatal'`) | `bhadra.locationName` (`"पाताल"`) |
1386
- | `eclipse.kind` / `eclipse.subtype` | `eclipse.description` |
1387
- | `muhurtaScore.factors[].code` | `muhurtaScore.reasons` (English only) |
1388
-
1389
- `MuhurtaScore.reasons` is diagnostic English and not a stable format; use
1390
- `factors` for anything shown to a user or branched on in code.
1391
-
1392
- ## Types & Exports
1393
-
1394
- <details>
1395
- <summary><strong>Core, Pancha Anga, Festivals</strong></summary>
1396
-
1397
- ```typescript
1398
- interface GeoLocation { latitude: number; longitude: number; elevation?: number; }
1399
- interface TimePeriod { start: Date; end: Date; }
1400
-
1401
- interface TithiInfo {
1402
- index: number; // 0-29
1403
- name: string;
1404
- paksha: string; // "Shukla"/"Krishna" (en), "शुक्ल"/"कृष्ण" (hi)
1405
- number: number; // 1-15 within the paksha
1406
- completionPercentage: number;
1407
- endTime: Date | null;
1408
- }
1409
- // NakshatraInfo, YogaInfo, KaranaInfo follow the same pattern.
1410
- // DailyTithiInfo extends with startTime + isActiveAtSunrise.
1411
-
1412
- interface VaraInfo {
1413
- index: number; // 0 = Sunday … 6 = Saturday
1414
- name: string; // localized (e.g. "Raviwara")
1415
- shortName: string;
1416
- englishName: string; // always English
1417
- }
1418
-
1419
- interface FestivalInfo {
1420
- key: string; // stable, language-independent id — match on this
1421
- name: string; // localized — display only
1422
- type: 'major' | 'minor' | 'ekadashi' | 'smarta_ekadashi' | 'vaishnava_ekadashi'
1423
- | 'pradosha' | 'sankranti' | 'eclipse';
1424
- description?: string;
1425
- deferralDate?: Date; // Smarta Ekadashi → Dwadashi fast date
1426
- }
1427
-
1428
- type FestivalRegion =
1429
- | 'all'
1430
- | 'tamil-nadu' | 'kerala' | 'karnataka' | 'andhra-pradesh' | 'telangana'
1431
- | 'west-bengal' | 'odisha' | 'assam' | 'bihar' | 'jharkhand'
1432
- | 'gujarat' | 'maharashtra' | 'goa' | 'rajasthan'
1433
- | 'punjab' | 'haryana' | 'himachal-pradesh' | 'uttarakhand'
1434
- | 'uttar-pradesh' | 'madhya-pradesh'
1435
- | 'nepal';
1436
-
1437
- // Legacy slugs accepted (mapped internally): 'tamil' → 'tamil-nadu',
1438
- // 'bengal' → 'west-bengal', 'north-india' → 'all'.
1439
- ```
1440
-
1441
- </details>
1442
-
1443
- <details>
1444
- <summary><strong>Eclipses & Bhadra</strong></summary>
1445
-
1446
- ```typescript
1447
- interface EclipseInfo {
1448
- kind: 'solar' | 'lunar';
1449
- subtype: 'partial' | 'total' | 'annular' | 'penumbral';
1450
- start: Date; peak: Date; end: Date;
1451
- visibleFromLocation: boolean;
1452
- obscuration: number; // disc AREA covered at peak, [0, 1]
1453
- magnitude: number; // catalogue magnitude — disc DIAMETER covered.
1454
- // Not [0, 1]: >1 for a total eclipse, negative
1455
- // for a penumbral lunar one (the Moon misses
1456
- // the umbra), exactly as NASA's canon prints it.
1457
- sutakStart: Date; // 12 h pre-solar / 9 h pre-lunar
1458
- sutakEnd: Date;
1459
- description: string;
1460
- }
1461
-
1462
- interface BhadraInfo {
1463
- start: Date; end: Date;
1464
- location: 'earth' | 'heaven' | 'paatal'; // 'earth' = malefic for all work
1465
- locationName: string; // localized display name
1466
- isActive: boolean;
1467
- }
1468
- ```
1469
-
1470
- </details>
1471
-
1472
- <details>
1473
- <summary><strong>Jyotish</strong></summary>
685
+ 📖 [Options & Localization →](https://dharmagya.app/docs/panchang-ts/localization)
1474
686
 
1475
687
  ```typescript
1476
- type GrahaName = 'Sun' | 'Moon' | 'Mars' | 'Mercury' | 'Jupiter'
1477
- | 'Venus' | 'Saturn' | 'Rahu' | 'Ketu';
1478
-
1479
- interface GrahaPosition {
1480
- planet: GrahaName;
1481
- siderealLongitude: number;
1482
- rashi: RashiInfo;
1483
- degreeInRashi: number;
1484
- nakshatra: NakshatraInfo;
1485
- isRetrograde: boolean; // always false for Sun/Moon; always true for Rahu/Ketu
1486
- }
1487
-
1488
- type DashaLord = 'Ketu' | 'Venus' | 'Sun' | 'Moon' | 'Mars'
1489
- | 'Rahu' | 'Jupiter' | 'Saturn' | 'Mercury';
1490
-
1491
- interface MahaDasha { lord: DashaLord; startDate: Date; endDate: Date;
1492
- years: number; antarDashas: AntarDasha[]; }
1493
- interface VimshottariDashaResult {
1494
- currentMahaDashaLord: DashaLord;
1495
- currentIndex: number;
1496
- mahaDashas: MahaDasha[];
1497
- }
1498
-
1499
- interface ChandraBalamInfo {
1500
- house: number; // 1 = janma rashi; 12 = rashi before janma
1501
- quality: 'strong' | 'weak'; // Shubha houses = 1,3,6,7,10,11
1502
- englishName: string; // "Shubha" | "Ashubha"
1503
- name: string;
1504
- }
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
1505
691
 
1506
- interface TarabalaInfo {
1507
- taraIndex: number; // 0..8 in 9-tara cycle from janma nakshatra
1508
- englishName: string; // Janma | Sampat | Vipat | Kshema | Pratyari
1509
- // | Sadhaka | Vadha | Mitra | Ati-Mitra
1510
- name: string;
1511
- quality: 'auspicious' | 'inauspicious';
1512
- }
692
+ // All options: timezone (number | IANA string), ayanamsa (5), language (en|hi),
693
+ // masaSystem (purnimanta|amanta), region, computeEndTimes, sections,
694
+ // janmaRashi, janmaNakshatra.
1513
695
  ```
1514
696
 
1515
- </details>
1516
-
1517
- <details>
1518
- <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`.
1519
700
 
1520
- ```typescript
1521
- // Primary entry points
1522
- getDailyPanchang, getInstantPanchang
1523
-
1524
- // Astronomy
1525
- getSunrise, getSunset, getMoonrise, getMoonset
1526
- getSiderealSunLongitude, getSiderealMoonLongitude, getAyanamsa
1527
-
1528
- // Inauspicious / Muhurta
1529
- computeRahuKalam, computeGulikaKalam, computeYamaganda
1530
- computeVarjyam, computeGandaMula, computeAnandadiYoga
1531
- computePanchakaRahita, computeDoGhati, computeGowriPanchangam
1532
- computePanchaka, classifyPanchaka, isPanchakaDosha, findPanchakaOnset
1533
- computeAbhijitMuhurta, computeBrahmaMuhurta, computeVijayaMuhurta
1534
- computeGodhuliMuhurta, computeNishitaMuhurta, computeAmritKala
1535
- computeMadhyahna, computePratahSandhya, computeSayahnaSandhya
1536
-
1537
- // Eclipses
1538
- getUpcomingSolarEclipse, getUpcomingLunarEclipse, getEclipseDuringDay
1539
- isEclipseVisibleAnyPhase
1540
-
1541
- // Moon phases (new / quarters / full as precise instants)
1542
- computeMoonPhasesInRange
1543
-
1544
- // Jyotish — planets, dashas, transits
1545
- computePlanetaryPositions, GRAHA_ABBR
1546
- computeVimshottariDasha, computeVimshottariDashaFromBirth, computeVimshottariPratyantar
1547
- computeAshtottariDasha, computeYoginiDasha, computeCharaDasha, computeNarayanDasha
1548
- computeChandraBalam, computeTarabala, computeSadeSati
1549
-
1550
- // Jyotish — chart
1551
- computeLagna, computeBhava, computeRashiChart, computeNavamsa, computeDivisionalChart
1552
- computeHoraLagna, computeGhatiLagna, computeBhavaLagna, computeSripatiLagna
1553
- computeDignity
1554
-
1555
- // Jyotish — strength, yogas, sensitive
1556
- computeAspects, computeShadbala, computeBhavaBala, computeAshtakavarga
1557
- computeYogas, computeJaiminiKarakas
1558
- computeVarshaphala, computeTithiPravesha, computeArudhas, computeUpagrahas, computeArgala
1559
-
1560
- // Jyotish — compatibility, doshas
1561
- computeAshtakoot, computePathuPorutham
1562
- computeMangalDosha, computeMangalCompatibility, computeKaalSarp, computePitruDosha
1563
-
1564
- // KP / Prashna
1565
- computeKpSubLord, computeKpCuspalSubLords, computeKpSignificators
1566
- computePrashnaChart
1567
-
1568
- // Muhurta engine
1569
- scoreMuhurta, computeAuspiciousDatesInRange, STOCK_MUHURTA_RULES
1570
- computeVaraTithiYogas
1571
- vivahRule, grihaPraveshRule, namakaranaRule, vidyarambhRule, vahanKharidiRule
1572
- annaprashanRule, mundanRule, upanayanamRule, karnavedhaRule
1573
- aksharabhyasamRule, seemanthamRule, shopOpeningRule, travelStartRule
1574
-
1575
- // Calendar conversion + yearly listings
1576
- convertGregorianToHindu, convertHinduToGregorian
1577
- getKaliYugaYear, getHinduNewYear, computeSamvat
1578
- computeEkadashiDatesForYear, computeSankrantisForYear, computeFestivalsInRange
1579
- getUpcomingEclipses, computeEclipsesInRange
1580
-
1581
- // Static data tables — build one, cache the JSON, then read it back through
1582
- // the engine-free panchang-ts/festivals · /eclipses · /moon-phases entries.
1583
- // No table ships with the package.
1584
- buildFestivalsTable, buildEclipsesTable, buildMoonPhasesTable
1585
-
1586
- // Errors
1587
- PanchangError
1588
- ```
701
+ ## Types & Exports
1589
702
 
1590
- </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`).
1591
707
 
1592
708
  ---
1593
709
 
@@ -1602,10 +718,7 @@ Two-pass rendering pattern for smooth UI:
1602
718
  import { getDailyPanchang } from 'panchang-ts';
1603
719
  import { InteractionManager } from 'react-native';
1604
720
 
1605
- // Pass 1 — cheapest useful result: elements, slots, muhurtas
1606
- // (~0.25 ms Node on a new date, ~0.14 ms on one already seen).
1607
- // `sections` is the lever; `computeEndTimes: false` only helps once it is
1608
- // narrowed, and slightly hurts on a full-section call.
721
+ // Pass 1 — cheapest useful result: elements, slots, muhurtas (~0.25 ms).
1609
722
  const fast = getDailyPanchang(date, location, {
1610
723
  timezone: 330,
1611
724
  sections: [],
@@ -1613,7 +726,7 @@ const fast = getDailyPanchang(date, location, {
1613
726
  });
1614
727
  setState(fast);
1615
728
 
1616
- // Pass 2 — background, everything (~0.41 ms Node)
729
+ // Pass 2 — background, everything (~0.41 ms).
1617
730
  InteractionManager.runAfterInteractions(() => {
1618
731
  setState(getDailyPanchang(date, location, { timezone: 330 }));
1619
732
  });
@@ -1623,6 +736,8 @@ InteractionManager.runAfterInteractions(() => {
1623
736
 
1624
737
  ## Accuracy
1625
738
 
739
+ 📖 [Full accuracy notes →](https://dharmagya.app/docs/panchang-ts/accuracy)
740
+
1626
741
  8,368 tests across 121 files, including fixtures cross-verified against reference
1627
742
  panchang calculations spanning 2025–2026 across 10 Indian cities plus New York,
1628
743
  London, Sydney, Dubai, Singapore (diaspora fixtures cover DST on
@@ -1633,213 +748,68 @@ London, Sydney, Dubai, Singapore (diaspora fixtures cover DST on
1633
748
  | Sunrise / Sunset | ≤29 s observed vs reference minute-midpoint (±45 s tolerance) |
1634
749
  | Moonrise / Moonset | Meeus apparent-upper-limb (refraction + parallax); ~3–5 min vs simpler-horizon authorities is expected |
1635
750
  | Tithi / Nakshatra / Yoga / Karana names | Exact match vs reference |
1636
- | Tithi / Nakshatra / Yoga / Karana end-times | ≤60 s vs Drik across all 20 audited comparisons (tithi 46 s, karana 51 s, nakshatra 24 s, yoga 60 s) |
751
+ | Tithi / Nakshatra / Yoga / Karana end-times | ≤60 s vs Drik across all 20 audited comparisons |
1637
752
  | Ayanamsa (Lahiri) | Reproduces DrikPanchang's published value to ~0.01″ across 1950–2050 |
1638
753
  | Planetary positions (Sun–Saturn) | ±0.02° sidereal |
1639
- | Planetary positions (Rahu/Ketu, mean node) | ≤0.5° typical; ±2° tolerance |
1640
- | 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) |
1641
755
  | Lagna sidereal longitude | Cross-checked against Jagannath Hora reference charts |
1642
756
  | D1 / D9 house placement | Exact match vs reference for 9-graha placement |
1643
757
  | Ashtakoot total | ±1 point per pair across 30+ matched pairs |
1644
758
  | Sade Sati arc start/end | ±1–2 days vs authoritative ephemerides |
1645
759
 
1646
- **End-time drift.** Drik publishes end times to the minute, so each comparison
1647
- above carries ±30 s of quantization that, not the search, dominates what is
1648
- left. Two independent checks bound the library's own contribution: the reported
1649
- value matches an exact bisection of the same index function to ≤24 ms, and Sun
1650
- and Moon agree with Drik's sidereal positions to well under an arcsecond
1651
- (`tests/validation/element-endtime-audit.test.ts` carries the working).
1652
-
1653
- **Ayanamsa.** Only Lahiri is verified against an external reference — Drik
1654
- publishes no value for the other four. Raman, KP, True Chitrapaksha and
1655
- Thirukanitham are held at their historical offsets from Lahiri, so correcting
1656
- Lahiri carried them along rather than silently changing how each relates to it.
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).
1657
765
 
1658
- **Detection notes.** **Aadal / Vidaal** follow the classical Moon-from-Sun
1659
- nakshatra-distance rule (AstroShastra, HoraSarvam, Ernst Wilhelm), NOT the
1660
- Tamil-Vakya weekday rule used by some online panchangs. **Varjyam** emits the
1661
- sunrise-anchored nakshatra's window only. **Do Ghati Muhurta** does not rotate
1662
- by weekday — the same 30-name deity-keyed sequence applies every day.
1663
-
1664
- ### Festival Detection — Documented Tradeoff
1665
-
1666
- The library uses **tithi-at-sunrise** to resolve a festival to a calendar day.
1667
- Some authorities use other classical rules for certain festivals; where those
1668
- rules pick a different day, output can drift ±1 day:
1669
-
1670
- | Alternative rule | Affects |
1671
- |---|---|
1672
- | Tithi-at-midnight | Krishna Janmashtami, Maha Shivaratri, Diwali / Lakshmi Puja |
1673
- | Madhyahna-vyapini | Ganesh Chaturthi (edge years), Akshaya Tritiya 2026 |
1674
- | Kshaya-tithi handling | Ugadi 2026-03-19 (Pratipada is Kshaya) |
1675
-
1676
- Everything else — Holi, Ugadi (non-Kshaya years), Rama Navami, Raksha Bandhan,
1677
- Ganesh Chaturthi (normal years), Navaratri, Dussehra, Karva Chauth, Hanuman
1678
- Jayanti — matches the canonical date across 2025 and 2026 fixtures.
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.
1679
769
 
1680
770
  ---
1681
771
 
1682
772
  ## Performance
1683
773
 
1684
- Measured at Pune on an Apple M-series laptop under Node 24, median of 11
1685
- processes per configuration. Treat them as relative guidance, not a spec — they
1686
- move with hardware, latitude and date.
774
+ 📖 [Full performance notes →](https://dharmagya.app/docs/panchang-ts/performance)
1687
775
 
1688
- Two columns, because they differ and both are real. **Distinct days** is the
1689
- calendar-scan cost: every call misses the solar rise/set cache. **Same day
1690
- repeated** is what a UI that re-renders one date sees, and what `npm run bench`
1691
- reports. The last column is the **published 4.3.1 package**, installed from npm
1692
- and benchmarked beside this one.
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.
1693
780
 
1694
781
  | `getDailyPanchang` call | Distinct days | Same day repeated | 4.3.1 (distinct) |
1695
782
  |---|---|---|---|
1696
783
  | Default (all sections + end-times) | **~0.41 ms** | **~0.17 ms** | ~6.06 ms |
1697
- | `computeEndTimes: false` | ~0.39 ms | ~0.15 ms | ~5.63 ms |
1698
- | Without `'festivals'` | ~0.39 ms | — | n/a |
1699
- | `sections: ['festivals', 'eclipse']` | ~0.40 ms | — | n/a |
1700
- | `sections: []` | ~0.27 ms | — | n/a |
1701
784
  | `sections: []` + `computeEndTimes: false` | ~0.25 ms | ~0.14 ms | n/a |
1702
785
  | `getInstantPanchang` | ~0.21 ms | ~0.10 ms | ~0.43 ms |
1703
786
 
1704
- `sections` did not exist before v5, so the rows using it have no 4.3.1
1705
- counterpart — passing it to 4.3.1 is silently ignored and you get a full run.
1706
-
1707
- **A default day is ~15× cheaper than in 4.3.1**, and a repeated day ~37×. Most
1708
- of that is not tuning: 4.3.1 ran `astronomy-engine`'s full lunar theory inside
1709
- the eclipse search on every day containing a syzygy, and had no cache that
1710
- survived a call.
1711
-
1712
- One surface moved the other way, and it is deliberate: the **raw longitude
1713
- getters** (`getSiderealSunLongitude`, `getSiderealMoonLongitude`) cost about
1714
- twice what they did in 4.3.1 per call — ~14 µs against ~7 µs for the Moon,
1715
- ~2.8 µs against ~1.4 µs for the Sun — because the own-ephemeris series keep
1716
- roughly three times `astronomy-engine`'s accuracy against JPL DE441, and the
1717
- evaluation is sine-bound, so more terms cost proportionally more. Every
1718
- documented workflow (`getDailyPanchang`, `getInstantPanchang`, the year/range
1719
- APIs, the static tables) amortizes those reads through caches and is faster
1720
- than 4.3.1 by the factors above; the per-call price is only visible to code
1721
- calling the raw getters in a tight loop over distinct instants. For scanning
1722
- workloads, prefer the range APIs or `getDailyPanchang` — they read through
1723
- interpolated longitude blocks precisely so that this cost is paid once per day
1724
- rather than once per read.
1725
-
1726
- The same trade surfaces once more in the birth-chart *primitives*:
1727
- `computeRashiChart` and `computeNavamsa` measure ~0.31 ms against ~0.09 ms on
1728
- published 4.3.1 — a chart is fifteen full-accuracy planet evaluations (three
1729
- per planet, for the retrograde probes) and nothing amortizes them. The deeper
1730
- chart stack inverts it again: `computeShadbala` and `computeBhavaBala` come
1731
- out ~2× *faster* than 4.3.1, because v5 computes the positions once and reuses
1732
- them. At a third of a millisecond per chart this is irrelevant interactively;
1733
- it is visible only to code building thousands of charts in a batch.
1734
-
1735
- Cost is dominated by ephemeris evaluations, so the lever that matters is the one
1736
- that avoids them:
1737
-
1738
- - **`sections`** — skip the optional ephemeris-backed blocks you don't need.
1739
- Dropping `'festivals'` takes a default call from ~0.41 ms to ~0.39 ms cold,
1740
- and dropping everything takes it to ~0.27 ms. See
1741
- [Narrowing the work](#narrowing-the-work).
1742
- - **`computeEndTimes: false`** — a small win, never a large one. It skips the
1743
- transition searches, but those read through the same interpolated longitude
1744
- blocks the rest of the call has already built, so what it saves is arithmetic
1745
- rather than ephemeris: ~5% cold, ~10% warm. Use it to drop `endTime` fields
1746
- you don't want, not to go faster.
1747
-
1748
- *Changed in v5.* Both entry points now always interpolate, so output depends
1749
- on neither `sections` nor `computeEndTimes` — see `INTERPOLATE_ALWAYS` in
1750
- `src/core/panchang.ts`. Earlier development builds chose the longitude cache's
1751
- mode from `computeEndTimes`, which made asking for *less* output cost *more*
1752
- on a full call; that is gone.
1753
-
1754
- Repeated calls for the same location-day are cheaper because solar rise/set
1755
- events are cached process-wide, keyed on `(direction, lat, lon, elevation, UTC
1756
- day)` and bounded at 20,000 entries. The cache makes sunrise single-valued as
1757
- well as fast — see [Upgrading from 4.x](#sunrise-is-single-valued-per-location-day).
1758
- It does not make a *single* cold rise/set call cheaper — against 4.3.1
1759
- `getSunrise` is +1.8%, `getSunset` +8.8%, `getMoonrise` +5.5% and `getMoonset`
1760
- +6.6%, i.e. unchanged to slightly worse — what it removes is the second and
1761
- every later call for the same day.
1762
-
1763
- Range helpers apply the same narrowing internally:
1764
- `computeEkadashiDatesForYear` reads only the tithi at sunrise and so runs with
1765
- every optional section off (**~18 ms** for a full year, against ~2,360 ms in
1766
- 4.3.1); `computeFestivalsInRange` keeps only `'festivals'` and `'eclipse'`
1767
- (**~131 ms/year**, against ~2,180); `computeSankrantisForYear` needs only the
1768
- Sun, so it scans one solar longitude per day and bisects the 12 transits rather
1769
- than building a panchang each day (**~3.3 ms/year**, against ~154).
1770
-
1771
- Birth-chart helpers are independent — calling them does not add work to
1772
- `getDailyPanchang`. Within them, `computeShadbala` and `computeBhavaBala` build
1773
- the natal positions once and derive all seven charts from them (~0.33 ms each,
1774
- against ~0.72 in 4.3.1).
1775
-
1776
- **Charts are the one place v5 is slower.** `computeRashiChart` and
1777
- `computeNavamsa` cost **~0.32 ms** against ~0.10 in 4.3.1 — 3.3×, entirely the
1778
- planetary ephemeris, and the deliberate price of an order-of-magnitude accuracy
1779
- gain against JPL DE441 (Mercury 6.50″ → 0.30″, Venus 19.59″ → 0.86″). If you
1780
- build many charts and do not need that precision, 4.x was cheaper; nothing else
1781
- in the library regressed.
1782
-
1783
- ### Narrowing the work
1784
-
1785
- `PanchangSection` lists the four optional blocks. Everything else a daily
1786
- panchang returns — the five elements, slot systems, muhurtas, inauspicious
1787
- periods, masa / samvat / rashi — is arithmetic over the sunrise / sunset /
1788
- next-sunrise triplet and is always computed, because skipping it would save
1789
- nothing.
1790
-
1791
- | Section | Covers | Fields when omitted |
1792
- |---|---|---|
1793
- | `'festivals'` | Festival detection — needs the prior day's sunrise/sunset, the next day's transit, per-kala tithi anchors, and the prior day's Chandra Masa | `festivals: []` — but an eclipse entry is still prepended when `'eclipse'` is on |
1794
- | `'eclipse'` | Eclipse overlapping the Hindu day | `eclipse: null` |
1795
- | `'moonTimes'` | `moon.rise` / `moon.set` | `null` |
1796
- | `'lunarWindows'` | Bhadra, Varjyam, Panchaka-Rahita — each binary-searches lunar longitude across the day | `null` / `[]` |
1797
-
1798
- ```typescript
1799
- // Everything (default).
1800
- getDailyPanchang(date, loc, { timezone: 330 });
1801
-
1802
- // Festivals only — no moon times, no Bhadra/Varjyam windows.
1803
- getDailyPanchang(date, loc, {
1804
- timezone: 330,
1805
- sections: ['festivals', 'eclipse'],
1806
- });
1807
-
1808
- // Cheapest useful call: elements, slots, muhurtas and inauspicious periods
1809
- // only. Those are arithmetic on the sunrise triplet and are always computed.
1810
- getDailyPanchang(date, loc, {
1811
- timezone: 330,
1812
- sections: [],
1813
- computeEndTimes: false,
1814
- });
1815
- ```
1816
-
1817
- Omitting a section leaves its fields at their documented empty value (`null`
1818
- or `[]`) — never a half-filled one.
787
+ **A default day is ~15× cheaper than 4.3.1**, a repeated day ~37×. The levers:
1819
788
 
1820
- Narrowing is **exactly output-neutral**: every field a narrowed call does
1821
- compute is identical, to the millisecond, to what the full call would have
1822
- returned. `sections` only decides what is skipped, never what a computed value
1823
- is. (This is guaranteed by `LongitudeCache` memoizing on the exact instant. It
1824
- was not true while that memo binned longitudes into 60-second buckets, when
1825
- narrowing could shift transition times by up to 63 s.)
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.
1826
795
 
1827
- ### A note on Hermes / React Native
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.
1828
801
 
1829
- Earlier versions of this table also quoted Hermes figures. Those were budget
1830
- targets from the project plan, never measurements: `npm run test:hermes` runs
1831
- `hermes-parser` over the built bundle to prove the syntax is Hermes-compatible,
1832
- which is a *parse* check and does not execute anything. Hermes numbers will be
1833
- published here once they are actually measured on device.
1834
-
1835
- What does carry over is the shape of the cost: it is dominated by ephemeris
1836
- math, so the `sections` and `computeEndTimes` levers above have the same
1837
- proportional effect on any runtime.
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.
1838
806
 
1839
807
  ---
1840
808
 
1841
809
  ## Error Handling
1842
810
 
811
+ 📖 [Errors & Compatibility →](https://dharmagya.app/docs/panchang-ts/errors)
812
+
1843
813
  ```typescript
1844
814
  import { PanchangError } from 'panchang-ts';
1845
815
 
@@ -1854,7 +824,7 @@ try {
1854
824
  ```
1855
825
 
1856
826
  Error codes: `INVALID_DATE`, `INVALID_LATITUDE`, `INVALID_LONGITUDE`,
1857
- `INVALID_ELEVATION`, `INVALID_TIMEZONE`, `INVALID_AYANAMSA`,
827
+ `INVALID_ELEVATION`, `INVALID_TIMEZONE`, `INVALID_AYANAMSA`, `INVALID_INPUT`,
1858
828
  `TIMEZONE_RESOLUTION_FAILED`, `NO_SUNRISE`, `NO_SUNSET`, `SEARCH_DIVERGED`,
1859
829
  `CIRCUMPOLAR` (Placidus-KP houses above ±66.5°).
1860
830
 
@@ -1871,18 +841,25 @@ for the Moon).
1871
841
 
1872
842
  | Environment | Support |
1873
843
  |---|---|
1874
- | Node.js 18+ | Supported |
844
+ | Node.js 22 (per the package `engines` field) | Supported |
1875
845
  | React Native (Hermes) | Supported (pass `timezone` as number) |
1876
846
  | Expo (managed + bare) | Supported |
1877
847
  | Browser (modern, ESM) | Supported |
1878
848
  | Browser (legacy / IE) | Not supported |
1879
849
 
850
+ The build targets ES2020 (ESM + CJS, full `.d.ts`), has **zero runtime
851
+ dependencies**, and is `sideEffects: false`.
852
+
1880
853
  ---
1881
854
 
1882
855
  ## Acknowledgements
1883
856
 
1884
- [astronomy-engine](https://github.com/cosinekitty/astronomy) by Don Cross — the
1885
- 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.
1886
863
 
1887
864
  ## License
1888
865