<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Svarbhanu Neel</title>
    <description>The latest articles on DEV Community by Svarbhanu Neel (@svarbhanu).</description>
    <link>https://dev.to/svarbhanu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4015079%2Fdbe168ca-5f2b-492f-857b-b60eea44dd50.jpg</url>
      <title>DEV Community: Svarbhanu Neel</title>
      <link>https://dev.to/svarbhanu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/svarbhanu"/>
    <language>en</language>
    <item>
      <title>A library named "eclipse" should compute eclipses: grahan 1.0</title>
      <dc:creator>Svarbhanu Neel</dc:creator>
      <pubDate>Mon, 06 Jul 2026 12:29:32 +0000</pubDate>
      <link>https://dev.to/svarbhanu/a-library-named-eclipse-should-compute-eclipses-grahan-10-3h9b</link>
      <guid>https://dev.to/svarbhanu/a-library-named-eclipse-should-compute-eclipses-grahan-10-3h9b</guid>
      <description>&lt;p&gt;ix months ago I started an experiment: could one TypeScript library compute&lt;br&gt;
everything a Nepali calendar app needs (sunrise, moon phases, the Hindu&lt;br&gt;
panchang, birth charts, Bikram Sambat dates, even eclipses) with &lt;strong&gt;zero&lt;br&gt;
runtime dependencies&lt;/strong&gt;, MIT licensed, and accuracy you can actually measure?&lt;/p&gt;

&lt;p&gt;Today &lt;a href="https://github.com/svarbhanu/grahan" rel="noopener noreferrer"&gt;grahan&lt;/a&gt; hit 1.0. The name is&lt;br&gt;
ग्रहण, the Nepali/Sanskrit word for &lt;em&gt;eclipse&lt;/em&gt;, so it wasn't allowed to ship&lt;br&gt;
1.0 without living up to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nextSolarEclipseAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;julianDayFromDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dateFromJulianDay&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grahan/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// When does Kathmandu next see the Sun bitten? Works for any lat/lon.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;julianDayFromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-06T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nextSolarEclipseAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;27.7172&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;85.324&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;// 'partial': 2027-08-02, the Egypt totality grazing Nepal&lt;/span&gt;
&lt;span class="nf"&gt;dateFromJulianDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maximum&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 2027-08-02T11:05:02Z (16:50 local)&lt;/span&gt;
&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;magnitude&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                   &lt;span class="c1"&gt;// 0.0751, a small bite off the solar diameter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That covers every solar and lunar eclipse from 1900 to 2100: detection,&lt;br&gt;
type, magnitudes, contact times, local visibility. All computed from raw&lt;br&gt;
geometry in plain TypeScript. No WASM, no data files, no dependencies.&lt;/p&gt;
&lt;h2&gt;
  
  
  The feature calendar sites actually show
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;panchang()&lt;/code&gt; tells you the sky &lt;em&gt;now&lt;/em&gt;. But printed panchangs and sites like&lt;br&gt;
drikpanchang label a &lt;strong&gt;day&lt;/strong&gt; by its sunrise elements, with the time each one&lt;br&gt;
ends. That's &lt;code&gt;panchangAtSunrise()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;panchangAtSunrise&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grahan/vedic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;panchangAtSunrise&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;27.7172&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;85.324&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Asia/Kathmandu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vaar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// 'Thursday'&lt;/span&gt;
&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tithi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// 'Dwitiya', the day's label...&lt;/span&gt;
&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tithi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;endsAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// ...upto 09:53 local, then:&lt;/span&gt;
&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tithi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// 'Tritiya'&lt;/span&gt;
&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;karana&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ['Gara', 'Vanija', 'Vishti']&lt;/span&gt;
&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rahuKaal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// 13:51 to 15:35 that Thursday&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;tithi&lt;/code&gt; is an &lt;strong&gt;array&lt;/strong&gt;. That's deliberate honesty: lunar days drift&lt;br&gt;
against solar days, so some civil dates hold &lt;em&gt;three&lt;/em&gt; tithis (a &lt;em&gt;kshaya&lt;/em&gt; day,&lt;br&gt;
where one tithi begins and ends entirely between two sunrises) and some&lt;br&gt;
tithis span two sunrises (&lt;em&gt;vriddhi&lt;/em&gt;). Most libraries squash this into a&lt;br&gt;
single value and silently disagree with every printed calendar a few days&lt;br&gt;
per month. grahan returns the ordered list of spans touching the day, each&lt;br&gt;
with its real start and end instant.&lt;/p&gt;

&lt;p&gt;I cross-checked the end times against drikpanchang's rendered pages: every&lt;br&gt;
element name matched exactly, every time within about 80 seconds (they&lt;br&gt;
print whole minutes).&lt;/p&gt;

&lt;h2&gt;
  
  
  Accuracy: measured, not hoped
&lt;/h2&gt;

&lt;p&gt;Swiss Ephemeris is the industry standard, but it's AGPL, so linking it was&lt;br&gt;
off the table for an MIT library. Instead, everything is implemented from&lt;br&gt;
public-domain sources (Meeus's &lt;em&gt;Astronomical Algorithms&lt;/em&gt;, VSOP87, ELP-2000)&lt;br&gt;
and verified against Swiss Ephemeris &lt;strong&gt;outputs&lt;/strong&gt;, which are facts, not&lt;br&gt;
code. An offline Python script generates JSON reference fixtures; tests&lt;br&gt;
assert tolerances and print an accuracy report on every CI run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quantity&lt;/th&gt;
&lt;th&gt;Measured vs Swiss Ephemeris (1900–2100)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sun longitude&lt;/td&gt;
&lt;td&gt;max 4.6″, mean 0.9″&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moon longitude&lt;/td&gt;
&lt;td&gt;max 65″, mean 10.5″&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planets&lt;/td&gt;
&lt;td&gt;max 7.8″ (Mercury) to 1.1″ (Saturn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sunrise/sunset&lt;/td&gt;
&lt;td&gt;max 4.6 s over 272 events, 5 sites incl. 71°N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Panchang end times&lt;/td&gt;
&lt;td&gt;max 37.5 s over 122 boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eclipse instants&lt;/td&gt;
&lt;td&gt;mean ~20 s; detection and type exact on all 909 events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three bugs this method caught that eyeballing never would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Arctic is a unit test.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two eclipses where the references disagree.&lt;/strong&gt; For 1927-01-03 and
1948-05-09, Swiss Ephemeris says &lt;em&gt;hybrid&lt;/em&gt; while NASA's Espenak catalog
says &lt;em&gt;annular&lt;/em&gt;, 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 17.2″ systematic error&lt;/strong&gt; in the lunar node turned out to be a frame
mismatch (mean vs true equinox). It was diagnosed purely because the
error's &lt;em&gt;scale&lt;/em&gt; matched the nutation amplitude. When your tests print
arcseconds, error signatures become fingerprints.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What else is in the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kundali()&lt;/code&gt;: 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 (&lt;code&gt;node: 'true'&lt;/code&gt;),
computed the way Swiss Ephemeris does it: from the Moon's instantaneous
orbit plane.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@grahan/calendars&lt;/code&gt;: Bikram Sambat to AD conversion for BS 1975–2200,
with verified tables through 2083 and beyond that a Surya Siddhanta
projection, clearly flagged &lt;code&gt;projected: true&lt;/code&gt;, because pretending
certainty about 2199 would be a lie.&lt;/li&gt;
&lt;li&gt;The secular core stands alone: Julian days, ΔT, sidereal time, and
sunrise/sunset with explicit &lt;code&gt;'always-up'&lt;/code&gt; / &lt;code&gt;'always-down'&lt;/code&gt; polar
states. No fabricated 00:00 sunrises.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What 1.0 means here
&lt;/h2&gt;

&lt;p&gt;Not "it compiles". For 1.0 I froze and tested the API surface (the exact&lt;br&gt;
export list of each package is pinned by tests), made every public function&lt;br&gt;
throw a &lt;code&gt;RangeError&lt;/code&gt; naming the offending value on bad input (latitude 95,&lt;br&gt;
&lt;code&gt;new Date(NaN)&lt;/code&gt;, month 13; nothing silently returns NaN), and published an&lt;br&gt;
&lt;a href="https://svarbhanu.github.io/grahan/" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; generated from the&lt;br&gt;
JSDoc.&lt;/p&gt;

&lt;p&gt;Two production details I'm particularly happy with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The panchang core tree-shakes to 9.7 KB gzipped.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releases are tokenless.&lt;/strong&gt; Publishing happens from GitHub Actions via
npm trusted publishing (OIDC), so no npm token exists anywhere, and every
package carries a SLSA provenance attestation. &lt;code&gt;npm audit signatures&lt;/code&gt;
verifies 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @grahan/vedic   &lt;span class="c"&gt;# panchang + charts (pulls in @grahan/core)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/svarbhanu/grahan" rel="noopener noreferrer"&gt;https://github.com/svarbhanu/grahan&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API reference: &lt;a href="https://svarbhanu.github.io/grahan/" rel="noopener noreferrer"&gt;https://svarbhanu.github.io/grahan/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@grahan/core" rel="noopener noreferrer"&gt;@grahan/core&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@grahan/vedic" rel="noopener noreferrer"&gt;@grahan/vedic&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@grahan/calendars" rel="noopener noreferrer"&gt;@grahan/calendars&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: lunar months (masa/adhika-masa), moonrise/moonset, festival rules,&lt;br&gt;
and more calendars on the same core. &lt;strong&gt;Which would you use first: Hijri,&lt;br&gt;
Hebrew, or Chinese lunisolar?&lt;/strong&gt; The architecture is one secular core with&lt;br&gt;
cultural layers on top, so your answer genuinely steers the roadmap.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>astronomy</category>
    </item>
    <item>
      <title>I built a zero-dependency sky engine in TypeScript, verified to 4.6 arcseconds</title>
      <dc:creator>Svarbhanu Neel</dc:creator>
      <pubDate>Sat, 04 Jul 2026 13:21:55 +0000</pubDate>
      <link>https://dev.to/svarbhanu/i-built-a-zero-dependency-sky-engine-in-typescript-verified-to-46-arcseconds-317h</link>
      <guid>https://dev.to/svarbhanu/i-built-a-zero-dependency-sky-engine-in-typescript-verified-to-46-arcseconds-317h</guid>
      <description>&lt;p&gt;Today I shipped v0.1.0 of &lt;a href="https://github.com/svarbhanu/grahan" rel="noopener noreferrer"&gt;grahan&lt;/a&gt;, an MIT-licensed TypeScript library that computes Sun/Moon positions, sunrise/sunset, moon phases, and the Vedic panchang (tithi, nakshatra, yoga, karana, Rahu Kaal). Zero runtime dependencies. Runs in Node 18+, browsers, and edge runtimes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @grahan/vedic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;panchang&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grahan/vedic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;panchang&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-02T06:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;27.7172&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;85.324&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Asia/Kathmandu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// p.tithi      → { index: 17, paksha: 'krishna', name: 'Tritiya' }&lt;/span&gt;
&lt;span class="c1"&gt;// p.nakshatra  → { index: 21, name: 'Shravana', pada: 1 }&lt;/span&gt;
&lt;span class="c1"&gt;// p.rahuKaal   → 13:51 to 15:35 local, that Thursday&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This post is about the three engineering decisions that mattered, and the bugs the process caught that I would never have found by eyeballing outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why zero dependencies
&lt;/h2&gt;

&lt;p&gt;The industry-standard ephemeris is Swiss Ephemeris. It is superb, but AGPL (or paid), so linking it was off the table for an MIT library. The alternatives were: wrap a WASM build (license problem remains), depend on a chain of npm astronomy packages of varying maintenance, or implement from the public-domain sources: Meeus's &lt;em&gt;Astronomical Algorithms&lt;/em&gt;, the VSOP87 planetary theory, and the ELP-2000 lunar series.&lt;/p&gt;

&lt;p&gt;I implemented from sources. It also bought me something I didn't expect to value so much: the whole library is auditable. Every formula has a citation to a chapter of Meeus or an official data file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule one: generate the answer sheet before writing the formula
&lt;/h2&gt;

&lt;p&gt;Swiss Ephemeris is AGPL &lt;em&gt;code&lt;/em&gt;, but its &lt;em&gt;outputs&lt;/em&gt; are facts. So before writing any calculation, I ran an offline Python script (pyswisseph) that generated JSON reference fixtures and committed them to the repo: 120 Sun/Moon longitudes spanning 1900 to 2100, 150 sunrise/sunset events across 5 sites from Singapore to Utqiagvik at 71°N, 41 ayanamsa epochs. Tests read the JSON and assert tolerances, and CI prints an accuracy report on every run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[accuracy] sun apparent longitude: n=120 max=4.60″ mean=0.85″
[accuracy] moon apparent longitude: n=120 max=65.37″ mean=10.46″
[accuracy] sunrise/sunset: n=272 max=4.6s mean=0.9s polar-states=28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Promised tolerances are ±36″ (Sun), ±180″ (Moon), ±60 s (rise/set). Measured is 8 to 13 times inside the promise. The README publishes both columns, promise and measurement, because accuracy claims without numbers are marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule two: don't type coefficient tables by hand
&lt;/h2&gt;

&lt;p&gt;The Sun's position needs a truncated VSOP87 series: hundreds of coefficients. Typing them from a book is how you get a library that's subtly wrong forever. Instead, a script downloads the official VSOP87D data file from the CDS astronomical data center, truncates it by amplitude, and emits the TypeScript table with provenance in the header. The Moon's 120-term table I did have to type from Meeus, but the fixtures had my back: the mean error came out at 10.5″, exactly the intrinsic accuracy Meeus states for that truncation. A single typo would have inflated it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs the fixtures caught
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The nutation fingerprint.&lt;/strong&gt; My lunar-node function disagreed with the reference by a systematic 11 to 18 arcseconds. The &lt;em&gt;scale&lt;/em&gt; was the clue: nutation oscillates ±17.2″. Swiss Ephemeris reports the node on the true equinox of date; my polynomial gave the mean equinox. One line fixed it, and max error fell to 0.35″. Good reference data doesn't just say &lt;em&gt;wrong&lt;/em&gt;. The error's shape tells you &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sunset before sunrise.&lt;/strong&gt; At 71°N, near the midnight-sun transition, a local calendar day can contain a sunset at 01:30 and a sunrise at 02:40, in that order. My test asserted &lt;code&gt;sunrise &amp;lt; sunset&lt;/code&gt; as a universal property. The fixture data taught me otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The neighbouring solar cycle.&lt;/strong&gt; Also at 71°N: a local day whose sunset belongs to the &lt;em&gt;previous&lt;/em&gt; day's solar arc, 15 minutes after local midnight. My first algorithm only searched around that day's own solar noon and reported "always up." The fix: consider events from adjacent transits and keep whatever lands inside the local day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The refraction calibration.&lt;/strong&gt; First full run: every site showed sunrise late and sunset early by the same amount. The day was symmetrically too short, and that signature means exactly one thing: the horizon-altitude constant is wrong. The textbook says 34′ of refraction; Swiss Ephemeris effectively bends about 36.7′ at the event's true altitude. At tangent polar crossings that 0.6′ costs two minutes. One calibrated constant, documented with its provenance, took max error from 115 s to 4.6 s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The empty tarball.&lt;/strong&gt; My npm build hook was &lt;code&gt;prepublishOnly&lt;/code&gt;, which does not run on &lt;code&gt;pack&lt;/code&gt;. CI packed tarballs with no compiled code in them. I only caught it because the CI installs the packed tarballs with plain npm into a scratch project on Node 18/20/22 and runs a smoke test, exactly as a consumer would. If your CI only ever tests your source tree, you are not testing the thing you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: secular core, cultural layers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@grahan/core&lt;/code&gt; is pure astronomy. A weather or photography app can use it without touching a single Vedic concept. &lt;code&gt;@grahan/vedic&lt;/code&gt; layers the cultural interpretation on top: the Lahiri ayanamsa (fitted to reference values to 0.002″), then tithi, nakshatra, yoga, and karana as pure functions over longitudes. Planned layers on the same core: world calendars (Bikram Sambat, Hijri, Hebrew), prayer times, tropical charts.&lt;/p&gt;

&lt;p&gt;A detail I enjoyed: the ayanamsa &lt;em&gt;cancels&lt;/em&gt; in the Moon minus Sun difference, so tithi works on tropical longitudes directly. But it doesn't cancel in the Moon plus Sun &lt;em&gt;sum&lt;/em&gt;, so yoga genuinely needs the sidereal frame. The kind of thing you only internalize by implementing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Phase 2 is &lt;code&gt;kundali()&lt;/code&gt;, full birth charts: the remaining planets, lagna, whole-sign houses, D9, and SVG chart renderers. Same method: fixtures first, formulas second.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/svarbhanu/grahan" rel="noopener noreferrer"&gt;https://github.com/svarbhanu/grahan&lt;/a&gt; · Packages: &lt;a href="https://www.npmjs.com/package/@grahan/core" rel="noopener noreferrer"&gt;@grahan/core&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@grahan/vedic" rel="noopener noreferrer"&gt;@grahan/vedic&lt;/a&gt;. Issues and PRs welcome, especially from people who know panchang traditions better than I do.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>astrnomy</category>
    </item>
  </channel>
</rss>
