UNPKG

pray-calc

Version:

Islamic prayer times with a physics-grounded dynamic twilight angle algorithm. Covers Fajr, Sunrise, Dhuhr, Asr, Maghrib, Isha, Qiyam. Includes 14 traditional fixed-angle methods for comparison.

1 lines 56.5 kB
{"version":3,"sources":["../src/getTimes.ts","../src/constants.ts","../src/getSolarEphemeris.ts","../src/getMSC.ts","../src/getAngles.ts","../src/getAsr.ts","../src/getQiyam.ts","../src/getMidnight.ts","../src/validate.ts","../src/calcTimes.ts","../src/getTimesAll.ts","../src/calcTimesAll.ts"],"sourcesContent":["/**\n * Core prayer times computation using the PrayCalc Dynamic Method.\n *\n * Returns all prayer times as fractional hours using the dynamic twilight\n * angle algorithm. Times are in local time as determined by the timezone\n * offset (tz parameter).\n */\n\nimport { getSpa } from 'nrel-spa';\nimport { computeAngles } from './getAngles.js';\nimport { getAsr } from './getAsr.js';\nimport { getQiyam } from './getQiyam.js';\nimport { getMidnight } from './getMidnight.js';\nimport { validateInputs } from './validate.js';\nimport { DHUHR_OFFSET_MINUTES } from './constants.js';\nimport type { PrayerTimes } from './types.js';\n\n/**\n * Compute prayer times for a given date and location.\n *\n * Uses the dynamic twilight angle algorithm to determine Fajr and Isha\n * depression angles, then solves for all prayer events via SPA.\n *\n * @param date - Observer's local date (time-of-day is ignored)\n * @param lat - Latitude in decimal degrees (-90 to 90, south = negative)\n * @param lng - Longitude in decimal degrees (-180 to 180, west = negative)\n * @param tz - UTC offset in hours (e.g. -5 for EST). Defaults to the\n * system timezone derived from the Date object.\n * @param elevation - Observer elevation in meters (default: 0)\n * @param temperature - Ambient temperature in °C (default: 15)\n * @param pressure - Atmospheric pressure in mbar/hPa (default: 1013.25)\n * @param hanafi - Asr convention: false = Shafi'i/Maliki/Hanbali (default),\n * true = Hanafi\n * @returns Prayer times as fractional hours and the dynamic angles used.\n * Any time that cannot be computed (e.g. polar night/day, or the\n * sun never reaching the required depression) is returned as `NaN`.\n * @throws {RangeError} if lat, lng, tz, or elevation are out of valid range\n */\nexport function getTimes(\n date: Date,\n lat: number,\n lng: number,\n tz: number = -date.getTimezoneOffset() / 60,\n elevation = 0,\n temperature = 15,\n pressure = 1013.25,\n hanafi = false,\n): PrayerTimes {\n validateInputs(lat, lng, tz, elevation);\n\n // 1. Compute dynamic twilight angles and reuse solar declination.\n const { fajrAngle, ishaAngle, decl } = computeAngles(\n date,\n lat,\n lng,\n elevation,\n temperature,\n pressure,\n );\n\n // 2. Convert depression angles to SPA zenith angles.\n // SPA uses zenith angle (90° + depression) for custom altitude events.\n const fajrZenith = 90 + fajrAngle;\n const ishaZenith = 90 + ishaAngle;\n\n // 3. Run SPA for solar position + custom twilight times.\n const spaOpts = { elevation, temperature, pressure };\n const spaData = getSpa(date, lat, lng, tz, spaOpts, [fajrZenith, ishaZenith]);\n\n const fajrTime = spaData.angles[0].sunrise;\n const sunriseTime = spaData.sunrise;\n const noonTime = spaData.solarNoon;\n const maghribTime = spaData.sunset;\n const ishaTime = spaData.angles[1].sunset;\n\n // Dhuhr: offset after solar noon (standard practice to confirm transit).\n const dhuhrTime = noonTime + DHUHR_OFFSET_MINUTES / 60;\n\n // 4. Asr time (reuses declination from computeAngles — no extra ephemeris call).\n const asrTime = getAsr(noonTime, lat, decl, hanafi);\n\n // 5. Qiyam al-Layl (last third of the night).\n const qiyamTime = getQiyam(fajrTime, ishaTime);\n\n // 6. Midnight: midpoint between Maghrib and Fajr (standard definition).\n const midnightTime = getMidnight(maghribTime, fajrTime);\n\n return {\n Qiyam: isFinite(qiyamTime) ? qiyamTime : NaN,\n Fajr: isFinite(fajrTime) ? fajrTime : NaN,\n Sunrise: isFinite(sunriseTime) ? sunriseTime : NaN,\n Noon: isFinite(noonTime) ? noonTime : NaN,\n Dhuhr: isFinite(dhuhrTime) ? dhuhrTime : NaN,\n Asr: isFinite(asrTime) ? asrTime : NaN,\n Maghrib: isFinite(maghribTime) ? maghribTime : NaN,\n Isha: isFinite(ishaTime) ? ishaTime : NaN,\n Midnight: isFinite(midnightTime) ? midnightTime : NaN,\n angles: { fajrAngle, ishaAngle },\n };\n}\n","/**\n * Shared constants for pray-calc.\n */\n\n/** Degrees-to-radians conversion factor. */\nexport const DEG = Math.PI / 180;\n\n/**\n * Minutes added to solar noon to obtain Dhuhr time.\n *\n * Standard practice adds a small buffer after geometric solar transit to\n * ensure the sun has clearly passed the meridian before Dhuhr begins.\n * The 2.5-minute convention is widely used across Islamic timekeeping\n * authorities and accounts for the sun's angular diameter (~0.5°) plus\n * a small safety margin.\n */\nexport const DHUHR_OFFSET_MINUTES = 2.5;\n\n/**\n * Minimum allowed dynamic twilight depression angle (degrees).\n *\n * At very high latitudes in summer the MCW base angle can drop below\n * physically meaningful values. 10° is the lower clamp — below this\n * the sky is too bright for any twilight definition.\n */\nexport const ANGLE_MIN = 10;\n\n/**\n * Maximum allowed dynamic twilight depression angle (degrees).\n *\n * 22° is the upper clamp. Values above ~20° correspond to deep\n * astronomical twilight where the sky is indistinguishable from full\n * night. No standard method exceeds 20° for Fajr.\n */\nexport const ANGLE_MAX = 22;\n","/**\n * High-accuracy solar ephemeris features without a full SPA call.\n *\n * Uses Jean Meeus \"Astronomical Algorithms\" (2nd ed., Ch. 25) low-precision\n * formulas, accurate to approximately ±0.01° for solar declination and\n * ±0.0001 AU for Earth-Sun distance over the years 1950-2050. This is\n * sufficient for computing twilight angles; exact Sun positioning for\n * prayer time solving still uses the full SPA via nrel-spa.\n */\n\nimport { DEG } from './constants.js';\n\n/** Julian Date from a JavaScript Date (UTC). */\nexport function toJulianDate(date: Date): number {\n return date.getTime() / 86400000 + 2440587.5;\n}\n\nexport interface SolarEphemeris {\n /** Solar declination in degrees. */\n decl: number;\n /** Earth-Sun distance in AU. */\n r: number;\n /** Apparent solar ecliptic longitude in radians (season phase θ, 0–2π). */\n eclLon: number;\n}\n\n/**\n * Compute solar declination, Earth-Sun distance, and ecliptic longitude\n * from a Julian Date. Accuracy: ~0.01° for declination, ~0.0001 AU for r.\n */\nexport function solarEphemeris(jd: number): SolarEphemeris {\n const T = (jd - 2451545.0) / 36525.0;\n\n // Geometric mean longitude L0 (degrees)\n const L0 = (((280.46646 + 36000.76983 * T + 0.0003032 * T * T) % 360) + 360) % 360;\n\n // Mean anomaly M (degrees)\n const M = (((357.52911 + 35999.05029 * T - 0.0001537 * T * T) % 360) + 360) % 360;\n const Mrad = M * DEG;\n\n // Orbital eccentricity\n const e = 0.016708634 - 0.000042037 * T - 0.0000001267 * T * T;\n\n // Equation of center C (degrees)\n const C =\n (1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(Mrad) +\n (0.019993 - 0.000101 * T) * Math.sin(2 * Mrad) +\n 0.000289 * Math.sin(3 * Mrad);\n\n // Sun's true longitude (degrees)\n const sunLon = L0 + C;\n\n // Sun's true anomaly (degrees)\n const nu = M + C;\n const nuRad = nu * DEG;\n\n // Earth-Sun distance in AU\n const r = (1.000001018 * (1 - e * e)) / (1 + e * Math.cos(nuRad));\n\n // Longitude of ascending node of Moon's orbit (for nutation)\n const Omega = (((125.04 - 1934.136 * T) % 360) + 360) % 360;\n const OmegaRad = Omega * DEG;\n\n // Apparent solar longitude corrected for nutation and aberration\n const lambda = sunLon - 0.00569 - 0.00478 * Math.sin(OmegaRad);\n const lambdaRad = lambda * DEG;\n\n // Mean obliquity of the ecliptic (degrees)\n const epsilon0 = 23.439291 - 0.013004 * T - 1.638e-7 * T * T + 5.036e-7 * T * T * T;\n\n // True obliquity with nutation correction\n const epsilon = (epsilon0 + 0.00256 * Math.cos(OmegaRad)) * DEG;\n\n // Solar declination\n const sinDecl = Math.sin(epsilon) * Math.sin(lambdaRad);\n const decl = Math.asin(Math.max(-1, Math.min(1, sinDecl))) / DEG;\n\n // Ecliptic longitude as season phase θ ∈ [0, 2π)\n const eclLon = ((lambdaRad % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);\n\n return { decl, r, eclLon };\n}\n\n/**\n * Solar vertical angular speed near a given hour angle H (radians),\n * in degrees per hour. Useful as a confidence weight for angle predictions:\n * higher speed means each degree of angle error is fewer minutes of time error.\n *\n * Formula: dh/dt ≈ 15 × cos(φ) × cos(δ) × sin(H) [°/hr]\n */\nexport function solarVerticalSpeed(latRad: number, declRad: number, hAngleRad: number): number {\n return 15 * Math.abs(Math.cos(latRad) * Math.cos(declRad) * Math.sin(hAngleRad));\n}\n\n/**\n * Compute the atmospheric refraction correction (degrees) for a given\n * apparent solar altitude using the Bennett/Saemundsson formula.\n * Scaled for pressure and temperature.\n *\n * Returns a positive correction (the Sun appears higher than its geometric\n * position). For altitudes below -1°, returns 0 (not meaningful for Fajr/Isha\n * at depression angles like 12–20°, but included for completeness).\n */\nexport function atmosphericRefraction(\n altitudeDeg: number,\n pressureMbar = 1013.25,\n temperatureC = 15,\n): number {\n if (altitudeDeg < -1) return 0;\n // Bennett's formula in arcminutes\n const R0 = 1.02 / Math.tan((altitudeDeg + 10.3 / (altitudeDeg + 5.11)) * DEG);\n // Scale for pressure and temperature\n const R = R0 * (pressureMbar / 1010) * (283 / (273 + temperatureC));\n return Math.max(0, R) / 60; // convert arcminutes to degrees\n}\n","/**\n * Moonsighting Committee Worldwide (MCW) seasonal algorithm.\n *\n * Computes Fajr and Isha as time offsets from sunrise/sunset using the\n * empirical piecewise-linear seasonal functions developed by the Moonsighting\n * Committee Worldwide (Khalid Shaukat). The functions were derived by\n * curve-fitting observations of Subh Sadiq (true dawn) and the end of\n * Shafaq (twilight glow) across multiple latitudes.\n *\n * Reference: moonsighting.com/isha_fajr.html\n *\n * ## MCW Coefficient Key\n *\n * The piecewise-linear anchor values (a, b, c, d) follow the pattern:\n * value = BASE + (SLOPE / LAT_SCALE) × |latitude|\n *\n * where BASE is the equatorial offset in minutes, SLOPE is the per-degree\n * latitude coefficient, and LAT_SCALE = 55° is the normalisation latitude.\n * Coefficients were curve-fit to multi-latitude observations of Subh Sadiq\n * and Shafaq by the Moonsighting Committee (moonsighting.com/isha_fajr.html).\n *\n * High-latitude handling (|lat| > 55°): falls back to 1/7-night rule.\n */\n\nexport type ShafaqMode = 'general' | 'ahmer' | 'abyad';\n\n/**\n * Normalisation latitude (degrees) used as the divisor in MCW latitude\n * scaling coefficients. All MCW slope values are expressed per 55° of\n * latitude so that the piecewise function smoothly scales from equator\n * to mid-high latitudes.\n */\nconst LAT_SCALE = 55;\n\nfunction isLeapYear(year: number): boolean {\n return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;\n}\n\n/**\n * Compute the MCW seasonal index (dyy): days elapsed since the nearest\n * winter solstice (Northern Hemisphere) or summer solstice (Southern).\n */\nfunction computeDyy(date: Date, latitude: number): { dyy: number; daysInYear: number } {\n const year = date.getFullYear();\n const daysInYear = isLeapYear(year) ? 366 : 365;\n\n // Reference solstice: Dec 21 for Northern, Jun 21 for Southern\n const refMonth = latitude >= 0 ? 11 : 5; // Dec = 11, Jun = 5\n const refDay = 21;\n\n const zeroDate = new Date(year, refMonth, refDay);\n let diffDays = Math.floor(\n (Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) -\n Date.UTC(zeroDate.getFullYear(), zeroDate.getMonth(), zeroDate.getDate())) /\n 86400000,\n );\n if (diffDays < 0) diffDays += daysInYear;\n\n return { dyy: diffDays, daysInYear };\n}\n\n/**\n * Piecewise-linear seasonal interpolation over 6 segments.\n * a, b, c, d are the reference values at the seasonal anchor points.\n */\nfunction interpolateSegment(\n dyy: number,\n daysInYear: number,\n a: number,\n b: number,\n c: number,\n d: number,\n): number {\n if (dyy < 91) {\n return a + ((b - a) / 91) * dyy;\n } else if (dyy < 137) {\n return b + ((c - b) / 46) * (dyy - 91);\n } else if (dyy < 183) {\n return c + ((d - c) / 46) * (dyy - 137);\n } else if (dyy < 229) {\n return d + ((c - d) / 46) * (dyy - 183);\n } else if (dyy < 275) {\n return c + ((b - c) / 46) * (dyy - 229);\n } else {\n const len = daysInYear - 275;\n return b + ((a - b) / len) * (dyy - 275);\n }\n}\n\n/**\n * Compute Fajr offset in minutes before sunrise using the MCW algorithm.\n *\n * Returns minutes before sunrise. At latitudes above 55°, the 1/7-night\n * approximation is recommended (handled at the calling site).\n */\nexport function getMscFajr(date: Date, latitude: number): number {\n const latAbs = Math.abs(latitude);\n const { dyy, daysInYear } = computeDyy(date, latitude);\n\n // Anchor values: BASE + (SLOPE / LAT_SCALE) × |lat|\n // BASE = 75 min (equatorial Fajr offset). Slopes from MCW curve-fit.\n const a = 75 + (28.65 / LAT_SCALE) * latAbs;\n const b = 75 + (19.44 / LAT_SCALE) * latAbs;\n const c = 75 + (32.74 / LAT_SCALE) * latAbs;\n const d = 75 + (48.1 / LAT_SCALE) * latAbs;\n\n return Math.round(interpolateSegment(dyy, daysInYear, a, b, c, d));\n}\n\n/**\n * Compute Isha offset in minutes after sunset using the MCW algorithm.\n *\n * Three Shafaq modes:\n * - 'general': blend that reduces hardship at high latitudes (default)\n * - 'ahmer': based on disappearance of redness (shafaq ahmer)\n * - 'abyad': based on disappearance of whiteness (shafaq abyad), later\n */\nexport function getMscIsha(date: Date, latitude: number, shafaq: ShafaqMode = 'general'): number {\n const latAbs = Math.abs(latitude);\n const { dyy, daysInYear } = computeDyy(date, latitude);\n\n let a: number, b: number, c: number, d: number;\n\n switch (shafaq) {\n case 'ahmer':\n // Shafaq ahmer (red glow): BASE = 62 min (shorter twilight)\n a = 62 + (17.4 / LAT_SCALE) * latAbs;\n b = 62 - (7.16 / LAT_SCALE) * latAbs;\n c = 62 + (5.12 / LAT_SCALE) * latAbs;\n d = 62 + (19.44 / LAT_SCALE) * latAbs;\n break;\n case 'abyad':\n // Shafaq abyad (white glow): BASE = 75 min (longer twilight)\n a = 75 + (25.6 / LAT_SCALE) * latAbs;\n b = 75 + (7.16 / LAT_SCALE) * latAbs;\n c = 75 + (36.84 / LAT_SCALE) * latAbs;\n d = 75 + (81.84 / LAT_SCALE) * latAbs;\n break;\n default: // 'general'\n // General (blended) mode: BASE = 75 min\n a = 75 + (25.6 / LAT_SCALE) * latAbs;\n b = 75 + (2.05 / LAT_SCALE) * latAbs;\n c = 75 - (9.21 / LAT_SCALE) * latAbs;\n d = 75 + (6.14 / LAT_SCALE) * latAbs;\n }\n\n return Math.round(interpolateSegment(dyy, daysInYear, a, b, c, d));\n}\n\n/**\n * Convert MCW minutes-before-sunrise to an equivalent solar depression angle\n * in degrees, using exact spherical trigonometry.\n *\n * This is the inverse of the standard hour-angle sunrise formula and gives\n * the depression angle that corresponds to a given pre-sunrise interval at\n * the observer's latitude and the given solar declination.\n *\n * Returns NaN if the geometry is unreachable (polar day/night).\n */\nexport function minutesToDepression(minutes: number, latDeg: number, declDeg: number): number {\n const phi = latDeg * (Math.PI / 180);\n const delta = declDeg * (Math.PI / 180);\n\n const cosPhi = Math.cos(phi);\n const sinPhi = Math.sin(phi);\n const cosDelta = Math.cos(delta);\n const sinDelta = Math.sin(delta);\n\n // Standard sunrise/sunset: h = -0.833° (includes refraction + semi-diameter)\n const h0 = -0.833 * (Math.PI / 180);\n const sinH0 = Math.sin(h0);\n\n const denominator = cosPhi * cosDelta;\n if (Math.abs(denominator) < 1e-10) return NaN;\n\n // Hour angle at standard sunrise\n const cosH_rise = (sinH0 - sinPhi * sinDelta) / denominator;\n\n if (cosH_rise < -1) return NaN; // polar night\n if (cosH_rise > 1) return NaN; // polar day\n\n const H_rise = Math.acos(cosH_rise); // radians\n\n // Hour angle at the prayer time (further from solar noon)\n const deltaH = (minutes / 60) * 15 * (Math.PI / 180);\n const H_prayer = H_rise + deltaH;\n\n // Cap at π (midnight) - sun cannot go further below horizon\n if (H_prayer > Math.PI) {\n // Return the depression at midnight (minimum possible for this date/lat)\n const sinH_midnight = sinPhi * sinDelta + cosPhi * cosDelta * Math.cos(Math.PI);\n const h_midnight = Math.asin(Math.max(-1, Math.min(1, sinH_midnight)));\n return -h_midnight / (Math.PI / 180);\n }\n\n // Solar altitude at H_prayer\n const sinH_prayer = sinPhi * sinDelta + cosPhi * cosDelta * Math.cos(H_prayer);\n const h_prayer = Math.asin(Math.max(-1, Math.min(1, sinH_prayer)));\n\n // Depression angle: positive when sun is below horizon\n return -h_prayer / (Math.PI / 180);\n}\n","/**\n * Dynamic twilight angle algorithm — PrayCalc Dynamic Method v2.\n *\n * Computes adaptive Fajr and Isha solar depression angles that accurately\n * track the observable phenomenon (Subh Sadiq / end of Shafaq) across all\n * latitudes and seasons, replacing a static angle with a physics-informed\n * estimate.\n *\n * ## Algorithm\n *\n * The research literature establishes that \"true dawn\" and \"end of twilight\"\n * are not tied to a single universal solar depression angle. The required\n * angle varies with latitude, season, and atmospheric conditions. Field\n * studies show approximately:\n *\n * - Low latitudes (0–30°): ~16–19° (dark-sky conditions approach 18–19°)\n * - Mid-latitudes (30–45°): ~14–17°, with seasonal variation\n * - High latitudes (45–55°):~11–15°, strongly seasonal (shallow in summer)\n *\n * This implementation uses a three-layer model:\n *\n * 1. **MSC base**: The Moonsighting Committee Worldwide (MCW) piecewise\n * seasonal function is used as the empirical baseline — the most widely\n * validated and observation-calibrated model available. The MCW minutes-\n * before-sunrise value is converted to an equivalent depression angle\n * via exact spherical trigonometry.\n *\n * 2. **Ephemeris corrections**: Physics-based adjustments derived from\n * accurate solar position features (ecliptic longitude, Earth-Sun\n * distance, solar vertical speed). These smooth over the MCW's piecewise\n * discontinuities and capture the small irradiance variation (~3.3%)\n * due to Earth's orbital eccentricity (perihelion in January, aphelion\n * in July).\n *\n * 3. **Environmental corrections**: Observer elevation (horizon dip) and\n * atmospheric refraction scaled to local pressure and temperature.\n *\n * ## Why this is better than a fixed angle\n *\n * Fixed angles (e.g., 18°, 15°) do not adapt to latitude-season geometry\n * and break outright at higher latitudes in summer when the sun never reaches\n * 15° below the horizon. This algorithm produces smooth, continuous values\n * validated against the MCW observational corpus and enhanced by physical\n * corrections the MCW piecewise model cannot express.\n *\n * ## References\n *\n * - Moonsighting Committee Worldwide (Khalid Shaukat): moonsighting.com\n * - Deep-research reports PCP1–PCP5 (archived in internal docs)\n * - Jean Meeus, Astronomical Algorithms (2nd ed., 1998)\n */\n\nimport { toJulianDate, solarEphemeris, atmosphericRefraction } from './getSolarEphemeris.js';\nimport { getMscFajr, getMscIsha, minutesToDepression } from './getMSC.js';\nimport { DEG, ANGLE_MIN, ANGLE_MAX } from './constants.js';\nimport type { TwilightAngles } from './types.js';\n\n/** Internal result type including ephemeris data for caller reuse. */\nexport interface AnglesWithEphemeris extends TwilightAngles {\n /** Solar declination in degrees (reusable for Asr computation). */\n decl: number;\n}\n\n/** Clamp a value to [min, max]. */\nfunction clip(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, value));\n}\n\n/** Round to 3 decimal places. */\nfunction round3(value: number): number {\n return Math.round(value * 1000) / 1000;\n}\n\n/**\n * Compute the Earth-Sun distance correction in degrees.\n *\n * Earth's orbit is slightly elliptical (eccentricity ~0.017). At perihelion\n * (≈Jan 3) r ≈ 0.983 AU; at aphelion (≈Jul 4) r ≈ 1.017 AU. The 3.3%\n * irradiance variation affects when twilight brightness crosses the detection\n * threshold. At perihelion, higher irradiance means the threshold is crossed\n * at a slightly deeper depression (earlier Fajr / later Isha); at aphelion,\n * the reverse. Effect magnitude: ≈ ±0.15°.\n *\n * Physical basis: L_tw ∝ r^{-2}, so threshold α is reached at a value\n * proportional to −(1/2) ln(r). The negative sign is because higher\n * irradiance means dawn is detectable at a slightly deeper Sun position,\n * increasing the angle.\n */\nfunction earthSunDistanceCorrection(r: number): number {\n // −0.5 × ln(r): positive correction at aphelion (r > 1), negative at perihelion\n // At r = 0.983: correction ≈ −0.5 × (−0.017) = +0.009° (tiny, physically correct)\n // At r = 1.017: correction ≈ −0.5 × 0.017 = −0.009° (tiny)\n // Scale factor 0.5 chosen to keep the effect physically realistic.\n // Full irradiance effect would be larger; 0.5 accounts for the non-linear\n // relationship between irradiance and perceived brightness threshold.\n return -0.5 * Math.log(r);\n}\n\n/**\n * Smooth Fourier season correction to remove the MCW's piecewise artifacts\n * and add hemisphere-symmetric season curvature.\n *\n * The correction uses the solar ecliptic longitude θ (season phase) and\n * |φ| × seasonal interaction. Coefficients are calibrated to:\n * - match MCW behavior at key anchor latitudes (0°, 30°, 50°)\n * - reduce step-function artifacts at the MCW segment boundaries (dyy ≈ 91, 137, ...)\n * - add a mild correction for the June-solstice / December-solstice asymmetry\n * driven by r (perihelion in January vs aphelion in July)\n *\n * Net effect is small (< 0.3°) and primarily improves day-to-day smoothness.\n */\nfunction fourierSmoothingCorrection(eclLon: number, latAbsDeg: number): number {\n const theta = eclLon; // solar ecliptic longitude, radians [0, 2π)\n const phi = latAbsDeg * DEG;\n\n // First harmonic: small annual asymmetry correction\n // The perihelion/aphelion asymmetry causes slightly different twilight\n // behavior in January vs July even at the same declination.\n const a1 = 0.03 * Math.sin(theta); // peaks at ~Jun solstice\n const b1 = -0.05 * Math.cos(theta); // peaks at equinoxes\n\n // Second harmonic: semi-annual variation\n const a2 = 0.02 * Math.sin(2 * theta);\n const b2 = 0.02 * Math.cos(2 * theta);\n\n // Latitude × season interaction: refines the MCW's latitude scaling\n const c1 = -0.008 * phi * Math.sin(theta);\n const d1 = 0.004 * phi * Math.cos(theta);\n\n return a1 + b1 + a2 + b2 + c1 + d1;\n}\n\n/**\n * Internal: compute angles and return solar declination for Asr reuse.\n *\n * This avoids recomputing solarEphemeris in getTimes/getTimesAll.\n */\nexport function computeAngles(\n date: Date,\n lat: number,\n lng: number,\n elevation = 0,\n temperature = 15,\n pressure = 1013.25,\n): AnglesWithEphemeris {\n // 1. Solar ephemeris features at solar noon of the given date.\n // Using UTC noon as a stable reference that avoids timezone artifacts.\n const noonDate = new Date(\n Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0),\n );\n const jd = toJulianDate(noonDate);\n const { decl, r, eclLon } = solarEphemeris(jd);\n\n // 2. MCW reference times (minutes before/after sunrise/sunset).\n const mscFajrMin = getMscFajr(date, lat);\n const mscIshaMin = getMscIsha(date, lat);\n\n // 3. Convert MCW minutes to equivalent solar depression angles using\n // exact spherical trigonometry with the accurate Meeus declination.\n let fajrBase = minutesToDepression(mscFajrMin, lat, decl);\n let ishaBase = minutesToDepression(mscIshaMin, lat, decl);\n\n // Handle polar or unreachable geometry: fall back to safe defaults.\n if (!isFinite(fajrBase) || isNaN(fajrBase)) fajrBase = 18.0;\n if (!isFinite(ishaBase) || isNaN(ishaBase)) ishaBase = 18.0;\n\n // 4. Earth-Sun distance correction (±0.015°, positive at aphelion).\n const rCorr = earthSunDistanceCorrection(r);\n\n // 5. Fourier smoothing correction (< 0.3° total).\n const fourierCorr = fourierSmoothingCorrection(eclLon, Math.abs(lat));\n\n // 6. Atmospheric refraction at the expected twilight depression.\n // Refraction at 15° below horizon is small (~0.06°). We apply it with\n // opposite sign for Fajr vs Isha: refraction raises apparent altitude,\n // meaning the true geometric sun is slightly deeper than the perceived one.\n // For Fajr (morning): refraction effectively means dawn occurs slightly\n // earlier (when sun is slightly deeper geometrically) → add to Fajr angle.\n // For Isha (evening): same physical effect → add to Isha angle.\n // Note: nrel-spa already accounts for refraction near the horizon. Here we\n // apply the correction to the twilight angle itself (deeper depression zone).\n const refrFajr = atmosphericRefraction(-(fajrBase + 0.5), pressure, temperature);\n const refrIsha = atmosphericRefraction(-(ishaBase + 0.5), pressure, temperature);\n\n // 7. Elevation correction: higher observers see further around Earth's curvature,\n // effectively dipping the horizon. This makes sunrise slightly earlier and\n // sunset slightly later. For Fajr/Isha, the effect is ~0.08°/km elevation.\n // Using dip = 1.06 × √(h_km) in degrees, scaled by factor for twilight\n // (the dip effect is reduced at large depression angles vs the horizon).\n const horizonDipDeg = 1.06 * Math.sqrt(elevation / 1000);\n // Apply 30% of horizon dip to twilight angles (remainder already captured\n // by nrel-spa's elevation-adjusted sunrise/sunset computation).\n const elevCorr = horizonDipDeg * 0.3;\n\n // 8. Assemble final angles with all corrections.\n const rawFajr = fajrBase + rCorr + fourierCorr + refrFajr + elevCorr;\n const rawIsha = ishaBase + rCorr + fourierCorr + refrIsha + elevCorr;\n\n const fajrAngle = round3(clip(rawFajr, ANGLE_MIN, ANGLE_MAX));\n const ishaAngle = round3(clip(rawIsha, ANGLE_MIN, ANGLE_MAX));\n\n return { fajrAngle, ishaAngle, decl };\n}\n\n/**\n * Compute dynamic twilight depression angles for Fajr and Isha.\n *\n * @param date - Observer's local date (time-of-day is ignored)\n * @param lat - Latitude in decimal degrees (-90 to 90)\n * @param lng - Longitude in decimal degrees (-180 to 180, currently unused; reserved)\n * @param elevation - Observer elevation in meters (default: 0)\n * @param temperature - Ambient temperature in °C (default: 15)\n * @param pressure - Atmospheric pressure in mbar (default: 1013.25)\n * @returns Fajr and Isha depression angles in degrees\n */\nexport function getAngles(\n date: Date,\n lat: number,\n lng: number,\n elevation = 0,\n temperature = 15,\n pressure = 1013.25,\n): TwilightAngles {\n const { fajrAngle, ishaAngle } = computeAngles(date, lat, lng, elevation, temperature, pressure);\n return { fajrAngle, ishaAngle };\n}\n","/**\n * Asr prayer time calculation.\n *\n * Asr begins when the shadow of an object equals (Shafi'i/Maliki/Hanbali)\n * or twice (Hanafi) the object's length plus its shadow at solar noon.\n * This is a pure spherical trigonometry problem once solar declination\n * and solar noon are known.\n */\n\nimport { DEG } from './constants.js';\n\n/**\n * Compute Asr time as fractional hours.\n *\n * @param solarNoon - Solar noon in fractional hours (from getSpa)\n * @param latitude - Observer latitude in degrees\n * @param declination - Solar declination in degrees (from solarEphemeris)\n * @param hanafi - true for Hanafi (shadow factor 2), false for Shafi'i (factor 1)\n * @returns Fractional hours, or NaN if the sun never reaches the required altitude\n */\nexport function getAsr(\n solarNoon: number,\n latitude: number,\n declination: number,\n hanafi = false,\n): number {\n const phi = latitude * DEG;\n const delta = declination * DEG;\n const shadowFactor = hanafi ? 2 : 1;\n\n // Required solar altitude:\n // tan(A) = 1 / (shadowFactor + tan(|φ − δ|))\n const X = Math.abs(phi - delta);\n const tanA = 1 / (shadowFactor + Math.tan(X));\n const sinA = tanA / Math.sqrt(1 + tanA * tanA); // sin(atan(tanA))\n\n // Solve the hour-angle equation:\n // cos(H0) = (sin(A) − sin(φ)sin(δ)) / (cos(φ)cos(δ))\n const cosH0 = (sinA - Math.sin(phi) * Math.sin(delta)) / (Math.cos(phi) * Math.cos(delta));\n\n if (cosH0 < -1 || cosH0 > 1) return NaN; // sun never reaches A\n\n // H0 in hours (15°/hr)\n const H0h = Math.acos(cosH0) / DEG / 15;\n\n return solarNoon + H0h;\n}\n","/**\n * Qiyam al-Layl (night prayer) time calculation.\n *\n * Returns the start of the last third of the night, which is the recommended\n * time for Tahajjud / Qiyam al-Layl. The night is defined as the period\n * from Isha to Fajr.\n */\n\n/**\n * Compute the start of the last third of the night.\n *\n * @param fajrTime - Fajr time in fractional hours\n * @param ishaTime - Isha time in fractional hours\n * @returns Start of the last third of the night (fractional hours)\n */\nexport function getQiyam(fajrTime: number, ishaTime: number): number {\n // If Fajr is numerically earlier (e.g. 5.5) than Isha (e.g. 21.5), Fajr\n // is actually the NEXT day — add 24 to get the correct night length.\n const adjustedFajr = fajrTime < ishaTime ? fajrTime + 24 : fajrTime;\n\n const nightLength = adjustedFajr - ishaTime;\n const lastThirdStart = ishaTime + (2 * nightLength) / 3;\n\n return lastThirdStart >= 24 ? lastThirdStart - 24 : lastThirdStart;\n}\n","/**\n * Islamic midnight calculation.\n *\n * Returns the midpoint of the night, commonly used as the endpoint\n * of the Isha prayer window. The standard definition uses the interval\n * from Maghrib to Fajr; the astronomical variant uses Maghrib to Sunrise.\n */\n\n/**\n * Compute the midpoint of the night.\n *\n * @param maghribTime - Maghrib (sunset) time in fractional hours\n * @param endTime - Fajr or Sunrise time in fractional hours (next day)\n * @returns Midnight as fractional hours\n */\nexport function getMidnight(maghribTime: number, endTime: number): number {\n // If endTime is numerically earlier (e.g. 5.5) than Maghrib (e.g. 20.0),\n // the endpoint is on the NEXT day: add 24 to get the correct span.\n const adjusted = endTime < maghribTime ? endTime + 24 : endTime;\n\n const mid = maghribTime + (adjusted - maghribTime) / 2;\n\n return mid >= 24 ? mid - 24 : mid;\n}\n","/**\n * Input validation for public API boundaries.\n */\n\n/**\n * Validate geographic and atmospheric inputs for prayer time computation.\n *\n * @throws {RangeError} if any parameter is out of its valid range\n */\nexport function validateInputs(lat: number, lng: number, tz?: number, elevation?: number): void {\n if (!Number.isFinite(lat) || lat < -90 || lat > 90) {\n throw new RangeError(`latitude must be between -90 and 90, got ${lat}`);\n }\n if (!Number.isFinite(lng) || lng < -180 || lng > 180) {\n throw new RangeError(`longitude must be between -180 and 180, got ${lng}`);\n }\n if (tz !== undefined && (!Number.isFinite(tz) || tz < -14 || tz > 14)) {\n throw new RangeError(`timezone offset must be between -14 and 14, got ${tz}`);\n }\n if (elevation !== undefined && (!Number.isFinite(elevation) || elevation < -500)) {\n throw new RangeError(`elevation must be >= -500m, got ${elevation}`);\n }\n}\n","/**\n * Formatted prayer times using the PrayCalc Dynamic Method.\n */\n\nimport { formatTime } from 'nrel-spa';\nimport { getTimes } from './getTimes.js';\nimport type { FormattedPrayerTimes } from './types.js';\n\n/**\n * Compute prayer times formatted as HH:MM:SS strings.\n *\n * Uses the dynamic twilight angle algorithm. See getTimes() for full parameter\n * documentation.\n *\n * @returns Prayer times as HH:MM:SS strings. Returns \"N/A\" for any time that\n * cannot be computed (polar night, unreachable angle, etc.).\n */\nexport function calcTimes(\n date: Date,\n lat: number,\n lng: number,\n tz: number = -date.getTimezoneOffset() / 60,\n elevation = 0,\n temperature = 15,\n pressure = 1013.25,\n hanafi = false,\n): FormattedPrayerTimes {\n const raw = getTimes(date, lat, lng, tz, elevation, temperature, pressure, hanafi);\n\n // Sort by fractional hour value so output reflects chronological order.\n // Angles are preserved as-is (not time values).\n return {\n Qiyam: formatTime(raw.Qiyam),\n Fajr: formatTime(raw.Fajr),\n Sunrise: formatTime(raw.Sunrise),\n Noon: formatTime(raw.Noon),\n Dhuhr: formatTime(raw.Dhuhr),\n Asr: formatTime(raw.Asr),\n Maghrib: formatTime(raw.Maghrib),\n Isha: formatTime(raw.Isha),\n Midnight: formatTime(raw.Midnight),\n angles: raw.angles,\n };\n}\n","/**\n * Prayer times comparison — all methods.\n *\n * Returns the PrayCalc Dynamic times plus comparison times for every\n * supported traditional method, all as fractional hours.\n *\n * Supported methods (14 total):\n *\n * | ID | Name | Fajr | Isha | Region |\n * |---------|----------------------------------------------|-------|-----------------|-----------------|\n * | UOIF | Union des Org. Islamiques de France | 12° | 12° | France |\n * | ISNACA | IQNA / Islamic Council of North America | 13° | 13° | Canada |\n * | ISNA | FCNA / Islamic Society of North America | 15° | 15° | US, UK, AU, NZ |\n * | SAMR | Spiritual Admin. of Muslims of Russia | 16° | 15° | Russia |\n * | IGUT | Inst. of Geophysics, Univ. of Tehran | 17.7° | 14° | Iran, Shia use |\n * | MWL | Muslim World League | 18° | 17° | Global default |\n * | DIBT | Diyanet İşleri Başkanlığı, Turkey | 18° | 17° | Turkey |\n * | Karachi | University of Islamic Sciences, Karachi | 18° | 18° | PK, BD, IN, AF |\n * | Kuwait | Kuwait Ministry of Islamic Affairs | 18° | 17.5° | Kuwait |\n * | UAQ | Umm Al-Qura University, Makkah | 18.5° | +90 min sunset | Saudi / Gulf |\n * | Qatar | Qatar / Gulf (standard minutes interval) | 18° | +90 min sunset | Qatar, Gulf |\n * | Egypt | Egyptian General Authority of Survey | 19.5° | 17.5° | EG, SY, IQ, LB |\n * | MUIS | Majlis Ugama Islam Singapura | 20° | 18° | Singapore |\n * | MSC | Moonsighting Committee Worldwide (seasonal) | — | — | Global |\n */\n\nimport { getSpa } from 'nrel-spa';\nimport { computeAngles } from './getAngles.js';\nimport { getAsr } from './getAsr.js';\nimport { getQiyam } from './getQiyam.js';\nimport { getMidnight } from './getMidnight.js';\nimport { getMscFajr, getMscIsha } from './getMSC.js';\nimport { validateInputs } from './validate.js';\nimport { DHUHR_OFFSET_MINUTES } from './constants.js';\nimport type { MethodDefinition, PrayerTimesAll } from './types.js';\n\n/** All supported traditional methods. */\nconst METHODS: MethodDefinition[] = [\n {\n id: 'UOIF',\n name: 'Union des Organisations Islamiques de France',\n region: 'France',\n fajrAngle: 12,\n ishaAngle: 12,\n },\n {\n id: 'ISNACA',\n name: 'IQNA / Islamic Council of North America',\n region: 'Canada',\n fajrAngle: 13,\n ishaAngle: 13,\n },\n {\n id: 'ISNA',\n name: 'FCNA / Islamic Society of North America',\n region: 'US, UK, AU, NZ',\n fajrAngle: 15,\n ishaAngle: 15,\n },\n {\n id: 'SAMR',\n name: 'Spiritual Administration of Muslims of Russia',\n region: 'Russia',\n fajrAngle: 16,\n ishaAngle: 15,\n },\n {\n id: 'IGUT',\n name: 'Institute of Geophysics, University of Tehran',\n region: 'Iran',\n fajrAngle: 17.7,\n ishaAngle: 14,\n },\n { id: 'MWL', name: 'Muslim World League', region: 'Global', fajrAngle: 18, ishaAngle: 17 },\n {\n id: 'DIBT',\n name: 'Diyanet İşleri Başkanlığı, Turkey',\n region: 'Turkey',\n fajrAngle: 18,\n ishaAngle: 17,\n },\n {\n id: 'Karachi',\n name: 'University of Islamic Sciences, Karachi',\n region: 'PK, BD, IN, AF',\n fajrAngle: 18,\n ishaAngle: 18,\n },\n {\n id: 'Kuwait',\n name: 'Kuwait Ministry of Islamic Affairs',\n region: 'Kuwait',\n fajrAngle: 18,\n ishaAngle: 17.5,\n },\n {\n id: 'UAQ',\n name: 'Umm Al-Qura University, Makkah',\n region: 'Saudi Arabia',\n fajrAngle: 18.5,\n ishaAngle: null,\n ishaMinutes: 90,\n },\n {\n id: 'Qatar',\n name: 'Qatar / Gulf Standard',\n region: 'Qatar, Gulf',\n fajrAngle: 18,\n ishaAngle: null,\n ishaMinutes: 90,\n },\n {\n id: 'Egypt',\n name: 'Egyptian General Authority of Survey',\n region: 'EG, SY, IQ, LB',\n fajrAngle: 19.5,\n ishaAngle: 17.5,\n },\n {\n id: 'MUIS',\n name: 'Majlis Ugama Islam Singapura',\n region: 'Singapore',\n fajrAngle: 20,\n ishaAngle: 18,\n },\n {\n id: 'MSC',\n name: 'Moonsighting Committee Worldwide',\n region: 'Global',\n fajrAngle: null,\n ishaAngle: null,\n useMSC: true,\n },\n];\n\n/**\n * Compute prayer times plus all traditional method comparisons.\n *\n * @param date - Observer's local date (time-of-day is ignored)\n * @param lat - Latitude in decimal degrees (-90 to 90)\n * @param lng - Longitude in decimal degrees (-180 to 180)\n * @param tz - UTC offset in hours (defaults to system tz)\n * @param elevation - Observer elevation in meters (default: 0)\n * @param temperature - Ambient temperature in °C (default: 15)\n * @param pressure - Atmospheric pressure in mbar (default: 1013.25)\n * @param hanafi - Asr convention: false = Shafi'i (default), true = Hanafi\n * @returns Prayer times for the dynamic method plus all traditional methods.\n * Any time that cannot be computed is returned as `NaN`.\n * Methods map contains `[fajrTime, ishaTime]` per method.\n * @throws {RangeError} if lat, lng, tz, or elevation are out of valid range\n */\nexport function getTimesAll(\n date: Date,\n lat: number,\n lng: number,\n tz: number = -date.getTimezoneOffset() / 60,\n elevation = 0,\n temperature = 15,\n pressure = 1013.25,\n hanafi = false,\n): PrayerTimesAll {\n validateInputs(lat, lng, tz, elevation);\n\n // 1. Dynamic angles and reusable solar declination.\n const { fajrAngle, ishaAngle, decl } = computeAngles(\n date,\n lat,\n lng,\n elevation,\n temperature,\n pressure,\n );\n\n // 2. Build batch zenith angles for the SPA call:\n // Slot 0: dynamic Fajr, Slot 1: dynamic Isha, then pairs for each method.\n // Methods with null angles (UAQ-isha, Qatar-isha, MSC) get a placeholder\n // that is overridden below.\n const methodZeniths: number[] = [];\n for (const m of METHODS) {\n const fZ = m.fajrAngle !== null ? 90 + m.fajrAngle : 90 + 18; // placeholder for non-angle Fajr\n const iZ = m.ishaAngle !== null ? 90 + m.ishaAngle : 90 + 18; // placeholder for fixed-minute Isha\n methodZeniths.push(fZ, iZ);\n }\n\n const allZeniths: [number, ...number[]] = [\n 90 + fajrAngle,\n 90 + ishaAngle,\n ...(methodZeniths as number[]),\n ] as [number, ...number[]];\n\n const spaOpts = { elevation, temperature, pressure };\n const spaData = getSpa(date, lat, lng, tz, spaOpts, allZeniths);\n\n // 3. Extract core times (index 0 = dynamic Fajr, index 1 = dynamic Isha).\n const fajrTime = spaData.angles[0].sunrise;\n const sunriseTime = spaData.sunrise;\n const noonTime = spaData.solarNoon;\n const maghribTime = spaData.sunset;\n const ishaTime = spaData.angles[1].sunset;\n const dhuhrTime = noonTime + DHUHR_OFFSET_MINUTES / 60;\n\n // 4. Asr time (reuses declination from computeAngles — no extra ephemeris call).\n const asrTime = getAsr(noonTime, lat, decl, hanafi);\n const qiyamTime = getQiyam(fajrTime, ishaTime);\n const midnightTime = getMidnight(maghribTime, fajrTime);\n\n // 5. Build Methods map.\n const Methods: Record<string, [number, number]> = {};\n\n for (let i = 0; i < METHODS.length; i++) {\n const m = METHODS[i];\n const spaBaseIdx = 2 + i * 2; // angles index offset for this method\n\n let methodFajr = spaData.angles[spaBaseIdx].sunrise;\n let methodIsha: number;\n\n if (m.useMSC) {\n // MSC: seasonal minutes from sunrise/sunset.\n const mscFajrMin = getMscFajr(date, lat);\n const mscIshaMin = getMscIsha(date, lat);\n methodFajr = isFinite(sunriseTime) ? sunriseTime - mscFajrMin / 60 : NaN;\n methodIsha = isFinite(maghribTime) ? maghribTime + mscIshaMin / 60 : NaN;\n } else if (m.ishaMinutes !== undefined) {\n // Fixed-minute Isha (UAQ = 90 min, Qatar = 90 min after sunset).\n methodIsha = isFinite(maghribTime) ? maghribTime + m.ishaMinutes / 60 : NaN;\n } else {\n methodIsha = spaData.angles[spaBaseIdx + 1].sunset;\n }\n\n Methods[m.id] = [methodFajr, methodIsha];\n }\n\n return {\n Qiyam: isFinite(qiyamTime) ? qiyamTime : NaN,\n Fajr: isFinite(fajrTime) ? fajrTime : NaN,\n Sunrise: isFinite(sunriseTime) ? sunriseTime : NaN,\n Noon: isFinite(noonTime) ? noonTime : NaN,\n Dhuhr: isFinite(dhuhrTime) ? dhuhrTime : NaN,\n Asr: isFinite(asrTime) ? asrTime : NaN,\n Maghrib: isFinite(maghribTime) ? maghribTime : NaN,\n Isha: isFinite(ishaTime) ? ishaTime : NaN,\n Midnight: isFinite(midnightTime) ? midnightTime : NaN,\n Methods,\n angles: { fajrAngle, ishaAngle },\n };\n}\n\n/** Exported method list for documentation and tooling use. */\nexport { METHODS };\n","/**\n * Formatted prayer times — dynamic method plus all traditional method comparisons.\n */\n\nimport { formatTime } from 'nrel-spa';\nimport { getTimesAll } from './getTimesAll.js';\nimport type { FormattedPrayerTimesAll } from './types.js';\n\n/**\n * Compute prayer times formatted as HH:MM:SS strings, plus comparison times\n * for every supported traditional method.\n *\n * Uses the dynamic twilight angle algorithm for the primary times. See\n * getTimesAll() for full parameter documentation.\n *\n * @returns All prayer times as HH:MM:SS strings. \"N/A\" for unreachable events.\n * Methods map contains [fajrString, ishaString] per method.\n */\nexport function calcTimesAll(\n date: Date,\n lat: number,\n lng: number,\n tz: number = -date.getTimezoneOffset() / 60,\n elevation = 0,\n temperature = 15,\n pressure = 1013.25,\n hanafi = false,\n): FormattedPrayerTimesAll {\n const raw = getTimesAll(date, lat, lng, tz, elevation, temperature, pressure, hanafi);\n\n const Methods: Record<string, [string, string]> = {};\n for (const [id, [fajr, isha]] of Object.entries(raw.Methods)) {\n Methods[id] = [formatTime(fajr), formatTime(isha)];\n }\n\n return {\n Qiyam: formatTime(raw.Qiyam),\n Fajr: formatTime(raw.Fajr),\n Sunrise: formatTime(raw.Sunrise),\n Noon: formatTime(raw.Noon),\n Dhuhr: formatTime(raw.Dhuhr),\n Asr: formatTime(raw.Asr),\n Maghrib: formatTime(raw.Maghrib),\n Isha: formatTime(raw.Isha),\n Midnight: formatTime(raw.Midnight),\n angles: raw.angles,\n Methods,\n };\n}\n"],"mappings":"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