pray-calc/src/getTimes.ts
Aric Camarata 6f165b79ad ci: fix eslint parser devDeps, typed linting config, prettier formatting
- 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
2026-05-31 08:47:17 -04:00

104 lines
4.1 KiB
TypeScript

/**
* Core prayer times computation using the PrayCalc Dynamic Method.
*
* Returns all prayer times as fractional hours using the dynamic twilight
* angle algorithm. Times are in local time as determined by the timezone
* offset (tz parameter).
*/
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 { validateInputs } from "./validate.js";
import { DHUHR_OFFSET_MINUTES } from "./constants.js";
import type { PrayerTimes } from "./types.js";
/**
* Compute prayer times for a given date and location.
*
* Uses the dynamic twilight angle algorithm to determine Fajr and Isha
* depression angles, then solves for all prayer events via SPA.
*
* @param date - Observer's local date (time-of-day is ignored)
* @param lat - Latitude in decimal degrees (-90 to 90, south = negative)
* @param lng - Longitude in decimal degrees (-180 to 180, west = negative)
* @param tz - UTC offset in hours (e.g. -5 for EST). Defaults to the
* system timezone derived from the Date object.
* @param elevation - Observer elevation in meters (default: 0)
* @param temperature - Ambient temperature in °C (default: 15)
* @param pressure - Atmospheric pressure in mbar/hPa (default: 1013.25)
* @param hanafi - Asr convention: false = Shafi'i/Maliki/Hanbali (default),
* true = Hanafi
* @returns Prayer times as fractional hours and the dynamic angles used.
* Any time that cannot be computed (e.g. polar night/day, or the
* sun never reaching the required depression) is returned as `NaN`.
* @throws {RangeError} if lat, lng, tz, or elevation are out of valid range
*/
export function getTimes(
date: Date,
lat: number,
lng: number,
tz: number = -date.getTimezoneOffset() / 60,
elevation = 0,
temperature = 15,
pressure = 1013.25,
hanafi = false,
): PrayerTimes {
validateInputs(lat, lng, tz, elevation);
// 1. Compute dynamic twilight angles and reuse solar declination.
const { fajrAngle, ishaAngle, decl } = computeAngles(
date,
lat,
lng,
elevation,
temperature,
pressure,
);
// 2. Convert depression angles to SPA zenith angles.
// SPA uses zenith angle (90° + depression) for custom altitude events.
const fajrZenith = 90 + fajrAngle;
const ishaZenith = 90 + ishaAngle;
// 3. Run SPA for solar position + custom twilight times.
const spaOpts = { elevation, temperature, pressure };
const spaData = getSpa(date, lat, lng, tz, spaOpts, [fajrZenith, ishaZenith]);
// Non-null assertions: getSpa was called with exactly [fajrZenith, ishaZenith], so
// index 0 and 1 are always defined.
// 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;
// Dhuhr: offset after solar noon (standard practice to confirm transit).
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);
// 5. Qiyam al-Layl (last third of the night).
const qiyamTime = getQiyam(fajrTime, ishaTime);
// 6. Midnight: midpoint between Maghrib and Fajr (standard definition).
const midnightTime = getMidnight(maghribTime, fajrTime);
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,
angles: { fajrAngle, ishaAngle },
};
}