pray-calc/src/getTimesAll.ts

257 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 };