mirror of
https://github.com/acamarata/pray-calc.git
synced 2026-06-30 19:04:26 +00:00
refactor: code quality improvements across the board
- Extract magic numbers into named constants (DHUHR_OFFSET_MINUTES, ANGLE_MIN/MAX, LAT_SCALE) with source citations for MCW coefficients - Add input validation (RangeError) for lat, lng, tz, elevation on all public API functions (getTimes, getTimesAll) - Optimize solar ephemeris: computeAngles() returns declination so getTimes/getTimesAll reuse it for Asr instead of computing twice - DRY: shared constants.ts for DEG, Dhuhr offset, angle bounds - Improve MethodEntry type with labeled tuple elements and NaN docs - Add stricter tsconfig (noImplicitReturns, noFallthroughCasesInSwitch) - Switch tests to node:test framework (TAP output, describe/it blocks) - Add 8 new input validation tests (104 ESM + 13 CJS total) - Add ESLint + Prettier with CI lint job - Remove src/ from npm package files (smaller published tarball) - Document NaN return behavior in JSDoc for getTimes/getTimesAll
This commit is contained in:
parent
a8d15bc85d
commit
8f39fcd82e
21 changed files with 1759 additions and 830 deletions
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
|
|
@ -24,8 +24,24 @@ jobs:
|
|||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: node test.mjs
|
||||
- run: node test-cjs.cjs
|
||||
- run: node --test test.mjs
|
||||
- run: node --test test-cjs.cjs
|
||||
|
||||
lint:
|
||||
name: Lint & Format
|
||||
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
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run lint
|
||||
- run: pnpm run format:check
|
||||
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -56,3 +56,9 @@ coverage/
|
|||
.windsurf/
|
||||
.cody/
|
||||
.sourcegraph/
|
||||
.vscode/*
|
||||
.codex/
|
||||
.aider/
|
||||
.aider.chat.history.md
|
||||
.continue/
|
||||
.gemini/
|
||||
|
|
|
|||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
12
eslint.config.mjs
Normal file
12
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs'],
|
||||
},
|
||||
);
|
||||
13
package.json
13
package.json
|
|
@ -22,7 +22,6 @@
|
|||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/",
|
||||
"src/",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE"
|
||||
|
|
@ -31,7 +30,10 @@
|
|||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"pretest": "tsup",
|
||||
"test": "node test.mjs && node test-cjs.cjs",
|
||||
"test": "node --test test.mjs && node --test test-cjs.cjs",
|
||||
"lint": "eslint src/",
|
||||
"format": "prettier --write src/",
|
||||
"format:check": "prettier --check src/",
|
||||
"prepublishOnly": "tsup"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
@ -72,8 +74,13 @@
|
|||
"nrel-spa": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.3.0",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.8.1",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
733
pnpm-lock.yaml
733
pnpm-lock.yaml
|
|
@ -12,15 +12,30 @@ importers:
|
|||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1(eslint@10.0.3)
|
||||
'@types/node':
|
||||
specifier: ^25.3.0
|
||||
version: 25.3.0
|
||||
eslint:
|
||||
specifier: ^10.0.3
|
||||
version: 10.0.3
|
||||
eslint-config-prettier:
|
||||
specifier: ^10.1.8
|
||||
version: 10.1.8(eslint@10.0.3)
|
||||
prettier:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1
|
||||
tsup:
|
||||
specifier: ^8.5.1
|
||||
version: 8.5.1(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
typescript-eslint:
|
||||
specifier: ^8.56.1
|
||||
version: 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -180,6 +195,61 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1':
|
||||
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
|
||||
'@eslint-community/regexpp@4.12.2':
|
||||
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
|
||||
'@eslint/config-array@0.23.3':
|
||||
resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@eslint/config-helpers@0.5.3':
|
||||
resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@eslint/core@1.1.1':
|
||||
resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@eslint/js@10.0.1':
|
||||
resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
peerDependencies:
|
||||
eslint: ^10.0.0
|
||||
peerDependenciesMeta:
|
||||
eslint:
|
||||
optional: true
|
||||
|
||||
'@eslint/object-schema@3.0.3':
|
||||
resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@eslint/plugin-kit@0.6.1':
|
||||
resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1':
|
||||
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||
engines: {node: '>=12.22'}
|
||||
|
||||
'@humanwhocodes/retry@0.4.3':
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
|
|
@ -331,20 +401,101 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@types/esrecurse@4.3.1':
|
||||
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/node@25.3.0':
|
||||
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.56.1':
|
||||
resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^8.56.1
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/parser@8.56.1':
|
||||
resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/project-service@8.56.1':
|
||||
resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/scope-manager@8.56.1':
|
||||
resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.56.1':
|
||||
resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/type-utils@8.56.1':
|
||||
resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/types@8.56.1':
|
||||
resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.56.1':
|
||||
resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/utils@8.56.1':
|
||||
resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.56.1':
|
||||
resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
acorn-jsx@5.3.2:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ajv@6.14.0:
|
||||
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
balanced-match@4.0.4:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
brace-expansion@5.0.4:
|
||||
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
bundle-require@5.1.0:
|
||||
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
|
@ -370,6 +521,10 @@ packages:
|
|||
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
|
||||
engines: {node: ^14.18.0 || >=16.10.0}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
|
@ -379,11 +534,75 @@ packages:
|
|||
supports-color:
|
||||
optional: true
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
escape-string-regexp@4.0.0:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
eslint-config-prettier@10.1.8:
|
||||
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
eslint: '>=7.0.0'
|
||||
|
||||
eslint-scope@9.1.2:
|
||||
resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
eslint-visitor-keys@3.4.3:
|
||||
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
eslint-visitor-keys@5.0.1:
|
||||
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
eslint@10.0.3:
|
||||
resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
jiti: '*'
|
||||
peerDependenciesMeta:
|
||||
jiti:
|
||||
optional: true
|
||||
|
||||
espree@11.2.0:
|
||||
resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
esquery@1.7.0:
|
||||
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
esrecurse@4.3.0:
|
||||
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
estraverse@5.3.0:
|
||||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
esutils@2.0.3:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-json-stable-stringify@2.1.0:
|
||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||
|
||||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
|
@ -393,18 +612,76 @@ packages:
|
|||
picomatch:
|
||||
optional: true
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
find-up@5.0.0:
|
||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
|
||||
|
||||
flat-cache@4.0.1:
|
||||
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
flatted@3.3.4:
|
||||
resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
glob-parent@6.0.2:
|
||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
ignore@7.0.5:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
imurmurhash@0.1.4:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
lilconfig@3.1.3:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -416,9 +693,17 @@ packages:
|
|||
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
minimatch@10.2.4:
|
||||
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
mlly@1.8.0:
|
||||
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
||||
|
||||
|
|
@ -428,6 +713,9 @@ packages:
|
|||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
nrel-spa@2.0.1:
|
||||
resolution: {integrity: sha512-KwsudVfAHMUwz9RwhriI7oNqFYz77+VGi2vUpJeR+xNx57MU28EYcdt1TQ1frEDbpBXkF4EJxM62Hi2iX6QNCA==}
|
||||
engines: {node: '>=20'}
|
||||
|
|
@ -436,6 +724,26 @@ packages:
|
|||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
p-locate@5.0.0:
|
||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
|
|
@ -471,6 +779,19 @@ packages:
|
|||
yaml:
|
||||
optional: true
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prettier@3.8.1:
|
||||
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
|
@ -484,6 +805,19 @@ packages:
|
|||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.4:
|
||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shebang-regex@3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
source-map@0.7.6:
|
||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
|
@ -511,6 +845,12 @@ packages:
|
|||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
|
||||
ts-api-utils@2.4.0:
|
||||
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
||||
engines: {node: '>=18.12'}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
|
|
@ -533,6 +873,17 @@ packages:
|
|||
typescript:
|
||||
optional: true
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
typescript-eslint@8.56.1:
|
||||
resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
|
|
@ -544,6 +895,22 @@ packages:
|
|||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
word-wrap@1.2.5:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
|
|
@ -624,6 +991,51 @@ snapshots:
|
|||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.3)':
|
||||
dependencies:
|
||||
eslint: 10.0.3
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
|
||||
'@eslint/config-array@0.23.3':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 3.0.3
|
||||
debug: 4.4.3
|
||||
minimatch: 10.2.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@eslint/config-helpers@0.5.3':
|
||||
dependencies:
|
||||
'@eslint/core': 1.1.1
|
||||
|
||||
'@eslint/core@1.1.1':
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@eslint/js@10.0.1(eslint@10.0.3)':
|
||||
optionalDependencies:
|
||||
eslint: 10.0.3
|
||||
|
||||
'@eslint/object-schema@3.0.3': {}
|
||||
|
||||
'@eslint/plugin-kit@0.6.1':
|
||||
dependencies:
|
||||
'@eslint/core': 1.1.1
|
||||
levn: 0.4.1
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
dependencies:
|
||||
'@humanfs/core': 0.19.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1': {}
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
|
@ -713,16 +1125,128 @@ snapshots:
|
|||
'@rollup/rollup-win32-x64-msvc@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@types/esrecurse@4.3.1': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@25.3.0':
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.56.1
|
||||
'@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.56.1
|
||||
eslint: 10.0.3
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.56.1
|
||||
'@typescript-eslint/types': 8.56.1
|
||||
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.56.1
|
||||
debug: 4.4.3
|
||||
eslint: 10.0.3
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.56.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.56.1
|
||||
debug: 4.4.3
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/scope-manager@8.56.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.56.1
|
||||
'@typescript-eslint/visitor-keys': 8.56.1
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.56.1
|
||||
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
eslint: 10.0.3
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@8.56.1': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.56.1(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.56.1
|
||||
'@typescript-eslint/visitor-keys': 8.56.1
|
||||
debug: 4.4.3
|
||||
minimatch: 10.2.4
|
||||
semver: 7.7.4
|
||||
tinyglobby: 0.2.15
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3)
|
||||
'@typescript-eslint/scope-manager': 8.56.1
|
||||
'@typescript-eslint/types': 8.56.1
|
||||
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||
eslint: 10.0.3
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.56.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.56.1
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.16.0):
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
ajv@6.14.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
balanced-match@4.0.4: {}
|
||||
|
||||
brace-expansion@5.0.4:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
|
||||
bundle-require@5.1.0(esbuild@0.27.3):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
|
|
@ -740,10 +1264,18 @@ snapshots:
|
|||
|
||||
consola@3.4.2: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
|
|
@ -773,31 +1305,164 @@ snapshots:
|
|||
'@esbuild/win32-ia32': 0.27.3
|
||||
'@esbuild/win32-x64': 0.27.3
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
eslint-config-prettier@10.1.8(eslint@10.0.3):
|
||||
dependencies:
|
||||
eslint: 10.0.3
|
||||
|
||||
eslint-scope@9.1.2:
|
||||
dependencies:
|
||||
'@types/esrecurse': 4.3.1
|
||||
'@types/estree': 1.0.8
|
||||
esrecurse: 4.3.0
|
||||
estraverse: 5.3.0
|
||||
|
||||
eslint-visitor-keys@3.4.3: {}
|
||||
|
||||
eslint-visitor-keys@5.0.1: {}
|
||||
|
||||
eslint@10.0.3:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3)
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/config-array': 0.23.3
|
||||
'@eslint/config-helpers': 0.5.3
|
||||
'@eslint/core': 1.1.1
|
||||
'@eslint/plugin-kit': 0.6.1
|
||||
'@humanfs/node': 0.16.7
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
'@types/estree': 1.0.8
|
||||
ajv: 6.14.0
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 9.1.2
|
||||
eslint-visitor-keys: 5.0.1
|
||||
espree: 11.2.0
|
||||
esquery: 1.7.0
|
||||
esutils: 2.0.3
|
||||
fast-deep-equal: 3.1.3
|
||||
file-entry-cache: 8.0.0
|
||||
find-up: 5.0.0
|
||||
glob-parent: 6.0.2
|
||||
ignore: 5.3.2
|
||||
imurmurhash: 0.1.4
|
||||
is-glob: 4.0.3
|
||||
json-stable-stringify-without-jsonify: 1.0.1
|
||||
minimatch: 10.2.4
|
||||
natural-compare: 1.4.0
|
||||
optionator: 0.9.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
espree@11.2.0:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
acorn-jsx: 5.3.2(acorn@8.16.0)
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
esquery@1.7.0:
|
||||
dependencies:
|
||||
estraverse: 5.3.0
|
||||
|
||||
esrecurse@4.3.0:
|
||||
dependencies:
|
||||
estraverse: 5.3.0
|
||||
|
||||
estraverse@5.3.0: {}
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-json-stable-stringify@2.1.0: {}
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
|
||||
find-up@5.0.0:
|
||||
dependencies:
|
||||
locate-path: 6.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
dependencies:
|
||||
magic-string: 0.30.21
|
||||
mlly: 1.8.0
|
||||
rollup: 4.59.0
|
||||
|
||||
flat-cache@4.0.1:
|
||||
dependencies:
|
||||
flatted: 3.3.4
|
||||
keyv: 4.5.4
|
||||
|
||||
flatted@3.3.4: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
glob-parent@6.0.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
load-tsconfig@0.2.5: {}
|
||||
|
||||
locate-path@6.0.0:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
minimatch@10.2.4:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.4
|
||||
|
||||
mlly@1.8.0:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
|
@ -813,10 +1478,33 @@ snapshots:
|
|||
object-assign: 4.1.1
|
||||
thenify-all: 1.6.0
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
nrel-spa@2.0.1: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
fast-levenshtein: 2.0.6
|
||||
levn: 0.4.1
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
word-wrap: 1.2.5
|
||||
|
||||
p-limit@3.1.0:
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
|
||||
p-locate@5.0.0:
|
||||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
|
@ -835,6 +1523,12 @@ snapshots:
|
|||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.8.1: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
|
@ -870,6 +1564,14 @@ snapshots:
|
|||
'@rollup/rollup-win32-x64-msvc': 4.59.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
source-map@0.7.6: {}
|
||||
|
||||
sucrase@3.35.1:
|
||||
|
|
@ -899,6 +1601,10 @@ snapshots:
|
|||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
ts-api-utils@2.4.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsup@8.5.1(typescript@5.9.3):
|
||||
|
|
@ -928,8 +1634,35 @@ snapshots:
|
|||
- tsx
|
||||
- yaml
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
typescript-eslint@8.56.1(eslint@10.0.3)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
|
||||
eslint: 10.0.3
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
ufo@1.6.3: {}
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
|
|
|||
35
src/constants.ts
Normal file
35
src/constants.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Shared constants for pray-calc.
|
||||
*/
|
||||
|
||||
/** Degrees-to-radians conversion factor. */
|
||||
export const DEG = Math.PI / 180;
|
||||
|
||||
/**
|
||||
* Minutes added to solar noon to obtain Dhuhr time.
|
||||
*
|
||||
* Standard practice adds a small buffer after geometric solar transit to
|
||||
* ensure the sun has clearly passed the meridian before Dhuhr begins.
|
||||
* The 2.5-minute convention is widely used across Islamic timekeeping
|
||||
* authorities and accounts for the sun's angular diameter (~0.5°) plus
|
||||
* a small safety margin.
|
||||
*/
|
||||
export const DHUHR_OFFSET_MINUTES = 2.5;
|
||||
|
||||
/**
|
||||
* Minimum allowed dynamic twilight depression angle (degrees).
|
||||
*
|
||||
* At very high latitudes in summer the MCW base angle can drop below
|
||||
* physically meaningful values. 10° is the lower clamp — below this
|
||||
* the sky is too bright for any twilight definition.
|
||||
*/
|
||||
export const ANGLE_MIN = 10;
|
||||
|
||||
/**
|
||||
* Maximum allowed dynamic twilight depression angle (degrees).
|
||||
*
|
||||
* 22° is the upper clamp. Values above ~20° correspond to deep
|
||||
* astronomical twilight where the sky is indistinguishable from full
|
||||
* night. No standard method exceeds 20° for Fajr.
|
||||
*/
|
||||
export const ANGLE_MAX = 22;
|
||||
|
|
@ -52,13 +52,14 @@
|
|||
|
||||
import { toJulianDate, solarEphemeris, atmosphericRefraction } from './getSolarEphemeris.js';
|
||||
import { getMscFajr, getMscIsha, minutesToDepression } from './getMSC.js';
|
||||
import { DEG, ANGLE_MIN, ANGLE_MAX } from './constants.js';
|
||||
import type { TwilightAngles } from './types.js';
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
const FAJR_MIN = 10;
|
||||
const FAJR_MAX = 22;
|
||||
const ISHA_MIN = 10;
|
||||
const ISHA_MAX = 22;
|
||||
/** Internal result type including ephemeris data for caller reuse. */
|
||||
export interface AnglesWithEphemeris extends TwilightAngles {
|
||||
/** Solar declination in degrees (reusable for Asr computation). */
|
||||
decl: number;
|
||||
}
|
||||
|
||||
/** Clamp a value to [min, max]. */
|
||||
function clip(value: number, min: number, max: number): number {
|
||||
|
|
@ -108,10 +109,7 @@ function earthSunDistanceCorrection(r: number): number {
|
|||
*
|
||||
* Net effect is small (< 0.3°) and primarily improves day-to-day smoothness.
|
||||
*/
|
||||
function fourierSmoothingCorrection(
|
||||
eclLon: number,
|
||||
latAbsDeg: number,
|
||||
): number {
|
||||
function fourierSmoothingCorrection(eclLon: number, latAbsDeg: number): number {
|
||||
const theta = eclLon; // solar ecliptic longitude, radians [0, 2π)
|
||||
const phi = latAbsDeg * DEG;
|
||||
|
||||
|
|
@ -133,27 +131,23 @@ function fourierSmoothingCorrection(
|
|||
}
|
||||
|
||||
/**
|
||||
* Compute dynamic twilight depression angles for Fajr and Isha.
|
||||
* Internal: compute angles and return solar declination for Asr reuse.
|
||||
*
|
||||
* @param date - Observer's local date (time-of-day is ignored)
|
||||
* @param lat - Latitude in decimal degrees
|
||||
* @param lng - Longitude in decimal degrees (currently unused; reserved)
|
||||
* @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)
|
||||
* @returns Fajr and Isha depression angles in degrees
|
||||
* This avoids recomputing solarEphemeris in getTimes/getTimesAll.
|
||||
*/
|
||||
export function getAngles(
|
||||
export function computeAngles(
|
||||
date: Date,
|
||||
lat: number,
|
||||
lng: number,
|
||||
elevation = 0,
|
||||
temperature = 15,
|
||||
pressure = 1013.25,
|
||||
): TwilightAngles {
|
||||
): AnglesWithEphemeris {
|
||||
// 1. Solar ephemeris features at solar noon of the given date.
|
||||
// Using UTC noon as a stable reference that avoids timezone artifacts.
|
||||
const noonDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0));
|
||||
const noonDate = new Date(
|
||||
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0),
|
||||
);
|
||||
const jd = toJulianDate(noonDate);
|
||||
const { decl, r, eclLon } = solarEphemeris(jd);
|
||||
|
||||
|
|
@ -202,8 +196,31 @@ export function getAngles(
|
|||
const rawFajr = fajrBase + rCorr + fourierCorr + refrFajr + elevCorr;
|
||||
const rawIsha = ishaBase + rCorr + fourierCorr + refrIsha + elevCorr;
|
||||
|
||||
const fajrAngle = round3(clip(rawFajr, FAJR_MIN, FAJR_MAX));
|
||||
const ishaAngle = round3(clip(rawIsha, ISHA_MIN, ISHA_MAX));
|
||||
const fajrAngle = round3(clip(rawFajr, ANGLE_MIN, ANGLE_MAX));
|
||||
const ishaAngle = round3(clip(rawIsha, ANGLE_MIN, ANGLE_MAX));
|
||||
|
||||
return { fajrAngle, ishaAngle, decl };
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute dynamic twilight depression angles for Fajr and Isha.
|
||||
*
|
||||
* @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, currently unused; reserved)
|
||||
* @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)
|
||||
* @returns Fajr and Isha depression angles in degrees
|
||||
*/
|
||||
export function getAngles(
|
||||
date: Date,
|
||||
lat: number,
|
||||
lng: number,
|
||||
elevation = 0,
|
||||
temperature = 15,
|
||||
pressure = 1013.25,
|
||||
): TwilightAngles {
|
||||
const { fajrAngle, ishaAngle } = computeAngles(date, lat, lng, elevation, temperature, pressure);
|
||||
return { fajrAngle, ishaAngle };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* and solar noon are known.
|
||||
*/
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
import { DEG } from './constants.js';
|
||||
|
||||
/**
|
||||
* Compute Asr time as fractional hours.
|
||||
|
|
@ -36,9 +36,7 @@ export function getAsr(
|
|||
|
||||
// Solve the hour-angle equation:
|
||||
// cos(H0) = (sin(A) − sin(φ)sin(δ)) / (cos(φ)cos(δ))
|
||||
const cosH0 =
|
||||
(sinA - Math.sin(phi) * Math.sin(delta)) /
|
||||
(Math.cos(phi) * Math.cos(delta));
|
||||
const cosH0 = (sinA - Math.sin(phi) * Math.sin(delta)) / (Math.cos(phi) * Math.cos(delta));
|
||||
|
||||
if (cosH0 < -1 || cosH0 > 1) return NaN; // sun never reaches A
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,29 @@
|
|||
*
|
||||
* Reference: moonsighting.com/isha_fajr.html
|
||||
*
|
||||
* ## MCW Coefficient Key
|
||||
*
|
||||
* The piecewise-linear anchor values (a, b, c, d) follow the pattern:
|
||||
* value = BASE + (SLOPE / LAT_SCALE) × |latitude|
|
||||
*
|
||||
* where BASE is the equatorial offset in minutes, SLOPE is the per-degree
|
||||
* latitude coefficient, and LAT_SCALE = 55° is the normalisation latitude.
|
||||
* Coefficients were curve-fit to multi-latitude observations of Subh Sadiq
|
||||
* and Shafaq by the Moonsighting Committee (moonsighting.com/isha_fajr.html).
|
||||
*
|
||||
* High-latitude handling (|lat| > 55°): falls back to 1/7-night rule.
|
||||
*/
|
||||
|
||||
export type ShafaqMode = 'general' | 'ahmer' | 'abyad';
|
||||
|
||||
/**
|
||||
* Normalisation latitude (degrees) used as the divisor in MCW latitude
|
||||
* scaling coefficients. All MCW slope values are expressed per 55° of
|
||||
* latitude so that the piecewise function smoothly scales from equator
|
||||
* to mid-high latitudes.
|
||||
*/
|
||||
const LAT_SCALE = 55;
|
||||
|
||||
function isLeapYear(year: number): boolean {
|
||||
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
||||
}
|
||||
|
|
@ -79,10 +97,12 @@ export function getMscFajr(date: Date, latitude: number): number {
|
|||
const latAbs = Math.abs(latitude);
|
||||
const { dyy, daysInYear } = computeDyy(date, latitude);
|
||||
|
||||
const a = 75 + (28.65 / 55) * latAbs;
|
||||
const b = 75 + (19.44 / 55) * latAbs;
|
||||
const c = 75 + (32.74 / 55) * latAbs;
|
||||
const d = 75 + (48.1 / 55) * latAbs;
|
||||
// Anchor values: BASE + (SLOPE / LAT_SCALE) × |lat|
|
||||
// BASE = 75 min (equatorial Fajr offset). Slopes from MCW curve-fit.
|
||||
const a = 75 + (28.65 / LAT_SCALE) * latAbs;
|
||||
const b = 75 + (19.44 / LAT_SCALE) * latAbs;
|
||||
const c = 75 + (32.74 / LAT_SCALE) * latAbs;
|
||||
const d = 75 + (48.1 / LAT_SCALE) * latAbs;
|
||||
|
||||
return Math.round(interpolateSegment(dyy, daysInYear, a, b, c, d));
|
||||
}
|
||||
|
|
@ -95,11 +115,7 @@ export function getMscFajr(date: Date, latitude: number): number {
|
|||
* - 'ahmer': based on disappearance of redness (shafaq ahmer)
|
||||
* - 'abyad': based on disappearance of whiteness (shafaq abyad), later
|
||||
*/
|
||||
export function getMscIsha(
|
||||
date: Date,
|
||||
latitude: number,
|
||||
shafaq: ShafaqMode = 'general',
|
||||
): number {
|
||||
export function getMscIsha(date: Date, latitude: number, shafaq: ShafaqMode = 'general'): number {
|
||||
const latAbs = Math.abs(latitude);
|
||||
const { dyy, daysInYear } = computeDyy(date, latitude);
|
||||
|
||||
|
|
@ -107,22 +123,25 @@ export function getMscIsha(
|
|||
|
||||
switch (shafaq) {
|
||||
case 'ahmer':
|
||||
a = 62 + (17.4 / 55) * latAbs;
|
||||
b = 62 - (7.16 / 55) * latAbs;
|
||||
c = 62 + (5.12 / 55) * latAbs;
|
||||
d = 62 + (19.44 / 55) * latAbs;
|
||||
// Shafaq ahmer (red glow): BASE = 62 min (shorter twilight)
|
||||
a = 62 + (17.4 / LAT_SCALE) * latAbs;
|
||||
b = 62 - (7.16 / LAT_SCALE) * latAbs;
|
||||
c = 62 + (5.12 / LAT_SCALE) * latAbs;
|
||||
d = 62 + (19.44 / LAT_SCALE) * latAbs;
|
||||
break;
|
||||
case 'abyad':
|
||||
a = 75 + (25.6 / 55) * latAbs;
|
||||
b = 75 + (7.16 / 55) * latAbs;
|
||||
c = 75 + (36.84 / 55) * latAbs;
|
||||
d = 75 + (81.84 / 55) * latAbs;
|
||||
// Shafaq abyad (white glow): BASE = 75 min (longer twilight)
|
||||
a = 75 + (25.6 / LAT_SCALE) * latAbs;
|
||||
b = 75 + (7.16 / LAT_SCALE) * latAbs;
|
||||
c = 75 + (36.84 / LAT_SCALE) * latAbs;
|
||||
d = 75 + (81.84 / LAT_SCALE) * latAbs;
|
||||
break;
|
||||
default: // 'general'
|
||||
a = 75 + (25.6 / 55) * latAbs;
|
||||
b = 75 + (2.05 / 55) * latAbs;
|
||||
c = 75 - (9.21 / 55) * latAbs;
|
||||
d = 75 + (6.14 / 55) * latAbs;
|
||||
// General (blended) mode: BASE = 75 min
|
||||
a = 75 + (25.6 / LAT_SCALE) * latAbs;
|
||||
b = 75 + (2.05 / LAT_SCALE) * latAbs;
|
||||
c = 75 - (9.21 / LAT_SCALE) * latAbs;
|
||||
d = 75 + (6.14 / LAT_SCALE) * latAbs;
|
||||
}
|
||||
|
||||
return Math.round(interpolateSegment(dyy, daysInYear, a, b, c, d));
|
||||
|
|
@ -138,11 +157,7 @@ export function getMscIsha(
|
|||
*
|
||||
* Returns NaN if the geometry is unreachable (polar day/night).
|
||||
*/
|
||||
export function minutesToDepression(
|
||||
minutes: number,
|
||||
latDeg: number,
|
||||
declDeg: number,
|
||||
): number {
|
||||
export function minutesToDepression(minutes: number, latDeg: number, declDeg: number): number {
|
||||
const phi = latDeg * (Math.PI / 180);
|
||||
const delta = declDeg * (Math.PI / 180);
|
||||
|
||||
|
|
@ -179,8 +194,7 @@ export function minutesToDepression(
|
|||
}
|
||||
|
||||
// Solar altitude at H_prayer
|
||||
const sinH_prayer =
|
||||
sinPhi * sinDelta + cosPhi * cosDelta * Math.cos(H_prayer);
|
||||
const sinH_prayer = sinPhi * sinDelta + cosPhi * cosDelta * Math.cos(H_prayer);
|
||||
const h_prayer = Math.asin(Math.max(-1, Math.min(1, sinH_prayer)));
|
||||
|
||||
// Depression angle: positive when sun is below horizon
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* prayer time solving still uses the full SPA via nrel-spa.
|
||||
*/
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
import { DEG } from './constants.js';
|
||||
|
||||
/** Julian Date from a JavaScript Date (UTC). */
|
||||
export function toJulianDate(date: Date): number {
|
||||
|
|
@ -32,10 +32,10 @@ export function solarEphemeris(jd: number): SolarEphemeris {
|
|||
const T = (jd - 2451545.0) / 36525.0;
|
||||
|
||||
// Geometric mean longitude L0 (degrees)
|
||||
const L0 = ((280.46646 + 36000.76983 * T + 0.0003032 * T * T) % 360 + 360) % 360;
|
||||
const L0 = (((280.46646 + 36000.76983 * T + 0.0003032 * T * T) % 360) + 360) % 360;
|
||||
|
||||
// Mean anomaly M (degrees)
|
||||
const M = ((357.52911 + 35999.05029 * T - 0.0001537 * T * T) % 360 + 360) % 360;
|
||||
const M = (((357.52911 + 35999.05029 * T - 0.0001537 * T * T) % 360) + 360) % 360;
|
||||
const Mrad = M * DEG;
|
||||
|
||||
// Orbital eccentricity
|
||||
|
|
@ -58,7 +58,7 @@ export function solarEphemeris(jd: number): SolarEphemeris {
|
|||
const r = (1.000001018 * (1 - e * e)) / (1 + e * Math.cos(nuRad));
|
||||
|
||||
// Longitude of ascending node of Moon's orbit (for nutation)
|
||||
const Omega = ((125.04 - 1934.136 * T) % 360 + 360) % 360;
|
||||
const Omega = (((125.04 - 1934.136 * T) % 360) + 360) % 360;
|
||||
const OmegaRad = Omega * DEG;
|
||||
|
||||
// Apparent solar longitude corrected for nutation and aberration
|
||||
|
|
@ -66,11 +66,7 @@ export function solarEphemeris(jd: number): SolarEphemeris {
|
|||
const lambdaRad = lambda * DEG;
|
||||
|
||||
// Mean obliquity of the ecliptic (degrees)
|
||||
const epsilon0 =
|
||||
23.439291 -
|
||||
0.013004 * T -
|
||||
1.638e-7 * T * T +
|
||||
5.036e-7 * T * T * T;
|
||||
const epsilon0 = 23.439291 - 0.013004 * T - 1.638e-7 * T * T + 5.036e-7 * T * T * T;
|
||||
|
||||
// True obliquity with nutation correction
|
||||
const epsilon = (epsilon0 + 0.00256 * Math.cos(OmegaRad)) * DEG;
|
||||
|
|
@ -92,11 +88,7 @@ export function solarEphemeris(jd: number): SolarEphemeris {
|
|||
*
|
||||
* Formula: dh/dt ≈ 15 × cos(φ) × cos(δ) × sin(H) [°/hr]
|
||||
*/
|
||||
export function solarVerticalSpeed(
|
||||
latRad: number,
|
||||
declRad: number,
|
||||
hAngleRad: number,
|
||||
): number {
|
||||
export function solarVerticalSpeed(latRad: number, declRad: number, hAngleRad: number): number {
|
||||
return 15 * Math.abs(Math.cos(latRad) * Math.cos(declRad) * Math.sin(hAngleRad));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,26 +7,33 @@
|
|||
*/
|
||||
|
||||
import { getSpa } from 'nrel-spa';
|
||||
import { toJulianDate, solarEphemeris } from './getSolarEphemeris.js';
|
||||
import { getAngles } from './getAngles.js';
|
||||
import { computeAngles } from './getAngles.js';
|
||||
import { getAsr } from './getAsr.js';
|
||||
import { getQiyam } from './getQiyam.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
|
||||
* @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
|
||||
* @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,
|
||||
|
|
@ -38,8 +45,17 @@ export function getTimes(
|
|||
pressure = 1013.25,
|
||||
hanafi = false,
|
||||
): PrayerTimes {
|
||||
// 1. Compute dynamic twilight angles.
|
||||
const { fajrAngle, ishaAngle } = getAngles(date, lat, lng, elevation, temperature, pressure);
|
||||
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.
|
||||
|
|
@ -56,19 +72,13 @@ export function getTimes(
|
|||
const maghribTime = spaData.sunset;
|
||||
const ishaTime = spaData.angles[1].sunset;
|
||||
|
||||
// Dhuhr: 2.5 minutes after solar noon (standard practice to confirm transit).
|
||||
const dhuhrTime = noonTime + 2.5 / 60;
|
||||
// Dhuhr: offset after solar noon (standard practice to confirm transit).
|
||||
const dhuhrTime = noonTime + DHUHR_OFFSET_MINUTES / 60;
|
||||
|
||||
// 4. Solar declination for Asr (Meeus formula, accurate to ~0.01°).
|
||||
const jd = toJulianDate(
|
||||
new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0)),
|
||||
);
|
||||
const { decl } = solarEphemeris(jd);
|
||||
|
||||
// 5. Asr time.
|
||||
// 4. Asr time (reuses declination from computeAngles — no extra ephemeris call).
|
||||
const asrTime = getAsr(noonTime, lat, decl, hanafi);
|
||||
|
||||
// 6. Qiyam al-Layl (last third of the night).
|
||||
// 5. Qiyam al-Layl (last third of the night).
|
||||
const qiyamTime = getQiyam(fajrTime, ishaTime);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -25,43 +25,128 @@
|
|||
*/
|
||||
|
||||
import { getSpa } from 'nrel-spa';
|
||||
import { toJulianDate, solarEphemeris } from './getSolarEphemeris.js';
|
||||
import { getAngles } from './getAngles.js';
|
||||
import { computeAngles } from './getAngles.js';
|
||||
import { getAsr } from './getAsr.js';
|
||||
import { getQiyam } from './getQiyam.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: '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 },
|
||||
{
|
||||
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
|
||||
* @param lat - Latitude in decimal degrees
|
||||
* @param lng - Longitude in decimal degrees
|
||||
* @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
|
||||
* @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,
|
||||
|
|
@ -73,8 +158,17 @@ export function getTimesAll(
|
|||
pressure = 1013.25,
|
||||
hanafi = false,
|
||||
): PrayerTimesAll {
|
||||
// 1. Dynamic angles.
|
||||
const { fajrAngle, ishaAngle } = getAngles(date, lat, lng, elevation, temperature, pressure);
|
||||
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.
|
||||
|
|
@ -102,14 +196,9 @@ export function getTimesAll(
|
|||
const noonTime = spaData.solarNoon;
|
||||
const maghribTime = spaData.sunset;
|
||||
const ishaTime = spaData.angles[1].sunset;
|
||||
const dhuhrTime = noonTime + 2.5 / 60;
|
||||
|
||||
// 4. Solar declination for Asr.
|
||||
const jd = toJulianDate(
|
||||
new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0)),
|
||||
);
|
||||
const { decl } = solarEphemeris(jd);
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export { getAsr } from './getAsr.js';
|
|||
export { getQiyam } from './getQiyam.js';
|
||||
export { getMscFajr, getMscIsha } from './getMSC.js';
|
||||
export { solarEphemeris, toJulianDate } from './getSolarEphemeris.js';
|
||||
export { DHUHR_OFFSET_MINUTES, ANGLE_MIN, ANGLE_MAX } from './constants.js';
|
||||
|
||||
export type {
|
||||
FractionalHours,
|
||||
|
|
|
|||
12
src/types.ts
12
src/types.ts
|
|
@ -57,8 +57,16 @@ export interface FormattedPrayerTimes {
|
|||
angles: TwilightAngles;
|
||||
}
|
||||
|
||||
/** Method entry in the Methods map: [fajrTime, ishaTime] as fractional hours. */
|
||||
export type MethodEntry = [FractionalHours, FractionalHours];
|
||||
/**
|
||||
* Method entry in the Methods map: `[fajrTime, ishaTime]` as fractional hours.
|
||||
*
|
||||
* - Index 0 (`fajr`): Fajr time for this method (fractional hours, or `NaN`)
|
||||
* - Index 1 (`isha`): Isha time for this method (fractional hours, or `NaN`)
|
||||
*
|
||||
* A value of `NaN` indicates the event is unreachable at this location/date
|
||||
* (e.g. the sun never dips to 18° below the horizon at high latitudes in summer).
|
||||
*/
|
||||
export type MethodEntry = [fajr: FractionalHours, isha: FractionalHours];
|
||||
|
||||
/** Prayer times plus all method comparison times as fractional hours. */
|
||||
export interface PrayerTimesAll extends PrayerTimes {
|
||||
|
|
|
|||
23
src/validate.ts
Normal file
23
src/validate.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Input validation for public API boundaries.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validate geographic and atmospheric inputs for prayer time computation.
|
||||
*
|
||||
* @throws {RangeError} if any parameter is out of its valid range
|
||||
*/
|
||||
export function validateInputs(lat: number, lng: number, tz?: number, elevation?: number): void {
|
||||
if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
|
||||
throw new RangeError(`latitude must be between -90 and 90, got ${lat}`);
|
||||
}
|
||||
if (!Number.isFinite(lng) || lng < -180 || lng > 180) {
|
||||
throw new RangeError(`longitude must be between -180 and 180, got ${lng}`);
|
||||
}
|
||||
if (tz !== undefined && (!Number.isFinite(tz) || tz < -14 || tz > 14)) {
|
||||
throw new RangeError(`timezone offset must be between -14 and 14, got ${tz}`);
|
||||
}
|
||||
if (elevation !== undefined && (!Number.isFinite(elevation) || elevation < -500)) {
|
||||
throw new RangeError(`elevation must be >= -500m, got ${elevation}`);
|
||||
}
|
||||
}
|
||||
55
test-cjs.cjs
55
test-cjs.cjs
|
|
@ -6,7 +6,8 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
getTimes,
|
||||
calcTimes,
|
||||
|
|
@ -22,28 +23,13 @@ const {
|
|||
METHODS,
|
||||
} = require('./dist/index.cjs');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ${name}... PASS`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.error(` ${name}... FAIL: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n[CJS] Core exports');
|
||||
|
||||
test('METHODS exported and has 14 entries', () => {
|
||||
describe('[CJS] Core exports', () => {
|
||||
it('METHODS exported and has 14 entries', () => {
|
||||
assert(Array.isArray(METHODS));
|
||||
assert.strictEqual(METHODS.length, 14);
|
||||
});
|
||||
|
||||
test('getTimes returns valid structure', () => {
|
||||
it('getTimes returns valid structure', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 40.7128, -74.0060, -4);
|
||||
assert(isFinite(t.Fajr), `Fajr=${t.Fajr}`);
|
||||
assert(isFinite(t.Sunrise), `Sunrise=${t.Sunrise}`);
|
||||
|
|
@ -52,19 +38,19 @@ test('getTimes returns valid structure', () => {
|
|||
assert(typeof t.angles.fajrAngle === 'number');
|
||||
});
|
||||
|
||||
test('calcTimes returns HH:MM:SS strings', () => {
|
||||
it('calcTimes returns HH:MM:SS strings', () => {
|
||||
const t = calcTimes(new Date('2024-06-21'), 40.7128, -74.0060, -4);
|
||||
assert(/^\d{2}:\d{2}:\d{2}$/.test(t.Fajr), `Fajr="${t.Fajr}"`);
|
||||
assert(/^\d{2}:\d{2}:\d{2}$/.test(t.Sunrise), `Sunrise="${t.Sunrise}"`);
|
||||
assert(/^\d{2}:\d{2}:\d{2}$/.test(t.Maghrib), `Maghrib="${t.Maghrib}"`);
|
||||
});
|
||||
|
||||
test('getTimesAll returns 14 methods', () => {
|
||||
it('getTimesAll returns 14 methods', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 40.7128, -74.0060, -4);
|
||||
assert.strictEqual(Object.keys(t.Methods).length, 14);
|
||||
});
|
||||
|
||||
test('calcTimesAll Methods are string pairs', () => {
|
||||
it('calcTimesAll Methods are string pairs', () => {
|
||||
const t = calcTimesAll(new Date('2024-06-21'), 40.7128, -74.0060, -4);
|
||||
for (const [fajr, isha] of Object.values(t.Methods)) {
|
||||
assert(typeof fajr === 'string');
|
||||
|
|
@ -72,34 +58,34 @@ test('calcTimesAll Methods are string pairs', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('getAngles returns bounded angles', () => {
|
||||
it('getAngles returns bounded angles', () => {
|
||||
const a = getAngles(new Date('2024-06-21'), 40.7128, -74.0060);
|
||||
assert(a.fajrAngle >= 10 && a.fajrAngle <= 22);
|
||||
assert(a.ishaAngle >= 10 && a.ishaAngle <= 22);
|
||||
});
|
||||
|
||||
test('getAsr Hanafi later than Shafii', () => {
|
||||
it('getAsr Hanafi later than Shafii', () => {
|
||||
const s = getAsr(12.0, 40.7, 20.0, false);
|
||||
const h = getAsr(12.0, 40.7, 20.0, true);
|
||||
assert(h > s);
|
||||
});
|
||||
|
||||
test('getQiyam returns a number', () => {
|
||||
it('getQiyam returns a number', () => {
|
||||
const q = getQiyam(4.0, 22.0);
|
||||
assert(typeof q === 'number');
|
||||
});
|
||||
|
||||
test('getMscFajr returns positive minutes', () => {
|
||||
it('getMscFajr returns positive minutes', () => {
|
||||
const m = getMscFajr(new Date('2024-06-21'), 40.7);
|
||||
assert(m > 0);
|
||||
});
|
||||
|
||||
test('getMscIsha returns positive minutes', () => {
|
||||
it('getMscIsha returns positive minutes', () => {
|
||||
const m = getMscIsha(new Date('2024-06-21'), 40.7);
|
||||
assert(m > 0);
|
||||
});
|
||||
|
||||
test('toJulianDate and solarEphemeris work', () => {
|
||||
it('toJulianDate and solarEphemeris work', () => {
|
||||
const jd = toJulianDate(new Date(Date.UTC(2024, 5, 21, 12, 0, 0)));
|
||||
const e = solarEphemeris(jd);
|
||||
assert(typeof e.decl === 'number');
|
||||
|
|
@ -107,16 +93,13 @@ test('toJulianDate and solarEphemeris work', () => {
|
|||
assert(typeof e.eclLon === 'number');
|
||||
});
|
||||
|
||||
test('Makkah all-methods comparison — UAQ Isha = Maghrib + 90min', () => {
|
||||
it('Makkah all-methods comparison — UAQ Isha = Maghrib + 90min', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 21.4225, 39.8262, 3);
|
||||
const diff = (t.Methods.UAQ[1] - t.Maghrib) * 60;
|
||||
assert(Math.abs(diff - 90) < 2, `UAQ isha diff=${diff}`);
|
||||
});
|
||||
|
||||
const total = passed + failed;
|
||||
console.log(`\n${'─'.repeat(50)}`);
|
||||
console.log(`${passed}/${total} CJS tests passed`);
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
it('rejects invalid inputs', () => {
|
||||
assert.throws(() => getTimes(new Date('2024-06-21'), 91, 0, 0), { name: 'RangeError' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
377
test.mjs
377
test.mjs
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* pray-calc v2 test suite — 100 scenarios.
|
||||
* pray-calc v2 test suite.
|
||||
*
|
||||
* Tests cover:
|
||||
* - Equatorial, tropical, mid-latitude, high-latitude locations
|
||||
|
|
@ -7,12 +7,13 @@
|
|||
* - Both Asr conventions (Shafi'i / Hanafi)
|
||||
* - Atmospheric parameters (pressure, temperature, elevation)
|
||||
* - All exported functions
|
||||
* - Edge cases (polar regions, missing events)
|
||||
* - Edge cases (polar regions, missing events, invalid inputs)
|
||||
* - Dynamic vs. traditional method comparison
|
||||
* - Type exports and METHODS array
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
getTimes,
|
||||
calcTimes,
|
||||
|
|
@ -26,36 +27,19 @@ import {
|
|||
solarEphemeris,
|
||||
toJulianDate,
|
||||
METHODS,
|
||||
DHUHR_OFFSET_MINUTES,
|
||||
ANGLE_MIN,
|
||||
ANGLE_MAX,
|
||||
} from './dist/index.mjs';
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ${name}... PASS`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.error(` ${name}... FAIL: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function approx(a, b, tol = 0.05) {
|
||||
// Times within ±tol hours (~3 minutes default tolerance)
|
||||
return Math.abs(a - b) < tol;
|
||||
}
|
||||
|
||||
function approxAngle(a, b, tol = 0.5) {
|
||||
// Angles within ±tol degrees
|
||||
return Math.abs(a - b) < tol;
|
||||
}
|
||||
|
||||
function validTime(t) {
|
||||
return typeof t === 'number' && isFinite(t) && t >= 0 && t < 24;
|
||||
}
|
||||
|
||||
function hm(h, m) {
|
||||
return h + m / 60;
|
||||
}
|
||||
|
|
@ -63,13 +47,12 @@ function hm(h, m) {
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 1: Exports and type structure
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[1] Exports and type structure');
|
||||
|
||||
test('METHODS array has 14 entries', () => {
|
||||
describe('Exports and type structure', () => {
|
||||
it('METHODS array has 14 entries', () => {
|
||||
assert.strictEqual(METHODS.length, 14);
|
||||
});
|
||||
|
||||
test('METHODS has expected IDs', () => {
|
||||
it('METHODS has expected IDs', () => {
|
||||
const ids = METHODS.map(m => m.id);
|
||||
for (const expected of ['UOIF','ISNACA','ISNA','SAMR','IGUT','MWL','DIBT',
|
||||
'Karachi','Kuwait','UAQ','Qatar','Egypt','MUIS','MSC']) {
|
||||
|
|
@ -77,7 +60,7 @@ test('METHODS has expected IDs', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('METHODS fields present', () => {
|
||||
it('METHODS fields present', () => {
|
||||
for (const m of METHODS) {
|
||||
assert(typeof m.id === 'string');
|
||||
assert(typeof m.name === 'string');
|
||||
|
|
@ -87,35 +70,43 @@ test('METHODS fields present', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('MSC method has useMSC=true and null angles', () => {
|
||||
it('MSC method has useMSC=true and null angles', () => {
|
||||
const msc = METHODS.find(m => m.id === 'MSC');
|
||||
assert(msc.useMSC === true);
|
||||
assert(msc.fajrAngle === null);
|
||||
assert(msc.ishaAngle === null);
|
||||
});
|
||||
|
||||
test('UAQ has ishaMinutes=90', () => {
|
||||
it('UAQ has ishaMinutes=90', () => {
|
||||
const uaq = METHODS.find(m => m.id === 'UAQ');
|
||||
assert.strictEqual(uaq.ishaMinutes, 90);
|
||||
});
|
||||
|
||||
test('Qatar has ishaMinutes=90', () => {
|
||||
it('Qatar has ishaMinutes=90', () => {
|
||||
const qatar = METHODS.find(m => m.id === 'Qatar');
|
||||
assert.strictEqual(qatar.ishaMinutes, 90);
|
||||
});
|
||||
|
||||
it('DHUHR_OFFSET_MINUTES is exported and equals 2.5', () => {
|
||||
assert.strictEqual(DHUHR_OFFSET_MINUTES, 2.5);
|
||||
});
|
||||
|
||||
it('ANGLE_MIN and ANGLE_MAX are exported', () => {
|
||||
assert.strictEqual(ANGLE_MIN, 10);
|
||||
assert.strictEqual(ANGLE_MAX, 22);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 2: toJulianDate and solarEphemeris
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[2] Solar ephemeris');
|
||||
|
||||
test('toJulianDate J2000 epoch', () => {
|
||||
// Jan 1.5, 2000 = JD 2451545.0
|
||||
describe('Solar ephemeris', () => {
|
||||
it('toJulianDate J2000 epoch', () => {
|
||||
const jd = toJulianDate(new Date(Date.UTC(2000, 0, 1, 12, 0, 0)));
|
||||
assert(approxAngle(jd, 2451545.0, 1.0), `Got ${jd}`);
|
||||
});
|
||||
|
||||
test('solarEphemeris returns valid structure', () => {
|
||||
it('solarEphemeris returns valid structure', () => {
|
||||
const jd = toJulianDate(new Date(Date.UTC(2024, 5, 21, 12, 0, 0)));
|
||||
const e = solarEphemeris(jd);
|
||||
assert(typeof e.decl === 'number');
|
||||
|
|
@ -123,19 +114,19 @@ test('solarEphemeris returns valid structure', () => {
|
|||
assert(typeof e.eclLon === 'number');
|
||||
});
|
||||
|
||||
test('solarEphemeris summer solstice declination ~+23.44', () => {
|
||||
it('solarEphemeris summer solstice declination ~+23.44', () => {
|
||||
const jd = toJulianDate(new Date(Date.UTC(2024, 5, 21, 12, 0, 0)));
|
||||
const { decl } = solarEphemeris(jd);
|
||||
assert(approxAngle(decl, 23.44, 0.15), `Got decl=${decl}`);
|
||||
});
|
||||
|
||||
test('solarEphemeris winter solstice declination ~-23.44', () => {
|
||||
it('solarEphemeris winter solstice declination ~-23.44', () => {
|
||||
const jd = toJulianDate(new Date(Date.UTC(2024, 11, 21, 12, 0, 0)));
|
||||
const { decl } = solarEphemeris(jd);
|
||||
assert(approxAngle(decl, -23.44, 0.15), `Got decl=${decl}`);
|
||||
});
|
||||
|
||||
test('solarEphemeris r within range [0.98, 1.02] AU', () => {
|
||||
it('solarEphemeris r within range [0.98, 1.02] AU', () => {
|
||||
const dates = [
|
||||
new Date(Date.UTC(2024, 0, 3)), // perihelion
|
||||
new Date(Date.UTC(2024, 6, 4)), // aphelion
|
||||
|
|
@ -147,24 +138,24 @@ test('solarEphemeris r within range [0.98, 1.02] AU', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('solarEphemeris equinox declination near 0', () => {
|
||||
it('solarEphemeris equinox declination near 0', () => {
|
||||
const jd = toJulianDate(new Date(Date.UTC(2024, 2, 20, 12, 0, 0)));
|
||||
const { decl } = solarEphemeris(jd);
|
||||
assert(Math.abs(decl) < 1.0, `Got decl=${decl} at equinox`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 3: getAngles — dynamic Fajr/Isha depression
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[3] getAngles — dynamic depression');
|
||||
|
||||
test('getAngles returns object with fajrAngle and ishaAngle', () => {
|
||||
describe('getAngles — dynamic depression', () => {
|
||||
it('returns object with fajrAngle and ishaAngle', () => {
|
||||
const a = getAngles(new Date('2024-06-21'), 40.7, -74.0);
|
||||
assert(typeof a.fajrAngle === 'number');
|
||||
assert(typeof a.ishaAngle === 'number');
|
||||
});
|
||||
|
||||
test('getAngles angles within physical bounds [10,22]', () => {
|
||||
it('angles within physical bounds [10,22]', () => {
|
||||
const locations = [
|
||||
[0, 0], [21, 39], [40.7, -74], [51.5, -0.1], [55.8, -4.2], [-33.9, 151.2],
|
||||
];
|
||||
|
|
@ -180,125 +171,114 @@ test('getAngles angles within physical bounds [10,22]', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('getAngles equatorial latitude near 18', () => {
|
||||
// Near equator, should converge toward ~18°
|
||||
const { fajrAngle } = getAngles(new Date('2024-06-21'), 1.3, 103.8); // Singapore
|
||||
it('equatorial latitude near 18', () => {
|
||||
const { fajrAngle } = getAngles(new Date('2024-06-21'), 1.3, 103.8);
|
||||
assert(fajrAngle > 16 && fajrAngle < 22, `fajrAngle=${fajrAngle}`);
|
||||
});
|
||||
|
||||
test('getAngles high-latitude summer smaller than 18', () => {
|
||||
// London summer — angle should be well below 18 due to oblique sun path
|
||||
it('high-latitude summer smaller than 18', () => {
|
||||
const { fajrAngle } = getAngles(new Date('2024-06-21'), 51.5, -0.1);
|
||||
assert(fajrAngle < 17, `Expected <17, got ${fajrAngle} at London summer solstice`);
|
||||
});
|
||||
|
||||
test('getAngles elevation parameter accepted', () => {
|
||||
it('elevation parameter accepted', () => {
|
||||
const a1 = getAngles(new Date('2024-06-21'), 40.7, -74.0, 0);
|
||||
const a2 = getAngles(new Date('2024-06-21'), 40.7, -74.0, 1000);
|
||||
assert(typeof a1.fajrAngle === 'number');
|
||||
assert(typeof a2.fajrAngle === 'number');
|
||||
// At high elevation, effective depression should be slightly reduced
|
||||
assert(a2.fajrAngle <= a1.fajrAngle + 0.5, 'Elevation should not increase angle by more than 0.5');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 4: getAsr
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[4] getAsr');
|
||||
|
||||
test('getAsr Shafii returns finite time', () => {
|
||||
describe('getAsr', () => {
|
||||
it('Shafii returns finite time', () => {
|
||||
const asr = getAsr(12.0, 40.7128, 20.0, false);
|
||||
assert(isFinite(asr), `Expected finite, got ${asr}`);
|
||||
});
|
||||
|
||||
test('getAsr Hanafi is later than Shafii', () => {
|
||||
it('Hanafi is later than Shafii', () => {
|
||||
const asrS = getAsr(12.0, 40.7, 20.0, false);
|
||||
const asrH = getAsr(12.0, 40.7, 20.0, true);
|
||||
assert(asrH > asrS, `Hanafi ${asrH} should be later than Shafi'i ${asrS}`);
|
||||
});
|
||||
|
||||
test('getAsr reasonable range (afternoon)', () => {
|
||||
const asr = getAsr(12.1, 21.4, 20.0, false); // Makkah-ish
|
||||
it('reasonable range (afternoon)', () => {
|
||||
const asr = getAsr(12.1, 21.4, 20.0, false);
|
||||
assert(asr > 14 && asr < 18, `Got ${asr}`);
|
||||
});
|
||||
|
||||
test('getAsr Hanafi Makkah afternoon', () => {
|
||||
it('Hanafi Makkah afternoon', () => {
|
||||
const asr = getAsr(12.1, 21.4, 20.0, true);
|
||||
assert(asr > 15 && asr < 19, `Got ${asr}`);
|
||||
});
|
||||
|
||||
test('getAsr returns NaN when sun never reaches altitude', () => {
|
||||
// Extreme case: very high latitude, extreme declination
|
||||
it('returns NaN when sun never reaches altitude', () => {
|
||||
const asr = getAsr(12.0, 89.0, -23.4, false);
|
||||
// Near north pole in winter, sun may not reach Asr altitude
|
||||
// Result should be NaN or finite — just verify it returns a number
|
||||
assert(typeof asr === 'number', 'Should return a number');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 5: getQiyam
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[5] getQiyam');
|
||||
|
||||
test('getQiyam returns last-third start', () => {
|
||||
// Isha at 22:00, Fajr at 04:00 next day → night = 6h
|
||||
// Last third starts at 22 + 4 = 02:00
|
||||
describe('getQiyam', () => {
|
||||
it('returns last-third start', () => {
|
||||
const q = getQiyam(4.0, 22.0);
|
||||
assert(approx(q, 2.0, 0.1), `Got ${q}`);
|
||||
});
|
||||
|
||||
test('getQiyam handles wrap-around midnight', () => {
|
||||
it('handles wrap-around midnight', () => {
|
||||
const q = getQiyam(3.5, 21.0);
|
||||
// Night = 3.5 + 24 - 21 = 6.5h; last third = 21 + (2/3)*6.5 = 25.33 → 1.33 (01:20)
|
||||
const expected = 21.0 + (2 / 3) * (3.5 + 24 - 21.0);
|
||||
const normalized = expected >= 24 ? expected - 24 : expected;
|
||||
assert(approx(q, normalized, 0.1), `Got ${q}, expected ~${normalized}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 6: getMscFajr / getMscIsha
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[6] MSC minute offsets');
|
||||
|
||||
test('getMscFajr returns positive minutes', () => {
|
||||
describe('MSC minute offsets', () => {
|
||||
it('getMscFajr returns positive minutes', () => {
|
||||
const m = getMscFajr(new Date('2024-06-21'), 40.7);
|
||||
assert(m > 0, `Got ${m}`);
|
||||
});
|
||||
|
||||
test('getMscIsha returns positive minutes', () => {
|
||||
it('getMscIsha returns positive minutes', () => {
|
||||
const m = getMscIsha(new Date('2024-06-21'), 40.7);
|
||||
assert(m > 0, `Got ${m}`);
|
||||
});
|
||||
|
||||
test('getMscFajr increases with latitude (summer)', () => {
|
||||
it('getMscFajr increases with latitude (summer)', () => {
|
||||
const m30 = getMscFajr(new Date('2024-06-21'), 30);
|
||||
const m50 = getMscFajr(new Date('2024-06-21'), 50);
|
||||
assert(m50 > m30, `Expected lat50 (${m50}) > lat30 (${m30})`);
|
||||
});
|
||||
|
||||
test('getMscFajr equator ~75 minutes year-round', () => {
|
||||
it('getMscFajr equator ~75 minutes year-round', () => {
|
||||
const summer = getMscFajr(new Date('2024-06-21'), 0);
|
||||
const winter = getMscFajr(new Date('2024-12-21'), 0);
|
||||
assert(approx(summer, 75, 5), `Summer: ${summer}`);
|
||||
assert(approx(winter, 75, 5), `Winter: ${winter}`);
|
||||
});
|
||||
|
||||
test('getMscIsha shafaq modes return different values at high lat', () => {
|
||||
it('getMscIsha shafaq modes return different values at high lat', () => {
|
||||
const general = getMscIsha(new Date('2024-06-21'), 51.5, 'general');
|
||||
const ahmer = getMscIsha(new Date('2024-06-21'), 51.5, 'ahmer');
|
||||
const abyad = getMscIsha(new Date('2024-06-21'), 51.5, 'abyad');
|
||||
// All should be positive
|
||||
assert(general > 0 && ahmer > 0 && abyad > 0);
|
||||
// Ahmer (red glow) ends earlier, so fewer minutes after sunset
|
||||
assert(ahmer <= general, `ahmer ${ahmer} should be <= general ${general}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 7: getTimes — core output structure
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[7] getTimes — structure');
|
||||
|
||||
test('getTimes returns all required fields', () => {
|
||||
describe('getTimes — structure', () => {
|
||||
it('returns all required fields', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 40.7, -74.0);
|
||||
for (const field of ['Qiyam','Fajr','Sunrise','Noon','Dhuhr','Asr','Maghrib','Isha']) {
|
||||
assert(field in t, `Missing field: ${field}`);
|
||||
|
|
@ -308,11 +288,8 @@ test('getTimes returns all required fields', () => {
|
|||
assert('ishaAngle' in t.angles);
|
||||
});
|
||||
|
||||
test('getTimes chronological order', () => {
|
||||
// Use explicit tz=-4 (EDT) so CI (UTC) and local machines give identical results.
|
||||
// Without it, NY's Maghrib falls past UTC midnight, wrapping to ~0.5h < Asr(~21h).
|
||||
it('chronological order', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
// Fajr < Sunrise < Noon < Dhuhr ≈ Noon < Asr < Maghrib < Isha
|
||||
assert(t.Fajr < t.Sunrise, `Fajr(${t.Fajr}) < Sunrise(${t.Sunrise})`);
|
||||
assert(t.Sunrise < t.Noon, `Sunrise(${t.Sunrise}) < Noon(${t.Noon})`);
|
||||
assert(t.Noon <= t.Dhuhr, `Noon(${t.Noon}) <= Dhuhr(${t.Dhuhr})`);
|
||||
|
|
@ -321,76 +298,71 @@ test('getTimes chronological order', () => {
|
|||
assert(t.Maghrib < t.Isha, `Maghrib(${t.Maghrib}) < Isha(${t.Isha})`);
|
||||
});
|
||||
|
||||
test('getTimes Dhuhr is slightly after Noon', () => {
|
||||
it('Dhuhr is slightly after Noon', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 40.7, -74.0);
|
||||
const diff = (t.Dhuhr - t.Noon) * 60; // minutes
|
||||
const diff = (t.Dhuhr - t.Noon) * 60;
|
||||
assert(diff > 2 && diff < 4, `Dhuhr - Noon = ${diff} min`);
|
||||
});
|
||||
|
||||
test('getTimes angles present and in bounds', () => {
|
||||
it('angles present and in bounds', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 40.7, -74.0);
|
||||
assert(t.angles.fajrAngle > 10 && t.angles.fajrAngle < 22);
|
||||
assert(t.angles.ishaAngle > 10 && t.angles.ishaAngle < 22);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 8: getTimes — geographic validation
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[8] getTimes — geographic scenarios');
|
||||
|
||||
// Reference times from independent sources (tolerances ±4 min = 0.067h)
|
||||
describe('getTimes — geographic scenarios', () => {
|
||||
const TOL = 0.07; // ~4 minutes
|
||||
|
||||
test('Makkah summer solstice — Sunrise ~05:39', () => {
|
||||
// Makkah 39.83°E, UTC+3: solar noon ~12:23 local. Sunrise ~5:39.
|
||||
it('Makkah summer solstice — Sunrise ~05:39', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 21.4225, 39.8262, 3);
|
||||
assert(approx(t.Sunrise, hm(5,39), 0.12), `Got ${t.Sunrise}`);
|
||||
});
|
||||
|
||||
test('Makkah summer solstice — Maghrib ~19:06', () => {
|
||||
// Makkah summer solstice sunset: ~19:06-19:10 local.
|
||||
it('Makkah summer solstice — Maghrib ~19:06', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 21.4225, 39.8262, 3);
|
||||
assert(approx(t.Maghrib, hm(19,7), 0.12), `Got ${t.Maghrib}`);
|
||||
});
|
||||
|
||||
test('New York summer solstice — Sunrise ~05:25', () => {
|
||||
it('New York summer solstice — Sunrise ~05:25', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 40.7128, -74.0060, -4);
|
||||
assert(approx(t.Sunrise, hm(5,25), TOL), `Got ${t.Sunrise}`);
|
||||
});
|
||||
|
||||
test('New York summer solstice — Sunset ~20:31', () => {
|
||||
it('New York summer solstice — Sunset ~20:31', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 40.7128, -74.0060, -4);
|
||||
assert(approx(t.Maghrib, hm(20,31), TOL), `Got ${t.Maghrib}`);
|
||||
});
|
||||
|
||||
test('New York winter solstice — Sunrise ~07:20', () => {
|
||||
it('New York winter solstice — Sunrise ~07:20', () => {
|
||||
const t = getTimes(new Date('2024-12-21'), 40.7128, -74.0060, -5);
|
||||
assert(approx(t.Sunrise, hm(7,20), TOL), `Got ${t.Sunrise}`);
|
||||
});
|
||||
|
||||
test('New York winter solstice — Sunset ~16:32', () => {
|
||||
it('New York winter solstice — Sunset ~16:32', () => {
|
||||
const t = getTimes(new Date('2024-12-21'), 40.7128, -74.0060, -5);
|
||||
assert(approx(t.Maghrib, hm(16,32), TOL), `Got ${t.Maghrib}`);
|
||||
});
|
||||
|
||||
test('London summer — Sunrise ~04:43', () => {
|
||||
it('London summer — Sunrise ~04:43', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 51.5074, -0.1278, 1);
|
||||
assert(approx(t.Sunrise, hm(4,43), TOL), `Got ${t.Sunrise}`);
|
||||
});
|
||||
|
||||
test('London summer — Sunset ~21:21', () => {
|
||||
it('London summer — Sunset ~21:21', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 51.5074, -0.1278, 1);
|
||||
assert(approx(t.Maghrib, hm(21,21), TOL), `Got ${t.Maghrib}`);
|
||||
});
|
||||
|
||||
test('Sydney summer (Gregorian Jan) — Sunrise ~06:00', () => {
|
||||
// Sydney 151.21°E, UTC+11: solar noon ~12:04. Sunrise ~5:59-6:01 Jan 15.
|
||||
it('Sydney summer (Gregorian Jan) — Sunrise ~06:00', () => {
|
||||
const t = getTimes(new Date('2024-01-15'), -33.8688, 151.2093, 11);
|
||||
assert(approx(t.Sunrise, hm(6,0), 0.12), `Got ${t.Sunrise}`);
|
||||
});
|
||||
|
||||
test('Jakarta — Sunrise within 20min of 5:50 year-round', () => {
|
||||
// Jakarta 106.85°E, UTC+7: sunrise varies 5:30-6:10 across the year.
|
||||
it('Jakarta — Sunrise within 20min of 5:50 year-round', () => {
|
||||
for (const month of [1, 4, 7, 10]) {
|
||||
const t = getTimes(new Date(`2024-${String(month).padStart(2,'0')}-15`),
|
||||
-6.2088, 106.8456, 7);
|
||||
|
|
@ -398,68 +370,61 @@ test('Jakarta — Sunrise within 20min of 5:50 year-round', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('Singapore — all times finite', () => {
|
||||
it('Singapore — all times finite', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 1.3521, 103.8198, 8);
|
||||
for (const field of ['Fajr','Sunrise','Noon','Dhuhr','Asr','Maghrib','Isha']) {
|
||||
assert(isFinite(t[field]), `${field} should be finite`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Cairo summer — Sunrise ~06:00 ±12min', () => {
|
||||
it('Cairo summer — Sunrise ~06:00 ±12min', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 30.0444, 31.2357, 3);
|
||||
assert(approx(t.Sunrise, hm(6, 0), 0.20), `Got ${t.Sunrise}`);
|
||||
});
|
||||
|
||||
test('Istanbul spring equinox — Noon ~13:11 ±10min', () => {
|
||||
// Istanbul 28.98°E, UTC+3: solar noon = 12:00 + (45-28.98)/15 = 13:04 + eq-of-time ~7min = ~13:11
|
||||
it('Istanbul spring equinox — Noon ~13:11 ±10min', () => {
|
||||
const t = getTimes(new Date('2024-03-20'), 41.0082, 28.9784, 3);
|
||||
assert(approx(t.Noon, hm(13,11), 0.17), `Got ${t.Noon}`);
|
||||
});
|
||||
|
||||
test('Karachi summer — Maghrib ~19:20 ±10min', () => {
|
||||
it('Karachi summer — Maghrib ~19:20 ±10min', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 24.8607, 67.0011, 5);
|
||||
assert(approx(t.Maghrib, hm(19,20), 0.17), `Got ${t.Maghrib}`);
|
||||
});
|
||||
|
||||
test('Toronto summer — Sunset ~21:02 ±12min', () => {
|
||||
// Toronto 79.38°W, UTC-4: solar noon ~13:17. Sunset June 21 ~21:00-21:04.
|
||||
it('Toronto summer — Sunset ~21:02 ±12min', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 43.6532, -79.3832, -4);
|
||||
assert(approx(t.Maghrib, hm(21,2), 0.22), `Got ${t.Maghrib}`);
|
||||
});
|
||||
|
||||
test('Reykjavik summer — Sunrise and Maghrib finite', () => {
|
||||
// ~64°N — high latitude, Midnight Sun territory
|
||||
it('Reykjavik summer — Sunrise and Maghrib finite', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 64.1265, -21.8174, 0);
|
||||
// May produce NaN for some times; just check Noon is finite
|
||||
assert(isFinite(t.Noon), `Noon should be finite`);
|
||||
});
|
||||
|
||||
test('South pole winter — Noon finite', () => {
|
||||
it('South pole winter — Noon finite', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), -90, 0, 0);
|
||||
// Extreme case — just should not throw
|
||||
assert(typeof t.Noon === 'number');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 9: getTimes — seasonal variation at fixed location
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[9] getTimes — seasonal variation');
|
||||
|
||||
test('NY Sunrise earlier in summer than winter', () => {
|
||||
describe('getTimes — seasonal variation', () => {
|
||||
it('NY Sunrise earlier in summer than winter', () => {
|
||||
const summer = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4).Sunrise;
|
||||
const winter = getTimes(new Date('2024-12-21'), 40.7, -74.0, -5).Sunrise;
|
||||
assert(summer < winter, `Summer ${summer} < Winter ${winter}`);
|
||||
});
|
||||
|
||||
test('NY Sunset later in summer than winter', () => {
|
||||
it('NY Sunset later in summer than winter', () => {
|
||||
const summer = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4).Maghrib;
|
||||
const winter = getTimes(new Date('2024-12-21'), 40.7, -74.0, -5).Maghrib;
|
||||
assert(summer > winter, `Summer ${summer} > Winter ${winter}`);
|
||||
});
|
||||
|
||||
test('Noon time consistent across seasons (same tz, within 30 min)', () => {
|
||||
// Use EST (-5) for all dates to avoid EDT/EST offset masking the comparison.
|
||||
// Equation of time spans ±16 min; NY longitude offset is fixed. Max variation ~30 min.
|
||||
it('Noon time consistent across seasons (same tz, within 30 min)', () => {
|
||||
const base = getTimes(new Date('2024-06-21'), 40.7, -74.0, -5).Noon;
|
||||
for (const d of ['2024-01-15','2024-04-01','2024-09-22','2024-12-21']) {
|
||||
const t = getTimes(new Date(d), 40.7, -74.0, -5).Noon;
|
||||
|
|
@ -467,23 +432,23 @@ test('Noon time consistent across seasons (same tz, within 30 min)', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('Fajr angle smaller in London summer than London winter', () => {
|
||||
it('Fajr angle smaller in London summer than London winter', () => {
|
||||
const summer = getAngles(new Date('2024-06-21'), 51.5, -0.1).fajrAngle;
|
||||
const winter = getAngles(new Date('2024-12-21'), 51.5, -0.1).fajrAngle;
|
||||
assert(summer < winter, `Summer ${summer} should be < Winter ${winter}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 10: Hanafi vs Shafi'i
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[10] Asr convention');
|
||||
|
||||
test('Hanafi Asr later than Shafii at multiple locations', () => {
|
||||
describe('Asr convention', () => {
|
||||
it('Hanafi Asr later than Shafii at multiple locations', () => {
|
||||
const locations = [
|
||||
[40.7, -74.0, -4], // New York
|
||||
[21.4, 39.8, 3], // Makkah
|
||||
[51.5, -0.1, 1], // London
|
||||
[-33.9, 151.2, 10], // Sydney
|
||||
[40.7, -74.0, -4],
|
||||
[21.4, 39.8, 3],
|
||||
[51.5, -0.1, 1],
|
||||
[-33.9, 151.2, 10],
|
||||
];
|
||||
for (const [lat, lng, tz] of locations) {
|
||||
const tS = getTimes(new Date('2024-06-21'), lat, lng, tz, 0, 15, 1013.25, false);
|
||||
|
|
@ -493,43 +458,41 @@ test('Hanafi Asr later than Shafii at multiple locations', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('Hanafi-Shafii difference 20-85 min at typical latitudes', () => {
|
||||
// At high summer latitudes (long day), the shadow-ratio difference can reach ~75 min.
|
||||
it('Hanafi-Shafii difference 20-85 min at typical latitudes', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
const tH = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4, 0, 15, 1013.25, true);
|
||||
const diffMin = (tH.Asr - t.Asr) * 60;
|
||||
assert(diffMin > 20 && diffMin < 85, `Difference ${diffMin} min`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 11: Atmospheric parameters
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[11] Atmospheric parameters');
|
||||
|
||||
test('Higher elevation brings Sunrise earlier', () => {
|
||||
describe('Atmospheric parameters', () => {
|
||||
it('Higher elevation brings Sunrise earlier', () => {
|
||||
const t0 = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4, 0);
|
||||
const t1 = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4, 2000);
|
||||
assert(t1.Sunrise <= t0.Sunrise, `High-elevation sunrise (${t1.Sunrise}) should be <= sea level (${t0.Sunrise})`);
|
||||
});
|
||||
|
||||
test('Temperature and pressure accepted without error', () => {
|
||||
it('Temperature and pressure accepted without error', () => {
|
||||
const t = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4, 100, 5, 950);
|
||||
assert(isFinite(t.Sunrise));
|
||||
});
|
||||
|
||||
test('Extreme cold reduces refraction slightly', () => {
|
||||
it('Extreme cold reduces refraction slightly', () => {
|
||||
const tHot = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4, 0, 40, 1013.25);
|
||||
const tCold = getTimes(new Date('2024-06-21'), 40.7, -74.0, -4, 0, -20, 1013.25);
|
||||
// Both should return finite values
|
||||
assert(isFinite(tHot.Sunrise) && isFinite(tCold.Sunrise));
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 12: calcTimes — formatted output
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[12] calcTimes — formatting');
|
||||
|
||||
test('calcTimes returns HH:MM:SS strings', () => {
|
||||
describe('calcTimes — formatting', () => {
|
||||
it('returns HH:MM:SS strings', () => {
|
||||
const t = calcTimes(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
const timeRe = /^\d{2}:\d{2}:\d{2}$/;
|
||||
for (const field of ['Fajr','Sunrise','Noon','Dhuhr','Asr','Maghrib','Isha']) {
|
||||
|
|
@ -537,39 +500,38 @@ test('calcTimes returns HH:MM:SS strings', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('calcTimes Qiyam returns HH:MM:SS or N/A', () => {
|
||||
it('Qiyam returns HH:MM:SS or N/A', () => {
|
||||
const t = calcTimes(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
assert(t.Qiyam === 'N/A' || /^\d{2}:\d{2}:\d{2}$/.test(t.Qiyam),
|
||||
`Qiyam="${t.Qiyam}"`);
|
||||
});
|
||||
|
||||
test('calcTimes angles preserved correctly', () => {
|
||||
it('angles preserved correctly', () => {
|
||||
const t = calcTimes(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
assert(typeof t.angles.fajrAngle === 'number');
|
||||
assert(typeof t.angles.ishaAngle === 'number');
|
||||
});
|
||||
|
||||
test('calcTimes default timezone matches getTimes', () => {
|
||||
it('default timezone matches getTimes', () => {
|
||||
const date = new Date('2024-06-21T12:00:00.000Z');
|
||||
const raw = getTimes(date, 40.7, -74.0);
|
||||
const fmt = calcTimes(date, 40.7, -74.0);
|
||||
// Sunrise should parse to same fractional hour
|
||||
const [h, m, s] = fmt.Sunrise.split(':').map(Number);
|
||||
const parsed = h + m / 60 + s / 3600;
|
||||
assert(approx(parsed, raw.Sunrise, 0.005), `Parsed ${parsed}, raw ${raw.Sunrise}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 13: getTimesAll — method comparison
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[13] getTimesAll — method comparison');
|
||||
|
||||
test('getTimesAll returns Methods map with 14 entries', () => {
|
||||
describe('getTimesAll — method comparison', () => {
|
||||
it('returns Methods map with 14 entries', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
assert.strictEqual(Object.keys(t.Methods).length, 14);
|
||||
});
|
||||
|
||||
test('getTimesAll Methods entries are [number, number]', () => {
|
||||
it('Methods entries are [number, number]', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
for (const [id, [fajr, isha]] of Object.entries(t.Methods)) {
|
||||
assert(typeof fajr === 'number', `${id} fajr is not a number`);
|
||||
|
|
@ -577,32 +539,29 @@ test('getTimesAll Methods entries are [number, number]', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('getTimesAll ISNA Fajr is finite at NY summer', () => {
|
||||
it('ISNA Fajr is finite at NY summer', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
assert(isFinite(t.Methods.ISNA[0]), `ISNA Fajr=${t.Methods.ISNA[0]}`);
|
||||
});
|
||||
|
||||
test('getTimesAll MWL Isha at London summer may be NaN (18° fails)', () => {
|
||||
it('MWL Isha at London summer may be NaN (18° fails)', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 51.5, -0.1, 1);
|
||||
// MWL uses 17° Isha. London summer — may or may not reach it.
|
||||
// Just verify it's a number (finite or NaN)
|
||||
assert(typeof t.Methods.MWL[1] === 'number');
|
||||
});
|
||||
|
||||
test('getTimesAll UAQ Isha = Maghrib + 90min', () => {
|
||||
it('UAQ Isha = Maghrib + 90min', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 21.4, 39.8, 3);
|
||||
const diff = (t.Methods.UAQ[1] - t.Maghrib) * 60;
|
||||
assert(approx(diff, 90, 2), `UAQ isha diff=${diff} min, expected 90`);
|
||||
});
|
||||
|
||||
test('getTimesAll Qatar Isha = Maghrib + 90min', () => {
|
||||
it('Qatar Isha = Maghrib + 90min', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 25.3, 51.5, 3);
|
||||
const diff = (t.Methods.Qatar[1] - t.Maghrib) * 60;
|
||||
assert(approx(diff, 90, 2), `Qatar isha diff=${diff} min, expected 90`);
|
||||
});
|
||||
|
||||
test('getTimesAll higher-angle methods have earlier Fajr', () => {
|
||||
// MUIS (20°) should give earlier Fajr than ISNA (15°)
|
||||
it('higher-angle methods have earlier Fajr', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 1.3, 103.8, 8);
|
||||
const muis = t.Methods.MUIS[0];
|
||||
const isna = t.Methods.ISNA[0];
|
||||
|
|
@ -611,20 +570,17 @@ test('getTimesAll higher-angle methods have earlier Fajr', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('getTimesAll dynamic Fajr within method range', () => {
|
||||
// Higher depression angle = earlier Fajr. Dynamic (14.8°) falls between 12° (UOIF, latest)
|
||||
// and 18° (Karachi, earliest). So: Karachi[0] <= dynamic <= UOIF[0].
|
||||
it('dynamic Fajr within method range', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
const earliest = t.Methods.Karachi[0]; // 18° → earliest Fajr
|
||||
const latest = t.Methods.UOIF[0]; // 12° → latest Fajr
|
||||
const earliest = t.Methods.Karachi[0];
|
||||
const latest = t.Methods.UOIF[0];
|
||||
if (isFinite(earliest) && isFinite(latest)) {
|
||||
assert(t.Fajr >= earliest - 0.10 && t.Fajr <= latest + 0.10,
|
||||
`Dynamic Fajr ${t.Fajr} not between Karachi=${earliest} and UOIF=${latest}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('getTimesAll MSC and dynamic are close', () => {
|
||||
// MSC is the base for the dynamic method — they should be within ~20 minutes
|
||||
it('MSC and dynamic are close', () => {
|
||||
const t = getTimesAll(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
const mscFajr = t.Methods.MSC[0];
|
||||
const dynFajr = t.Fajr;
|
||||
|
|
@ -633,20 +589,20 @@ test('getTimesAll MSC and dynamic are close', () => {
|
|||
assert(diffMin < 25, `MSC Fajr (${mscFajr}) vs Dynamic Fajr (${dynFajr}) = ${diffMin} min`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 14: calcTimesAll — formatted all methods
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[14] calcTimesAll');
|
||||
|
||||
test('calcTimesAll returns formatted strings', () => {
|
||||
describe('calcTimesAll', () => {
|
||||
it('returns formatted strings', () => {
|
||||
const t = calcTimesAll(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
const timeRe = /^\d{2}:\d{2}:\d{2}$/;
|
||||
assert(timeRe.test(t.Fajr), `Fajr="${t.Fajr}"`);
|
||||
assert(timeRe.test(t.Maghrib), `Maghrib="${t.Maghrib}"`);
|
||||
});
|
||||
|
||||
test('calcTimesAll Methods entries are [string, string]', () => {
|
||||
it('Methods entries are [string, string]', () => {
|
||||
const t = calcTimesAll(new Date('2024-06-21'), 40.7, -74.0, -4);
|
||||
for (const [id, [fajr, isha]] of Object.entries(t.Methods)) {
|
||||
assert(typeof fajr === 'string', `${id} fajr is not a string`);
|
||||
|
|
@ -654,60 +610,55 @@ test('calcTimesAll Methods entries are [string, string]', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('calcTimesAll N/A for unreachable events', () => {
|
||||
// At very high lat summer, some 18° methods may be N/A
|
||||
it('N/A for unreachable events', () => {
|
||||
const t = calcTimesAll(new Date('2024-06-21'), 58.0, 25.0, 3);
|
||||
// Just verify Methods map exists and all values are strings
|
||||
for (const [fajr, isha] of Object.values(t.Methods)) {
|
||||
assert(typeof fajr === 'string');
|
||||
assert(typeof isha === 'string');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 15: Multi-year and edge date coverage
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[15] Date coverage');
|
||||
|
||||
test('Works across multiple years', () => {
|
||||
describe('Date coverage', () => {
|
||||
it('works across multiple years', () => {
|
||||
for (const year of [2020, 2022, 2024, 2025, 2026]) {
|
||||
const t = getTimes(new Date(`${year}-06-21`), 40.7, -74.0, -4);
|
||||
assert(isFinite(t.Sunrise), `Year ${year} Sunrise not finite`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Works on Feb 29 in leap year', () => {
|
||||
it('works on Feb 29 in leap year', () => {
|
||||
const t = getTimes(new Date('2024-02-29'), 40.7, -74.0, -5);
|
||||
assert(isFinite(t.Fajr), 'Feb 29 Fajr not finite');
|
||||
});
|
||||
|
||||
test('Works on Dec 31', () => {
|
||||
it('works on Dec 31', () => {
|
||||
const t = getTimes(new Date('2024-12-31'), 40.7, -74.0, -5);
|
||||
assert(isFinite(t.Sunrise));
|
||||
});
|
||||
|
||||
test('Works on Jan 1', () => {
|
||||
it('works on Jan 1', () => {
|
||||
const t = getTimes(new Date('2024-01-01'), 40.7, -74.0, -5);
|
||||
assert(isFinite(t.Sunrise));
|
||||
});
|
||||
|
||||
test('Both equinoxes consistent', () => {
|
||||
// NY 74°W, UTC-4 (EDT in both March 20 and Sep 22): solar noon ~12:56 EDT.
|
||||
// At equinox, day ≈ 12h, sunrise ≈ noon − 6h ≈ 6:56 EDT.
|
||||
it('both equinoxes consistent', () => {
|
||||
const t1 = getTimes(new Date('2024-03-20'), 40.7, -74.0, -4);
|
||||
const t2 = getTimes(new Date('2024-09-22'), 40.7, -74.0, -4);
|
||||
assert(approx(t1.Sunrise, hm(6,57), 0.30), `Spring equinox Sunrise ${t1.Sunrise}`);
|
||||
assert(approx(t2.Sunrise, hm(6,54), 0.30), `Autumn equinox Sunrise ${t2.Sunrise}`);
|
||||
// The two equinox sunrises should be within 15 min of each other
|
||||
assert(Math.abs(t1.Sunrise - t2.Sunrise) < 0.25,
|
||||
`Equinox sunrises differ by ${Math.abs(t1.Sunrise - t2.Sunrise) * 60} min`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 16: Global coverage — additional locations
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[16] Global coverage');
|
||||
|
||||
describe('Global coverage', () => {
|
||||
const globalLocations = [
|
||||
{ name: 'Dubai', lat: 25.2048, lng: 55.2708, tz: 4, date: '2024-06-21' },
|
||||
{ name: 'Kuala Lumpur', lat: 3.1390, lng: 101.6869, tz: 8, date: '2024-06-21' },
|
||||
|
|
@ -722,53 +673,79 @@ const globalLocations = [
|
|||
];
|
||||
|
||||
for (const loc of globalLocations) {
|
||||
test(`${loc.name} — all times numeric`, () => {
|
||||
it(`${loc.name} — all times numeric`, () => {
|
||||
const t = getTimes(new Date(loc.date), loc.lat, loc.lng, loc.tz);
|
||||
assert(typeof t.Fajr === 'number', `Fajr: ${t.Fajr}`);
|
||||
assert(typeof t.Noon === 'number', `Noon: ${t.Noon}`);
|
||||
assert(typeof t.Maghrib === 'number', `Maghrib: ${t.Maghrib}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Section 17: Winter scenarios
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
console.log('\n[17] Winter scenarios');
|
||||
|
||||
test('London winter — all core times finite', () => {
|
||||
describe('Winter scenarios', () => {
|
||||
it('London winter — all core times finite', () => {
|
||||
const t = getTimes(new Date('2024-12-21'), 51.5, -0.1, 0);
|
||||
for (const f of ['Fajr','Sunrise','Noon','Dhuhr','Asr','Maghrib','Isha']) {
|
||||
assert(isFinite(t[f]), `${f}=${t[f]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Moscow winter — all core times finite', () => {
|
||||
it('Moscow winter — all core times finite', () => {
|
||||
const t = getTimes(new Date('2024-12-21'), 55.8, 37.6, 3);
|
||||
for (const f of ['Fajr','Sunrise','Noon','Dhuhr','Asr','Maghrib','Isha']) {
|
||||
assert(isFinite(t[f]), `${f}=${t[f]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Oslo winter — Noon finite', () => {
|
||||
it('Oslo winter — Noon finite', () => {
|
||||
const t = getTimes(new Date('2024-12-21'), 59.9, 10.8, 1);
|
||||
assert(isFinite(t.Noon));
|
||||
});
|
||||
|
||||
test('Oslo winter — Sunrise, Sunset near solstice values', () => {
|
||||
it('Oslo winter — Sunrise, Sunset near solstice values', () => {
|
||||
const t = getTimes(new Date('2024-12-21'), 59.9, 10.8, 1);
|
||||
// Oslo Dec 21: Sunrise ~09:18, Sunset ~15:12
|
||||
if (isFinite(t.Sunrise)) {
|
||||
assert(approx(t.Sunrise, hm(9,18), 0.25), `Oslo Sunrise ${t.Sunrise}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Summary
|
||||
// Section 18: Input validation
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
const total = passed + failed;
|
||||
console.log(`\n${'─'.repeat(50)}`);
|
||||
console.log(`${passed}/${total} tests passed`);
|
||||
describe('Input validation', () => {
|
||||
it('rejects latitude > 90', () => {
|
||||
assert.throws(() => getTimes(new Date('2024-06-21'), 91, 0, 0), { name: 'RangeError' });
|
||||
});
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
it('rejects latitude < -90', () => {
|
||||
assert.throws(() => getTimes(new Date('2024-06-21'), -91, 0, 0), { name: 'RangeError' });
|
||||
});
|
||||
|
||||
it('rejects longitude > 180', () => {
|
||||
assert.throws(() => getTimes(new Date('2024-06-21'), 0, 181, 0), { name: 'RangeError' });
|
||||
});
|
||||
|
||||
it('rejects longitude < -180', () => {
|
||||
assert.throws(() => getTimes(new Date('2024-06-21'), 0, -181, 0), { name: 'RangeError' });
|
||||
});
|
||||
|
||||
it('rejects timezone > 14', () => {
|
||||
assert.throws(() => getTimes(new Date('2024-06-21'), 0, 0, 15), { name: 'RangeError' });
|
||||
});
|
||||
|
||||
it('rejects NaN latitude', () => {
|
||||
assert.throws(() => getTimes(new Date('2024-06-21'), NaN, 0, 0), { name: 'RangeError' });
|
||||
});
|
||||
|
||||
it('rejects Infinity longitude', () => {
|
||||
assert.throws(() => getTimes(new Date('2024-06-21'), 0, Infinity, 0), { name: 'RangeError' });
|
||||
});
|
||||
|
||||
it('getTimesAll also validates', () => {
|
||||
assert.throws(() => getTimesAll(new Date('2024-06-21'), 91, 0, 0), { name: 'RangeError' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue