diff --git a/.claude/AGENTS.md b/.claude/AGENTS.md deleted file mode 120000 index 681311e..0000000 --- a/.claude/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/.claude/AGENTS.md b/.claude/AGENTS.md new file mode 100644 index 0000000..4ca82ab --- /dev/null +++ b/.claude/AGENTS.md @@ -0,0 +1,116 @@ +# pray-calc — PRI (Per-Repo Instructions) + +**Cascade:** GCI → ASI → PPI (`/Volumes/X9/Sites/acamarata/.claude/CLAUDE.md`) → **PRI (this file)** + +## What This Is + +High-precision Islamic prayer times calculator in TypeScript. Computes Fajr, Dhuhr, Asr, +Maghrib, Isha, Qiyam, Sunrise, and solar Noon for any location on Earth. Uses a physics- +grounded dynamic twilight angle algorithm as the primary method. Includes 14 traditional +fixed-angle methods for direct comparison. + +**npm:** `pray-calc@2.1.0` +**Language:** TypeScript source → dual CJS/ESM build (tsup) +**Node requirement:** >=20 +**License:** MIT + +## Dependencies + +- **nrel-spa** (^2.0.1) — NREL Solar Position Algorithm (zero-dep, sync, acamarata's own) +- No moon dependency. Moon data: use `moon-sighting` package. + +Dev: @types/node, tsup, typescript + +## v2 API Surface + +```typescript +// Core functions +getTimes(date, lat, lng, tz?, elevation?, temperature?, pressure?, hanafi?): PrayerTimes +calcTimes(...): FormattedPrayerTimes +getTimesAll(...): PrayerTimesAll +calcTimesAll(...): FormattedPrayerTimesAll + +// Low-level +getAngles(date, lat, lng, elevation?, temperature?, pressure?): TwilightAngles +getAsr(solarNoon, latitude, declination, hanafi?): number +getQiyam(fajrTime, ishaTime): number +getMscFajr(date, latitude): number // minutes before sunrise +getMscIsha(date, latitude, shafaq?): number // minutes after sunset + +// Ephemeris +solarEphemeris(jd): { decl, r, eclLon } +toJulianDate(date): number + +// Reference data +METHODS: MethodDefinition[] +``` + +`nrel-spa` API (v2.x): `getSpa(date, lat, lng, tz, opts, zeniths)` and `formatTime(h)` +Note: `fractalTime` was renamed to `formatTime` in v2.0.0. `fractalTime` does not exist. + +## Module Structure + +``` +src/ +├── index.ts Main exports +├── types.ts All TypeScript types +├── getSolarEphemeris.ts Jean Meeus Ch. 25 — decl, r, eclLon +├── getMSC.ts MSC piecewise seasonal model +├── getAngles.ts Dynamic angle algorithm (3 layers) +├── getAsr.ts Pure-math Asr (no SPA dependency) +├── getQiyam.ts Last-third-of-night +├── getTimes.ts Raw fractional-hour output +├── calcTimes.ts Formatted HH:MM:SS output +├── getTimesAll.ts All-methods batch SPA call +└── calcTimesAll.ts All-methods formatted output +``` + +## Domain Reference + +Algorithm math, method angle table, and astronomical research facts: + — load when working on angle calculations or method constants. + +## getSpa Batch Pattern + +`getTimesAll` uses one SPA call for all 14×2 + 2 dynamic zenith angles: + +```typescript +allZeniths = [90+fajrAngle, 90+ishaAngle, ...methodZeniths] +// methodZeniths: 14 methods × 2 = 28 entries +// Methods with null ishaAngle get placeholder 90+18 (overridden post-call) +spaData.angles[0].sunrise → dynamic Fajr +spaData.angles[1].sunset → dynamic Isha +spaData.angles[2+i*2].sunrise → method[i] Fajr +spaData.angles[2+i*2+1].sunset → method[i] Isha (if angle-based) +``` + +## Commands + +- `pnpm build` — tsup dual CJS/ESM build +- `pnpm run typecheck` — tsc --noEmit +- `pnpm test` — build + node test.mjs + node test-cjs.cjs (94 ESM + 12 CJS = 106 tests) + +## Version History + +- **v2.0.0** — TypeScript rewrite, 14 methods, physics corrections, no suncalc, no moon +- **v1.7.2** — Last CJS version with moon functions (getMoon, getMoonPhase, etc.) +- Moon functions migrated to `moon-sighting` package + +## Moon Migration + +All moon functions removed from v2. Users should: + +```bash +pnpm add moon-sighting +``` + +Map: `getMoon` → `getMoon`, `getMoonPhase` → `getMoonPhase`, etc. +See `.github/wiki/Moon-Migration.md` for full table. moon-sighting uses Meeus Ch.47/48 + +Odeh 2006 visibility, no suncalc dependency. + +## Related + +- nrel-spa: `~/Sites/acamarata/nrel-spa/` — solar foundation +- moon-sighting: `~/Sites/acamarata/moon-sighting/` — lunar data +- praycalc.com — live demo +- praycalc.net — documentation site diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a8bda1..60bc085 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,13 +15,12 @@ jobs: node-version: [20, 22, 24] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: pnpm + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm build - run: node --test test.mjs @@ -32,13 +31,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm run lint - run: pnpm run format:check @@ -48,13 +46,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm run typecheck @@ -63,13 +60,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm build - name: Verify pack contents diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6068010 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [2.1.1] - 2026-05-28 + +### Changed +- Flatten exports map to ADR-015 standard (import/require/types at top level) +- Add "./package.json" export condition +- Add coverage script (c8 --reporter=lcov) +- Migrate CI from pnpm/action-setup to corepack enable + +## [2.1.0] - 2026-05-28 + +### Added +- Initial release diff --git a/README.md b/README.md index 81ef439..fe51db4 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ [![CI](https://github.com/acamarata/pray-calc/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/pray-calc/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -Islamic prayer times for any location and date. The primary method uses a physics-grounded dynamic twilight angle algorithm that adjusts Fajr and Isha angles for latitude, season, Earth-Sun distance, and atmospheric conditions. Fourteen traditional fixed-angle methods are included for direct comparison. +Islamic prayer times for any location and date. The primary method uses a physics-grounded dynamic twilight angle algorithm that adjusts Fajr and Isha angles for latitude, season, Earth-Sun distance, and atmospheric conditions. Fourteen traditional fixed-angle methods are included for comparison. Single runtime dependency: [nrel-spa](https://github.com/acamarata/nrel-spa). ## Installation ```bash -pnpm add pray-calc # or npm install pray-calc +npm install pray-calc ``` ## Quick Start @@ -19,9 +19,9 @@ import { calcTimes } from 'pray-calc'; const times = calcTimes( new Date('2024-06-21'), - 40.7128, // New York latitude - -74.0060, // longitude - -4, // UTC offset (hours) + 40.7128, // New York latitude + -74.0060, // longitude + -4, // UTC offset (hours) ); console.log(times.Fajr); // "03:51:24" @@ -30,150 +30,15 @@ console.log(times.Dhuhr); // "13:01:17" console.log(times.Asr); // "17:02:43" console.log(times.Maghrib); // "20:31:17" console.log(times.Isha); // "22:07:43" -console.log(times.angles); // { fajrAngle: 14.8, ishaAngle: 14.6 } ``` -### CJS +CommonJS: -```javascript +```js const { calcTimes } = require('pray-calc'); ``` -### Compare all methods - -```typescript -import { calcTimesAll } from 'pray-calc'; - -const all = calcTimesAll(new Date('2024-06-21'), 40.7128, -74.0060, -4); - -// Dynamic primary times -console.log(all.Fajr); // "03:51:24" - -// Traditional method comparison -console.log(all.Methods.ISNA); // ["03:57:12", "22:22:18"] [fajr, isha] -console.log(all.Methods.MWL); // ["03:25:08", "22:40:31"] -console.log(all.Methods.MSC); // ["03:53:41", "22:09:12"] -``` - -## API - -### `getTimes(date, lat, lng, tz?, elevation?, temperature?, pressure?, hanafi?)` - -Returns raw fractional-hour prayer times using the dynamic method. - -| Parameter | Type | Default | Description | -| --------- | ---- | ------- | ----------- | -| `date` | `Date` | required | Observer's local date | -| `lat` | `number` | required | Latitude, decimal degrees | -| `lng` | `number` | required | Longitude, decimal degrees | -| `tz` | `number` | system offset | UTC offset in hours | -| `elevation` | `number` | `0` | Meters above sea level | -| `temperature` | `number` | `15` | Ambient temperature, °C | -| `pressure` | `number` | `1013.25` | Atmospheric pressure, mbar | -| `hanafi` | `boolean` | `false` | Asr convention: false = Shafi'i, true = Hanafi | - -Returns `PrayerTimes`: `{ Qiyam, Fajr, Sunrise, Noon, Dhuhr, Asr, Maghrib, Isha, Midnight, angles }`. -All times are fractional hours in local time (e.g., `5.5` = 05:30:00). `NaN` when an event -cannot be computed (polar night, etc.). - -### `calcTimes(date, lat, lng, tz?, elevation?, temperature?, pressure?, hanafi?)` - -Same as `getTimes`, formatted as `HH:MM:SS` strings. Returns `"N/A"` for unavailable times. - -### `getTimesAll(...)` - -Same signature. Returns `PrayerTimesAll`: extends `PrayerTimes` with `Methods`, a record -mapping each of the 14 method IDs to `[fajrTime, ishaTime]` as fractional hours. - -### `calcTimesAll(...)` - -Same as `getTimesAll`, fully formatted. `Methods` values are `[fajrString, ishaString]`. - -### `getAngles(date, lat, lng, elevation?, temperature?, pressure?)` - -Returns `{ fajrAngle, ishaAngle }` in degrees (positive = below horizon). - -### `getAsr(solarNoon, latitude, declination, hanafi?)` - -Computes Asr from solar noon time, latitude, and solar declination. Returns fractional hours. - -### `getQiyam(fajrTime, ishaTime)` - -Returns the start of the last third of the night as fractional hours. - -### `getMidnight(maghribTime, endTime)` - -Returns the midpoint of the night as fractional hours. Pass Fajr as `endTime` for the -standard definition (Maghrib-to-Fajr midpoint), or Sunrise for the astronomical variant. - -### `getMscFajr(date, latitude)` / `getMscIsha(date, latitude, shafaq?)` - -Moonsighting Committee Worldwide minute offsets: minutes before sunrise (Fajr) and -minutes after sunset (Isha). `shafaq` controls which twilight phase is used for Isha: -`'general'` (default), `'ahmer'` (red glow), or `'abyad'` (white glow). - -### `METHODS` - -Exported array of all 14 `MethodDefinition` objects. - -## Supported Methods - -| ID | Name | Fajr | Isha | Region | -| -- | ---- | ---- | ---- | ------ | -| `UOIF` | Union des Organisations 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 Administration of Muslims of Russia | 16° | 15° | Russia | -| `IGUT` | Institute of Geophysics, Univ. of Tehran | 17.7° | 14° | Iran | -| `MWL` | Muslim World League | 18° | 17° | Global | -| `DIBT` | Diyanet, Turkey | 18° | 17° | Turkey | -| `Karachi` | Univ. of Islamic Sciences, Karachi | 18° | 18° | PK, BD, IN, AF | -| `Kuwait` | Kuwait Ministry of Islamic Affairs | 18° | 17.5° | Kuwait | -| `UAQ` | Umm Al-Qura Univ., Makkah | 18.5° | +90 min | Saudi Arabia | -| `Qatar` | Qatar / Gulf Standard | 18° | +90 min | 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 | seasonal | Global | - -## Dynamic Method - -Standard prayer time libraries use a fixed angle (e.g., MWL: 18°) applied globally. -This works near the equator but fails at higher latitudes: above 48.5°N in summer, the -Sun never reaches 18° depression, so a 18°-everywhere library produces missing Isha -times. Observational campaigns also show that at mid-latitudes, true dawn appears when -the Sun is around 14–16° below the horizon, not 18°. - -The dynamic method computes the angle in three layers: - -1. **MSC seasonal base**: Khalid Shaukat's piecewise model, calibrated against field - observations across latitudes 0°–55°N/S. Returns minutes before/after sunrise/sunset, - converted to depression degrees via spherical trigonometry. - -2. **Physics corrections**: Earth-Sun distance (r via Jean Meeus elliptical orbit), - Fourier harmonic smoothing, atmospheric refraction at the computed altitude, and - elevation horizon dip. - -3. **Physical bounds**: clipped to [10°, 22°]. - -At the equator the result converges to approximately 18°, consistent with historical -usage. At 50–55°N in summer it falls to 12–14°, matching empirical UK observations. - -Full detail: [Dynamic Algorithm wiki page](https://github.com/acamarata/pray-calc/wiki/Dynamic-Algorithm) - -## Architecture - -- Only runtime dependency: `nrel-spa` (NREL Solar Position Algorithm) -- `getSolarEphemeris`: Jean Meeus Ch. 25: declination, Earth-Sun distance, ecliptic lon -- `getTimesAll`: single batch SPA call for all 14×2 + 2 dynamic zenith angles - -Full detail: [Architecture wiki page](https://github.com/acamarata/pray-calc/wiki/Architecture) - -## Compatibility - -- Node.js >= 20 -- ESM and CJS builds included -- TypeScript types bundled -- No browser-incompatible APIs +Use `calcTimesAll` to get all 14 traditional method times alongside the dynamic result. ## TypeScript @@ -182,39 +47,24 @@ import type { PrayerTimes, FormattedPrayerTimes, PrayerTimesAll, - FormattedPrayerTimesAll, - TwilightAngles, MethodDefinition, } from 'pray-calc'; ``` ## Documentation -Full documentation: [GitHub Wiki](https://github.com/acamarata/pray-calc/wiki) - -- [API Reference](https://github.com/acamarata/pray-calc/wiki/API-Reference) -- [Dynamic Algorithm](https://github.com/acamarata/pray-calc/wiki/Dynamic-Algorithm) -- [Traditional Methods](https://github.com/acamarata/pray-calc/wiki/Traditional-Methods) -- [Architecture](https://github.com/acamarata/pray-calc/wiki/Architecture) -- [Twilight Physics](https://github.com/acamarata/pray-calc/wiki/Twilight-Physics) -- [High-Latitude Handling](https://github.com/acamarata/pray-calc/wiki/High-Latitude) +Full API reference, dynamic algorithm details, traditional method table, and high-latitude handling: [GitHub Wiki](https://github.com/acamarata/pray-calc/wiki) ## Related -- [nrel-spa](https://github.com/acamarata/nrel-spa): NREL Solar Position Algorithm +- [nrel-spa](https://github.com/acamarata/nrel-spa): NREL Solar Position Algorithm (the solar foundation) - [luxon-hijri](https://github.com/acamarata/luxon-hijri): Hijri/Gregorian calendar -- [moon-sighting](https://github.com/acamarata/moon-sighting): Crescent visibility +- [moon-sighting](https://github.com/acamarata/moon-sighting): Crescent visibility calculations ## Acknowledgments -The solar position calculations use [nrel-spa](https://github.com/acamarata/nrel-spa), a pure JavaScript port of the Solar Position Algorithm (SPA) developed by Ibrahim Reda and Afshin Andreas at the National Renewable Energy Laboratory (NREL): - -> Reda, I., Andreas, A. (2004). "Solar Position Algorithm for Solar Radiation Applications." Solar Energy, 76(5), 577-589. - -The seasonal twilight model builds on the work of Khalid Shaukat (Moonsighting Committee Worldwide). +Solar position calculations use [nrel-spa](https://github.com/acamarata/nrel-spa), a port of the NREL SPA by Ibrahim Reda and Afshin Andreas. The seasonal twilight model builds on the work of Khalid Shaukat (Moonsighting Committee Worldwide). ## License MIT. Copyright (c) 2023-2026 Aric Camarata. - -See [LICENSE](LICENSE) for full terms. diff --git a/package.json b/package.json index 184876a..c500347 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pray-calc", - "version": "2.1.0", + "version": "2.1.1", "description": "Islamic prayer times with a physics-grounded dynamic twilight angle algorithm. Covers Fajr, Sunrise, Dhuhr, Asr, Maghrib, Isha, Qiyam. Includes 14 traditional fixed-angle methods for comparison.", "author": "Aric Camarata", "license": "MIT", @@ -9,15 +9,11 @@ "types": "./dist/index.d.ts", "exports": { ".": { - "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" - }, - "require": { - "types": "./dist/index.d.ts", - "default": "./dist/index.cjs" - } - } + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" }, "sideEffects": false, "files": [ @@ -34,7 +30,8 @@ "lint": "eslint src/", "format": "prettier --write src/", "format:check": "prettier --check src/", - "prepublishOnly": "tsup" + "prepublishOnly": "tsup", + "coverage": "c8 --reporter=lcov --reporter=text node --test" }, "keywords": [ "prayer-times", @@ -82,5 +79,7 @@ "tsup": "^8.5.1", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1" - } + }, + "type": "module", + "packageManager": "pnpm@10.11.1" }