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.

534 lines (520 loc) 18.9 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ANGLE_MAX: () => ANGLE_MAX, ANGLE_MIN: () => ANGLE_MIN, DHUHR_OFFSET_MINUTES: () => DHUHR_OFFSET_MINUTES, METHODS: () => METHODS, calcTimes: () => calcTimes, calcTimesAll: () => calcTimesAll, getAngles: () => getAngles, getAsr: () => getAsr, getMidnight: () => getMidnight, getMscFajr: () => getMscFajr, getMscIsha: () => getMscIsha, getQiyam: () => getQiyam, getTimes: () => getTimes, getTimesAll: () => getTimesAll, solarEphemeris: () => solarEphemeris, toJulianDate: () => toJulianDate }); module.exports = __toCommonJS(index_exports); // src/getTimes.ts var import_nrel_spa = require("nrel-spa"); // src/constants.ts var DEG = Math.PI / 180; var DHUHR_OFFSET_MINUTES = 2.5; var ANGLE_MIN = 10; var ANGLE_MAX = 22; // src/getSolarEphemeris.ts function toJulianDate(date) { return date.getTime() / 864e5 + 24405875e-1; } function solarEphemeris(jd) { const T = (jd - 2451545) / 36525; const L0 = ((280.46646 + 36000.76983 * T + 3032e-7 * T * T) % 360 + 360) % 360; const M = ((357.52911 + 35999.05029 * T - 1537e-7 * T * T) % 360 + 360) % 360; const Mrad = M * DEG; const e = 0.016708634 - 42037e-9 * T - 1267e-10 * T * T; const C = (1.914602 - 4817e-6 * T - 14e-6 * T * T) * Math.sin(Mrad) + (0.019993 - 101e-6 * T) * Math.sin(2 * Mrad) + 289e-6 * Math.sin(3 * Mrad); const sunLon = L0 + C; const nu = M + C; const nuRad = nu * DEG; const r = 1.000001018 * (1 - e * e) / (1 + e * Math.cos(nuRad)); const Omega = ((125.04 - 1934.136 * T) % 360 + 360) % 360; const OmegaRad = Omega * DEG; const lambda = sunLon - 569e-5 - 478e-5 * Math.sin(OmegaRad); const lambdaRad = lambda * DEG; const epsilon0 = 23.439291 - 0.013004 * T - 1638e-10 * T * T + 5036e-10 * T * T * T; const epsilon = (epsilon0 + 256e-5 * Math.cos(OmegaRad)) * DEG; const sinDecl = Math.sin(epsilon) * Math.sin(lambdaRad); const decl = Math.asin(Math.max(-1, Math.min(1, sinDecl))) / DEG; const eclLon = (lambdaRad % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI); return { decl, r, eclLon }; } function atmosphericRefraction(altitudeDeg, pressureMbar = 1013.25, temperatureC = 15) { if (altitudeDeg < -1) return 0; const R0 = 1.02 / Math.tan((altitudeDeg + 10.3 / (altitudeDeg + 5.11)) * DEG); const R = R0 * (pressureMbar / 1010) * (283 / (273 + temperatureC)); return Math.max(0, R) / 60; } // src/getMSC.ts var LAT_SCALE = 55; function isLeapYear(year) { return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; } function computeDyy(date, latitude) { const year = date.getFullYear(); const daysInYear = isLeapYear(year) ? 366 : 365; const refMonth = latitude >= 0 ? 11 : 5; const refDay = 21; const zeroDate = new Date(year, refMonth, refDay); let diffDays = Math.floor( (Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(zeroDate.getFullYear(), zeroDate.getMonth(), zeroDate.getDate())) / 864e5 ); if (diffDays < 0) diffDays += daysInYear; return { dyy: diffDays, daysInYear }; } function interpolateSegment(dyy, daysInYear, a, b, c, d) { if (dyy < 91) { return a + (b - a) / 91 * dyy; } else if (dyy < 137) { return b + (c - b) / 46 * (dyy - 91); } else if (dyy < 183) { return c + (d - c) / 46 * (dyy - 137); } else if (dyy < 229) { return d + (c - d) / 46 * (dyy - 183); } else if (dyy < 275) { return c + (b - c) / 46 * (dyy - 229); } else { const len = daysInYear - 275; return b + (a - b) / len * (dyy - 275); } } function getMscFajr(date, latitude) { const latAbs = Math.abs(latitude); const { dyy, daysInYear } = computeDyy(date, latitude); const a = 75 + 28.65 / LAT_SCALE * latAbs; const b = 75 + 19.44 / LAT_SCALE * latAbs; const c = 75 + 32.74 / LAT_SCALE * latAbs; const d = 75 + 48.1 / LAT_SCALE * latAbs; return Math.round(interpolateSegment(dyy, daysInYear, a, b, c, d)); } function getMscIsha(date, latitude, shafaq = "general") { const latAbs = Math.abs(latitude); const { dyy, daysInYear } = computeDyy(date, latitude); let a, b, c, d; switch (shafaq) { case "ahmer": a = 62 + 17.4 / LAT_SCALE * latAbs; b = 62 - 7.16 / LAT_SCALE * latAbs; c = 62 + 5.12 / LAT_SCALE * latAbs; d = 62 + 19.44 / LAT_SCALE * latAbs; break; case "abyad": a = 75 + 25.6 / LAT_SCALE * latAbs; b = 75 + 7.16 / LAT_SCALE * latAbs; c = 75 + 36.84 / LAT_SCALE * latAbs; d = 75 + 81.84 / LAT_SCALE * latAbs; break; default: a = 75 + 25.6 / LAT_SCALE * latAbs; b = 75 + 2.05 / LAT_SCALE * latAbs; c = 75 - 9.21 / LAT_SCALE * latAbs; d = 75 + 6.14 / LAT_SCALE * latAbs; } return Math.round(interpolateSegment(dyy, daysInYear, a, b, c, d)); } function minutesToDepression(minutes, latDeg, declDeg) { const phi = latDeg * (Math.PI / 180); const delta = declDeg * (Math.PI / 180); const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); const cosDelta = Math.cos(delta); const sinDelta = Math.sin(delta); const h0 = -0.833 * (Math.PI / 180); const sinH0 = Math.sin(h0); const denominator = cosPhi * cosDelta; if (Math.abs(denominator) < 1e-10) return NaN; const cosH_rise = (sinH0 - sinPhi * sinDelta) / denominator; if (cosH_rise < -1) return NaN; if (cosH_rise > 1) return NaN; const H_rise = Math.acos(cosH_rise); const deltaH = minutes / 60 * 15 * (Math.PI / 180); const H_prayer = H_rise + deltaH; if (H_prayer > Math.PI) { const sinH_midnight = sinPhi * sinDelta + cosPhi * cosDelta * Math.cos(Math.PI); const h_midnight = Math.asin(Math.max(-1, Math.min(1, sinH_midnight))); return -h_midnight / (Math.PI / 180); } const sinH_prayer = sinPhi * sinDelta + cosPhi * cosDelta * Math.cos(H_prayer); const h_prayer = Math.asin(Math.max(-1, Math.min(1, sinH_prayer))); return -h_prayer / (Math.PI / 180); } // src/getAngles.ts function clip(value, min, max) { return Math.max(min, Math.min(max, value)); } function round3(value) { return Math.round(value * 1e3) / 1e3; } function earthSunDistanceCorrection(r) { return -0.5 * Math.log(r); } function fourierSmoothingCorrection(eclLon, latAbsDeg) { const theta = eclLon; const phi = latAbsDeg * DEG; const a1 = 0.03 * Math.sin(theta); const b1 = -0.05 * Math.cos(theta); const a2 = 0.02 * Math.sin(2 * theta); const b2 = 0.02 * Math.cos(2 * theta); const c1 = -8e-3 * phi * Math.sin(theta); const d1 = 4e-3 * phi * Math.cos(theta); return a1 + b1 + a2 + b2 + c1 + d1; } function computeAngles(date, lat, lng, elevation = 0, temperature = 15, pressure = 1013.25) { const noonDate = new Date( Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0) ); const jd = toJulianDate(noonDate); const { decl, r, eclLon } = solarEphemeris(jd); const mscFajrMin = getMscFajr(date, lat); const mscIshaMin = getMscIsha(date, lat); let fajrBase = minutesToDepression(mscFajrMin, lat, decl); let ishaBase = minutesToDepression(mscIshaMin, lat, decl); if (!isFinite(fajrBase) || isNaN(fajrBase)) fajrBase = 18; if (!isFinite(ishaBase) || isNaN(ishaBase)) ishaBase = 18; const rCorr = earthSunDistanceCorrection(r); const fourierCorr = fourierSmoothingCorrection(eclLon, Math.abs(lat)); const refrFajr = atmosphericRefraction(-(fajrBase + 0.5), pressure, temperature); const refrIsha = atmosphericRefraction(-(ishaBase + 0.5), pressure, temperature); const horizonDipDeg = 1.06 * Math.sqrt(elevation / 1e3); const elevCorr = horizonDipDeg * 0.3; const rawFajr = fajrBase + rCorr + fourierCorr + refrFajr + elevCorr; const rawIsha = ishaBase + rCorr + fourierCorr + refrIsha + elevCorr; const fajrAngle = round3(clip(rawFajr, ANGLE_MIN, ANGLE_MAX)); const ishaAngle = round3(clip(rawIsha, ANGLE_MIN, ANGLE_MAX)); return { fajrAngle, ishaAngle, decl }; } function getAngles(date, lat, lng, elevation = 0, temperature = 15, pressure = 1013.25) { const { fajrAngle, ishaAngle } = computeAngles(date, lat, lng, elevation, temperature, pressure); return { fajrAngle, ishaAngle }; } // src/getAsr.ts function getAsr(solarNoon, latitude, declination, hanafi = false) { const phi = latitude * DEG; const delta = declination * DEG; const shadowFactor = hanafi ? 2 : 1; const X = Math.abs(phi - delta); const tanA = 1 / (shadowFactor + Math.tan(X)); const sinA = tanA / Math.sqrt(1 + tanA * tanA); const cosH0 = (sinA - Math.sin(phi) * Math.sin(delta)) / (Math.cos(phi) * Math.cos(delta)); if (cosH0 < -1 || cosH0 > 1) return NaN; const H0h = Math.acos(cosH0) / DEG / 15; return solarNoon + H0h; } // src/getQiyam.ts function getQiyam(fajrTime, ishaTime) { const adjustedFajr = fajrTime < ishaTime ? fajrTime + 24 : fajrTime; const nightLength = adjustedFajr - ishaTime; const lastThirdStart = ishaTime + 2 * nightLength / 3; return lastThirdStart >= 24 ? lastThirdStart - 24 : lastThirdStart; } // src/getMidnight.ts function getMidnight(maghribTime, endTime) { const adjusted = endTime < maghribTime ? endTime + 24 : endTime; const mid = maghribTime + (adjusted - maghribTime) / 2; return mid >= 24 ? mid - 24 : mid; } // src/validate.ts function validateInputs(lat, lng, tz, elevation) { if (!Number.isFinite(lat) || lat < -90 || lat > 90) { throw new RangeError(`latitude must be between -90 and 90, got ${lat}`); } if (!Number.isFinite(lng) || lng < -180 || lng > 180) { throw new RangeError(`longitude must be between -180 and 180, got ${lng}`); } if (tz !== void 0 && (!Number.isFinite(tz) || tz < -14 || tz > 14)) { throw new RangeError(`timezone offset must be between -14 and 14, got ${tz}`); } if (elevation !== void 0 && (!Number.isFinite(elevation) || elevation < -500)) { throw new RangeError(`elevation must be >= -500m, got ${elevation}`); } } // src/getTimes.ts function getTimes(date, lat, lng, tz = -date.getTimezoneOffset() / 60, elevation = 0, temperature = 15, pressure = 1013.25, hanafi = false) { validateInputs(lat, lng, tz, elevation); const { fajrAngle, ishaAngle, decl } = computeAngles( date, lat, lng, elevation, temperature, pressure ); const fajrZenith = 90 + fajrAngle; const ishaZenith = 90 + ishaAngle; const spaOpts = { elevation, temperature, pressure }; const spaData = (0, import_nrel_spa.getSpa)(date, lat, lng, tz, spaOpts, [fajrZenith, ishaZenith]); const fajrTime = spaData.angles[0].sunrise; const sunriseTime = spaData.sunrise; const noonTime = spaData.solarNoon; const maghribTime = spaData.sunset; const ishaTime = spaData.angles[1].sunset; const dhuhrTime = noonTime + DHUHR_OFFSET_MINUTES / 60; const asrTime = getAsr(noonTime, lat, decl, hanafi); const qiyamTime = getQiyam(fajrTime, ishaTime); const midnightTime = getMidnight(maghribTime, fajrTime); return { Qiyam: isFinite(qiyamTime) ? qiyamTime : NaN, Fajr: isFinite(fajrTime) ? fajrTime : NaN, Sunrise: isFinite(sunriseTime) ? sunriseTime : NaN, Noon: isFinite(noonTime) ? noonTime : NaN, Dhuhr: isFinite(dhuhrTime) ? dhuhrTime : NaN, Asr: isFinite(asrTime) ? asrTime : NaN, Maghrib: isFinite(maghribTime) ? maghribTime : NaN, Isha: isFinite(ishaTime) ? ishaTime : NaN, Midnight: isFinite(midnightTime) ? midnightTime : NaN, angles: { fajrAngle, ishaAngle } }; } // src/calcTimes.ts var import_nrel_spa2 = require("nrel-spa"); function calcTimes(date, lat, lng, tz = -date.getTimezoneOffset() / 60, elevation = 0, temperature = 15, pressure = 1013.25, hanafi = false) { const raw = getTimes(date, lat, lng, tz, elevation, temperature, pressure, hanafi); return { Qiyam: (0, import_nrel_spa2.formatTime)(raw.Qiyam), Fajr: (0, import_nrel_spa2.formatTime)(raw.Fajr), Sunrise: (0, import_nrel_spa2.formatTime)(raw.Sunrise), Noon: (0, import_nrel_spa2.formatTime)(raw.Noon), Dhuhr: (0, import_nrel_spa2.formatTime)(raw.Dhuhr), Asr: (0, import_nrel_spa2.formatTime)(raw.Asr), Maghrib: (0, import_nrel_spa2.formatTime)(raw.Maghrib), Isha: (0, import_nrel_spa2.formatTime)(raw.Isha), Midnight: (0, import_nrel_spa2.formatTime)(raw.Midnight), angles: raw.angles }; } // src/getTimesAll.ts var import_nrel_spa3 = require("nrel-spa"); var METHODS = [ { id: "UOIF", name: "Union des Organisations Islamiques de France", region: "France", fajrAngle: 12, ishaAngle: 12 }, { id: "ISNACA", name: "IQNA / Islamic Council of North America", region: "Canada", fajrAngle: 13, ishaAngle: 13 }, { id: "ISNA", name: "FCNA / Islamic Society of North America", region: "US, UK, AU, NZ", fajrAngle: 15, ishaAngle: 15 }, { id: "SAMR", name: "Spiritual Administration of Muslims of Russia", region: "Russia", fajrAngle: 16, ishaAngle: 15 }, { id: "IGUT", name: "Institute of Geophysics, University of Tehran", region: "Iran", fajrAngle: 17.7, ishaAngle: 14 }, { id: "MWL", name: "Muslim World League", region: "Global", fajrAngle: 18, ishaAngle: 17 }, { id: "DIBT", name: "Diyanet \u0130\u015Fleri Ba\u015Fkanl\u0131\u011F\u0131, Turkey", region: "Turkey", fajrAngle: 18, ishaAngle: 17 }, { id: "Karachi", name: "University of Islamic Sciences, Karachi", region: "PK, BD, IN, AF", fajrAngle: 18, ishaAngle: 18 }, { id: "Kuwait", name: "Kuwait Ministry of Islamic Affairs", region: "Kuwait", fajrAngle: 18, ishaAngle: 17.5 }, { id: "UAQ", name: "Umm Al-Qura University, Makkah", region: "Saudi Arabia", fajrAngle: 18.5, ishaAngle: null, ishaMinutes: 90 }, { id: "Qatar", name: "Qatar / Gulf Standard", region: "Qatar, Gulf", fajrAngle: 18, ishaAngle: null, ishaMinutes: 90 }, { id: "Egypt", name: "Egyptian General Authority of Survey", region: "EG, SY, IQ, LB", fajrAngle: 19.5, ishaAngle: 17.5 }, { id: "MUIS", name: "Majlis Ugama Islam Singapura", region: "Singapore", fajrAngle: 20, ishaAngle: 18 }, { id: "MSC", name: "Moonsighting Committee Worldwide", region: "Global", fajrAngle: null, ishaAngle: null, useMSC: true } ]; function getTimesAll(date, lat, lng, tz = -date.getTimezoneOffset() / 60, elevation = 0, temperature = 15, pressure = 1013.25, hanafi = false) { validateInputs(lat, lng, tz, elevation); const { fajrAngle, ishaAngle, decl } = computeAngles( date, lat, lng, elevation, temperature, pressure ); const methodZeniths = []; for (const m of METHODS) { const fZ = m.fajrAngle !== null ? 90 + m.fajrAngle : 90 + 18; const iZ = m.ishaAngle !== null ? 90 + m.ishaAngle : 90 + 18; methodZeniths.push(fZ, iZ); } const allZeniths = [ 90 + fajrAngle, 90 + ishaAngle, ...methodZeniths ]; const spaOpts = { elevation, temperature, pressure }; const spaData = (0, import_nrel_spa3.getSpa)(date, lat, lng, tz, spaOpts, allZeniths); const fajrTime = spaData.angles[0].sunrise; const sunriseTime = spaData.sunrise; const noonTime = spaData.solarNoon; const maghribTime = spaData.sunset; const ishaTime = spaData.angles[1].sunset; const dhuhrTime = noonTime + DHUHR_OFFSET_MINUTES / 60; const asrTime = getAsr(noonTime, lat, decl, hanafi); const qiyamTime = getQiyam(fajrTime, ishaTime); const midnightTime = getMidnight(maghribTime, fajrTime); const Methods = {}; for (let i = 0; i < METHODS.length; i++) { const m = METHODS[i]; const spaBaseIdx = 2 + i * 2; let methodFajr = spaData.angles[spaBaseIdx].sunrise; let methodIsha; if (m.useMSC) { const mscFajrMin = getMscFajr(date, lat); const mscIshaMin = getMscIsha(date, lat); methodFajr = isFinite(sunriseTime) ? sunriseTime - mscFajrMin / 60 : NaN; methodIsha = isFinite(maghribTime) ? maghribTime + mscIshaMin / 60 : NaN; } else if (m.ishaMinutes !== void 0) { methodIsha = isFinite(maghribTime) ? maghribTime + m.ishaMinutes / 60 : NaN; } else { methodIsha = spaData.angles[spaBaseIdx + 1].sunset; } Methods[m.id] = [methodFajr, methodIsha]; } return { Qiyam: isFinite(qiyamTime) ? qiyamTime : NaN, Fajr: isFinite(fajrTime) ? fajrTime : NaN, Sunrise: isFinite(sunriseTime) ? sunriseTime : NaN, Noon: isFinite(noonTime) ? noonTime : NaN, Dhuhr: isFinite(dhuhrTime) ? dhuhrTime : NaN, Asr: isFinite(asrTime) ? asrTime : NaN, Maghrib: isFinite(maghribTime) ? maghribTime : NaN, Isha: isFinite(ishaTime) ? ishaTime : NaN, Midnight: isFinite(midnightTime) ? midnightTime : NaN, Methods, angles: { fajrAngle, ishaAngle } }; } // src/calcTimesAll.ts var import_nrel_spa4 = require("nrel-spa"); function calcTimesAll(date, lat, lng, tz = -date.getTimezoneOffset() / 60, elevation = 0, temperature = 15, pressure = 1013.25, hanafi = false) { const raw = getTimesAll(date, lat, lng, tz, elevation, temperature, pressure, hanafi); const Methods = {}; for (const [id, [fajr, isha]] of Object.entries(raw.Methods)) { Methods[id] = [(0, import_nrel_spa4.formatTime)(fajr), (0, import_nrel_spa4.formatTime)(isha)]; } return { Qiyam: (0, import_nrel_spa4.formatTime)(raw.Qiyam), Fajr: (0, import_nrel_spa4.formatTime)(raw.Fajr), Sunrise: (0, import_nrel_spa4.formatTime)(raw.Sunrise), Noon: (0, import_nrel_spa4.formatTime)(raw.Noon), Dhuhr: (0, import_nrel_spa4.formatTime)(raw.Dhuhr), Asr: (0, import_nrel_spa4.formatTime)(raw.Asr), Maghrib: (0, import_nrel_spa4.formatTime)(raw.Maghrib), Isha: (0, import_nrel_spa4.formatTime)(raw.Isha), Midnight: (0, import_nrel_spa4.formatTime)(raw.Midnight), angles: raw.angles, Methods }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ANGLE_MAX, ANGLE_MIN, DHUHR_OFFSET_MINUTES, METHODS, calcTimes, calcTimesAll, getAngles, getAsr, getMidnight, getMscFajr, getMscIsha, getQiyam, getTimes, getTimesAll, solarEphemeris, toJulianDate }); //# sourceMappingURL=index.cjs.map