// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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 }; }