@bidyashish/panchang
Version:
Complete Hindu Panchanga calculator with Tithi, Nakshatra, Yoga, Karana, Vara calculations and Swiss Ephemeris precision
4 lines • 89.2 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/calculations/ephemeris.ts", "../src/utils/index.ts", "../src/calculations/planetary.ts", "../src/panchanga/index.ts", "../src/index.ts"],
"sourcesContent": ["import * as swisseph from 'swisseph';\nimport { Position, EclipticCoordinates, Location } from '../types/astronomical';\nimport { normalizeAngle } from '../utils/index';\nimport { PlanetaryPosition, Planetary } from './planetary';\n\nexport interface AyanamsaInfo {\n name: string;\n id: number;\n degree: number;\n description: string;\n}\n\nexport interface CelestialPosition extends Position {\n distance: number;\n longitudeSpeed: number;\n latitudeSpeed: number;\n distanceSpeed: number;\n}\n\nexport class Ephemeris {\n private readonly planetMap: { [key: string]: number } = {\n 'Sun': 0, // SE_SUN\n 'Moon': 1, // SE_MOON\n 'Mercury': 2, // SE_MERCURY\n 'Venus': 3, // SE_VENUS\n 'Mars': 4, // SE_MARS\n 'Jupiter': 5, // SE_JUPITER\n 'Saturn': 6, // SE_SATURN\n 'Uranus': 7, // SE_URANUS\n 'Neptune': 8, // SE_NEPTUNE\n 'Pluto': 9, // SE_PLUTO\n 'Rahu': 11, // SE_MEAN_NODE (North Node)\n 'Ketu': -1 // Special handling for South Node (180\u00B0 from Rahu)\n };\n\n private ephemerisPath: string = '';\n\n constructor(ephemerisPath?: string) {\n this.ephemerisPath = ephemerisPath || '/usr/share/libswe/ephe';\n this.initializeSwissEph();\n }\n\n private initializeSwissEph(): void {\n try {\n swisseph.swe_set_ephe_path(this.ephemerisPath);\n console.log(`Swiss Ephemeris initialized with path: ${this.ephemerisPath}`);\n } catch (error) {\n console.warn('Could not set ephemeris path, using default built-in data');\n }\n }\n\n calculatePosition(date: Date, body: string): Position {\n const jd = this.dateToJulian(date);\n const planetId = this.getPlanetId(body);\n \n try {\n let result: any;\n \n if (body === 'Ketu') {\n // Ketu is 180\u00B0 opposite to Rahu\n result = swisseph.swe_calc_ut(jd, 11, 0); // Calculate Rahu first\n const ketuLongitude = normalizeAngle((result.longitude || result[0] || 0) + 180);\n return {\n longitude: ketuLongitude,\n latitude: -(result.latitude || result[1] || 0) // Opposite latitude\n };\n } else {\n result = swisseph.swe_calc_ut(jd, planetId, 0);\n }\n \n return {\n longitude: normalizeAngle(result.longitude || result[0] || 0),\n latitude: result.latitude || result[1] || 0\n };\n } catch (error) {\n console.warn(`Swiss Ephemeris calculation failed for ${body}, using fallback`);\n return this.getFallbackPosition(body, date);\n }\n }\n\n calculateSiderealPosition(date: Date, body: string, ayanamsa?: number): Position {\n const currentAyanamsa = ayanamsa || this.calculateLahiriAyanamsa(date);\n const tropicalPosition = this.calculatePosition(date, body);\n \n return {\n longitude: normalizeAngle(tropicalPosition.longitude - currentAyanamsa),\n latitude: tropicalPosition.latitude\n };\n }\n\n calculateLahiriAyanamsa(date: Date): number {\n try {\n const jd = this.dateToJulian(date);\n // Set Lahiri ayanamsa (SE_SIDM_LAHIRI = 1)\n swisseph.swe_set_sid_mode(1, 0, 0);\n const ayanamsa = swisseph.swe_get_ayanamsa_ut(jd);\n return ayanamsa || this.getFallbackLahiriAyanamsa(date);\n } catch (error) {\n return this.getFallbackLahiriAyanamsa(date);\n }\n }\n\n private getFallbackLahiriAyanamsa(date: Date): number {\n // Accurate Lahiri ayanamsa formula based on Chitrapaksha ayanamsa\n // Reference: N.C. Lahiri's formula with modern corrections\n const year = date.getFullYear();\n const month = date.getMonth() + 1;\n const day = date.getDate();\n \n // Convert to decimal year\n const decimalYear = year + (month - 1) / 12 + (day - 1) / 365.25;\n \n // Lahiri ayanamsa formula (more accurate)\n // Base value at 1900.0 + rate of change\n const t = (decimalYear - 1900.0) / 100.0;\n \n // Improved Lahiri formula with higher-order terms\n const ayanamsa = 22.460 + 1.3915817 * t - 0.0130125 * t * t - 0.0000333 * t * t * t;\n \n return ayanamsa;\n }\n\n /**\n * Get all available ayanamsa systems with their degrees for a given date\n * @param date Date for ayanamsa calculation\n * @returns Array of ayanamsa information including name, ID, degree, and description\n */\n getAyanamsa(date: Date): AyanamsaInfo[] {\n const jd = this.dateToJulian(date);\n \n // Swiss Ephemeris Ayanamsa Systems (SE_SIDM constants)\n const ayanamsaSystems = [\n { id: 0, name: 'Fagan/Bradley', description: 'Fagan/Bradley (Western Sidereal)' },\n { id: 1, name: 'Lahiri', description: 'Lahiri (Chitrapaksha) - Official Indian Government' },\n { id: 2, name: 'De Luce', description: 'De Luce ayanamsa' },\n { id: 3, name: 'Raman', description: 'B.V. Raman ayanamsa' },\n { id: 4, name: 'Ushashashi', description: 'Ushashashi ayanamsa' },\n { id: 5, name: 'Krishnamurti', description: 'Krishnamurti ayanamsa (KP System)' },\n { id: 6, name: 'Djwhal Khul', description: 'Djwhal Khul ayanamsa' },\n { id: 7, name: 'Yukteshwar', description: 'Sri Yukteshwar ayanamsa' },\n { id: 8, name: 'J.N. Bhasin', description: 'J.N. Bhasin ayanamsa' },\n { id: 9, name: 'Babylonian (Kugler 1)', description: 'Babylonian ayanamsa (Kugler 1)' },\n { id: 10, name: 'Babylonian (Kugler 2)', description: 'Babylonian ayanamsa (Kugler 2)' },\n { id: 11, name: 'Babylonian (Kugler 3)', description: 'Babylonian ayanamsa (Kugler 3)' },\n { id: 12, name: 'Babylonian (Huber)', description: 'Babylonian ayanamsa (Huber)' },\n { id: 13, name: 'Eta Piscium', description: 'Eta Piscium ayanamsa' },\n { id: 14, name: 'Aldebaran 15 Tau', description: 'Aldebaran at 15\u00B0 Taurus' },\n { id: 15, name: 'Hipparchos', description: 'Hipparchos ayanamsa' },\n { id: 16, name: 'Sassanian', description: 'Sassanian ayanamsa' },\n { id: 17, name: 'Galact. Center (Brand)', description: 'Galactic Center ayanamsa (Brand)' },\n { id: 18, name: 'J2000', description: 'J2000.0 reference frame' },\n { id: 19, name: 'J1900', description: 'J1900.0 reference frame' },\n { id: 20, name: 'B1950', description: 'B1950.0 reference frame' },\n { id: 21, name: 'Suryasiddhanta', description: 'Suryasiddhanta ayanamsa' },\n { id: 22, name: 'Suryasiddhanta (mean Sun)', description: 'Suryasiddhanta (mean Sun)' },\n { id: 23, name: 'Aryabhata', description: 'Aryabhata ayanamsa' },\n { id: 24, name: 'Aryabhata 522', description: 'Aryabhata 522 CE ayanamsa' },\n { id: 25, name: 'Babylonian (Britton)', description: 'Babylonian ayanamsa (Britton)' },\n { id: 26, name: 'True Chitra', description: 'True Chitra ayanamsa' },\n { id: 27, name: 'True Revati', description: 'True Revati ayanamsa' },\n { id: 28, name: 'True Pushya', description: 'True Pushya ayanamsa' },\n { id: 29, name: 'Galactic (Gil Brand)', description: 'Galactic Center (Gil Brand)' },\n { id: 30, name: 'Galactic Equator (IAU1958)', description: 'Galactic Equator (IAU1958)' },\n { id: 31, name: 'Galactic Equator', description: 'Galactic Equator' },\n { id: 32, name: 'Galactic Equator (mid-Mula)', description: 'Galactic Equator at mid-Mula' },\n { id: 33, name: 'Skydram (Mardyks)', description: 'Skydram ayanamsa (Mardyks)' },\n { id: 34, name: 'True Mula', description: 'True Mula ayanamsa' },\n { id: 35, name: 'Dhruva Galactic Center', description: 'Dhruva Galactic Center ayanamsa' },\n { id: 36, name: 'Aryabhata Mean Sun', description: 'Aryabhata Mean Sun ayanamsa' },\n { id: 37, name: 'Lahiri VP285', description: 'Lahiri VP285 ayanamsa' },\n { id: 38, name: 'Krishnamurti VP291', description: 'Krishnamurti VP291 ayanamsa' },\n { id: 39, name: 'Lahiri ICRC', description: 'Lahiri ICRC ayanamsa' }\n ];\n\n const results: AyanamsaInfo[] = [];\n\n ayanamsaSystems.forEach(system => {\n try {\n // Set the ayanamsa mode\n swisseph.swe_set_sid_mode(system.id, 0, 0);\n \n // Get ayanamsa value for the given date\n const ayanamsaValue = swisseph.swe_get_ayanamsa_ut(jd);\n \n results.push({\n name: system.name,\n id: system.id,\n degree: ayanamsaValue || this.getFallbackAyanamsa(system.id, date),\n description: system.description\n });\n } catch (error) {\n // If Swiss Ephemeris fails, use fallback calculation\n results.push({\n name: system.name,\n id: system.id,\n degree: this.getFallbackAyanamsa(system.id, date),\n description: system.description\n });\n }\n });\n\n // Sort by degree value for easier comparison\n results.sort((a, b) => a.degree - b.degree);\n \n return results;\n }\n\n /**\n * Get a specific ayanamsa value by name or ID\n * @param date Date for calculation\n * @param ayanamsaId Ayanamsa ID or name\n * @returns Ayanamsa information\n */\n getSpecificAyanamsa(date: Date, ayanamsaId: number | string): AyanamsaInfo | null {\n const allAyanamsas = this.getAyanamsa(date);\n \n if (typeof ayanamsaId === 'number') {\n return allAyanamsas.find(a => a.id === ayanamsaId) || null;\n } else {\n // First try exact match\n const exactMatch = allAyanamsas.find(a => \n a.name.toLowerCase() === ayanamsaId.toLowerCase()\n );\n \n if (exactMatch) {\n return exactMatch;\n }\n \n // Then try partial match\n return allAyanamsas.find(a => \n a.name.toLowerCase().includes(ayanamsaId.toLowerCase())\n ) || null;\n }\n }\n\n private getFallbackAyanamsa(systemId: number, date: Date): number {\n const year = date.getFullYear();\n const t = (year - 1900) / 100;\n \n // Approximate calculations for different ayanamsa systems\n switch (systemId) {\n case 0: // Fagan/Bradley\n return 24.740 + 1.39 * t - 0.01 * t * t;\n case 1: // Lahiri\n return 22.460 + 1.39 * t - 0.01 * t * t;\n case 3: // Raman\n return 21.580 + 1.39 * t - 0.01 * t * t;\n case 5: // Krishnamurti\n return 23.230 + 1.39 * t - 0.01 * t * t;\n case 7: // Yukteshwar\n return 22.460 + 1.39 * t - 0.01 * t * t;\n default:\n // Default to Lahiri approximation\n return 22.460 + 1.39 * t - 0.01 * t * t;\n }\n }\n\n calculateSunrise(date: Date, location: Location): Date | null {\n try {\n const jd = this.dateToJulian(date);\n \n // Try Swiss Ephemeris first\n try {\n // Use Swiss Ephemeris for accurate sunrise calculation\n // Note: Swiss Ephemeris API varies by version, using fallback for now\n return this.calculateSunriseAccurate(date, location);\n } catch (ephError) {\n console.log('Swiss Ephemeris sunrise calculation failed, using fallback');\n }\n \n // Fallback calculation with better accuracy\n return this.calculateSunriseAccurate(date, location);\n \n } catch (error) {\n console.warn('Sunrise calculation failed:', error);\n return this.calculateSunriseSimple(date, location);\n }\n }\n \n private calculateSunriseAccurate(date: Date, location: Location): Date {\n const jd = this.dateToJulian(date);\n const lat = location.latitude * Math.PI / 180;\n const lng = location.longitude * Math.PI / 180;\n \n // More accurate solar position calculation\n const n = jd - 2451545.0;\n const L = (280.460 + 0.9856474 * n) * Math.PI / 180;\n const g = (357.528 + 0.9856003 * n) * Math.PI / 180;\n const lambda = L + (1.915 * Math.sin(g) + 0.020 * Math.sin(2 * g)) * Math.PI / 180;\n \n const alpha = Math.atan2(Math.cos(23.439 * Math.PI / 180) * Math.sin(lambda), Math.cos(lambda));\n const delta = Math.asin(Math.sin(23.439 * Math.PI / 180) * Math.sin(lambda));\n \n // Atmospheric refraction correction (approximately -50 arcminutes)\n const h0 = -0.8333 * Math.PI / 180;\n \n const cosH = (Math.sin(h0) - Math.sin(lat) * Math.sin(delta)) / (Math.cos(lat) * Math.cos(delta));\n \n if (cosH > 1) {\n // No sunrise (polar night)\n const sunrise = new Date(date);\n sunrise.setHours(6, 0, 0, 0);\n return sunrise;\n }\n \n if (cosH < -1) {\n // No sunset (midnight sun)\n const sunrise = new Date(date);\n sunrise.setHours(0, 0, 0, 0);\n return sunrise;\n }\n \n const H = Math.acos(cosH);\n const t = (alpha - lng - H) / (2 * Math.PI);\n \n // Convert to actual time\n const utcTime = (jd + t - Math.floor(jd + t)) * 24;\n const sunrise = new Date(date);\n const hours = Math.floor(utcTime);\n const minutes = Math.floor((utcTime - hours) * 60);\n const seconds = Math.floor(((utcTime - hours) * 60 - minutes) * 60);\n \n sunrise.setUTCHours(hours, minutes, seconds, 0);\n return sunrise;\n }\n \n private calculateSunriseSimple(date: Date, location: Location): Date {\n const sunrise = new Date(date);\n sunrise.setHours(6, 0, 0, 0);\n return sunrise;\n }\n\n calculateSunset(date: Date, location: Location): Date | null {\n try {\n const jd = this.dateToJulian(date);\n \n // Try Swiss Ephemeris first\n try {\n // Use Swiss Ephemeris for accurate sunset calculation\n // Note: Swiss Ephemeris API varies by version, using fallback for now\n return this.calculateSunsetAccurate(date, location);\n } catch (ephError) {\n console.log('Swiss Ephemeris sunset calculation failed, using fallback');\n }\n \n // Fallback calculation with better accuracy\n return this.calculateSunsetAccurate(date, location);\n \n } catch (error) {\n console.warn('Sunset calculation failed:', error);\n return this.calculateSunsetSimple(date, location);\n }\n }\n \n private calculateSunsetAccurate(date: Date, location: Location): Date {\n const jd = this.dateToJulian(date);\n const lat = location.latitude * Math.PI / 180;\n const lng = location.longitude * Math.PI / 180;\n \n // More accurate solar position calculation\n const n = jd - 2451545.0;\n const L = (280.460 + 0.9856474 * n) * Math.PI / 180;\n const g = (357.528 + 0.9856003 * n) * Math.PI / 180;\n const lambda = L + (1.915 * Math.sin(g) + 0.020 * Math.sin(2 * g)) * Math.PI / 180;\n \n const alpha = Math.atan2(Math.cos(23.439 * Math.PI / 180) * Math.sin(lambda), Math.cos(lambda));\n const delta = Math.asin(Math.sin(23.439 * Math.PI / 180) * Math.sin(lambda));\n \n // Atmospheric refraction correction (approximately -50 arcminutes)\n const h0 = -0.8333 * Math.PI / 180;\n \n const cosH = (Math.sin(h0) - Math.sin(lat) * Math.sin(delta)) / (Math.cos(lat) * Math.cos(delta));\n \n if (cosH > 1) {\n // No sunset (polar night)\n const sunset = new Date(date);\n sunset.setHours(18, 0, 0, 0);\n return sunset;\n }\n \n if (cosH < -1) {\n // No sunset (midnight sun)\n const sunset = new Date(date);\n sunset.setHours(23, 59, 59, 999);\n return sunset;\n }\n \n const H = Math.acos(cosH);\n const t = (alpha - lng + H) / (2 * Math.PI);\n \n // Convert to actual time\n const utcTime = (jd + t - Math.floor(jd + t)) * 24;\n const sunset = new Date(date);\n const hours = Math.floor(utcTime);\n const minutes = Math.floor((utcTime - hours) * 60);\n const seconds = Math.floor(((utcTime - hours) * 60 - minutes) * 60);\n \n sunset.setUTCHours(hours, minutes, seconds, 0);\n return sunset;\n }\n \n private calculateSunsetSimple(date: Date, location: Location): Date {\n const sunset = new Date(date);\n sunset.setHours(18, 0, 0, 0);\n return sunset;\n }\n\n calculateNakshatra(longitude: number): { nakshatra: number; pada: number; name: string } {\n const nakshatraNames = [\n 'Ashwini', 'Bharani', 'Krittika', 'Rohini', 'Mrigashira', 'Ardra',\n 'Punarvasu', 'Pushya', 'Ashlesha', 'Magha', 'Purva Phalguni', 'Uttara Phalguni',\n 'Hasta', 'Chitra', 'Swati', 'Vishakha', 'Anuradha', 'Jyeshtha',\n 'Mula', 'Purva Ashadha', 'Uttara Ashadha', 'Shravana', 'Dhanishta', 'Shatabhisha',\n 'Purva Bhadrapada', 'Uttara Bhadrapada', 'Revati'\n ];\n\n const oneNakshatra = 360 / 27; // 13\u00B020'\n const onePada = oneNakshatra / 4; // 3\u00B020'\n \n const normalizedLon = normalizeAngle(longitude);\n const nakshatraNum = Math.floor(normalizedLon / oneNakshatra) + 1;\n const remainder = normalizedLon % oneNakshatra;\n const padaNum = Math.floor(remainder / onePada) + 1;\n \n return {\n nakshatra: nakshatraNum,\n pada: padaNum,\n name: nakshatraNames[nakshatraNum - 1] || 'Unknown'\n };\n }\n\n private dateToJulian(date: Date): number {\n const year = date.getUTCFullYear();\n const month = date.getUTCMonth() + 1;\n const day = date.getUTCDate();\n const hour = date.getUTCHours() + \n date.getUTCMinutes() / 60 + \n date.getUTCSeconds() / 3600;\n\n try {\n return swisseph.swe_julday(year, month, day, hour, 1); // SE_GREG_CAL = 1\n } catch (error) {\n // Fallback Julian Day calculation\n return date.getTime() / 86400000 + 2440587.5;\n }\n }\n\n private julianToDate(jd: number): Date {\n try {\n const result = swisseph.swe_revjul(jd, 1); // SE_GREG_CAL = 1\n return new Date(result.year, result.month - 1, result.day, \n Math.floor(result.hour), \n Math.floor((result.hour % 1) * 60));\n } catch (error) {\n // Fallback conversion\n return new Date((jd - 2440587.5) * 86400000);\n }\n }\n\n private getPlanetId(body: string): number {\n return this.planetMap[body] !== undefined ? this.planetMap[body] : 0; // Default to Sun\n }\n\n private getFallbackPosition(body: string, date: Date): Position {\n const dayOfYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000);\n const year = date.getFullYear();\n const centuriesFromJ2000 = (year - 2000) / 100;\n \n const basePositions: { [key: string]: { dailyMotion: number; epoch2000: number; lat: number } } = {\n 'Sun': { dailyMotion: 0.985647, epoch2000: 280.460, lat: 0 },\n 'Moon': { dailyMotion: 13.176396, epoch2000: 218.316, lat: 5.145 },\n 'Mercury': { dailyMotion: 4.092317, epoch2000: 252.251, lat: 3.395 },\n 'Venus': { dailyMotion: 1.602136, epoch2000: 181.980, lat: 3.395 },\n 'Mars': { dailyMotion: 0.524071, epoch2000: 355.433, lat: 1.850 },\n 'Jupiter': { dailyMotion: 0.083056, epoch2000: 34.351, lat: 1.304 },\n 'Saturn': { dailyMotion: 0.033371, epoch2000: 50.078, lat: 2.489 }\n };\n\n const bodyData = basePositions[body] || basePositions['Sun'];\n const longitude = normalizeAngle(\n bodyData.epoch2000 + \n bodyData.dailyMotion * dayOfYear + \n centuriesFromJ2000 * 1.7\n );\n \n const latitude = Math.sin(dayOfYear * 0.1) * bodyData.lat;\n\n return { longitude, latitude };\n }\n\n getCurrentPlanets(date: Date = new Date(), ayanamsaId: number = 1): PlanetaryPosition[] {\n const planetary = new Planetary();\n const planets = ['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn'];\n const positions: PlanetaryPosition[] = [];\n\n // Get ayanamsa value for the date\n const ayanamsaInfo = this.getSpecificAyanamsa(date, ayanamsaId);\n const ayanamsa = ayanamsaInfo ? ayanamsaInfo.degree : 24.0; // Default to approximate Lahiri\n\n for (const planet of planets) {\n try {\n const position = this.calculatePosition(date, planet);\n \n // Convert to sidereal longitude by subtracting ayanamsa\n const siderealLongitude = normalizeAngle(position.longitude - ayanamsa);\n \n // Calculate Rashi and Nakshatra\n const rashi = planetary.calculateRashi(siderealLongitude);\n const nakshatra = planetary.calculateNakshatra(siderealLongitude);\n\n positions.push({\n planet: planet,\n longitude: siderealLongitude,\n latitude: position.latitude,\n rashi: rashi,\n nakshatra: nakshatra\n });\n } catch (error) {\n console.warn(`Could not calculate position for ${planet}:`, error);\n // Add with fallback data\n const fallbackPos = this.getFallbackPosition(planet, date);\n const siderealLongitude = normalizeAngle(fallbackPos.longitude - ayanamsa);\n \n positions.push({\n planet: planet,\n longitude: siderealLongitude,\n latitude: fallbackPos.latitude,\n rashi: planetary.calculateRashi(siderealLongitude),\n nakshatra: planetary.calculateNakshatra(siderealLongitude)\n });\n }\n }\n\n return positions;\n }\n\n cleanup(): void {\n try {\n swisseph.swe_close();\n } catch (error) {\n console.warn('Error closing Swiss Ephemeris:', error);\n }\n }\n}", "/**\n * Utility functions for astronomical calculations\n */\n\nexport function formatDate(date: Date): string {\n return date.toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit'\n });\n}\n\nexport function degreesToRadians(degrees: number): number {\n return degrees * (Math.PI / 180);\n}\n\nexport function radiansToDegrees(radians: number): number {\n return radians * (180 / Math.PI);\n}\n\nexport function normalizeAngle(angle: number): number {\n let normalized = angle % 360;\n if (normalized < 0) {\n normalized += 360;\n }\n return normalized;\n}\n\nexport function toDMS(degrees: number): { degrees: number; minutes: number; seconds: number } {\n const absValue = Math.abs(degrees);\n const d = Math.floor(absValue);\n const mins = (absValue - d) * 60;\n const m = Math.floor(mins);\n const s = Math.round((mins - m) * 60 * 1000) / 1000;\n \n return { \n degrees: degrees >= 0 ? d : -d, \n minutes: m, \n seconds: s \n };\n}\n\nexport function julianDayToDate(jd: number): Date {\n const a = jd + 32044;\n const b = (4 * a + 3) / 146097;\n const c = a - (146097 * b) / 4;\n const d = (4 * c + 3) / 1461;\n const e = c - (1461 * d) / 4;\n const m = (5 * e + 2) / 153;\n \n const day = e - (153 * m + 2) / 5 + 1;\n const month = m + 3 - 12 * (m / 10);\n const year = 100 * b + d - 4800 + m / 10;\n \n return new Date(year, month - 1, day);\n}", "import { OrbitalElements } from '../types/astronomical';\nimport { normalizeAngle } from '../utils/index';\n\nexport interface OrbitalParameters {\n perihelion: number;\n aphelion: number;\n eccentricity: number;\n semiMajorAxis: number;\n orbitalPeriod: number;\n inclination: number;\n longitudeOfAscendingNode: number;\n argumentOfPeriapsis: number;\n}\n\nexport interface TithiInfo {\n tithi: number;\n name: string;\n percentage: number;\n isWaxing: boolean;\n}\n\nexport interface RashiInfo {\n rashi: number;\n name: string;\n element: string;\n ruler: string;\n degree: number; // Position within the rashi (0-30\u00B0)\n}\n\nexport interface NakshatraInfo {\n nakshatra: number;\n name: string;\n pada: number;\n ruler: string;\n deity: string;\n symbol: string;\n degree: number; // Position within nakshatra\n}\n\nexport interface PlanetaryPosition {\n planet: string;\n longitude: number; // Sidereal longitude\n latitude: number;\n rashi: RashiInfo;\n nakshatra: NakshatraInfo;\n}\n\nexport const RASHIS = [\n { name: \"Mesha\", element: \"Fire\", ruler: \"Mars\" },\n { name: \"Vrishabha\", element: \"Earth\", ruler: \"Venus\" },\n { name: \"Mithuna\", element: \"Air\", ruler: \"Mercury\" },\n { name: \"Karka\", element: \"Water\", ruler: \"Moon\" },\n { name: \"Simha\", element: \"Fire\", ruler: \"Sun\" },\n { name: \"Kanya\", element: \"Earth\", ruler: \"Mercury\" },\n { name: \"Tula\", element: \"Air\", ruler: \"Venus\" },\n { name: \"Vrishchika\", element: \"Water\", ruler: \"Mars\" },\n { name: \"Dhanu\", element: \"Fire\", ruler: \"Jupiter\" },\n { name: \"Makara\", element: \"Earth\", ruler: \"Saturn\" },\n { name: \"Kumbha\", element: \"Air\", ruler: \"Saturn\" },\n { name: \"Meena\", element: \"Water\", ruler: \"Jupiter\" }\n];\n\nexport const NAKSHATRAS = [\n { name: \"Ashwini\", ruler: \"Ketu\", deity: \"Ashwini Kumaras\", symbol: \"Horse's head\" },\n { name: \"Bharani\", ruler: \"Venus\", deity: \"Yama\", symbol: \"Yoni\" },\n { name: \"Krittika\", ruler: \"Sun\", deity: \"Agni\", symbol: \"Razor/flame\" },\n { name: \"Rohini\", ruler: \"Moon\", deity: \"Brahma\", symbol: \"Cart/chariot\" },\n { name: \"Mrigashira\", ruler: \"Mars\", deity: \"Soma\", symbol: \"Deer's head\" },\n { name: \"Ardra\", ruler: \"Rahu\", deity: \"Rudra\", symbol: \"Teardrop\" },\n { name: \"Punarvasu\", ruler: \"Jupiter\", deity: \"Aditi\", symbol: \"Quiver of arrows\" },\n { name: \"Pushya\", ruler: \"Saturn\", deity: \"Brihaspati\", symbol: \"Cow's udder\" },\n { name: \"Ashlesha\", ruler: \"Mercury\", deity: \"Nagas\", symbol: \"Serpent\" },\n { name: \"Magha\", ruler: \"Ketu\", deity: \"Pitrs\", symbol: \"Throne\" },\n { name: \"Purva Phalguni\", ruler: \"Venus\", deity: \"Bhaga\", symbol: \"Hammock\" },\n { name: \"Uttara Phalguni\", ruler: \"Sun\", deity: \"Aryaman\", symbol: \"Bed\" },\n { name: \"Hasta\", ruler: \"Moon\", deity: \"Savitar\", symbol: \"Hand\" },\n { name: \"Chitra\", ruler: \"Mars\", deity: \"Vishvakarma\", symbol: \"Pearl\" },\n { name: \"Swati\", ruler: \"Rahu\", deity: \"Vayu\", symbol: \"Sword\" },\n { name: \"Vishakha\", ruler: \"Jupiter\", deity: \"Indragni\", symbol: \"Triumphal arch\" },\n { name: \"Anuradha\", ruler: \"Saturn\", deity: \"Mitra\", symbol: \"Lotus\" },\n { name: \"Jyeshtha\", ruler: \"Mercury\", deity: \"Indra\", symbol: \"Earring\" },\n { name: \"Mula\", ruler: \"Ketu\", deity: \"Nirriti\", symbol: \"Bunch of roots\" },\n { name: \"Purva Ashadha\", ruler: \"Venus\", deity: \"Apah\", symbol: \"Fan\" },\n { name: \"Uttara Ashadha\", ruler: \"Sun\", deity: \"Vishvedevas\", symbol: \"Elephant tusk\" },\n { name: \"Shravana\", ruler: \"Moon\", deity: \"Vishnu\", symbol: \"Ear\" },\n { name: \"Dhanishtha\", ruler: \"Mars\", deity: \"Vasus\", symbol: \"Drum\" },\n { name: \"Shatabhisha\", ruler: \"Rahu\", deity: \"Varuna\", symbol: \"Circle\" },\n { name: \"Purva Bhadrapada\", ruler: \"Jupiter\", deity: \"Ajaikapat\", symbol: \"Front legs of bed\" },\n { name: \"Uttara Bhadrapada\", ruler: \"Saturn\", deity: \"Ahirbudhnya\", symbol: \"Back legs of bed\" },\n { name: \"Revati\", ruler: \"Mercury\", deity: \"Pushan\", symbol: \"Fish\" }\n];\n\nexport class Planetary {\n private readonly orbitalData: { [key: string]: OrbitalParameters } = {\n 'Mercury': {\n perihelion: 0.307,\n aphelion: 0.467,\n eccentricity: 0.2056,\n semiMajorAxis: 0.387,\n orbitalPeriod: 87.97,\n inclination: 7.005,\n longitudeOfAscendingNode: 48.331,\n argumentOfPeriapsis: 29.124\n },\n 'Venus': {\n perihelion: 0.718,\n aphelion: 0.728,\n eccentricity: 0.0067,\n semiMajorAxis: 0.723,\n orbitalPeriod: 224.70,\n inclination: 3.395,\n longitudeOfAscendingNode: 76.680,\n argumentOfPeriapsis: 54.884\n },\n 'Earth': {\n perihelion: 0.983,\n aphelion: 1.017,\n eccentricity: 0.0167,\n semiMajorAxis: 1.000,\n orbitalPeriod: 365.26,\n inclination: 0.000,\n longitudeOfAscendingNode: 0.000,\n argumentOfPeriapsis: 114.208\n },\n 'Mars': {\n perihelion: 1.381,\n aphelion: 1.666,\n eccentricity: 0.0935,\n semiMajorAxis: 1.524,\n orbitalPeriod: 686.98,\n inclination: 1.850,\n longitudeOfAscendingNode: 49.558,\n argumentOfPeriapsis: 286.502\n },\n 'Jupiter': {\n perihelion: 4.950,\n aphelion: 5.455,\n eccentricity: 0.0489,\n semiMajorAxis: 5.203,\n orbitalPeriod: 4332.59,\n inclination: 1.304,\n longitudeOfAscendingNode: 100.464,\n argumentOfPeriapsis: 273.867\n },\n 'Saturn': {\n perihelion: 9.020,\n aphelion: 10.054,\n eccentricity: 0.0565,\n semiMajorAxis: 9.537,\n orbitalPeriod: 10759.22,\n inclination: 2.489,\n longitudeOfAscendingNode: 113.665,\n argumentOfPeriapsis: 339.392\n },\n 'Uranus': {\n perihelion: 18.324,\n aphelion: 20.110,\n eccentricity: 0.0457,\n semiMajorAxis: 19.217,\n orbitalPeriod: 30688.5,\n inclination: 0.773,\n longitudeOfAscendingNode: 74.006,\n argumentOfPeriapsis: 96.998\n },\n 'Neptune': {\n perihelion: 29.820,\n aphelion: 30.330,\n eccentricity: 0.0113,\n semiMajorAxis: 30.075,\n orbitalPeriod: 60182,\n inclination: 1.770,\n longitudeOfAscendingNode: 131.784,\n argumentOfPeriapsis: 276.336\n }\n };\n\n calculateOrbit(planet: string, date: Date): { perihelion: number; aphelion: number; eccentricity: number } {\n const orbitalParams = this.orbitalData[planet];\n \n if (!orbitalParams) {\n // Return default values for unknown planets\n return {\n perihelion: 1.0,\n aphelion: 1.0,\n eccentricity: 0.0\n };\n }\n\n // Apply small variations based on date for more realistic simulation\n const daysSinceEpoch = (date.getTime() - new Date('2000-01-01').getTime()) / (1000 * 60 * 60 * 24);\n const variation = Math.sin(daysSinceEpoch / 365.25) * 0.001;\n\n return {\n perihelion: orbitalParams.perihelion * (1 + variation),\n aphelion: orbitalParams.aphelion * (1 + variation),\n eccentricity: orbitalParams.eccentricity * (1 + variation * 0.1)\n };\n }\n\n calculateTithi(sunLongitude: number, moonLongitude: number): TithiInfo {\n const tithiNames = [\n 'Pratipada', 'Dwitiya', 'Tritiya', 'Chaturthi', 'Panchami', 'Shashthi',\n 'Saptami', 'Ashtami', 'Navami', 'Dashami', 'Ekadashi', 'Dwadashi',\n 'Trayodashi', 'Chaturdashi', 'Purnima/Amavasya'\n ];\n\n // Calculate elongation (longitude difference between Moon and Sun)\n // This follows the correct astronomical definition\n let elongation = normalizeAngle(moonLongitude - sunLongitude);\n \n // Each tithi spans 12 degrees (360\u00B0 / 30 tithis)\n const tithiLength = 12;\n \n // Calculate tithi number (1-30)\n const tithiNumber = Math.floor(elongation / tithiLength) + 1;\n \n // Calculate percentage completion of current tithi\n const remainder = elongation % tithiLength;\n const percentage = (remainder / tithiLength) * 100;\n \n // Determine paksha (fortnight) and adjust tithi\n let finalTithi: number;\n let isWaxing: boolean;\n let tithiName: string;\n \n if (tithiNumber <= 15) {\n // Shukla Paksha (Waxing Moon) - Tithis 1-15\n isWaxing = true;\n finalTithi = tithiNumber;\n if (finalTithi === 15) {\n tithiName = 'Purnima'; // Full Moon\n } else {\n tithiName = tithiNames[finalTithi - 1];\n }\n } else {\n // Krishna Paksha (Waning Moon) - Tithis 16-30, numbered as 1-15\n isWaxing = false;\n finalTithi = tithiNumber - 15;\n if (finalTithi === 15) {\n tithiName = 'Amavasya'; // New Moon\n } else {\n tithiName = tithiNames[finalTithi - 1];\n }\n }\n \n return {\n tithi: finalTithi,\n name: tithiName,\n percentage: percentage,\n isWaxing: isWaxing\n };\n }\n\n calculateYoga(sunLongitude: number, moonLongitude: number): { yoga: number; name: string } {\n const yogaNames = [\n 'Vishkumbha', 'Preeti', 'Ayushman', 'Saubhagya', 'Shobhana', 'Atiganda',\n 'Sukarman', 'Dhriti', 'Shoola', 'Ganda', 'Vriddhi', 'Dhruva',\n 'Vyaghata', 'Harshana', 'Vajra', 'Siddhi', 'Vyatipata', 'Variyan',\n 'Parigha', 'Shiva', 'Siddha', 'Sadhya', 'Shubha', 'Shukla',\n 'Brahma', 'Indra', 'Vaidhriti'\n ];\n\n // Yoga is the sum of Sun and Moon longitudes\n // Each yoga spans 13\u00B020' (360\u00B0 / 27 yogas = 13.333...\u00B0)\n const sum = normalizeAngle(sunLongitude + moonLongitude);\n const yogaArc = 360 / 27; // 13.333... degrees per yoga\n const yogaNumber = Math.floor(sum / yogaArc) + 1;\n \n // Ensure yoga number is within valid range (1-27)\n const validYogaNumber = Math.max(1, Math.min(27, yogaNumber));\n \n return {\n yoga: validYogaNumber,\n name: yogaNames[validYogaNumber - 1] || 'Unknown'\n };\n }\n\n calculateKarana(sunLongitude: number, moonLongitude: number): { karana: number; name: string } {\n const karanaNames = [\n 'Bava', 'Balava', 'Kaulava', 'Taitila', 'Gara', 'Vanija', 'Vishti',\n 'Shakuni', 'Chatushpada', 'Naga', 'Kimstughna'\n ];\n\n // Calculate elongation (longitude difference between Moon and Sun)\n const elongation = normalizeAngle(moonLongitude - sunLongitude);\n \n // Each karana spans 6 degrees (half a tithi)\n // There are 60 karanas in a lunar month (30 tithis \u00D7 2 karanas per tithi)\n const karanaArc = 6; // degrees\n const karanaNumber = Math.floor(elongation / karanaArc) + 1;\n \n // Handle the cyclic nature of karanas\n let karanaIndex: number;\n let finalKaranaNumber: number;\n \n if (karanaNumber <= 57) {\n // First 57 karanas: 7 movable karanas repeat 8 times, plus one more cycle starts\n karanaIndex = (karanaNumber - 1) % 7;\n finalKaranaNumber = karanaNumber;\n } else if (karanaNumber <= 60) {\n // Last 4 karanas (58-60, plus one special case): fixed karanas\n karanaIndex = 7 + (karanaNumber - 58);\n finalKaranaNumber = karanaNumber;\n } else {\n // Handle overflow (shouldn't happen, but safety check)\n karanaIndex = (karanaNumber - 1) % 7;\n finalKaranaNumber = ((karanaNumber - 1) % 60) + 1;\n }\n \n // Ensure we don't go beyond array bounds\n karanaIndex = Math.min(Math.max(0, karanaIndex), karanaNames.length - 1);\n \n return {\n karana: finalKaranaNumber,\n name: karanaNames[karanaIndex]\n };\n }\n\n getOrbitalPeriod(planet: string): number {\n const orbitalParams = this.orbitalData[planet];\n return orbitalParams ? orbitalParams.orbitalPeriod : 365.25;\n }\n\n getSemiMajorAxis(planet: string): number {\n const orbitalParams = this.orbitalData[planet];\n return orbitalParams ? orbitalParams.semiMajorAxis : 1.0;\n }\n\n getEccentricity(planet: string): number {\n const orbitalParams = this.orbitalData[planet];\n return orbitalParams ? orbitalParams.eccentricity : 0.0;\n }\n\n getInclination(planet: string): number {\n const orbitalParams = this.orbitalData[planet];\n return orbitalParams ? orbitalParams.inclination : 0.0;\n }\n\n calculateMeanAnomaly(planet: string, date: Date): number {\n const orbitalParams = this.orbitalData[planet];\n if (!orbitalParams) return 0;\n\n const daysSinceEpoch = (date.getTime() - new Date('2000-01-01').getTime()) / (1000 * 60 * 60 * 24);\n const meanMotion = 360 / orbitalParams.orbitalPeriod; // degrees per day\n \n return normalizeAngle(meanMotion * daysSinceEpoch);\n }\n\n calculateTrueAnomaly(planet: string, date: Date): number {\n const meanAnomaly = this.calculateMeanAnomaly(planet, date);\n const eccentricity = this.getEccentricity(planet);\n \n // Simplified calculation using first-order approximation\n // True Anomaly \u2248 Mean Anomaly + 2 * eccentricity * sin(Mean Anomaly)\n const meanAnomalyRad = meanAnomaly * Math.PI / 180;\n const trueAnomalyRad = meanAnomalyRad + 2 * eccentricity * Math.sin(meanAnomalyRad);\n \n return normalizeAngle(trueAnomalyRad * 180 / Math.PI);\n }\n\n calculateRashi(longitude: number): RashiInfo {\n // Each rashi is 30 degrees\n const rashiNumber = Math.floor(longitude / 30);\n const degreeInRashi = longitude % 30;\n \n const rashiData = RASHIS[rashiNumber];\n \n return {\n rashi: rashiNumber + 1,\n name: rashiData.name,\n element: rashiData.element,\n ruler: rashiData.ruler,\n degree: degreeInRashi\n };\n }\n\n calculateNakshatra(longitude: number): NakshatraInfo {\n // Each nakshatra is 13.333... degrees (360/27)\n const nakshatraSize = 360 / 27;\n const nakshatraNumber = Math.floor(longitude / nakshatraSize);\n const degreeInNakshatra = longitude % nakshatraSize;\n \n // Each nakshatra has 4 padas\n const pada = Math.floor(degreeInNakshatra / (nakshatraSize / 4)) + 1;\n \n const nakshatraData = NAKSHATRAS[nakshatraNumber];\n \n return {\n nakshatra: nakshatraNumber + 1,\n name: nakshatraData.name,\n pada: pada,\n ruler: nakshatraData.ruler,\n deity: nakshatraData.deity,\n symbol: nakshatraData.symbol,\n degree: degreeInNakshatra\n };\n }\n}", "/**\n * Panchanga calculations - Traditional Hindu calendar system\n * Based on the original panchanga.py implementation\n */\n\nimport { Ephemeris } from '../calculations/ephemeris';\nimport { Planetary, TithiInfo } from '../calculations/planetary';\nimport { Location } from '../types/astronomical';\nimport { normalizeAngle, formatDate } from '../utils/index';\n\nexport interface PanchangaData {\n date: Date;\n location?: { latitude: number; longitude: number; timezone: string; name?: string };\n tithi: TithiInfo;\n nakshatra: { nakshatra: number; pada: number; name: string };\n yoga: { yoga: number; name: string };\n karana: { karana: number; name: string };\n vara: { vara: number; name: string };\n sunrise: Date | null;\n sunset: Date | null;\n moonPhase: string;\n}\n\nexport class Panchanga {\n private ephemeris: Ephemeris;\n private planetary: Planetary;\n\n private varaNames = [\n 'Sunday', 'Monday', 'Tuesday', 'Wednesday', \n 'Thursday', 'Friday', 'Saturday'\n ];\n\n constructor() {\n this.ephemeris = new Ephemeris();\n this.planetary = new Planetary();\n }\n\n calculatePanchanga(date: Date, location: Location, useSidereal: boolean = true): PanchangaData {\n // Traditional Panchanga calculations should be done at sunrise\n // This follows the correct Vedic astronomical principles\n const sunrise = this.ephemeris.calculateSunrise(date, location);\n const calculationTime = sunrise || date; // Use sunrise if available, otherwise input date\n \n let sunPosition: any;\n let moonPosition: any;\n \n if (useSidereal) {\n // Calculate sidereal positions using Lahiri ayanamsa\n const ayanamsa = this.ephemeris.calculateLahiriAyanamsa(calculationTime);\n \n // Get tropical positions first\n const sunTropical = this.ephemeris.calculatePosition(calculationTime, 'Sun');\n const moonTropical = this.ephemeris.calculatePosition(calculationTime, 'Moon');\n \n // Convert to sidereal by subtracting ayanamsa\n sunPosition = {\n longitude: this.normalizeAngle(sunTropical.longitude - ayanamsa),\n latitude: sunTropical.latitude\n };\n \n moonPosition = {\n longitude: this.normalizeAngle(moonTropical.longitude - ayanamsa),\n latitude: moonTropical.latitude\n };\n } else {\n // Use tropical positions\n sunPosition = this.ephemeris.calculatePosition(calculationTime, 'Sun');\n moonPosition = this.ephemeris.calculatePosition(calculationTime, 'Moon');\n }\n\n // Calculate Panchanga elements using corrected formulas\n const tithi = this.planetary.calculateTithi(sunPosition.longitude, moonPosition.longitude);\n const nakshatra = this.ephemeris.calculateNakshatra(moonPosition.longitude);\n const yoga = this.planetary.calculateYoga(sunPosition.longitude, moonPosition.longitude);\n const karana = this.planetary.calculateKarana(sunPosition.longitude, moonPosition.longitude);\n const vara = this.getVara(date); // Vara is based on the civil calendar date\n\n // Calculate sunrise and sunset for the day\n const sunriseTime = this.ephemeris.calculateSunrise(date, location);\n const sunsetTime = this.ephemeris.calculateSunset(date, location);\n\n // Determine moon phase based on positions\n const moonPhase = this.getMoonPhase(sunPosition.longitude, moonPosition.longitude);\n\n return {\n date,\n location: { \n latitude: location.latitude, \n longitude: location.longitude, \n timezone: location.timezone || 'UTC',\n name: location.name\n },\n tithi,\n nakshatra,\n yoga,\n karana,\n vara,\n sunrise: sunriseTime,\n sunset: sunsetTime,\n moonPhase\n };\n }\n \n private normalizeAngle(angle: number): number {\n let normalized = angle % 360;\n if (normalized < 0) {\n normalized += 360;\n }\n return normalized;\n }\n\n private getVara(date: Date): { vara: number; name: string } {\n // Correct Vara (weekday) calculation based on Julian Day Number\n // The astronomical day starts at noon, but for calendar purposes\n // we use the civil day starting at midnight\n const jd = this.dateToJulian(date);\n \n // Standard formula: vara = (JD + 1.5) mod 7\n // JD 0 corresponds to Monday, so we adjust accordingly\n const varaNumber = Math.floor((jd + 1.5) % 7);\n \n return {\n vara: varaNumber,\n name: this.varaNames[varaNumber]\n };\n }\n\n /**\n * Convert Date to Julian Day Number using accurate algorithm\n */\n private dateToJulian(date: Date): number {\n const year = date.getUTCFullYear();\n const month = date.getUTCMonth() + 1;\n const day = date.getUTCDate();\n const hour = date.getUTCHours();\n const minute = date.getUTCMinutes();\n const second = date.getUTCSeconds();\n const millisecond = date.getUTCMilliseconds();\n\n // Convert time to decimal hours\n const decimalHour = hour + minute / 60.0 + second / 3600.0 + millisecond / 3600000.0;\n\n // Standard Julian Day calculation algorithm\n let a = Math.floor((14 - month) / 12);\n let y = year + 4800 - a;\n let m = month + 12 * a - 3;\n \n let jd = day + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045;\n \n // Add the time component\n jd += (decimalHour - 12.0) / 24.0;\n \n return jd;\n }\n\n private getMoonPhase(sunLongitude: number, moonLongitude: number): string {\n const longitudeDiff = normalizeAngle(moonLongitude - sunLongitude);\n \n if (longitudeDiff < 45) return 'New Moon';\n if (longitudeDiff < 90) return 'Waxing Crescent';\n if (longitudeDiff < 135) return 'First Quarter';\n if (longitudeDiff < 180) return 'Waxing Gibbous';\n if (longitudeDiff < 225) return 'Full Moon';\n if (longitudeDiff < 270) return 'Waning Gibbous';\n if (longitudeDiff < 315) return 'Last Quarter';\n return 'Waning Crescent';\n }\n\n /**\n * Calculate Rahu Kaal (inauspicious time period)\n * Rahu Kaal is 1/8th of the day length, occurring at different times based on weekday\n */\n calculateRahuKaal(date: Date, location: Location): { start: Date | null; end: Date | null } | null {\n const sunrise = this.ephemeris.calculateSunrise(date, location);\n const sunset = this.ephemeris.calculateSunset(date, location);\n \n if (!sunrise || !sunset) return null;\n\n const dayLength = sunset.getTime() - sunrise.getTime();\n const oneEighth = dayLength / 8; // Divide day into 8 equal parts\n \n // Get the day of week (0 = Sunday)\n const dayOfWeek = date.getDay();\n let rahuKaalPeriod: number;\n\n // Correct Rahu Kaal timing based on weekday\n // Each period is 1/8th of the day from sunrise\n switch (dayOfWeek) {\n case 0: rahuKaalPeriod = 4; break; // Sunday - 5th period (4th index)\n case 1: rahuKaalPeriod = 1; break; // Monday - 2nd period (1st index)\n case 2: rahuKaalPeriod = 6; break; // Tuesday - 7th period (6th index)\n case 3: rahuKaalPeriod = 3; break; // Wednesday - 4th period (3rd index)\n case 4: rahuKaalPeriod = 2; break; // Thursday - 3rd period (2nd index)\n case 5: rahuKaalPeriod = 5; break; // Friday - 6th period (5th index)\n case 6: rahuKaalPeriod = 0; break; // Saturday - 1st period (0th index)\n default: rahuKaalPeriod = 0;\n }\n\n const startTime = new Date(sunrise.getTime() + (rahuKaalPeriod * oneEighth));\n const endTime = new Date(startTime.getTime() + oneEighth);\n\n return { start: startTime, end: endTime };\n }\n\n /**\n * Generate formatted Panchanga report\n */\n generateReport(panchangaData: PanchangaData): string {\n const { date, location, tithi, nakshatra, yoga,