diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..180d0ad --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +# Ignore Git history +.git +# Ignore local dev/test files +/tests/ +/bin/ +**/*.c +**/*.h +getMoonVisibility.js +getMSC.js +test*.js \ No newline at end of file diff --git a/getAngles.js b/getAngles.js index ea43792..a645703 100644 --- a/getAngles.js +++ b/getAngles.js @@ -1,27 +1,63 @@ +// getAngles.js +'use strict'; + +const PI = Math.PI; + +/** + * Calculate dynamic Fajr and Isha depression angles based on the current date (season) + * plus refraction and elevation adjustments. + * + * @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. + */ function getAngles(elevation = 0, pressure = 1013.25, temperature = 15) { - const baseAngle = 18; // Base angle for astronomical twilight + // 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); - // Calculate refraction adjusted angle at the horizon (altitude = 0) - const refraction = calculateAtmosphericRefraction(0, pressure, temperature); + // 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); - // Elevation adjustment (approximate) - const elevationAdjustment = elevation / 1000 * 0.1; // Rough estimate + // 3) Seasonal factor (−1 to +1) + const seasonalFactor = Math.sin(declRad); - // Adjust angles for Fajr and Isha - const fajrAngle = baseAngle + refraction + elevationAdjustment; - const ishaAngle = baseAngle - refraction - elevationAdjustment; + // 4) Base and amplitude for twilight angle (±2° variation) + const baseAngle = 18.0; + const amplitude = 2.0; + const seasonalAngle = baseAngle + amplitude * seasonalFactor; - return { fajrAngle, ishaAngle }; + // 5) Refraction correction at horizon (altitude = 0) + const refraction = calculateAtmosphericRefraction(0, pressure, temperature); + + // 6) Elevation adjustment (0.1° per 1000 m) + const elevationAdjustment = (elevation / 1000) * 0.1; + + // 7) Final angles + const fajrAngle = seasonalAngle + refraction + elevationAdjustment; + const ishaAngle = seasonalAngle - refraction - elevationAdjustment; + + return { fajrAngle, ishaAngle }; } +/** + * Compute atmospheric refraction correction at a given 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. + */ function calculateAtmosphericRefraction(altitude, pressure = 1013.25, temperature = 10) { - const altInRadians = altitude * Math.PI / 180; - let R = 1.0 / Math.tan(altInRadians + 7.31 / (altInRadians + 0.077)); - R = R / 60; // Convert from arcminutes to degrees - R = (pressure / 1010) * (283 / (273 + temperature)) * R; - return R; + 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; } -module.exports = { - getAngles -}; +module.exports = { getAngles }; \ No newline at end of file diff --git a/getAsr.js b/getAsr.js index 4aebc58..37076da 100644 --- a/getAsr.js +++ b/getAsr.js @@ -1,28 +1,66 @@ -const { getSpa, fractalTime } = require('nrel-spa'); +// getAsr.js +'use strict'; -function getAsr(solarNoonDate, latitude, longitude, tz, standard = true) { - const solarNoonAltitude = getSpa(solarNoonDate, latitude, longitude, tz).zenith; - const shadowLengthAtNoon = 1 / Math.tan((90 - solarNoonAltitude) * Math.PI / 180); +const { SpaData, spa_calculate, SPA_ZA_RTS } = require('nrel-spa/dist/spa'); - // For standard method: Shadow length = object height + shadow length at noon - // For Hanafi method: Shadow length = 2 * object height + shadow length at noon - const targetShadowLength = standard ? 1 + shadowLengthAtNoon : 2 + shadowLengthAtNoon; +/** + * Compute Asr time (fractional hours) for a given date and location. + * + * @param {Date} date - Local date/time for the calculation. + * @param {number} latitude - Observer latitude in decimal degrees. + * @param {number} longitude - Observer longitude in decimal degrees. + * @param {number} timezone - Timezone offset from UTC in hours (negative west). + * @param {boolean}[standard] - true for Shāfiʿī (shadow=1), false for Ḥanafī (shadow=2). + * @returns {number|null} Fractional‐hour Asr time (local), or null if unreachable. + */ +function getAsr(date, latitude, longitude, timezone, standard = true) { + // Load inputs into SPA struct + const data = new SpaData(); + data.year = date.getFullYear(); + data.month = date.getMonth() + 1; + data.day = date.getDate(); + data.hour = date.getHours(); + data.minute = date.getMinutes(); + data.second = date.getSeconds(); + data.delta_ut1 = 0.0; + data.delta_t = 67.0; + data.timezone = timezone; + data.longitude = longitude; + data.latitude = latitude; + data.elevation = 0.0; + data.pressure = 1013.0; + data.temperature = 15.0; + data.slope = 0.0; + data.azm_rotation = 0.0; + data.atmos_refract = 0.5667; + data.function = SPA_ZA_RTS; - // Increment time from noon to find Asr time - let asrTime; - for (let i = 0; i < 720; i++) { // Check next 12 hours - const testTime = new Date(solarNoonDate.getTime() + i * 60000); // Increment by one minute - const currentAltitude = getSpa(testTime, latitude, longitude, tz).zenith; - const currentShadowLength = 1 / Math.tan((90 - currentAltitude) * Math.PI / 180); - if (currentShadowLength >= targetShadowLength) { - asrTime = testTime; - break; - } - } + // Perform SPA calculation + if (spa_calculate(data) !== 0) return null; - return asrTime ? asrTime.getHours() + asrTime.getMinutes() / 60 + asrTime.getSeconds() / 3600 : null; + // Convert angles to radians + const φ = latitude * Math.PI / 180; + const δ = data.delta * Math.PI / 180; + const transit = data.suntransit; // fractional‐hour solar noon + + // Compute required solar elevation A for Asr: + const shadowFactor = standard ? 1 : 2; + const X = Math.abs(φ - δ); + const opp = 1; + const adj = shadowFactor + Math.tan(X); + const hyp = Math.hypot(opp, adj); + const sinA = opp / hyp; + + // Solve hour‐angle H0: cos(H0) = (sinA - sinφ·sinδ) / (cosφ·cosδ) + const cosH0 = (sinA - Math.sin(φ) * Math.sin(δ)) / + (Math.cos(φ) * Math.cos(δ)); + if (cosH0 < -1 || cosH0 > 1) return null; // sun never reaches A + + // Convert H0 (rad) to hours + const H0h = (Math.acos(cosH0) * 180 / Math.PI) / 15; + + // Asr time = solar noon + H0h + return transit + H0h; } -module.exports = { - getAsr -}; +module.exports = { getAsr }; \ No newline at end of file diff --git a/getMSC.js b/getMSC.js index bc3039b..c25481d 100644 --- a/getMSC.js +++ b/getMSC.js @@ -1,95 +1,125 @@ // getMSC.js +'use strict'; + +/** + * Base class for Moonsighting.com seasonal interpolation algorithm. + * Computes a day-of-year offset (dyy) from the nearest solstice and + * interpolates minutes for Fajr (before sunrise) or Isha (after sunset). + */ class PrayerTimes { - constructor(date, latitude) { - this.date = new Date(date); - this.latitude = latitude; - this.getDyy(); - } + constructor(date, latitude) { + this.date = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + this.latitude = latitude; + this.year = this.date.getFullYear(); + this.daysInYear = isLeapYear(this.year) ? 366 : 365; + this.computeDyy(); + } - getDyy() { - const year = this.date.getFullYear(); - const northDate = new Date(`${year}-12-21`); - const southDate = new Date(`${year}-06-21`); - const zeroDate = this.latitude > 0 ? northDate : southDate; - this.dyy = Math.floor((this.date - zeroDate) / (1000 * 60 * 60 * 24)); - if (this.dyy < 0) { - this.dyy += 365; - } - } + computeDyy() { + // Reference solstice: Dec 21 (Northern Hemisphere) or Jun 21 (Southern) + const northSolstice = new Date(this.year, 11, 21); + const southSolstice = new Date(this.year, 5, 21); + const zeroDate = this.latitude >= 0 ? northSolstice : southSolstice; - getMinutes() { - if (this.dyy < 91) - return this.a + ((this.b - this.a) / 91) * this.dyy; - if (this.dyy < 137) - return this.b + ((this.c - this.b) / 46) * (this.dyy - 91); - if (this.dyy < 183) - return this.c + ((this.d - this.c) / 46) * (this.dyy - 137); - if (this.dyy < 229) - return this.d + ((this.c - this.d) / 46) * (this.dyy - 183); - if (this.dyy < 275) - return this.c + ((this.b - this.c) / 46) * (this.dyy - 229); - return this.b + ((this.a - this.b) / 91) * (this.dyy - 275); + let diffDays = Math.floor((this.date - zeroDate) / 86400000); + if (diffDays < 0) diffDays += this.daysInYear; + this.dyy = diffDays; + } + + getMinutesSegment() { + const { a, b, c, d, dyy, daysInYear } = this; + + 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); } + } } +/** + * Fajr: returns minutes before sunrise. + */ class Fajr extends PrayerTimes { - constructor(date, latitude) { - super(date, latitude); - this.a = 75 + (28.65 / 55) * Math.abs(latitude); - this.b = 75 + (19.44 / 55) * Math.abs(latitude); - this.c = 75 + (32.74 / 55) * Math.abs(latitude); - this.d = 75 + (48.1 / 55) * Math.abs(latitude); - } + constructor(date, latitude) { + super(date, latitude); + const latAbs = Math.abs(latitude); + this.a = 75 + (28.65 / 55) * latAbs; + this.b = 75 + (19.44 / 55) * latAbs; + this.c = 75 + (32.74 / 55) * latAbs; + this.d = 75 + (48.10 / 55) * latAbs; + } - getMinutesBeforeSunrise() { - return Math.round(this.getMinutes()); - } + getMinutesBeforeSunrise() { + return Math.round(this.getMinutesSegment()); + } } +/** + * Isha: returns minutes after sunset. + */ class Isha extends PrayerTimes { - static SHAFAQ_AHMER = 'ahmer'; - static SHAFAQ_ABYAD = 'abyad'; - static SHAFAQ_GENERAL = 'general'; + static SHAFAQ_GENERAL = 'general'; + static SHAFAQ_AHMER = 'ahmer'; + static SHAFAQ_ABYAD = 'abyad'; - constructor(date, latitude, shafaq = Isha.SHAFAQ_GENERAL) { - super(date, latitude); - this.setShafaq(shafaq); + constructor(date, latitude, shafaq = Isha.SHAFAQ_GENERAL) { + super(date, latitude); + this.setShafaq(shafaq); + } + + setShafaq(shafaq) { + this.shafaq = shafaq; + const latAbs = Math.abs(this.latitude); + + switch (shafaq) { + case Isha.SHAFAQ_AHMER: + this.a = 62 + (17.4 / 55) * latAbs; + this.b = 62 - (7.16 / 55) * latAbs; + this.c = 62 + (5.12 / 55) * latAbs; + this.d = 62 + (19.44 / 55) * latAbs; + break; + + case Isha.SHAFAQ_ABYAD: + this.a = 75 + (25.6 / 55) * latAbs; + this.b = 75 + (7.16 / 55) * latAbs; + this.c = 75 + (36.84 / 55) * latAbs; + this.d = 75 + (81.84 / 55) * latAbs; + break; + + default: // general + this.a = 75 + (25.6 / 55) * latAbs; + this.b = 75 + (2.05 / 55) * latAbs; + this.c = 75 - (9.21 / 55) * latAbs; + this.d = 75 + (6.14 / 55) * latAbs; + break; } + } - setShafaq(shafaq) { - this.shafaq = shafaq; + getMinutesAfterSunset() { + return Math.round(this.getMinutesSegment()); + } +} - if (shafaq === Isha.SHAFAQ_AHMER) { - this.a = 62 + (17.4 / 55) * Math.abs(this.latitude); - this.b = 62 - (7.16 / 55) * Math.abs(this.latitude); - this.c = 62 + (5.12 / 55) * Math.abs(this.latitude); - this.d = 62 + (19.44 / 55) * Math.abs(this.latitude); - } else if (shafaq === Isha.SHAFAQ_ABYAD) { - this.a = 75 + (25.6 / 55) * Math.abs(this.latitude); - this.b = 75 + (7.16 / 55) * Math.abs(this.latitude); - this.c = 75 + (36.84 / 55) * Math.abs(this.latitude); - this.d = 75 + (81.84 / 55) * Math.abs(this.latitude); - } else { - this.a = 75 + (25.6 / 55) * Math.abs(this.latitude); - this.c = 75 - (9.21 / 55) * Math.abs(this.latitude); - this.b = 75 + (2.05 / 55) * Math.abs(this.latitude); - this.d = 75 + (6.14 / 55) * Math.abs(this.latitude); - } - } - - getMinutesAfterSunset() { - return Math.round(this.getMinutes()); - } +function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); } function getFajr(date, latitude) { - const fajr = new Fajr(date, latitude); - return fajr.getMinutesBeforeSunrise(); + return new Fajr(date, latitude).getMinutesBeforeSunrise(); } function getIsha(date, latitude, shafaq = Isha.SHAFAQ_GENERAL) { - const isha = new Isha(date, latitude, shafaq); - return isha.getMinutesAfterSunset(); + return new Isha(date, latitude, shafaq).getMinutesAfterSunset(); } module.exports = { getFajr, getIsha, Isha, Fajr }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2bf9567..b33d2da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,23 @@ { "name": "pray-calc", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pray-calc", - "version": "1.5.0", - "license": "ISC", + "version": "1.6.0", + "license": "MIT", "dependencies": { - "nrel-spa": "^1.2.2", + "nrel-spa": "^1.3.0", "suncalc": "^1.9.0" } }, "node_modules/nrel-spa": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/nrel-spa/-/nrel-spa-1.2.2.tgz", - "integrity": "sha512-2mycHd7PP0l+pPjPvrT6vr+PRHufCFOxsPcpaIOQ9GwhhfzgyTvSsLKPsqC6ZENNU65yq4PetT5XlWj3CoLjiQ==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/nrel-spa/-/nrel-spa-1.3.0.tgz", + "integrity": "sha512-vZPD4TjCxGQsrstyyZGtnJtU8y1bJghBki5MR9HbDyhGsO+mHM+l3ffI7l3rIjTJsJBoQP7gyDwQntiPy3vUHA==", + "license": "MIT" }, "node_modules/suncalc": { "version": "1.9.0", diff --git a/package.json b/package.json index bc93f1c..ee0a5db 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,43 @@ { "name": "pray-calc", - "version": "1.5.1", - "description": "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.6.0", + "description": "Prayer times calculator using nrel-spa and custom formula for Fajr and Isha angles (as well as traditional static angle methods)", "main": "index.js", "scripts": { "test": "node test.js" }, - "author": "Ummat", - "license": "MIT", "repository": { "type": "git", "url": "https://github.com/ummeco/pray-calc.git" }, - "homepage": "https://github.com/ummeco/pray-calc#readme", + "keywords": [ + "prayer", + "islam", + "fajr", + "dhuhr", + "asr", + "maghrib", + "isha", + "nrel-spa", + "solar-position" + ], + "author": "Ummat", + "license": "MIT", "bugs": { "url": "https://github.com/ummeco/pray-calc/issues" - } + }, + "homepage": "https://github.com/ummeco/pray-calc#readme", + "dependencies": { + "nrel-spa": "^1.3.0", + "suncalc": "^1.9.0" + }, + "files": [ + "index.js", + "getTimes.js", + "getAsr.js", + "getAngles.js", + "dist/", + "README.md", + "CHANGELOG.md" + ] } diff --git a/test.js b/test.js index a83e871..66aa78a 100644 --- a/test.js +++ b/test.js @@ -1,13 +1,14 @@ const { getTimes, calcTimesAll } = require('./index'); // Manually setting the date to January 1, 2024 -const date = new Date('2024-01-01T00:00:00Z'); +//const date = new Date('2024-01-01T00:00:00Z'); +const date = new Date(); // NYC - minimum params const city = "New York" const lat = 40.7128; const lng = -74.006; -const tz = -5 +const tz = -4 const elevation = null const temperature = null const pressure = null