UNPKG

@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
"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