mihrab-methods 0.0.0-stage → 1.0.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.
- package/LICENSE +373 -0
- package/README.md +319 -2
- package/certification.json +120 -0
- package/data/corrections/algeria.json +784 -0
- package/data/corrections/diyanet.json +18444 -0
- package/data/corrections/indonesia.json +210 -0
- package/data/corrections/morocco.json +993 -0
- package/data/corrections/sweden.json +516 -0
- package/dist/cjs/algeria.d.ts +4 -0
- package/dist/cjs/algeria.js +22 -0
- package/dist/cjs/authorities/algeria.d.ts +3 -0
- package/dist/cjs/authorities/algeria.js +57 -0
- package/dist/cjs/authorities/diyanet.d.ts +9 -0
- package/dist/cjs/authorities/diyanet.js +90 -0
- package/dist/cjs/authorities/indonesia.d.ts +4 -0
- package/dist/cjs/authorities/indonesia.js +82 -0
- package/dist/cjs/authorities/morocco.d.ts +5 -0
- package/dist/cjs/authorities/morocco.js +65 -0
- package/dist/cjs/authorities/slug.d.ts +2 -0
- package/dist/cjs/authorities/slug.js +17 -0
- package/dist/cjs/authorities/sweden.d.ts +3 -0
- package/dist/cjs/authorities/sweden.js +58 -0
- package/dist/cjs/core/api.d.ts +61 -0
- package/dist/cjs/core/api.js +176 -0
- package/dist/cjs/core/civilDate.d.ts +17 -0
- package/dist/cjs/core/civilDate.js +41 -0
- package/dist/cjs/core/clock.d.ts +15 -0
- package/dist/cjs/core/clock.js +46 -0
- package/dist/cjs/core/compute.d.ts +10 -0
- package/dist/cjs/core/compute.js +59 -0
- package/dist/cjs/core/correctionList.d.ts +25 -0
- package/dist/cjs/core/correctionList.js +62 -0
- package/dist/cjs/core/fetchLatest.d.ts +57 -0
- package/dist/cjs/core/fetchLatest.js +137 -0
- package/dist/cjs/core/index.d.ts +14 -0
- package/dist/cjs/core/index.js +45 -0
- package/dist/cjs/core/registry.d.ts +28 -0
- package/dist/cjs/core/registry.js +29 -0
- package/dist/cjs/core/types.d.ts +193 -0
- package/dist/cjs/core/types.js +22 -0
- package/dist/cjs/core.d.ts +2 -0
- package/dist/cjs/core.js +18 -0
- package/dist/cjs/data/generated/algeriaCertification.d.ts +3 -0
- package/dist/cjs/data/generated/algeriaCertification.js +4 -0
- package/dist/cjs/data/generated/algeriaCities.d.ts +8 -0
- package/dist/cjs/data/generated/algeriaCities.js +5 -0
- package/dist/cjs/data/generated/algeriaCorrections.d.ts +3 -0
- package/dist/cjs/data/generated/algeriaCorrections.js +4 -0
- package/dist/cjs/data/generated/algeriaMethodPoints.d.ts +2 -0
- package/dist/cjs/data/generated/algeriaMethodPoints.js +5 -0
- package/dist/cjs/data/generated/countryOutlines.d.ts +5 -0
- package/dist/cjs/data/generated/countryOutlines.js +5 -0
- package/dist/cjs/data/generated/diyanetCertification.d.ts +3 -0
- package/dist/cjs/data/generated/diyanetCertification.js +4 -0
- package/dist/cjs/data/generated/diyanetCorrections.d.ts +3 -0
- package/dist/cjs/data/generated/diyanetCorrections.js +4 -0
- package/dist/cjs/data/generated/diyanetCountries.d.ts +5 -0
- package/dist/cjs/data/generated/diyanetCountries.js +5 -0
- package/dist/cjs/data/generated/diyanetTurkeyPoints.d.ts +7 -0
- package/dist/cjs/data/generated/diyanetTurkeyPoints.js +5 -0
- package/dist/cjs/data/generated/diyanetWorldPoints.d.ts +10 -0
- package/dist/cjs/data/generated/diyanetWorldPoints.js +5 -0
- package/dist/cjs/data/generated/indonesiaCertification.d.ts +3 -0
- package/dist/cjs/data/generated/indonesiaCertification.js +4 -0
- package/dist/cjs/data/generated/indonesiaCorrections.d.ts +3 -0
- package/dist/cjs/data/generated/indonesiaCorrections.js +4 -0
- package/dist/cjs/data/generated/kemenagMethodPoints.d.ts +2 -0
- package/dist/cjs/data/generated/kemenagMethodPoints.js +5 -0
- package/dist/cjs/data/generated/kemenagPlaces.d.ts +8 -0
- package/dist/cjs/data/generated/kemenagPlaces.js +5 -0
- package/dist/cjs/data/generated/moroccoCertification.d.ts +3 -0
- package/dist/cjs/data/generated/moroccoCertification.js +4 -0
- package/dist/cjs/data/generated/moroccoCities.d.ts +8 -0
- package/dist/cjs/data/generated/moroccoCities.js +5 -0
- package/dist/cjs/data/generated/moroccoCorrections.d.ts +3 -0
- package/dist/cjs/data/generated/moroccoCorrections.js +4 -0
- package/dist/cjs/data/generated/moroccoMethodPoints.d.ts +2 -0
- package/dist/cjs/data/generated/moroccoMethodPoints.js +5 -0
- package/dist/cjs/data/generated/swedenCertification.d.ts +3 -0
- package/dist/cjs/data/generated/swedenCertification.js +4 -0
- package/dist/cjs/data/generated/swedenCorrections.d.ts +3 -0
- package/dist/cjs/data/generated/swedenCorrections.js +4 -0
- package/dist/cjs/data/generated/swedenPlaces.d.ts +5 -0
- package/dist/cjs/data/generated/swedenPlaces.js +5 -0
- package/dist/cjs/diyanet.d.ts +4 -0
- package/dist/cjs/diyanet.js +22 -0
- package/dist/cjs/index.d.ts +19 -0
- package/dist/cjs/index.js +37 -0
- package/dist/cjs/indonesia.d.ts +4 -0
- package/dist/cjs/indonesia.js +22 -0
- package/dist/cjs/locate.d.ts +21 -0
- package/dist/cjs/locate.js +204 -0
- package/dist/cjs/methods/algeria.d.ts +56 -0
- package/dist/cjs/methods/algeria.js +119 -0
- package/dist/cjs/methods/indonesia.d.ts +31 -0
- package/dist/cjs/methods/indonesia.js +94 -0
- package/dist/cjs/methods/morocco.d.ts +61 -0
- package/dist/cjs/methods/morocco.js +113 -0
- package/dist/cjs/methods/revisions.d.ts +15 -0
- package/dist/cjs/methods/revisions.js +18 -0
- package/dist/cjs/methods/scratch-congress.d.ts +87 -0
- package/dist/cjs/methods/scratch-congress.js +539 -0
- package/dist/cjs/methods/sweden.d.ts +88 -0
- package/dist/cjs/methods/sweden.js +568 -0
- package/dist/cjs/morocco.d.ts +4 -0
- package/dist/cjs/morocco.js +22 -0
- package/dist/cjs/sun/preciseSun.d.ts +19 -0
- package/dist/cjs/sun/preciseSun.js +185 -0
- package/dist/cjs/sweden.d.ts +4 -0
- package/dist/cjs/sweden.js +22 -0
- package/dist/esm/algeria.d.ts +4 -0
- package/dist/esm/algeria.js +4 -0
- package/dist/esm/authorities/algeria.d.ts +3 -0
- package/dist/esm/authorities/algeria.js +51 -0
- package/dist/esm/authorities/diyanet.d.ts +9 -0
- package/dist/esm/authorities/diyanet.js +83 -0
- package/dist/esm/authorities/indonesia.d.ts +4 -0
- package/dist/esm/authorities/indonesia.js +75 -0
- package/dist/esm/authorities/morocco.d.ts +5 -0
- package/dist/esm/authorities/morocco.js +59 -0
- package/dist/esm/authorities/slug.d.ts +2 -0
- package/dist/esm/authorities/slug.js +14 -0
- package/dist/esm/authorities/sweden.d.ts +3 -0
- package/dist/esm/authorities/sweden.js +52 -0
- package/dist/esm/core/api.d.ts +61 -0
- package/dist/esm/core/api.js +162 -0
- package/dist/esm/core/civilDate.d.ts +17 -0
- package/dist/esm/core/civilDate.js +36 -0
- package/dist/esm/core/clock.d.ts +15 -0
- package/dist/esm/core/clock.js +39 -0
- package/dist/esm/core/compute.d.ts +10 -0
- package/dist/esm/core/compute.js +56 -0
- package/dist/esm/core/correctionList.d.ts +25 -0
- package/dist/esm/core/correctionList.js +56 -0
- package/dist/esm/core/fetchLatest.d.ts +57 -0
- package/dist/esm/core/fetchLatest.js +98 -0
- package/dist/esm/core/index.d.ts +14 -0
- package/dist/esm/core/index.js +14 -0
- package/dist/esm/core/registry.d.ts +28 -0
- package/dist/esm/core/registry.js +23 -0
- package/dist/esm/core/types.d.ts +193 -0
- package/dist/esm/core/types.js +18 -0
- package/dist/esm/core.d.ts +2 -0
- package/dist/esm/core.js +2 -0
- package/dist/esm/data/generated/algeriaCertification.d.ts +3 -0
- package/dist/esm/data/generated/algeriaCertification.js +2 -0
- package/dist/esm/data/generated/algeriaCities.d.ts +8 -0
- package/dist/esm/data/generated/algeriaCities.js +3 -0
- package/dist/esm/data/generated/algeriaCorrections.d.ts +3 -0
- package/dist/esm/data/generated/algeriaCorrections.js +2 -0
- package/dist/esm/data/generated/algeriaMethodPoints.d.ts +2 -0
- package/dist/esm/data/generated/algeriaMethodPoints.js +3 -0
- package/dist/esm/data/generated/countryOutlines.d.ts +5 -0
- package/dist/esm/data/generated/countryOutlines.js +3 -0
- package/dist/esm/data/generated/diyanetCertification.d.ts +3 -0
- package/dist/esm/data/generated/diyanetCertification.js +2 -0
- package/dist/esm/data/generated/diyanetCorrections.d.ts +3 -0
- package/dist/esm/data/generated/diyanetCorrections.js +2 -0
- package/dist/esm/data/generated/diyanetCountries.d.ts +5 -0
- package/dist/esm/data/generated/diyanetCountries.js +3 -0
- package/dist/esm/data/generated/diyanetTurkeyPoints.d.ts +7 -0
- package/dist/esm/data/generated/diyanetTurkeyPoints.js +3 -0
- package/dist/esm/data/generated/diyanetWorldPoints.d.ts +10 -0
- package/dist/esm/data/generated/diyanetWorldPoints.js +3 -0
- package/dist/esm/data/generated/indonesiaCertification.d.ts +3 -0
- package/dist/esm/data/generated/indonesiaCertification.js +2 -0
- package/dist/esm/data/generated/indonesiaCorrections.d.ts +3 -0
- package/dist/esm/data/generated/indonesiaCorrections.js +2 -0
- package/dist/esm/data/generated/kemenagMethodPoints.d.ts +2 -0
- package/dist/esm/data/generated/kemenagMethodPoints.js +3 -0
- package/dist/esm/data/generated/kemenagPlaces.d.ts +8 -0
- package/dist/esm/data/generated/kemenagPlaces.js +3 -0
- package/dist/esm/data/generated/moroccoCertification.d.ts +3 -0
- package/dist/esm/data/generated/moroccoCertification.js +2 -0
- package/dist/esm/data/generated/moroccoCities.d.ts +8 -0
- package/dist/esm/data/generated/moroccoCities.js +3 -0
- package/dist/esm/data/generated/moroccoCorrections.d.ts +3 -0
- package/dist/esm/data/generated/moroccoCorrections.js +2 -0
- package/dist/esm/data/generated/moroccoMethodPoints.d.ts +2 -0
- package/dist/esm/data/generated/moroccoMethodPoints.js +3 -0
- package/dist/esm/data/generated/swedenCertification.d.ts +3 -0
- package/dist/esm/data/generated/swedenCertification.js +2 -0
- package/dist/esm/data/generated/swedenCorrections.d.ts +3 -0
- package/dist/esm/data/generated/swedenCorrections.js +2 -0
- package/dist/esm/data/generated/swedenPlaces.d.ts +5 -0
- package/dist/esm/data/generated/swedenPlaces.js +3 -0
- package/dist/esm/diyanet.d.ts +4 -0
- package/dist/esm/diyanet.js +4 -0
- package/dist/esm/index.d.ts +19 -0
- package/dist/esm/index.js +19 -0
- package/dist/esm/indonesia.d.ts +4 -0
- package/dist/esm/indonesia.js +4 -0
- package/dist/esm/locate.d.ts +21 -0
- package/dist/esm/locate.js +196 -0
- package/dist/esm/methods/algeria.d.ts +56 -0
- package/dist/esm/methods/algeria.js +114 -0
- package/dist/esm/methods/indonesia.d.ts +31 -0
- package/dist/esm/methods/indonesia.js +90 -0
- package/dist/esm/methods/morocco.d.ts +61 -0
- package/dist/esm/methods/morocco.js +107 -0
- package/dist/esm/methods/revisions.d.ts +15 -0
- package/dist/esm/methods/revisions.js +15 -0
- package/dist/esm/methods/scratch-congress.d.ts +87 -0
- package/dist/esm/methods/scratch-congress.js +534 -0
- package/dist/esm/methods/sweden.d.ts +88 -0
- package/dist/esm/methods/sweden.js +563 -0
- package/dist/esm/morocco.d.ts +4 -0
- package/dist/esm/morocco.js +4 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/sun/preciseSun.d.ts +19 -0
- package/dist/esm/sun/preciseSun.js +181 -0
- package/dist/esm/sweden.d.ts +4 -0
- package/dist/esm/sweden.js +4 -0
- package/docs/methods.md +190 -0
- package/package.json +178 -4
- package/schemas/certification.schema.json +59 -0
- 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): 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,162 @@
|
|
|
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
|
+
return { from: checks.map(c => c.from).sort()[0], to: checks.map(c => c.to).sort().at(-1) };
|
|
28
|
+
}
|
|
29
|
+
/** The authority's places, each at the point its method is computed at. */
|
|
30
|
+
export function placesOf(authority) {
|
|
31
|
+
return authorityData(authority).places;
|
|
32
|
+
}
|
|
33
|
+
/** The authorities this build has loaded. */
|
|
34
|
+
export function loadedAuthorityIds() {
|
|
35
|
+
return loadedAuthorities();
|
|
36
|
+
}
|
|
37
|
+
/** A place by its stable id (`<authority>:<key>`), among the loaded authorities. */
|
|
38
|
+
export function placeById(id) {
|
|
39
|
+
const i = id.indexOf(':');
|
|
40
|
+
if (i < 0)
|
|
41
|
+
return null;
|
|
42
|
+
const authority = id.slice(0, i);
|
|
43
|
+
if (!isLoaded(authority))
|
|
44
|
+
return null;
|
|
45
|
+
return byId(authority).get(id) ?? null;
|
|
46
|
+
}
|
|
47
|
+
const idIndexes = new Map();
|
|
48
|
+
function byId(authority) {
|
|
49
|
+
let m = idIndexes.get(authority);
|
|
50
|
+
if (!m)
|
|
51
|
+
idIndexes.set(authority, (m = new Map(placesOf(authority).map(p => [p.id, p]))));
|
|
52
|
+
return m;
|
|
53
|
+
}
|
|
54
|
+
const RAD = Math.PI / 180;
|
|
55
|
+
/** Great-circle distance, km. */
|
|
56
|
+
export function distanceKm(lat1, lon1, lat2, lon2) {
|
|
57
|
+
const dLat = (lat2 - lat1) * RAD;
|
|
58
|
+
const dLon = (lon2 - lon1) * RAD;
|
|
59
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * RAD) * Math.cos(lat2 * RAD) * Math.sin(dLon / 2) ** 2;
|
|
60
|
+
return 2 * 6371.0088 * Math.asin(Math.min(1, Math.sqrt(a)));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The authority's place nearest a point — how an app finds the table for a
|
|
64
|
+
* location. `maxKm` (default: no limit) returns null beyond that distance.
|
|
65
|
+
* Türkiye's districts and Diyanet's worldwide places are both 'diyanet'.
|
|
66
|
+
*/
|
|
67
|
+
export function nearestPlace(authority, lat, lon, opts = {}) {
|
|
68
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lon))
|
|
69
|
+
return null;
|
|
70
|
+
let best = null;
|
|
71
|
+
for (const place of placesOf(authority)) {
|
|
72
|
+
const d = distanceKm(lat, lon, place.lat, place.lon);
|
|
73
|
+
if (!best || d < best.distanceKm)
|
|
74
|
+
best = { place, distanceKm: d };
|
|
75
|
+
}
|
|
76
|
+
if (best && opts.maxKm !== undefined && best.distanceKm > opts.maxKm)
|
|
77
|
+
return null;
|
|
78
|
+
return best;
|
|
79
|
+
}
|
|
80
|
+
function dayTimes(place, d, instants, status) {
|
|
81
|
+
const data = authorityData(place.authority);
|
|
82
|
+
const span = certifiedSpan(place.authority);
|
|
83
|
+
const clock = tableClockAt(place, d.year, d.month0, d.day);
|
|
84
|
+
let times = null;
|
|
85
|
+
if (instants) {
|
|
86
|
+
times = {};
|
|
87
|
+
for (const k of PRAYER_KEYS)
|
|
88
|
+
times[k] = { instant: instants[k], printed: printedAt(instants[k], clock.offsetMinutes) };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
authority: place.authority,
|
|
92
|
+
placeId: place.id,
|
|
93
|
+
date: d.iso,
|
|
94
|
+
status,
|
|
95
|
+
certifiedFrom: span?.from ?? null,
|
|
96
|
+
certifiedUntil: span?.to ?? null,
|
|
97
|
+
revision: data.revision,
|
|
98
|
+
clock,
|
|
99
|
+
times,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function statusFor(authority, iso) {
|
|
103
|
+
const span = certifiedSpan(authority);
|
|
104
|
+
if (!span)
|
|
105
|
+
return 'uncertified';
|
|
106
|
+
return iso < span.from || iso > span.to ? 'method-only' : 'certified';
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The place's prayer times on a civil date, as the authority prints them:
|
|
110
|
+
* each prayer's exact moment and its wall-clock minute on the table's clock.
|
|
111
|
+
*
|
|
112
|
+
* Inside the dates the method was certified over, `status` is 'certified':
|
|
113
|
+
* the authority's own printed minute. Outside them it is 'method-only': the
|
|
114
|
+
* method alone, which is on the printed minute all but rarely and never a
|
|
115
|
+
* minute further — the honest answer for a date the authority has not
|
|
116
|
+
* published yet. `{ strict: true }` throws `UncertifiedMethodError` there
|
|
117
|
+
* instead. A method not certified at all throws either way: it is not for
|
|
118
|
+
* showing anyone their prayer times (`candidateTimes` runs it regardless).
|
|
119
|
+
*/
|
|
120
|
+
export function timesFor(place, date, opts = {}) {
|
|
121
|
+
if (!isCertified(place.authority))
|
|
122
|
+
throw new UncertifiedMethodError(place.authority);
|
|
123
|
+
const d = parseCivilDate(date);
|
|
124
|
+
const status = statusFor(place.authority, d.iso);
|
|
125
|
+
if (status === 'method-only' && opts.strict)
|
|
126
|
+
throw new UncertifiedMethodError(place.authority, d.iso);
|
|
127
|
+
return dayTimes(place, d, candidateInstants(place, d.year, d.month0, d.day, opts.asrShadow ?? 1), status);
|
|
128
|
+
}
|
|
129
|
+
/** `timesFor` for every date from `from` to `to`, both inclusive — a month's timetable, a year's. */
|
|
130
|
+
export function timesForRange(place, from, to, opts = {}) {
|
|
131
|
+
const a = parseCivilDate(from);
|
|
132
|
+
const b = parseCivilDate(to);
|
|
133
|
+
if (a.iso > b.iso)
|
|
134
|
+
throw new RangeError(`The range runs backwards: ${a.iso} to ${b.iso}.`);
|
|
135
|
+
const out = [];
|
|
136
|
+
for (let d = a; d.iso <= b.iso; d = nextDay(d))
|
|
137
|
+
out.push(timesFor(place, d.iso, opts));
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The method as it stands, certified or not, with or without its correction
|
|
142
|
+
* list (`corrections: false`): for research and for checking the method
|
|
143
|
+
* against the tables. `status` says what it would be served as.
|
|
144
|
+
*/
|
|
145
|
+
export function candidateTimes(place, date, opts = {}) {
|
|
146
|
+
const d = parseCivilDate(date);
|
|
147
|
+
return dayTimes(place, d, candidateInstants(place, d.year, d.month0, d.day, opts.asrShadow ?? 1, { corrections: opts.corrections }), statusFor(place.authority, d.iso));
|
|
148
|
+
}
|
|
149
|
+
/** What the method is: its parameters, revision, certification, correction list and sources. */
|
|
150
|
+
export function methodInfo(authority) {
|
|
151
|
+
const data = authorityData(authority);
|
|
152
|
+
const cov = data.corrections.coverage();
|
|
153
|
+
return {
|
|
154
|
+
authority,
|
|
155
|
+
...data.description,
|
|
156
|
+
revision: data.revision,
|
|
157
|
+
certified: isCertified(authority),
|
|
158
|
+
certification: data.certification,
|
|
159
|
+
corrections: { from: cov.from, to: cov.to, entries: data.corrections.entries() },
|
|
160
|
+
places: data.places.length,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -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,15 @@
|
|
|
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
|
+
/** Minutes east of UTC that a zone keeps at noon UTC of a date (month 0-based). */
|
|
9
|
+
export declare function clockOffsetAt(tz: string, year: number, month: number, day: number): number;
|
|
10
|
+
/** 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
|
+
export declare function placeClockAt(place: Place, year: number, month: number, day: number): number;
|
|
12
|
+
export declare function offsetLabel(offsetMinutes: number): string;
|
|
13
|
+
export declare function tableClockAt(place: Place, year: number, month: number, day: number): TableClock;
|
|
14
|
+
/** 'HH:MM' of an instant on a clock, as a table prints it. */
|
|
15
|
+
export declare function printedAt(instant: Date, offsetMinutes: number): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const offsetFormats = new Map();
|
|
2
|
+
/** Minutes east of UTC that a zone keeps at noon UTC of a date (month 0-based). */
|
|
3
|
+
export function clockOffsetAt(tz, year, month, day) {
|
|
4
|
+
let f = offsetFormats.get(tz);
|
|
5
|
+
if (!f)
|
|
6
|
+
offsetFormats.set(tz, (f = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'longOffset' })));
|
|
7
|
+
const z = f.formatToParts(new Date(Date.UTC(year, month, day, 12))).find(x => x.type === 'timeZoneName').value;
|
|
8
|
+
const m = /GMT([+-])(\d\d):?(\d\d)?/.exec(z);
|
|
9
|
+
return m ? (m[1] === '-' ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3] ?? 0)) : 0;
|
|
10
|
+
}
|
|
11
|
+
/** 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. */
|
|
12
|
+
export function placeClockAt(place, year, month, day) {
|
|
13
|
+
if (place.clock) {
|
|
14
|
+
const date = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
15
|
+
let off = place.clock[0][1];
|
|
16
|
+
for (const [from, o] of place.clock)
|
|
17
|
+
if (from <= date)
|
|
18
|
+
off = o;
|
|
19
|
+
return off;
|
|
20
|
+
}
|
|
21
|
+
return clockOffsetAt(place.tz, year, month, day);
|
|
22
|
+
}
|
|
23
|
+
export function offsetLabel(offsetMinutes) {
|
|
24
|
+
const sign = offsetMinutes < 0 ? '-' : '+';
|
|
25
|
+
const abs = Math.abs(offsetMinutes);
|
|
26
|
+
return `UTC${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
|
|
27
|
+
}
|
|
28
|
+
export function tableClockAt(place, year, month, day) {
|
|
29
|
+
const offsetMinutes = placeClockAt(place, year, month, day);
|
|
30
|
+
return place.clock
|
|
31
|
+
? { zone: null, offsetMinutes, label: offsetLabel(offsetMinutes) }
|
|
32
|
+
: { zone: place.tz, offsetMinutes, label: place.tz };
|
|
33
|
+
}
|
|
34
|
+
/** 'HH:MM' of an instant on a clock, as a table prints it. */
|
|
35
|
+
export function printedAt(instant, offsetMinutes) {
|
|
36
|
+
const local = Math.floor(instant.getTime() / 60_000) + offsetMinutes;
|
|
37
|
+
const m = ((local % 1440) + 1440) % 1440;
|
|
38
|
+
return `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
|
|
39
|
+
}
|
|
@@ -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,57 @@
|
|
|
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, else node:crypto, else the hash is not checked. */
|
|
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
|
+
/** Put a list in force for the process — one verified by `fetchLatestCorrections`, or restored from the app's own storage. */
|
|
56
|
+
export declare function applyCorrections(authority: Authority, list: CorrectionsFile, revision: number): boolean;
|
|
57
|
+
export declare function fetchLatestCorrections(opts: FetchOptions): Promise<FetchResult>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping an app current without a release: the repository publishes
|
|
3
|
+
* `certification.json` and the correction lists it certifies, and an app
|
|
4
|
+
* that computes a method's revision can take any newer list certified for
|
|
5
|
+
* that revision. This is the check done right — the revision, the list's
|
|
6
|
+
* identity, and its SHA-256 as the certification names it.
|
|
7
|
+
*/
|
|
8
|
+
import { authorityData } from './registry.js';
|
|
9
|
+
import { isCorrectionsFile } from './correctionList.js';
|
|
10
|
+
/** Where the feed lives. A tagged release (`.../v1.2.0/...`) can replace `main` for a pinned address. */
|
|
11
|
+
export const DEFAULT_FEED_URL = 'https://raw.githubusercontent.com/MihrabHQ/mihrab-methods/main';
|
|
12
|
+
export function summarize(list) {
|
|
13
|
+
return { from: list.from, to: list.to, entries: Object.values(list.points).reduce((n, e) => n + e.length, 0) };
|
|
14
|
+
}
|
|
15
|
+
const same = (a, b) => a.from === b.from && a.to === b.to && a.entries === b.entries;
|
|
16
|
+
async function defaultSha256(text) {
|
|
17
|
+
const bytes = new TextEncoder().encode(text);
|
|
18
|
+
const subtle = globalThis.crypto?.subtle;
|
|
19
|
+
if (subtle) {
|
|
20
|
+
const buf = await subtle.digest('SHA-256', bytes);
|
|
21
|
+
return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const { createHash } = await import('node:crypto');
|
|
25
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Put a list in force for the process — one verified by `fetchLatestCorrections`, or restored from the app's own storage. */
|
|
32
|
+
export function applyCorrections(authority, list, revision) {
|
|
33
|
+
const data = authorityData(authority);
|
|
34
|
+
if (revision !== data.revision || !isCorrectionsFile(list))
|
|
35
|
+
return false;
|
|
36
|
+
const now = data.corrections.coverage();
|
|
37
|
+
if (list.to < now.to)
|
|
38
|
+
return false;
|
|
39
|
+
data.corrections.replace(list);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
export async function fetchLatestCorrections(opts) {
|
|
43
|
+
const data = authorityData(opts.authority);
|
|
44
|
+
const have = { ...data.corrections.coverage(), entries: data.corrections.entries() };
|
|
45
|
+
const revision = opts.revision ?? data.revision;
|
|
46
|
+
const base = (opts.baseUrl ?? DEFAULT_FEED_URL).replace(/\/+$/, '');
|
|
47
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
48
|
+
if (!doFetch)
|
|
49
|
+
return { status: 'rejected', reason: 'no fetch available', have };
|
|
50
|
+
const certRes = await doFetch(`${base}/certification.json`);
|
|
51
|
+
if (!certRes.ok)
|
|
52
|
+
return { status: 'rejected', reason: `certification.json: HTTP ${certRes.status}`, have };
|
|
53
|
+
const cert = (await certRes.json());
|
|
54
|
+
const entry = cert?.[opts.authority];
|
|
55
|
+
if (!entry || typeof entry !== 'object' || typeof entry.certified !== 'boolean') {
|
|
56
|
+
return { status: 'rejected', reason: `certification.json has no entry for ${opts.authority}`, have };
|
|
57
|
+
}
|
|
58
|
+
if (typeof entry.revision === 'number' && entry.revision > revision) {
|
|
59
|
+
return { status: 'method-moved', remoteRevision: entry.revision, have };
|
|
60
|
+
}
|
|
61
|
+
if (!entry.certified)
|
|
62
|
+
return { status: 'uncertified', have };
|
|
63
|
+
if (entry.revision !== revision)
|
|
64
|
+
return { status: 'rejected', reason: `certified for revision ${entry.revision}, this build computes ${revision}`, have };
|
|
65
|
+
const named = entry.corrections;
|
|
66
|
+
if (!named || typeof named.from !== 'string' || typeof named.to !== 'string' || typeof named.entries !== 'number') {
|
|
67
|
+
return { status: 'rejected', reason: 'the certification names no correction list', have };
|
|
68
|
+
}
|
|
69
|
+
if (same(have, named) || named.to < have.to)
|
|
70
|
+
return { status: 'current', have, certified: true };
|
|
71
|
+
const listRes = await doFetch(`${base}/data/corrections/${opts.authority}.json`);
|
|
72
|
+
if (!listRes.ok)
|
|
73
|
+
return { status: 'rejected', reason: `${opts.authority}.json: HTTP ${listRes.status}`, have };
|
|
74
|
+
const text = await listRes.text();
|
|
75
|
+
let list;
|
|
76
|
+
try {
|
|
77
|
+
list = JSON.parse(text);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { status: 'rejected', reason: 'the list is not JSON', have };
|
|
81
|
+
}
|
|
82
|
+
if (!isCorrectionsFile(list))
|
|
83
|
+
return { status: 'rejected', reason: 'the list does not have the shape of one', have };
|
|
84
|
+
const summary = summarize(list);
|
|
85
|
+
if (!same(summary, named))
|
|
86
|
+
return { status: 'rejected', reason: 'the list is not the one the certification names', have };
|
|
87
|
+
let integrity = 'unchecked';
|
|
88
|
+
if (named.sha256) {
|
|
89
|
+
const hash = opts.sha256 ? await opts.sha256(text) : await defaultSha256(text);
|
|
90
|
+
if (hash !== null) {
|
|
91
|
+
if (hash.toLowerCase() !== named.sha256.toLowerCase())
|
|
92
|
+
return { status: 'rejected', reason: 'the list’s SHA-256 is not the one the certification names', have };
|
|
93
|
+
integrity = 'verified';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const applied = opts.apply === false ? false : applyCorrections(opts.authority, list, revision);
|
|
97
|
+
return { status: 'updated', list, summary, integrity, applied };
|
|
98
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library without any authority's data: the calls, the types, the sun.
|
|
3
|
+
* Load an authority (`mihrab-methods/<authority>`, or `mihrab-methods` for
|
|
4
|
+
* all) before calling anything that needs its places or its list.
|
|
5
|
+
*/
|
|
6
|
+
export { candidateTimes, certificationOf, certifiedSpan, distanceKm, isCertified, loadedAuthorityIds, methodInfo, nearestPlace, placeById, placesOf, timesFor, timesForRange, type NearestPlace, } from './api.js';
|
|
7
|
+
export { candidateInstants, type ComputeOptions } from './compute.js';
|
|
8
|
+
export { parseCivilDate, isoOf } from './civilDate.js';
|
|
9
|
+
export { placeClockAt, clockOffsetAt, tableClockAt, printedAt } from './clock.js';
|
|
10
|
+
export { fetchLatestCorrections, applyCorrections, type FetchOptions, type FetchResult } from './fetchLatest.js';
|
|
11
|
+
export { isCorrectionsFile, pointKey } from './correctionList.js';
|
|
12
|
+
export { METHOD_REVISION } from '../methods/revisions.js';
|
|
13
|
+
export { preciseApparentSun } from '../sun/preciseSun.js';
|
|
14
|
+
export { AUTHORITIES, PRAYER_KEYS, UncertifiedMethodError, type Authority, type Certification, type CheckRecord, type CivilDate, type CorrectionsFile, type DayStatus, type DayTimes, type Instants, type MethodInfo, type Place, type PrayerKey, type PrayerTime, type TableClock, type TimesOptions, } from './types.js';
|