Something went wrong. Try again.
A Zig library for astronomical calculations: the Sun and Moon from anywhere on Earth, the Moon's phases and libration, and time scales.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! An angle, so that degrees cannot be read as radians or the other way//! round. It holds radians and costs nothing over a bare `f64`.//!//! Every angle a calculation returns is one of these. Construct one from//! whichever unit the value is in, and read it in whichever unit is wanted.
const std = @import("std");const math = std.math;
const Angle = @This();
/// The angle in radians. Read it through `radians`, `degrees` and the/// rest rather than directly, so that the unit is always said.rad: f64,
pub fn fromRadians(value: f64) Angle { return .{ .rad = value };}
pub fn fromDegrees(value: f64) Angle { return .{ .rad = value * math.rad_per_deg };}
/// From seconds of arc (″), the unit small quantities such as nutation are/// given in.pub fn fromArcseconds(value: f64) Angle { return fromDegrees(value / 3600);}
/// From hours of right ascension or hour angle: 24ʰ is 360°.pub fn fromHours(value: f64) Angle { return fromDegrees(value * 15);}
/// Degrees, minutes and seconds of arc, as 28°01′34.26″ is written. The/// sign is a field of its own, because −0°30′ has no negative part to carry/// it.pub const Dms = struct { negative: bool = false, degrees: f64, minutes: f64 = 0, seconds: f64 = 0,};
pub fn fromDms(dms: Dms) Angle { const magnitude = fromDegrees(dms.degrees + dms.minutes / 60 + dms.seconds / 3600); return if (dms.negative) magnitude.negate() else magnitude;}
/// Hours, minutes and seconds of time, as a right ascension of 7ʰ45ᵐ18.946ˢ/// is written.pub const Hms = struct { negative: bool = false, hours: f64, minutes: f64 = 0, seconds: f64 = 0,};
pub fn fromHms(hms: Hms) Angle { const magnitude = fromHours(hms.hours + hms.minutes / 60 + hms.seconds / 3600); return if (hms.negative) magnitude.negate() else magnitude;}
pub fn radians(self: Angle) f64 { return self.rad;}
pub fn degrees(self: Angle) f64 { return self.rad * math.deg_per_rad;}
pub fn arcseconds(self: Angle) f64 { return self.degrees() * 3600;}
pub fn hours(self: Angle) f64 { return self.degrees() / 15;}
pub fn negate(self: Angle) Angle { return .{ .rad = -self.rad };}
pub fn add(self: Angle, other: Angle) Angle { return .{ .rad = self.rad + other.rad };}
pub fn sub(self: Angle, other: Angle) Angle { return .{ .rad = self.rad - other.rad };}
/// The same direction, reduced to [0°, 360°): the range of longitudes,/// right ascensions and position angles. NaN and ±∞ give NaN.pub fn normalized(self: Angle) Angle { if (!math.isFinite(self.rad)) return .{ .rad = math.nan(f64) }; const r = @mod(self.rad, math.tau); // `@mod` of a tiny negative number can round up to τ itself. return .{ .rad = if (r >= math.tau) 0 else r };}
/// The same direction, reduced to (−180°, +180°]: the range of small signed/// quantities such as libration. NaN and ±∞ give NaN.pub fn signed(self: Angle) Angle { const r = self.normalized().rad; return .{ .rad = if (r > math.pi) r - math.tau else r };}