An stb-style collection of single-header C libraries for Muslim applications.
Drop a header in, define its *_IMPLEMENTATION macro in one translation unit,
and you are done. C11 and C++17, no build system, no package manager.
| Header | Version | Provides | Depends on |
|---|---|---|---|
prayertimes.h |
v0.2.4 |
Prayer times, 21 calculation methods | <math.h> |
hijri.h |
v0.4.1 |
Hijri calendar, crescent visibility models | <math.h> |
timezone.h |
v0.1.2 |
IANA zone name → UTC offset, DST applied | OS timezone database |
prayertimes.h and hijri.h are independent and dependency-free.
timezone.h is optional and is the only header that touches the OS.
#define PRAYERTIMES_IMPLEMENTATION
#include "prayertimes.h"
const MethodParams *params = method_params_get(CALC_KEMENAG);
struct PrayerTimes t = calculate_prayer_times(
2025, 11, 21, // date
-6.2851291, // latitude, negative = South
106.9814968, // longitude, positive = East
7.0, // UTC offset in hours
params);
char buf[16];
format_time_hm(t.fajr, buf, sizeof buf);
printf("Fajr: %s\n", buf); // Fajr: 04:05gcc -std=c11 -Wall -Wextra -Wpedantic -O2 examples/prayertimes_example.c -lm -o exampleEach method sets a Fajr angle, an Isha angle or fixed interval, a Maghrib offset, an Asr shadow factor (1 = Shafi'i, 2 = Hanafi), and an ihtiyat precautionary margin. Kemenag is the default.
| Key | Method | Region |
|---|---|---|
mwl |
Muslim World League | Europe, Far East |
makkah |
Umm al-Qura, Makkah | Arabian Peninsula |
isna |
ISNA | North America |
egypt |
Egyptian General Authority | Africa, Middle East |
karachi |
Univ. Islamic Sciences, Karachi | Pakistan, India, Bangladesh |
turkey |
Diyanet | Turkey |
singapore |
MUIS | Singapore |
jakim |
JAKIM | Malaysia |
kemenag |
KEMENAG (default) | Indonesia |
france |
UOIF | France |
russia |
Spiritual Administration | Russia |
dubai |
GAIAE | UAE |
qatar · kuwait · jordan · gulf |
Ministries of Awqaf | Gulf states |
tunisia · algeria · morocco |
Ministries of Religious Affairs | Maghreb |
portugal |
Comunidade Islâmica de Lisboa | Portugal |
moonsighting |
Moonsighting Committee | Worldwide |
const MethodParams *mwl = method_params_get(CALC_MWL);
CalcMethod m = method_from_string("isna"); // case-insensitivePass CALC_CUSTOM with your own angles for anything not listed.
To walk a date range without struct tm or mktime, and therefore without
local-time hazards, use the static inline day-number helpers:
for (long s = mt_days_from_civil(2026, 7, 1); s <= mt_days_from_civil(2026, 7, 31); s++) {
int y, m, d;
mt_civil_from_days(s, &y, &m, &d);
struct PrayerTimes t = calculate_prayer_times(y, m, d, lat, lon, tz, params);
}Times are cross-checked against published timetables to within 1–2 minutes
depending on method. Those references are third-party calculators, not primary
authorities. See docs/KEMENAG_METHOD.md for the
worked mathematics.
The API is layered deliberately, and the layers mean different things:
HijriEveningParameters— explicit astronomical quantities for one evening at one location: sunset, moonset, conjunction, altitudes, both geocentric and topocentric elongation, Moon age, lag.HijriLocalPredicate— one threshold condition at one observer location.- Calendar functions — convert a Gregorian date to a Hijri date.
A local predicate is not a national or global authority decision. Real authorities combine calculation with geographic aggregation, actual sighting reports, and administrative rulings. Applications must add that themselves.
HijriLocation jakarta = {-6.2088, 106.8456, 8.0, "Jakarta"};
HijriDate date;
hijri_from_gregorian_with_local_predicate(
2026, 7, 27, &jakarta, HIJRI_PREDICATE_MABIMS_2021, &date);
hijri_umm_al_qura_from_gregorian(2026, 7, 27, &date); // Mecca-based, takes no locationYallop and Odeh are graded visibility classifications, not calendar policies —
hijri_yallop_evaluate_evening() and hijri_odeh_evaluate_evening() return
best-time parameters, a score, and a zone. Turning a zone into a date is an
application policy you define.
Accuracy. The lunar position uses the full Meeus ch. 47 series. Measured
against 24 JPL Horizons epochs spanning 1900–2100: 0.0051° in longitude,
0.0006° in latitude, 41.9 km in distance. Nutation and aberration are not
applied, so these are geometric positions referred to the mean equinox of date.
Calendar conversion is measured separately against a different reference.
hijri_umm_al_qura_from_gregorian() reads the published Umm al-Qura table for
1300–1600 AH (1882–2174 CE) rather than reconstructing it astronomically: 198
of 198 official month starts over 2015–2030, exact. The ~600-byte table is
derived from the ICU/CLDR islamic-umalqura calendar, verified byte-identical
to ICU's own Unicode-licensed data, and corroborated by independently
documented Saudi dates for the modern era. One honest caveat: for years before
~1423 AH (2002) the table everyone ships is a retro-computation, and three of
five documented historical correspondences (1932, 1979, 1992) differ from it
by one day. Outside the table's range the astronomical reconstruction (92.4%
measured) takes over. The measurements, anchors, and why reconstruction is
capped — including months where the official table departs from its own
stated rule — are recorded in
docs/research/2026-08-01-umm-al-qura-oracle.md.
Against Indonesia's official calendar (Kemenag, 37 month starts 2024–2026,
validated via seven independent government announcements), the MABIMS 2021
predicate at the westernmost point is never one day early and supports 33 of
37 official starts — measured, fixture-enforced, and deliberately not
claimed as exact reproduction: the official calendar includes months the
criterion alone does not produce. Details and the geocentric-elongation
convention finding are in
docs/research/2026-08-01-kemenag-reference.md.
Against the published calendars of Malaysia, Singapore and Brunei, every month from 2024 to 2026 that each publishes, the same predicate supports every month start and is never one day early, fixture-enforced: JAKIM 37 starts, MUIS 27, Brunei's Taqwim 27. The three calendars agree on every month they share. Malaysia and Brunei are evaluated at Kuala Lumpur and Singapore at Singapore. Brunei uses Kuala Lumpur because its Taqwim follows the regional calendar, and Bandar Seri Begawan misses two months of that shared calendar, one of them in Brunei's own record. See docs/research/2026-10-16-mabims-full-calendars.md.
Against Muhammadiyah's official calendar, the Wujudul Hilal predicate at
Yogyakarta reproduces all twelve Maklumat-announced month starts of 1443 to
1446 H, never early, matching every published wujud/belum-wujud verdict,
fixture-enforced. See
docs/research/2026-08-01-muhammadiyah-reference.md.
From 1447 H Muhammadiyah follows KHGT (Kalender Hijriah Global Tunggal, Istanbul 2016), a global criterion this library does not implement. Wujudul Hilal reproduces the Muhammadiyah calendar through 1446 H and no further. Selecting it for dates after that computes the criterion Muhammadiyah used previously, not the one it uses now.
Authority registry. hijri_authority_get() returns a calculation profile for a named authority: which rule, at which reference point, for which dates, and what it was checked against. hijri_authority_from_gregorian() converts a date under that profile.
HijriDate date;
HijriAuthorityStatus status = hijri_authority_from_gregorian(
HIJRI_AUTHORITY_KEMENAG, 2026, 7, 27, &date);
if (status == HIJRI_AUTHORITY_IN_RULE_PERIOD) {
/* date is filled, and the rule governed that day */
} else if (status == HIJRI_AUTHORITY_OUTSIDE_RULE_PERIOD) {
/* date is filled, but the library has no source for the rule governing that day */
}| Authority | Rule | Reference point | Checked against |
|---|---|---|---|
HIJRI_AUTHORITY_KEMENAG |
MABIMS 2021 | Sabang | 33 of 37 official calendar months |
HIJRI_AUTHORITY_JAKIM |
MABIMS 2021 | Kuala Lumpur | 37 of 37 official calendar months |
HIJRI_AUTHORITY_MUIS |
MABIMS 2021 | Singapore | 27 of 27 official calendar months |
HIJRI_AUTHORITY_BRUNEI_KHEU |
MABIMS 2021 | Kuala Lumpur | 27 of 27 official calendar months |
HIJRI_AUTHORITY_UMM_AL_QURA |
published table | none | 198 of 198 months |
HIJRI_AUTHORITY_MUHAMMADIYAH_WUJUDUL_HILAL |
Wujudul Hilal | Yogyakarta | 12 of 12 announced starts, through 1446 H |
HIJRI_AUTHORITY_DIYANET |
published table | none | 186 of 186 months |
HIJRI_AUTHORITY_DIYANET is Diyanet's own published calendar, every month start from 2021-01-14 to 2035-12-31, as printed on two Diyanet sites that agree on every start they share. Outside those dates it returns HIJRI_AUTHORITY_UNRESOLVED: Diyanet computes its calendar by the 2016 Istanbul rule, which asks whether the crescent is visible anywhere on Earth, and no single point reproduces it. See docs/research/2026-10-17-diyanet-calendar.md.
An entry is a calculation profile, not the authority. The result is the rule computed at one point and chained month to month, and each authority can and does depart from it by ruling. The counts above are read from the entry itself, and the test suite recomputes them from the fixtures, so the table in the header cannot drift from the tests. Only authorities with a fixture have an entry. The sourced period for each rule is in docs/research/2026-10-10-authority-rule-periods.md, and why one reference point stands in for a territory is in docs/research/2026-10-10-region-aggregation-measured.md.
Announced month starts. An authority rules on a month, and the ruling can differ from the calculation. hijri_authority_from_gregorian_announced() takes the caller's list of announced starts and applies it over the registry. A month an announcement moves keeps the month after it at 29 or 30 days, and once a month start agrees with the calculation again the registry's own answer is returned.
/* 1 Ramadan 1446 as Kemenag announced it, a day before the calculation. */
static const HijriAnnouncedStart announced[] = {{{2025, 3, 1}, 1446, 9}};
HijriDate date;
HijriAnnouncedStatus status = hijri_authority_from_gregorian_announced(
HIJRI_AUTHORITY_KEMENAG, announced, 1, 2025, 3, 1, &date);
/* status == HIJRI_ANNOUNCED_FROM_ANNOUNCEMENT, date is 1446-09-01 */The header ships no announcements and never judges one. The caller decides where announcements come from and whether to trust them. A list out of date order, with two entries whose gap cannot fit 29 or 30 days a month, or whose entry in effect names a Hijri month the calculation does not place within two days of its date, is rejected with HIJRI_ANNOUNCED_INVALID_LIST. Against the official Kemenag calendar from Rajab 1445 to Rajab 1448, the seven starts the government announced bring the days the registry gets wrong from 90 to 30. The 30 left are the two Rajab months, which nobody announced.
A calculated result is still not an observation, and nothing here decides religious validity.
Numerical uncertainty and policy. hijri_predicate_margins() reports how
far each term of a predicate sits from its own threshold, in that term's own
units, and nothing else. It does not combine terms, convert between units, or
label a margin as near, so it cannot change a decision. Whether a margin is
small enough to be unsafe is left to the application, and whether a term
sitting at exactly its threshold passes is a convention the criterion states,
not a measurement, which is why HijriDecisionTermMargin reports strict
per term. The error bars a margin should be read against are the ones
documented in hijri.h itself, under NUMERICAL UNCERTAINTY AND POLICY and
ACCURACY CAVEAT, and are not repeated here since duplicated figures are how
this file's past accuracy mistakes happened. That section also records that
the solar longitude residual is biased rather than symmetric, so a margin
should be weighed against the relevant side of that range, not against a
symmetric tolerance.
Thread safety and determinism. Every public function in hijri.h is a
pure function of its arguments, keeps no state between calls, and is safe to
call from any number of threads at once. Results are bit-identical run to
run on the same platform, same compiler, and same flags, which is exactly
what make baseline checks on every run. Measured between glibc 2.44 and
musl 1.2.6 on x86-64, the baseline CSV is byte-identical and Julian Day
instants never differ across a 336-row sweep, though angular quantities
occasionally differ by 1 to 2 units in the last place. Other platforms and
architectures are not measured. This determinism claim is specific to
hijri.h, see hijri.h itself for the full contract.
prayertimes.htakes a fixed numeric UTC offset and has no notion of zones or DST — deliberately, since DST is a political rule rather than an astronomical one. For a DST-active date you must pass the adjusted offset yourself (1.0for London in summer,0.0in winter).
timezone.h will resolve it for you from the host OS timezone database:
#define MUSLIM_TIMEZONE_IMPLEMENTATION // exactly one translation unit
#include "timezone.h"
char zone[64];
get_system_timezone(zone, sizeof zone); // "Europe/London"
double tz = 0.0;
parse_timezone_offset(zone, time(NULL), &tz); // DST appliedparse_timezone_offset returns 0 on success with the offset written to
*out, or -1 on failure (unresolvable zone, or a NULL argument), in which
case *out is left untouched — check the return value before trusting tz.
It touches no global timezone state, so it is safe to call from multiple
threads at once.
On a platform without a timezone database, keep supplying the offset yourself.
Using the library needs no build system — drop the header in. The Makefile is
for checking the library itself, and needs GNU make (gmake on the BSDs):
make # everything below
make test # build all tests strict-C11 and run them
make cxx # compile every header as C++17
make baseline # regenerate the research CSV and compare
make check CC=clang CXX=clang++ # same checks, different toolchainCI runs make test cxx examples on Linux and macOS with gcc and clang on every
push and pull request, and make baseline on Linux only — the CSV prints Julian
Days to nine decimals and libm rounding differs between platforms, so a
byte-exact comparison is reproducible per-platform rather than universally.
Each target enforces something the project asserts and which was previously
checked only by remembering to run a command — prayertimes.h once shipped
broken under its own documented build line because nobody ran it, and the first
CI run caught a macOS/glibc difference no local check could have.
tests/test_ephemeris_oracle.c validates the lunar series against a vendored JPL
Horizons fixture, Meeus's own printed worked example, and published ΔT values.
tests/test_hijri.c covers calendar arithmetic, predicate thresholds, and a
longitude sweep asserting each observer's own local evening is used.
The research baseline is a generated artifact, committed so changes to it are
visible in review. Any change to the ephemeris, the predicates, or the evening
calculation moves it, and make baseline fails until it is regenerated with
make baseline-update. No value in that CSV is reference truth: it is
produced by the library it is used to check, and its nine-decimal Julian Days
imply precision the underlying accuracy does not support.
Sources, conventions, and fixture admission decisions are recorded in
docs/research/.
The release tag and the header versions are different numbers on purpose.
If you downloaded 2026.08.18 and opened prayertimes.h to find v0.1.1,
nothing is wrong.
| Looks like | Answers | |
|---|---|---|
| Release tag | 2026.08.18 |
When this snapshot was cut |
| Header version | v0.1.1 |
Whether this header is compatible with the one you already have |
The headers are independent libraries that happen to share a repository. You vendor one file, not the set, so each carries its own semantic version and they move at their own pace. A header that did not change does not get a new number, which is why the three above are not all equal.
That leaves nothing sensible for a single repository-wide semantic version to
mean. It would have to describe three libraries at once, so it could not tell
you anything reliable about the one you actually copied. Release tags are
therefore calendar dates, YYYY.MM.DD, which make no compatibility claim at
all. Every release lists the version of each header it contains.
Use the release tag to pin a download. Use the header version to reason about compatibility.
The first release, v0.1.0, predates this scheme and keeps its name.
Language bindings maintained alongside this repository. Each vendors the C headers directly and pins the ABI in its own test suite, so a changed struct or signature fails there rather than diverging silently.
| Language | Repository | Vendors |
|---|---|---|
| Rust | libmuslim-rs | prayertimes.h, timezone.h |
| Dart | libmuslim-dart | prayertimes.h |
| Project | What it is |
|---|---|
| muslimtify | Prayer time CLI and notification daemon for Linux and Windows |
docs/KEMENAG_METHOD.md— prayer-time mathematics (Indonesian)docs/INTERNATIONAL_METHODS.md— method parametersdocs/HIJRI_CALCULATIONS.md— Hijri calculationsdocs/METHOD_TOLERANCES.md— accuracy expectationsROADMAP.md— what is planned and what is deliberately not- Understanding Islamic Prayer Time Calculations
Changes to calculation methods must be verified against a source, and the
source recorded. docs/research/hijri-2020-2025-sources.md shows the standard:
a fixture is admitted only when its value and its conventions are
unambiguous, and an unknown convention is recorded as a gap rather than guessed.
MIT. Copyright 2025-2026 muslimtify-org.