@openwaters/almanac
v0.4.2
Published
Offline sun & moon engine: positions, rise/set, moon phase, lunar and solar eclipses.
Downloads
842
Readme
Almanac
Offline sky engine for the Salish Sea and everywhere else: Sun and Moon positions, rise, set, twilight, Moon phase, lunar eclipses, solar eclipses for an observer, and fixed-star altitude and azimuth from catalog positions, computed from pure geometry with zero network and zero runtime data files.
Twin implementations, one behavior:
swift/— SwiftPM packageAlmanactypescript/— npm@openwaters/almanacfixtures/— the shared test corpus (JPL Horizons, USNO, Espenak) both suites must pass; the contract that keeps the ports identical
Supported interval: 1950-01-01T00:00Z ≤ t < 2101-01-01T00:00Z; results outside it raise a typed error. All instants are UT1-accurate, while unknown future DUT1 is outside the civil-UTC accuracy promise. See the public contract for the time model.
- Contract:
docs/CONTRACT.md, including coordinates, public behavior, accuracy, and fixture evidence. - Scope:
docs/ROADMAP.md, including supported behavior and deliberate boundaries. - Development and releases:
CONTRIBUTING.md, including pinned tools and required checks. - Landing page: openwaters.io/sky, with the library running live in a browser.
Algorithms translated from Astronomy Engine (MIT, Don Cross) — see NOTICE. MIT licensed.
Performance
Almanac includes a shared performance harness for both ports: 22 workloads cover positions, a 228-hour sky track, short/year/polar event windows, full-range moon phases, next/previous/range lunar and solar eclipse searches, including empty windows, and solar obscuration over a 228-hour track.
Event searches find altitude extrema with Brent's method and altitude crossings with a cosine-seeded secant solver, each root proven inside a half-second bracket, and the TypeScript Sun series evaluates its terms as straight-line arithmetic. Median query time relative to v0.4.1:
| Workload | TypeScript: less time | Swift: less time | | --- | ---: | ---: | | Sun events / year | 73.4% | 62.8% | | Moon events / year | 57.6% | 57.4% | | Sun events / 228 hours | 73.4% | 61.9% | | Moon events / 228 hours | 57.1% | 57.0% | | Sun positions / 1,024 hours | 35.5% | — | | Lunar eclipses / 1950–2100 | 24.6% | — |
Swift already evaluated the Sun series efficiently, so its gains are confined to the searches. Each comparison builds both revisions with the same harness and toolchain, then takes seven interleaved process pairs with 300 ms warmup per process. Build and startup time are excluded. Timings vary by machine; the shared correctness fixtures and parity tolerances remain the accuracy gates.
Reproduce the comparison locally from the repository root with mise 2026.9.1 or newer after installing the configured Node and Swift versions in mise.toml:
mise install
mise exec -- npm ci --prefix typescript
mise exec -- node benchmarks/run.mjs --base v0.4.1Pass --skip '^solar/' when the base predates 0.4.0, which is when the solar eclipse workloads arrived.
CI runs the harness on code changes and fails on median regressions over 20%. Results include timing tables, raw samples, checksums, and revision/toolchain metadata. See the harness guide for choosing a baseline, running one port, and inspecting reports.
Event searches scale with window length. Run a full 151-year sweep in a worker or background task; use shorter windows for interactive queries.
Usage
TypeScript
npm install @openwaters/almanacimport { nextLunarEclipse, lunarEclipseVisibility, nextSolarEclipse, solarObscuration, sunEvents, starAltAz } from '@openwaters/almanac';
const observer = { latitudeDeg: 48.5, longitudeDeg: -123.0 };
const eclipse = nextLunarEclipse(new Date());
const visibility = lunarEclipseVisibility(eclipse, observer);
console.log(eclipse.kind, eclipse.peak, visibility.visibleAtPeak);
const solar = nextSolarEclipse(new Date(), observer);
console.log(solar.kind, solar.peak, solar.obscuration, solar.sunAltDeg.peak);
console.log(solarObscuration(solar.peak, observer)); // fraction of the Sun's disc covered, 0 to 1
const today = new Date();
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
for (const { kind, time } of sunEvents(today, tomorrow, observer)) {
console.log(kind, time.toISOString());
}
// Betelgeuse from its J2000 catalog position: az/alt in degrees, refracted.
const { azDeg, altDeg } = starAltAz(88.792939, 7.407064, today, observer);Swift
.package(url: "https://github.com/openwatersio/almanac.git", exact: "0.4.0")import Almanac
import Foundation
let observer = try Observer(latitudeDeg: 48.5, longitudeDeg: -123.0)
let eclipse = try nextLunarEclipse(after: Date())
let visibility = try lunarEclipseVisibility(eclipse, observer: observer)
print(eclipse.kind, eclipse.peak, visibility.visibleAtPeak)
let solar = try nextSolarEclipse(after: Date(), observer: observer)
print(solar.kind, solar.peak, solar.obscuration, solar.sunAltDeg.peak)
print(try solarObscuration(at: solar.peak, observer: observer)) // fraction of the Sun's disc covered, 0 to 1
let today = Date()
let tomorrow = today.addingTimeInterval(24 * 60 * 60)
for event in try sunEvents(from: today, to: tomorrow, observer: observer) {
print(event.kind, event.time)
}
// Betelgeuse from its J2000 catalog position: az/alt in degrees, refracted.
let star = try starAltAz(raDeg: 88.792939, decDeg: 7.407064, at: today, observer: observer)Eclipse searches
Search for a previous eclipse or all eclipses in a time window:
import { previousLunarEclipse, lunarEclipses } from '@openwaters/almanac';
const last = previousLunarEclipse(new Date());
const eclipses = lunarEclipses(new Date('2026-08-24T00:00:00Z'), new Date('2026-09-02T12:00:00Z'));let last = try previousLunarEclipse(before: Date())
let eclipses = try lunarEclipses(from: today, to: tomorrow)Ranges include peaks at the start and exclude peaks at the end. Contacts may
extend outside the range. Previous/next searches skip peaks within 100 ms of the
anchor. Search results are global; apply lunarEclipseVisibility for an observer.
Solar eclipses are searched for an observer, because their contacts only exist for a place: nextSolarEclipse(after, observer), previousSolarEclipse(before, observer), and solarEclipses(startUtc, endUtc, observer). An eclipse whose Sun is below the horizon at C1, the peak, and C4 is not returned.
