mihrab-methods 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +23 -6
  2. package/certification.json +23 -23
  3. package/data/corrections/indonesia.json +528 -132
  4. package/data/corrections/morocco.json +201 -914
  5. package/dist/cjs/authorities/indonesia.js +4 -4
  6. package/dist/cjs/authorities/morocco.js +7 -6
  7. package/dist/cjs/core/compute.js +1 -1
  8. package/dist/cjs/data/generated/algeriaCertification.js +1 -1
  9. package/dist/cjs/data/generated/diyanetCertification.js +1 -1
  10. package/dist/cjs/data/generated/diyanetWorldPoints.js +1 -1
  11. package/dist/cjs/data/generated/indonesiaCertification.js +1 -1
  12. package/dist/cjs/data/generated/indonesiaCorrections.js +1 -1
  13. package/dist/cjs/data/generated/kemenagMethodPoints.js +1 -1
  14. package/dist/cjs/data/generated/moroccoCertification.js +1 -1
  15. package/dist/cjs/data/generated/moroccoCorrections.js +1 -1
  16. package/dist/cjs/data/generated/moroccoMethodPoints.js +1 -1
  17. package/dist/cjs/data/generated/swedenCertification.js +1 -1
  18. package/dist/cjs/methods/indonesia.d.ts +2 -2
  19. package/dist/cjs/methods/indonesia.js +16 -9
  20. package/dist/cjs/methods/morocco.d.ts +43 -20
  21. package/dist/cjs/methods/morocco.js +59 -25
  22. package/dist/cjs/methods/revisions.d.ts +2 -2
  23. package/dist/cjs/methods/revisions.js +2 -2
  24. package/dist/cjs/server.d.ts +12 -0
  25. package/dist/cjs/server.js +132 -0
  26. package/dist/cjs/sun/preciseSun.d.ts +6 -1
  27. package/dist/cjs/sun/preciseSun.js +7 -2
  28. package/dist/esm/authorities/indonesia.js +4 -4
  29. package/dist/esm/authorities/morocco.js +7 -6
  30. package/dist/esm/core/compute.js +1 -1
  31. package/dist/esm/data/generated/algeriaCertification.js +1 -1
  32. package/dist/esm/data/generated/diyanetCertification.js +1 -1
  33. package/dist/esm/data/generated/diyanetWorldPoints.js +1 -1
  34. package/dist/esm/data/generated/indonesiaCertification.js +1 -1
  35. package/dist/esm/data/generated/indonesiaCorrections.js +1 -1
  36. package/dist/esm/data/generated/kemenagMethodPoints.js +1 -1
  37. package/dist/esm/data/generated/moroccoCertification.js +1 -1
  38. package/dist/esm/data/generated/moroccoCorrections.js +1 -1
  39. package/dist/esm/data/generated/moroccoMethodPoints.js +1 -1
  40. package/dist/esm/data/generated/swedenCertification.js +1 -1
  41. package/dist/esm/methods/indonesia.d.ts +2 -2
  42. package/dist/esm/methods/indonesia.js +16 -9
  43. package/dist/esm/methods/morocco.d.ts +43 -20
  44. package/dist/esm/methods/morocco.js +54 -24
  45. package/dist/esm/methods/revisions.d.ts +2 -2
  46. package/dist/esm/methods/revisions.js +2 -2
  47. package/dist/esm/server.d.ts +12 -0
  48. package/dist/esm/server.js +126 -0
  49. package/dist/esm/sun/preciseSun.d.ts +6 -1
  50. package/dist/esm/sun/preciseSun.js +7 -2
  51. package/docs/methods.md +34 -10
  52. package/package.json +11 -2
  53. package/dist/cjs/methods/scratch-congress.d.ts +0 -87
  54. package/dist/cjs/methods/scratch-congress.js +0 -539
  55. package/dist/esm/methods/scratch-congress.d.ts +0 -87
  56. package/dist/esm/methods/scratch-congress.js +0 -534
@@ -2,26 +2,44 @@
2
2
  * Morocco — the Ministry of Habous and Islamic Affairs's method, as its
3
3
  * timetable is computed (issue #10).
4
4
  *
5
- * Read off 77 days of the ministry's tables for its 164 located cities:
5
+ * Read off the ministry's tables: 96 days of 2026 for its 164 located
6
+ * cities, and — through the Internet Archive — its pages of 2014–2025 for
7
+ * Rabat, Figuig, Tangier, Casablanca, Laayoune and a dozen more, every
8
+ * month of the year. The ministry's program changed between 2019 and
9
+ * 2023; this is the one it has used since (2023–2026), and what it does:
6
10
  *
7
11
  * • a precise Sun — Meeus's apparent Sun with the full nutation and his
8
12
  * chapter-28 equation of time; a JPL ephemeris fits no better, NOAA's
9
13
  * and the Almanac's series far worse — taken at each prayer's own
10
14
  * moment (iterated), declination and equation of time alike;
11
- * • Fajr 19°, Isha 17°, Asr one shadow length (Shafi'i);
15
+ * • Fajr 19° and Isha 17°, true altitudes;
12
16
  * • Dhuhr five minutes after the transit;
13
- * • sunrise 2.85 minutes before, and Maghrib 3.7 after, the Sun's upper
14
- * limb on the horizon — refraction 34′ and the Sun's semi-diameter for
15
- * its distance that day — less the city's own horizon dip;
17
+ * • Asr on the APPARENT Sun, by the classical rule: a gnomon's shadow at
18
+ * noon (the noon declination, the noon altitude raised by refraction)
19
+ * plus one length (Shafi'i), and the moment the apparent Sun casts it
20
+ * — refraction by Sæmundsson's formula from the true altitude at noon,
21
+ * Bennett's from the apparent at Asr (Meeus 16.4 and 16.3);
22
+ * • sunrise one minute before, and Maghrib 1.8 minutes after, the Sun
23
+ * on an altitude −(34′ + s) − dip: refraction, the semi-diameter for
24
+ * its distance that day, and the city's own allowance for its horizon;
16
25
  * • Fajr rounded down to the minute, the rest to the nearest.
17
26
  *
18
- * The dip is the ministry's own per city and per side — the horizon east
19
- * at sunrise, west at Maghrib, which mountains or the sea make different —
20
- * fitted from its tables with the city's point (`data/moroccoMethodPoints.json`,
21
- * about 0.02° per √metre of height); elsewhere 0.02°·√height, 0.86 of it at
22
- * Maghrib. On 77 days of the ministry's tables for its 164 located cities
23
- * this is its printed minute for 99.12 % of times, and never further than
24
- * one; `data/corrections/morocco.json` holds the rest.
27
+ * The horizon allowance ("dip") is the ministry's own per city and per
28
+ * side — east at sunrise, west at Maghrib — fitted from its tables with
29
+ * the city's point (`data/moroccoMethodPoints.json`); it follows the
30
+ * city's height closely (about 0.32° + 0.025°·√metres east, 0.37° +
31
+ * 0.021°·√metres west; 92 % correlation), which is what a city off the
32
+ * list gets. It is an angle, not minutes: the allowance in minutes grows
33
+ * towards the solstices, as the ministry's does, and the fixed minute
34
+ * margins are the small rest. Fitted on the autumn alone (the first
35
+ * version of this method) the two could not be told apart, and the method
36
+ * drifted a tenth of a minute by late November.
37
+ *
38
+ * On the 2026 tables this is the ministry's printed minute for 99.88 % of
39
+ * times (114 of 94,350 off, each within a second or two of a minute's
40
+ * edge: the ministry's Sun and this one differ by that much), and never
41
+ * further than one; `data/corrections/morocco.json` holds the rest. On
42
+ * the archive's 2023–2026 pages, every month of the year, 99.3 %.
25
43
  */
26
44
  const RAD = Math.PI / 180;
27
45
  const DAY_MS = 86_400_000;
@@ -49,16 +67,21 @@ export function meeusApparentSun(d) {
49
67
  }
50
68
  const ITERATIONS = 3;
51
69
  /** Minutes before the horizon crossing that the ministry prints sunrise, and after it Maghrib. */
52
- export const MOROCCO_SUNRISE_MARGIN = -2.85;
53
- export const MOROCCO_MAGHRIB_MARGIN = 3.7;
54
- /** The share of the horizon dip Maghrib takes, against sunrise's whole, where a city's own is not known. */
55
- export const MOROCCO_MAGHRIB_DIP_SHARE = 0.86;
56
- /** Degrees of dip per √metre where the ministry's own value is not known. */
57
- export const MOROCCO_DIP_PER_SQRT_M = 0.02;
70
+ export const MOROCCO_SUNRISE_MARGIN = -1.0;
71
+ export const MOROCCO_MAGHRIB_MARGIN = 1.8;
72
+ /** The horizon allowance of a city whose own is not known: degrees, from its height in metres, east (sunrise) and west (Maghrib). */
73
+ export const moroccoDipEast = (heightM) => 0.32 + 0.025 * Math.sqrt(Math.max(heightM, 0));
74
+ export const moroccoDipWest = (heightM) => 0.37 + 0.021 * Math.sqrt(Math.max(heightM, 0));
75
+ /** Refraction (degrees) that lifts a TRUE altitude h (degrees) to its apparent one — Sæmundsson, Meeus 16.4. */
76
+ export const refractionFromTrue = (h) => (1.02 / Math.tan((h + 10.3 / (h + 5.11)) * RAD)) / 60;
77
+ /** Refraction (degrees) under an APPARENT altitude h (degrees) — Bennett, Meeus 16.3. */
78
+ export const refractionFromApparent = (h) => (1 / Math.tan((h + 7.31 / (h + 4.4)) * RAD)) / 60;
58
79
  /** Exact times, minutes past 00:00 UT of the civil date, margins applied, before rounding. NaN where one does not form. */
59
- export function moroccoRawMinutes(latitude, longitude, year, month, day, dipDeg = 0, asrShadow = 1,
60
- /** The horizon's dip westward, at Maghrib; MOROCCO_MAGHRIB_DIP_SHARE of `dipDeg` when not known. */
61
- dipMaghribDeg = MOROCCO_MAGHRIB_DIP_SHARE * dipDeg) {
80
+ export function moroccoRawMinutes(latitude, longitude, year, month, day,
81
+ /** The horizon allowance eastward, at sunrise, degrees: the city's own, else `moroccoDipEast(height)`. */
82
+ dipDeg = moroccoDipEast(0), asrShadow = 1,
83
+ /** The horizon allowance westward, at Maghrib; `moroccoDipWest(0)` offset by the east's excess when not known. */
84
+ dipMaghribDeg = dipDeg + (moroccoDipWest(0) - moroccoDipEast(0))) {
62
85
  const d0 = (Date.UTC(year, month, day) - Date.UTC(2000, 0, 1)) / DAY_MS - 0.5;
63
86
  const at = (t) => meeusApparentSun(d0 + t / 1440);
64
87
  let noon = 720 - 4 * longitude;
@@ -82,12 +105,19 @@ dipMaghribDeg = MOROCCO_MAGHRIB_DIP_SHARE * dipDeg) {
82
105
  return t;
83
106
  };
84
107
  const fixed = (a) => () => a;
85
- const asrAltitude = (dec) => Math.atan(1 / (asrShadow + Math.tan(Math.abs(latitude - dec / RAD) * RAD))) / RAD;
108
+ // Asr on the apparent Sun: the noon shadow of a gnomon from the apparent noon altitude (the noon
109
+ // declination), plus `asrShadow` lengths; the Sun's apparent altitude that casts it; the true
110
+ // altitude under it is what the hour angle is solved for, with the declination at the event.
111
+ const noonDec = at(noon).dec;
112
+ const noonTrue = 90 - Math.abs(latitude - noonDec / RAD);
113
+ const noonApparent = noonTrue + refractionFromTrue(noonTrue);
114
+ const asrApparent = Math.atan(1 / (asrShadow + 1 / Math.tan(noonApparent * RAD))) / RAD;
115
+ const asrTrue = asrApparent - refractionFromApparent(asrApparent);
86
116
  return {
87
117
  fajr: event(fixed(-19), -1, noon - 420) - 0.5,
88
118
  sunrise: event(fixed(horizon - dipDeg), -1, noon - 360) + MOROCCO_SUNRISE_MARGIN,
89
119
  dhuhr: noon + 5,
90
- asr: event(asrAltitude, 1, noon + 200),
120
+ asr: event(fixed(asrTrue), 1, noon + 200),
91
121
  maghrib: event(fixed(horizon - dipMaghribDeg), 1, noon + 360) + MOROCCO_MAGHRIB_MARGIN,
92
122
  isha: event(fixed(-17), 1, noon + 420),
93
123
  };
@@ -96,7 +126,7 @@ dipMaghribDeg = MOROCCO_MAGHRIB_DIP_SHARE * dipDeg) {
96
126
  * The ministry's times for a point and civil date (month 0-based), on the
97
127
  * minute. Null when a time does not form there.
98
128
  */
99
- export function moroccoTimes(latitude, longitude, year, month, day, dipDeg = 0, asrShadow = 1, dipMaghribDeg) {
129
+ export function moroccoTimes(latitude, longitude, year, month, day, dipDeg, asrShadow = 1, dipMaghribDeg) {
100
130
  const r = moroccoRawMinutes(latitude, longitude, year, month, day, dipDeg, asrShadow, dipMaghribDeg);
101
131
  const all = [r.fajr, r.sunrise, r.dhuhr, r.asr, r.maghrib, r.isha];
102
132
  if (!all.every(Number.isFinite))
@@ -9,7 +9,7 @@
9
9
  export declare const METHOD_REVISION: {
10
10
  readonly sweden: 1;
11
11
  readonly diyanet: 1;
12
- readonly morocco: 1;
12
+ readonly morocco: 2;
13
13
  readonly algeria: 1;
14
- readonly indonesia: 1;
14
+ readonly indonesia: 2;
15
15
  };
@@ -9,7 +9,7 @@
9
9
  export const METHOD_REVISION = {
10
10
  sweden: 1,
11
11
  diyanet: 1,
12
- morocco: 1,
12
+ morocco: 2,
13
13
  algeria: 1,
14
- indonesia: 1,
14
+ indonesia: 2,
15
15
  };
@@ -0,0 +1,12 @@
1
+ export declare class HttpError extends Error {
2
+ readonly status: number;
3
+ constructor(status: number, message: string);
4
+ }
5
+ /** The answer to a request path and query. Throws HttpError, UncertifiedMethodError, RangeError or TypeError. */
6
+ export declare function handle(url: URL): unknown;
7
+ /** Status, body and headers for a request, errors included — what both servers send. */
8
+ export declare function respond(url: URL): {
9
+ status: number;
10
+ body: string;
11
+ headers: Record<string, string>;
12
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The HTTP API over the library — one handler, served two ways: by
3
+ * `tools/serve.ts` on Node (`npm run serve`) and by `api/worker.ts` at the
4
+ * edge (the public API). Nothing is logged or stored; every answer is JSON.
5
+ *
6
+ * GET / the routes, the methods' certification
7
+ * GET /v1/methods every method, as methodInfo gives it
8
+ * GET /v1/places?authority=algeria the authority's places (id, name, country, lat, lon, tz)
9
+ * GET /v1/nearest?authority=algeria&lat=36.75&lng=3.06
10
+ * GET /v1/times?place=algeria:27&date=2026-10-15 one day, as timesFor gives it
11
+ * GET /v1/times?authority=algeria&lat=36.75&lng=3.06&date=2026-10-15
12
+ * GET /v1/times?place=algeria:27&from=2026-10-01&to=2026-10-31
13
+ * GET /v1/authority?lat=36.75&lng=3.06 defaultAuthorityFor, with the country
14
+ *
15
+ * Options on /v1/times: asr=2 for the Hanafi Asr; strict=1 to refuse a date
16
+ * outside the certified span (HTTP 422) instead of answering 'method-only'.
17
+ */
18
+ import { AUTHORITIES, methodInfo, nearestPlace, placeById, placesOf, timesFor, timesForRange, UncertifiedMethodError } from './index.js';
19
+ import { countryAt, defaultAuthorityFor } from './locate.js';
20
+ export class HttpError extends Error {
21
+ status;
22
+ constructor(status, message) {
23
+ super(message);
24
+ this.status = status;
25
+ }
26
+ }
27
+ const isAuthority = (x) => !!x && AUTHORITIES.includes(x);
28
+ const num = (q, k) => {
29
+ const v = Number(q.get(k));
30
+ if (q.get(k) === null || !Number.isFinite(v))
31
+ throw new HttpError(400, `${k}: a number is needed`);
32
+ return v;
33
+ };
34
+ const authorityOf = (q) => {
35
+ const a = q.get('authority');
36
+ if (!isAuthority(a))
37
+ throw new HttpError(400, `authority: one of ${AUTHORITIES.join(', ')}`);
38
+ return a;
39
+ };
40
+ const placeOf = (q) => {
41
+ const id = q.get('place');
42
+ if (id) {
43
+ const p = placeById(id);
44
+ if (!p)
45
+ throw new HttpError(404, `no place ${id}`);
46
+ return p;
47
+ }
48
+ const near = nearestPlace(authorityOf(q), num(q, 'lat'), num(q, 'lng'));
49
+ if (!near)
50
+ throw new HttpError(404, 'no place near there');
51
+ return near.place;
52
+ };
53
+ const brief = (p) => ({ id: p.id, name: p.name, country: p.country, lat: p.lat, lon: p.lon, tz: p.tz });
54
+ const ROUTES = [
55
+ '/v1/methods',
56
+ '/v1/places?authority=<authority>',
57
+ '/v1/nearest?authority=<authority>&lat=<lat>&lng=<lng>',
58
+ '/v1/times?place=<authority:id>&date=<YYYY-MM-DD>[&asr=2][&strict=1]',
59
+ '/v1/times?authority=<authority>&lat=<lat>&lng=<lng>&date=<YYYY-MM-DD>',
60
+ '/v1/times?place=<authority:id>&from=<YYYY-MM-DD>&to=<YYYY-MM-DD>',
61
+ '/v1/authority?lat=<lat>&lng=<lng>',
62
+ ];
63
+ /** The answer to a request path and query. Throws HttpError, UncertifiedMethodError, RangeError or TypeError. */
64
+ export function handle(url) {
65
+ const q = url.searchParams;
66
+ switch (url.pathname.replace(/\/+$/, '') || '/') {
67
+ case '/':
68
+ return {
69
+ name: 'mihrab-methods',
70
+ description: 'The prayer-time authorities’ own calculation methods, each reproducing its published timetable to the minute.',
71
+ docs: 'https://github.com/MihrabHQ/mihrab-methods#readme',
72
+ routes: ROUTES,
73
+ authorities: AUTHORITIES,
74
+ // each method's record, as the library carries it (the feed on main may be newer: methodInfo says the date)
75
+ methods: AUTHORITIES.map(a => {
76
+ const m = methodInfo(a);
77
+ return { authority: a, name: m.name, revision: m.revision, certified: m.certified, checkedAt: m.certification?.checkedAt ?? null, places: m.places };
78
+ }),
79
+ };
80
+ case '/v1/methods':
81
+ return AUTHORITIES.map(methodInfo);
82
+ case '/v1/places':
83
+ return placesOf(authorityOf(q)).map(brief);
84
+ case '/v1/nearest': {
85
+ const near = nearestPlace(authorityOf(q), num(q, 'lat'), num(q, 'lng'));
86
+ return near && { place: brief(near.place), distanceKm: Math.round(near.distanceKm * 10) / 10 };
87
+ }
88
+ case '/v1/authority': {
89
+ const lat = num(q, 'lat'), lng = num(q, 'lng');
90
+ return { country: countryAt(lat, lng), authority: defaultAuthorityFor(lat, lng) };
91
+ }
92
+ case '/v1/times': {
93
+ const place = placeOf(q);
94
+ const opts = { asrShadow: q.get('asr') === '2' ? 2 : 1, strict: q.get('strict') === '1' };
95
+ const from = q.get('from'), to = q.get('to');
96
+ if (from && to)
97
+ return { place: brief(place), days: timesForRange(place, from, to, opts) };
98
+ return { place: brief(place), ...timesFor(place, q.get('date') ?? new Date().toISOString().slice(0, 10), opts) };
99
+ }
100
+ default:
101
+ throw new HttpError(404, `no such route; the routes are ${ROUTES.join(' · ')}`);
102
+ }
103
+ }
104
+ /** Status, body and headers for a request, errors included — what both servers send. */
105
+ export function respond(url) {
106
+ let status = 200;
107
+ let body;
108
+ try {
109
+ body = handle(url);
110
+ }
111
+ catch (e) {
112
+ status = e instanceof HttpError ? e.status : e instanceof UncertifiedMethodError ? 422 : e instanceof RangeError || e instanceof TypeError ? 400 : 500;
113
+ body = { error: e.message };
114
+ }
115
+ return {
116
+ status,
117
+ body: JSON.stringify(body),
118
+ headers: {
119
+ 'content-type': 'application/json; charset=utf-8',
120
+ 'access-control-allow-origin': '*',
121
+ 'access-control-allow-methods': 'GET, OPTIONS',
122
+ // A day's times never change between releases: let browsers and the edge keep an answer for an hour.
123
+ 'cache-control': status === 200 ? 'public, max-age=3600' : 'no-store',
124
+ },
125
+ };
126
+ }
@@ -12,8 +12,13 @@ export declare function deltaT(ut: number): number;
12
12
  * The apparent Sun at `d` days from J2000.0 (UT): declination (rad) and
13
13
  * equation of time (minutes), such that the transit at longitude λ (east) is
14
14
  * 720 − 4λ − eot minutes UT. Same contract as `meeusApparentSun`.
15
+ *
16
+ * `deltaTSeconds` is what is added to UT for the ephemeris' own time (TT):
17
+ * ΔT as it is (about 70 s in the 2020s) unless given. A program that takes
18
+ * its clock time for the ephemeris' time is 0 here — the Sun 70 seconds
19
+ * behind where it is, a fifth of a second on every time of the day.
15
20
  */
16
- export declare function preciseApparentSun(d: number): {
21
+ export declare function preciseApparentSun(d: number, deltaTSeconds?: number): {
17
22
  dec: number;
18
23
  eot: number;
19
24
  };
@@ -158,9 +158,14 @@ function nutate(v, oblm, oblt, psi) {
158
158
  * The apparent Sun at `d` days from J2000.0 (UT): declination (rad) and
159
159
  * equation of time (minutes), such that the transit at longitude λ (east) is
160
160
  * 720 − 4λ − eot minutes UT. Same contract as `meeusApparentSun`.
161
+ *
162
+ * `deltaTSeconds` is what is added to UT for the ephemeris' own time (TT):
163
+ * ΔT as it is (about 70 s in the 2020s) unless given. A program that takes
164
+ * its clock time for the ephemeris' time is 0 here — the Sun 70 seconds
165
+ * behind where it is, a fifth of a second on every time of the day.
161
166
  */
162
- export function preciseApparentSun(d) {
163
- const tt = d + deltaT(d) / 86400;
167
+ export function preciseApparentSun(d, deltaTSeconds = deltaT(d)) {
168
+ const tt = d + deltaTSeconds / 86400;
164
169
  const e = earthJ2000(tt - 1 / C_AUDAY);
165
170
  const nut = nutation(tt);
166
171
  const mobl = meanObliquity(tt);
package/docs/methods.md CHANGED
@@ -110,7 +110,11 @@ statement of it that gives the tables.
110
110
  ## Morocco
111
111
 
112
112
  `src/methods/morocco.ts`. The Ministry of Habous and Islamic Affairs, read
113
- off its tables for 164 cities.
113
+ off its tables for 164 cities: 96 days of 2026 for all of them, and — through
114
+ the Internet Archive's copies of its pages — 2014 to 2025 for Rabat, Figuig,
115
+ Tangier, Casablanca, Laayoune and a dozen more, every month of the year. The
116
+ ministry's program changed between 2019 and 2023 (until then Asr was the
117
+ noon shadow on the true sun); this is the program it has used since.
114
118
 
115
119
  - The sun is **Meeus's apparent sun** with the full nutation (the mean
116
120
  elements above; apparent longitude `L0 + C − 0.00569 + Δψ`, with
@@ -118,14 +122,26 @@ off its tables for 164 cities.
118
122
  and his chapter-28 equation of time
119
123
  `E = L0 − 0.0057183 − α + Δψ cos ε`, taken at **each prayer's own
120
124
  moment** (iterated), declination and equation of time alike.
121
- - Fajr −19°, Isha −17°, Asr one shadow length (Shafiʿi).
125
+ - Fajr −19°, Isha −17°, true altitudes.
122
126
  - Dhuhr five minutes after the transit.
123
- - Sunrise 2.85 minutes before, and Maghrib 3.7 minutes after, the sun's
124
- upper limb on the horizon: altitude `−(34′ + s) − dip`, with s the sun's
125
- semi-diameter for its distance that day and dip the city's own horizon
126
- dip — eastward at sunrise (`Place.dipDeg`), westward at Maghrib
127
- (`Place.dipMaghribDeg`), each fitted from the tables with the city's
128
- point; where not fitted, `0.02°·√height(m)`, and 0.86 of it at Maghrib.
127
+ - Asr on the **apparent** sun, by the classical rule: with δ the noon
128
+ declination, the noon altitude `h = 90° − |φ − δ|` is raised by
129
+ refraction, `h' = h + R(h)`, `R(h) = 1.02′ / tan(h + 10.3/(h + 5.11))`
130
+ (Sæmundsson, Meeus 16.4, h in degrees); the gnomon's shadow is `cot h'`,
131
+ and Asr is when the apparent sun's shadow is one length more:
132
+ apparent altitude `a' = arctan(1 / (1 + cot h'))`, and the true altitude
133
+ the hour angle is solved for, `a = a' − R'(a')`,
134
+ `R'(a') = 1′ / tan(a' + 7.31/(a' + 4.4))` (Bennett, Meeus 16.3), with the
135
+ declination at the event. Hanafi takes two lengths.
136
+ - Sunrise one minute before, and Maghrib 1.8 minutes after, the sun on the
137
+ altitude `−(34′ + s) − dip`, with s the sun's semi-diameter for its
138
+ distance that day and dip the city's own horizon allowance — eastward at
139
+ sunrise (`Place.dipDeg`), westward at Maghrib (`Place.dipMaghribDeg`),
140
+ each fitted from the tables with the city's point. It is an angle: the
141
+ minutes it is worth grow towards the solstices, as the ministry's do.
142
+ It follows the city's height (92 % correlation), and a city not fitted
143
+ gets `0.32° + 0.025°·√height(m)` east and `0.37° + 0.021°·√height(m)`
144
+ west.
129
145
  - Fajr rounded **down** to the minute, every other time to the nearest.
130
146
  - The ministry's clock is GMT+1 until 17 September 2026 and GMT from then
131
147
  on (`Place.clock`); the zone databases still move Morocco's clock, so the
@@ -164,6 +180,11 @@ cities.
164
180
  after Astronomy Engine, within 0.05 s and 0.5″ of JPL's DE421), taken at
165
181
  each prayer's own moment, iterated three times. Meeus's shorter series
166
182
  drifts by a second and leaves a minute in two hundred off.
183
+ - **No ΔT**: the ministry's program takes the clock's Universal Time for
184
+ the ephemeris' own time, so its sun stands about 70 seconds behind and
185
+ every time of the day comes a fifth of a second early
186
+ (`preciseApparentSun(d, 0)`). A port that adds ΔT is a minute off on
187
+ about one time in two hundred.
167
188
  - Fajr −20°, Isha −18°.
168
189
  - Sunrise and Maghrib at −1°, and at −2° in the regencies the ministry
169
190
  classes as highland (`Place.highland`); in six highland regencies of Papua
@@ -173,8 +194,11 @@ cities.
173
194
  entry gives it), the hour angle from the sun at Asr (four iterations).
174
195
  - Every time rounded **up**, after a margin: +2 minutes on Fajr, Asr,
175
196
  Maghrib and Isha, +3 on Dhuhr, −3 on sunrise.
176
- - Each regency at its own fitted point; Karawang's Asr, Maghrib and Isha at a
177
- second point about 2 km north-east (`Place.afternoon`).
197
+ - Each regency at the **coordinates the ministry gives for it**, to a
198
+ hundredth of an arc-second. Nothing is fitted. One is written with a minus
199
+ and a hemisphere both (Pekajang, "−01° 12′ S"), and the ministry's program
200
+ reads it as −1° + 12′ and then south of that: 0.8° north, which is where
201
+ its table is computed.
178
202
  - Each regency is printed in its province's zone (WIB, WITA or WIT).
179
203
 
180
204
  ## Checking a port
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mihrab-methods",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "The prayer-time authorities' own calculation methods, each reproducing its published timetable to the minute: Sweden (Diyanet, as Islamiska Förbundet prints it), Diyanet worldwide, Morocco (Habous), Algeria (Ministry of Religious Affairs), Indonesia (Kemenag).",
5
5
  "license": "MPL-2.0",
6
6
  "author": "Hassan El Ghamri",
@@ -28,7 +28,14 @@
28
28
  "./dist/*/morocco.js",
29
29
  "./dist/*/algeria.js",
30
30
  "./dist/*/indonesia.js",
31
- "./dist/*/authorities/*.js"
31
+ "./dist/*/authorities/*.js",
32
+ "./src/index.ts",
33
+ "./src/sweden.ts",
34
+ "./src/diyanet.ts",
35
+ "./src/morocco.ts",
36
+ "./src/algeria.ts",
37
+ "./src/indonesia.ts",
38
+ "./src/authorities/*.ts"
32
39
  ],
33
40
  "engines": {
34
41
  "node": ">=18"
@@ -169,6 +176,8 @@
169
176
  "precertify": "npm run embed",
170
177
  "certify": "tsx tools/certify.ts",
171
178
  "vectors": "npm run embed && tsx tools/vectors.ts",
179
+ "prefeed-check": "npm run embed",
180
+ "feed-check": "tsx tools/feed-check.ts",
172
181
  "readme": "node scripts/readme.mjs",
173
182
  "preflight": "npm run typecheck && npm test && node scripts/readme.mjs --check",
174
183
  "serve": "npm run embed && tsx tools/serve.ts"
@@ -1,87 +0,0 @@
1
- /**
2
- * The International Prayer Times Congress method (Istanbul, 2021 — the
3
- * Diyanet and the European Council for Fatwa and Research) as Diyanet's
4
- * tables realise it. Islamiska Förbundet's Swedish tables are those
5
- * tables, row for row, so this is Sweden's method (id 24).
6
- *
7
- * The published rules (awqatsalah.com, "Tespit Kriterleri"):
8
- * - Dhuhr five minutes after transit; Asr one shadow (Dhuhr when the
9
- * shadow never forms), four minutes late; sunrise seven early,
10
- * Maghrib seven late ("temkin").
11
- * - Neither the day nor the night is shorter than five hours.
12
- * - True Fajr at 18°, true Isha at 16°. Where the 18° dawn no longer
13
- * forms, the share of the night the sharia night's third took on the
14
- * last day it did is carried: Isha that share of the night after
15
- * Maghrib, Fajr the same share scaled by 18/16 before sunrise.
16
- * - The move from true times to the carried ones is gradual, starting
17
- * twenty minutes before (Isha) or after (Fajr) the takdir.
18
- *
19
- * What the tables turn out to do with them, read off Diyanet's own tables
20
- * for all 160 Swedish places, 2024–2027 (99.98 % of times on the printed
21
- * minute, the rest a fraction of a second from the rounding point):
22
- * - The sun is taken once a day, at 0h UT less 45 s: Meeus's mean
23
- * elements and equation of the centre, the TRUE (geometric) longitude,
24
- * the apparent obliquity (mean plus nutation), the equation of time as
25
- * the mean longitude less the right ascension, 0.106 s later; and
26
- * two small waves by the mean longitude the formula leaves out: a few
27
- * hundredths of a second on the transit, a tenth of an arc-second on
28
- * the declination.
29
- * - Coordinates are kept to three decimals of a degree.
30
- * - Asr that never forms is printed at Dhuhr's time.
31
- * - The night a share is taken of is the day's own: 24 h less the
32
- * printed day, sunrise to Maghrib.
33
- * - The five-hour floors hold each end separately, 150 minutes from the
34
- * printed Dhuhr or from the midnight after it. Where midsummer's
35
- * night is floored at both ends (to within a minute), that night
36
- * anchors the season, and the floored times never step back: before
37
- * midsummer no sunrise earlier than midsummer's and a floored Maghrib
38
- * no earlier than the day before; after it no sunrise earlier nor
39
- * Maghrib later than on any day since midsummer — and Fajr and Isha
40
- * carried the same way.
41
- * - The gradual transition is a straight line across the days next to
42
- * the gap on which the true time lies within twenty minutes of the
43
- * takdir at the gap's edge: twenty minutes away at the far end, the
44
- * takdir at the edge.
45
- *
46
- * Beyond Sweden the same code gives Diyanet's tables worldwide, read off
47
- * them for places on six continents:
48
- * - Where the dawn forms all summer but midsummer's true Isha (at 16°)
49
- * falls later than the share carried from that night, midsummer is the
50
- * takdir's day — a gap of one day, with its ramps (from about 44.4°
51
- * north: Austria, central France, northern Bosnia).
52
- * - Isha is printed at 16° in some countries and 17° in others; the
53
- * high-latitude rules are worked at 16° either way (`ishaDeg`).
54
- *
55
- * Times are minutes past 0h UT of the date, a frame the clock changes
56
- * cannot bend; `congressTimes` turns them into instants.
57
- */
58
- /**
59
- * Isha's depression: the Congress's 16° (Diyanet's in much of Europe,
60
- * Russia, Central Asia, China and North America), or Diyanet's own 17°
61
- * (elsewhere). The high-latitude rules are the Congress's at 16° either way;
62
- * the angle sets only the true Isha printed.
63
- */
64
- export declare const CONGRESS_ISHA_DEG = 16;
65
- export declare const DIYANET_ISHA_DEG = 17;
66
- export type CongressInstants = {
67
- fajr: Date;
68
- sunrise: Date;
69
- dhuhr: Date;
70
- asr: Date;
71
- maghrib: Date;
72
- isha: Date;
73
- };
74
- /**
75
- * The Congress method's times for a calendar date (year, month 0–11, day),
76
- * as instants, unrounded. `asrShadow` is 1 for the standard Asr, 2 for the
77
- * Hanafi one; the tables print the standard. `ishaDeg` is Isha's depression:
78
- * 16° (the Congress's, Diyanet's in Europe and North America) or 17°
79
- * (Diyanet's everywhere else).
80
- */
81
- export declare function congressTimes(latitude: number, longitude: number, year: number, month: number, day: number, asrShadow?: number, ishaDeg?: number): CongressInstants;
82
- /** For tests and the verification tool: the year's structure as the method sees it. */
83
- export declare function congressYearInfo(latitude: number, longitude: number, year: number): {
84
- gap: [string, string] | null;
85
- ratio: number | null;
86
- midsummerFloors: boolean;
87
- };