mihrab-methods 1.0.0 → 1.0.2
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.
- package/README.md +91 -291
- package/certification.json +16 -5
- package/dist/cjs/authorities/algeria.d.ts +1 -1
- package/dist/cjs/authorities/indonesia.d.ts +1 -1
- package/dist/cjs/authorities/morocco.d.ts +1 -1
- package/dist/cjs/authorities/sweden.d.ts +1 -1
- package/dist/cjs/core/api.d.ts +1 -1
- package/dist/cjs/core/api.js +2 -1
- package/dist/cjs/core/clock.d.ts +8 -1
- package/dist/cjs/core/clock.js +29 -8
- package/dist/cjs/core/fetchLatest.d.ts +5 -1
- package/dist/cjs/core/fetchLatest.js +101 -46
- package/dist/cjs/core/registry.d.ts +1 -1
- package/dist/cjs/core/registry.js +5 -0
- package/dist/cjs/core/types.d.ts +4 -0
- package/dist/cjs/data/generated/algeriaCertification.js +1 -1
- package/dist/cjs/data/generated/diyanetCertification.js +1 -1
- package/dist/cjs/data/generated/indonesiaCertification.js +1 -1
- package/dist/cjs/data/generated/moroccoCertification.js +1 -1
- package/dist/cjs/data/generated/swedenCertification.js +1 -1
- package/dist/cjs/locate.js +1 -1
- package/dist/esm/authorities/algeria.d.ts +1 -1
- package/dist/esm/authorities/indonesia.d.ts +1 -1
- package/dist/esm/authorities/morocco.d.ts +1 -1
- package/dist/esm/authorities/sweden.d.ts +1 -1
- package/dist/esm/core/api.d.ts +1 -1
- package/dist/esm/core/api.js +2 -1
- package/dist/esm/core/clock.d.ts +8 -1
- package/dist/esm/core/clock.js +29 -8
- package/dist/esm/core/fetchLatest.d.ts +5 -1
- package/dist/esm/core/fetchLatest.js +99 -13
- package/dist/esm/core/registry.d.ts +1 -1
- package/dist/esm/core/registry.js +5 -0
- package/dist/esm/core/types.d.ts +4 -0
- package/dist/esm/data/generated/algeriaCertification.js +1 -1
- package/dist/esm/data/generated/diyanetCertification.js +1 -1
- package/dist/esm/data/generated/indonesiaCertification.js +1 -1
- package/dist/esm/data/generated/moroccoCertification.js +1 -1
- package/dist/esm/data/generated/swedenCertification.js +1 -1
- package/dist/esm/locate.js +1 -1
- package/package.json +3 -1
- package/schemas/certification.schema.json +119 -24
package/README.md
CHANGED
|
@@ -2,319 +2,119 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/mihrab-methods)
|
|
4
4
|
[](https://github.com/MihrabHQ/mihrab-methods/actions/workflows/ci.yml)
|
|
5
|
+
[](#how-verification-works)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
**Prayer times that match the published timetable to the printed minute.**
|
|
8
|
+
Each national authority's own calculation method, worked out again from its
|
|
9
|
+
published times and checked day by day against its timetables: <!--fig:total.off-->0<!--/fig--> of
|
|
10
|
+
<!--fig:total.times-->71,896,761<!--/fig--> times off the printed minute. Free for any app, site or
|
|
11
|
+
mosque to use.
|
|
10
12
|
|
|
11
13
|
```sh
|
|
12
14
|
npm install mihrab-methods
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
```ts
|
|
16
|
-
import { nearestPlace, timesFor } from 'mihrab-methods/
|
|
17
|
-
|
|
18
|
-
const { place } = nearestPlace('
|
|
19
|
-
const day = timesFor(place, '2026-10-15');
|
|
20
|
-
day.status;
|
|
21
|
-
day.clock.label;
|
|
22
|
-
day.times!.fajr;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
It is a library, in TypeScript, with a small server over it for everything
|
|
26
|
-
else (`npm run serve`); see [Use](#use).
|
|
27
|
-
|
|
28
|
-
Each method is the authority's own formula, worked out again from its
|
|
29
|
-
published times, with a small, fixed correction list for the few times where the formula
|
|
30
|
-
alone lands a fraction of a second on the wrong side of a minute. The aim is that
|
|
31
|
-
any app, site or mosque can give the times its community prays by, without
|
|
32
|
-
asking anyone.
|
|
33
|
-
|
|
34
|
-
Methods today:
|
|
35
|
-
|
|
36
|
-
| Authority | Method | On the printed minute | Within a minute | Checked over |
|
|
37
|
-
|---|---|---|---|---|
|
|
38
|
-
| **Sweden** — Diyanet's method, as used by Islamiska Förbundet | `src/methods/sweden.ts` — the Istanbul congress method as Diyanet computes it, with `data/corrections/sweden.json` | **100 %** of Diyanet's timetables (1.39 M times, 2024–2027) and of Islamiska Förbundet's (107 cities, 59 640 times, Oct 2026 → Nov 2027, with the rows set aside below) — by the method alone, 99.981 % and 99.966 % | 100 % | 2024 → 2027 |
|
|
39
|
-
| **Diyanet** — Türkiye and worldwide | `src/methods/sweden.ts` with Isha's angle by country (16° or 17°), a point per prayer per year in Türkiye, with `data/corrections/diyanet.json` | **100 %** of Diyanet's timetables for all 880 districts of Türkiye and Northern Cyprus and 7 252 places in 197 other countries (70.0 M times) — 99.987 % by the method alone | 100 % | 2024 → 2027 |
|
|
40
|
-
| **Morocco** — Ministry of Habous | `src/methods/morocco.ts` — each city's own sunrise and Maghrib horizon, the sun's semidiameter by its distance, the tables' own clock (GMT+1 until 17 Sep 2026), with `data/corrections/morocco.json` | **100 %** of the ministry's tables for its 164 located cities (75 768 times) — 99.122 % by the method alone | 100 % | 27 Aug → 11 Nov 2026 |
|
|
41
|
-
| **Algeria** — Ministry of Religious Affairs and Wakfs | `src/methods/algeria.ts`, with `data/corrections/algeria.json` | **100 %** of the ministry's 1448 AH table for its 68 cities (101 184 times) — 99.366 % by the method alone | 100 % | 1 Oct 2026 → 5 Jun 2027 |
|
|
42
|
-
| **Indonesia** — Kemenag | `src/methods/indonesia.ts`, with `data/corrections/indonesia.json` | **100 %** of the ministry's tables for all 517 regencies and cities (266 772 times) — 99.969 % by the method alone | 100 % | 7 Oct → 31 Dec 2026 |
|
|
43
|
-
|
|
44
|
-
The goal is 100 % on the printed minute, for every time of a full year, for
|
|
45
|
-
each method.
|
|
46
|
-
|
|
47
|
-
## Certification: 100 % or not at all
|
|
48
|
-
|
|
49
|
-
A method may be used only once it reproduces **every** time of its
|
|
50
|
-
authority's timetable on the printed minute — not one minute off, anywhere.
|
|
51
|
-
`npm run certify` checks each method day by day and writes
|
|
52
|
-
`certification.json`; `timesFor` throws `UncertifiedMethodError` for any
|
|
53
|
-
method not certified there, and says `status: 'method-only'` for a date
|
|
54
|
-
beyond the span it was checked over. `candidateTimes` runs a method
|
|
55
|
-
regardless, for research and for the check itself — not for showing anyone
|
|
56
|
-
their prayer times.
|
|
57
|
-
|
|
58
|
-
Days the timetable itself gets wrong (another town's times under a name, a
|
|
59
|
-
previous year's figures for the date, a figure that differs from the
|
|
60
|
-
authority's own) are set aside and listed in the record, so each can be
|
|
61
|
-
judged.
|
|
62
|
-
|
|
63
|
-
Today (checked 2026-10-16) **every method is certified**: Sweden and Diyanet for 2024–2027, and Morocco, Algeria and Indonesia for the tables published so far:
|
|
64
|
-
|
|
65
|
-
| Authority | Off the printed minute |
|
|
66
|
-
|---|---|
|
|
67
|
-
| Sweden | 0 of 59 640 (Islamiska Förbundet); 0 of 1 393 794 (Diyanet) |
|
|
68
|
-
| Diyanet (Türkiye and worldwide) | 0 of 69 999 603 |
|
|
69
|
-
| Morocco | 0 of 75 768 |
|
|
70
|
-
| Algeria | 0 of 101 184 |
|
|
71
|
-
| Indonesia | 0 of 266 772 |
|
|
72
|
-
|
|
73
|
-
## Status
|
|
74
|
-
|
|
75
|
-
| Method | Where it stands |
|
|
76
|
-
|---|---|
|
|
77
|
-
| Sweden | Certified, 2024–2027, against Diyanet's tables for Sweden (159 places), and against Islamiska Förbundet's published tables (11 Oct 2026 → 19 Nov 2027) for 107 of its 108 cities. Set aside from Islamiska Förbundet's: Kiruna, printed with Umeå's times; 33 395 times that differ from Diyanet's own table for the day (its 2027 rows repeat 2026's figures); and 646 rows copied from another day. On each, the method gives Diyanet's own table. |
|
|
78
|
-
| Diyanet | Certified, 2024–2027, for every district of Türkiye and Northern Cyprus and 7 252 places in 197 other countries: 8 291 of the 8 528 places Diyanet lists, with Sweden's 159. Each place is computed at a point of its own, checked to lie in the place's country. Not yet included: 147 places that could not be placed — seven of them Diyanet serves no table for (Grozny, Gudermes, Shali and Urus-Martan, Rožaje, Monte Carlo, Punta Arenas), the rest names the gazetteer does not match; 75 whose tables the method does not yet follow over all four years — about half of them in Mexico and Australia, whose tables keep a clock of their own, the rest scattered (Kazan, Fez, Ushuaia, Svalbard among them); 12 whose tables Diyanet computes at a point elsewhere — in another country, at sea or in the other hemisphere (Tupiza, Ribeirão das Neves, Montería, Hami, Kungälv and Galena among them); Syktyvkar, whose 2024 and 2027 tables print a Fajr hours from its 2025 and 2026 ones; and Ping Tung City and Rilchingen-Hanweiler, each listed again at the same point (as Pingtung and Kleinblittersdorf) with a table a minute apart on two days. |
|
|
79
|
-
| Morocco | Certified for 27 Aug → 11 Nov 2026 (164 cities): the method gives 99.12 % alone, and a list of 665 corrections the rest. Certification covers only the dates in the table: each newly published month is checked against the method, and its corrections are added in a new list. |
|
|
80
|
-
| Algeria | Certified for 1448 AH (1 Oct 2026 → 5 Jun 2027, 68 cities): the method gives 99.37 % alone, and a list of 642 corrections the rest. The method is at its limit: the angles, horizon, Maghrib margin, rounding, sun series, iterations and city points are each sharp optima, and the 642 are independent differences of about a second or less (no pattern by prayer, city, date or season; refitting the points to them only fits noise, and is worse on days held out). |
|
|
81
|
-
| Indonesia | Certified for 7 Oct → 31 Dec 2026 (517 regencies and cities): the method gives 99.969 % alone, and a list of 84 corrections the rest. Karawang's Asr, Maghrib and Isha are computed at a second point, about 2 km north-east of its first (its afternoon fits it, and it holds on days held out). The 84 are independent differences at a minute's edge (median 0.2 s, nine in ten under 1.3 s, the largest 3.9 s) — eight at Sibolga, whose mornings no point fits — and refitting points to them only fits noise. Certification covers only the dates in the tables: each newly published month is checked against the method, and its corrections are added in a new list. |
|
|
82
|
-
|
|
83
|
-
## Formulas and correction lists: why this can be used freely
|
|
84
|
-
|
|
85
|
-
Under EU and Swedish law, this approach is lawful use and "insubstantial
|
|
86
|
-
extraction": an unprotected mathematical formula, worked out from published
|
|
87
|
-
times, plus a very small, static list of factual data points that fixes edge
|
|
88
|
-
cases.
|
|
89
|
-
|
|
90
|
-
- **The methods are formulas.** A method of calculation is not protected as
|
|
91
|
-
such. What is here is the arithmetic: the sun's position, the angles, the
|
|
92
|
-
margins and the high-latitude rules each authority applies. It is written
|
|
93
|
-
from scratch, not copied.
|
|
94
|
-
- **The correction lists are microscopically small and static.** Each one
|
|
95
|
-
holds only the few times where the authority's own rounding falls the
|
|
96
|
-
other way: a point, a date, a prayer, a minute either way. Sweden's is 270
|
|
97
|
-
entries against 1.39 million times (0.02 %). The aim is the same for every
|
|
98
|
-
method: get the formula as close as possible, so the list that remains is
|
|
99
|
-
insubstantial.
|
|
100
|
-
- **No timetable is part of this repository.** It holds no copy of any
|
|
101
|
-
authority's tables. The times are computed, for any date and any point.
|
|
102
|
-
|
|
103
|
-
This is the project's position, not legal advice. The same rule applies to
|
|
104
|
-
every method added here: the best formula first, and a correction list only
|
|
105
|
-
for what is left.
|
|
106
|
-
|
|
107
|
-
## Sweden's and Diyanet's correction lists
|
|
108
|
-
|
|
109
|
-
The method reproduces Diyanet's computation; what it cannot reproduce is the
|
|
110
|
-
arithmetic of Diyanet's program, which puts about one time in five thousand on
|
|
111
|
-
the other side of a minute's rounding point — a fraction of a second away,
|
|
112
|
-
independently from place to place and day to day. `data/corrections/sweden.json`
|
|
113
|
-
lists those times (point, date, prayer, a minute either way: 270 over
|
|
114
|
-
2024–2027) and `timesFor` applies them. The list covers the years the
|
|
115
|
-
timetables do, and certification holds over those dates only: outside them
|
|
116
|
-
`timesFor` answers with `status: 'method-only'` (or refuses, with
|
|
117
|
-
`strict`). Diyanet's worldwide list
|
|
118
|
-
(`data/corrections/diyanet.json`) is the same kind: 9 118 entries at 4 660 points against
|
|
119
|
-
70.0 million times. Each year, once the next year's timetables are out:
|
|
120
|
-
|
|
121
|
-
```sh
|
|
122
|
-
npm run corrections && for a in diyanet morocco algeria indonesia; do npm run corrections -- $a; done && npm run certify
|
|
18
|
+
import { nearestPlace, timesFor } from 'mihrab-methods/diyanet'; // one authority's data, or 'mihrab-methods' for all
|
|
19
|
+
|
|
20
|
+
const { place } = nearestPlace('diyanet', 41.01, 28.98)!; // Istanbul's district
|
|
21
|
+
const day = timesFor(place, '2026-10-15'); // or { year: 2026, month: 10, day: 15 }
|
|
22
|
+
day.status; // 'certified': inside the dates the method was checked over
|
|
23
|
+
day.clock.label; // 'Europe/Istanbul': the clock the table prints the day in
|
|
24
|
+
day.times!.fajr; // { instant: Date, printed: 'HH:MM' } — show printed, schedule on instant
|
|
25
|
+
day.certifiedUntil; // the last date checked; past it, status is 'method-only'
|
|
123
26
|
```
|
|
124
27
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
28
|
+
The full API, the HTTP server and the per-authority imports are in
|
|
29
|
+
[docs/usage.md](docs/usage.md).
|
|
30
|
+
|
|
31
|
+
## What is certified
|
|
32
|
+
|
|
33
|
+
<!-- certification-table:start -->
|
|
34
|
+
| Authority | Tables matched | Places | Times checked | Off the printed minute | By the method alone | Correction entries | Checked over |
|
|
35
|
+
|---|---|---:|---:|---:|---:|---:|---|
|
|
36
|
+
| **Sweden** | Islamiska Förbundet’s tables | 107 | 59,640 | **0** | 99.966 % | 270 | 11 Oct 2026 → 19 Nov 2027 |
|
|
37
|
+
| | Diyanet’s own tables | 159 | 1,393,794 | **0** | 99.981 % | | 1 Jan 2024 → 31 Dec 2027 |
|
|
38
|
+
| **Diyanet** | its tables worldwide | 8,132 | 69,999,603 | **0** | 99.986 % | 9,118 | 1 Jan 2024 → 31 Dec 2027 |
|
|
39
|
+
| **Morocco** | Habous | 164 | 75,768 | **0** | 99.122 % | 665 | 27 Aug 2026 → 11 Nov 2026 |
|
|
40
|
+
| **Algeria** | Ministry of Religious Affairs | 68 | 101,184 | **0** | 99.365 % | 642 | 1 Oct 2026 → 5 Jun 2027 |
|
|
41
|
+
| **Indonesia** | Kemenag | 517 | 266,772 | **0** | 99.968 % | 84 | 7 Oct 2026 → 31 Dec 2026 |
|
|
42
|
+
|
|
43
|
+
5 of 5 methods certified; 0 of 71,896,761 times off the printed minute. Last checked 18 October 2026. Generated from [`certification.json`](certification.json) by `scripts/readme.mjs`.
|
|
44
|
+
<!-- certification-table:end -->
|
|
45
|
+
|
|
46
|
+
"By the method alone" is the formula without its correction list. The list
|
|
47
|
+
holds the few times the authority's own program puts a fraction of a second
|
|
48
|
+
on the other side of a minute's rounding point: a point, a date, a prayer, a
|
|
49
|
+
minute either way. Why that can be used freely is in
|
|
50
|
+
[docs/legal.md](docs/legal.md); what each method still leaves out is in
|
|
51
|
+
[docs/status.md](docs/status.md).
|
|
52
|
+
|
|
53
|
+
## How verification works
|
|
54
|
+
|
|
55
|
+
1. **The tables.** For each authority, its own published timetables for
|
|
56
|
+
every place it lists, every day it has published. They are not part of
|
|
57
|
+
this repository and are not redistributed; the check reads them from
|
|
58
|
+
`MIHRAB_TABLES`.
|
|
59
|
+
2. **The check.** [`tools/verify.ts`](tools/verify.ts) computes every place on
|
|
60
|
+
every day and compares each of the six times with the printed minute, on
|
|
61
|
+
the clock the table prints that day in. Days the table itself gets wrong
|
|
62
|
+
(another town's times under a name, a previous year's rows) are set aside
|
|
63
|
+
and listed in the record, so each can be judged.
|
|
64
|
+
3. **The record.** [`tools/certify.ts`](tools/certify.ts) writes
|
|
65
|
+
[`certification.json`](certification.json): per authority, the tables
|
|
66
|
+
checked, the times, how many are off with and without the correction
|
|
67
|
+
list, the dates, the method's revision and the list's SHA-256.
|
|
68
|
+
4. **The gate.** A method is certified only at **0** off. `timesFor` refuses
|
|
69
|
+
an uncertified method, and says `'method-only'` for a date past the
|
|
70
|
+
checked span. Every number on this page, on the Mihrab website and in its
|
|
71
|
+
store listings is generated from that record (`scripts/readme.mjs`).
|
|
72
|
+
|
|
73
|
+
Times that differ from your mosque's?
|
|
74
|
+
[Open a report](https://github.com/MihrabHQ/mihrab-methods/issues/new?template=times-differ.yml)
|
|
75
|
+
with the city, the date, the prayer and the printed time.
|
|
153
76
|
|
|
154
77
|
## Keeping an app current
|
|
155
78
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
(`corrections`: the dates it covers and how many times it moves):
|
|
160
|
-
|
|
161
|
-
```json
|
|
162
|
-
"morocco": { "certified": true, "revision": 1,
|
|
163
|
-
"corrections": { "from": "2026-08-27", "to": "2026-11-11", "entries": 665 }, … }
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
An app that computes revision 1 can take any newer list certified for
|
|
167
|
-
revision 1 straight from `data/corrections/<authority>.json` — no release
|
|
168
|
-
needed. A higher revision means the method itself changed and its lists no
|
|
169
|
-
longer fit the older code: the app should ask its user to update.
|
|
170
|
-
`certified: false` means the authority has published tables the method is
|
|
171
|
-
not yet on every minute of. Past a list's `to`, the method alone applies.
|
|
172
|
-
|
|
173
|
-
The check done right is in the library:
|
|
79
|
+
The record and the correction lists are a feed: an app that computes a
|
|
80
|
+
method's revision can take any newer list certified for that revision, with
|
|
81
|
+
no release.
|
|
174
82
|
|
|
175
83
|
```ts
|
|
176
84
|
import { fetchLatestCorrections } from 'mihrab-methods/morocco';
|
|
177
85
|
const r = await fetchLatestCorrections({ authority: 'morocco' });
|
|
178
|
-
//
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
It reads `certification.json`, takes a list only for the revision this
|
|
182
|
-
build computes, only when it is the list the certification names (dates,
|
|
183
|
-
size, and the **SHA-256** of the file as published), and puts it in force
|
|
184
|
-
(`applyCorrections` restores one an app has stored). Both files have a
|
|
185
|
-
published schema: `schemas/certification.schema.json` and
|
|
186
|
-
`schemas/corrections.schema.json`; `npm test` validates them.
|
|
187
|
-
|
|
188
|
-
Addresses: `main` is the live feed (what the Mihrab app reads). Every
|
|
189
|
-
release is also tagged, and the same paths under a tag are fixed for good:
|
|
190
|
-
`https://raw.githubusercontent.com/MihrabHQ/mihrab-methods/v1.0.0/certification.json`.
|
|
191
|
-
Pass a tag's base URL as `baseUrl` to pin.
|
|
192
|
-
|
|
193
|
-
The revision is raised whenever a change to a method or to its places'
|
|
194
|
-
points changes what it prints; `npm test` fails until certification.json
|
|
195
|
-
names the current revision and list. `CHANGELOG.md` says what each
|
|
196
|
-
revision changed in the output.
|
|
197
|
-
|
|
198
|
-
## Use
|
|
199
|
-
|
|
200
|
-
### As a package
|
|
201
|
-
|
|
202
|
-
```sh
|
|
203
|
-
npm install mihrab-methods
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
One entry point per authority, so an app ships only the data it needs
|
|
207
|
-
(Indonesia's is 100 KB; Diyanet's worldwide points and list are 1.4 MB):
|
|
208
|
-
|
|
209
|
-
| Import | Loads |
|
|
210
|
-
|---|---|
|
|
211
|
-
| `mihrab-methods` | all five |
|
|
212
|
-
| `mihrab-methods/sweden`, `/diyanet`, `/morocco`, `/algeria`, `/indonesia` | one authority: its places, list and certification |
|
|
213
|
-
| `mihrab-methods/locate` | `countryAt`, `defaultAuthorityFor` — the country at a point, and the authority whose table it prays by (400 KB of outlines) |
|
|
214
|
-
| `mihrab-methods/core` | the calls and types with no data (load authorities yourself) |
|
|
215
|
-
|
|
216
|
-
CommonJS and ES modules, with types. Node 18+, or any bundler; nothing is
|
|
217
|
-
read from disk or fetched unless you call `fetchLatestCorrections`.
|
|
218
|
-
|
|
219
|
-
```ts
|
|
220
|
-
import { placesOf, placeById, nearestPlace, timesFor, timesForRange, candidateTimes, methodInfo } from 'mihrab-methods';
|
|
221
|
-
import { defaultAuthorityFor } from 'mihrab-methods/locate';
|
|
222
|
-
|
|
223
|
-
defaultAuthorityFor(59.33, 18.07); // 'sweden' (null where none of the five is the authority)
|
|
224
|
-
const { place, distanceKm } = nearestPlace('sweden', 59.33, 18.07)!;
|
|
225
|
-
place.id; // 'sweden:stockholm' — stable; place.country 'SE'
|
|
226
|
-
placeById('diyanet:9146'); // Adana
|
|
227
|
-
|
|
228
|
-
timesFor(place, '2026-10-15'); // or { year: 2026, month: 10, day: 15 } — January is 1
|
|
229
|
-
// {
|
|
230
|
-
// status: 'certified', 'method-only' outside the certified dates: the method alone,
|
|
231
|
-
// on the printed minute all but rarely, never a minute further
|
|
232
|
-
// date: '2026-10-15', certifiedFrom: '2024-01-01', certifiedUntil: '2027-12-31', revision: 1,
|
|
233
|
-
// clock: { zone: 'Europe/Stockholm', offsetMinutes: 120, label: 'Europe/Stockholm' },
|
|
234
|
-
// times: { fajr: { instant: Date, printed: '05:13' }, sunrise: …, dhuhr: …, asr: …, maghrib: …, isha: … }
|
|
235
|
-
// }
|
|
236
|
-
timesFor(place, '2031-01-01', { strict: true }); // throws UncertifiedMethodError instead
|
|
237
|
-
timesFor(place, '2026-10-15', { asrShadow: 2 }); // the Hanafi Asr (the tables print the standard one)
|
|
238
|
-
timesForRange(place, '2026-10-01', '2026-10-31'); // a month, one DayTimes per day
|
|
239
|
-
candidateTimes(place, '2026-10-15', { corrections: false }); // the formula alone — research
|
|
240
|
-
methodInfo('morocco'); // angles, margins, rounding, revision, certification, sources, spec
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
`printed` is the wall-clock minute on `clock`, which is the clock the
|
|
244
|
-
authority prints the day in: the place's zone as kept at noon, or the
|
|
245
|
-
table's own clock where it keeps one no zone does (Morocco's GMT+1 until
|
|
246
|
-
September 2026; some of Diyanet's). An app shows `printed` and schedules
|
|
247
|
-
on `instant`; nothing needs rounding or converting.
|
|
248
|
-
|
|
249
|
-
### As a server
|
|
250
|
-
|
|
251
|
-
```sh
|
|
252
|
-
npm run serve # http://localhost:8787
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
```
|
|
256
|
-
GET /v1/times?authority=diyanet&lat=41.01&lng=28.98&date=2026-10-15
|
|
257
|
-
GET /v1/times?place=algeria:27&from=2026-10-01&to=2026-10-31
|
|
258
|
-
GET /v1/nearest?authority=morocco&lat=33.57&lng=-7.59
|
|
259
|
-
GET /v1/authority?lat=59.33&lng=18.07
|
|
260
|
-
GET /v1/places?authority=indonesia
|
|
261
|
-
GET /v1/methods
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Self-hostable, nothing stored (`tools/serve.ts`, eighty lines).
|
|
265
|
-
|
|
266
|
-
### In another language
|
|
267
|
-
|
|
268
|
-
`docs/methods.md` states each method in prose and formulas, and
|
|
269
|
-
`test/vectors.json` is a small sample of places, dates and the times to
|
|
270
|
-
expect — enough to prove a port matches, too small to be a timetable.
|
|
271
|
-
|
|
272
|
-
### This repository
|
|
273
|
-
|
|
274
|
-
```sh
|
|
275
|
-
npm install # NODE_ENV must not be "production", or the dev tools are skipped
|
|
276
|
-
npm test # the library, the schemas, the vectors; the table checks when the tables are present
|
|
277
|
-
npm run build # dist/ (CommonJS and ESM, with types)
|
|
278
|
-
npm run verify # every method against the reference timetables
|
|
279
|
-
npm run verify -- morocco indonesia
|
|
280
|
-
npm run verify -- --alone # each method without its correction list
|
|
281
|
-
npm run corrections # rewrites Sweden's correction list (`-- diyanet`, `-- morocco`, … for the others)
|
|
282
|
-
npm run certify # rewrites certification.json — what may be used
|
|
283
|
-
npm run vectors # rewrites test/vectors.json (after a method revision)
|
|
86
|
+
// 'current' | 'updated' | 'method-moved' (update the app) | 'uncertified' | 'rejected'
|
|
284
87
|
```
|
|
285
88
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
nothing.
|
|
89
|
+
It takes a list only for the revision the build computes, only when it is
|
|
90
|
+
the list the record names, and only when its SHA-256 matches. Schemas, tags
|
|
91
|
+
as fixed addresses and the details: [docs/feed.md](docs/feed.md).
|
|
290
92
|
|
|
291
|
-
##
|
|
93
|
+
## More
|
|
292
94
|
|
|
293
|
-
|
|
|
95
|
+
| | |
|
|
294
96
|
|---|---|
|
|
295
|
-
|
|
|
296
|
-
|
|
|
297
|
-
|
|
|
298
|
-
|
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
reads `certification.json` and the lists from this repository's `main`;
|
|
311
|
-
that feed does not change shape between versions (`schemas/`).
|
|
97
|
+
| [docs/usage.md](docs/usage.md) | The API, the entry points, the HTTP server, this repository's commands and layout |
|
|
98
|
+
| [docs/methods.md](docs/methods.md) | Each method in words and formulas, for ports to other languages; `test/vectors.json` to check one |
|
|
99
|
+
| [docs/status.md](docs/status.md) | Per method: what is certified, what was set aside, what is not yet covered |
|
|
100
|
+
| [docs/diyanet.md](docs/diyanet.md) | What Diyanet's worldwide tables add: Isha by country, midsummer, Türkiye's points, tables on their own clocks |
|
|
101
|
+
| [docs/feed.md](docs/feed.md) | `certification.json` and the lists as a feed: schemas, hashes, revisions, tags |
|
|
102
|
+
| [docs/legal.md](docs/legal.md) | Formulas and correction lists: why this can be used freely |
|
|
103
|
+
| [CONTRIBUTING.md](CONTRIBUTING.md) | Adding an authority: 100 % or nothing |
|
|
104
|
+
| [CHANGELOG.md](CHANGELOG.md) | Each version, and what each method revision changed in the output |
|
|
105
|
+
|
|
106
|
+
The Mihrab app computes these methods on the phone; a method that improves
|
|
107
|
+
here goes back as the same file in Mihrab's `src/providers/`.
|
|
108
|
+
|
|
109
|
+
Mihrab and this library are independent: not affiliated with, endorsed by
|
|
110
|
+
or speaking for any of the authorities whose published timetables they
|
|
111
|
+
match.
|
|
312
112
|
|
|
313
113
|
## Licence
|
|
314
114
|
|
|
315
115
|
[MPL-2.0](LICENSE). Use the library in any app, open or closed, on a phone
|
|
316
|
-
or on a server; what the licence asks is that changes to *these files* —
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
116
|
+
or on a server; what the licence asks is that changes to *these files* — a
|
|
117
|
+
method fitted closer to its table, a corrected point — are published under
|
|
118
|
+
the same terms, so an improvement to a method reaches everyone. The Mihrab
|
|
119
|
+
app stays AGPL-3.0 and includes this library as the MPL allows. Versions
|
|
120
|
+
before 1.0.0 were AGPL-3.0-or-later.
|
package/certification.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sweden": {
|
|
3
3
|
"certified": true,
|
|
4
|
-
"checkedAt": "2026-10-
|
|
4
|
+
"checkedAt": "2026-10-18",
|
|
5
|
+
"places": 108,
|
|
5
6
|
"checks": [
|
|
6
7
|
{
|
|
7
8
|
"tables": "Sweden — Islamiska Förbundet’s tables",
|
|
8
9
|
"places": 107,
|
|
9
10
|
"times": 59640,
|
|
10
11
|
"off": 0,
|
|
12
|
+
"offWithoutCorrections": 20,
|
|
11
13
|
"from": "2026-10-11",
|
|
12
14
|
"to": "2027-11-19",
|
|
13
15
|
"setAside": {
|
|
@@ -21,6 +23,7 @@
|
|
|
21
23
|
"places": 159,
|
|
22
24
|
"times": 1393794,
|
|
23
25
|
"off": 0,
|
|
26
|
+
"offWithoutCorrections": 259,
|
|
24
27
|
"from": "2024-01-01",
|
|
25
28
|
"to": "2027-12-31"
|
|
26
29
|
}
|
|
@@ -35,13 +38,15 @@
|
|
|
35
38
|
},
|
|
36
39
|
"diyanet": {
|
|
37
40
|
"certified": true,
|
|
38
|
-
"checkedAt": "2026-10-
|
|
41
|
+
"checkedAt": "2026-10-18",
|
|
42
|
+
"places": 8132,
|
|
39
43
|
"checks": [
|
|
40
44
|
{
|
|
41
45
|
"tables": "Diyanet — its tables worldwide",
|
|
42
46
|
"places": 8132,
|
|
43
47
|
"times": 69999603,
|
|
44
48
|
"off": 0,
|
|
49
|
+
"offWithoutCorrections": 9129,
|
|
45
50
|
"from": "2024-01-01",
|
|
46
51
|
"to": "2027-12-31"
|
|
47
52
|
}
|
|
@@ -56,13 +61,15 @@
|
|
|
56
61
|
},
|
|
57
62
|
"morocco": {
|
|
58
63
|
"certified": true,
|
|
59
|
-
"checkedAt": "2026-10-
|
|
64
|
+
"checkedAt": "2026-10-18",
|
|
65
|
+
"places": 164,
|
|
60
66
|
"checks": [
|
|
61
67
|
{
|
|
62
68
|
"tables": "Morocco — Habous",
|
|
63
69
|
"places": 164,
|
|
64
70
|
"times": 75768,
|
|
65
71
|
"off": 0,
|
|
72
|
+
"offWithoutCorrections": 665,
|
|
66
73
|
"from": "2026-08-27",
|
|
67
74
|
"to": "2026-11-11"
|
|
68
75
|
}
|
|
@@ -77,13 +84,15 @@
|
|
|
77
84
|
},
|
|
78
85
|
"algeria": {
|
|
79
86
|
"certified": true,
|
|
80
|
-
"checkedAt": "2026-10-
|
|
87
|
+
"checkedAt": "2026-10-18",
|
|
88
|
+
"places": 68,
|
|
81
89
|
"checks": [
|
|
82
90
|
{
|
|
83
91
|
"tables": "Algeria — Ministry of Religious Affairs",
|
|
84
92
|
"places": 68,
|
|
85
93
|
"times": 101184,
|
|
86
94
|
"off": 0,
|
|
95
|
+
"offWithoutCorrections": 642,
|
|
87
96
|
"from": "2026-10-01",
|
|
88
97
|
"to": "2027-06-05"
|
|
89
98
|
}
|
|
@@ -98,13 +107,15 @@
|
|
|
98
107
|
},
|
|
99
108
|
"indonesia": {
|
|
100
109
|
"certified": true,
|
|
101
|
-
"checkedAt": "2026-10-
|
|
110
|
+
"checkedAt": "2026-10-18",
|
|
111
|
+
"places": 517,
|
|
102
112
|
"checks": [
|
|
103
113
|
{
|
|
104
114
|
"tables": "Indonesia — Kemenag",
|
|
105
115
|
"places": 517,
|
|
106
116
|
"times": 266772,
|
|
107
117
|
"off": 0,
|
|
118
|
+
"offWithoutCorrections": 84,
|
|
108
119
|
"from": "2026-10-07",
|
|
109
120
|
"to": "2026-12-31"
|
|
110
121
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Place } from '../core/types.js';
|
|
2
2
|
export declare function indonesiaZone(province: string): string;
|
|
3
3
|
export declare const indonesia: import("../core/registry.js").AuthorityData;
|
|
4
|
-
export declare const places: Place[];
|
|
4
|
+
export declare const places: readonly Place[];
|
|
@@ -2,4 +2,4 @@ import type { Place } from '../core/types.js';
|
|
|
2
2
|
/** The ministry's clock: GMT+1, then GMT — its tables print GMT from 17 September 2026, three days before the clocks moved. */
|
|
3
3
|
export declare const MOROCCO_CLOCK: [string, number][];
|
|
4
4
|
export declare const morocco: import("../core/registry.js").AuthorityData;
|
|
5
|
-
export declare const places: Place[];
|
|
5
|
+
export declare const places: readonly Place[];
|
package/dist/cjs/core/api.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare function certifiedSpan(authority: Authority): {
|
|
|
17
17
|
to: string;
|
|
18
18
|
} | null;
|
|
19
19
|
/** The authority's places, each at the point its method is computed at. */
|
|
20
|
-
export declare function placesOf(authority: Authority): Place[];
|
|
20
|
+
export declare function placesOf(authority: Authority): readonly Place[];
|
|
21
21
|
/** The authorities this build has loaded. */
|
|
22
22
|
export declare function loadedAuthorityIds(): Authority[];
|
|
23
23
|
/** A place by its stable id (`<authority>:<key>`), among the loaded authorities. */
|
package/dist/cjs/core/api.js
CHANGED
|
@@ -38,7 +38,8 @@ function certifiedSpan(authority) {
|
|
|
38
38
|
if (!isCertified(authority))
|
|
39
39
|
return null;
|
|
40
40
|
const checks = certificationOf(authority).checks;
|
|
41
|
-
|
|
41
|
+
const tos = checks.map(c => c.to).sort();
|
|
42
|
+
return { from: checks.map(c => c.from).sort()[0], to: tos[tos.length - 1] };
|
|
42
43
|
}
|
|
43
44
|
/** The authority's places, each at the point its method is computed at. */
|
|
44
45
|
function placesOf(authority) {
|
package/dist/cjs/core/clock.d.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* the clocks go forward a Fajr before the change is printed in the new time.
|
|
6
6
|
*/
|
|
7
7
|
import type { Place, TableClock } from './types.js';
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Minutes east of UTC that a zone keeps at noon UTC of a date (month 0-based).
|
|
10
|
+
*
|
|
11
|
+
* Read from the zone's wall clock (year … minute, as every Intl has them),
|
|
12
|
+
* not from `timeZoneName: 'longOffset'`, which not every runtime supports:
|
|
13
|
+
* where it is missing the name comes back as 'GMT+1' or a zone's own
|
|
14
|
+
* abbreviation, and an offset parsed from that is silently wrong.
|
|
15
|
+
*/
|
|
9
16
|
export declare function clockOffsetAt(tz: string, year: number, month: number, day: number): number;
|
|
10
17
|
/** The clock a place's table prints a date in: its own (`Place.clock`) where it has one, else its zone's. Month 0-based. */
|
|
11
18
|
export declare function placeClockAt(place: Place, year: number, month: number, day: number): number;
|
package/dist/cjs/core/clock.js
CHANGED
|
@@ -5,15 +5,36 @@ exports.placeClockAt = placeClockAt;
|
|
|
5
5
|
exports.offsetLabel = offsetLabel;
|
|
6
6
|
exports.tableClockAt = tableClockAt;
|
|
7
7
|
exports.printedAt = printedAt;
|
|
8
|
-
const
|
|
9
|
-
/**
|
|
8
|
+
const formats = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Minutes east of UTC that a zone keeps at noon UTC of a date (month 0-based).
|
|
11
|
+
*
|
|
12
|
+
* Read from the zone's wall clock (year … minute, as every Intl has them),
|
|
13
|
+
* not from `timeZoneName: 'longOffset'`, which not every runtime supports:
|
|
14
|
+
* where it is missing the name comes back as 'GMT+1' or a zone's own
|
|
15
|
+
* abbreviation, and an offset parsed from that is silently wrong.
|
|
16
|
+
*/
|
|
10
17
|
function clockOffsetAt(tz, year, month, day) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const at = Date.UTC(year, month, day, 12);
|
|
19
|
+
let f = formats.get(tz);
|
|
20
|
+
if (!f) {
|
|
21
|
+
f = new Intl.DateTimeFormat('en-US', {
|
|
22
|
+
timeZone: tz,
|
|
23
|
+
year: 'numeric',
|
|
24
|
+
month: 'numeric',
|
|
25
|
+
day: 'numeric',
|
|
26
|
+
hour: 'numeric',
|
|
27
|
+
minute: 'numeric',
|
|
28
|
+
hourCycle: 'h23',
|
|
29
|
+
});
|
|
30
|
+
formats.set(tz, f);
|
|
31
|
+
}
|
|
32
|
+
const parts = f.formatToParts(new Date(at));
|
|
33
|
+
const n = (type) => Number(parts.find(p => p.type === type)?.value);
|
|
34
|
+
const wall = Date.UTC(n('year'), n('month') - 1, n('day'), n('hour') % 24, n('minute'));
|
|
35
|
+
if (!Number.isFinite(wall))
|
|
36
|
+
throw new RangeError(`The zone ${tz} could not be read on this runtime.`);
|
|
37
|
+
return Math.round((wall - at) / 60_000);
|
|
17
38
|
}
|
|
18
39
|
/** The clock a place's table prints a date in: its own (`Place.clock`) where it has one, else its zone's. Month 0-based. */
|
|
19
40
|
function placeClockAt(place, year, month, day) {
|