foundation: add length, time, frequency, angle, and temperature units
This commit is contained in:
parent
c08fcd0f90
commit
e861789cc1
|
|
@ -207,3 +207,6 @@ version = "0.1.0"
|
|||
[[package]]
|
||||
name = "apsis-units"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"apsis-core",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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` 等)作为整合点
|
||||
|
||||
各层内容:
|
||||
|
||||
|
|
|
|||
|
|
@ -11,3 +11,4 @@ publish = false
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
apsis-core.workspace = true
|
||||
|
|
|
|||
|
|
@ -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::<Radians>().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<U: Unit<Dimension = Angle>>() {}
|
||||
assert_angle::<Radians>();
|
||||
assert_angle::<Degrees>();
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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<U: Unit<Dimension = Frequency>>() {}
|
||||
assert_frequency::<Hertz>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_includes_the_symbol() {
|
||||
assert_eq!(Hertz::new(1_000.0).to_string(), "1000 Hz");
|
||||
}
|
||||
}
|
||||
|
|
@ -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::<Meters>().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<U: Unit<Dimension = Length>>() {}
|
||||
assert_length::<Meters>();
|
||||
assert_length::<Kilometers>();
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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<f64>`, `Div<f64>`, `Mul<Self> for
|
||||
/// f64`, and `Div<Self>` 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<f64> for $quantity {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, rhs: f64) -> Self {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Div<f64> 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;
|
||||
|
|
@ -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<U: Unit<Dimension = Temperature>>() {}
|
||||
assert_temperature::<Kelvin>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_includes_the_symbol() {
|
||||
assert_eq!(Kelvin::new(300.0).to_string(), "300 K");
|
||||
}
|
||||
}
|
||||
|
|
@ -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<U: Unit<Dimension = Time>>() {}
|
||||
assert_time::<Seconds>();
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue