mihrab-methods 1.3.2__py3-none-any.whl
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.
- mihrab_methods/__init__.py +7 -0
- mihrab_methods/api.py +162 -0
- mihrab_methods/data/certification.json +154 -0
- mihrab_methods/data/corrections/algeria.json +784 -0
- mihrab_methods/data/corrections/diyanet.json +18899 -0
- mihrab_methods/data/corrections/indonesia.json +606 -0
- mihrab_methods/data/corrections/morocco.json +280 -0
- mihrab_methods/data/corrections/sweden.json +516 -0
- mihrab_methods/data/corrections/turkiyetakvimi.json +1886 -0
- mihrab_methods/methods.py +638 -0
- mihrab_methods/sun.py +234 -0
- mihrab_methods-1.3.2.dist-info/METADATA +77 -0
- mihrab_methods-1.3.2.dist-info/RECORD +15 -0
- mihrab_methods-1.3.2.dist-info/WHEEL +5 -0
- mihrab_methods-1.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""mihrab-methods in Python: the prayer-time methods of five authorities,
|
|
2
|
+
ported from the TypeScript library and held to its test vectors."""
|
|
3
|
+
from .api import (PRAYERS, REVISIONS, CorrectionList, clock_offset_at, load_corrections, minutes_for, place_clock_at,
|
|
4
|
+
point_key, printed, times_for)
|
|
5
|
+
|
|
6
|
+
__all__ = ['PRAYERS', 'REVISIONS', 'CorrectionList', 'clock_offset_at', 'load_corrections', 'minutes_for',
|
|
7
|
+
'place_clock_at', 'point_key', 'printed', 'times_for']
|
mihrab_methods/api.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""A place and a date in, the day's six times out: the authority's method,
|
|
2
|
+
then its correction list — what `timesFor` does in the TypeScript library.
|
|
3
|
+
|
|
4
|
+
A place is a dict shaped like the library's `Place` (see docs/api.md):
|
|
5
|
+
`authority`, `lat`, `lon`, `tz`, and where the list has them `ishaDeg`,
|
|
6
|
+
`clock`, `prayerPoints`, `dipDeg`, `dipMaghribDeg`, `highland`,
|
|
7
|
+
`twilightDip`, `afternoon`, `temkin`.
|
|
8
|
+
"""
|
|
9
|
+
import datetime
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import os
|
|
13
|
+
from decimal import ROUND_HALF_UP, Decimal
|
|
14
|
+
from zoneinfo import ZoneInfo
|
|
15
|
+
|
|
16
|
+
from . import methods
|
|
17
|
+
|
|
18
|
+
PRAYERS = ('fajr', 'sunrise', 'dhuhr', 'asr', 'maghrib', 'isha')
|
|
19
|
+
#: The method revisions this port reproduces. A correction list belongs to one revision.
|
|
20
|
+
REVISIONS = {'sweden': 1, 'diyanet': 1, 'morocco': 2, 'algeria': 1, 'indonesia': 2, 'turkiyetakvimi': 1}
|
|
21
|
+
|
|
22
|
+
_UTC = datetime.timezone.utc
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _to_fixed(x):
|
|
26
|
+
"""A coordinate to three decimals, as JavaScript's toFixed(3) writes it:
|
|
27
|
+
the binary value's exact decimal expansion, ties away from zero."""
|
|
28
|
+
return str(Decimal(float(x) + 0.0).quantize(Decimal('0.001'), rounding=ROUND_HALF_UP)) # + 0.0: -0 is written 0.000
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def point_key(lat, lon):
|
|
32
|
+
return '%s,%s' % (_to_fixed(lat), _to_fixed(lon))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CorrectionList:
|
|
36
|
+
"""data/corrections/<authority>.json, indexed: point -> date -> {prayer: minutes}."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, source):
|
|
39
|
+
self.source = source
|
|
40
|
+
self._index = {}
|
|
41
|
+
for point, entries in source.get('points', {}).items():
|
|
42
|
+
by_date = self._index.setdefault(point, {})
|
|
43
|
+
for e in entries:
|
|
44
|
+
date, key, step = e.split(' ')
|
|
45
|
+
by_date.setdefault(date, {})[key] = int(step)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def load(cls, path):
|
|
49
|
+
with open(path, encoding='utf-8') as f:
|
|
50
|
+
return cls(json.load(f))
|
|
51
|
+
|
|
52
|
+
def correction(self, lat, lon, date):
|
|
53
|
+
return self._index.get(point_key(lat, lon), {}).get(date)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def clock_offset_at(tz, year, month0, day):
|
|
57
|
+
"""Minutes east of UTC that a zone keeps at noon UTC of a date."""
|
|
58
|
+
at = datetime.datetime(year, month0 + 1, 1, 12, tzinfo=_UTC) + datetime.timedelta(days=day - 1)
|
|
59
|
+
return round(at.astimezone(ZoneInfo(tz)).utcoffset().total_seconds() / 60)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def place_clock_at(place, year, month0, day):
|
|
63
|
+
"""The clock a place's table prints a date in: its own where it has one, else its zone's."""
|
|
64
|
+
clock = place.get('clock')
|
|
65
|
+
if clock:
|
|
66
|
+
date = '%04d-%02d-%02d' % (year, month0 + 1, day)
|
|
67
|
+
off = clock[0][1]
|
|
68
|
+
for start, o in clock:
|
|
69
|
+
if start <= date:
|
|
70
|
+
off = o
|
|
71
|
+
return off
|
|
72
|
+
return clock_offset_at(place['tz'], year, month0, day)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _congress(place, year, month0, day, asr_shadow):
|
|
76
|
+
clock = place_clock_at(place, year, month0, day)
|
|
77
|
+
isha_deg = place.get('ishaDeg', methods.CONGRESS_ISHA_DEG)
|
|
78
|
+
pts_by_year = place.get('prayerPoints')
|
|
79
|
+
if pts_by_year:
|
|
80
|
+
years = sorted(int(y) for y in pts_by_year)
|
|
81
|
+
best = years[0]
|
|
82
|
+
for y in years:
|
|
83
|
+
if abs(y - year) < abs(best - year):
|
|
84
|
+
best = y
|
|
85
|
+
pts = pts_by_year[str(best)]
|
|
86
|
+
raw = {k: methods.congress_minutes(pts[i][0], pts[i][1], year, month0, day, asr_shadow, isha_deg, clock)[k]
|
|
87
|
+
for i, k in enumerate(PRAYERS)}
|
|
88
|
+
else:
|
|
89
|
+
raw = methods.congress_minutes(place['lat'], place['lon'], year, month0, day, asr_shadow, isha_deg, clock)
|
|
90
|
+
# the library carries an instant in whole milliseconds, then rounds it to the minute
|
|
91
|
+
return {k: math.floor(math.trunc(v * 60000) / 60000 + 0.5) for k, v in raw.items()}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def minutes_for(place, date, corrections=None, asr_shadow=1):
|
|
95
|
+
"""Minutes past 00:00 UT of `date` ('YYYY-MM-DD') for the six times, or None where the method gives none."""
|
|
96
|
+
year, month, day = (int(p) for p in date.split('-'))
|
|
97
|
+
month0 = month - 1
|
|
98
|
+
a = place['authority']
|
|
99
|
+
if a in ('sweden', 'diyanet'):
|
|
100
|
+
t = _congress(place, year, month0, day, asr_shadow)
|
|
101
|
+
elif a == 'morocco':
|
|
102
|
+
t = methods.morocco_minutes(place['lat'], place['lon'], year, month0, day, place.get('dipDeg'), asr_shadow,
|
|
103
|
+
place.get('dipMaghribDeg'))
|
|
104
|
+
elif a == 'algeria':
|
|
105
|
+
t = methods.algeria_minutes(place['lat'], place['lon'], year, month0, day, asr_shadow)
|
|
106
|
+
elif a == 'indonesia':
|
|
107
|
+
t = methods.kemenag_minutes(place['lat'], place['lon'], year, month0, day, bool(place.get('highland')),
|
|
108
|
+
bool(place.get('twilightDip')), asr_shadow, place.get('afternoon'))
|
|
109
|
+
elif a == 'turkiyetakvimi':
|
|
110
|
+
t = methods.takvim_minutes(place['lat'], place['lon'], year, month0, day, place.get('temkin'), asr_shadow)
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError('unknown authority: %r' % (a,))
|
|
113
|
+
if t is None:
|
|
114
|
+
return None
|
|
115
|
+
c = corrections.correction(place['lat'], place['lon'], date) if corrections else None
|
|
116
|
+
if c:
|
|
117
|
+
for k, step in c.items():
|
|
118
|
+
if not (k == 'asr' and asr_shadow != 1): # the lists are of the standard Asr only
|
|
119
|
+
t[k] += step
|
|
120
|
+
return t
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def printed(minutes, offset_minutes):
|
|
124
|
+
"""'HH:MM' of a time on a clock, as a table prints it."""
|
|
125
|
+
m = (minutes + offset_minutes) % 1440
|
|
126
|
+
return '%02d:%02d' % (m // 60, m % 60)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def times_for(place, date, corrections=None, asr_shadow=1):
|
|
130
|
+
"""The day as a table prints it: {'clock': minutes east of UTC, 'printed': {...}, 'instants': {...}} or None.
|
|
131
|
+
|
|
132
|
+
`corrections` is the authority's CorrectionList; without it the times are the bare method's,
|
|
133
|
+
which is the published table to within the list's few minutes, not on every one.
|
|
134
|
+
"""
|
|
135
|
+
t = minutes_for(place, date, corrections, asr_shadow)
|
|
136
|
+
if t is None:
|
|
137
|
+
return None
|
|
138
|
+
year, month, day = (int(p) for p in date.split('-'))
|
|
139
|
+
off = place_clock_at(place, year, month - 1, day)
|
|
140
|
+
start = datetime.datetime(year, month, day, tzinfo=_UTC)
|
|
141
|
+
return {
|
|
142
|
+
'clock': off,
|
|
143
|
+
'printed': {k: printed(t[k], off) for k in PRAYERS},
|
|
144
|
+
'instants': {k: start + datetime.timedelta(minutes=t[k]) for k in PRAYERS},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
#: The lists the installed package carries (those of its release); absent in a source checkout.
|
|
149
|
+
BUNDLED_CORRECTIONS = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data', 'corrections')
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def load_corrections(directory=None):
|
|
153
|
+
"""Every list in a directory laid out as the repository's data/corrections;
|
|
154
|
+
with no directory, the lists the installed package carries."""
|
|
155
|
+
if directory is None:
|
|
156
|
+
directory = BUNDLED_CORRECTIONS
|
|
157
|
+
out = {}
|
|
158
|
+
for a in REVISIONS:
|
|
159
|
+
p = os.path.join(directory, a + '.json')
|
|
160
|
+
if os.path.exists(p):
|
|
161
|
+
out[a] = CorrectionList.load(p)
|
|
162
|
+
return out
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sweden": {
|
|
3
|
+
"certified": true,
|
|
4
|
+
"checkedAt": "2026-11-09",
|
|
5
|
+
"places": 108,
|
|
6
|
+
"checks": [
|
|
7
|
+
{
|
|
8
|
+
"tables": "Sweden — Islamiska Förbundet’s tables",
|
|
9
|
+
"places": 107,
|
|
10
|
+
"times": 63114,
|
|
11
|
+
"off": 0,
|
|
12
|
+
"offWithoutCorrections": 20,
|
|
13
|
+
"from": "2026-10-11",
|
|
14
|
+
"to": "2027-12-14",
|
|
15
|
+
"setAside": {
|
|
16
|
+
"misplaced": [],
|
|
17
|
+
"sourceFaults": 35491,
|
|
18
|
+
"copiedRows": 696
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"tables": "Sweden — Diyanet’s own tables",
|
|
23
|
+
"places": 159,
|
|
24
|
+
"times": 1393794,
|
|
25
|
+
"off": 0,
|
|
26
|
+
"offWithoutCorrections": 259,
|
|
27
|
+
"from": "2024-01-01",
|
|
28
|
+
"to": "2027-12-31"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"corrections": {
|
|
32
|
+
"from": "2024-01-01",
|
|
33
|
+
"to": "2027-12-31",
|
|
34
|
+
"entries": 270,
|
|
35
|
+
"sha256": "173d7145b5ec917882f8ac9890dd1fa5f3143bd714eb540482861bf8a6bac660"
|
|
36
|
+
},
|
|
37
|
+
"revision": 1
|
|
38
|
+
},
|
|
39
|
+
"diyanet": {
|
|
40
|
+
"certified": true,
|
|
41
|
+
"checkedAt": "2026-11-11",
|
|
42
|
+
"places": 8318,
|
|
43
|
+
"checks": [
|
|
44
|
+
{
|
|
45
|
+
"tables": "Diyanet — its tables worldwide",
|
|
46
|
+
"places": 8318,
|
|
47
|
+
"times": 71630078,
|
|
48
|
+
"off": 0,
|
|
49
|
+
"offWithoutCorrections": 9368,
|
|
50
|
+
"from": "2024-01-01",
|
|
51
|
+
"to": "2027-12-31"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"corrections": {
|
|
55
|
+
"from": "2024-01-01",
|
|
56
|
+
"to": "2027-12-31",
|
|
57
|
+
"entries": 9357,
|
|
58
|
+
"sha256": "a8c6a7305611f8dcedc04a64efb8dd150d9a588c2b3ac3391bfb776d70a2ec83"
|
|
59
|
+
},
|
|
60
|
+
"revision": 1
|
|
61
|
+
},
|
|
62
|
+
"morocco": {
|
|
63
|
+
"certified": true,
|
|
64
|
+
"checkedAt": "2026-11-11",
|
|
65
|
+
"places": 164,
|
|
66
|
+
"checks": [
|
|
67
|
+
{
|
|
68
|
+
"tables": "Morocco — Habous",
|
|
69
|
+
"places": 164,
|
|
70
|
+
"times": 94350,
|
|
71
|
+
"off": 0,
|
|
72
|
+
"offWithoutCorrections": 114,
|
|
73
|
+
"from": "2026-08-27",
|
|
74
|
+
"to": "2026-11-30"
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"corrections": {
|
|
78
|
+
"from": "2026-08-27",
|
|
79
|
+
"to": "2026-11-30",
|
|
80
|
+
"entries": 114,
|
|
81
|
+
"sha256": "2ae7161a7164f7ccebf1aa9f81628a810ce8fdd337e0645907e6c58d0be6aae9"
|
|
82
|
+
},
|
|
83
|
+
"revision": 2
|
|
84
|
+
},
|
|
85
|
+
"algeria": {
|
|
86
|
+
"certified": true,
|
|
87
|
+
"checkedAt": "2026-11-09",
|
|
88
|
+
"places": 68,
|
|
89
|
+
"checks": [
|
|
90
|
+
{
|
|
91
|
+
"tables": "Algeria — Ministry of Religious Affairs",
|
|
92
|
+
"places": 68,
|
|
93
|
+
"times": 101184,
|
|
94
|
+
"off": 0,
|
|
95
|
+
"offWithoutCorrections": 642,
|
|
96
|
+
"from": "2026-10-01",
|
|
97
|
+
"to": "2027-06-05"
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"corrections": {
|
|
101
|
+
"from": "2026-10-01",
|
|
102
|
+
"to": "2027-06-05",
|
|
103
|
+
"entries": 642,
|
|
104
|
+
"sha256": "f8d227cdd88aabe5bcd1775ec0324abfdae07a8a3986fdd19816337c2d4ab3ca"
|
|
105
|
+
},
|
|
106
|
+
"revision": 1
|
|
107
|
+
},
|
|
108
|
+
"indonesia": {
|
|
109
|
+
"certified": true,
|
|
110
|
+
"checkedAt": "2026-11-11",
|
|
111
|
+
"places": 517,
|
|
112
|
+
"checks": [
|
|
113
|
+
{
|
|
114
|
+
"tables": "Indonesia — Kemenag",
|
|
115
|
+
"places": 517,
|
|
116
|
+
"times": 362934,
|
|
117
|
+
"off": 0,
|
|
118
|
+
"offWithoutCorrections": 234,
|
|
119
|
+
"from": "2026-10-07",
|
|
120
|
+
"to": "2027-01-31"
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"corrections": {
|
|
124
|
+
"from": "2026-10-07",
|
|
125
|
+
"to": "2027-01-31",
|
|
126
|
+
"entries": 234,
|
|
127
|
+
"sha256": "a3f1c30576ddddbfda6cadabe442e1bcf3fb476b018b269d7d4783edd4e39d03"
|
|
128
|
+
},
|
|
129
|
+
"revision": 2
|
|
130
|
+
},
|
|
131
|
+
"turkiyetakvimi": {
|
|
132
|
+
"certified": true,
|
|
133
|
+
"checkedAt": "2026-11-11",
|
|
134
|
+
"places": 821,
|
|
135
|
+
"checks": [
|
|
136
|
+
{
|
|
137
|
+
"tables": "Türkiye Takvimi — its cities’ years",
|
|
138
|
+
"places": 821,
|
|
139
|
+
"times": 1812768,
|
|
140
|
+
"off": 0,
|
|
141
|
+
"offWithoutCorrections": 860,
|
|
142
|
+
"from": "2025-12-31",
|
|
143
|
+
"to": "2027-01-02"
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"corrections": {
|
|
147
|
+
"from": "2025-12-31",
|
|
148
|
+
"to": "2027-01-02",
|
|
149
|
+
"entries": 860,
|
|
150
|
+
"sha256": "edf5bd37ccd06dbcb87300a6c943353adb624a545ff6ea416a8f89715713d7bd"
|
|
151
|
+
},
|
|
152
|
+
"revision": 1
|
|
153
|
+
}
|
|
154
|
+
}
|