mirror of
https://github.com/acamarata/pray-calc.git
synced 2026-07-02 03:40:39 +00:00
Major update for core calculation algorithm and bug fixes
This commit is contained in:
parent
269cb3bb11
commit
9e0428a6d9
6 changed files with 143 additions and 115 deletions
|
|
@ -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
|
- Updated to use the new "nrel-spa" v1.3.0
|
||||||
|
|
||||||
## [1.6.1] - 2025-05-04
|
## [1.6.1] - 2025-05-04
|
||||||
### Fixed
|
|
||||||
- Fixed missing modules and types definitions lost in last update
|
- Fixed missing modules and types definitions lost in last update
|
||||||
- Locked `suncalc` dependency to `^1.9.0`
|
- Locked `suncalc` dependency to `^1.9.0`
|
||||||
- Clarified scripts: `build`, `test`, and `prepublishOnly` in `package.json`
|
- Clarified scripts: `build`, `test`, and `prepublishOnly` in `package.json`
|
||||||
|
|
||||||
## [1.6.2] - 2025-05-04
|
## [1.6.2] - 2025-05-04
|
||||||
### Fixed
|
- Fixed Package issues
|
||||||
- Package issues
|
|
||||||
|
## [1.7.0] = 2025-05-04
|
||||||
|
- Major update to main algorithm
|
||||||
|
- Fixes to syntax and bugs
|
||||||
18
README.md
18
README.md
|
|
@ -1,15 +1,21 @@
|
||||||
|
|
||||||
# pray-calc
|
# 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
|
## Installation
|
||||||
|
|
||||||
|
|
@ -75,4 +81,4 @@ Contributions are welcome!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
91
getAngles.js
91
getAngles.js
|
|
@ -4,60 +4,75 @@
|
||||||
const PI = Math.PI;
|
const PI = Math.PI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate dynamic Fajr and Isha depression angles based on the current date (season)
|
* Calculate dynamic Fajr and Isha depression angles based on latitude, date (season),
|
||||||
* plus refraction and elevation adjustments.
|
* with adjustments for refraction and observer elevation.
|
||||||
*
|
*
|
||||||
* @param {number} [elevation=0] Observer elevation in meters.
|
* @param {Date} date Calculation date
|
||||||
* @param {number} [pressure=1013.25] Atmospheric pressure in mbar.
|
* @param {number} lat Latitude in degrees
|
||||||
* @param {number} [temperature=15] Temperature in °C.
|
* @param {number} lng Longitude (currently unused but kept for compatibility)
|
||||||
* @returns {{ fajrAngle: number, ishaAngle: number }} Depression angles in degrees.
|
* @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) {
|
function getAngles(date, lat, lng, elevation = 0, temperature = 15, pressure = 1013.25) {
|
||||||
// 1) Compute day of year (1–365/366)
|
// 1) Compute day of year
|
||||||
const today = new Date();
|
const startOfYear = Date.UTC(date.getUTCFullYear(), 0, 0);
|
||||||
const start = Date.UTC(today.getFullYear(), 0, 0);
|
const dayOfYear = Math.floor((date - startOfYear) / 86400000);
|
||||||
const diffMs = today - start;
|
|
||||||
const dayOfYear = Math.floor(diffMs / 86400000);
|
|
||||||
|
|
||||||
// 2) Approximate solar declination δ (radians):
|
// 2) Latitude factor (normalized from 0 at equator to 1 at 55° latitude)
|
||||||
// δ = 23.44° * sin(2π * (day + 284) / 365)
|
const latitudeFactor = Math.min(Math.abs(lat) / 55, 1);
|
||||||
const declRad = (23.44 * Math.sin(2 * PI * (dayOfYear + 284) / 365)) * (PI / 180);
|
|
||||||
|
|
||||||
// 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);
|
const seasonalFactor = Math.sin(declRad);
|
||||||
|
|
||||||
// 4) Base and amplitude for twilight angle (±2° variation)
|
// 5) Twilight angle calculation
|
||||||
const baseAngle = 18.0;
|
const baseAngle = 18.0;
|
||||||
const amplitude = 2.0;
|
const latitudeAdjustment = 4.0 * latitudeFactor; // varies up to ±4°
|
||||||
const seasonalAngle = baseAngle + amplitude * seasonalFactor;
|
const seasonalAdjustment = 1.5 * seasonalFactor; // varies up to ±1.5°
|
||||||
|
|
||||||
// 5) Refraction correction at horizon (altitude = 0)
|
const dynamicAngle = baseAngle - latitudeAdjustment - seasonalAdjustment;
|
||||||
const refraction = calculateAtmosphericRefraction(0, pressure, temperature);
|
|
||||||
|
|
||||||
// 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;
|
const elevationAdjustment = (elevation / 1000) * 0.1;
|
||||||
|
|
||||||
// 7) Final angles
|
// 8) Final adjusted angles
|
||||||
const fajrAngle = seasonalAngle + refraction + elevationAdjustment;
|
const fajrAngle = dynamicAngle + refraction + elevationAdjustment;
|
||||||
const ishaAngle = seasonalAngle - 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} altitude - Solar altitude in degrees
|
||||||
* @param {number} pressure Atmospheric pressure in mbar.
|
* @param {number} pressure - Atmospheric pressure in mbar
|
||||||
* @param {number} temperature Temperature in °C.
|
* @param {number} temperature - Temperature in °C
|
||||||
* @returns {number} Refraction correction in degrees.
|
* @returns {number} Refraction correction in degrees
|
||||||
*/
|
*/
|
||||||
function calculateAtmosphericRefraction(altitude, pressure = 1013.25, temperature = 10) {
|
function calculateAtmosphericRefraction(altitude, pressure = 1013.25, temperature = 15) {
|
||||||
const altRad = altitude * PI / 180;
|
if (altitude >= 0) altitude = -0.1; // ensure negative for twilight
|
||||||
let R = 1.0 / Math.tan(altRad + 7.31 / (altRad + 0.077));
|
const altRad = altitude * (PI / 180);
|
||||||
R = R / 60; // arcminutes → degrees
|
const R = (pressure / 1010) * (283 / (273 + temperature)) * (1.02 / Math.tan(altRad + 10.3 / (altRad + 5.11))) / 60;
|
||||||
R = (pressure / 1010) * (283 / (273 + temperature)) * R;
|
return Math.abs(R); // positive correction
|
||||||
return R;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getAngles };
|
/**
|
||||||
|
* 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 };
|
||||||
|
|
|
||||||
98
getTimes.js
98
getTimes.js
|
|
@ -1,51 +1,71 @@
|
||||||
|
// getTimes.js
|
||||||
const { getSpa } = require('nrel-spa');
|
const { getSpa } = require('nrel-spa');
|
||||||
const { getAngles } = require('./getAngles');
|
const { getAngles } = require('./getAngles');
|
||||||
const { getAsr } = require('./getAsr');
|
const { getAsr } = require('./getAsr');
|
||||||
const { getQiyam } = require('./getQiyam');
|
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
|
* Compute all prayer times for a given date and location.
|
||||||
const { fajrAngle, ishaAngle } = getAngles(elevation, pressure, temperature);
|
*
|
||||||
|
* @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
|
// 2️⃣ Get core SPA output with custom angles
|
||||||
const spaParams = { elevation, temperature, pressure };
|
const spaParams = { elevation, temperature, pressure };
|
||||||
const spaData = getSpa(date, lat, lng, tz, spaParams, [fajrAngle+90, ishaAngle+90]);
|
const spaData = getSpa(date, lat, lng, tz, spaParams, [fajrAngle + 90, ishaAngle + 90]);
|
||||||
|
|
||||||
// Organize prayer times
|
// Basic prayer times (fractional hours)
|
||||||
const fajrTime = spaData.angles[0].sunrise; // Lower time from custom angle
|
const fajrTime = spaData.angles[0].sunrise;
|
||||||
const sunriseTime = spaData.sunrise;
|
const sunriseTime = spaData.sunrise;
|
||||||
const noonTime = spaData.solarNoon;
|
const noonTime = spaData.solarNoon;
|
||||||
const dhuhrTime = spaData.solarNoon + ((1/60) * 2.5); // 2.5 minutes as a fraction of an hour
|
const dhuhrTime = spaData.solarNoon + (2.5 / 60);
|
||||||
const maghribTime = spaData.sunset;
|
const maghribTime = spaData.sunset;
|
||||||
const ishaTime = spaData.angles[1].sunset; // Higher time from custom angle
|
const ishaTime = spaData.angles[1].sunset;
|
||||||
|
|
||||||
// Step 3: Calculate Asr time
|
// 3️⃣ Calculate Asr (fractional hours)
|
||||||
const solarNoonHours = Math.floor(spaData.solarNoon);
|
// Build a Date for solar noon in local time
|
||||||
const solarNoonMinutes = Math.floor((spaData.solarNoon - solarNoonHours) * 60);
|
const hn = Math.floor(noonTime);
|
||||||
const solarNoonSeconds = Math.floor((spaData.solarNoon * 3600) - (solarNoonHours * 3600) - (solarNoonMinutes * 60));
|
const mn = Math.floor((noonTime - hn) * 60);
|
||||||
const solarNoonDate = new Date(date);
|
const sn = Math.floor(noonTime * 3600 - hn * 3600 - mn * 60);
|
||||||
solarNoonDate.setHours(solarNoonHours, solarNoonMinutes, solarNoonSeconds);
|
const solarNoonDate = new Date(date);
|
||||||
const asrPrayerTime = getAsr(solarNoonDate, lat, lng, tz, standard);
|
solarNoonDate.setHours(hn, mn, sn, 0);
|
||||||
|
const asrTime = getAsr(solarNoonDate, lat, lng, tz, standard);
|
||||||
// Step 4: Calculate Qiyam time
|
|
||||||
const qiyamTime = getQiyam(fajrTime, ishaTime);
|
|
||||||
|
|
||||||
// Final prayer times object
|
// 4️⃣ Calculate Qiyam (last third of the night)
|
||||||
const prayerTimes = {
|
const qiyamTime = getQiyam(fajrTime, ishaTime);
|
||||||
Qiyam: qiyamTime,
|
|
||||||
Fajr: fajrTime,
|
|
||||||
Sunrise: sunriseTime,
|
|
||||||
Noon: noonTime,
|
|
||||||
Dhuhr: dhuhrTime,
|
|
||||||
Asr: asrPrayerTime,
|
|
||||||
Maghrib: maghribTime,
|
|
||||||
Isha: ishaTime,
|
|
||||||
Angles: [fajrAngle, ishaAngle]
|
|
||||||
};
|
|
||||||
|
|
||||||
return prayerTimes;
|
return {
|
||||||
|
Qiyam: qiyamTime,
|
||||||
|
Fajr: fajrTime,
|
||||||
|
Sunrise: sunriseTime,
|
||||||
|
Noon: noonTime,
|
||||||
|
Dhuhr: dhuhrTime,
|
||||||
|
Asr: asrTime,
|
||||||
|
Maghrib: maghribTime,
|
||||||
|
Isha: ishaTime,
|
||||||
|
Angles: [fajrAngle, ishaAngle]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = { getTimes };
|
||||||
getTimes
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const methods = [
|
||||||
|
|
||||||
function getTimesAll(date, lat, lng, tz, elevation = 50, temperature = 15, pressure = 1013.25, standard = true) {
|
function getTimesAll(date, lat, lng, tz, elevation = 50, temperature = 15, pressure = 1013.25, standard = true) {
|
||||||
// Step 1: Get the custom angles
|
// 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]);
|
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
|
// Step 2: Get SPA data with custom angle for Fajr/Isha and other methods
|
||||||
|
|
|
||||||
41
test.js
41
test.js
|
|
@ -1,33 +1,18 @@
|
||||||
|
// test.js
|
||||||
const { getTimes, calcTimesAll } = require('./index');
|
const { getTimes, calcTimesAll } = require('./index');
|
||||||
|
|
||||||
// Manually setting the date to January 1, 2024
|
// Use: Today's date in NY
|
||||||
//const date = new Date('2024-01-01T00:00:00Z');
|
|
||||||
const date = new Date();
|
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 min = getTimes(date, lat, lng); // use minimal paramter input
|
||||||
const city = "New York"
|
const full = calcTimesAll(date, lat, lng, tzOffset); // full params
|
||||||
const lat = 40.7128;
|
|
||||||
const lng = -74.006;
|
|
||||||
const tz = -4
|
|
||||||
const elevation = null
|
|
||||||
const temperature = null
|
|
||||||
const pressure = null
|
|
||||||
|
|
||||||
/* Jakarta - all params
|
// Output
|
||||||
const city = "Jakarta"
|
console.log(`\nTest: ${city} on ${date.toLocaleString('en-US', { timeZone: 'America/New_York' })}\n`);
|
||||||
const lat = -6.2088
|
console.log("getTimes =", min, "\n");
|
||||||
const lng = 106.8456
|
console.log("calcTimesAll =", full, "\n");
|
||||||
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");
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue