@ishubhamx/panchangam-js
Version:
Enhanced Indian Panchangam (Hindu Calendar) library with comprehensive Vedic features including Muhurta calculations, planetary positions, Rashi placements, and auspicious/inauspicious time calculations
1,106 lines • 70 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTithi = getTithi;
exports.getNakshatra = getNakshatra;
exports.getYoga = getYoga;
exports.getKarana = getKarana;
exports.getVara = getVara;
exports.getSunrise = getSunrise;
exports.getSunset = getSunset;
exports.getMoonrise = getMoonrise;
exports.getMoonset = getMoonset;
exports.findNakshatraStart = findNakshatraStart;
exports.findNakshatraEnd = findNakshatraEnd;
exports.findTithiStart = findTithiStart;
exports.findTithiEnd = findTithiEnd;
exports.findYogaEnd = findYogaEnd;
exports.getPlanetaryPosition = getPlanetaryPosition;
exports.getRahuPosition = getRahuPosition;
exports.getKetuPosition = getKetuPosition;
exports.calculateAbhijitMuhurta = calculateAbhijitMuhurta;
exports.calculateBrahmaMuhurta = calculateBrahmaMuhurta;
exports.calculateGovardhanMuhurta = calculateGovardhanMuhurta;
exports.calculateYamagandaKalam = calculateYamagandaKalam;
exports.calculateGulikaKalam = calculateGulikaKalam;
exports.calculateDurMuhurta = calculateDurMuhurta;
exports.calculateChandraBalam = calculateChandraBalam;
exports.getCurrentHora = getCurrentHora;
exports.calculateRahuKalam = calculateRahuKalam;
exports.findKaranaTransitions = findKaranaTransitions;
exports.findTithiTransitions = findTithiTransitions;
exports.findNakshatraTransitions = findNakshatraTransitions;
exports.findYogaTransitions = findYogaTransitions;
exports.getPaksha = getPaksha;
exports.getAyana = getAyana;
exports.getRitu = getRitu;
exports.getMasa = getMasa;
exports.getSamvat = getSamvat;
exports.getNakshatraPada = getNakshatraPada;
exports.getRashi = getRashi;
exports.getSunNakshatra = getSunNakshatra;
exports.findNextSankranti = findNextSankranti;
exports.findSankrantisInRange = findSankrantisInRange;
exports.getSankrantiForDate = getSankrantiForDate;
exports.getPanchak = getPanchak;
exports.getUdayaLagna = getUdayaLagna;
exports.findRashiTransitions = findRashiTransitions;
exports.calculateTaraBalam = calculateTaraBalam;
exports.calculateChandraBalamFromRashi = calculateChandraBalamFromRashi;
exports.calculateVarjyam = calculateVarjyam;
exports.calculateAmritKalam = calculateAmritKalam;
exports.getSpecialYoga = getSpecialYoga;
exports.calculateVimshottariDasha = calculateVimshottariDasha;
const ayanamsa_1 = require("./ayanamsa");
const tarabalam_1 = require("./tarabalam");
const astronomy_engine_1 = require("astronomy-engine");
const constants_1 = require("./constants");
// ===== Named Constants =====
/** Milliseconds in one day */
const MS_PER_DAY = 24 * 60 * 60 * 1000;
/** Milliseconds in one hour */
const MS_PER_HOUR = 60 * 60 * 1000;
/** Milliseconds in one minute */
const MS_PER_MINUTE = 60 * 1000;
/** Maximum iterations for binary search convergence */
const BINARY_SEARCH_MAX_ITERATIONS = 20;
/** Lookahead window for binary search (2 days in ms) */
const BINARY_SEARCH_LOOKAHEAD_MS = 2 * MS_PER_DAY;
/** Lookback window for finding nakshatra start (32 hours in ms) */
const NAKSHATRA_LOOKBACK_MS = 32 * MS_PER_HOUR;
/** Julian Day of Unix Epoch (1970-01-01T00:00:00 UTC) */
const JD_UNIX_EPOCH = 2440587.5;
/** Julian Day of J2000.0 epoch */
const JD_J2000 = 2451545.0;
/** Days per Julian century */
const DAYS_PER_CENTURY = 36525.0;
/** Milliseconds per Gregorian year (365.25 days) */
const MS_PER_YEAR = 365.25 * MS_PER_DAY;
/** Sankranti search window (40 days in ms) */
const SANKRANTI_SEARCH_WINDOW_MS = 40 * MS_PER_DAY;
/** Sankranti search lookback (35 days in ms) */
const SANKRANTI_LOOKBACK_MS = 35 * MS_PER_DAY;
/** Advance to next Sankranti (~25 days in ms) */
const SANKRANTI_ADVANCE_MS = 25 * MS_PER_DAY;
/** Finite difference half-window for speed calculation (30 minutes in ms) */
const SPEED_CALC_HALF_WINDOW_MS = 30 * MS_PER_MINUTE;
/**
* Calculate Tithi (lunar day) from Sun and Moon sidereal longitudes.
*
* @param sunLon - Sidereal longitude of the Sun (0-360)
* @param moonLon - Sidereal longitude of the Moon (0-360)
* @returns Tithi index, **0-indexed** (0-29).
* 0-14 = Shukla Prathama to Purnima,
* 15-29 = Krishna Prathama to Amavasya.
* Use `getTithiAtSunrise()` from udaya-tithi.ts for 1-indexed (1-30) values
* required by festival detection.
*/
function getTithi(sunLon, moonLon) {
let longitudeDifference = moonLon - sunLon;
if (longitudeDifference < 0) {
longitudeDifference += 360;
}
return Math.floor(longitudeDifference / 12);
}
/**
* Calculate the current Nakshatra (lunar mansion) from the Moon's sidereal longitude.
* @param moonLon - Sidereal longitude of the Moon (0-360)
* @returns Nakshatra index (0-26): 0 = Ashwini, 26 = Revati
*/
function getNakshatra(moonLon) {
return Math.floor(moonLon / (13 + 1 / 3));
}
/**
* Calculate the current Yoga from Sun and Moon sidereal longitudes.
* @param sunLonSidereal - Sidereal longitude of the Sun (0-360)
* @param moonLonSidereal - Sidereal longitude of the Moon (0-360)
* @returns Yoga index (0-26): 0 = Vishkambha, 26 = Vaidhriti
*/
function getYoga(sunLonSidereal, moonLonSidereal) {
const totalLongitude = (sunLonSidereal + moonLonSidereal) % 360;
return Math.floor(totalLongitude / (13 + 1 / 3)) % 27;
}
/**
* Calculate the current Karana (half-tithi) from Sun and Moon longitudes.
* @param sunLon - Sidereal longitude of the Sun (0-360)
* @param moonLon - Sidereal longitude of the Moon (0-360)
* @returns Karana name string
*/
function getKarana(sunLon, moonLon) {
let longitudeDifference = moonLon - sunLon;
if (longitudeDifference < 0) {
longitudeDifference += 360;
}
const karanaIndexAbs = Math.floor(longitudeDifference / 6);
if (karanaIndexAbs === 0) {
return "Kimstughna";
}
if (karanaIndexAbs === 57) {
return "Shakuni";
}
if (karanaIndexAbs === 58) {
return "Chatushpada";
}
if (karanaIndexAbs === 59) {
return "Naga";
}
const repeatingIndex = (karanaIndexAbs - 1) % 7;
return constants_1.repeatingKaranaNames[repeatingIndex];
}
/**
* Returns the weekday (0=Sunday, ...) based on the Observer's local time.
*
* @param date - Date to check
* @param observer - Observer location (used to approximate timezone from longitude if timezoneOffsetMinutes is not given)
* @param timezoneOffsetMinutes - Explicit timezone offset in minutes (e.g. 330 for IST). Preferred over longitude approximation.
*/
function getVara(date, observer, timezoneOffsetMinutes) {
if (timezoneOffsetMinutes !== undefined) {
const tzOffsetMs = timezoneOffsetMinutes * MS_PER_MINUTE;
const localDate = new Date(date.getTime() + tzOffsetMs);
return localDate.getUTCDay();
}
if (observer) {
// Approximate timezone from longitude: 15° = 1 hour
// NOTE: This can be ~20min off for zones like IST (5.5h vs 5.17h for 77.6°E).
// Prefer passing timezoneOffsetMinutes for accuracy.
const tzOffsetMs = (observer.longitude / 15.0) * MS_PER_HOUR;
const localDate = new Date(date.getTime() + tzOffsetMs);
return localDate.getUTCDay();
}
return date.getUTCDay();
}
function getStartOfLocalDay(date, observer, options) {
let tzOffsetMs;
if (options && options.timezoneOffset !== undefined) {
// User provided offset in minutes.
// Convention: Timezone Offset is usually defined as Local - UTC in minutes?
// JS date.getTimezoneOffset() returns UTC - Local in minutes.
// Let's assume input 'timezoneOffset' matches standard connection:
// +330 for IST (+5:30), -480 for PST (-8:00).
// So we add this to UTC timestamp to get Local Time.
tzOffsetMs = options.timezoneOffset * MS_PER_MINUTE;
}
else {
// Approximate Timezone Offset based on Longitude
// 15 degrees = 1 hour. East is positive, West is negative.
// Rounding to nearest hour handles cases like Seattle (-122.1 => -8.14 => -8) better than floor/raw.
tzOffsetMs = Math.round(observer.longitude / 15.0) * MS_PER_HOUR;
}
// Create a date shifted to "Observer Local Time"
const localDate = new Date(date.getTime() + tzOffsetMs);
localDate.setUTCHours(0, 0, 0, 0); // Set to Local Midnight
// Shift back to UTC to get the actual UTC timestamp of Local Midnight
const startOfDay = new Date(localDate.getTime() - tzOffsetMs);
// End of day is 24 hours later
const endOfDay = new Date(startOfDay.getTime() + MS_PER_DAY - 1);
return { start: startOfDay, end: endOfDay };
}
function getSunrise(date, observer, options) {
const { start: startOfDay, end: endOfDay } = getStartOfLocalDay(date, observer, options);
// Always search forward (+1) from start of the local day
const time = (0, astronomy_engine_1.SearchRiseSet)(astronomy_engine_1.Body.Sun, observer, 1, startOfDay, 1);
if (!time)
return null;
const sunrise = time.date;
if (sunrise >= startOfDay && sunrise <= endOfDay) {
return sunrise;
}
return null;
}
function getSunset(date, observer, options) {
const { start: startOfDay, end: endOfDay } = getStartOfLocalDay(date, observer, options);
// Search for SET (-1) event starting from local midnight
const time = (0, astronomy_engine_1.SearchRiseSet)(astronomy_engine_1.Body.Sun, observer, -1, startOfDay, 1);
if (!time)
return null;
const sunset = time.date;
if (sunset >= startOfDay && sunset <= endOfDay) {
return sunset;
}
return null;
}
function getMoonrise(date, observer, options) {
const { start: startOfDay, end: endOfDay } = getStartOfLocalDay(date, observer, options);
const time = (0, astronomy_engine_1.SearchRiseSet)(astronomy_engine_1.Body.Moon, observer, 1, startOfDay, 1);
if (!time)
return null;
const moonrise = time.date;
if (moonrise >= startOfDay && moonrise <= endOfDay) {
return moonrise;
}
return null;
}
function getMoonset(date, observer, options) {
const { start: startOfDay, end: endOfDay } = getStartOfLocalDay(date, observer, options);
// Search for SET (-1) event starting from local midnight
const time = (0, astronomy_engine_1.SearchRiseSet)(astronomy_engine_1.Body.Moon, observer, -1, startOfDay, 1);
if (!time)
return null;
const moonset = time.date;
if (moonset >= startOfDay && moonset <= endOfDay) {
return moonset;
}
return null;
}
/**
* A generic search function to find the time when a function f(t) crosses zero.
* It uses a binary search approach.
*/
function search(f, startDate) {
let a = startDate;
let b = new Date(startDate.getTime() + BINARY_SEARCH_LOOKAHEAD_MS);
let fa = f(a);
let fb = f(b);
if (fa * fb >= 0) {
return null;
}
for (let i = 0; i < BINARY_SEARCH_MAX_ITERATIONS; i++) {
const m = new Date((a.getTime() + b.getTime()) / 2);
const fm = f(m);
if (fm * fa < 0) {
b = m;
fb = fm;
}
else {
a = m;
fa = fm;
}
}
return a;
}
function findNakshatraStart(date, ayanamsa) {
const moonLonInitial = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, date, true)).elon;
// Sidereal Longitude
const moonLonSidereal = (moonLonInitial - ayanamsa + 360) % 360;
const currentNakshatraIndex = Math.floor(moonLonSidereal / (13 + 1 / 3));
const startNakshatraLongitude = currentNakshatraIndex * (13 + 1 / 3);
const targetLon = startNakshatraLongitude; // This is in Sidereal frame
const nakshatraFunc = (d) => {
let moonLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
let moonLonSid = (moonLon - ayanamsa + 360) % 360;
// Handle the 360->0 wrap-around for the search.
if (moonLonSid > targetLon + 180) {
moonLonSid -= 360;
}
// Standard diff logic
let diff = moonLonSid - targetLon;
if (diff > 180)
diff -= 360;
if (diff < -180)
diff += 360;
return diff;
};
// A nakshatra lasts about a day (mean 24h 20m, max can be ~26h+).
// Searching from 32 hours before ensures we catch the start even for long nakshatras.
const searchStartDate = new Date(date.getTime() - NAKSHATRA_LOOKBACK_MS);
return search(nakshatraFunc, searchStartDate);
}
function findNakshatraEnd(date, ayanamsa) {
const moonLonInitial = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, date, true)).elon;
const moonLonSidereal = (moonLonInitial - ayanamsa + 360) % 360;
const currentNakshatra = Math.floor(moonLonSidereal / (13 + 1 / 3));
const nextNakshatraLongitude = (currentNakshatra + 1) * (13 + 1 / 3);
const targetLon = nextNakshatraLongitude % 360;
const nakshatraFunc = (d) => {
let moonLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
let moonLonSid = (moonLon - ayanamsa + 360) % 360;
// Handle the 360->0 wrap-around
let diff = moonLonSid - targetLon;
if (diff > 180)
diff -= 360;
if (diff < -180)
diff += 360;
return diff;
};
return search(nakshatraFunc, date);
}
function findTithiStart(date) {
const sunLonInitial = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, date, true)).elon;
const moonLonInitial = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, date, true)).elon;
let diffInitial = moonLonInitial - sunLonInitial;
if (diffInitial < 0)
diffInitial += 360;
const currentTithi = Math.floor(diffInitial / 12);
const startTithiAngle = currentTithi * 12;
const targetAngle = startTithiAngle % 360;
// Fix for findTithiStart
const tithiFuncStart = (d) => {
const sunLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, d, true)).elon;
const moonLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
let diff = moonLon - sunLon;
if (diff < 0)
diff += 360;
// Special handling for Target 0 (Amavasya -> Prathama boundary)
if (targetAngle === 0) {
if (diff > 180) {
return diff - 360;
}
return diff;
}
// Handle the 360->0 wrap-around for search.
if (diff > targetAngle + 180) {
diff -= 360;
}
if (diff < targetAngle - 180) {
diff += 360;
}
return diff - targetAngle;
};
// A tithi is slightly less than a day. Searching from 25h before is safe.
const searchStartDate = new Date(date.getTime() - 25 * MS_PER_HOUR);
return search(tithiFuncStart, searchStartDate);
}
function findTithiEnd(date) {
const sunLonInitial = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, date, true)).elon;
const moonLonInitial = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, date, true)).elon;
let diffInitial = moonLonInitial - sunLonInitial;
if (diffInitial < 0)
diffInitial += 360;
const currentTithi = Math.floor(diffInitial / 12);
const nextTithiAngle = (currentTithi + 1) * 12;
const targetAngle = nextTithiAngle % 360;
const tithiFunc = (d) => {
const sunLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, d, true)).elon;
const moonLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
let diff = moonLon - sunLon;
if (diff < 0)
diff += 360;
// Special handling for Target 0 (Amavasya -> Prathama boundary)
if (targetAngle === 0) {
if (diff > 180) {
return diff - 360;
}
return diff;
}
if (diff < targetAngle - 180) {
diff += 360;
}
return diff - targetAngle;
};
return search(tithiFunc, date);
}
function findYogaEnd(date, ayanamsa) {
const sunLonInitial = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, date, true)).elon;
const moonLonInitial = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, date, true)).elon;
const sunLonSid = (sunLonInitial - ayanamsa + 360) % 360;
const moonLonSid = (moonLonInitial - ayanamsa + 360) % 360;
const totalLongitudeInitial = (sunLonSid + moonLonSid) % 360;
const yogaWidth = 360 / 27; // 13 degrees 20 minutes
const currentYogaTotalIndex = Math.floor(totalLongitudeInitial / yogaWidth);
const nextYogaBoundary = (currentYogaTotalIndex + 1) * yogaWidth;
const yogaFunc = (d) => {
const sunLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, d, true)).elon;
const moonLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
let sunLonS = (sunLon - ayanamsa + 360) % 360;
let moonLonS = (moonLon - ayanamsa + 360) % 360;
let totalLon = (sunLonS + moonLonS) % 360;
if (totalLon < 90 && nextYogaBoundary > 270) {
totalLon += 360;
}
return totalLon - nextYogaBoundary;
};
return search(yogaFunc, date);
}
function getPlanetaryPosition(body, date, ayanamsa) {
// 1. Calculate Position at T
const vector = (0, astronomy_engine_1.GeoVector)(body, date, true);
const ecliptic = (0, astronomy_engine_1.Ecliptic)(vector);
const tropicalLon = ecliptic.elon;
const longitude = (tropicalLon - ayanamsa + 360) % 360;
const rashi = Math.floor(longitude / 30);
const degree = longitude % 30;
// 2. Calculate Speed & Retrograde (via Finite Difference of 1 hour)
// T_minus = date - 30 min, T_plus = date + 30 min
const tMinus = new Date(date.getTime() - SPEED_CALC_HALF_WINDOW_MS);
const tPlus = new Date(date.getTime() + SPEED_CALC_HALF_WINDOW_MS);
const vMinus = (0, astronomy_engine_1.GeoVector)(body, tMinus, true);
const eMinus = (0, astronomy_engine_1.Ecliptic)(vMinus).elon;
const vPlus = (0, astronomy_engine_1.GeoVector)(body, tPlus, true);
const ePlus = (0, astronomy_engine_1.Ecliptic)(vPlus).elon;
// Handle Wrap: 359 -> 1
let diff = ePlus - eMinus;
if (diff > 180)
diff -= 360;
if (diff < -180)
diff += 360;
// Diff is for 1 hour. Speed per day = Diff * 24.
const speed = diff * 24;
const dignity = getPlanetaryDignity(body, rashi);
return {
longitude,
rashi,
rashiName: constants_1.rashiNames[rashi],
degree,
isRetrograde: speed < 0,
speed,
dignity
};
}
function getPlanetaryDignity(planet, rashi) {
if (constants_1.planetExaltation[planet] === rashi)
return 'exalted';
if (constants_1.planetDebilitation[planet] === rashi)
return 'debilitated';
if (constants_1.planetOwnSigns[planet]?.includes(rashi))
return 'own';
return 'neutral';
}
// Julian Centuries from J2000.0
function getJulianCenturies(date) {
const jd = (date.getTime() / (MS_PER_DAY)) + JD_UNIX_EPOCH;
return (jd - JD_J2000) / DAYS_PER_CENTURY;
}
function getRahuPosition(date, ayanamsa) {
// Mean Node of Moon (Meeus, Ch 47)
// Ω = 125.04452 - 1934.136261 * T + 0.0020708 * T^2 + T^3 / 450000
const T = getJulianCenturies(date);
let meanNode = 125.04452 - 1934.136261 * T + 0.0020708 * T * T + (T * T * T) / 450000;
// Normalize to 0-360
meanNode = meanNode % 360;
if (meanNode < 0)
meanNode += 360;
// True Node vs Mean Node? Drik often uses True Node.
// Task requested "Rahu (Mean Node)". Sticking to Mean.
// Sidereal Longitude of Rahu
const longitude = (meanNode - ayanamsa + 360) % 360;
const rashi = Math.floor(longitude / 30);
const degree = longitude % 30;
// Nodes are always retrograde ( Mean Node is always retrograde, True Node varies slightly but general motion is retrograde).
// Speed: Derivative of formula. -1934 deg / century. Approx -0.05 deg/day.
const speed = -0.05295; // roughly -19 degrees per year
const dignity = getPlanetaryDignity("Rahu", rashi);
return {
longitude,
rashi,
rashiName: constants_1.rashiNames[rashi],
degree,
isRetrograde: true,
speed,
dignity
};
}
function getKetuPosition(rahuPos) {
const ketuLon = (rahuPos.longitude + 180) % 360;
const rashi = Math.floor(ketuLon / 30);
const degree = ketuLon % 30;
const dignity = getPlanetaryDignity("Ketu", rashi);
return {
longitude: ketuLon,
rashi,
rashiName: constants_1.rashiNames[rashi],
degree,
isRetrograde: true,
speed: rahuPos.speed,
dignity
};
}
function calculateAbhijitMuhurta(sunrise, sunset) {
if (!sunrise || !sunset)
return null;
const dayDuration = sunset.getTime() - sunrise.getTime();
// Rigorous: 8th Muhurta of the 15 segments of Dinamana
const muhurtaDuration = dayDuration / 15;
const abhijitStart = new Date(sunrise.getTime() + 7 * muhurtaDuration);
const abhijitEnd = new Date(sunrise.getTime() + 8 * muhurtaDuration);
return {
start: abhijitStart,
end: abhijitEnd
};
}
function calculateBrahmaMuhurta(sunrise, prevSunset) {
if (!sunrise)
return null;
let muhurtaDuration = 48 * MS_PER_MINUTE; // Default approximation
if (prevSunset) {
// Rigorous: Night Duration (Ratri Mana) divided by 15.
// Brahma Muhurta is the 14th Muhurta (2nd to last).
const nightDuration = sunrise.getTime() - prevSunset.getTime();
muhurtaDuration = nightDuration / 15;
}
// It ends 1 Muhurta before Sunrise, starts 2 Muhurtas before.
const brahmaMuhurtaEnd = new Date(sunrise.getTime() - 1 * muhurtaDuration);
const brahmaMuhurtaStart = new Date(sunrise.getTime() - 2 * muhurtaDuration);
return {
start: brahmaMuhurtaStart,
end: brahmaMuhurtaEnd
};
}
function calculateGovardhanMuhurta(sunrise, sunset) {
if (!sunrise || !sunset)
return null;
const dayDuration = sunset.getTime() - sunrise.getTime();
// Govardhan Muhurta is in the afternoon, typically in the 6th hour (5/8 to 6/8 of day)
const govardhanStart = new Date(sunrise.getTime() + (5 * dayDuration / 8));
const govardhanEnd = new Date(sunrise.getTime() + (6 * dayDuration / 8));
return {
start: govardhanStart,
end: govardhanEnd
};
}
function calculateYamagandaKalam(sunrise, sunset, vara) {
if (!sunrise || !sunset)
return null;
const daylightMillis = sunset.getTime() - sunrise.getTime();
const portionMillis = daylightMillis / 8;
// Yamaganda Kalam portions for each day: Sun, Mon, Tue, Wed, Thu, Fri, Sat
// Rule: Sun=5, Mon=4, Tue=3, Wed=2, Thu=1, Fri=7, Sat=6
const yamagandaPortionIndex = [5, 4, 3, 2, 1, 7, 6];
const portionIndex = yamagandaPortionIndex[vara];
const startMillis = sunrise.getTime() + (portionIndex - 1) * portionMillis;
const endMillis = sunrise.getTime() + portionIndex * portionMillis;
return {
start: new Date(startMillis),
end: new Date(endMillis)
};
}
function calculateGulikaKalam(sunrise, sunset, vara) {
if (!sunrise || !sunset)
return null;
const daylightMillis = sunset.getTime() - sunrise.getTime();
const portionMillis = daylightMillis / 8;
// Gulika Kalam portions for each day: Sun, Mon, Tue, Wed, Thu, Fri, Sat
// Rule: Sun=7, Mon=6, Tue=5, Wed=4, Thu=3, Fri=2, Sat=1
const gulikaPortionIndex = [7, 6, 5, 4, 3, 2, 1];
const portionIndex = gulikaPortionIndex[vara];
const startMillis = sunrise.getTime() + (portionIndex - 1) * portionMillis;
const endMillis = sunrise.getTime() + portionIndex * portionMillis;
return {
start: new Date(startMillis),
end: new Date(endMillis)
};
}
/**
* Calculate Dur (inauspicious) Muhurtas for the day.
* Traditional Panchanga specifies two dur muhurtas per weekday.
*
* @param sunrise - Sunrise time
* @param sunset - Sunset time
* @param vara - Weekday (0=Sun … 6=Sat). When omitted, falls back to static [4, 6].
*/
function calculateDurMuhurta(sunrise, sunset, vara) {
if (!sunrise || !sunset)
return null;
const dayDuration = sunset.getTime() - sunrise.getTime();
const muhurtaDuration = dayDuration / 15; // Day is divided into 15 muhurtas
// Weekday-specific dur muhurta indices (1-indexed muhurta numbers)
// Source: Traditional Muhurta Shastra
// Format: [muhurta1, muhurta2] for each vara Sun–Sat
const durMuhurtaByVara = [
[14, 10], // Sun
[8, 2], // Mon
[4, 10], // Tue
[12, 8], // Wed
[10, 2], // Thu
[4, 6], // Fri
[14, 6], // Sat
];
const indices = (vara !== undefined && vara >= 0 && vara <= 6)
? durMuhurtaByVara[vara]
: [4, 6]; // Fallback to the old default (Friday's values)
const durMuhurtas = [];
for (const idx of indices) {
const start = new Date(sunrise.getTime() + (idx - 1) * muhurtaDuration);
const end = new Date(sunrise.getTime() + idx * muhurtaDuration);
durMuhurtas.push({ start, end });
}
// Sort chronologically
durMuhurtas.sort((a, b) => a.start.getTime() - b.start.getTime());
return durMuhurtas;
}
function calculateChandraBalam(moonLon, sunLon) {
// Calculate moon strength based on the angular distance from sun
let angularDistance = Math.abs(moonLon - sunLon);
if (angularDistance > 180) {
angularDistance = 360 - angularDistance;
}
// Full moon (180 degrees apart) = 100% strength
// New moon (0 degrees apart) = 0% strength
return Math.round((angularDistance / 180) * 100);
}
/**
* Get the current Hora (planetary hour) lord.
*
* @param date - Current time
* @param sunrise - Sunrise time for the day
* @param observer - Observer location
* @param timezoneOffsetMinutes - Explicit TZ offset in minutes (e.g. 330 for IST) for accurate weekday
*/
function getCurrentHora(date, sunrise, observer, timezoneOffsetMinutes) {
if (!sunrise)
return constants_1.horaRulers[0]; // Default to Sun
const dayOfWeek = getVara(date, observer, timezoneOffsetMinutes);
const millisecondsFromSunrise = date.getTime() - sunrise.getTime();
// If the time is before sunrise, use the previous day's calculation
if (millisecondsFromSunrise < 0) {
// Calculate previous day's sunrise
const prevDay = new Date(date.getTime() - MS_PER_DAY);
const prevDayOfWeek = getVara(prevDay, observer, timezoneOffsetMinutes);
const hoursFromPrevSunrise = Math.abs(millisecondsFromSunrise) / (1000 * 60 * 60);
const dayStartPlanet = [0, 3, 6, 2, 5, 1, 4]; // Sun=0, Moon=3, Mars=6, Mercury=2, Jupiter=5, Venus=1, Saturn=4
const startPlanetIndex = dayStartPlanet[prevDayOfWeek];
const horaIndex = (startPlanetIndex + Math.floor(24 - hoursFromPrevSunrise)) % 7;
return constants_1.horaRulers[horaIndex];
}
const hoursFromSunrise = millisecondsFromSunrise / (1000 * 60 * 60);
// Each hora is approximately 1 hour
// Starting planet varies by day of week
const dayStartPlanet = [0, 3, 6, 2, 5, 1, 4]; // Sun=0, Moon=3, Mars=6, Mercury=2, Jupiter=5, Venus=1, Saturn=4
const startPlanetIndex = dayStartPlanet[dayOfWeek];
const horaIndex = (startPlanetIndex + Math.floor(hoursFromSunrise)) % 7;
return constants_1.horaRulers[horaIndex];
}
function calculateRahuKalam(sunrise, sunset, vara) {
if (!sunrise || !sunset) {
return null;
}
const daylightMillis = sunset.getTime() - sunrise.getTime();
const portionMillis = daylightMillis / 8;
const rahuKalamPortionIndex = [8, 2, 7, 5, 6, 4, 3]; // Sun, Mon, Tue, Wed, Thu, Fri, Sat
const portionIndex = rahuKalamPortionIndex[vara];
const startMillis = sunrise.getTime() + (portionIndex - 1) * portionMillis;
const endMillis = sunrise.getTime() + portionIndex * portionMillis;
return {
start: new Date(startMillis),
end: new Date(endMillis)
};
}
function findKaranaTransitions(startDate, endDate) {
const transitions = [];
let current = new Date(startDate);
let lastKarana = getKarana((0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, current, true)).elon, (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, current, true)).elon);
while (current < endDate) {
// Find next Karana end
const nextKaranaEnd = (() => {
// Karana changes every 6 degrees of moon-sun difference
const sunLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, current, true)).elon;
const moonLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, current, true)).elon;
let diff = moonLon - sunLon;
if (diff < 0)
diff += 360;
const karanaIndexAbs = Math.floor(diff / 6);
const nextKaranaAngle = (karanaIndexAbs + 1) * 6;
const targetAngle = nextKaranaAngle % 360;
const karanaFunc = (d) => {
const sunLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, d, true)).elon;
const moonLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
let diff = moonLon - sunLon;
if (diff < 0)
diff += 360;
if (diff < targetAngle - 180)
diff += 360;
return diff - targetAngle;
};
return search(karanaFunc, current);
})();
if (!nextKaranaEnd || nextKaranaEnd > endDate) {
// Last Karana for the day
transitions.push({ name: lastKarana, startTime: new Date(current), endTime: endDate });
break;
}
else {
transitions.push({ name: lastKarana, startTime: new Date(current), endTime: nextKaranaEnd });
current = new Date(nextKaranaEnd.getTime() + MS_PER_MINUTE); // move 1 min ahead to avoid infinite loop
lastKarana = getKarana((0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, current, true)).elon, (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, current, true)).elon);
}
}
return transitions;
}
function findTithiTransitions(startDate, endDate) {
const transitions = [];
let current = new Date(startDate);
let lastTithi = getTithi((0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, current, true)).elon, (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, current, true)).elon);
while (current < endDate) {
const nextTithiEnd = (() => {
const sunLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, current, true)).elon;
const moonLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, current, true)).elon;
let diff = moonLon - sunLon;
if (diff < 0)
diff += 360;
const tithiIndex = Math.floor(diff / 12);
const nextTithiAngle = (tithiIndex + 1) * 12;
const targetAngle = nextTithiAngle % 360;
const tithiFunc = (d) => {
const sunLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, d, true)).elon;
const moonLon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
let diff = moonLon - sunLon;
if (diff < 0)
diff += 360;
// If target is 0 (Amavasya -> Shukla Prathama), we need special handling
// because diff will jump from ~359 to ~1.
// We want the function to be continuous crossing zero.
if (targetAngle === 0) {
// If we are near 360 (e.g. 359), treat it as negative relative to 0
if (diff > 180) {
return diff - 360;
}
return diff;
}
if (diff < targetAngle - 180)
diff += 360;
return diff - targetAngle;
};
return search(tithiFunc, current);
})();
if (!nextTithiEnd || nextTithiEnd > endDate) {
transitions.push({ index: lastTithi, name: constants_1.tithiNames[lastTithi] || String(lastTithi), startTime: new Date(current), endTime: endDate });
break;
}
else {
transitions.push({ index: lastTithi, name: constants_1.tithiNames[lastTithi] || String(lastTithi), startTime: new Date(current), endTime: nextTithiEnd });
current = new Date(nextTithiEnd.getTime() + MS_PER_MINUTE);
lastTithi = getTithi((0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, current, true)).elon, (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, current, true)).elon);
}
}
return transitions;
}
function findNakshatraTransitions(startDate, endDate, ayanamsa) {
const transitions = [];
let current = new Date(startDate);
const getSiderealMoon = (d) => {
const m = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
return (m - ayanamsa + 360) % 360;
};
let lastNakshatra = getNakshatra(getSiderealMoon(current));
while (current < endDate) {
const nextNakshatraEnd = (() => {
const moonLonSid = getSiderealMoon(current);
const nakshatraIndex = Math.floor(moonLonSid / (13 + 1 / 3));
const nextNakshatraLongitude = (nakshatraIndex + 1) * (13 + 1 / 3);
const targetLon = nextNakshatraLongitude % 360;
const nakshatraFunc = (d) => {
let m = getSiderealMoon(d);
let diff = m - targetLon;
if (diff > 180)
diff -= 360;
if (diff < -180)
diff += 360;
return diff;
};
return search(nakshatraFunc, current);
})();
if (!nextNakshatraEnd || nextNakshatraEnd > endDate) {
transitions.push({ index: lastNakshatra, name: constants_1.nakshatraNames[lastNakshatra] || String(lastNakshatra), startTime: new Date(current), endTime: endDate });
break;
}
else {
transitions.push({ index: lastNakshatra, name: constants_1.nakshatraNames[lastNakshatra] || String(lastNakshatra), startTime: new Date(current), endTime: nextNakshatraEnd });
current = new Date(nextNakshatraEnd.getTime() + MS_PER_MINUTE);
lastNakshatra = getNakshatra(getSiderealMoon(current));
}
}
return transitions;
}
function findYogaTransitions(startDate, endDate, ayanamsa) {
const transitions = [];
let current = new Date(startDate);
const getSiderealSum = (d) => {
const sun = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, d, true)).elon;
const moon = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
return (((sun - ayanamsa + 360) % 360) + ((moon - ayanamsa + 360) % 360)) % 360;
};
let lastYoga = getYoga(((0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, current, true)).elon - ayanamsa + 360) % 360, ((0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, current, true)).elon - ayanamsa + 360) % 360);
while (current < endDate) {
const nextYogaEnd = (() => {
const totalLongitude = getSiderealSum(current);
const yogaWidth = 360 / 27;
const yogaIndex = Math.floor(totalLongitude / yogaWidth);
const nextYogaBoundary = (yogaIndex + 1) * yogaWidth;
const yogaFunc = (d) => {
let totalLon = getSiderealSum(d);
if (totalLon < 90 && nextYogaBoundary > 270) {
totalLon += 360;
}
return totalLon - nextYogaBoundary;
};
return search(yogaFunc, current);
})();
if (!nextYogaEnd || nextYogaEnd > endDate) {
transitions.push({ index: lastYoga, name: constants_1.yogaNames[lastYoga] || String(lastYoga), startTime: new Date(current), endTime: endDate });
break;
}
else {
transitions.push({ index: lastYoga, name: constants_1.yogaNames[lastYoga] || String(lastYoga), startTime: new Date(current), endTime: nextYogaEnd });
current = new Date(nextYogaEnd.getTime() + MS_PER_MINUTE);
const s = ((0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, current, true)).elon - ayanamsa + 360) % 360;
const m = ((0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, current, true)).elon - ayanamsa + 360) % 360;
lastYoga = getYoga(s, m);
}
}
return transitions;
}
function getPaksha(tithi) {
return (tithi >= 0 && tithi <= 14) ? constants_1.pakshaNames[0] : constants_1.pakshaNames[1];
}
function getAyana(sunLon) {
// Sun tropical longitude.
// 0-90: Uttarayana (Spring)
// 90-180: Dakshinayana (Summer)
// Wait, Tropical Cancer (90) is start of Dakshinayana.
// Tropical Capricorn (270) is start of Uttarayana.
// So 270 -> 360 -> 90 is Uttarayana.
// 90 -> 180 -> 270 is Dakshinayana.
if (sunLon >= 90 && sunLon < 270) {
return constants_1.ayanaNames[1]; // Dakshinayana
}
else {
return constants_1.ayanaNames[0]; // Uttarayana
}
}
function getRitu(sunLon) {
// 6 Ritus, 60 degrees each.
// Vasant: 330 - 30 (Pisces - Aries)
// Grishma: 30 - 90 (Taurus - Gemini)
// Varsha: 90 - 150
// Sharad: 150 - 210
// Hemant: 210 - 270
// Shishir: 270 - 330
// Normalize to 0-360 starting from 330?
// Let's use simple logic
if (sunLon >= 330 || sunLon < 30)
return constants_1.rituNames[0]; // Vasant
if (sunLon >= 30 && sunLon < 90)
return constants_1.rituNames[1]; // Grishma
if (sunLon >= 90 && sunLon < 150)
return constants_1.rituNames[2]; // Varsha
if (sunLon >= 150 && sunLon < 210)
return constants_1.rituNames[3]; // Sharad
if (sunLon >= 210 && sunLon < 270)
return constants_1.rituNames[4]; // Hemant
return constants_1.rituNames[5]; // Shishir
}
function getMasa(sunLon, moonLon, date, calendarType = 'purnimanta') {
// 1. Find previous New Moon
// Use an approximate earlier time to start search
// Avg deviation of Moon from Sun is 12.19 deg/day.
let diff = moonLon - sunLon;
while (diff < 0)
diff += 360;
// Search function: When (MoonLon - SunLon) % 360 = 0
// Search callback passes an object with .date property (AstroTime-like)
const angleFunc = (t) => {
const d = t.date;
const s = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, d, true)).elon;
const m = (0, astronomy_engine_1.Ecliptic)((0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Moon, d, true)).elon;
let df = m - s;
while (df < 0)
df += 360;
while (df >= 360)
df -= 360;
if (df > 180)
df -= 360;
return df;
};
// Days since last New Moon
const daysBack = diff / 12.19;
let newMoonDate;
// In the Amanta system the last day of the month is Amavasya (new-moon day).
// The civil day on which the new moon occurs belongs to the ENDING month,
// not the new one. Therefore we always search backward for the *previous*
// new moon to anchor the month. A forward search here would incorrectly
// assign Amavasya to the next month.
const startTime = new Date(date.getTime() - (daysBack + 1) * MS_PER_DAY);
const backwardEvent = (0, astronomy_engine_1.Search)(angleFunc, (0, astronomy_engine_1.MakeTime)(startTime), (0, astronomy_engine_1.MakeTime)(startTime).AddDays(5));
newMoonDate = backwardEvent ? backwardEvent.date : date;
const anchorDate = newMoonDate;
// 2. Get Sun Rashi at start (Previous New Moon)
const ayanamsa = (0, ayanamsa_1.getAyanamsa)(anchorDate);
const sunVectorStart = (0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, anchorDate, true);
const sunTropStart = (0, astronomy_engine_1.Ecliptic)(sunVectorStart).elon;
const sunSiderealStart = (sunTropStart - ayanamsa + 360) % 360;
const sunRashiStart = Math.floor(sunSiderealStart / 30);
// 3. Find Next New Moon to check if Sun changes Rashi (Sankranti)
// Approx 29.53 days later.
const nextNewMoonEst = new Date(anchorDate.getTime() + 29.53 * MS_PER_DAY);
const nextNewMoonEvent = (0, astronomy_engine_1.Search)(angleFunc, (0, astronomy_engine_1.MakeTime)(nextNewMoonEst), (0, astronomy_engine_1.MakeTime)(nextNewMoonEst).AddDays(2));
const nextNewMoonDate = nextNewMoonEvent ? nextNewMoonEvent.date : nextNewMoonEst;
// Get Sun Rashi at End (Next New Moon)
const ayanamsaEnd = (0, ayanamsa_1.getAyanamsa)(nextNewMoonDate);
const sunVectorEnd = (0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, nextNewMoonDate, true);
const sunTropEnd = (0, astronomy_engine_1.Ecliptic)(sunVectorEnd).elon;
const sunSiderealEnd = (sunTropEnd - ayanamsaEnd + 360) % 360;
const sunRashiEnd = Math.floor(sunSiderealEnd / 30);
// Adhika Masa if Sun Rashi strictly does not change
const isAdhika = (sunRashiStart === sunRashiEnd);
// Masa Index (Amanta)
let masaIndex = (sunRashiStart + 1) % 12;
// 4. Adjust for Purnimanta if needed
// In Purnimanta, the month changes at Purnima (Full Moon).
// The Krishna Paksha (Moon-Sun diff 180–360) belongs to the NEXT Amanta month.
if (calendarType === 'purnimanta') {
let angleDiff = moonLon - sunLon;
while (angleDiff < 0)
angleDiff += 360;
// Krishna Paksha: 180–360 degrees
if (angleDiff >= 180 && !isAdhika) {
masaIndex = (masaIndex + 1) % 12;
}
}
return {
index: masaIndex,
name: constants_1.masaNames[masaIndex],
isAdhika: isAdhika
};
}
function getSamvat(date, masaIndex) {
// Shaka Samvat
// Year AD - 78 (or 79 if before Chaitra)
// We already have masaIndex. If masaIndex >= 0 (Chaitra), it is AD-78.
// But masaIndex is based on Sun's Rashi.
// If Sun is in Pisces, it is Chaitra.
// This logic holds: New Year starts at Chaitra.
let yearAD = date.getUTCFullYear();
let shaka = yearAD - 78;
// If Month is Phalguna (11) or Pausha/Magha and it is early in the year...
// Actually, "Chaitra" starts when Sun enters Pisces (Minark).
// So if Sun Rashi is < 11 (Aquarius) and year is same?
// Let's rely on MasaIndex.
// If we are in the *end* of the Saka year (Phalguna), we are still in (Year-1).
// Chaitra (Index 0) is the start.
// But Chaitra usually falls in March/April.
// If date is Jan, we are in Pausha/Magha/Phalguna of previous Saka year.
// So if date < March 22 approx?
// Better: If MasaIndex is > 8 (approx Pausha, Magha, Phalguna) and Month is Jan/Feb/Mar...
// Actually simpler:
// If (MasaIndex == 11 (Phalguna) || MasaIndex == 10 (Magha) || MasaIndex == 9 (Pausha)), reduce Saka by 1.
// Why? Because Chaitra (0) starts roughly March.
// Jan/Feb will be Magha/Phalguna of *previous* Saka year.
if (masaIndex > 8 && date.getUTCMonth() < 3) {
shaka -= 1;
}
const vikram = shaka + 135;
// Samvatsara
// 60 year cycle.
// 2026 AD (Jan) -> Shaka 1947.
// Drik says "Kalayukta".
// Reference: 2023 AD -> Shaka 1945 -> "Shobhakrit" (37).
// Shaka 1947 should be 37 + 2 = 39?
// 1946 = Krodhi (38).
// 1947 = Vishvavasu (39).
// 2026 Jan 8 is Shaka 1947.
// Wait, Drik says: "Samvatsara: Kalayukta upto 03:07 PM, Apr 25, 2025"??
// No, screenshot says: "2082 Kalayukta".
// "Shaka Samvat 1947 Vishvavasu".
// 1947 -> Vishvavasu.
// My list index 38 is "Krodhi", 39 is "Vishvavasu".
// So index = (Shaka - Offset) % 60.
// 1945 -> 37.
// 1945 - X = 37. X = 1908.
// (1947 - 1908) % 60 = 39.
// Formula: (Shaka - 12) % 60 ? No. 1908 % 60 = 48.
// (Shaka + 9) % 60?
// (1945 + 9) % 60 = 1954 % 60 = 34. Close.
// Let's find Offset: (1945 + Offset) % 60 = 37.
// Offset = 37 - (1945 % 60) = 37 - 25 = 12.
// So Index = (Shaka + 12) % 60.
// Test: (1947 + 11) % 60 = 1958 % 60 = 38. Correct (Vishvavasu).
const samvatIndex = (shaka + 11) % 60;
const samvatsara = constants_1.samvatsaraNames[samvatIndex];
return { vikram, shaka, samvatsara };
}
function getNakshatraPada(moonLon) {
// Each Nakshatra is 13deg 20min (13.3333 deg)
// Each Pada is 1/4th of that = 3deg 20min (3.3333 deg)
// Formula: floor(moonLon / 3.3333) % 4 + 1
// Careful with precision. 3 deg 20 min = 3 + 20/60 = 3.33333333...
const padaLen = 3 + (20 / 60);
const totalPadas = Math.floor(moonLon / padaLen);
return (totalPadas % 4) + 1;
}
function getRashi(lon) {
const index = Math.floor(lon / 30);
// Handle edge case just in case 360 -> 12
const safeIndex = index % 12;
return {
index: safeIndex,
name: constants_1.rashiNames[safeIndex]
};
}
function getSunNakshatra(sunLon) {
// sunLon is Sidereal longitude
const index = getNakshatra(sunLon);
const pada = getNakshatraPada(sunLon);
return {
index,
name: constants_1.nakshatraNames[index],
pada
};
}
/**
* Find the next Sankranti (Sun's ingress into a Rashi) from a given date.
* Sankranti marks the Sun entering a new sidereal zodiac sign.
*
* @param date - Starting date to search from
* @param ayanamsa - Ayanamsa value for sidereal calculation
* @returns SankrantiInfo with exact time, rashi, and punya kalam
*/
function findNextSankranti(date, ayanamsa) {
// Get current sidereal Sun position
const sunVector = (0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, date, true);
const sunTrop = (0, astronomy_engine_1.Ecliptic)(sunVector).elon;
const sunSidereal = (sunTrop - ayanamsa + 360) % 360;
const currentRashi = Math.floor(sunSidereal / 30);
// Next Rashi boundary
const nextRashi = (currentRashi + 1) % 12;
const targetLongitude = nextRashi * 30;
// Binary search for exact moment Sun crosses the boundary
// Sun moves ~1 degree per day, so Rashi transit can be up to 30+ days away
const sankrantiFunc = (d) => {
const sv = (0, astronomy_engine_1.GeoVector)(astronomy_engine_1.Body.Sun, d, true);
const st = (0, astronomy_engine_1.Ecliptic)(sv).elon;
let sidereal = (st - ayanamsa + 360) % 360;
// Handle wrap-around at 360/0
let diff = sidereal - targetLongitude;
if (diff > 180)
diff -= 360;
if (diff < -180)
diff += 360;
return diff;
};
// Extended search: 40 days (Sun takes ~30 days per Rashi)
let lo = date.getTime();
let hi = lo + SANKRANTI_SEARCH_WINDOW_MS;
let fLo = sankrantiFunc(new Date(lo));
let fHi = sankrantiFunc(new Date(hi));
// If no sign change, Sankranti not in this window
if (fLo * fHi >= 0) {
return null;
}
// Binary search for zero crossing
for (let i = 0; i < 50; i++) {
const mid = (lo + hi) / 2;
const fMid = sankrantiFunc(new Date(mid));
if (Math.abs(fMid) < 0.00001) {
break;
}
if (fLo * fMid < 0) {
hi = mid;
fHi = fMid;
}
else {
lo = mid;
fLo = fMid;
}
}
const exactTime = new Date((lo + hi) / 2);
// Calculate Punya Kalam (auspicious window around Sankranti)
// Typically 16 Ghatis (6h 24min) before and after for most Sankrantis
// Makar Sankranti has special 40 Ghati (16h) punya kalam
const punyaDurationMs = nextRashi === 9
? 16 * MS_PER_HOUR // 16 hours for Makar Sankranti
: 6.4 * MS_PER_HOUR; // 6h 24m for others
const punyaKalam = {
start: new Date(exactTime.getTime() - punyaDurationMs),
end: new Date(exactTime.getTime() + punyaDurationMs)
};
return {
rashi: nextRashi,
rashiName: constants_1.rashiNames[nextRashi],
name: constants_1.sankrantiNames[nextRashi],
exactTime,
punyaKalam
};
}
/**
* Find all Sankrantis within a date range.
*
* @param startDate - Start of da