ix months ago I started an experiment: could one TypeScript library compute
everything a Nepali calendar app needs (sunrise, moon phases, the Hindu
panchang, birth charts, Bikram Sambat dates, even eclipses) with zero
runtime dependencies, MIT licensed, and accuracy you can actually measure?
Today grahan hit 1.0. The name is
ग्रहण, the Nepali/Sanskrit word for eclipse, so it wasn't allowed to ship
1.0 without living up to it:
import { nextSolarEclipseAt, julianDayFromDate, dateFromJulianDay } from '@grahan/core';
// When does Kathmandu next see the Sun bitten? Works for any lat/lon.
const jd = julianDayFromDate(new Date('2026-07-06T00:00:00Z'));
const e = nextSolarEclipseAt(jd, { latitude: 27.7172, longitude: 85.324 });
e.type; // 'partial': 2027-08-02, the Egypt totality grazing Nepal
dateFromJulianDay(e.maximum); // 2027-08-02T11:05:02Z (16:50 local)
e.magnitude; // 0.0751, a small bite off the solar diameter
That covers every solar and lunar eclipse from 1900 to 2100: detection,
type, magnitudes, contact times, local visibility. All computed from raw
geometry in plain TypeScript. No WASM, no data files, no dependencies.
The feature calendar sites actually show
panchang() tells you the sky now. But printed panchangs and sites like
drikpanchang label a day by its sunrise elements, with the time each one
ends. That's panchangAtSunrise():
import { panchangAtSunrise } from '@grahan/vedic';
const day = panchangAtSunrise({
year: 2026, month: 7, day: 2,
latitude: 27.7172, longitude: 85.324, timezone: 'Asia/Kathmandu',
});
day.vaar.name; // 'Thursday'
day.tithi[0].name; // 'Dwitiya', the day's label...
day.tithi[0].endsAt; // ...upto 09:53 local, then:
day.tithi[1].name; // 'Tritiya'
day.karana.map(k => k.name); // ['Gara', 'Vanija', 'Vishti']
day.rahuKaal; // 13:51 to 15:35 that Thursday
Notice tithi is an array. That's deliberate honesty: lunar days drift
against solar days, so some civil dates hold three tithis (a kshaya day,
where one tithi begins and ends entirely between two sunrises) and some
tithis span two sunrises (vriddhi). Most libraries squash this into a
single value and silently disagree with every printed calendar a few days
per month. grahan returns the ordered list of spans touching the day, each
with its real start and end instant.
I cross-checked the end times against drikpanchang's rendered pages: every
element name matched exactly, every time within about 80 seconds (they
print whole minutes).
Accuracy: measured, not hoped
Swiss Ephemeris is the industry standard, but it's AGPL, so linking it was
off the table for an MIT library. Instead, everything is implemented from
public-domain sources (Meeus's Astronomical Algorithms, VSOP87, ELP-2000)
and verified against Swiss Ephemeris outputs, which are facts, not
code. An offline Python script generates JSON reference fixtures; tests
assert tolerances and print an accuracy report on every CI run:
| Quantity | Measured vs Swiss Ephemeris (1900–2100) |
|---|---|
| Sun longitude | max 4.6″, mean 0.9″ |
| Moon longitude | max 65″, mean 10.5″ |
| Planets | max 7.8″ (Mercury) to 1.1″ (Saturn) |
| Sunrise/sunset | max 4.6 s over 272 events, 5 sites incl. 71°N |
| Panchang end times | max 37.5 s over 122 boundaries |
| Eclipse instants | mean ~20 s; detection and type exact on all 909 events |
Three bugs this method caught that eyeballing never would:
- The Arctic is a unit test. At Utqiagvik (71°N) the sun can set before it rises within one civil day, and near the polar-night boundary the "nearest" sunrise belongs to a neighbouring solar cycle. Two real algorithm bugs, both caught by fixtures I only generated because 71°N felt like a good edge case.
- Two eclipses where the references disagree. For 1927-01-03 and 1948-05-09, Swiss Ephemeris says hybrid while NASA's Espenak catalog says annular, with 3 seconds and 0 seconds of annularity respectively. My independent shadow geometry sided with NASA both times. Knife-edge cases are where you learn whose model does what.
- A 17.2″ systematic error in the lunar node turned out to be a frame mismatch (mean vs true equinox). It was diagnosed purely because the error's scale matched the nutation amplitude. When your tests print arcseconds, error signatures become fingerprints.
What else is in the box
-
kundali(): sidereal birth charts with nine grahas, lagna, whole-sign houses, navamsa, plus SVG renderers in North and South Indian styles, Vimshottari dashas, transits, muhurta windows, gun-milan matching. Rahu can follow the mean node or the true osculating node (node: 'true'), computed the way Swiss Ephemeris does it: from the Moon's instantaneous orbit plane. -
@grahan/calendars: Bikram Sambat to AD conversion for BS 1975–2200, with verified tables through 2083 and beyond that a Surya Siddhanta projection, clearly flaggedprojected: true, because pretending certainty about 2199 would be a lie. - The secular core stands alone: Julian days, ΔT, sidereal time, and
sunrise/sunset with explicit
'always-up'/'always-down'polar states. No fabricated 00:00 sunrises.
What 1.0 means here
Not "it compiles". For 1.0 I froze and tested the API surface (the exact
export list of each package is pinned by tests), made every public function
throw a RangeError naming the offending value on bad input (latitude 95,
new Date(NaN), month 13; nothing silently returns NaN), and published an
API reference generated from the
JSDoc.
Two production details I'm particularly happy with:
- The panchang core tree-shakes to 9.7 KB gzipped. CI proves it: a scratch consumer bundles the shipped tarballs with esbuild in browser mode (so any Node builtin is a hard build error), runs the bundle, and asserts the planet tables and eclipse code actually shake off.
-
Releases are tokenless. Publishing happens from GitHub Actions via
npm trusted publishing (OIDC), so no npm token exists anywhere, and every
package carries a SLSA provenance attestation.
npm audit signaturesverifies the whole chain. The release workflow also refuses to publish a partial release: every package must pass an OIDC preflight before any of them ships.
Try it
npm install @grahan/vedic # panchang + charts (pulls in @grahan/core)
- GitHub: https://github.com/svarbhanu/grahan
- API reference: https://svarbhanu.github.io/grahan/
- npm: @grahan/core · @grahan/vedic · @grahan/calendars
Next up: lunar months (masa/adhika-masa), moonrise/moonset, festival rules,
and more calendars on the same core. Which would you use first: Hijri,
Hebrew, or Chinese lunisolar? The architecture is one secular core with
cultural layers on top, so your answer genuinely steers the roadmap.
Top comments (0)