diff --git a/Cargo.lock b/Cargo.lock index 7edef5b..eb75e68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,3 +207,6 @@ version = "0.1.0" [[package]] name = "apsis-units" version = "0.1.0" +dependencies = [ + "apsis-core", +] diff --git a/README.md b/README.md index 6a4fa03..f810711 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs 约束: -- 依赖只能从左向右 +- 依赖只能从左向右,禁止反向依赖 - 禁止跨层跳跃,例如 `orbit` 不得直接依赖 `network` -- 禁止同层平级依赖 +- 层内允许依赖该层的基准 crate:`foundation` 内 `apsis-core` 为基准,其余 crate 可依赖它;其余层内部以各自的门面 crate(`apsis-orbit`、`apsis-rf`、`apsis-sim` 等)作为整合点 各层内容: diff --git a/crates/foundation/apsis-units/Cargo.toml b/crates/foundation/apsis-units/Cargo.toml index cc79179..cdeb6b2 100644 --- a/crates/foundation/apsis-units/Cargo.toml +++ b/crates/foundation/apsis-units/Cargo.toml @@ -11,3 +11,4 @@ publish = false workspace = true [dependencies] +apsis-core.workspace = true diff --git a/crates/foundation/apsis-units/src/angle.rs b/crates/foundation/apsis-units/src/angle.rs new file mode 100644 index 0000000..6a53863 --- /dev/null +++ b/crates/foundation/apsis-units/src/angle.rs @@ -0,0 +1,193 @@ +//! Plane angle quantities. +//! +//! The base unit is the radian. [`Radians`] and [`Degrees`] both measure +//! [`Angle`](crate::dimension::Angle) and convert into each other through +//! [`Quantity::to_unit`](apsis_core::Quantity::to_unit). +//! +//! Degrees are not a power of ten multiple of radians, so the conversion +//! factor is the irrational constant pi. The factor is derived from +//! [`f64::consts::PI`](core::f64::consts::PI) rather than from a rounded +//! literal. + +use crate::dimension::Angle; +use crate::macros::impl_quantity_ops; +use apsis_core::{Quantity, Unit}; +use core::f64::consts::PI; +use core::fmt; + +/// An angle in radians. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +pub struct Radians(f64); + +/// An angle in degrees. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +pub struct Degrees(f64); + +impl Radians { + /// Creates an angle from a value in radians. + pub const fn new(value: f64) -> Self { + Self(value) + } + + /// Returns this angle in radians. + pub const fn value(self) -> f64 { + self.0 + } + + /// Converts this angle to degrees. + pub fn to_degrees(self) -> Degrees { + Degrees(self.0 * 180.0 / PI) + } + + /// Returns this angle wrapped to the half-open interval `[-pi, pi)`. + /// + /// Wrapping uses [`f64::rem_euclid`](f64::rem_euclid) so the result is + /// well defined for negative inputs as well as large ones. + pub fn wrap(self) -> Self { + let two_pi = 2.0 * PI; + let wrapped = (self.0 + PI).rem_euclid(two_pi) - PI; + Self(wrapped) + } +} + +impl Degrees { + /// Creates an angle from a value in degrees. + pub const fn new(value: f64) -> Self { + Self(value) + } + + /// Returns this angle in degrees. + pub const fn value(self) -> f64 { + self.0 + } + + /// Converts this angle to radians. + pub fn to_radians(self) -> Radians { + Radians(self.0 * PI / 180.0) + } +} + +impl Unit for Radians { + type Dimension = Angle; + + fn factor_to_base() -> f64 { + 1.0 + } + + fn symbol() -> &'static str { + "rad" + } +} + +impl Unit for Degrees { + type Dimension = Angle; + + fn factor_to_base() -> f64 { + PI / 180.0 + } + + fn symbol() -> &'static str { + "deg" + } +} + +impl Quantity for Radians { + type U = Radians; + + fn value(self) -> f64 { + self.0 + } + + fn from_value(value: f64) -> Self { + Self(value) + } +} + +impl Quantity for Degrees { + type U = Degrees; + + fn value(self) -> f64 { + self.0 + } + + fn from_value(value: f64) -> Self { + Self(value) + } +} + +impl fmt::Display for Radians { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} rad", self.0) + } +} + +impl fmt::Display for Degrees { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} deg", self.0) + } +} + +impl_quantity_ops!(Radians); +impl_quantity_ops!(Degrees); + +#[cfg(test)] +mod tests { + use super::{Degrees, Radians}; + use crate::dimension::Angle; + use apsis_core::{Quantity, Unit}; + use core::f64::consts::PI; + + #[test] + fn direct_conversions_are_consistent() { + assert!((Degrees::new(180.0).to_radians().value() - PI).abs() < 1e-12); + assert!((Radians::new(PI).to_degrees().value() - 180.0).abs() < 1e-12); + assert!((Degrees::new(90.0).to_radians().value() - PI / 2.0).abs() < 1e-12); + } + + #[test] + fn generic_conversion_matches_direct_conversion() { + let angle = Degrees::new(45.0); + let via_trait: Radians = angle.to_unit(); + assert!((via_trait.value() - angle.to_radians().value()).abs() < 1e-12); + } + + #[test] + fn conversion_round_trips() { + let original = Degrees::new(137.5); + let round_tripped: Degrees = original.to_unit::().to_unit(); + assert!((round_tripped.value() - original.value()).abs() < 1e-12); + } + + #[test] + fn wrap_places_angles_in_the_canonical_range() { + for turns in -3..=3 { + let angle = Radians::new(PI + f64::from(turns) * 2.0 * PI); + let wrapped = angle.wrap(); + assert!(wrapped.value() >= -PI); + assert!(wrapped.value() < PI); + // Wrapping shifts by whole turns, so the angle is preserved + // modulo two pi. + let difference = angle.value() - wrapped.value(); + assert!((difference / (2.0 * PI)).fract().abs() < 1e-12); + } + assert!((Radians::new(0.0).wrap().value()).abs() < 1e-12); + assert!((Radians::new(PI).wrap().value() + PI).abs() < 1e-12); + } + + #[test] + fn unit_metadata_is_available() { + assert_eq!(Radians::symbol(), "rad"); + assert_eq!(Degrees::symbol(), "deg"); + assert_eq!(Radians::factor_to_base(), 1.0); + assert!((Degrees::factor_to_base() - PI / 180.0).abs() < 1e-15); + fn assert_angle>() {} + assert_angle::(); + assert_angle::(); + } + + #[test] + fn display_includes_the_symbol() { + assert_eq!(Radians::new(1.0).to_string(), "1 rad"); + assert_eq!(Degrees::new(90.0).to_string(), "90 deg"); + } +} diff --git a/crates/foundation/apsis-units/src/dimension.rs b/crates/foundation/apsis-units/src/dimension.rs new file mode 100644 index 0000000..407a26c --- /dev/null +++ b/crates/foundation/apsis-units/src/dimension.rs @@ -0,0 +1,26 @@ +//! Physical dimension markers. +//! +//! Each marker is a zero sized type naming one physical dimension. A +//! [`Unit`](apsis_core::Unit) declares which dimension it measures through +//! its `Dimension` associated type, and conversion between quantities is +//! only permitted when the dimensions match. + +/// Length. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Length; + +/// Duration. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Time; + +/// Frequency, the reciprocal of time. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Frequency; + +/// Plane angle. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Angle; + +/// Thermodynamic temperature. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Temperature; diff --git a/crates/foundation/apsis-units/src/frequency.rs b/crates/foundation/apsis-units/src/frequency.rs new file mode 100644 index 0000000..6b1090e --- /dev/null +++ b/crates/foundation/apsis-units/src/frequency.rs @@ -0,0 +1,96 @@ +//! Frequency quantities. +//! +//! The base unit is the hertz, one cycle per second. [`Hertz`] measures +//! [`Frequency`](crate::dimension::Frequency). + +use crate::dimension::Frequency; +use crate::macros::impl_quantity_ops; +use apsis_core::{Quantity, Unit}; +use core::fmt; + +/// A frequency in hertz. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +pub struct Hertz(f64); + +impl Hertz { + /// Creates a frequency from a value in hertz. + pub const fn new(value: f64) -> Self { + Self(value) + } + + /// Returns this frequency in hertz. + pub const fn value(self) -> f64 { + self.0 + } + + /// Returns the period corresponding to this frequency, in seconds. + /// + /// Returns `f64::INFINITY` when the frequency is zero, which is the + /// correct limit of an infinite period. + pub fn period(self) -> f64 { + 1.0 / self.0 + } +} + +impl Unit for Hertz { + type Dimension = Frequency; + + fn factor_to_base() -> f64 { + 1.0 + } + + fn symbol() -> &'static str { + "Hz" + } +} + +impl Quantity for Hertz { + type U = Hertz; + + fn value(self) -> f64 { + self.0 + } + + fn from_value(value: f64) -> Self { + Self(value) + } +} + +impl fmt::Display for Hertz { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} Hz", self.0) + } +} + +impl_quantity_ops!(Hertz); + +#[cfg(test)] +mod tests { + use super::Hertz; + use crate::dimension::Frequency; + use apsis_core::Unit; + + #[test] + fn stores_the_given_value() { + assert_eq!(Hertz::new(1_575.42).value(), 1_575.42); + } + + #[test] + fn period_is_the_reciprocal() { + assert!((Hertz::new(2.0).period() - 0.5).abs() < 1e-12); + assert!(Hertz::new(0.0).period().is_infinite()); + } + + #[test] + fn unit_metadata_is_available() { + assert_eq!(Hertz::symbol(), "Hz"); + assert_eq!(Hertz::factor_to_base(), 1.0); + fn assert_frequency>() {} + assert_frequency::(); + } + + #[test] + fn display_includes_the_symbol() { + assert_eq!(Hertz::new(1_000.0).to_string(), "1000 Hz"); + } +} diff --git a/crates/foundation/apsis-units/src/length.rs b/crates/foundation/apsis-units/src/length.rs new file mode 100644 index 0000000..bb47c19 --- /dev/null +++ b/crates/foundation/apsis-units/src/length.rs @@ -0,0 +1,184 @@ +//! Length quantities. +//! +//! The base unit is the meter. [`Meters`] and [`Kilometers`] both measure +//! [`Length`](crate::dimension::Length) and convert into each other through +//! [`Quantity::to_unit`](apsis_core::Quantity::to_unit). + +use crate::dimension::Length; +use crate::macros::impl_quantity_ops; +use apsis_core::{Quantity, Unit}; +use core::fmt; + +/// A length in meters. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +pub struct Meters(f64); + +/// A length in kilometers. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +pub struct Kilometers(f64); + +impl Meters { + /// Creates a length from a value in meters. + pub const fn new(value: f64) -> Self { + Self(value) + } + + /// Returns this length in meters. + pub const fn value(self) -> f64 { + self.0 + } + + /// Converts this length to kilometers. + pub fn to_kilometers(self) -> Kilometers { + Kilometers(self.0 / 1_000.0) + } +} + +impl Kilometers { + /// Creates a length from a value in kilometers. + pub const fn new(value: f64) -> Self { + Self(value) + } + + /// Returns this length in kilometers. + pub const fn value(self) -> f64 { + self.0 + } + + /// Converts this length to meters. + pub fn to_meters(self) -> Meters { + Meters(self.0 * 1_000.0) + } +} + +impl Unit for Meters { + type Dimension = Length; + + fn factor_to_base() -> f64 { + 1.0 + } + + fn symbol() -> &'static str { + "m" + } +} + +impl Unit for Kilometers { + type Dimension = Length; + + fn factor_to_base() -> f64 { + 1_000.0 + } + + fn symbol() -> &'static str { + "km" + } +} + +impl Quantity for Meters { + type U = Meters; + + fn value(self) -> f64 { + self.0 + } + + fn from_value(value: f64) -> Self { + Self(value) + } +} + +impl Quantity for Kilometers { + type U = Kilometers; + + fn value(self) -> f64 { + self.0 + } + + fn from_value(value: f64) -> Self { + Self(value) + } +} + +impl fmt::Display for Meters { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} m", self.0) + } +} + +impl fmt::Display for Kilometers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} km", self.0) + } +} + +impl_quantity_ops!(Meters); +impl_quantity_ops!(Kilometers); + +#[cfg(test)] +mod tests { + use super::{Kilometers, Meters}; + use crate::dimension::Length; + use apsis_core::{Quantity, Unit}; + + #[test] + fn stores_the_given_value() { + assert_eq!(Meters::new(12.5).value(), 12.5); + assert_eq!(Kilometers::new(-3.0).value(), -3.0); + } + + #[test] + fn direct_conversions_are_consistent() { + assert_eq!(Meters::new(2_500.0).to_kilometers().value(), 2.5); + assert_eq!(Kilometers::new(2.5).to_meters().value(), 2_500.0); + } + + #[test] + fn generic_conversion_matches_direct_conversion() { + let distance = Meters::new(2_500.0); + let via_trait: Kilometers = distance.to_unit(); + assert_eq!(via_trait, distance.to_kilometers()); + } + + #[test] + fn conversion_round_trips() { + let original = Kilometers::new(42.195); + let round_tripped: Kilometers = original.to_unit::().to_unit(); + assert!((round_tripped.value() - original.value()).abs() < 1e-12); + } + + #[test] + fn units_report_their_metadata() { + assert_eq!(Meters::symbol(), "m"); + assert_eq!(Kilometers::symbol(), "km"); + assert_eq!(Meters::factor_to_base(), 1.0); + assert_eq!(Kilometers::factor_to_base(), 1_000.0); + // Both units measure the same dimension, which is what makes the + // generic conversion above type check. + fn assert_length>() {} + assert_length::(); + assert_length::(); + } + + #[test] + fn arithmetic_operates_on_the_stored_value() { + let sum = Meters::new(1.5) + Meters::new(2.5); + assert_eq!(sum.value(), 4.0); + let difference = Meters::new(4.0) - Meters::new(1.0); + assert_eq!(difference.value(), 3.0); + let scaled = Meters::new(3.0) * 2.0; + assert_eq!(scaled.value(), 6.0); + let straddled = 2.0 * Meters::new(3.0); + assert_eq!(straddled.value(), 6.0); + let halved = Meters::new(3.0) / 2.0; + assert_eq!(halved.value(), 1.5); + let ratio = Meters::new(3.0) / Meters::new(1.5); + assert_eq!(ratio, 2.0); + assert_eq!((-Meters::new(1.0)).value(), -1.0); + } + + #[test] + fn display_includes_the_symbol() { + assert_eq!(Meters::new(1.0).to_string(), "1 m"); + assert_eq!(Kilometers::new(2.0).to_string(), "2 km"); + } +} diff --git a/crates/foundation/apsis-units/src/lib.rs b/crates/foundation/apsis-units/src/lib.rs index 34b7492..59f88bd 100644 --- a/crates/foundation/apsis-units/src/lib.rs +++ b/crates/foundation/apsis-units/src/lib.rs @@ -1 +1,45 @@ -//! Strongly typed newtypes for physical quantities such as meters, seconds, hertz, radians, and degrees. +//! Strongly typed newtypes for physical quantities. +//! +//! Each quantity is a newtype over `f64` that carries its unit in the type +//! system, so a length cannot be passed where a duration is expected. Units +//! of the same dimension convert through +//! [`Quantity::to_unit`](apsis_core::Quantity::to_unit), and each unit also +//! offers direct conversions for the common cases. +//! +//! A newtype implements both [`Unit`] and [`Quantity`], with its own type as +//! the associated unit. For example [`Meters`] is a [`Length`](dimension::Length) +//! unit and a quantity whose unit is itself. +//! +//! The dimensions covered here are length, time, frequency, angle, and +//! thermodynamic temperature. Temperature is the only affine scale in the +//! set, so Celsius is handled by explicit methods on [`Kelvin`] rather than +//! by the linear [`Unit`] conversion factor. +//! +//! ``` +//! use apsis_core::Quantity; +//! use apsis_units::{Kilometers, Meters}; +//! +//! let distance = Meters::new(2_500.0); +//! let in_kilometers: Kilometers = distance.to_unit(); +//! assert!((in_kilometers.value() - 2.5).abs() < 1e-12); +//! ``` + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +pub mod dimension; +mod macros; + +mod angle; +mod frequency; +mod length; +mod temperature; +mod time; + +pub use angle::{Degrees, Radians}; +pub use frequency::Hertz; +pub use length::{Kilometers, Meters}; +pub use temperature::Kelvin; +pub use time::Seconds; + +pub use apsis_core::{Quantity, Unit}; diff --git a/crates/foundation/apsis-units/src/macros.rs b/crates/foundation/apsis-units/src/macros.rs new file mode 100644 index 0000000..dbab19c --- /dev/null +++ b/crates/foundation/apsis-units/src/macros.rs @@ -0,0 +1,73 @@ +//! Internal helpers that generate the arithmetic operators for a quantity +//! newtype. +//! +//! The operators are mechanical, so they are generated from one definition +//! rather than repeated per unit. The macro is crate internal; the crate +//! documentation describes the public behaviour. + +/// Implements `Add`, `Sub`, `Neg`, `Mul`, `Div`, `Mul for +/// f64`, and `Div` for a `f64` backed quantity newtype. +/// +/// The newtype must be a tuple struct with a single `f64` field visible in +/// the invoking module. +macro_rules! impl_quantity_ops { + ($quantity:ident) => { + impl core::ops::Add for $quantity { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0) + } + } + + impl core::ops::Sub for $quantity { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0) + } + } + + impl core::ops::Neg for $quantity { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } + } + + impl core::ops::Mul for $quantity { + type Output = Self; + + fn mul(self, rhs: f64) -> Self { + Self(self.0 * rhs) + } + } + + impl core::ops::Div for $quantity { + type Output = Self; + + fn div(self, rhs: f64) -> Self { + Self(self.0 / rhs) + } + } + + impl core::ops::Mul<$quantity> for f64 { + type Output = $quantity; + + fn mul(self, rhs: $quantity) -> $quantity { + $quantity(self * rhs.0) + } + } + + impl core::ops::Div<$quantity> for $quantity { + type Output = f64; + + fn div(self, rhs: Self) -> f64 { + self.0 / rhs.0 + } + } + }; +} + +pub(crate) use impl_quantity_ops; diff --git a/crates/foundation/apsis-units/src/temperature.rs b/crates/foundation/apsis-units/src/temperature.rs new file mode 100644 index 0000000..d45d6a6 --- /dev/null +++ b/crates/foundation/apsis-units/src/temperature.rs @@ -0,0 +1,122 @@ +//! Thermodynamic temperature quantities. +//! +//! The base unit is the kelvin. [`Kelvin`] measures +//! [`Temperature`](crate::dimension::Temperature). +//! +//! Temperature is the one affine scale in this crate. The Celsius scale +//! shares the kelvin's magnitude but not its zero, so converting between +//! them requires an offset and cannot be expressed by the linear +//! [`Unit`] factor. Celsius is therefore offered +//! through explicit methods rather than as a second [`Unit`] type. + +use crate::dimension::Temperature; +use crate::macros::impl_quantity_ops; +use apsis_core::{Quantity, Unit}; +use core::fmt; + +/// A thermodynamic temperature in kelvin. +/// +/// Values below absolute zero are not rejected, because intermediate +/// arithmetic may legitimately pass through them; callers that require a +/// physical temperature should check the value. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +pub struct Kelvin(f64); + +impl Kelvin { + /// Creates a temperature from a value in kelvin. + pub const fn new(value: f64) -> Self { + Self(value) + } + + /// Creates a temperature from a value in degrees Celsius. + /// + /// Uses the exact offset defined by the Celsius scale. + pub fn from_celsius(celsius: f64) -> Self { + Self(celsius + apsis_core::constant::ZERO_CELSIUS_IN_KELVIN) + } + + /// Returns this temperature in kelvin. + pub const fn value(self) -> f64 { + self.0 + } + + /// Converts this temperature to degrees Celsius. + pub fn to_celsius(self) -> f64 { + self.0 - apsis_core::constant::ZERO_CELSIUS_IN_KELVIN + } +} + +impl Unit for Kelvin { + type Dimension = Temperature; + + fn factor_to_base() -> f64 { + 1.0 + } + + fn symbol() -> &'static str { + "K" + } +} + +impl Quantity for Kelvin { + type U = Kelvin; + + fn value(self) -> f64 { + self.0 + } + + fn from_value(value: f64) -> Self { + Self(value) + } +} + +impl fmt::Display for Kelvin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} K", self.0) + } +} + +impl_quantity_ops!(Kelvin); + +#[cfg(test)] +mod tests { + use super::Kelvin; + use crate::dimension::Temperature; + use apsis_core::Unit; + use apsis_core::constant::ZERO_CELSIUS_IN_KELVIN; + + #[test] + fn stores_the_given_value() { + assert_eq!(Kelvin::new(290.0).value(), 290.0); + } + + #[test] + fn celsius_conversion_uses_the_exact_offset() { + let freezing = Kelvin::from_celsius(0.0); + assert_eq!(freezing.value(), ZERO_CELSIUS_IN_KELVIN); + assert_eq!(freezing.to_celsius(), 0.0); + let boiling = Kelvin::from_celsius(100.0); + assert!((boiling.to_celsius() - 100.0).abs() < 1e-12); + } + + #[test] + fn celsius_conversion_round_trips() { + let original = Kelvin::new(273.15 + 21.5); + assert!( + (Kelvin::from_celsius(original.to_celsius()).value() - original.value()).abs() < 1e-12 + ); + } + + #[test] + fn unit_metadata_is_available() { + assert_eq!(Kelvin::symbol(), "K"); + assert_eq!(Kelvin::factor_to_base(), 1.0); + fn assert_temperature>() {} + assert_temperature::(); + } + + #[test] + fn display_includes_the_symbol() { + assert_eq!(Kelvin::new(300.0).to_string(), "300 K"); + } +} diff --git a/crates/foundation/apsis-units/src/time.rs b/crates/foundation/apsis-units/src/time.rs new file mode 100644 index 0000000..19e93c3 --- /dev/null +++ b/crates/foundation/apsis-units/src/time.rs @@ -0,0 +1,91 @@ +//! Duration quantities. +//! +//! The base unit is the second. [`Seconds`] is the only unit defined here; +//! it measures [`Time`](crate::dimension::Time). + +use crate::dimension::Time; +use crate::macros::impl_quantity_ops; +use apsis_core::{Quantity, Unit}; +use core::fmt; + +/// A duration in seconds. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +pub struct Seconds(f64); + +impl Seconds { + /// Creates a duration from a value in seconds. + pub const fn new(value: f64) -> Self { + Self(value) + } + + /// Returns this duration in seconds. + pub const fn value(self) -> f64 { + self.0 + } +} + +impl Unit for Seconds { + type Dimension = Time; + + fn factor_to_base() -> f64 { + 1.0 + } + + fn symbol() -> &'static str { + "s" + } +} + +impl Quantity for Seconds { + type U = Seconds; + + fn value(self) -> f64 { + self.0 + } + + fn from_value(value: f64) -> Self { + Self(value) + } +} + +impl fmt::Display for Seconds { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} s", self.0) + } +} + +impl_quantity_ops!(Seconds); + +#[cfg(test)] +mod tests { + use super::Seconds; + use crate::dimension::Time; + use apsis_core::Unit; + + #[test] + fn stores_the_given_value() { + assert_eq!(Seconds::new(0.5).value(), 0.5); + } + + #[test] + fn unit_metadata_is_available() { + assert_eq!(Seconds::symbol(), "s"); + assert_eq!(Seconds::factor_to_base(), 1.0); + fn assert_time>() {} + assert_time::(); + } + + #[test] + fn arithmetic_operates_on_the_stored_value() { + assert_eq!((Seconds::new(1.0) + Seconds::new(2.0)).value(), 3.0); + assert_eq!((Seconds::new(3.0) - Seconds::new(2.0)).value(), 1.0); + assert_eq!((Seconds::new(3.0) * 2.0).value(), 6.0); + assert_eq!((Seconds::new(3.0) / 3.0).value(), 1.0); + assert_eq!(Seconds::new(6.0) / Seconds::new(3.0), 2.0); + } + + #[test] + fn display_includes_the_symbol() { + assert_eq!(Seconds::new(1.5).to_string(), "1.5 s"); + } +}