Accuracy
How 716,314 golden test cases hold x-iztro to zero divergence from JS iztro, and where that claim stops.
For: everyone. The "How the hash comparison works" section is for developers
The most important property of a charting library is that the results are right. And "right" has no authority to appeal to in Zi Wei Dou Shu — differences between implementations usually come from school-of-thought choices, and it is hard to say who is wrong.
So x-iztro sets a very concrete target instead: identical field for field to JS iztro v2.5.8. Treating that as the gold standard turns differences from a matter of opinion into automatically detectable bugs.
What iztro is, and why it makes a good gold standard
iztro is an open-source Zi Wei Dou Shu charting library written in TypeScript. It is one of the most complete and longest-maintained open-source implementations in the field, and a fair number of frontend projects and mini-programs use it.
The reason for choosing it as the reference is not "it must be correct", but three engineering properties:
- Complete: natal chart, six horoscope levels, four groups of twelve gods, year-derived adjective stars, the Zhongzhou school, six chart languages — nothing missing. You can only compare against something that has the surface to compare.
- Deterministic: the same input always yields the same output, with no randomness and no external dependencies, so any difference is a difference in logic rather than noise.
- Pinnable: pinning the version at v2.5.8 makes the reference stable. When iztro is upgraded, regenerating the reference data turns the list of failing cases into the list of behavioural changes between versions.
What "accurate" does and does not mean here
Agreeing with iztro ≠ the one correct answer in the art
What x-iztro guarantees is: given the same set of school choices, it computes exactly what a mature implementation computes.
It does not guarantee that those school choices are themselves "right". Whether geng's Hua Ke goes to Taiyin or Tianfu, whether the year's stem and branch turn over at lunar New Year or at the Beginning of Spring (立春, the solar term around 4 February), whether the late Zi hour belongs to today or tomorrow — these have always been disputed. iztro picked one set, x-iztro follows it verbatim, and turns the disputed points into configuration switches so you can decide for yourself.
If your school differs from the default, change the config or supply a custom mutagen table; do not expect the default output to match your lineage.
The year range the golden data covers
The reference data is generated on the JS side, so the cases are concentrated in the year range the JS implementation generates reliably, with a further sampled layer every ten years across the boundary eras (1583–1983 and 2044–2100).
x-iztro itself supports Gregorian years 1583–9999. Years outside the sampled range still chart, but they have not been compared case by case against golden data — verify for yourself when using extreme years.
The coverage matrix
All reference data is generated by the pinned version of JS iztro, 716,314 cases in total:
| Layer | Cases | Coverage | Data format |
|---|---|---|---|
| Tier 1 | 1,560 | 60 years × 13 hours × both genders, every field compared one by one (including display fields, the Original palace and structured dates) | Full JSON |
| Tier 2 | 37,440 | 60 years × the 1st and 15th of each month × 13 hours × both genders | Compact JSON |
| Tier 3 | 586,430 | every day of 60 years × 13 hours × both genders × fix_leap (leap months twice) | SHA-256 CSV |
| Boundary eras | 46,228 | Sampled every 10 years across 1583–1983 and 2044–2100, filling the blind spot left by Tier 1/2/3 covering only 1984–2043 | SHA-256 CSV |
| Horoscope | 5,760 | 360 charts × 16 target dates, all six horoscope levels, all fields | Compact JSON |
| Variants | 14,268 | by_lunar day by day through leap months, the Zhongzhou school, all six chart languages | CSV / JSON |
| Config | 9,696 | Non-default values of the four boundary switches, at both the charting and horoscope layers | CSV / JSON |
| Zhongzhou chart types | 12,488 | Heaven / earth / human plate | SHA-256 CSV |
| 1602 window | 2,444 | 1602-2-20 through 4-25 day by day × 13 hours × both genders (leap-month dates twice for fix_leap), locking the leap-month correction layer | SHA-256 CSV |
| Total | 716,314 |
Not counted in the table above:
- 1,559 translation reverse-lookup cases: compared one by one against the actual values of
iztro's
kot, holding the disambiguation order for homographic names. - 13 binding contract cases: the DTO compared key by key and value by value against iztro's
JSON.stringifyoutput. - 209 Python end-to-end cases (including custom mutagen and brightness tables, full hour coverage, patterns and knowledge packs).
- Go end-to-end tests: golden comparison, star placement, concurrency correctness, override tables, and a barrage of invalid input.
- C FFI boundary safety tests: any invalid input must return an error JSON rather than crash.
- Prompt snapshot tests: natal and horoscope prompts compared byte for byte in both Chinese and English.
What each layer guards against
Tier 1 catches field-level divergence. It compares every field including display strings and the Original palace flag, so a translation or format that differs from iztro shows up immediately.
Tier 2 and Tier 3 catch boundary dates. Errors in Zi Wei often appear only on particular dates — leap months, month ends, the start of a year, solar-term changeovers. Tier 3 covers every single day of 60 years, without exception.
Boundary eras catch the far ends of the year range. Tier 1/2/3 concentrate on 1984–2043; this layer samples every ten years out to 1583 and 2100, guarding against the calendar algorithm quietly drifting at the extremes.
Horoscope catches the horoscope layers. The 16 target dates are chosen exactly where trouble lives: one per each of the 12 yearly branches, the childhood scope, advanced age, a leap month, and the late Zi hour.
Variants catch schools and chart languages. The Zhongzhou school and all six chart languages get a full comparison each, so switching algorithm school or language introduces no drift.
Config and Zhongzhou chart types catch boundaries and plates. The Beginning-of-Spring window day by day, the late Zi hour, the days around a birthday — every switch is verified day by day inside the window where it makes a difference.
The 1602 window catches defects in the lunar-calendar dependency itself. The Rust-side lunar
library's month table contradicts itself in 1602 (a 31-day second month); x-iztro corrects it at its
single lunar-table entry point against values cross-confirmed by three independent sources —
lunar-typescript, the Shou-Xing almanac (sxtwl) and the Korea Astronomy and Space Science
Institute's tables — and this layer locks the corrected window day by day. A separate full-domain
scan of every date from 1583 to 9999 (~6.1 million charts, #[ignore]d) found no second window of
the same kind.
How the hash comparison works
For developersTier 3 has 586,430 cases; storing full JSON would run to tens of gigabytes. So these layers compare the SHA-256 of a canonical string:
tests/golden/canonical.mjs on the JS side and tests/common/mod.rs on the Rust side implement the
same serialization rules and are byte-for-byte isomorphic. Each side flattens its chart into the
same canonical string and the hashes are compared — what is stored is the first 32 hex characters of
a SHA-256 rather than tens of kilobytes of JSON.
When a hash mismatches, the --inspect family of generator flags replays that case's JS output and
diffs it against the Rust canonical string, pinpointing the offending field directly.
Running the tests
# The regular layers: unit + Tier 1/2 + horoscope + variants + config + contract + the 1602 window, about a minute
cargo test
# All of Tier 3: 586,430 cases, about 70 seconds
cargo test --release --test golden_tier3 -- --ignored
# Binding end-to-end
cd python && pytest tests/ # run maturin develop first
cd go/iztro && go test ./...Regenerating the reference data
Requires a Node.js environment:
cd tests/golden
npm ci
npm run gen:all # every layer; per-generator scripts are the gen:* entries in package.jsonThe tier 3 and boundary-era generators skip files that already exist, so interrupted runs resume;
tier 3 also takes node generate_tier3.mjs --range <from> <to> for parallel sharding (the full run
is about 30 minutes).
Tracking a new iztro release
The procedure is fixed:
- Bump the pinned iztro version in
tests/golden/package.json. - Regenerate all the reference data.
- Run
cargo test.
The list of failing cases is the list of behavioural differences between the two versions — no need to read a changelog, the tests tell you directly which fields moved.
That procedure covers numeric divergence. Tests say nothing when iztro adds or removes an API; that part is checked by hand against Migrating from iztro: API mapping.
What zero tolerance means
Any divergent case is treated as a bug; "the difference is tiny" and "that field doesn't matter" are not accepted as reasons. Wherever iztro has a feature or a data table, x-iztro must produce the same result — and only on top of that does anything else get discussed (language-independent keys, prompt generation, giving the Config switches proper semantics).