Releases: muslimtify-org/libmuslim
Release list
2026.08.22
The twilight grazing residual is gone, and the bug behind it turned out not to be an accuracy problem at all. Moon rise and set now say what they mean at high latitude.
| Header | Version | Changed |
|---|---|---|
prayertimes.h |
v0.2.2 to v0.2.3 |
yes |
hijri.h |
v0.1.0 to v0.1.1 |
yes |
timezone.h |
v0.1.1 |
no |
prayertimes.h v0.2.3
refine_event is gone (#85, closing #79). Fajr, isha, sunrise and sunset are now solved the same way, by bisecting true solar altitude between local noon and local midnight, so the event's existence is decided at the event rather than at 0h UT.
#79 asked for a closest approach method, on the reading that the residual was the solver failing to converge on a real crossing. It was not. Of the 186 grazing points the oracle flagged, 45 were days on which the Sun never reaches the angle at all, so the 30 arcmin being measured was the distance to an event that was not there. Deciding existence first removes the residual without needing a closest approach method.
| Oracle | Before | After |
|---|---|---|
| twilight grazing points | 186, max 30.1964 arcmin | 160, max 0.1970 arcmin |
| polar grazing points | 172, max 9.3160 arcmin | 169, max 0.4556 arcmin |
Measured against the 2026.08.21 asset over MWL at longitude 0, latitude -70 to 70 in steps of 5, every day of 2026, all five fields. Of 52925 values, 30968 move. 14 change the displayed HH:MM, and every one of those 14 is a day on which no event exists and the library now says so instead of reporting a near midnight crossing that was wrong by up to four hours. The other 30954 move by at most 0.0003 seconds.
Existence itself was then checked directly against JPL DE440 over the same grid, 42340 decisions across sunrise, sunset, fajr and isha. Two disagree, both a single day at latitude -70 in November, where the low precision solar model and DE440 differ by 0.0035 degrees on a 0.0013 degree margin. That is the sun model's own noise, not the solver's.
No API change. struct PrayerTimes, MethodParams and every function signature are identical to v0.2.2.
hijri.h v0.1.1
Both rise and set finders classified a failed search from one extra sample, treating it as the extremum of the search window (#83, closing #82). That holds for the Sun, whose finder is handed local midnight so the sample lands on local noon. It does not hold for the Moon, whose own day is 24h 50m, so the sample lands at an arbitrary phase.
Latitude -89 to 89 in steps of 2, every day of 2026:
| before | after | |
|---|---|---|
| false moonset labels | 820 | 0 |
| false sunset labels | 26 | 0 |
| events the finder missed | 12 | 0 |
Not near misses. NEVER_RISES for a Moon spanning -15.38 to +3.60 degrees.
HIJRI_EVENT_NOT_FOUND is no longer reserved. A 24 hour window holding no moonset is not always circumpolar, since the Moon sets once per 24h 50m, so roughly one evening in thirty has no moonset at any latitude. Callers that switch exhaustively on HijriEventStatus should check that arm before upgrading.
Documentation
hijri.h now carries a measured statement of the range it is good over geographically, not a described one (#84, second half of #27). Latitude verdicts with the errors that justify them, why the equatorial 3.4 percent figure is not what it looks like, the longitude table, and where the elevation reasoning lives.
Known issues
#51 and #56 remain open design questions, on polar circle behaviour and on whether prayer times should carry a day offset outside 0 to 24.
The bracket between local noon and local midnight uses noon plus 12 hours for midnight, which is the antisolar transit to within the equation of time. The approximation is marked in the source with the measurement that bounds it.
2026.08.21
Accuracy work at high latitude, the first oracle coverage fajr and isha have ever had, and a check that stops a released version number from naming two different files.
| Header | Version | Changed |
|---|---|---|
prayertimes.h |
v0.2.1 to v0.2.2 |
yes |
hijri.h |
v0.1.0 |
documentation only |
timezone.h |
v0.1.0 to v0.1.1 |
yes, Windows only |
prayertimes.h v0.2.2
refine_event now iterates to convergence instead of taking a single correction step (#80). Measured against the 2026.08.20 asset over MWL at longitude 0, latitude -70 to 70 in steps of 5, every day of 2026, all five fields: 30793 of 52925 values move, 52 change the displayed HH:MM, and the largest shift is 200 seconds at latitude -70 on 17 November. The large ones are all in the grazing band, where the Sun approaches the target depression without clearly crossing it.
No API change. struct PrayerTimes, MethodParams and every function signature are identical, so anything compiled against v0.2.1 still compiles.
timezone.h v0.1.1
The Windows lookup enumerated the timezone registry on every call. It now enumerates once behind InitOnceExecuteOnce (#76), which took 2000 lookups from 41 ms each to 0.0255 ms each. muslim_iana_to_windows_zone was removed as dead code and restored when it turned out a consumer links against it (#77).
No change on Linux or macOS.
Validation
Fajr and isha had no oracle at all before this release. They have one now, comparing against JPL DE440 in the angle domain rather than the time domain, since at grazing incidence a small angular error becomes a large time error (#73, closing #52). The polar region got the same treatment (#72). The oracle grew from 14236 checks to 39549.
hijri.h gained a measured statement of what actually bounds it, rather than a described one: validated 1900 to 2100, Umm al-Qura table 1882 to 2174, and a delta T model adequate from 1600 to 2200 (#78).
Infrastructure
The Windows branch of timezone.h is roughly 160 lines that nothing had ever compiled, on CI or on the author's machine. It is now built and tested under mingw-w64 (#74).
tools/check-header-versions.sh compares each header against every published release asset and fails when a release already carries that version string over different code (#81). Comments are stripped first, so documentation changes do not demand a bump. This exists because the same mistake happened three times: v0.2.0 shipped twice with different behaviour, then v0.2.1 did, then timezone.h v0.1.0 did.
Known issues
#79 is open. Twilight times can be up to 6 minutes out where the Sun grazes the target depression. Iterating refine_event reduces it but does not remove it, because at those latitudes the event can cease to exist between iterations. A different method is needed, solving for closest approach rather than for a root.
#51 and #56 are open design questions about polar-circle behaviour and the day offset.
2026.08.20
Headers in this release
| Header | Version | Changed since 2026.08.19 |
|---|---|---|
prayertimes.h |
v0.2.1 |
Yes |
hijri.h |
v0.1.0 |
No |
timezone.h |
v0.1.0 |
No |
The tag is a calendar date and the headers carry their own semantic versions, so these numbers are not meant to match. Pin a download to the tag. Reason about compatibility from the header version.
prayertimes.h goes from v0.2.0 to v0.2.1. A patch, not a minor: struct PrayerTimes, MethodParams and every function signature are unchanged, so anything compiled against v0.2.0 still compiles and still links. Only values returned inside the polar circle move.
If you pinned 2026.08.19, upgrade. That release reported prayers out of order at high latitude and returned a formula artifact as a prayer time, and its header calls itself v0.2.0 exactly as the fixed one used to, so a vendored copy cannot be told apart by reading it.
Fixed: prayers came back out of order inside the polar circle
A method carrying a reference latitude borrowed sunrise and sunset from that latitude but left fajr, isha and asr solved at the real one, then reported both in a single schedule.
2026-01-01 at 78.22 N, MWL
fajr 7.546 dhuhr 12.012 asr 14.735 maghrib 16.430 isha 16.121
isha 18.5 minutes before maghrib
At that latitude the Sun peaks near -11.2 degrees, so it never crosses -0.833 and there is genuinely no sunset, but it does cross -17, so isha is a real event. Maghrib came from 45 N and isha from 78 N.
41 days of 2026 at Longyearbyen under MWL, all in the polar night. The whole day is now solved at one latitude.
Fixed: asr was reported where the Sun casts no shadow
The Sun's greatest altitude on a day is 90 - fabs(lat - decl), so a separation of 90 degrees or more means it never rises and no shadow of any ratio is cast. The formula did not say so. Past 90 degrees its tangent turns negative, it returned a negative shadow altitude, and that was solved into a plausible looking time.
2026-01-01 at 78.22 N, Sun peaks at -11.235 deg
Kemenag asr altitude -13.922 deg asr 14:47 reported as a prayer time
This was the one field that survived an isfinite() check on days where fajr, maghrib and isha were all correctly unavailable, so a caller filtering per field would have displayed it. A wrong time is worse than no time.
2926 points of a 40810 point grid were affected. Now none.
Fixed: the substituted isha could precede maghrib
Moonsighting sets maghrib 3 minutes after sunset and takes isha as one seventh of the night. On a bright night of 19.5 minutes one seventh is 2.8 minutes, so the fixed offset overtook the fraction.
Tromso 2026-05-18
maghrib = sunset + 3 min = 23.590332
isha = sunset + night/7 = 23.586724 13.0 seconds early
The evening substitution now measures from maghrib. Every method whose maghrib is sunset is unaffected, because for them the two anchors are the same instant.
What did not move
No published-table comparison changed across any of the three fixes. Each was verified by diffing all 702 comparison lines against the previous state, rather than by comparing the residual histogram, because an unchanged histogram does not prove that no individual time moved.
The residual distribution stands at 369 checks at 0 minutes, 326 at 1 and 7 at 2.
Known and deliberate
A 40810 point grid, latitudes -85 to 85 in steps of 5, every seventh day of 2026, all 22 methods:
days with prayers out of order 0
asr finite with no possible shadow 0
days with a NaN field 5434
days with a field outside 0..24 69
The last two are by design. 20 of the 22 methods return non-finite prescribed times inside the polar circle because their authority publishes no rule for that case, tracked in #51. Values outside 0 to 24 carry a day offset that the double is the only thing recording, documented on the struct and tracked in #56.
New test
The five prescribed times are now asserted to stay in order at five locations across all 22 methods, not only the two that carry a reference latitude today. Checks go from 748 to 756.
2026.08.19
Headers in this release
| Header | Version | Changed since 2026.08.18 |
|---|---|---|
prayertimes.h |
v0.2.0 |
Yes |
hijri.h |
v0.1.0 |
No |
timezone.h |
v0.1.0 |
No |
The tag is a calendar date and the headers carry their own semantic versions, so these numbers are not meant to match. Pin a download to the tag. Reason about compatibility from the header version.
prayertimes.h goes from v0.1.1 to v0.2.0. Both changes below are breaking, and both change the layout of struct PrayerTimes, so anything binding this header across the ABI must be rebuilt rather than dropped in.
Breaking: struct PrayerTimes carries the five prescribed prayers only
sunrise and dhuha are removed, along with the DHUHA_ALTITUDE constant.
struct PrayerTimes {
double fajr;
double dhuhr;
double asr;
double maghrib;
double isha;
};Neither removed field is a prescribed prayer. Sunrise is the end of the fajr window. Dhuha is a voluntary prayer and it is carried only by Indonesian timetables, not by MWL, ISNA or Umm al-Qura.
The evidence that these were the weakest part of the surface was already in the repository. Every dhuha fixture came from a single source. tests/test_prayertimes_oracle.c asserted neither field, because the oracle covers maghrib against hijri_find_sunset and nothing else. DHUHA_ALTITUDE documented itself as 4 degrees 30 minutes on the line above while defining 4.3, a gap of about 0.8 minutes at Indonesian latitudes, which is under the 2 minute tolerance, so no test could tell which value was intended.
Both quantities are still computed internally. Maghrib is sunset, and every high-latitude substitution measures the night between sunset and sunrise.
Breaking: the high-latitude rule is a property of the calculation method
MethodParams gains high_lat_method and high_lat_ref, and HighLatMethod gains HIGHLAT_NEAREST_LATITUDE. The enum already existed in v0.1.1 but nothing read it, so the angle-based rule was hardcoded and applied to all 22 methods alike. Kemenag silently received a rule Kemenag never published.
Only two of the researched authorities publish a position. MWL carries HIGHLAT_ANGLE_BASED with a reference latitude of 45, which is what its own Fiqh Council decree names. Moonsighting Committee carries HIGHLAT_ONE_SEVENTH anchored at 60, which is what its published page states. The rest carry no reference latitude, because attributing a ruling to an authority that never issued one is the failure mode this change exists to avoid.
The reference latitude is consulted only inside the polar circle, where there is no sunrise or sunset and every angle-based rule is undefined by construction.
NaN days at latitude 68 north, longitude 0, over 2025:
method high_lat_ref fajr maghrib isha
Muslim World League 45 0 0 0
Moonsighting Committee 60 0 0 0
ISNA 0 52 76 52
Umm al-Qura, Makkah 0 52 76 76
Before this change every one of those rows read like the ISNA row, including MWL. The fajr and isha fallback was itself NaN above 66 degrees, because it measured a night whose endpoints did not exist.
A note on what this release does not claim: 20 of the 22 methods still return NaN inside the polar circle. That is deliberate and it is tracked in #51.
A halt condition, and how it was resolved
Assigning MWL the decree's proportional rule for the case where a night does exist broke three published-table checks, all London.
London 2026-06-15 isha computed 23:22 published 23:25 3 min
London 2026-07-15 fajr computed 02:55 published 02:40 15 min
London 2026-07-15 isha computed 23:08 published 23:25 17 min
The decree and the timetables published under the MWL name are two different things, and this library validates against the timetables. It was resolved by narrowing the implementation, not by widening the 2 minute tolerance that exposed it. The decree is now used only inside the polar circle, where the published tables have nothing to say.
Test counts
The published-table suite reports 745 checks, down from 932. 702 of those compare a computed time against a published table at a uniform 2 minute tolerance, with a residual distribution of 369 at 0 minutes, 326 at 1 and 7 at 2.
The fall is the sunrise and dhuha columns of the same tables. It is lost coverage of two fields the library no longer returns, not lost coverage of the five it does.
Bindings
The Rust and Dart bindings are updated for both changes and pin this ABI in their own probe tests.
- Rust: libmuslim-rs#12
- Dart: libmuslim-dart#5
2026.08.18
Headers in this release
| Header | Version | Changed since v0.1.0 |
|---|---|---|
prayertimes.h |
v0.1.1 |
Yes |
hijri.h |
v0.1.0 |
No |
timezone.h |
v0.1.0 |
No |
The tag is a calendar date and the headers carry their own semantic versions, so these numbers are not meant to match. Pin a download to the tag. Reason about compatibility from the header version. Each header explains this in its own banner, since a vendored file travels alone.
This is the first calendar-dated release. v0.1.0 keeps its name.
Fixed
The time formatters could emit a field no parser could read back. format_time_hm and format_time_hms cast to int before reducing, and the cast truncates toward zero, so any hour below zero produced a negative minute field. Non-finite input was worse, because (int)NAN is undefined behaviour.
before after
-0.104 -> "00:-6" -> "23:54"
-0.104 -> "00:-6:-13" -> "23:53:46"
NaN -> "-8:-2147483648" -> "--:--"
NaN -> "-8:-35791394:-8" -> "--:--:--"
Both inputs are reachable rather than hypothetical. The fajr and isha high-latitude fallback returns hours outside 0 to 24 on 107 days a year at Reykjavik and 23 at Anchorage, and the unguarded hour_angle call sites return NaN above 66 degrees north.
No computed time moved. The 895 published-table comparisons kept their 449, 435, 11 residual distribution across the change.
Documented
struct PrayerTimes had no doc comment at all. It now states both ways a field departs from an ordinary hour of the day, and seven checks pin the measured figures so the comment cannot drift from the code.
A field can be NaN, because above roughly 66 degrees the Sun may never reach the altitude an event is defined by. A field can also fall outside 0 to 24, meaning the event lands on the previous or the next calendar day.
The second is worth reading before you use this library at high latitude, because mishandling it looks like tidying up. The double is the only thing carrying the day offset, so reducing a field into 0 to 24 before building a timestamp moves the event silently onto the wrong day.
Known limits
Above roughly 66 degrees north or south, several fields are non-finite, and the fajr and isha high-latitude fallback is itself non-finite there because it is defined in terms of a sunset and sunrise that do not occur. dhuha reaches this first, from about 62.5 degrees. Tracked in #51, with the convention research and its primary sources in docs/research/2026-08-18-high-latitude-conventions.md.
Whether struct PrayerTimes should carry the day offset explicitly is open in #56.
Checks
932 in the published-table suite, 14236 in the oracle suite, 209 in the timezone suite. make check covers C11, C++17, the examples and the research baseline.
v0.1.0
First tagged release of libmuslim.
Three dependency-free single-header C libraries. Drop a file in, define one macro in one translation unit, done. No build system, no package manager, no runtime dependency beyond libm.
hijri.h astronomical Hijri calendar, crescent visibility predicates
prayertimes.h daily prayer times, 22 calculation methods
timezone.h optional DST-aware offset resolution from the host tzdb
MIT licensed. Free for commercial use, with no further permission to seek from anyone.
Why 0.1.0 and not 1.0.0
Because nothing has ever been published, so 0.x is free to claim now and expensive later.
Under semver, 0.x means anything may change, and that is the accurate description of a library whose supported date range is not yet documented and whose authority policy layer does not exist. Removing hijri_find_conjunction() was free precisely because of the pre-1.0 window, and there is more API-shaped work queued behind it.
Tracked in #27, which also records what a real 1.0.0 would require.
What is actually validated
Every number below is measured and reproducible with make check, not asserted.
test_hijri 2503 checks tabular roundtrip over 3,897,679 days
test_ephemeris_oracle 1157 checks Moon lon 0.0051 deg, lat 0.0006 deg, dist 41.9 km
test_yallop_tn69 555 checks 271 real crescent observations
test_prayertimes 910 checks published schedules, uniform 2 minute tolerance
test_prayertimes_oracle 14236 checks max 6.5966 s against a DE440 validated solver
test_timezone 209 checks
The lunar and solar ephemerides are cross-checked against two independent oracles, JPL Horizons and Skyfield on JPL DE440, which agree with each other to 0.0000668 deg before any library figure is read from them.
Official published calendars are reproduced for:
- Saudi Arabia, Umm al-Qura, 198 of 198 month starts
- Indonesia, Kemenag MABIMS 2021, 33 of 37, never early
- Indonesia, Muhammadiyah Wujudul Hilal, 12 of 12, for 1443 to 1446 H
Yallop's TN69 is validated against all 271 evening observations in Table 4, with a maximum q residual of 0.053577.
What is not
Stated plainly because a calculated result is not an observation.
- Malaysia, Brunei and Singapore use the same MABIMS 2021 criterion but have no fixture behind them. Candidate fixtures were rejected for lack of a citable primary source.
- Umm al-Qura reproduces by shipping the published table, not by computing it. Astronomical reconstruction reached 183 of 198, and 198 of 198 was proven unreachable from the published rule.
- Muhammadiyah coverage stops at 1446 H. They moved to KHGT from 1447 H, which is a different criterion and is not implemented.
- There is no authority policy layer. The library evaluates a criterion at one observer location. It does not aggregate across a nation, model itsbat, or weigh sighting testimony.
- Egypt, Turkey, ECFR and FCNA/ISNA are research candidates, not planned APIs, pending primary specifications.
- The supported date range is undocumented. The only range in the header is the span the ephemeris oracle covered.
Known issues
- #51
dhuhareturns NaN above roughly 62.5 degrees of latitude on days the Sun never reaches that altitude, with no error and no sentinel. Sunrise and dhuhr solve normally on those same days. - #52 Fajr and isha were refined without an oracle behind them. Isha moves up to 16 minutes at high latitude.
- #47 Odeh has no observation-level validation, because Table VI is image-only.
Verification
make check exits 0 under gcc and clang at f3d30a7. It builds every header, test and example as strict C11 with -Wall -Wextra -Wpedantic, compiles all three headers as C++17, and regenerates the research baseline CSV and byte-compares it against the committed copy, so numerical drift fails the build.
Consumers
- libmuslim-rs, Rust bindings
- libmuslim-dart, Dart FFI bindings
- muslimtify, prayer time CLI and daemon