moon-sighting
High-accuracy lunar crescent visibility using the JPL DE442S planetary ephemeris. Implements both Yallop (NAO TN 69) and Odeh criteria for Islamic new moon determination.
Overview
moon-sighting computes the probability of naked-eye lunar crescent visibility for any location on Earth. This is used to determine the start of Islamic lunar months (Ramadan, Dhul Hijjah, etc.) by astronomical calculation.
- JPL DE442S ephemeris (accurate lunar positions to arcsecond level)
- Yallop criteria (NAO Technical Note 69, 1997)
- Odeh criteria (2004, updated Yallop)
- Topocentric Moon and Sun positions
- Best time and best azimuth for sighting
- Crescent width, altitude, elongation, arc of vision
GitHub: github.com/acamarata/moon-sighting
Installation
<span><span style="color: var(--shiki-token-function)">pnpm</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">add</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">moon-sighting</span></span>
<span></span>
Kernel download
moon-sighting requires the JPL DE442S SPICE kernel file. This is a binary ephemeris file (~37 MB) that contains precise planetary positions.
<span><span style="color: var(--shiki-token-function)">npx</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">moon-sighting</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">download-kernel</span></span>
<span></span>
This downloads de442s.bsp to ~/.moon-sighting/kernels/. The kernel covers the years 1950–2050 at high precision, and extends to approximately 3000 BC / 3000 AD at reduced precision.
getMoonSightingReport
<span><span style="color: var(--shiki-token-function)">getMoonSightingReport</span><span style="color: var(--shiki-color-text)">(options: MoonSightingOptions): </span><span style="color: var(--shiki-token-constant)">Promise</span><span style="color: var(--shiki-token-keyword)"><</span><span style="color: var(--shiki-color-text)">MoonSightingReport</span><span style="color: var(--shiki-token-keyword)">></span></span>
<span></span>
Options
| Parameter | Type | Required | Description |
|---|---|---|---|
date | Date | Yes | Date of the expected new moon |
latitude | number | Yes | Observer latitude |
longitude | number | Yes | Observer longitude |
elevation | number | No | Metres above sea level (default: 0) |
Returns
<span><span style="color: var(--shiki-color-text)">{</span></span>
<span><span style="color: var(--shiki-color-text)"> date</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> string</span></span>
<span><span style="color: var(--shiki-color-text)"> location</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> { latitude</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number; longitude</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number; elevation</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number }</span></span>
<span><span style="color: var(--shiki-color-text)"> bestTime</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> Date </span><span style="color: var(--shiki-token-comment)">// UTC time of best sighting opportunity</span></span>
<span><span style="color: var(--shiki-color-text)"> bestAzimuth</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number </span><span style="color: var(--shiki-token-comment)">// degrees from north</span></span>
<span><span style="color: var(--shiki-color-text)"> moonAltitude</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number </span><span style="color: var(--shiki-token-comment)">// degrees above horizon at best time</span></span>
<span><span style="color: var(--shiki-color-text)"> sunAltitude</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number </span><span style="color: var(--shiki-token-comment)">// Sun altitude at best time (negative = below horizon)</span></span>
<span><span style="color: var(--shiki-color-text)"> elongation</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number </span><span style="color: var(--shiki-token-comment)">// angular separation Moon-Sun (degrees)</span></span>
<span><span style="color: var(--shiki-color-text)"> arcOfVision</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number </span><span style="color: var(--shiki-token-comment)">// altitude difference Moon-Sun (degrees)</span></span>
<span><span style="color: var(--shiki-color-text)"> crescentWidth</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number </span><span style="color: var(--shiki-token-comment)">// topocentric crescent width (arcminutes)</span></span>
<span><span style="color: var(--shiki-color-text)"> yallopCriteria</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'A'</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">|</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'B'</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">|</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'C'</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">|</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'D'</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">|</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'E'</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">|</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'F'</span></span>
<span><span style="color: var(--shiki-color-text)"> odehCriteria</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'visible'</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">|</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'not-visible'</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">|</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'marginal'</span></span>
<span><span style="color: var(--shiki-color-text)"> yallopQ</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> number </span><span style="color: var(--shiki-token-comment)">// Yallop q-value</span></span>
<span><span style="color: var(--shiki-color-text)">}</span></span>
<span></span>
<span><span style="color: var(--shiki-token-keyword)">import</span><span style="color: var(--shiki-color-text)"> { getMoonSightingReport } </span><span style="color: var(--shiki-token-keyword)">from</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'moon-sighting'</span></span>
<span></span>
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">report</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">await</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">getMoonSightingReport</span><span style="color: var(--shiki-color-text)">({</span></span>
<span><span style="color: var(--shiki-color-text)"> date</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">new</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">Date</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">'2026-02-17'</span><span style="color: var(--shiki-color-text)">)</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-comment)">// expected new moon date</span></span>
<span><span style="color: var(--shiki-color-text)"> latitude</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">21.3891</span><span style="color: var(--shiki-token-punctuation)">,</span></span>
<span><span style="color: var(--shiki-color-text)"> longitude</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">39.8579</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-comment)">// Mecca</span></span>
<span><span style="color: var(--shiki-color-text)"> elevation</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">270</span><span style="color: var(--shiki-token-punctuation)">,</span></span>
<span><span style="color: var(--shiki-color-text)">})</span></span>
<span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">report</span><span style="color: var(--shiki-color-text)">.yallopCriteria) </span><span style="color: var(--shiki-token-comment)">// 'A' — easily visible</span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">report</span><span style="color: var(--shiki-color-text)">.odehCriteria) </span><span style="color: var(--shiki-token-comment)">// 'visible'</span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">`Best time: </span><span style="color: var(--shiki-token-keyword)">${</span><span style="color: var(--shiki-token-constant)">report</span><span style="color: var(--shiki-token-function)">.</span><span style="color: var(--shiki-token-constant)">bestTime</span><span style="color: var(--shiki-token-function)">.toUTCString</span><span style="color: var(--shiki-color-text)">()</span><span style="color: var(--shiki-token-keyword)">}</span><span style="color: var(--shiki-token-string-expression)">`</span><span style="color: var(--shiki-color-text)">)</span></span>
<span><span style="color: var(--shiki-token-constant)">console</span><span style="color: var(--shiki-token-function)">.log</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">`Crescent width: </span><span style="color: var(--shiki-token-keyword)">${</span><span style="color: var(--shiki-token-constant)">report</span><span style="color: var(--shiki-token-function)">.</span><span style="color: var(--shiki-token-constant)">crescentWidth</span><span style="color: var(--shiki-token-function)">.toFixed</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">1</span><span style="color: var(--shiki-color-text)">)</span><span style="color: var(--shiki-token-keyword)">}</span><span style="color: var(--shiki-token-string-expression)">′`</span><span style="color: var(--shiki-color-text)">)</span></span>
<span></span>
Visibility criteria
Yallop criteria (NAO TN 69)
The Yallop q-value determines naked-eye visibility:
| q-value | Category | Meaning |
|---|---|---|
| q > +0.216 | A | Easily visible |
| 0.216 ≥ q > −0.014 | B | Visible under perfect conditions |
| −0.014 ≥ q > −0.160 | C | May need optical aid to find |
| −0.160 ≥ q > −0.232 | D | Only visible with optical aid |
| −0.232 ≥ q > −0.293 | E | Not visible with optical aid |
| q ≤ −0.293 | F | Below the new moon limit |
Odeh criteria
Mohammad Odeh (2004) updated the Yallop model with additional observational data:
| Category | Meaning |
|---|---|
| visible | Crescent visible to the naked eye |
| marginal | Crescent visibility uncertain |
| not-visible | Crescent not visible (optical aid only or invisible) |
CLI usage
<span><span style="color: var(--shiki-token-comment)"># Check crescent visibility for Riyadh</span></span>
<span><span style="color: var(--shiki-token-function)">npx</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">moon-sighting</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">--date</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">2026</span><span style="color: var(--shiki-token-string)">-02-17</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">--lat</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">24.6877</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string)">--lng</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">46.7219</span></span>
<span></span>
<span><span style="color: var(--shiki-token-comment)"># Output:</span></span>
<span><span style="color: var(--shiki-token-comment)"># Yallop: A (easily visible)</span></span>
<span><span style="color: var(--shiki-token-comment)"># Best time: 17:43 local (19:43 UTC)</span></span>
<span><span style="color: var(--shiki-token-comment)"># Azimuth: 248°</span></span>
<span><span style="color: var(--shiki-token-comment)"># Moon altitude: 12.3°</span></span>
<span><span style="color: var(--shiki-token-comment)"># Crescent width: 0.8′</span></span>
<span></span>