mirror of
https://github.com/acamarata/pray-calc.git
synced 2026-06-30 19:04:26 +00:00
- Add @typescript-eslint/parser and @typescript-eslint/eslint-plugin as direct devDependencies (were only transitive, not linked in node_modules) - Add files pattern and parserOptions.project to eslint.config.mjs so ESLint finds and type-checks src/**/*.ts files correctly - Run prettier --write to fix formatting across all src files
257 lines
9.2 KiB
TypeScript
257 lines
9.2 KiB
TypeScript
/**
|
||
* Prayer times comparison — all methods.
|
||
*
|
||
* Returns the PrayCalc Dynamic times plus comparison times for every
|
||
* supported traditional method, all as fractional hours.
|
||
*
|
||
* Supported methods (14 total):
|
||
*
|
||
* | ID | Name | Fajr | Isha | Region |
|
||
* |---------|----------------------------------------------|-------|-----------------|-----------------|
|
||
* | UOIF | Union des Org. Islamiques de France | 12° | 12° | France |
|
||
* | ISNACA | IQNA / Islamic Council of North America | 13° | 13° | Canada |
|
||
* | ISNA | FCNA / Islamic Society of North America | 15° | 15° | US, UK, AU, NZ |
|
||
* | SAMR | Spiritual Admin. of Muslims of Russia | 16° | 15° | Russia |
|
||
* | IGUT | Inst. of Geophysics, Univ. of Tehran | 17.7° | 14° | Iran, Shia use |
|
||
* | MWL | Muslim World League | 18° | 17° | Global default |
|
||
* | DIBT | Diyanet İşleri Başkanlığı, Turkey | 18° | 17° | Turkey |
|
||
* | Karachi | University of Islamic Sciences, Karachi | 18° | 18° | PK, BD, IN, AF |
|
||
* | Kuwait | Kuwait Ministry of Islamic Affairs | 18° | 17.5° | Kuwait |
|
||
* | UAQ | Umm Al-Qura University, Makkah | 18.5° | +90 min sunset | Saudi / Gulf |
|
||
* | Qatar | Qatar / Gulf (standard minutes interval) | 18° | +90 min sunset | Qatar, Gulf |
|
||
* | Egypt | Egyptian General Authority of Survey | 19.5° | 17.5° | EG, SY, IQ, LB |
|
||
* | MUIS | Majlis Ugama Islam Singapura | 20° | 18° | Singapore |
|
||
* | MSC | Moonsighting Committee Worldwide (seasonal) | — | — | Global |
|
||
*/
|
||
|
||
import { getSpa } from "nrel-spa";
|
||
import { computeAngles } from "./getAngles.js";
|
||
import { getAsr } from "./getAsr.js";
|
||
import { getQiyam } from "./getQiyam.js";
|
||
import { getMidnight } from "./getMidnight.js";
|
||
import { getMscFajr, getMscIsha } from "./getMSC.js";
|
||
import { validateInputs } from "./validate.js";
|
||
import { DHUHR_OFFSET_MINUTES } from "./constants.js";
|
||
import type { MethodDefinition, PrayerTimesAll } from "./types.js";
|
||
|
||
/** All supported traditional methods. */
|
||
const METHODS: MethodDefinition[] = [
|
||
{
|
||
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 İşleri Başkanlığı, 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,
|
||
},
|
||
];
|
||
|
||
/**
|
||
* Compute prayer times plus all traditional method comparisons.
|
||
*
|
||
* @param date - Observer's local date (time-of-day is ignored)
|
||
* @param lat - Latitude in decimal degrees (-90 to 90)
|
||
* @param lng - Longitude in decimal degrees (-180 to 180)
|
||
* @param tz - UTC offset in hours (defaults to system tz)
|
||
* @param elevation - Observer elevation in meters (default: 0)
|
||
* @param temperature - Ambient temperature in °C (default: 15)
|
||
* @param pressure - Atmospheric pressure in mbar (default: 1013.25)
|
||
* @param hanafi - Asr convention: false = Shafi'i (default), true = Hanafi
|
||
* @returns Prayer times for the dynamic method plus all traditional methods.
|
||
* Any time that cannot be computed is returned as `NaN`.
|
||
* Methods map contains `[fajrTime, ishaTime]` per method.
|
||
* @throws {RangeError} if lat, lng, tz, or elevation are out of valid range
|
||
*/
|
||
export function getTimesAll(
|
||
date: Date,
|
||
lat: number,
|
||
lng: number,
|
||
tz: number = -date.getTimezoneOffset() / 60,
|
||
elevation = 0,
|
||
temperature = 15,
|
||
pressure = 1013.25,
|
||
hanafi = false,
|
||
): PrayerTimesAll {
|
||
validateInputs(lat, lng, tz, elevation);
|
||
|
||
// 1. Dynamic angles and reusable solar declination.
|
||
const { fajrAngle, ishaAngle, decl } = computeAngles(
|
||
date,
|
||
lat,
|
||
lng,
|
||
elevation,
|
||
temperature,
|
||
pressure,
|
||
);
|
||
|
||
// 2. Build batch zenith angles for the SPA call:
|
||
// Slot 0: dynamic Fajr, Slot 1: dynamic Isha, then pairs for each method.
|
||
// Methods with null angles (UAQ-isha, Qatar-isha, MSC) get a placeholder
|
||
// that is overridden below.
|
||
const methodZeniths: number[] = [];
|
||
for (const m of METHODS) {
|
||
const fZ = m.fajrAngle !== null ? 90 + m.fajrAngle : 90 + 18; // placeholder for non-angle Fajr
|
||
const iZ = m.ishaAngle !== null ? 90 + m.ishaAngle : 90 + 18; // placeholder for fixed-minute Isha
|
||
methodZeniths.push(fZ, iZ);
|
||
}
|
||
|
||
const allZeniths: [number, ...number[]] = [
|
||
90 + fajrAngle,
|
||
90 + ishaAngle,
|
||
...(methodZeniths as number[]),
|
||
] as [number, ...number[]];
|
||
|
||
const spaOpts = { elevation, temperature, pressure };
|
||
const spaData = getSpa(date, lat, lng, tz, spaOpts, allZeniths);
|
||
|
||
// 3. Extract core times (index 0 = dynamic Fajr, index 1 = dynamic Isha).
|
||
// Non-null assertions: allZeniths guarantees at least 2 angle entries (index 0 and 1 always set).
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||
const fajrTime = spaData.angles[0]!.sunrise;
|
||
const sunriseTime = spaData.sunrise;
|
||
const noonTime = spaData.solarNoon;
|
||
const maghribTime = spaData.sunset;
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||
const ishaTime = spaData.angles[1]!.sunset;
|
||
const dhuhrTime = noonTime + DHUHR_OFFSET_MINUTES / 60;
|
||
|
||
// 4. Asr time (reuses declination from computeAngles — no extra ephemeris call).
|
||
const asrTime = getAsr(noonTime, lat, decl, hanafi);
|
||
const qiyamTime = getQiyam(fajrTime, ishaTime);
|
||
const midnightTime = getMidnight(maghribTime, fajrTime);
|
||
|
||
// 5. Build Methods map.
|
||
const Methods: Record<string, [number, number]> = {};
|
||
|
||
for (let i = 0; i < METHODS.length; i++) {
|
||
// Non-null assertion: METHODS.length is static (14), allZeniths was built with exactly
|
||
// 2 + METHODS.length*2 entries, so spaBaseIdx and spaBaseIdx+1 are always valid.
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||
const m = METHODS[i]!;
|
||
const spaBaseIdx = 2 + i * 2; // angles index offset for this method
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||
let methodFajr = spaData.angles[spaBaseIdx]!.sunrise;
|
||
let methodIsha: number;
|
||
|
||
if (m.useMSC) {
|
||
// MSC: seasonal minutes from sunrise/sunset.
|
||
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 !== undefined) {
|
||
// Fixed-minute Isha (UAQ = 90 min, Qatar = 90 min after sunset).
|
||
methodIsha = isFinite(maghribTime) ? maghribTime + m.ishaMinutes / 60 : NaN;
|
||
} else {
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||
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 },
|
||
};
|
||
}
|
||
|
||
/** Exported method list for documentation and tooling use. */
|
||
export { METHODS };
|