From cf8aa20577f8de639cf13629c7e9d33d03aa27a2 Mon Sep 17 00:00:00 2001 From: Ali Camarata Date: Sun, 12 Nov 2023 11:19:56 +0700 Subject: [PATCH] initial commit --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 3 ++ LICENSE | 7 ++++ README.md | 45 ++++++++++++++++++++++ calcTimes.js | 37 +++++++++++++++++++ calcTimesAll.js | 40 ++++++++++++++++++++ getAngles.js | 27 ++++++++++++++ getAsr.js | 29 +++++++++++++++ getEarthSunDistance.js | 29 +++++++++++++++ getMoon.js | 61 ++++++++++++++++++++++++++++++ getQiyam.js | 17 +++++++++ getTimes.js | 51 +++++++++++++++++++++++++ getTimesAll.js | 82 +++++++++++++++++++++++++++++++++++++++++ index.js | 12 ++++++ methods.json | 72 ++++++++++++++++++++++++++++++++++++ package-lock.json | 21 +++++++++++ package.json | 15 ++++++++ test-year.js | 34 +++++++++++++++++ test.js | 13 +++++++ 19 files changed, 595 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 calcTimes.js create mode 100644 calcTimesAll.js create mode 100644 getAngles.js create mode 100644 getAsr.js create mode 100644 getEarthSunDistance.js create mode 100644 getMoon.js create mode 100644 getQiyam.js create mode 100644 getTimes.js create mode 100644 getTimesAll.js create mode 100644 index.js create mode 100644 methods.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 test-year.js create mode 100644 test.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 key !== "Angles") + .sort(([, a], [, b]) => a - b); + + // Apply fractalTime on all sorted entries (except "Angle") + let sortedAndFormatted = sortedEntries.reduce((acc, [key, value]) => { + acc[key] = fractalTime(value); + return acc; + }, {}); + + // Add the "Angle" at the end + sortedAndFormatted["Angles"] = result["Angles"]; + + return sortedAndFormatted; +} + +module.exports = { + calcTimes +}; diff --git a/calcTimesAll.js b/calcTimesAll.js new file mode 100644 index 0000000..ca155f4 --- /dev/null +++ b/calcTimesAll.js @@ -0,0 +1,40 @@ +const { fractalTime } = require('nrel-spa'); +const { getTimesAll } = require('./getTimesAll'); + +function calcTimesAll(date, lat, lng, elevation = 10, temperature = 15, pressure = 1013.25) { + let result = getTimesAll(date, lat, lng, elevation, temperature, pressure); + + // Sort the methods by Fajr time + let sortedMethods = Object.entries(result.Methods) + .sort((a, b) => a[1][0] - b[1][0]) + .reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}); + + // Format the times inside sorted Methods + Object.entries(sortedMethods).forEach(([methodName, times]) => { + sortedMethods[methodName] = times.map(time => fractalTime(time)); + }); + + // Sort and format the other prayer times, excluding "Angles" and "Methods" + let sortedEntries = Object.entries(result) + .filter(([key]) => key !== "Angles" && key !== "Methods") + .sort(([, a], [, b]) => a - b); + + // Apply fractalTime on all sorted entries + let sortedAndFormatted = sortedEntries.reduce((acc, [key, value]) => { + acc[key] = fractalTime(value); + return acc; + }, {}); + + // Add the formatted "Methods" and "Angles" to the result + sortedAndFormatted["Methods"] = sortedMethods; + sortedAndFormatted["Angles"] = result["Angles"]; + + return sortedAndFormatted; +} + +module.exports = { + calcTimesAll +}; diff --git a/getAngles.js b/getAngles.js new file mode 100644 index 0000000..ea43792 --- /dev/null +++ b/getAngles.js @@ -0,0 +1,27 @@ +function getAngles(elevation = 0, pressure = 1013.25, temperature = 15) { + const baseAngle = 18; // Base angle for astronomical twilight + + // Calculate refraction adjusted angle at the horizon (altitude = 0) + const refraction = calculateAtmosphericRefraction(0, pressure, temperature); + + // Elevation adjustment (approximate) + const elevationAdjustment = elevation / 1000 * 0.1; // Rough estimate + + // Adjust angles for Fajr and Isha + const fajrAngle = baseAngle + refraction + elevationAdjustment; + const ishaAngle = baseAngle - refraction - elevationAdjustment; + + return { fajrAngle, ishaAngle }; +} + +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; +} + +module.exports = { + getAngles +}; diff --git a/getAsr.js b/getAsr.js new file mode 100644 index 0000000..d3db1db --- /dev/null +++ b/getAsr.js @@ -0,0 +1,29 @@ +const { getSpa, fractalTime } = require('nrel-spa'); + +function getAsr(solarNoonDate, latitude, longitude, standard = true) { + const solarNoonAltitude = getSpa(solarNoonDate, latitude, longitude).zenith; + const shadowLengthAtNoon = 1 / Math.tan((90 - solarNoonAltitude) * Math.PI / 180); + + // 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; + + // 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).zenith; + const currentShadowLength = 1 / Math.tan((90 - currentAltitude) * Math.PI / 180); + + if (currentShadowLength >= targetShadowLength) { + asrTime = testTime; + break; + } + } + + return asrTime ? asrTime.getHours() + asrTime.getMinutes() / 60 + asrTime.getSeconds() / 3600 : null; +} + +module.exports = { + getAsr +}; diff --git a/getEarthSunDistance.js b/getEarthSunDistance.js new file mode 100644 index 0000000..490e208 --- /dev/null +++ b/getEarthSunDistance.js @@ -0,0 +1,29 @@ +function getEarthSunDistance(date) { + // Constants + const a = 149597870.7; // Semi-major axis of Earth's orbit in km + const e = 0.0167086; // Orbital eccentricity of Earth + + // Calculate the day of the year + const start = new Date(date.getFullYear(), 0, 0); + const diff = date - start; + const oneDay = 86400000; // Milliseconds in one day + const dayOfYear = Math.floor(diff / oneDay); + + // Approximate the mean anomaly + const g = 357.529 + 0.98560028 * dayOfYear; + + // Convert to radians + const gInRadians = g * Math.PI / 180; + + // Use the approximation for the true anomaly (v) + const v = gInRadians + (1.914 * Math.sin(gInRadians)); // in radians + + // Calculate the distance + const r = a * (1 - e * e) / (1 + e * Math.cos(v)); + + return r; +} + +module.exports = { + getEarthSunDistance +}; diff --git a/getMoon.js b/getMoon.js new file mode 100644 index 0000000..1f24bc6 --- /dev/null +++ b/getMoon.js @@ -0,0 +1,61 @@ +const { getEarthSunDistance } = require('./getEarthSunDistance'); + +function getMoon(date, accurate = false) { + const PI = Math.PI; + const rad = PI / 180; + const e = rad * 23.4397; // obliquity of the Earth + + function toDays(date) { + return (date - new Date(2000, 0, 1)) / 86400000; + } + + function rightAscension(l, b) { + return Math.atan2(Math.sin(l) * Math.cos(e) - Math.tan(b) * Math.sin(e), Math.cos(l)); + } + + function declination(l, b) { + return Math.asin(Math.sin(b) * Math.cos(e) + Math.cos(b) * Math.sin(e) * Math.sin(l)); + } + + function sunCoords(d) { + const M = rad * (357.5291 + 0.98560028 * d); + const L = rad * (280.1470 + 360.9856235 * d) + (1.9148 - 0.004817 * d / 36525) * Math.sin(M) + 0.019993 - 0.000101 * d / 36525 * Math.cos(M); + return { + dec: declination(L, 0), + ra: rightAscension(L, 0) + }; + } + + function moonCoords(d) { + const L = rad * (218.316 + 13.176396 * d); + const M = rad * (134.963 + 13.064993 * d); + const F = rad * (93.272 + 13.229350 * d); + + const l = L + rad * 6.289 * Math.sin(M); // Moon's mean longitude + const b = rad * 5.128 * Math.sin(F); // Moon's mean latitude + const dt = 385001 - 20905 * Math.cos(M); // Distance to the moon in km + + return { + ra: rightAscension(l, b), + dec: declination(l, b), + dist: dt + }; + } + + const d = toDays(date); + const s = sunCoords(d); + const m = moonCoords(d); + + // distance from Earth to Sun in km + const sdist = accurate ? getEarthSunDistance(date) : 149598000; + + const phi = Math.acos(Math.sin(s.dec) * Math.sin(m.dec) + Math.cos(s.dec) * Math.cos(m.dec) * Math.cos(s.ra - m.ra)); + const inc = Math.atan2(sdist * Math.sin(phi), m.dist - sdist * Math.cos(phi)); + const angle = Math.atan2(Math.cos(s.dec) * Math.sin(s.ra - m.ra), Math.sin(s.dec) * Math.cos(m.dec) - Math.cos(s.dec) * Math.sin(m.dec) * Math.cos(s.ra - m.ra)); + + return { + fraction: (1 + Math.cos(inc)) / 2, + phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI, + angle: angle + }; +} diff --git a/getQiyam.js b/getQiyam.js new file mode 100644 index 0000000..b72b669 --- /dev/null +++ b/getQiyam.js @@ -0,0 +1,17 @@ +function getQiyam(fajrTime, ishaTime) { + // Adjust Fajr time if it is earlier than Isha time + const adjustedFajrTime = fajrTime < ishaTime ? fajrTime + 24 : fajrTime; + + // Calculate the length of the night + const nightLength = adjustedFajrTime - ishaTime; + + // Calculate the start of the last third of the night + const lastThirdStart = ishaTime + (2 * nightLength / 3); + + // If the result is greater than 24, adjust it to get the correct time + return lastThirdStart > 24 ? lastThirdStart - 24 : lastThirdStart; +} + +module.exports = { + getQiyam +}; diff --git a/getTimes.js b/getTimes.js new file mode 100644 index 0000000..e166566 --- /dev/null +++ b/getTimes.js @@ -0,0 +1,51 @@ +const { getSpa } = require('nrel-spa'); +const { getAngles } = require('./getAngles'); +const { getAsr } = require('./getAsr'); +const { getQiyam } = require('./getQiyam'); + +function getTimes(date, lat, lng, elevation = 50, temperature = 15, pressure = 1013.25, standard = true) { + // Step 1: Get the custom angles + const { fajrAngle, ishaAngle } = getAngles(elevation, pressure, temperature); + + // Step 2: Get SPA data with custom angle for Fajr/Isha + const spaParams = { elevation, temperature, pressure }; + const spaData = getSpa(date, lat, lng, 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 + + // 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, standard); + + // Step 4: Calculate Qiyam time + const qiyamTime = getQiyam(fajrTime, ishaTime); + + // 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] + }; + + return prayerTimes; +} + +module.exports = { + getTimes +}; diff --git a/getTimesAll.js b/getTimesAll.js new file mode 100644 index 0000000..40f7261 --- /dev/null +++ b/getTimesAll.js @@ -0,0 +1,82 @@ +const { getSpa } = require('nrel-spa'); +const { getAngles } = require('./getAngles'); +const { getAsr } = require('./getAsr'); +const { getQiyam } = require('./getQiyam'); + +const methods = [ + {n:'UOIF', f:12, i:12, r:'France'}, + {n:'ISNACA', f:13, i:13, r:'Canada'}, + {n:'ISNAUS', f:15, i:15, r:'US, UK'}, + {n:'SAMR', f:16, i:15, r:'RU'}, + {n:'MWL', f:18, i:17, r:'EU, US, Asia'}, + {n:'DIBT', f:18, i:17, r:'TR'}, + {n:'Karachi', f:18, i:18, r:'PK, BD, IN, AF, EU'}, + {n:'UAQ', f:18.5, i:18, r:'SA'}, + {n:'Egypt', f:19.5, i:17.5, r:'Africa, SY, IQ, LB'}, + {n:'MUIS', f:20, i:18, r:'SG'}, +]; + +function getTimesAll(date, lat, lng, elevation = 50, temperature = 15, pressure = 1013.25, standard = true) { + // Step 1: Get the custom angles + const { fajrAngle, ishaAngle } = getAngles(elevation, pressure, temperature); + 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 + const spaParams = { elevation, temperature, pressure }; + const spaData = getSpa(date, lat, lng, spaParams, [fajrAngle + 90, ishaAngle + 90, ...methodAngles.flat()]); + + // Organize prayer times + const fajrTime = spaData.angles[0].sunrise; + const sunriseTime = spaData.sunrise; + const noonTime = spaData.solarNoon; + const dhuhrTime = spaData.solarNoon + ((1 / 60) * 2.5); + 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, standard); + + // Step 4: Calculate Qiyam time + const qiyamTime = getQiyam(fajrTime, ishaTime); + + // Final prayer times object + const prayerTimes = { + Qiyam: qiyamTime, + Fajr: fajrTime, + Sunrise: sunriseTime, + Noon: noonTime, + Dhuhr: dhuhrTime, + Asr: asrPrayerTime, + Maghrib: maghribTime, + Isha: ishaTime, + Methods: {}, + Angles: [ fajrAngle, ishaAngle ] + }; + + // Adding other methods + methods.forEach((method, index) => { + const fajrIndex = 2 + (index * 2); + const ishaIndex = 3 + (index * 2); + + let fajrMethodTime = spaData.angles[fajrIndex].sunrise; + let ishaMethodTime = spaData.angles[ishaIndex].sunset; + + // Adjusting Isha time for Umm Al-Qura method + if (method.n === 'UAQ') { + ishaMethodTime = spaData.sunset + ((1 / 60) * 90); + } + + prayerTimes.Methods[method.n] = [fajrMethodTime, ishaMethodTime]; + }); + + return prayerTimes; +} + +module.exports = { + getTimesAll +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..adf216b --- /dev/null +++ b/index.js @@ -0,0 +1,12 @@ +const { getTimes } = require('./getTimes'); +const { calcTimes } = require('./calcTimes'); +const { getTimesAll } = require('./getTimesAll'); +const { calcTimesAll } = require('./calcTimesAll'); + + +module.exports = { + getTimes, + calcTimes, + getTimesAll, + calcTimesAll +}; diff --git a/methods.json b/methods.json new file mode 100644 index 0000000..13cf08d --- /dev/null +++ b/methods.json @@ -0,0 +1,72 @@ +[ + { + "ID": "UOIF", + "Name": "Union des Organisations Islamiques de France", + "Fajr": "12°", + "Isha": "12°", + "Region": "FR" + }, + { + "ID": "ISNACA", + "Name": "Islamic Society of North America - Canada", + "Fajr": "13°", + "Isha": "13°", + "Region": "CA" + }, + { + "ID": "ISNAUS", + "Name": "Islamic Society of North America - US", + "Fajr": "15°", + "Isha": "15°", + "Region": "US, UK, AU, NZ" + }, + { + "ID": "SAMR", + "Name": "Spiritual Administration of Muslims of Russia", + "Fajr": "16°", + "Isha": "15°", + "Region": "RU" + }, + { + "ID": "MWL", + "Name": "Muslim World League", + "Fajr": "18°", + "Isha": "17°", + "Region": "Most common globally (default)" + }, + { + "ID": "DIBT", + "Name": "Diyanet İşleri Başkanlığı, Turkey", + "Fajr": "18°", + "Isha": "17°", + "Region": "TR" + }, + { + "ID": "Karachi", + "Name": "University of Islamic Sciences, Karachi", + "Fajr": "18°", + "Isha": "18°", + "Region": "PK, BD, IN, AF" + }, + { + "ID": "UAQ", + "Name": "Umm Al-Qura University, Makkah", + "Fajr": "18.5°", + "Isha": "90 minutes after sunset", + "Region": "SA (Gulf States)" + }, + { + "ID": "Egypt", + "Name": "Egyptian General Authority of Survey", + "Fajr": "19.5°", + "Isha": "17.5°", + "Region": "EG, SY, IQ, LB, surrounding African countries" + }, + { + "ID": "MUIS", + "Name": "Majlis Ugama Islam Singapura", + "Fajr": "20°", + "Isha": "18°", + "Region": "SG" + } +] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3e63d6e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "praycalc", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "praycalc", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "nrel-spa": "^1.1.0" + } + }, + "node_modules/nrel-spa": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nrel-spa/-/nrel-spa-1.1.0.tgz", + "integrity": "sha512-uoVKBDIj5vvAMEzRr2LSKXo/r9IcadE46CeBSepU47ssByoDZ6QFUsmzTUvg9DAggGbVTTCCEvM29bGUI4YYzA==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..83f807a --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ + +{ + "name": "praycalc", + "version": "1.0.0", + "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)", + "main": "index.js", + "scripts": { + "test": "node test.js" + }, + "author": "USF", + "license": "ISC", + "dependencies": { + "nrel-spa": "^1.1.0" + } +} diff --git a/test-year.js b/test-year.js new file mode 100644 index 0000000..4c15441 --- /dev/null +++ b/test-year.js @@ -0,0 +1,34 @@ +const { calcTimes } = require('./index'); + +function formatTime(timeString) { + return timeString.slice(0, -4); // Trims the last 4 characters (".427") +} + +function getNYCPrayerTimesForYear(year) { + const lat = 40.7128; // Latitude for New York City + const lng = -74.0060; // Longitude for New York City + const elevation = 10; // Average elevation for NYC in meters + const temperature = 15; // Average temperature for NYC in Celsius + const pressure = 1013.25; // Average atmospheric pressure in millibars + + let prayerTimesForYear = `NYC Prayer Times for ${year}:\n\n`; + + for (let month = 0; month < 12; month++) { + for (let day = 1; day <= new Date(year, month + 1, 0).getDate(); day++) { + const date = new Date(year, month, day); + const formattedDate = date.toDateString().substring(4, 10); // "Dec 31" + const prayerTimes = calcTimes(date, lat, lng, elevation, temperature, pressure); + + prayerTimesForYear += `${formattedDate} = `; + prayerTimesForYear += `${formatTime(prayerTimes.Fajr)} / `; + prayerTimesForYear += `${formatTime(prayerTimes.Dhuhr)} / `; + prayerTimesForYear += `${formatTime(prayerTimes.Asr)} / `; + prayerTimesForYear += `${formatTime(prayerTimes.Maghrib)} / `; + prayerTimesForYear += `${formatTime(prayerTimes.Isha)}\n`; + } + } + + return prayerTimesForYear; +} + +console.log(getNYCPrayerTimesForYear(2023)); diff --git a/test.js b/test.js new file mode 100644 index 0000000..83be6f8 --- /dev/null +++ b/test.js @@ -0,0 +1,13 @@ +const { calcTimesAll } = require('./index'); + +const lat = 40.7128; // Latitude for New York City +const lng = -74.0060; // Longitude for New York City +const elevation = 10; // Average elevation for NYC in meters +const temperature = 15; // Average temperature for NYC in Celsius +const pressure = 1013.25; // Average atmospheric pressure in millibars + +const date = new Date(); +const prayerTimes = calcTimesAll(date, lat, lng, elevation, temperature, pressure); + +console.log(`NYC Prayer Times for Today:\n\n`); +console.log(prayerTimes);