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)">&lt;</span><span style="color: var(--shiki-color-text)">MoonSightingReport</span><span style="color: var(--shiki-token-keyword)">&gt;</span></span>
<span></span>

Options

ParameterTypeRequiredDescription
dateDateYesDate of the expected new moon
latitudenumberYesObserver latitude
longitudenumberYesObserver longitude
elevationnumberNoMetres 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)">&#39;A&#39;</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)">&#39;B&#39;</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)">&#39;C&#39;</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)">&#39;D&#39;</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)">&#39;E&#39;</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)">&#39;F&#39;</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)">&#39;visible&#39;</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)">&#39;not-visible&#39;</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)">&#39;marginal&#39;</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)">&#39;moon-sighting&#39;</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)">&#39;2026-02-17&#39;</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)">// &#39;A&#39; — 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)">// &#39;visible&#39;</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-valueCategoryMeaning
q > +0.216AEasily visible
0.216 ≥ q > −0.014BVisible under perfect conditions
−0.014 ≥ q > −0.160CMay need optical aid to find
−0.160 ≥ q > −0.232DOnly visible with optical aid
−0.232 ≥ q > −0.293ENot visible with optical aid
q ≤ −0.293FBelow the new moon limit

Odeh criteria

Mohammad Odeh (2004) updated the Yallop model with additional observational data:

CategoryMeaning
visibleCrescent visible to the naked eye
marginalCrescent visibility uncertain
not-visibleCrescent 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>