Skip to content
Documentation · all sections

Eclipses & Moon Phases

panchang · v5.4.0 · MIT

Solar and lunar eclipse detection with sutak windows, upcoming-eclipse search, precise lunar phase instants, and their engine-free table readers.

Eclipse on the daily result

If an eclipse belongs to the day you asked for, the daily result carries it in eclipse. On every other day that field is null. Which day that is has a precise rule, below.

import { getDailyPanchang } from 'panchang-ts';

const VARANASI = { latitude: 25.3176, longitude: 82.9739 };
const r = getDailyPanchang(new Date('2025-09-07'), VARANASI, { timezone: 330 })!;

if (r.eclipse) {
  console.log(r.eclipse.kind, r.eclipse.subtype);
  console.log(r.eclipse.obscuration);          // fraction of the disc AREA
  console.log(r.eclipse.magnitude);            // catalogue DIAMETER fraction
  console.log(r.eclipse.visibleFromLocation);  // body above horizon at peak
  console.log(r.eclipse.peakLocal);
  console.log(r.eclipse.sutakStartLocal, r.eclipse.sutakEndLocal);
}
// lunar total
// 1
// 1.3621799606665859
// true
// 2025-09-07T23:41:48.389+05:30
// 2025-09-07T12:57:05.498+05:30 2025-09-08T01:26:33.726+05:30

The five instants are start, peak, end, sutakStart and sutakEnd. Each one is a real Date, and each has a *Local string next to it carrying the location's offset, which is what you print. Sutak opens 12 hours (4 prahara) before a solar eclipse's start and 9 hours (3 prahara) before a lunar eclipse's umbral first contact, which comes after its start (the penumbral first contact). It closes at the solar end, or at the lunar umbral last contact.

Which day an eclipse belongs to

The day runs from sunrise to the next sunrise. A lunar eclipse belongs to the day its peak falls in. A solar eclipse belongs to the day it is first seen with the Sun up: at its peak if the Sun is up then, else at first contact, else at last contact. So one already under way at sunrise belongs to that sunrise's day, and no eclipse shows on more than one day. The surya_grahan and chandra_grahan festival entries follow the same day.

That holds since 5.4. Before, an eclipse could show on two consecutive days when sunrise fell between its syzygy and its peak: Varanasi's partial solar eclipse of 9 March 2016 also appeared, with its surya_grahan entry, on the 8th.

A solar eclipse that peaks below the horizon

visibleFromLocation is about the peak, and so are obscuration and magnitude. When a solar eclipse peaks with the Sun below the horizon but is seen at sunrise or sunset, its subtype and description describe the deepest phase you can actually see, at that sunrise or sunset. New York's partial eclipse of 29 March 2025 peaked at 06:12 EDT, half an hour before sunrise, and the Sun rose with 24% of its disc still covered.

import { getDailyPanchang } from 'panchang-ts';

const NY = { latitude: 40.7128, longitude: -74.0060 };
// noon UTC is 08:00 EDT, inside the New York day you mean
const r = getDailyPanchang(new Date('2025-03-29T12:00:00Z'), NY, { timezone: 'America/New_York' })!;
const e = r.eclipse!;

console.log(e.kind, e.subtype, e.visibleFromLocation);
console.log(Math.round(e.obscuration * 100));   // at the peak
console.log(e.description);                      // at the deepest phase seen
console.log(e.startLocal, e.peakLocal, e.endLocal);
// solar partial false
// 78
// Partial solar eclipse: 24% obscuration, visible from location.
// 2025-03-29T05:23:47.100-04:00 2025-03-29T06:12:54.416-04:00 2025-03-29T07:04:54.608-04:00

5.3 described the peak instead, as 78% and “not visible from location”, for an eclipse New York could watch at sunrise, and it listed the eclipse on 28 March as well. getUpcomingSolarEclipse and the listings below describe it the same way. The eclipses table is unaffected: it writes its own description from the peak values and marks this case with visibleAtPeak: false. See Upgrading 5.3 → 5.4.

Five calls find eclipses without you walking the calendar day by day. The two getUpcoming* calls take a search window in days and give you the first hit. getUpcomingEclipses gives you the next N of either kind. The last two list the eclipses whose peak falls in a local calendar year, or in a span you give, sorted by peak. Solar eclipses the location never sees are left out of all of them.

import {
  getUpcomingSolarEclipse, getUpcomingLunarEclipse,
  getUpcomingEclipses, computeEclipsesForYear, computeEclipsesInRange,
} from 'panchang-ts';

const VARANASI = { latitude: 25.3176, longitude: 82.9739 };
const from = new Date('2027-01-01');

const solar = getUpcomingSolarEclipse(from, VARANASI, 365);   // null if none in window
console.log(!!solar, solar!.subtype, solar!.peak.toISOString());

const lunar = getUpcomingLunarEclipse(from, VARANASI, 365)!;
console.log(lunar.subtype, lunar.peak.toISOString());

console.log(getUpcomingEclipses(from, VARANASI, 3).length);
console.log(computeEclipsesForYear(2027, VARANASI, { timezone: 330 }).length);
console.log(computeEclipsesInRange(new Date('2000-01-01'), new Date('2011-01-01'), VARANASI).length);
// true partial 2027-08-02T11:07:53.603Z
// penumbral 2027-02-20T23:12:55.672Z
// 3
// 4
// 32

In TypeScript a search hit is EclipseInfo-shaped, so you can pass one anywhere r.eclipse fits. The *Local strings are the exception: only getDailyPanchang fills them in, so format a search hit's instants yourself from its Date fields, and on the two getUpcoming* calls the *Local properties are not on the returned type at all. In Go a search hit is a separate struct. Its instants are epoch milliseconds such as PeakMs with no local-time strings, and the two upcoming calls take a language and return an ok flag rather than a null. Every call on this page that walks a range (getUpcomingEclipses, the two listings above, the moon-phase listings and both table builders below) also has a Go twin ending in Context, such as ComputeEclipsesInRangeContext, that takes a context.Context as its first argument and stops early once that context is cancelled or its deadline passes. The plain form runs to completion. GetUpcomingEclipsesContext is new in 5.4; it matters because GetUpcomingEclipses keeps walking until it has count eclipses, so a large count runs long.

A null from getUpcomingSolarEclipse or getUpcomingLunarEclipse means no eclipse in the window. Since 5.4 an Invalid Date passed to either throws INVALID_DATE instead of coming back null as well.

Listings select by peak

computeEclipsesInRange and computeEclipsesForYear keep an eclipse when its peak falls inside the span, so an eclipse near a boundary is listed exactly once. Before 5.4 they selected by the syzygy, so an eclipse close to a boundary could land in two adjacent listings, or in neither. At Darwin, the lunar eclipse that peaks at 23:58 local time on 31 December 2066 appeared in both the 2066 and the 2067 listings; it is now in 2066 only.

computeEclipsesForYear covers the local calendar year in timezone, taking each end of it at the offset in force on that date, which is right in a zone with daylight saving too (5.3 took both ends at the 1 July offset). Years 1900 and 2100 work at every offset. A year outside 1900 to 2100 throws INVALID_DATE, and since 5.4 that includes years 0 to 99, which 5.3 read as 1900 to 1999.

Build an eclipses table and cache it

No eclipse table ships with the library. Which eclipses are visible from a place, and so which of them carry sutak, depends on where you are. Build the table once for your location, save the JSON, and read it back later. Reading needs no engine, the same as the festivals table.

Build
import { buildEclipsesTable } from 'panchang-ts';             // needs the engine

const table = buildEclipsesTable({
  location: { latitude: 25.3176, longitude: 82.9739 },   // Varanasi
  timezoneOffsetMinutes: 330,
  startYear: 2024,
  endYear: 2031,
  languages: ['en', 'hi'],
  // visibleOnly: false → also include eclipses below the horizon (no sutak)
});
// persist JSON.stringify(table) — 8 years is ~8 KB

timezoneOffsetMinutes must be a whole number of minutes from -720 to 840, or the builder throws PanchangError INVALID_TIMEZONE, and a language other than 'en' or 'hi' throws a RangeError (INVALID_INPUT in Go). Both checks, on both table builders on this page, are new in 5.4: 5.3 built a table for an offset of 330.5 or 900, and crashed with a TypeError on an unknown language.

Read
import { readEclipsesForYear, readEclipsesForDate, readEclipsesYearRange }
  from 'panchang-ts/eclipses';                                // engine-free

// `table` is the saved JSON, parsed back

console.log(readEclipsesYearRange(table));
const e = readEclipsesForYear(table, 2025)![0].eclipses[0];
console.log(e.kind, e.subtype, e.obscuration, e.visibleAtPeak);
console.log(e.sutak!.start, e.sutak!.end);
console.log(readEclipsesForDate(table, '2025-09-07', 'hi')[0].name);
// { start: 2024, end: 2031 }
// lunar total 1 true
// 2025-09-07T07:27:05.498Z 2025-09-07T19:56:33.726Z
// पूर्ण चंद्र ग्रहण

// e also carries start, peak and end as ISO UTC strings, plus magnitude
// and visibleFromLocation

In Go the readers are package functions, new in 5.4: ReadEclipsesForYear, ReadEclipsesForDate (a time.Time), ReadEclipsesForDateKey (a YYYY-MM-DD string) and ReadEclipsesYearRange. Like the TypeScript subpath they need no engine, so no Session.

By default a table lists every eclipse visible from the location during any phase, so one already under way at moonrise or sunset is included. visibleAtPeak tells you whether greatest eclipse itself is above the horizon. In a table entry visibleFromLocation means visible during some phase, unlike the visibleFromLocation of a daily or search result, which is about the peak. Solar subtypes are what you would see locally, so an eclipse that is total somewhere on Earth can read partial from your location.

Moon phases

The four principal phases are new (Amavasya), first quarter, full (Purnima) and last quarter. You get them as exact instants. They are not the tithis of the same name, which are windows of roughly 24 hours.

import { computeMoonPhasesInRange, computeMoonPhasesForYear } from 'panchang-ts';

const phases = computeMoonPhasesInRange(new Date('2026-01-01'), new Date('2026-12-31'));
console.log(phases.length);
console.log(phases[0].phase, phases[0].time.toISOString());

console.log(computeMoonPhasesForYear(2027, { timezone: 330 }).length);
// 50
// full 2026-01-03T10:02:54.562Z
// 49

computeMoonPhasesForYear lists the phases of the local calendar year in timezone, taking each end of it at the offset in force on that date. Since 5.4 that holds in a zone with daylight saving too. 5.3 took both ends at the 1 July offset, so in New York the last quarter at 23:34 EST on 31 December 1920 was filed under 1921; it is now in 1920. Years 1900 and 2100 work at every offset, and a year outside 1900 to 2100 throws INVALID_DATE, including 0 to 99, which 5.3 quietly read as 1900 to 1999.

Moon-phases table

Same build-and-cache pattern, at panchang-ts/moon-phases. A phase is a global instant, so buildMoonPhasesTable takes no coordinates. The timezoneOffsetMinutes you give it only decides which calendar date each instant is filed under: a new moon at 19:52 UTC on Jan 18 is listed under Jan 19 in IST.

Build
import { buildMoonPhasesTable } from 'panchang-ts';          // needs the engine

const table = buildMoonPhasesTable({
  timezoneOffsetMinutes: 330,    // IST; -300 = US Eastern
  startYear: 2024,
  endYear: 2031,
  languages: ['en', 'hi'],
});
// persist JSON.stringify(table)
Read
import { readMoonPhasesForYear, readMoonPhasesForDate, readMoonPhasesYearRange }
  from 'panchang-ts/moon-phases';                            // engine-free

console.log(readMoonPhasesYearRange(table));
console.log(readMoonPhasesForYear(table, 2026)!.length);
const [en] = readMoonPhasesForDate(table, '2026-01-03');
const [hi] = readMoonPhasesForDate(table, '2026-01-03', 'hi');
console.log(en.phase, en.name, hi.name, en.time);
// { start: 2024, end: 2031 }
// 50
// full Full Moon पूर्णिमा 2026-01-03T10:02:54.562Z

Each entry gives you phase, the phase time as an ISO UTC string, and a name plus description in the language you asked for — one language per call, English by default.

In Go the same four readers are package functions, new in 5.4, named like the eclipse ones: ReadMoonPhasesForYear, ReadMoonPhasesForDate, ReadMoonPhasesForDateKey and ReadMoonPhasesYearRange. They take a types.AnyMoonPhasesFile, which also decodes a table written by 4.x.