mihrab-methods 0.0.0-stage → 1.0.1

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 (217) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +319 -2
  3. package/certification.json +120 -0
  4. package/data/corrections/algeria.json +784 -0
  5. package/data/corrections/diyanet.json +18444 -0
  6. package/data/corrections/indonesia.json +210 -0
  7. package/data/corrections/morocco.json +993 -0
  8. package/data/corrections/sweden.json +516 -0
  9. package/dist/cjs/algeria.d.ts +4 -0
  10. package/dist/cjs/algeria.js +22 -0
  11. package/dist/cjs/authorities/algeria.d.ts +3 -0
  12. package/dist/cjs/authorities/algeria.js +57 -0
  13. package/dist/cjs/authorities/diyanet.d.ts +9 -0
  14. package/dist/cjs/authorities/diyanet.js +90 -0
  15. package/dist/cjs/authorities/indonesia.d.ts +4 -0
  16. package/dist/cjs/authorities/indonesia.js +82 -0
  17. package/dist/cjs/authorities/morocco.d.ts +5 -0
  18. package/dist/cjs/authorities/morocco.js +65 -0
  19. package/dist/cjs/authorities/slug.d.ts +2 -0
  20. package/dist/cjs/authorities/slug.js +17 -0
  21. package/dist/cjs/authorities/sweden.d.ts +3 -0
  22. package/dist/cjs/authorities/sweden.js +58 -0
  23. package/dist/cjs/core/api.d.ts +61 -0
  24. package/dist/cjs/core/api.js +177 -0
  25. package/dist/cjs/core/civilDate.d.ts +17 -0
  26. package/dist/cjs/core/civilDate.js +41 -0
  27. package/dist/cjs/core/clock.d.ts +22 -0
  28. package/dist/cjs/core/clock.js +67 -0
  29. package/dist/cjs/core/compute.d.ts +10 -0
  30. package/dist/cjs/core/compute.js +59 -0
  31. package/dist/cjs/core/correctionList.d.ts +25 -0
  32. package/dist/cjs/core/correctionList.js +62 -0
  33. package/dist/cjs/core/fetchLatest.d.ts +61 -0
  34. package/dist/cjs/core/fetchLatest.js +192 -0
  35. package/dist/cjs/core/index.d.ts +14 -0
  36. package/dist/cjs/core/index.js +45 -0
  37. package/dist/cjs/core/registry.d.ts +28 -0
  38. package/dist/cjs/core/registry.js +34 -0
  39. package/dist/cjs/core/types.d.ts +193 -0
  40. package/dist/cjs/core/types.js +22 -0
  41. package/dist/cjs/core.d.ts +2 -0
  42. package/dist/cjs/core.js +18 -0
  43. package/dist/cjs/data/generated/algeriaCertification.d.ts +3 -0
  44. package/dist/cjs/data/generated/algeriaCertification.js +4 -0
  45. package/dist/cjs/data/generated/algeriaCities.d.ts +8 -0
  46. package/dist/cjs/data/generated/algeriaCities.js +5 -0
  47. package/dist/cjs/data/generated/algeriaCorrections.d.ts +3 -0
  48. package/dist/cjs/data/generated/algeriaCorrections.js +4 -0
  49. package/dist/cjs/data/generated/algeriaMethodPoints.d.ts +2 -0
  50. package/dist/cjs/data/generated/algeriaMethodPoints.js +5 -0
  51. package/dist/cjs/data/generated/countryOutlines.d.ts +5 -0
  52. package/dist/cjs/data/generated/countryOutlines.js +5 -0
  53. package/dist/cjs/data/generated/diyanetCertification.d.ts +3 -0
  54. package/dist/cjs/data/generated/diyanetCertification.js +4 -0
  55. package/dist/cjs/data/generated/diyanetCorrections.d.ts +3 -0
  56. package/dist/cjs/data/generated/diyanetCorrections.js +4 -0
  57. package/dist/cjs/data/generated/diyanetCountries.d.ts +5 -0
  58. package/dist/cjs/data/generated/diyanetCountries.js +5 -0
  59. package/dist/cjs/data/generated/diyanetTurkeyPoints.d.ts +7 -0
  60. package/dist/cjs/data/generated/diyanetTurkeyPoints.js +5 -0
  61. package/dist/cjs/data/generated/diyanetWorldPoints.d.ts +10 -0
  62. package/dist/cjs/data/generated/diyanetWorldPoints.js +5 -0
  63. package/dist/cjs/data/generated/indonesiaCertification.d.ts +3 -0
  64. package/dist/cjs/data/generated/indonesiaCertification.js +4 -0
  65. package/dist/cjs/data/generated/indonesiaCorrections.d.ts +3 -0
  66. package/dist/cjs/data/generated/indonesiaCorrections.js +4 -0
  67. package/dist/cjs/data/generated/kemenagMethodPoints.d.ts +2 -0
  68. package/dist/cjs/data/generated/kemenagMethodPoints.js +5 -0
  69. package/dist/cjs/data/generated/kemenagPlaces.d.ts +8 -0
  70. package/dist/cjs/data/generated/kemenagPlaces.js +5 -0
  71. package/dist/cjs/data/generated/moroccoCertification.d.ts +3 -0
  72. package/dist/cjs/data/generated/moroccoCertification.js +4 -0
  73. package/dist/cjs/data/generated/moroccoCities.d.ts +8 -0
  74. package/dist/cjs/data/generated/moroccoCities.js +5 -0
  75. package/dist/cjs/data/generated/moroccoCorrections.d.ts +3 -0
  76. package/dist/cjs/data/generated/moroccoCorrections.js +4 -0
  77. package/dist/cjs/data/generated/moroccoMethodPoints.d.ts +2 -0
  78. package/dist/cjs/data/generated/moroccoMethodPoints.js +5 -0
  79. package/dist/cjs/data/generated/swedenCertification.d.ts +3 -0
  80. package/dist/cjs/data/generated/swedenCertification.js +4 -0
  81. package/dist/cjs/data/generated/swedenCorrections.d.ts +3 -0
  82. package/dist/cjs/data/generated/swedenCorrections.js +4 -0
  83. package/dist/cjs/data/generated/swedenPlaces.d.ts +5 -0
  84. package/dist/cjs/data/generated/swedenPlaces.js +5 -0
  85. package/dist/cjs/diyanet.d.ts +4 -0
  86. package/dist/cjs/diyanet.js +22 -0
  87. package/dist/cjs/index.d.ts +19 -0
  88. package/dist/cjs/index.js +37 -0
  89. package/dist/cjs/indonesia.d.ts +4 -0
  90. package/dist/cjs/indonesia.js +22 -0
  91. package/dist/cjs/locate.d.ts +21 -0
  92. package/dist/cjs/locate.js +204 -0
  93. package/dist/cjs/methods/algeria.d.ts +56 -0
  94. package/dist/cjs/methods/algeria.js +119 -0
  95. package/dist/cjs/methods/indonesia.d.ts +31 -0
  96. package/dist/cjs/methods/indonesia.js +94 -0
  97. package/dist/cjs/methods/morocco.d.ts +61 -0
  98. package/dist/cjs/methods/morocco.js +113 -0
  99. package/dist/cjs/methods/revisions.d.ts +15 -0
  100. package/dist/cjs/methods/revisions.js +18 -0
  101. package/dist/cjs/methods/scratch-congress.d.ts +87 -0
  102. package/dist/cjs/methods/scratch-congress.js +539 -0
  103. package/dist/cjs/methods/sweden.d.ts +88 -0
  104. package/dist/cjs/methods/sweden.js +568 -0
  105. package/dist/cjs/morocco.d.ts +4 -0
  106. package/dist/cjs/morocco.js +22 -0
  107. package/dist/cjs/sun/preciseSun.d.ts +19 -0
  108. package/dist/cjs/sun/preciseSun.js +185 -0
  109. package/dist/cjs/sweden.d.ts +4 -0
  110. package/dist/cjs/sweden.js +22 -0
  111. package/dist/esm/algeria.d.ts +4 -0
  112. package/dist/esm/algeria.js +4 -0
  113. package/dist/esm/authorities/algeria.d.ts +3 -0
  114. package/dist/esm/authorities/algeria.js +51 -0
  115. package/dist/esm/authorities/diyanet.d.ts +9 -0
  116. package/dist/esm/authorities/diyanet.js +83 -0
  117. package/dist/esm/authorities/indonesia.d.ts +4 -0
  118. package/dist/esm/authorities/indonesia.js +75 -0
  119. package/dist/esm/authorities/morocco.d.ts +5 -0
  120. package/dist/esm/authorities/morocco.js +59 -0
  121. package/dist/esm/authorities/slug.d.ts +2 -0
  122. package/dist/esm/authorities/slug.js +14 -0
  123. package/dist/esm/authorities/sweden.d.ts +3 -0
  124. package/dist/esm/authorities/sweden.js +52 -0
  125. package/dist/esm/core/api.d.ts +61 -0
  126. package/dist/esm/core/api.js +163 -0
  127. package/dist/esm/core/civilDate.d.ts +17 -0
  128. package/dist/esm/core/civilDate.js +36 -0
  129. package/dist/esm/core/clock.d.ts +22 -0
  130. package/dist/esm/core/clock.js +60 -0
  131. package/dist/esm/core/compute.d.ts +10 -0
  132. package/dist/esm/core/compute.js +56 -0
  133. package/dist/esm/core/correctionList.d.ts +25 -0
  134. package/dist/esm/core/correctionList.js +56 -0
  135. package/dist/esm/core/fetchLatest.d.ts +61 -0
  136. package/dist/esm/core/fetchLatest.js +184 -0
  137. package/dist/esm/core/index.d.ts +14 -0
  138. package/dist/esm/core/index.js +14 -0
  139. package/dist/esm/core/registry.d.ts +28 -0
  140. package/dist/esm/core/registry.js +28 -0
  141. package/dist/esm/core/types.d.ts +193 -0
  142. package/dist/esm/core/types.js +18 -0
  143. package/dist/esm/core.d.ts +2 -0
  144. package/dist/esm/core.js +2 -0
  145. package/dist/esm/data/generated/algeriaCertification.d.ts +3 -0
  146. package/dist/esm/data/generated/algeriaCertification.js +2 -0
  147. package/dist/esm/data/generated/algeriaCities.d.ts +8 -0
  148. package/dist/esm/data/generated/algeriaCities.js +3 -0
  149. package/dist/esm/data/generated/algeriaCorrections.d.ts +3 -0
  150. package/dist/esm/data/generated/algeriaCorrections.js +2 -0
  151. package/dist/esm/data/generated/algeriaMethodPoints.d.ts +2 -0
  152. package/dist/esm/data/generated/algeriaMethodPoints.js +3 -0
  153. package/dist/esm/data/generated/countryOutlines.d.ts +5 -0
  154. package/dist/esm/data/generated/countryOutlines.js +3 -0
  155. package/dist/esm/data/generated/diyanetCertification.d.ts +3 -0
  156. package/dist/esm/data/generated/diyanetCertification.js +2 -0
  157. package/dist/esm/data/generated/diyanetCorrections.d.ts +3 -0
  158. package/dist/esm/data/generated/diyanetCorrections.js +2 -0
  159. package/dist/esm/data/generated/diyanetCountries.d.ts +5 -0
  160. package/dist/esm/data/generated/diyanetCountries.js +3 -0
  161. package/dist/esm/data/generated/diyanetTurkeyPoints.d.ts +7 -0
  162. package/dist/esm/data/generated/diyanetTurkeyPoints.js +3 -0
  163. package/dist/esm/data/generated/diyanetWorldPoints.d.ts +10 -0
  164. package/dist/esm/data/generated/diyanetWorldPoints.js +3 -0
  165. package/dist/esm/data/generated/indonesiaCertification.d.ts +3 -0
  166. package/dist/esm/data/generated/indonesiaCertification.js +2 -0
  167. package/dist/esm/data/generated/indonesiaCorrections.d.ts +3 -0
  168. package/dist/esm/data/generated/indonesiaCorrections.js +2 -0
  169. package/dist/esm/data/generated/kemenagMethodPoints.d.ts +2 -0
  170. package/dist/esm/data/generated/kemenagMethodPoints.js +3 -0
  171. package/dist/esm/data/generated/kemenagPlaces.d.ts +8 -0
  172. package/dist/esm/data/generated/kemenagPlaces.js +3 -0
  173. package/dist/esm/data/generated/moroccoCertification.d.ts +3 -0
  174. package/dist/esm/data/generated/moroccoCertification.js +2 -0
  175. package/dist/esm/data/generated/moroccoCities.d.ts +8 -0
  176. package/dist/esm/data/generated/moroccoCities.js +3 -0
  177. package/dist/esm/data/generated/moroccoCorrections.d.ts +3 -0
  178. package/dist/esm/data/generated/moroccoCorrections.js +2 -0
  179. package/dist/esm/data/generated/moroccoMethodPoints.d.ts +2 -0
  180. package/dist/esm/data/generated/moroccoMethodPoints.js +3 -0
  181. package/dist/esm/data/generated/swedenCertification.d.ts +3 -0
  182. package/dist/esm/data/generated/swedenCertification.js +2 -0
  183. package/dist/esm/data/generated/swedenCorrections.d.ts +3 -0
  184. package/dist/esm/data/generated/swedenCorrections.js +2 -0
  185. package/dist/esm/data/generated/swedenPlaces.d.ts +5 -0
  186. package/dist/esm/data/generated/swedenPlaces.js +3 -0
  187. package/dist/esm/diyanet.d.ts +4 -0
  188. package/dist/esm/diyanet.js +4 -0
  189. package/dist/esm/index.d.ts +19 -0
  190. package/dist/esm/index.js +19 -0
  191. package/dist/esm/indonesia.d.ts +4 -0
  192. package/dist/esm/indonesia.js +4 -0
  193. package/dist/esm/locate.d.ts +21 -0
  194. package/dist/esm/locate.js +196 -0
  195. package/dist/esm/methods/algeria.d.ts +56 -0
  196. package/dist/esm/methods/algeria.js +114 -0
  197. package/dist/esm/methods/indonesia.d.ts +31 -0
  198. package/dist/esm/methods/indonesia.js +90 -0
  199. package/dist/esm/methods/morocco.d.ts +61 -0
  200. package/dist/esm/methods/morocco.js +107 -0
  201. package/dist/esm/methods/revisions.d.ts +15 -0
  202. package/dist/esm/methods/revisions.js +15 -0
  203. package/dist/esm/methods/scratch-congress.d.ts +87 -0
  204. package/dist/esm/methods/scratch-congress.js +534 -0
  205. package/dist/esm/methods/sweden.d.ts +88 -0
  206. package/dist/esm/methods/sweden.js +563 -0
  207. package/dist/esm/morocco.d.ts +4 -0
  208. package/dist/esm/morocco.js +4 -0
  209. package/dist/esm/package.json +1 -0
  210. package/dist/esm/sun/preciseSun.d.ts +19 -0
  211. package/dist/esm/sun/preciseSun.js +181 -0
  212. package/dist/esm/sweden.d.ts +4 -0
  213. package/dist/esm/sweden.js +4 -0
  214. package/docs/methods.md +190 -0
  215. package/package.json +178 -4
  216. package/schemas/certification.schema.json +59 -0
  217. package/schemas/corrections.schema.json +29 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The library's calls. Each works for whichever authorities are loaded —
3
+ * `mihrab-methods` loads all five, `mihrab-methods/<authority>` one.
4
+ */
5
+ import { type ComputeOptions } from './compute.js';
6
+ import { type Authority, type Certification, type CivilDate, type DayTimes, type MethodInfo, type Place, type TimesOptions } from './types.js';
7
+ /**
8
+ * Certified: every check of the authority's tables has every time on the
9
+ * printed minute (and checked something). The flag alone is not trusted.
10
+ */
11
+ export declare function isCertified(authority: Authority): boolean;
12
+ /** The authority's record from certification.json, or null. */
13
+ export declare function certificationOf(authority: Authority): Certification | null;
14
+ /** The span of dates the method was checked over (and so is certified for), or null. */
15
+ export declare function certifiedSpan(authority: Authority): {
16
+ from: string;
17
+ to: string;
18
+ } | null;
19
+ /** The authority's places, each at the point its method is computed at. */
20
+ export declare function placesOf(authority: Authority): readonly Place[];
21
+ /** The authorities this build has loaded. */
22
+ export declare function loadedAuthorityIds(): Authority[];
23
+ /** A place by its stable id (`<authority>:<key>`), among the loaded authorities. */
24
+ export declare function placeById(id: string): Place | null;
25
+ /** Great-circle distance, km. */
26
+ export declare function distanceKm(lat1: number, lon1: number, lat2: number, lon2: number): number;
27
+ export type NearestPlace = {
28
+ place: Place;
29
+ distanceKm: number;
30
+ };
31
+ /**
32
+ * The authority's place nearest a point — how an app finds the table for a
33
+ * location. `maxKm` (default: no limit) returns null beyond that distance.
34
+ * Türkiye's districts and Diyanet's worldwide places are both 'diyanet'.
35
+ */
36
+ export declare function nearestPlace(authority: Authority, lat: number, lon: number, opts?: {
37
+ maxKm?: number;
38
+ }): NearestPlace | null;
39
+ /**
40
+ * The place's prayer times on a civil date, as the authority prints them:
41
+ * each prayer's exact moment and its wall-clock minute on the table's clock.
42
+ *
43
+ * Inside the dates the method was certified over, `status` is 'certified':
44
+ * the authority's own printed minute. Outside them it is 'method-only': the
45
+ * method alone, which is on the printed minute all but rarely and never a
46
+ * minute further — the honest answer for a date the authority has not
47
+ * published yet. `{ strict: true }` throws `UncertifiedMethodError` there
48
+ * instead. A method not certified at all throws either way: it is not for
49
+ * showing anyone their prayer times (`candidateTimes` runs it regardless).
50
+ */
51
+ export declare function timesFor(place: Place, date: CivilDate, opts?: TimesOptions): DayTimes;
52
+ /** `timesFor` for every date from `from` to `to`, both inclusive — a month's timetable, a year's. */
53
+ export declare function timesForRange(place: Place, from: CivilDate, to: CivilDate, opts?: TimesOptions): DayTimes[];
54
+ /**
55
+ * The method as it stands, certified or not, with or without its correction
56
+ * list (`corrections: false`): for research and for checking the method
57
+ * against the tables. `status` says what it would be served as.
58
+ */
59
+ export declare function candidateTimes(place: Place, date: CivilDate, opts?: TimesOptions & ComputeOptions): DayTimes;
60
+ /** What the method is: its parameters, revision, certification, correction list and sources. */
61
+ export declare function methodInfo(authority: Authority): MethodInfo;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The library's calls. Each works for whichever authorities are loaded —
3
+ * `mihrab-methods` loads all five, `mihrab-methods/<authority>` one.
4
+ */
5
+ import { candidateInstants } from './compute.js';
6
+ import { parseCivilDate, nextDay } from './civilDate.js';
7
+ import { printedAt, tableClockAt } from './clock.js';
8
+ import { authorityData, isLoaded, loadedAuthorities } from './registry.js';
9
+ import { PRAYER_KEYS, UncertifiedMethodError, } from './types.js';
10
+ /**
11
+ * Certified: every check of the authority's tables has every time on the
12
+ * printed minute (and checked something). The flag alone is not trusted.
13
+ */
14
+ export function isCertified(authority) {
15
+ const c = certificationOf(authority);
16
+ return !!c && c.certified && c.checks.length > 0 && c.checks.every(k => k.times > 0 && k.off === 0);
17
+ }
18
+ /** The authority's record from certification.json, or null. */
19
+ export function certificationOf(authority) {
20
+ return authorityData(authority).certification;
21
+ }
22
+ /** The span of dates the method was checked over (and so is certified for), or null. */
23
+ export function certifiedSpan(authority) {
24
+ if (!isCertified(authority))
25
+ return null;
26
+ const checks = certificationOf(authority).checks;
27
+ const tos = checks.map(c => c.to).sort();
28
+ return { from: checks.map(c => c.from).sort()[0], to: tos[tos.length - 1] };
29
+ }
30
+ /** The authority's places, each at the point its method is computed at. */
31
+ export function placesOf(authority) {
32
+ return authorityData(authority).places;
33
+ }
34
+ /** The authorities this build has loaded. */
35
+ export function loadedAuthorityIds() {
36
+ return loadedAuthorities();
37
+ }
38
+ /** A place by its stable id (`<authority>:<key>`), among the loaded authorities. */
39
+ export function placeById(id) {
40
+ const i = id.indexOf(':');
41
+ if (i < 0)
42
+ return null;
43
+ const authority = id.slice(0, i);
44
+ if (!isLoaded(authority))
45
+ return null;
46
+ return byId(authority).get(id) ?? null;
47
+ }
48
+ const idIndexes = new Map();
49
+ function byId(authority) {
50
+ let m = idIndexes.get(authority);
51
+ if (!m)
52
+ idIndexes.set(authority, (m = new Map(placesOf(authority).map(p => [p.id, p]))));
53
+ return m;
54
+ }
55
+ const RAD = Math.PI / 180;
56
+ /** Great-circle distance, km. */
57
+ export function distanceKm(lat1, lon1, lat2, lon2) {
58
+ const dLat = (lat2 - lat1) * RAD;
59
+ const dLon = (lon2 - lon1) * RAD;
60
+ const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * RAD) * Math.cos(lat2 * RAD) * Math.sin(dLon / 2) ** 2;
61
+ return 2 * 6371.0088 * Math.asin(Math.min(1, Math.sqrt(a)));
62
+ }
63
+ /**
64
+ * The authority's place nearest a point — how an app finds the table for a
65
+ * location. `maxKm` (default: no limit) returns null beyond that distance.
66
+ * Türkiye's districts and Diyanet's worldwide places are both 'diyanet'.
67
+ */
68
+ export function nearestPlace(authority, lat, lon, opts = {}) {
69
+ if (!Number.isFinite(lat) || !Number.isFinite(lon))
70
+ return null;
71
+ let best = null;
72
+ for (const place of placesOf(authority)) {
73
+ const d = distanceKm(lat, lon, place.lat, place.lon);
74
+ if (!best || d < best.distanceKm)
75
+ best = { place, distanceKm: d };
76
+ }
77
+ if (best && opts.maxKm !== undefined && best.distanceKm > opts.maxKm)
78
+ return null;
79
+ return best;
80
+ }
81
+ function dayTimes(place, d, instants, status) {
82
+ const data = authorityData(place.authority);
83
+ const span = certifiedSpan(place.authority);
84
+ const clock = tableClockAt(place, d.year, d.month0, d.day);
85
+ let times = null;
86
+ if (instants) {
87
+ times = {};
88
+ for (const k of PRAYER_KEYS)
89
+ times[k] = { instant: instants[k], printed: printedAt(instants[k], clock.offsetMinutes) };
90
+ }
91
+ return {
92
+ authority: place.authority,
93
+ placeId: place.id,
94
+ date: d.iso,
95
+ status,
96
+ certifiedFrom: span?.from ?? null,
97
+ certifiedUntil: span?.to ?? null,
98
+ revision: data.revision,
99
+ clock,
100
+ times,
101
+ };
102
+ }
103
+ function statusFor(authority, iso) {
104
+ const span = certifiedSpan(authority);
105
+ if (!span)
106
+ return 'uncertified';
107
+ return iso < span.from || iso > span.to ? 'method-only' : 'certified';
108
+ }
109
+ /**
110
+ * The place's prayer times on a civil date, as the authority prints them:
111
+ * each prayer's exact moment and its wall-clock minute on the table's clock.
112
+ *
113
+ * Inside the dates the method was certified over, `status` is 'certified':
114
+ * the authority's own printed minute. Outside them it is 'method-only': the
115
+ * method alone, which is on the printed minute all but rarely and never a
116
+ * minute further — the honest answer for a date the authority has not
117
+ * published yet. `{ strict: true }` throws `UncertifiedMethodError` there
118
+ * instead. A method not certified at all throws either way: it is not for
119
+ * showing anyone their prayer times (`candidateTimes` runs it regardless).
120
+ */
121
+ export function timesFor(place, date, opts = {}) {
122
+ if (!isCertified(place.authority))
123
+ throw new UncertifiedMethodError(place.authority);
124
+ const d = parseCivilDate(date);
125
+ const status = statusFor(place.authority, d.iso);
126
+ if (status === 'method-only' && opts.strict)
127
+ throw new UncertifiedMethodError(place.authority, d.iso);
128
+ return dayTimes(place, d, candidateInstants(place, d.year, d.month0, d.day, opts.asrShadow ?? 1), status);
129
+ }
130
+ /** `timesFor` for every date from `from` to `to`, both inclusive — a month's timetable, a year's. */
131
+ export function timesForRange(place, from, to, opts = {}) {
132
+ const a = parseCivilDate(from);
133
+ const b = parseCivilDate(to);
134
+ if (a.iso > b.iso)
135
+ throw new RangeError(`The range runs backwards: ${a.iso} to ${b.iso}.`);
136
+ const out = [];
137
+ for (let d = a; d.iso <= b.iso; d = nextDay(d))
138
+ out.push(timesFor(place, d.iso, opts));
139
+ return out;
140
+ }
141
+ /**
142
+ * The method as it stands, certified or not, with or without its correction
143
+ * list (`corrections: false`): for research and for checking the method
144
+ * against the tables. `status` says what it would be served as.
145
+ */
146
+ export function candidateTimes(place, date, opts = {}) {
147
+ const d = parseCivilDate(date);
148
+ return dayTimes(place, d, candidateInstants(place, d.year, d.month0, d.day, opts.asrShadow ?? 1, { corrections: opts.corrections }), statusFor(place.authority, d.iso));
149
+ }
150
+ /** What the method is: its parameters, revision, certification, correction list and sources. */
151
+ export function methodInfo(authority) {
152
+ const data = authorityData(authority);
153
+ const cov = data.corrections.coverage();
154
+ return {
155
+ authority,
156
+ ...data.description,
157
+ revision: data.revision,
158
+ certified: isCertified(authority),
159
+ certification: data.certification,
160
+ corrections: { from: cov.from, to: cov.to, entries: data.corrections.entries() },
161
+ places: data.places.length,
162
+ };
163
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Civil dates as the library takes them: 'YYYY-MM-DD', or { year, month,
3
+ * day } with January = 1. The methods themselves count months from 0, as
4
+ * JavaScript's Date does; that habit stops at this file.
5
+ */
6
+ import type { CivilDate } from './types.js';
7
+ export type Ymd = {
8
+ year: number;
9
+ month0: number;
10
+ day: number;
11
+ iso: string;
12
+ };
13
+ export declare function parseCivilDate(date: CivilDate): Ymd;
14
+ /** 'YYYY-MM-DD' from a year, a 0-based month and a day. */
15
+ export declare function isoOf(year: number, month0: number, day: number): string;
16
+ /** The day after, as a civil date (UTC arithmetic: no zone, no summer time). */
17
+ export declare function nextDay(d: Ymd): Ymd;
@@ -0,0 +1,36 @@
1
+ const ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
2
+ export function parseCivilDate(date) {
3
+ let year, month, day;
4
+ if (typeof date === 'string') {
5
+ const m = ISO.exec(date);
6
+ if (!m)
7
+ throw new TypeError(`Not a date: ${JSON.stringify(date)} — expected 'YYYY-MM-DD' or { year, month, day }.`);
8
+ [year, month, day] = [Number(m[1]), Number(m[2]), Number(m[3])];
9
+ }
10
+ else if (date && typeof date === 'object' && typeof date.year === 'number' && typeof date.month === 'number' && typeof date.day === 'number') {
11
+ ({ year, month, day } = date);
12
+ }
13
+ else {
14
+ throw new TypeError(typeof date === 'number'
15
+ ? 'timesFor(place, year, month, day) is no longer the call: pass one date, as \'YYYY-MM-DD\' or { year, month, day } with January = 1.'
16
+ : `Not a date: ${JSON.stringify(date)} — expected 'YYYY-MM-DD' or { year, month, day }.`);
17
+ }
18
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day) || month < 1 || month > 12 || day < 1 || day > 31) {
19
+ throw new RangeError(`Not a calendar date: ${isoOf(year, month - 1, day)} (months run 1–12).`);
20
+ }
21
+ // 31 April, 30 February: a Date would roll these over without a word.
22
+ const probe = new Date(Date.UTC(year, month - 1, day));
23
+ if (probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) {
24
+ throw new RangeError(`Not a calendar date: ${isoOf(year, month - 1, day)}.`);
25
+ }
26
+ return { year, month0: month - 1, day, iso: isoOf(year, month - 1, day) };
27
+ }
28
+ /** 'YYYY-MM-DD' from a year, a 0-based month and a day. */
29
+ export function isoOf(year, month0, day) {
30
+ return `${String(year).padStart(4, '0')}-${String(month0 + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
31
+ }
32
+ /** The day after, as a civil date (UTC arithmetic: no zone, no summer time). */
33
+ export function nextDay(d) {
34
+ const n = new Date(Date.UTC(d.year, d.month0, d.day + 1));
35
+ return { year: n.getUTCFullYear(), month0: n.getUTCMonth(), day: n.getUTCDate(), iso: isoOf(n.getUTCFullYear(), n.getUTCMonth(), n.getUTCDate()) };
36
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The clock a table prints a day in: the place's zone as kept at noon, or
3
+ * the table's own clock where it keeps one no zone does (`Place.clock`). A
4
+ * table prints the whole day in the clock kept at noon, so on the morning
5
+ * the clocks go forward a Fajr before the change is printed in the new time.
6
+ */
7
+ import type { Place, TableClock } from './types.js';
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
+ */
16
+ export declare function clockOffsetAt(tz: string, year: number, month: number, day: number): number;
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. */
18
+ export declare function placeClockAt(place: Place, year: number, month: number, day: number): number;
19
+ export declare function offsetLabel(offsetMinutes: number): string;
20
+ export declare function tableClockAt(place: Place, year: number, month: number, day: number): TableClock;
21
+ /** 'HH:MM' of an instant on a clock, as a table prints it. */
22
+ export declare function printedAt(instant: Date, offsetMinutes: number): string;
@@ -0,0 +1,60 @@
1
+ const formats = new Map();
2
+ /**
3
+ * Minutes east of UTC that a zone keeps at noon UTC of a date (month 0-based).
4
+ *
5
+ * Read from the zone's wall clock (year … minute, as every Intl has them),
6
+ * not from `timeZoneName: 'longOffset'`, which not every runtime supports:
7
+ * where it is missing the name comes back as 'GMT+1' or a zone's own
8
+ * abbreviation, and an offset parsed from that is silently wrong.
9
+ */
10
+ export function clockOffsetAt(tz, year, month, day) {
11
+ const at = Date.UTC(year, month, day, 12);
12
+ let f = formats.get(tz);
13
+ if (!f) {
14
+ f = new Intl.DateTimeFormat('en-US', {
15
+ timeZone: tz,
16
+ year: 'numeric',
17
+ month: 'numeric',
18
+ day: 'numeric',
19
+ hour: 'numeric',
20
+ minute: 'numeric',
21
+ hourCycle: 'h23',
22
+ });
23
+ formats.set(tz, f);
24
+ }
25
+ const parts = f.formatToParts(new Date(at));
26
+ const n = (type) => Number(parts.find(p => p.type === type)?.value);
27
+ const wall = Date.UTC(n('year'), n('month') - 1, n('day'), n('hour') % 24, n('minute'));
28
+ if (!Number.isFinite(wall))
29
+ throw new RangeError(`The zone ${tz} could not be read on this runtime.`);
30
+ return Math.round((wall - at) / 60_000);
31
+ }
32
+ /** 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. */
33
+ export function placeClockAt(place, year, month, day) {
34
+ if (place.clock) {
35
+ const date = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
36
+ let off = place.clock[0][1];
37
+ for (const [from, o] of place.clock)
38
+ if (from <= date)
39
+ off = o;
40
+ return off;
41
+ }
42
+ return clockOffsetAt(place.tz, year, month, day);
43
+ }
44
+ export function offsetLabel(offsetMinutes) {
45
+ const sign = offsetMinutes < 0 ? '-' : '+';
46
+ const abs = Math.abs(offsetMinutes);
47
+ return `UTC${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
48
+ }
49
+ export function tableClockAt(place, year, month, day) {
50
+ const offsetMinutes = placeClockAt(place, year, month, day);
51
+ return place.clock
52
+ ? { zone: null, offsetMinutes, label: offsetLabel(offsetMinutes) }
53
+ : { zone: place.tz, offsetMinutes, label: place.tz };
54
+ }
55
+ /** 'HH:MM' of an instant on a clock, as a table prints it. */
56
+ export function printedAt(instant, offsetMinutes) {
57
+ const local = Math.floor(instant.getTime() / 60_000) + offsetMinutes;
58
+ const m = ((local % 1440) + 1440) % 1440;
59
+ return `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
60
+ }
@@ -0,0 +1,10 @@
1
+ import type { Instants, Place } from './types.js';
2
+ export type ComputeOptions = {
3
+ corrections?: boolean;
4
+ };
5
+ /**
6
+ * The method as it stands, certified or not, for a civil date with the
7
+ * month 0-based. Null where the method gives no times for the day. Not for
8
+ * showing anyone their prayer times: `timesFor` is.
9
+ */
10
+ export declare function candidateInstants(place: Place, year: number, month: number, day: number, asrShadow?: 1 | 2, opts?: ComputeOptions): Instants | null;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The methods, run for a place and a civil date (month 0-based, as the
3
+ * methods count): the authority's own formula, then its correction list.
4
+ * This is the computation `timesFor` and the checks share; the API around
5
+ * it is in `api.ts`.
6
+ */
7
+ import { congressTimes } from '../methods/sweden.js';
8
+ import { moroccoTimes } from '../methods/morocco.js';
9
+ import { algeriaTimes } from '../methods/algeria.js';
10
+ import { kemenagTimes } from '../methods/indonesia.js';
11
+ import { authorityData } from './registry.js';
12
+ import { placeClockAt } from './clock.js';
13
+ /**
14
+ * The method as it stands, certified or not, for a civil date with the
15
+ * month 0-based. Null where the method gives no times for the day. Not for
16
+ * showing anyone their prayer times: `timesFor` is.
17
+ */
18
+ export function candidateInstants(place, year, month, day, asrShadow = 1, opts = {}) {
19
+ const fix = opts.corrections === false ? null : authorityData(place.authority).corrections;
20
+ switch (place.authority) {
21
+ case 'sweden':
22
+ case 'diyanet': {
23
+ // The Congress method's own rounding is inside it; its instants land on the minute (rounded, as the app does).
24
+ const clock = placeClockAt(place, year, month, day);
25
+ const t = place.prayerPoints ? perPrayer(place, place.prayerPoints, year, month, day, asrShadow) : congressTimes(place.lat, place.lon, year, month, day, asrShadow, place.ishaDeg, clock);
26
+ // then the correction list: the few times Diyanet's program rounds the other way (the standard Asr only)
27
+ const c = fix?.correction(place.lat, place.lon, year, month, day);
28
+ const min = (d, k) => new Date((Math.round(d.getTime() / 60_000) + (k === 'asr' && asrShadow !== 1 ? 0 : (c?.[k] ?? 0))) * 60_000);
29
+ return { fajr: min(t.fajr, 'fajr'), sunrise: min(t.sunrise, 'sunrise'), dhuhr: min(t.dhuhr, 'dhuhr'), asr: min(t.asr, 'asr'), maghrib: min(t.maghrib, 'maghrib'), isha: min(t.isha, 'isha') };
30
+ }
31
+ case 'morocco':
32
+ case 'algeria':
33
+ case 'indonesia': {
34
+ const t = place.authority === 'morocco'
35
+ ? moroccoTimes(place.lat, place.lon, year, month, day, place.dipDeg ?? 0, asrShadow, place.dipMaghribDeg)
36
+ : place.authority === 'algeria'
37
+ ? algeriaTimes(place.lat, place.lon, year, month, day, asrShadow)
38
+ : kemenagTimes(place.lat, place.lon, year, month, day, { highland: place.highland, twilightDip: place.twilightDip, asrShadow, afternoon: place.afternoon });
39
+ // the correction list: the authority's few times on the other side of a minute (the standard Asr only)
40
+ const c = !t || !fix ? undefined : fix.correction(place.lat, place.lon, year, month, day);
41
+ if (!t || !c)
42
+ return t;
43
+ const mv = (d, k) => new Date(d.getTime() + (k === 'asr' && asrShadow !== 1 ? 0 : (c[k] ?? 0)) * 60_000);
44
+ return { fajr: mv(t.fajr, 'fajr'), sunrise: mv(t.sunrise, 'sunrise'), dhuhr: mv(t.dhuhr, 'dhuhr'), asr: mv(t.asr, 'asr'), maghrib: mv(t.maghrib, 'maghrib'), isha: mv(t.isha, 'isha') };
45
+ }
46
+ }
47
+ }
48
+ /** Türkiye's districts: each prayer at its own point for the year (the nearest year's beyond the tables). */
49
+ function perPrayer(place, pts, year, month, day, asrShadow) {
50
+ const years = Object.keys(pts).map(Number).sort((a, b) => a - b);
51
+ const y = years.reduce((b, v) => (Math.abs(v - year) < Math.abs(b - year) ? v : b), years[0]);
52
+ const p = pts[String(y)];
53
+ const clock = placeClockAt(place, year, month, day);
54
+ const at = (i) => congressTimes(p[i][0], p[i][1], year, month, day, asrShadow, place.ishaDeg, clock);
55
+ return { fajr: at(0).fajr, sunrise: at(1).sunrise, dhuhr: at(2).dhuhr, asr: at(3).asr, maghrib: at(4).maghrib, isha: at(5).isha };
56
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A correction list, indexed for lookup: point → date → the minutes to add
3
+ * to each printed time. Built from the embedded data (or, for the tools,
4
+ * from a file read into the same shape).
5
+ */
6
+ import type { CorrectionsFile, PrayerKey } from './types.js';
7
+ export declare const pointKey: (lat: number, lon: number) => string;
8
+ export declare class CorrectionList {
9
+ private source;
10
+ private index;
11
+ constructor(source: CorrectionsFile);
12
+ private load;
13
+ /** The minutes to add to the point's printed times on a date (month 0-based), if any. */
14
+ correction(lat: number, lon: number, year: number, month: number, day: number): Partial<Record<PrayerKey, number>> | undefined;
15
+ coverage(): {
16
+ from: string;
17
+ to: string;
18
+ };
19
+ entries(): number;
20
+ file(): CorrectionsFile;
21
+ /** Put another list in force (a newer one for the same method revision). */
22
+ replace(source: CorrectionsFile): void;
23
+ }
24
+ /** A list that does not have the shape of one is refused whole. */
25
+ export declare function isCorrectionsFile(x: unknown): x is CorrectionsFile;
@@ -0,0 +1,56 @@
1
+ export const pointKey = (lat, lon) => `${lat.toFixed(3)},${lon.toFixed(3)}`;
2
+ export class CorrectionList {
3
+ source;
4
+ index = null;
5
+ constructor(source) {
6
+ this.source = source;
7
+ }
8
+ load() {
9
+ if (this.index)
10
+ return this.index;
11
+ const index = new Map();
12
+ for (const [point, list] of Object.entries(this.source.points)) {
13
+ const byDate = new Map();
14
+ for (const e of list) {
15
+ const [date, key, step] = e.split(' ');
16
+ const c = byDate.get(date) ?? {};
17
+ c[key] = Number(step);
18
+ byDate.set(date, c);
19
+ }
20
+ index.set(point, byDate);
21
+ }
22
+ return (this.index = index);
23
+ }
24
+ /** The minutes to add to the point's printed times on a date (month 0-based), if any. */
25
+ correction(lat, lon, year, month, day) {
26
+ const date = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
27
+ return this.load().get(pointKey(lat, lon))?.get(date);
28
+ }
29
+ coverage() {
30
+ return { from: this.source.from, to: this.source.to };
31
+ }
32
+ entries() {
33
+ return Object.values(this.source.points).reduce((n, e) => n + e.length, 0);
34
+ }
35
+ file() {
36
+ return this.source;
37
+ }
38
+ /** Put another list in force (a newer one for the same method revision). */
39
+ replace(source) {
40
+ this.source = source;
41
+ this.index = null;
42
+ }
43
+ }
44
+ /** A list that does not have the shape of one is refused whole. */
45
+ export function isCorrectionsFile(x) {
46
+ if (!x || typeof x !== 'object')
47
+ return false;
48
+ const c = x;
49
+ const date = /^\d{4}-\d{2}-\d{2}$/;
50
+ if (typeof c.from !== 'string' || typeof c.to !== 'string' || !date.test(c.from) || !date.test(c.to) || c.from > c.to)
51
+ return false;
52
+ if (!c.points || typeof c.points !== 'object')
53
+ return false;
54
+ const entry = /^\d{4}-\d{2}-\d{2} (fajr|sunrise|dhuhr|asr|maghrib|isha) [+-]1$/;
55
+ return Object.entries(c.points).every(([k, v]) => /^-?\d+\.\d{3},-?\d+\.\d{3}$/.test(k) && Array.isArray(v) && v.every(e => typeof e === 'string' && entry.test(e)));
56
+ }
@@ -0,0 +1,61 @@
1
+ import type { Authority, CorrectionsFile } from './types.js';
2
+ /** Where the feed lives. A tagged release (`.../v1.2.0/...`) can replace `main` for a pinned address. */
3
+ export declare const DEFAULT_FEED_URL = "https://raw.githubusercontent.com/MihrabHQ/mihrab-methods/main";
4
+ export type FetchOptions = {
5
+ authority: Authority;
6
+ /** The revision this build computes; default: the loaded method's. */
7
+ revision?: number;
8
+ /** The feed's base URL (default `DEFAULT_FEED_URL`): `<base>/certification.json`, `<base>/data/corrections/<authority>.json`. */
9
+ baseUrl?: string;
10
+ /** The fetch to use (default: the global one). */
11
+ fetch?: typeof fetch;
12
+ /** SHA-256 of a UTF-8 string as lowercase hex; default: WebCrypto where there is one, else a built-in digest. */
13
+ sha256?: (text: string) => Promise<string>;
14
+ /** Put a newer list in force in this process (default true). */
15
+ apply?: boolean;
16
+ };
17
+ export type ListSummary = {
18
+ from: string;
19
+ to: string;
20
+ entries: number;
21
+ };
22
+ export type FetchResult =
23
+ /** The certification names the list this build already has (or an older one). */
24
+ {
25
+ status: 'current';
26
+ have: ListSummary;
27
+ certified: boolean;
28
+ }
29
+ /** A newer list for this revision: its contents, and whether the hash matched (or could not be checked). */
30
+ | {
31
+ status: 'updated';
32
+ list: CorrectionsFile;
33
+ summary: ListSummary;
34
+ integrity: 'verified' | 'unchecked';
35
+ applied: boolean;
36
+ }
37
+ /** The method itself has moved on: its lists no longer fit this build, which should be updated. */
38
+ | {
39
+ status: 'method-moved';
40
+ remoteRevision: number;
41
+ have: ListSummary;
42
+ }
43
+ /** The authority has published tables the method is not yet certified on: keep the list in force. */
44
+ | {
45
+ status: 'uncertified';
46
+ have: ListSummary;
47
+ }
48
+ /** Something did not add up (a list not matching what the certification names, a bad hash): nothing taken. */
49
+ | {
50
+ status: 'rejected';
51
+ reason: string;
52
+ have: ListSummary;
53
+ };
54
+ export declare function summarize(list: CorrectionsFile): ListSummary;
55
+ /** UTF-8 bytes of a string, without TextEncoder (not every runtime has it). */
56
+ export declare function utf8(text: string): Uint8Array;
57
+ /** SHA-256 (FIPS 180-4) of bytes. */
58
+ export declare function sha256(data: Uint8Array): Uint8Array;
59
+ /** Put a list in force for the process — one verified by `fetchLatestCorrections`, or restored from the app's own storage. */
60
+ export declare function applyCorrections(authority: Authority, list: CorrectionsFile, revision: number): boolean;
61
+ export declare function fetchLatestCorrections(opts: FetchOptions): Promise<FetchResult>;