Changelog
[Unreleased]
Added
- UTC civil time ↔ JDE(TT) in C (
celestial_utc_to_jde,celestial_jde_to_utc,CelestialUTCTime), Python (utc_to_jde,jde_to_utc) and JavaScript (time.utcToJde,time.jdeToUtc). These expose the existing C++ conversions without changing their historical UT1 substitute, leap-second policy or domains. - Runnable Python and JavaScript UTC-input Jieqi examples with UTC+8 display.
Changed
- Prebuilt macOS arm64 libraries and Python wheels require macOS 15 or newer, up from macOS 14.
[v1.0.0]
Added
- Capabilities and model identities (#357): TT target-operation records, separate civil-house admission facts,
and separate operation/model text lookups. Python uses
Targetand kind-qualifiedModelKeyvalues;house_capability(delta_t_model=...)is the canonical civil-house policy query. DEFAULT is input-only; explicit selectable-model identities and selectors share their codes. - Independent ecliptic and equatorial selected-target queries in C++ (
ephemeris::position/equatorial_position), C (celestial_positions/celestial_equatorial_positions) and Python (positions/equatorial_positions). Caller order and all-or-nothing results are preserved; no rates, civil conversion, houses or chart joint window run. Each provider's declared window still applies. - Independent civil houses through
celestial_house_query/ Pythonhouse_query, with resolved Delta T and nutation keys. C++house::axesprovides the four principal angles without cusps; no separate C axes export. - Public
julian_day::Scale,Times,civil_to_ttandresolve_timesfor physical conversion, andchart::resolve_civil_timesfor the shared chart/civil-house admission policy.
Changed
calendar::Datetimenow takes a rawstd::chrono::durationas its date-plus-time argument (#379), validating it before nanosecond conversion. ReplaceDatetime{day, hh_mm_ss{elapsed}}withDatetime{day, elapsed}; an existing validhh_mm_sscan supply.to_duration(). The fraction and time-point constructors remain.to_fractionretains negative and multi-day inputs but rejects non-finite inputs and nanosecond counts outsideint64_t.- Jieqi JSON export uses schema
celestial-calendar/jieqi-table@2(#380): replaceunix_ms/iso_utcwithjd_ut1/iso_ut1. It keeps model UT1 without a UTC suffix or a claim of accuracy relative to UTC. Export now requires a verified build receipt; dirty-source builds record a snapshot digest and no source commit. - The 1.0 C ABI uses
celestial_exports,Celestialrecords and oneCELESTIAL_TARGET_*family, without 0.x forwarders or universal V1 suffixes. Six Delta T exports become one model-selectingcelestial_delta_t. Layouts/signatures are frozen within a major; incompatible changes need a parallel shape or new major. - Error kind is authoritative even with an empty message. Both error readers preserve state; other exports
clear or record it. Native invalid input maps to Python
ValueError/ JavaScriptRangeError; computation/resource/internal and malformed-success failures use library errors. - Automatic error logging and its C/Python/JavaScript configuration APIs are removed. JavaScript Sun distance is
distanceAu. Sized UTF-8 strings count NUL and support zero-capacity sizing, truncation and termination. - Chart/civil-house admission shares
[1885, 2100), strict UTC from 1972 and Algo4 derived-TT< 2464328.5before reverse conversion. House does not inherit the chart's joint target/rate window. The Algo4 policy excludes the final roughly 71.5 UT1 seconds of 2034; common time/model errors may now precede a chart joint-window error. - Finite-input position evaluation checks computed coordinates without promising physical plausibility or accuracy; computed failures use computation errors. Pluto/node hard windows are unchanged. No accepted numerical value changes; invalid-input acceptance/messages and error classes intentionally change as described in Migrating to 1.0. New equatorial reference tests use separate implementation-conformance and physical-regression thresholds; see Validation.
- The private WASM ABI follows the C migration; no public JavaScript position, equatorial, house, event or capability query is added.
Fixed
- Model identity filtering no longer assigns a vector from its own filtered iterators (#378). Each kind retains the order and count of the corresponding subset of ALL.
- Original year, month and day values are checked before chrono narrowing (#379), including floating-point
years, leap days and
uint32_tmonth/day values from C. Years accept chrono years, floating-point values and integral types with at most 64 value bits. Invalid durations and time points fail before unsafe conversion. - Test discovery and execution reject missing directories, incomplete or empty registrations, command failures
and zero actual GoogleTest executions, including under
python -O(#381). - Wheel builds use cibuildwheel 4.3.5 with hash-locked virtualenv 21.14.13, fixing the Bash prompt-escaping regression reported in GHSA-cx6f-vjhg-g3cc. These are build tools, not package runtime dependencies.
[v0.9.0] - 2026-10-19
Added
- C ABI interval longitude events (#303):
longitude_events,longitude_stationsandlongitude_difference_eventsover theEVENT_TARGET_*codes (fourteen identities, pinned equal to the chart targets inc_header_check.c). One count+fill call owns the results:event_countalways reports the total, caller-allocated slots receive the chronological prefix, truncation is observable, and a zero total is a legal no-event outcome told apart from failure bylast_error. No existing struct layout or export changed. - Python interval longitude events (#303):
longitude_events(),longitude_stations()andlongitude_difference_events()mirror the enums, event fields, units, time scales and failures in frozen dataclasses, validate intervals, target angles, body pairs and capacities before native execution, and raiseCelestialErroron native failure. Angles carry no aspect names or orb policy. Each query runs a native count pass and fill pass; a 50-day Mercury window costs milliseconds. - C++ half-open interval searches for supplied longitudes, stations and directed longitude differences across all fourteen ephemeris targets (#301, #304). Queries preserve signed-angle orientation, recognize exact numerical stationary contacts, and throw on unresolved isolation, refinement or work limits rather than return partial lists. The event guide specifies domains, limits and the conditional floating-point contract.
Changed
- The private WASM ABI mirrors the expanded C surface (#303): the export list, binding metadata, layouts, manifests and both ABI oracles carry the three event exports and their record layouts. No public JavaScript event API is added; JavaScript remains outside the event surface.
- C++ input rejections consistently throw
std::invalid_argument(#345), including unsupported dates or years, unknown models, invalid Jieqi indices and angle division by zero. Callers that caughtstd::out_of_rangeorstd::runtime_errorfor these inputs must update their handlers. Numerical convergence and consistency failures remainstd::runtime_error; numerical results, C ABI layouts and binding error behavior are unchanged. - Legacy solar-longitude root searches select their angular branch from the year's actual initial longitude, fixing wrong-branch roots near the upper supported year without changing its input domain or existing tolerances.
[v0.8.0] - 2026-10-17
Added
- Fixed-version
chart_snapshot_v1C ABI and Pythonchart_snapshot(#299), returning owned states for fourteen targets (ten bodies and four lunar nodes), modelled UT1/TT times, four axes and twelve cusps. Explicit UTC/UT1 input and house/model selectors retain the C++ complete-snapshot domains; nodes have zero latitude and absent distance. Python copies the result into frozen dataclasses and tuples, with installed typing and wheel payload checks. ItsCivilScale,HouseSystemandChartTargetselectors are string enums, asDeltaTModelalready is (#344); bare strings are rejected. - Private native/WASM ABI checks for nested records and bounded arrays, preserving all earlier layouts and exports (#299). The complete snapshot raises the raw-WASM cap to 1,300,000 bytes and the npm tarball cap to 800,000 bytes.
- JavaScript
chart.snapshot(civil, options)returns the same snapshot (#327).optionstakes an explicitscale("utc"or"ut1"),locationand housesystem, and an optional ΔTmodel; omittingmodelselects the default model. The result owns its values, independent of later calls and WASM memory growth; node distance isnull. Unknown option keys and an inheritedmodelthrow instead of falling back to a default. - A C++
astro::chart::calculateAPI for an explicitly scaled UTC/UT1 civil instant (#300). It returns geocentric positions and longitude rates for the same fourteen targets, modelled UT1/TT values and house axes/cusps. Nodes have no distance; invalid complete-snapshot inputs throw. - C++ enum selection of the five ΔT models, defaulting to
Model::ALGO5(#300). - A C++
astro::ephemeris::longitude_rateAPI for the Sun, Moon, Mercury through Pluto, and four lunar nodes (#306). It returns signed degrees per TT day using seven-point centered differentiation within finite domains and rejects incomplete or unrepresentable stencils. Descending-node rates equal their ascending-node rates. - Four C++ lunar-node longitudes from
lunar_node.hpp: mean and Meeus five-term true ascending nodes plus their descending antipodes (#305).positionaccepts JDE(TT), returns true-ecliptic and equinox-of-date longitude, and keeps the definition's exact-zero latitude implicit while physical distance is absent. - Ascendant, Descendant, Midheaven, Imum Coeli, and twelve house cusps for Equal, Whole Sign, and Placidus in the C++ header-only API (#59). The calculation keeps UT1 and TT explicit, accepts east-positive longitude, and rejects geographic poles, an Ascendant direction unresolved at round-off, and the Placidus polar domain instead of silently substituting another house system.
- Apparent geocentric ecliptic positions for Mercury through Neptune (#58), calculated from the complete VSOP87D planetary series with iterative light time, annual aberration, FK5 reduction, and nutation. Light time evaluates each planet at emission time and carries it onto the observation date's mean ecliptic axes before subtracting Earth, so both positions share one frame (#322). A wrap-aware centered-longitude marker reports whether each planet is retrograde without claiming an exact stationary instant.
- Target-neutral illumination geometry and observer-facing planetary separation, phase angle, and illuminated fraction, with the existing Moon illumination API retained as a thin wrapper (#60).
- Gregorian-year searches for planetary longitude conjunctions, oppositions, quadratures, spherical greatest
elongations, and exact longitude stations (#60). Results are ordered JDE(TT) events owned by half-open UTC year
boundaries; the
[1, 32766]input domain is not a remote-date ephemeris accuracy guarantee. - Pluto in the existing C++ position, illumination, retrograde, and outer-planet phenomena APIs (#298), using the
lower-precision Meeus Chapter 37 model. Apparent positions and geometry accept
[2409543.5, 2488069.5)JDE(TT), centered retrograde checks accept[2409544.0, 2488069.0), and complete event searches accept Gregorian years[1886, 2098]; these hard domains are not accuracy guarantees.
Changed
- C++ headers include what they use (#344, #349).
jieqi.hppno longer includesastro.hpp, anddatetime.hppno longer includes<functional>or<cstddef>; code that relied on these transitive includes must include the headers it uses.src/util/util.hpp, whichjieqi.hppand the algo1/algo3 lunar headers included, is removed: includeymd.hpp,hash.hpporcache.hppdirectly.src/util/random.hpp, used only by the tests, moves tosrc/test/. std::hash<calendar::Datetime>, theutil::hash::IsTupleconcept and the single-tupleutil::hash::hashoverload are removed (#344). The library never called them; hash a tuple withutil::hash::TupleHash, and give aDatetimeits own hasher.
Fixed
- Default C++ ΔT datetime evaluation delegates to
Model::ALGO5(#321), avoiding the out-of-range next-year construction at year 32767. It now rejects invalid datetimes; UT1/TT and pre-1972 UTC conversions inherit that rejection. - C and C++ entry points that take an integer year check it before converting it to
std::chrono::year(#332). An unrepresentable year passed tout1_to_jd,ut1_to_jde,apparent_solar_time,gregorian_to_lunar,solar_lon_root_discriminantorsolar_lon_roots, or to the C++ Sun longitude searchesastro::sun::geocentric_coord::math::{discriminant,find_roots}, could alias to another year and return that year's result. These calls now fail, and the error names the original year. Annual searches accept[1, 32766], and the C++ searches throwstd::invalid_argumentoutside it; single dates still accept year 32767. The Python and JavaScript packages already rejected such years before calling native code. - Every Python function that takes an enum raises
ValueErrorbefore any native call for an object of that enum class that is not one of its declared members (#352), aschart_snapshot()already did. Such an object, built by bypassing the enum constructor, raisedKeyErrororCelestialError, or ran as the member whose value it carried.
Errata
These notes correct entries of earlier releases. Where an entry was edited after its release, the CHANGELOG at that release's tag and its GitHub Release notes still show the earlier text.
- v0.7.0: "The npm tarball grows from nine to twelve members" should say eight to twelve. The v0.6.1 tarball had
eight members;
THIRD_PARTY_NOTICES.txtwas added after v0.6.1 (#257) and first shipped in v0.7.0, together withdate.mjs,validation.mjsanddate.d.ts. The entry also omits that v0.7.0 raised the npm tarball cap from 250,000 to 300,000 bytes (#267). - v0.6.0: after the v0.7.0 release, #288 changed four counts in that section without a note. "seven" recording
exports became 28 in three places, and "31 edge/error cases" became 30. The new numbers match the v0.6.0 tag:
its ABI manifest marks 28 of the 29 exports as recording, and
public_api_test.mjsasserts 30 edge cases. - v0.5.0: two entries were rewritten after the v0.5.0 tag and before v0.6.0. The WASM golden file was
toolbox/wasm_golden.jsonin v0.5.0; #221 renamed it totoolbox/bindings_golden.jsonand changed the entry to the new name. The "Release downloads now fail loud" entry now describes the v0.6.0 release process (rewritten by #218 and #235). In v0.5.0, artifacts had to come from "a successfulbuild_and_testrun of the exact tagged commit", and a single failed artifact download failed the whole download. - v0.0.0: #120 reworded the lunar-conversion note in the past tense; the 1901–2099 range is unchanged.
- Heading dates: v0.6.0 is dated 2026-08-17, but its tag commit is dated 2026-08-23 and the GitHub Release was published on 2026-08-24 (UTC). v0.0.0 is dated 2024-08-01, but its tag commit is dated 2024-08-03. The v0.7.0 heading has no date; its tag commit is dated 2026-09-16.
[v0.7.0] - 2026-09-16
Changed
- JavaScript namespace methods called before
init()completes now throwCelestialErrorwith the public operation andrecorded = false, preserving the message and precedence over argument validation.init()still resolves toundefined, shares its promise, and preserves original loader rejections and retry. - Project-authored material is now licensed under MIT. Retained third-party material keeps its source terms and remains
outside the project MIT grant;
THIRD_PARTY_NOTICES.txtand the source-tree attribution boundaries identify the applicable exceptions. - Lunar conversion documentation now states each algorithm's actual civil-date basis instead of one universal UTC+8 history: algo1 preserves HKO labels, algo2 applies its three-era UTC model followed by a fixed east-eight offset, and algo3 is their baked hybrid. Jieqi civil output is explicitly UT1 rather than an east-eight wall date, and Delta T profiles identify each model's source, frozen/current status, hard domain, and selection role without presenting test residuals as statistical error guarantees.
- Rise/set's name-carried time-scale contract is complete: the nine public UT1/TT parameters now use
ymd_ut1,transit_jde_tt,t0_jde_tt, ort1_jde_tt, andResult::{rise_jde,transit_jde,set_jde}becameResult::{rise_jde_tt,transit_jde_tt,set_jde_tt}without compatibility aliases. - JavaScript's
CivilDateis renamed toGregorianDate.GregorianDate,CivilDateTime, andLunarDateare independent records with mutually excludedfraction/isLeaptags; runtime validation also rejects these tags when inherited or explicitlyundefined. Civil and lunar results can no longer pass silently as Gregorian inputs tolunar.fromGregorian(). - JavaScript
JieqiMomentis now{ jieqi, momentUt1 }, replacing the flatindex/year/month/day/fractionshape.jieqi.moment()andjieqi.name()take theJieqiliteral value union in TypeScript, not a generalnumber.time.jdeToUt1(),sun.apparentSolarTime(), and nestedmomentUt1now returnCivilDateTimeResult, which extends the unchanged four-field civil input with integerhour/minuteand fractionalsecond. Inputs still readfraction; the derived clock fields do not impose millisecond rounding. - JavaScript
sun.apparentGeocentricCoordinates()andmoon.apparentGeocentricCoordinates()are renamed to singularapparentGeocentricCoordinate(), including theirCelestialError.operationnames. The old method and type names have no compatibility aliases. - JavaScript lunar-year validation now queries the native algorithm's range once per year-consuming call instead
of keeping a duplicate range table. Out-of-range lunar years still throw
RangeError; a native range-query failure throwsCelestialErrorwith the caller's public operation name. - The npm tarball grows from nine to twelve members for the pure date bridge, shared validators, and date declarations.
- Python
solar_longitude_roots()is renamed tosun_longitude_crossings(), including itsCelestialError.operationname, without a compatibility alias. Other flat function names are unchanged. - Python lunar-year validation now queries the native range once per year-consuming call instead of keeping a
duplicate range table. Out-of-range lunar years still raise
ValueError; native range-query failures raiseCelestialErrorwith the caller's public operation name. - The Python package README now documents algorithm-specific lunar date bases, the Jieqi UT1 contract, and Delta T
model profiles. Python's four-field
CivilDateTimeand nestedJieqiMomentare unchanged.
Added
- The
celestial-calendarnpm alias forwards the root and/dateentries to the exact same-version@0xf3cd/celestial, without bundling another WASM module. Publishing classifies the two package identities separately and permits a targeted npm retry with the same frozen candidate after fresh byte verification. - Chinese-first root and package documentation, with
README_EN.md, runnable date/Jieqi examples, and links to one canonical native runtime table and license section. The Python guide includes a local-wheel recipe; native C/C++ guidance distinguishes source headers from prebuilt C ABI archives. - Installed consumer checks for public Jieqi names, logging/default-model routing, longitude endpoints and TypeScript declarations; browser reference checks now run in Chrome and Firefox. Browser-download digests are recorded as non-blocking observations, not compared with expected hashes.
- JavaScript exports a frozen
Jieqiobject with 24 named constants, using the Python spellings fromLICHUN = 0throughDAHAN = 23, and a corresponding TypeScript0-through-23union. @0xf3cd/celestial/dateexportsdateToCivilUtc(),civilUtcToDate(),dateToCivilAtOffset(), andcivilAtOffsetToDate()withoutinit(), WASM, or runtime dependencies. The bridge does not supply IANA-zone, UT1/TT, or Date-to-JDE conversion. Seebindings/javascript/README.mdfor the date bridge contract.- Python adds the static
GregorianDate.from_date()factory andGregorianDate.to_date();gregorian_to_lunar()also accepts standard-library date-only values. Date subclasses are accepted, but alldatetime.datetimevalues are rejected. The bridge preserves date labels without timezone or time-scale conversion;GregorianDateremains wider than the standard-library date domain. Seebindings/python/README.mdfor the full contract.
[v0.6.1] - 2026-08-24
Fixed
- Python wheels now include the PEP 561
py.typedmarker, so type checkers consume the annotations already present in the public package. Wheel verification and a clean-installed mypy consumer both pin the contract.
[v0.6.0] - 2026-08-23
Added
JavaScript / TypeScript
@0xf3cd/celestial(#182): one ESM package now carries the complete 29-exportcelestial.hsurface behindconfig,time,sun,moon,jieqi, andlunar. Import is side-effect free; one explicitawait init()loads the package-owned WASM, and calculations stay synchronous afterwards. The hand-written declarations use string unions rather than runtime enum objects, keep JD/JDE/UT1 and units visible, and accept neitherDatenor implicit coercions.- JavaScript validates shape, finiteness, integer width, and each operation's domain before entering WASM. Bad input throws
TypeError/RangeError; native failures becomeCelestialError, and only the 28 recording C exports may contribute a native message. Legitimate absence remainsnullor[].
Python
celestial-calendarnative wheels (#211) expose the complete 29-export C ABI as a flatcelestial_calendarAPI built from enums, frozen dataclasses, scalars, tuples, andCelestialError; ctypes structs, count/fill protocols,last_error, and model-specific C symbols remain private. Inputs are checked before native calls, including a 4096-result cap onnew_moons_after.- One
py3wheel is built and clean-installed on floor/current Python for manylinux_2_28 x86_64/aarch64, macOS 14 arm64, and Windows AMD64. The ABI gate reconciles 29 signatures, 16 layouts, and 28 recording exports; every platform also replays the same 389-point native golden dataset as the WASM leg. Release collection requires exact duplicate-free inventories and publishes the same four wheel bytes to PyPI and GitHub Release, with SHA-256 sidecars on GitHub; no sdist is built.
Packaging and Verification
- The WASM recipe exports all 29 C entries plus internal allocation helpers and allows memory growth for the package singleton's bounded, never-evicted calendar caches. A test-only ABI oracle reconciles all 29 signatures, 16 struct layouts, and 28 recording functions with the header, implementation, binding table, and built module.
toolbox/build_npm.pyproduces one eight-file tarball whose version comes fromproject.py, with a 250,000-byte package cap and a 465,000-byte raw-WASM cap. The independent WASM leg checks 22 public methods, 30 edge/error cases, the shared 389-point native golden dataset, exact-tarball installation under Node 22 and the current Node, TypeScript declarations, and an Astro/Vite production build in Chrome. Thecelestial-wasmrelease artifact keeps the raw module and adds the exact tarball plus pack JSON/SHA-256 sidecars.- The emsdk checkout is pinned to a commit included in the cache key (#220). Before publishing, the release collector verifies the WASM archive's exact members, npm package identity, and tarball sidecar; it separately verifies each native archive's members, build version, and runtime-library hashes.
release_downloader.pyrepeats the archive-internal checks after download without comparing against the caller's checkout documentation. - The JavaScript package's acceptance suite now pins the inclusive endpoints of the civil, calendar, Jieqi, longitude, ΔT, and lunar-algorithm domains (#223). The ABI oracle proves its own export, signature, layout, and recording checks with directed mutations, while an inventory keeps every ABI, Node, browser, and TypeScript test entry connected to the workflow, runner, or configuration that executes it.
- Registry publication freezes one candidate before any irreversible workflow step. GitHub Release publishes first; a protected OIDC job sends its exact four wheels to PyPI, while npm either publishes the exact tarball through OIDC or proves the v0.6.0 bootstrap byte-identical. An unprivileged verifier then checks registry inventories, SHA-256, npm SHA-512 integrity, downloaded bytes, and clean installs. Nothing is rebuilt in the release workflow.
Changed
- Jieqi moment APIs now share the effective Gregorian-year domain
[401, 32766]across C++, C, Python, and JavaScript (#219).jieqi_ut1_momentrejects earlier years withstd::out_of_rangebefore calculating or caching a JDE; the lower-leveljieqi_jdeand its cache retain their[1, 32766]domain.
[v0.5.0] - 2026-08-15
Added
Astronomical Calculation
- Precession (#57):
astro::earth::precessioncarries both routes of Meeus ch.21 — the equatorial angles ζ/z/θ (21.2) with the rigorous transform (21.7), and the ecliptic η/Π/p with (λ₀, β₀) — andEclipticCoord{λ, β}joins the coordinate family. Verified three ways at 1e-6°: book Examples 21.a and 21.b, pymeeus cross-datasets (30 points per system, seed 42), and 12 points againsterfa.pmat76, whose IAU 1976 matrix route is an independent implementation of the same transform. Both cross-checks measured ~1e-14°, so the tolerance is set by the book's printed digits, not by the agreement. - Atmospheric refraction (#61):
astro::earth::refractionimplements Bennett (Meeus 16.3, from apparent altitude) and Saemundsson (16.4, from true altitude) under one T/P correction,R(T,P) = R · (P/1010) · (283/(273 + T_c)), withat_horizonfor the horizon case.rise_set::sun::h0_from(params)returns −(horizon refraction + 16′); the defaults — 15 °C, 1013.25 hPa, Bennett — reproduce the previousSTANDARD_ALTITUDE, so existing signatures and results are unchanged. - General rise/transit/set (#62):
sunrise_sunset.hppis generalized intorise_set.hpp, whose engine takes aBodyProvider(jde_tt → equatorial coordinates) plus h₀ and knows nothing about which body it is solving for; solar and lunar conventions live inrise_set::sunandrise_set::moon. Transit rides ch.15's m₀ secant form with dα/dt measured from the provider rather than a per-body rate constant, and existence is settled by scanning the day on a 96-cell (15-minute) grid, probing the edge cells, and golden-section-refining each candidate extremum — rather than assuming the extremum sits at H = ±180°, an assumption with a blind band whenever dδ/dt ≠ 0. Polar topology moves into the type (Polar{NONE, DAY, NIGHT}) andtransit_jdebecomes optional, since a lunar UT day can have none. The Moon uses UT-day windows to match almanac practice and h₀ = 0.7275·Π − refraction, held to 181 USNO rows at ±2 min — five sites over 1999–2049, plus Tromsø's polar and double-event days and the routine dates on which the Moon simply does not rise. - Moon phase (#33, #185):
phase_momentssolves the four principal phases (Sun–Moon elongation 0°, 90°, 180°, 270°),position_anglegives the bright limb's position angle (48.5), andmoon_illuminationreturns the illuminated fraction with the elongation it came from (48.1–48.3). Meeus Example 48.a prints χ = 285°.0; this library's VSOP87D + truncated ELP2000-82B route gives 285.0442°, pymeeus 285.04435°.
C ABI & Downstream
- Lunar converter exports (#128, #129, #130): the year-level C entry points accept
algo3(1600–2199) alongside 1 and 2, andTraditionalMonthwith the failablemonth_position/month_at_positionpair makes the two month numberings convertible in both directions — the helper rejects the mismatches (aleapflag on a non-leap month, a leap month in a year that has none) that a silent mapping would have swallowed. The converter's own doc blocks now say plainly that its "month" is a position index in which a leap month occupies its own slot. - Two new exports (#185):
moon_illumination(jde)for the illuminated fraction and elongation, andlocal_apparent_sidereal_time(jd_ut1, longitude)— the first C outlet for theastro::sidereallayer, which was already complete in C++. - WASM artifact (#163, #185, #187):
toolbox/build_wasm.pybuilds a browser/Node module withem++ -Oz -fwasm-exceptionsfrom one recipe shared verbatim by the manual and CI paths. The shipped module exports the jieqi family, the Julian Day conversions,moon_illumination,moon_position_angle,moon_phase_moments,local_apparent_sidereal_timeandlast_error.toolbox/bindings_golden.json(schema@2) holds it to the native build across five sections totalling 389 entries — jieqi 204, moon 41, sidereal 43, position angle 41, phases 60 — andwasm_check.mjschecks value agreement, the sret struct layout, the exception path and module size, on a CI leg of its own with emsdk pinned to 6.0.6. Releases ship the module alongside the platform packages. - Jieqi table JSON export (#164):
toolbox/jieqi_table.pyturnsquery_jieqi_momentinto one static table (default 1950–2051, the last year being tail margin so every 1950–2050 moment keeps its successor in-table). Entries are{year, idx, name_zh, unix_ms, iso_utc}, sorted by moment rather than by ABI index; the timescale is stated as modelled UT1 with the per-era distance from UTC written into the table's own metadata. Held by a./linter.py --jieqi-tablegate: count, strict ordering, year containment, the idx ↔ name mapping against an independent transcription, determinism, re-derivation through the second ctypes binding, and HKO almanac anchors.
Changed
algo2's supported window narrows from [410, 5000] to [410, 2500] (#139). The old upper bound was a convention rather than a physical limit, and honouring it meant returning an answer that is essentially random at the far end. The new bound follows an error budget from three sources: past 1972 the ΔAT table is frozen at 37 s and future leap seconds are physically unknowable, so the systematic offset against real future time is ΔT − 69.184 s, with the extrapolation uncertainty taken from NASA's ΔT catalogue (612 s at 2500 against 12341 s at 5000); before 1972 the Morrison–Stephenson parabola governs; across the whole window the VSOP/ELP truncation and mean-element extrapolation add a second-order term.AngleandDistance(#48, #53): the value member becomes private and drops itsconst. Aconstmember expresses non-assignability, not immutability — immutability was already carried by the interface, since every operator isconstand returns a new value and there are no setters — while costing regularity:std::optional<Angle<DEG>>was already instantiated and only escaped breaking because no consumer had written an assignment. The member did not become public in exchange: a bare value outside itsUnitcannot be read correctly, andas<>()/deg()/rad()state the unit at every read. Angle spellings are unified across the library in the same pass.- Export surface closed to the published C ABI (#91): explicit
CELESTIAL_APIon everycelestial.hentry point, hidden default visibility,SOVERSION= major.minor while 0.x, and release packaging viacmake --install— with a CI gate holding the exported symbol set to the header's entry points. Release archives change shape: the library now sits underlib/(bin/on Windows, with the import library inlib/) and the header underinclude/; the JSON build metadata stays at the top level. - Release downloads now fail loud (#72, #211): artifacts must come from three explicitly selected, successful dispatches of the exact protected tag commit; duplicate, missing, extra, colliding, or failed downloads stop the release. The release workflow validates the selected run IDs and main ancestry before freezing its candidate.
- Logging is opt-in: the default verbosity is
NONE, so loading the library no longer writes to the host's stdout unless asked. - Toolchains: the Linux legs move from clang 18 to clang 22 (ubuntu-26.04) and Windows from choco LLVM 20.1.8 to 22.1.7, with clang-tidy pinned by the runner image it ships with.
Fixed
- Jieqi year guard (#154):
calc_jieqi_jdehad no year check, andto_ymd'sstd::chrono::yearis ashort. Three shapes were measured before the fix —calc_jieqi_jde(65537, 春分)did not throw and returned year 1's JDE, a silently wrong answer;jieqi_jde(65537, …)andjieqi_jde(1, …)returned the same value, because the cache keyed on the untruncated year while storing the truncated answer, poisoning the entry and leaving the key space unbounded; andcalc_jieqi_jde(100000, …)wrapped around into the lower-bound guard and reported "The year -31072 is < 1". The input is now rejected asout_of_rangebefore any of that. - The last UT1 residue at year boundaries (#115): #84 moved
moments()' year boundaries onto the leap-second-aware path, but the same pattern remained insun::find_roots' year window and inJieqiGenerator's start-year inference. Both now resolve through UTC, and the UT1/UTC model gap is documented at the points that consume it. No numerical consequence — the gap stays orders of magnitude below the ~4-day jieqi/New-Year clearance. normalize_deg/normalize_radkeep their documented half-open range (#88). One comparison,_deg < 0.0, was asked to hold three things at once and held none: a small negative input could return exactly 360.0, becauserem + 360.0rounds up onceremfalls withinulp(360)/2, and a whole-period negative input returned −0.0, because-0.0 < 0.0is false and the sign bit rode through untouched.algo2now enforces the window it advertises (#140). It was the only one of the three lunar algorithms without a year check —algo1andalgo3both throwout_of_range— whileget_supported_lunar_year_rangeadvertised its bounds through the C ABI. Measured on the release build before the fix:get_lunar_year_info(2, 409)andget_lunar_year_info(2, 5001)both returnedvalid=true.- Header self-containment and the ODR ground beneath it (#71): namespace-scope
constexprhas internal linkage, so every translation unit was getting its own copy of the coefficient tables; they now have external linkage. Includes are audited per header and the self-containment gate runs on more than one platform, since a header leaning on a transitive include passes on one standard library and fails on another. hash_combine's finalizer (#149): the oldv_hash *= 0x9e3779b9only propagated low bits upward and was 32 bits wide — flipping input bit 63 flipped 7.7 output bits on average against an ideal of 32. It is now a splitmix64-shaped step, an xorshift bracketing a 64-bit golden-ratio multiply, which measures 27.8 at minimum; changing the constant alone was not enough (21.6), so the constant and the xorshift moved together.
[v0.4.0] - 2026-07-29
Added
Astronomical Calculation
- Leap-second-aware UTC time scale: the ΔAT table (28 entries, machine-checked against pyerfa),
utc_to_tt/tt_to_utcconversions, a UTC Julian Day family, and carry-awareadd_seconds; year/month boundary enumeration switched from its UT1 proxy to true UTC — a 1972–5000 scan surfaced 12 year-attribution flips (all in 3312+), pinned by directed goldens. - Equation of time & local apparent solar time (Meeus ch. 28):
equation_of_timeon the apparent-right-ascension route (28.1) with the (28.2) mean-longitude polynomial, andapparent(utc, longitude)riding the new UTC machinery. Verified against five independent families — book example 28.a, pymeeus internals (worst 0.0007 s), a Skyfield/DE440s definitional oracle (worst 0.22 s over 1900–2100), NOAA solcalc formulas, and the in-repo transit route (worst 0.17 s) — plus exact-rational τ = ±1 anchors pinning the high-order polynomial terms.
Calibration
- Moon golden regression against JPL Horizons/DE441: 41 epochs × 3 columns with banded tolerances. The long-standing lunar-distance doubt is closed: worst residual 45.689 km ≡ the ELP2000-82B truncation envelope (30–46 km, flat across ten millennia) — zero transcription bias.
- Sun & solar-term golden regression: 43 Horizons epochs (core band 0.105″ / 0.018″ / 1.4e-8 AU), and dual-axis jieqi verification — 76 DE441 instants in pure TT (worst 2.76 s) × 168 HKO almanac values through the full chain including ΔT (worst 0.525 min ≈ table rounding). The solar-term surface consumed by downstream calendrics is now verified to the second.
C ABI
- Published C header
celestial.h: every export with its documented error contract, struct-layout_Static_asserts, and a pure-C compile check in CI; release artifacts now ship the header. Newequation_of_time/apparent_solar_timeexports, and a thread-locallast_errorpilot on the Julian Day exports.
Fixed
- C-ABI boundary hardening: exceptions can no longer cross
extern "C"(catch-all handlers →valid = false); inputs are validated before narrowing casts with NaN-proof range guards, replacingasserts that are dead code in Release builds;GLOBAL_VERBOSITYis atomic; the lunar-algorithm bounds cache initializes lazily instead of running a full astronomical pipeline during library load. jd_to_ut1upper-bound guard, closing a JD →uint32overflow path.
Changed
- Retired legacy comparison datasets lacking provenance or mixing time scales (TAMU / timeanddate diff tests, UT1-labelled jieqi rows) in favor of the external-authority goldens above.
- Windows note in
celestial.h: the UCRT fail-fasts (0xc0000409) on writes to a closed stdout — a process kill, not a catchable exception.
[v0.3.0] - 2026-07-27
Added
Astronomical Calculation
- Sunrise & sunset line (Meeus, "Astronomical Algorithms" 2nd ed.): mean/true obliquity (ch. 22), ecliptic ↔ equatorial → horizontal coordinate transforms (ch. 13), GMST/GAST/LAST sidereal time with strict UT1/TT separation (ch. 12), apparent solar equatorial coordinates (ch. 25), and
sunrise_sunset.hpp— sunrise/sunset/upper transit, civil/nautical/astronomical twilight, and polar day/night detection (ch. 15). - End-to-end golden regression against external authorities (USNO rstt API × NOAA solcalc × Skyfield/JPL DE421): 143 pinned values across 7 sites × 4 seasonal dates including Tromsø polar cases; measured worst residual 0.55 min against the ±2 min contract.
- ΔT algo5, now the default: a single polynomial fitted on IERS Bulletin A observations (2005–2026.4) anchored to the Stephenson–Morrison–Hohenkerk long-term curve for extrapolation — removes the 2035 prediction cliff of the previous default.
Fixed
- Newton root finder returns the best-residual iterate instead of the last one, fixing silently wrong roots beyond JD 2²² (year 6771+: 2357 wrong sun-longitude roots in 6772–9999 → 0) and a silent truncation of moon-phase enumeration; bracket construction refined (margin 0.67° → 6.1°).
- ELP2000-82B coefficient transcription errors; aberration switched to Meeus (25.11) variable form.
julian_dayinput validation unified to a throw contract, with the 401 CE lower bound corrected.utilfunction cache rewritten thread-safe (TSAN data races 7 → 0); copies share the cache.
Changed
- CI resurrected after two years of bit rot: build matrix slimmed 8 → 2 with native ARM runners; clang-tidy gate aligned; automated AI code review on pull requests.
- Golden datasets now carry mandatory provenance (source, collection date, tolerance rationale) and regression tests are mutation-verified for detection power.
[v0.2.0] - 2024-08-17
Added
Calendar
- Supported more algorithms for lunar date conversion.
- Added more functions for Jieqi calculation.
- Used adaptive step size in Newton's method.
Utilities
- Supported hash and cache for arbitrary function, which is currently used in astro-calculation, jieqi calculation, and lunar date conversions.
Statistics
- Added a new notebook in folder
statistics:lunar_calendar.ipynbfor inspecting the accurate lunar month start moments, also for inspecting the differences between algo1 and algo2.
Test
- Implemented unit tests with GTest, covering lunar date conversions using different algorithms.
Linter
- Added
.ruff.tomlwhich configures ruff. Also change Python codes as per the linter suggestions.
[v0.1.0] - 2024-08-05
Added
Astronomical Calculation
- Added theory ELP2000-82B (truncated version, from Jean Meeus, "Astronomical Algorithms", Second Edition, Chapter 47).
- Supported calculations of the Geocentric Coordinate of the Moon, with corrections (perturbation, nutation, ...).
- Applied Newton's method to approximate the moments of Sun-Moon conjunctions (i.e. New Moons).
- Added other math utilities, e.g.,
Distance.
Statistics
- Added more notebooks in folder
statistics:moon_longitude.ipynbfor exploring Newton's Method on finding New Moon moments.new_moon.ipynbto compare calculated New Moon moments with other data sources.
Test
- Implemented unit tests with GTest, covering ELP2000-82B, Moon position calculation, and Sun-Moon conjunction moment prediction.
Automation
- Added
linter.pyto runruffandclang-tidy. - Created the
toolboxfolder, including:release_downloader.pyto download the assets from latest release.
[v0.0.0] - 2024-08-01
Added
Astronomical Calculation
- Supported conversions between JD (Julian Day) and UT1, and conversions between JDE (Julian Ephemeris Day) and TT.
- Supported calculations of Delta T.
- Supported conversions between UT1 and TT time scales.
- Supported calculations of the Heliocentric Coordinate of Earth.
- Supported calculations of the Geocentric Coordinate of the Sun, with corrections (FK5 system correction, nutation correction, etc.).
- Added other math utilities, e.g.,
Angle.
Statistics
- Added folder
statistics, for statistical analysis to evaluate different astronomical algorithms
Calendar
- Added
Datetime, a struct to hold a date and an accurate time, representing a UT1 or UTC moment. - Supported conversions between Lunar dates and Gregorian dates.
- At this release, only Gregorian years between 1901 and 2099 were supported — v0.2.0 brought more algorithms, and the README states the range each one covers.
- Applied Newton's method to approximate the moment when the Sun reaches a certain longitude.
- Supported queries of the Jieqi (节气) moments in given Gregorian years.
Test
- Implemented unit tests with GTest, covering core functionalities.
Automation
- Implemented the
automationPython package to manage the project in an automated manner. - Added
project.pyas the entry point for building and testing the project. - Created the
toolboxfolder, including:artifact_downloader.pyto download the latest shared libraries.build_info.pyto pack the platform, architecture, and shared lib info with a build.compiler_finder.pyto find the C and C++ compilers that satisfy a certain standard.