From 9e0428a6d9dccd67695b9f3ba723b445f41d1cd7 Mon Sep 17 00:00:00 2001 From: acamarata Date: Sun, 4 May 2025 11:34:23 -0400 Subject: [PATCH] Major update for core calculation algorithm and bug fixes --- CHANGELOG.md | 8 +++-- README.md | 18 ++++++---- getAngles.js | 91 ++++++++++++++++++++++++++-------------------- getTimes.js | 98 ++++++++++++++++++++++++++++++-------------------- getTimesAll.js | 2 +- test.js | 41 +++++++-------------- 6 files changed, 143 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aea7db2..6880f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,11 +32,13 @@ All notable changes to this project will be documented in this file. - Updated to use the new "nrel-spa" v1.3.0 ## [1.6.1] - 2025-05-04 -### Fixed - Fixed missing modules and types definitions lost in last update - Locked `suncalc` dependency to `^1.9.0` - Clarified scripts: `build`, `test`, and `prepublishOnly` in `package.json` ## [1.6.2] - 2025-05-04 -### Fixed -- Package issues +- Fixed Package issues + +## [1.7.0] = 2025-05-04 +- Major update to main algorithm +- Fixes to syntax and bugs \ No newline at end of file diff --git a/README.md b/README.md index 791262f..ae9aaf8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,21 @@ # pray-calc -Prayer times calculator using nrel-spa and custom formula for Fajr and Isha angles (as well as traditional static angle methods in the All function). +Prayer times calculator using nrel-spa and custom formula for Fajr and Isha angles (as well as traditional static angle methods in the All function). -## Version 1.5 +See PrayCalc.com for a fully functional implementation of this NPM Package. -With the release of version 1.5, we have integrated the MoonSighting (MSC) method. The MSC method is unique in its dynamic adjustment for latitude and seasonal variations, and it is well-researched and widely respected. While our custom method is still in development, the MSC method’s integration marks a significant enhancement. +See PrayCalc.net for our Wiki which goes into depth about what prayer times are, their definitions in Islam, how they are calculated, the explanation of the available legacy prayer time methods (using problematic static angles), issues with these in practice, and some detailed research into how we developed our custom formula for Fajr and Isha angles that should* work for any location or date dynamically with a single always working formula. -Our MSC implementation will have some accuracy improvement though due to using nrel-spa as the base calculations over other the more common but less accurate suncalc package or others. With that said the MSC method is imported as is and does not account for elevation for angle perspective adjustment (major) nor temperature, pressure, and weather for the atmospheric refraction (minor) variables that we have included. +* This is still in Beta and actively being developed, inshaa' Allah. -With some work we can incorporate all improvements from our method along with MSC's logic as well as these additional variables and come to something that is more accurate and more dynamic than currently available online anywhere in future versions, inshaa' Allah. +## Version 1.7 + +With the release of version 1.6 and 1.7 today many improvements have been implemented. + +Firstly, the core nrel-spa npm package implemenation fixed a major bug that made times off by up to a few minutes. We originally dedicated to making the first JS implementation of this algorithm (NREL-SPA) because it is the golden standard and most accurate algorithm out there but we saw no open-source release (or even references to proprietary) anywhere. This leads to the assumption that none of the available prayer calculators used this and relied on ones like "suncalc" which could cause errors of up to a few minutes. + +Secondly, we completely remade our dynamic custom angle calculation that made it many folds more accurate. The old formula was a rough approximation offsetting from the base 18° while this new algorithm completely calculates the angle based on scientific formulations refined using ML/AI on real-world data to come to something more ready for the public to review and give feedback on, inshaa' Allah. ## Installation @@ -75,4 +81,4 @@ Contributions are welcome! ## License -MIT License +MIT License \ No newline at end of file diff --git a/getAngles.js b/getAngles.js index a645703..0a43641 100644 --- a/getAngles.js +++ b/getAngles.js @@ -4,60 +4,75 @@ const PI = Math.PI; /** - * Calculate dynamic Fajr and Isha depression angles based on the current date (season) - * plus refraction and elevation adjustments. + * Calculate dynamic Fajr and Isha depression angles based on latitude, date (season), + * with adjustments for refraction and observer elevation. * - * @param {number} [elevation=0] Observer elevation in meters. - * @param {number} [pressure=1013.25] Atmospheric pressure in mbar. - * @param {number} [temperature=15] Temperature in °C. - * @returns {{ fajrAngle: number, ishaAngle: number }} Depression angles in degrees. + * @param {Date} date Calculation date + * @param {number} lat Latitude in degrees + * @param {number} lng Longitude (currently unused but kept for compatibility) + * @param {number} [elevation=0] Observer elevation in meters + * @param {number} [temperature=15] Temperature in °C + * @param {number} [pressure=1013.25] Atmospheric pressure in mbar + * @returns {{ fajrAngle: number, ishaAngle: number }} Twilight angles in degrees */ -function getAngles(elevation = 0, pressure = 1013.25, temperature = 15) { - // 1) Compute day of year (1–365/366) - const today = new Date(); - const start = Date.UTC(today.getFullYear(), 0, 0); - const diffMs = today - start; - const dayOfYear = Math.floor(diffMs / 86400000); +function getAngles(date, lat, lng, elevation = 0, temperature = 15, pressure = 1013.25) { + // 1) Compute day of year + const startOfYear = Date.UTC(date.getUTCFullYear(), 0, 0); + const dayOfYear = Math.floor((date - startOfYear) / 86400000); - // 2) Approximate solar declination δ (radians): - // δ = 23.44° * sin(2π * (day + 284) / 365) - const declRad = (23.44 * Math.sin(2 * PI * (dayOfYear + 284) / 365)) * (PI / 180); + // 2) Latitude factor (normalized from 0 at equator to 1 at 55° latitude) + const latitudeFactor = Math.min(Math.abs(lat) / 55, 1); - // 3) Seasonal factor (−1 to +1) + // 3) Approximate solar declination δ (in radians) + const declRad = 23.44 * Math.sin((2 * PI * (dayOfYear + 284)) / 365) * (PI / 180); + + // 4) Seasonal factor (ranges -1 to +1) const seasonalFactor = Math.sin(declRad); - // 4) Base and amplitude for twilight angle (±2° variation) + // 5) Twilight angle calculation const baseAngle = 18.0; - const amplitude = 2.0; - const seasonalAngle = baseAngle + amplitude * seasonalFactor; + const latitudeAdjustment = 4.0 * latitudeFactor; // varies up to ±4° + const seasonalAdjustment = 1.5 * seasonalFactor; // varies up to ±1.5° - // 5) Refraction correction at horizon (altitude = 0) - const refraction = calculateAtmosphericRefraction(0, pressure, temperature); + const dynamicAngle = baseAngle - latitudeAdjustment - seasonalAdjustment; - // 6) Elevation adjustment (0.1° per 1000 m) + // 6) Calculate refraction (minimal at large angles, thus simplified) + const refraction = calculateAtmosphericRefraction(-dynamicAngle, pressure, temperature); + + // 7) Elevation adjustment (~0.1° per 1000m) const elevationAdjustment = (elevation / 1000) * 0.1; - // 7) Final angles - const fajrAngle = seasonalAngle + refraction + elevationAdjustment; - const ishaAngle = seasonalAngle - refraction - elevationAdjustment; + // 8) Final adjusted angles + const fajrAngle = dynamicAngle + refraction + elevationAdjustment; + const ishaAngle = dynamicAngle - refraction - elevationAdjustment; - return { fajrAngle, ishaAngle }; + return { fajrAngle: roundAngle(fajrAngle), ishaAngle: roundAngle(ishaAngle) }; } /** - * Compute atmospheric refraction correction at a given altitude. + * Compute atmospheric refraction correction. + * Simplified for twilight angles (negative altitude). * - * @param {number} altitude Solar altitude in degrees. - * @param {number} pressure Atmospheric pressure in mbar. - * @param {number} temperature Temperature in °C. - * @returns {number} Refraction correction in degrees. + * @param {number} altitude - Solar altitude in degrees + * @param {number} pressure - Atmospheric pressure in mbar + * @param {number} temperature - Temperature in °C + * @returns {number} Refraction correction in degrees */ -function calculateAtmosphericRefraction(altitude, pressure = 1013.25, temperature = 10) { - const altRad = altitude * PI / 180; - let R = 1.0 / Math.tan(altRad + 7.31 / (altRad + 0.077)); - R = R / 60; // arcminutes → degrees - R = (pressure / 1010) * (283 / (273 + temperature)) * R; - return R; +function calculateAtmosphericRefraction(altitude, pressure = 1013.25, temperature = 15) { + if (altitude >= 0) altitude = -0.1; // ensure negative for twilight + const altRad = altitude * (PI / 180); + const R = (pressure / 1010) * (283 / (273 + temperature)) * (1.02 / Math.tan(altRad + 10.3 / (altRad + 5.11))) / 60; + return Math.abs(R); // positive correction } -module.exports = { getAngles }; \ No newline at end of file +/** + * Helper to round angles to 3 decimal places. + * + * @param {number} angle + * @returns {number} + */ +function roundAngle(angle) { + return Math.round(angle * 1000) / 1000; +} + +module.exports = { getAngles }; diff --git a/getTimes.js b/getTimes.js index 11c302e..1cd0024 100644 --- a/getTimes.js +++ b/getTimes.js @@ -1,51 +1,71 @@ +// getTimes.js const { getSpa } = require('nrel-spa'); const { getAngles } = require('./getAngles'); const { getAsr } = require('./getAsr'); const { getQiyam } = require('./getQiyam'); -function getTimes(date, lat, lng, tz, elevation = 50, temperature = 15, pressure = 1013.25, standard = true) { - // Step 1: Get the custom angles - const { fajrAngle, ishaAngle } = getAngles(elevation, pressure, temperature); +/** + * Compute all prayer times for a given date and location. + * + * @param {Date} date - Local date for calculation + * @param {number} lat - Latitude in decimal degrees + * @param {number} lng - Longitude in decimal degrees + * @param {number} [tz] - Timezone offset from UTC in hours (default: derived from date) + * @param {number} [elevation] - Observer elevation in meters (default: 50) + * @param {number} [temperature]- Ambient temperature in °C (default: 15) + * @param {number} [pressure] - Atmospheric pressure in mbar (default: 1013.25) + * @param {boolean} [standard] - true=Shāfiʿī (shadow factor 1), false=Ḥanafī (factor 2) + * + * @returns {Object} prayer times (fractional hours for Fajr, Sunrise, Dhuhr, Asr, Maghrib, Isha), plus Qiyam and angles + */ +function getTimes( + date, + lat, + lng, + tz = -date.getTimezoneOffset() / 60, + elevation = 50, + temperature = 15, + pressure = 1013.25, + standard = true +) { + // 1️⃣ Compute Fajr/Isha angles + const { fajrAngle, ishaAngle } = getAngles(date, lat, lng, elevation, temperature, pressure); - // Step 2: Get SPA data with custom angle for Fajr/Isha - const spaParams = { elevation, temperature, pressure }; - const spaData = getSpa(date, lat, lng, tz, spaParams, [fajrAngle+90, ishaAngle+90]); + // 2️⃣ Get core SPA output with custom angles + const spaParams = { elevation, temperature, pressure }; + const spaData = getSpa(date, lat, lng, tz, spaParams, [fajrAngle + 90, ishaAngle + 90]); - // Organize prayer times - const fajrTime = spaData.angles[0].sunrise; // Lower time from custom angle - const sunriseTime = spaData.sunrise; - const noonTime = spaData.solarNoon; - const dhuhrTime = spaData.solarNoon + ((1/60) * 2.5); // 2.5 minutes as a fraction of an hour - const maghribTime = spaData.sunset; - const ishaTime = spaData.angles[1].sunset; // Higher time from custom angle + // Basic prayer times (fractional hours) + const fajrTime = spaData.angles[0].sunrise; + const sunriseTime = spaData.sunrise; + const noonTime = spaData.solarNoon; + const dhuhrTime = spaData.solarNoon + (2.5 / 60); + const maghribTime = spaData.sunset; + const ishaTime = spaData.angles[1].sunset; - // Step 3: Calculate Asr time - const solarNoonHours = Math.floor(spaData.solarNoon); - const solarNoonMinutes = Math.floor((spaData.solarNoon - solarNoonHours) * 60); - const solarNoonSeconds = Math.floor((spaData.solarNoon * 3600) - (solarNoonHours * 3600) - (solarNoonMinutes * 60)); - const solarNoonDate = new Date(date); - solarNoonDate.setHours(solarNoonHours, solarNoonMinutes, solarNoonSeconds); - const asrPrayerTime = getAsr(solarNoonDate, lat, lng, tz, standard); - - // Step 4: Calculate Qiyam time - const qiyamTime = getQiyam(fajrTime, ishaTime); + // 3️⃣ Calculate Asr (fractional hours) + // Build a Date for solar noon in local time + const hn = Math.floor(noonTime); + const mn = Math.floor((noonTime - hn) * 60); + const sn = Math.floor(noonTime * 3600 - hn * 3600 - mn * 60); + const solarNoonDate = new Date(date); + solarNoonDate.setHours(hn, mn, sn, 0); + const asrTime = getAsr(solarNoonDate, lat, lng, tz, standard); - // Final prayer times object - const prayerTimes = { - Qiyam: qiyamTime, - Fajr: fajrTime, - Sunrise: sunriseTime, - Noon: noonTime, - Dhuhr: dhuhrTime, - Asr: asrPrayerTime, - Maghrib: maghribTime, - Isha: ishaTime, - Angles: [fajrAngle, ishaAngle] - }; + // 4️⃣ Calculate Qiyam (last third of the night) + const qiyamTime = getQiyam(fajrTime, ishaTime); - return prayerTimes; + return { + Qiyam: qiyamTime, + Fajr: fajrTime, + Sunrise: sunriseTime, + Noon: noonTime, + Dhuhr: dhuhrTime, + Asr: asrTime, + Maghrib: maghribTime, + Isha: ishaTime, + Angles: [fajrAngle, ishaAngle] + }; } -module.exports = { - getTimes -}; +module.exports = { getTimes }; diff --git a/getTimesAll.js b/getTimesAll.js index 8d82e66..5b5da43 100644 --- a/getTimesAll.js +++ b/getTimesAll.js @@ -20,7 +20,7 @@ const methods = [ function getTimesAll(date, lat, lng, tz, elevation = 50, temperature = 15, pressure = 1013.25, standard = true) { // Step 1: Get the custom angles - const { fajrAngle, ishaAngle } = getAngles(elevation, pressure, temperature); + const { fajrAngle, ishaAngle } = getAngles(date, lat, lng, elevation, temperature, pressure); const methodAngles = methods.map(m => [m.f + 90, m.i + 90]); // Step 2: Get SPA data with custom angle for Fajr/Isha and other methods diff --git a/test.js b/test.js index 66aa78a..7abc68a 100644 --- a/test.js +++ b/test.js @@ -1,33 +1,18 @@ +// test.js const { getTimes, calcTimesAll } = require('./index'); -// Manually setting the date to January 1, 2024 -//const date = new Date('2024-01-01T00:00:00Z'); +// Use: Today's date in NY const date = new Date(); +const city = "New York"; +const lat = 40.7128; +const lng = -74.0060; +process.env.TZ = 'America/New_York'; +const tzOffset = -date.getTimezoneOffset() / 60; -// NYC - minimum params -const city = "New York" -const lat = 40.7128; -const lng = -74.006; -const tz = -4 -const elevation = null -const temperature = null -const pressure = null +const min = getTimes(date, lat, lng); // use minimal paramter input +const full = calcTimesAll(date, lat, lng, tzOffset); // full params -/* Jakarta - all params -const city = "Jakarta" -const lat = -6.2088 -const lng = 106.8456 -const tz = 7 -const elevation = 18 -const temperature = 26.56 -const pressure = 1017 -*/ - -// Get results -const get = getTimes(date, lat, lng); // minimal args -const calc = calcTimesAll(date, lat, lng, tz, elevation, temperature, pressure); - -// Print results -console.log(`\nTest: ${city} with current Date():\n`) -console.log("getTimes =", get, "\n"); -console.log("calcTimesAll =", calc, "\n"); +// Output +console.log(`\nTest: ${city} on ${date.toLocaleString('en-US', { timeZone: 'America/New_York' })}\n`); +console.log("getTimes =", min, "\n"); +console.log("calcTimesAll =", full, "\n");