foundation: add error trait, result alias, constants, and unit trait
This commit is contained in:
parent
539f29c7b5
commit
b95cf4adf3
|
|
@ -0,0 +1,177 @@
|
||||||
|
//! Physical and astronomical constants used across the workspace.
|
||||||
|
//!
|
||||||
|
//! All values are expressed in SI units unless the name or doc comment
|
||||||
|
//! states otherwise. Values that are fixed by definition are given exactly;
|
||||||
|
//! measured values carry the number of significant digits justified by the
|
||||||
|
//! public source.
|
||||||
|
//!
|
||||||
|
//! Sources:
|
||||||
|
//!
|
||||||
|
//! - SI defining constants: BIPM, *The International System of Units (SI)*,
|
||||||
|
//! 9th edition, 2019.
|
||||||
|
//! - CODATA 2018 recommended values: E. Tiesinga et al., *CODATA
|
||||||
|
//! recommended values of the fundamental physical constants: 2018*,
|
||||||
|
//! Rev. Mod. Phys. 93, 025010, 2021.
|
||||||
|
//! - Earth gravitational parameter and equatorial radius: WGS 84,
|
||||||
|
//! NIMA TR8350.2, 2000.
|
||||||
|
//! - Astronomical unit: IAU 2012 Resolution B2.
|
||||||
|
//!
|
||||||
|
//! Where a value is defined exactly by a standard, the doc comment says so,
|
||||||
|
//! because several derived quantities depend on the exact value rather than
|
||||||
|
//! the rounded printed one.
|
||||||
|
|
||||||
|
/// Speed of light in vacuum, in meters per second.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the metre in the SI.
|
||||||
|
pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
|
||||||
|
|
||||||
|
/// Newtonian constant of gravitation, in cubic meters per kilogram per
|
||||||
|
/// second squared.
|
||||||
|
///
|
||||||
|
/// CODATA 2018 value with its standard uncertainty of
|
||||||
|
/// `1.5e-11 m^3 kg^-1 s^-2`.
|
||||||
|
pub const GRAVITATIONAL_CONSTANT: f64 = 6.674_30e-11;
|
||||||
|
|
||||||
|
/// Boltzmann constant, in joules per kelvin.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the kelvin in the SI.
|
||||||
|
pub const BOLTZMANN: f64 = 1.380_649e-23;
|
||||||
|
|
||||||
|
/// Planck constant, in joule seconds.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the kilogram in the SI.
|
||||||
|
pub const PLANCK: f64 = 6.626_070_15e-34;
|
||||||
|
|
||||||
|
/// Elementary charge, in coulombs.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the ampere in the SI.
|
||||||
|
pub const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
|
||||||
|
|
||||||
|
/// Astronomical unit, in meters.
|
||||||
|
///
|
||||||
|
/// Exact by IAU 2012 Resolution B2.
|
||||||
|
pub const ASTRONOMICAL_UNIT: f64 = 149_597_870_700.0;
|
||||||
|
|
||||||
|
/// Standard gravity, in meters per second squared.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the standard atmosphere.
|
||||||
|
pub const STANDARD_GRAVITY: f64 = 9.806_65;
|
||||||
|
|
||||||
|
/// One minute, in seconds.
|
||||||
|
pub const MINUTE: f64 = 60.0;
|
||||||
|
|
||||||
|
/// One hour, in seconds.
|
||||||
|
pub const HOUR: f64 = 3_600.0;
|
||||||
|
|
||||||
|
/// One day of 24 hours, in seconds.
|
||||||
|
///
|
||||||
|
/// This is the SI day. It is not the mean solar day, which is slightly
|
||||||
|
/// longer, and not the Julian day of 86 400 seconds used in Julian date
|
||||||
|
/// arithmetic, which is identical to this value.
|
||||||
|
pub const DAY: f64 = 86_400.0;
|
||||||
|
|
||||||
|
/// One Julian year of 365.25 days, in seconds.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the Julian year.
|
||||||
|
pub const JULIAN_YEAR: f64 = 31_557_600.0;
|
||||||
|
|
||||||
|
/// One Julian century of 36 525 days, in seconds.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the Julian year, used by precession and secular
|
||||||
|
/// rate expressions quoted per Julian century.
|
||||||
|
pub const JULIAN_CENTURY: f64 = 3_155_760_000.0;
|
||||||
|
|
||||||
|
/// Julian date of the Unix epoch, 1970-01-01T00:00:00 UTC.
|
||||||
|
///
|
||||||
|
/// This is `2 440 587.5` in the Julian date system.
|
||||||
|
pub const JULIAN_DATE_UNIX_EPOCH: f64 = 2_440_587.5;
|
||||||
|
|
||||||
|
/// Julian date of the J2000.0 epoch, 2000-01-01T12:00:00 TT.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the J2000.0 epoch.
|
||||||
|
pub const JULIAN_DATE_J2000: f64 = 2_451_545.0;
|
||||||
|
|
||||||
|
/// The number of seconds between the J2000.0 epoch and the Unix epoch.
|
||||||
|
///
|
||||||
|
/// Derived from [`JULIAN_DATE_J2000`] and [`JULIAN_DATE_UNIX_EPOCH`].
|
||||||
|
pub const SECONDS_J2000_TO_UNIX: f64 = (JULIAN_DATE_J2000 - JULIAN_DATE_UNIX_EPOCH) * DAY;
|
||||||
|
|
||||||
|
/// Earth gravitational parameter, in cubic kilometers per second squared.
|
||||||
|
///
|
||||||
|
/// WGS 84 value, `GM = 398 600.4418 km^3/s^2`, with an uncertainty that is
|
||||||
|
/// dominated by the uncertainty in the Newtonian constant of gravitation.
|
||||||
|
/// Orbit propagation uses kilometers, so the value is given in those units
|
||||||
|
/// rather than in SI.
|
||||||
|
pub const EARTH_MU: f64 = 398_600.441_8;
|
||||||
|
|
||||||
|
/// Earth equatorial radius, in kilometers.
|
||||||
|
///
|
||||||
|
/// WGS 84 semi-major axis, `a = 6 378.137 km`.
|
||||||
|
pub const EARTH_EQUATORIAL_RADIUS: f64 = 6_378.137;
|
||||||
|
|
||||||
|
/// Earth flattening, dimensionless.
|
||||||
|
///
|
||||||
|
/// WGS 84 value, `f = 1 / 298.257 223 563`.
|
||||||
|
pub const EARTH_FLATTENING: f64 = 1.0 / 298.257_223_563;
|
||||||
|
|
||||||
|
/// Earth rotation rate, in radians per second.
|
||||||
|
///
|
||||||
|
/// WGS 84 value. This is the nominal mean angular velocity of the Earth,
|
||||||
|
/// `omega = 7.292 115e-5 rad/s`.
|
||||||
|
pub const EARTH_ROTATION_RATE: f64 = 7.292_115_0e-5;
|
||||||
|
|
||||||
|
/// Earth second zonal harmonic, dimensionless.
|
||||||
|
///
|
||||||
|
/// WGS 84 value, `J2 = 1.082 626 683e-3`.
|
||||||
|
pub const EARTH_J2: f64 = 1.082_626_683e-3;
|
||||||
|
|
||||||
|
/// Standard atmosphere sea level pressure, in pascals.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the standard atmosphere.
|
||||||
|
pub const STANDARD_ATMOSPHERE: f64 = 101_325.0;
|
||||||
|
|
||||||
|
/// Zero degrees Celsius on the thermodynamic temperature scale, in kelvin.
|
||||||
|
///
|
||||||
|
/// Exact by definition of the Celsius scale.
|
||||||
|
pub const ZERO_CELSIUS_IN_KELVIN: f64 = 273.15;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defining_constants_are_exact() {
|
||||||
|
assert_eq!(SPEED_OF_LIGHT, 299_792_458.0);
|
||||||
|
assert_eq!(BOLTZMANN, 1.380_649e-23);
|
||||||
|
assert_eq!(PLANCK, 6.626_070_15e-34);
|
||||||
|
assert_eq!(ELEMENTARY_CHARGE, 1.602_176_634e-19);
|
||||||
|
assert_eq!(ASTRONOMICAL_UNIT, 149_597_870_700.0);
|
||||||
|
assert_eq!(STANDARD_GRAVITY, 9.806_65);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_constants_are_consistent() {
|
||||||
|
assert_eq!(HOUR, MINUTE * 60.0);
|
||||||
|
assert_eq!(DAY, HOUR * 24.0);
|
||||||
|
assert_eq!(JULIAN_YEAR, DAY * 365.25);
|
||||||
|
assert_eq!(JULIAN_CENTURY, JULIAN_YEAR * 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j2000_minus_unix_epoch_is_the_expected_span() {
|
||||||
|
// The two epochs are 10 957 days apart.
|
||||||
|
assert_eq!(JULIAN_DATE_J2000 - JULIAN_DATE_UNIX_EPOCH, 10_957.5);
|
||||||
|
assert_eq!(SECONDS_J2000_TO_UNIX, 10_957.5 * DAY);
|
||||||
|
assert_eq!(SECONDS_J2000_TO_UNIX, 946_728_000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn earth_flattening_matches_the_inverse_value() {
|
||||||
|
assert!((1.0 / EARTH_FLATTENING - 298.257_223_563).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn celsius_offset_is_exact() {
|
||||||
|
assert_eq!(ZERO_CELSIUS_IN_KELVIN, 273.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
//! The workspace error type and result alias.
|
||||||
|
//!
|
||||||
|
//! The workspace does not use an external error crate. Errors are modelled
|
||||||
|
//! as a single [`Error`] struct carrying an [`ErrorKind`], an optional
|
||||||
|
//! message, and an optional source. `ErrorKind` is a fieldless enum so that
|
||||||
|
//! callers can match on the failure category without allocating.
|
||||||
|
|
||||||
|
use core::error::Error as StdError;
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
/// The category of an [`Error`].
|
||||||
|
///
|
||||||
|
/// The variants are grouped by the layer that most often produces them, but
|
||||||
|
/// any layer may return any variant. Callers should match on the variant
|
||||||
|
/// that is relevant to their own recovery logic and treat the rest as
|
||||||
|
/// opaque.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ErrorKind {
|
||||||
|
/// An input value is outside the range the function accepts.
|
||||||
|
OutOfRange,
|
||||||
|
/// An input does not satisfy a documented precondition.
|
||||||
|
InvalidInput,
|
||||||
|
/// An operation is not defined for the given inputs, such as a
|
||||||
|
/// singularity in a coordinate transformation.
|
||||||
|
Undefined,
|
||||||
|
/// A requested item does not exist.
|
||||||
|
NotFound,
|
||||||
|
/// Two inputs are inconsistent with each other.
|
||||||
|
Inconsistent,
|
||||||
|
/// An operation did not converge within the allowed number of steps.
|
||||||
|
NotConverged,
|
||||||
|
/// Text could not be parsed into the expected structure.
|
||||||
|
Parse,
|
||||||
|
/// A numeric operation produced a non-finite result.
|
||||||
|
NotFinite,
|
||||||
|
/// A feature or capability is not implemented.
|
||||||
|
Unsupported,
|
||||||
|
/// A failure that does not fit any other category.
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ErrorKind {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let text = match self {
|
||||||
|
Self::OutOfRange => "value out of range",
|
||||||
|
Self::InvalidInput => "invalid input",
|
||||||
|
Self::Undefined => "operation undefined for these inputs",
|
||||||
|
Self::NotFound => "not found",
|
||||||
|
Self::Inconsistent => "inconsistent inputs",
|
||||||
|
Self::NotConverged => "did not converge",
|
||||||
|
Self::Parse => "parse error",
|
||||||
|
Self::NotFinite => "non-finite result",
|
||||||
|
Self::Unsupported => "unsupported",
|
||||||
|
Self::Other => "error",
|
||||||
|
};
|
||||||
|
f.write_str(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The error type used across the workspace.
|
||||||
|
///
|
||||||
|
/// `Error` carries a machine matchable [`ErrorKind`] plus a human readable
|
||||||
|
/// message. An optional source records the lower level error that caused
|
||||||
|
/// this one.
|
||||||
|
pub struct Error {
|
||||||
|
kind: ErrorKind,
|
||||||
|
message: Option<String>,
|
||||||
|
source: Option<Box<dyn StdError + Send + Sync + 'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
/// Creates an error with the given kind and no message.
|
||||||
|
pub fn new(kind: ErrorKind) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
message: None,
|
||||||
|
source: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an error with the given kind and message.
|
||||||
|
///
|
||||||
|
/// The message should describe the concrete cause of this specific
|
||||||
|
/// failure. It is meant for humans reading logs, not for matching.
|
||||||
|
pub fn with_message(kind: ErrorKind, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
message: Some(message.into()),
|
||||||
|
source: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an error with the given kind and a source error.
|
||||||
|
pub fn with_source<E>(kind: ErrorKind, source: E) -> Self
|
||||||
|
where
|
||||||
|
E: StdError + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
message: None,
|
||||||
|
source: Some(Box::new(source)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches a message to this error, replacing any existing message.
|
||||||
|
pub fn with_context(mut self, message: impl Into<String>) -> Self {
|
||||||
|
self.message = Some(message.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches a source error, replacing any existing source.
|
||||||
|
pub fn caused_by<E>(mut self, source: E) -> Self
|
||||||
|
where
|
||||||
|
E: StdError + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.source = Some(Box::new(source));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the category of this error.
|
||||||
|
pub fn kind(&self) -> ErrorKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the message attached to this error, if any.
|
||||||
|
pub fn message(&self) -> Option<&str> {
|
||||||
|
self.message.as_deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut debug = f.debug_struct("Error");
|
||||||
|
debug.field("kind", &self.kind);
|
||||||
|
if let Some(message) = &self.message {
|
||||||
|
debug.field("message", message);
|
||||||
|
}
|
||||||
|
if let Some(source) = &self.source {
|
||||||
|
debug.field("source", source);
|
||||||
|
}
|
||||||
|
debug.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match &self.message {
|
||||||
|
Some(message) => write!(f, "{}: {message}", self.kind),
|
||||||
|
None => write!(f, "{}", self.kind),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StdError for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
|
self.source
|
||||||
|
.as_ref()
|
||||||
|
.map(|source| source.as_ref() as &(dyn StdError + 'static))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ErrorKind> for Error {
|
||||||
|
fn from(kind: ErrorKind) -> Self {
|
||||||
|
Self::new(kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Result`](core::result::Result) whose error type is [`Error`].
|
||||||
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Error, ErrorKind, Result};
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Underlying;
|
||||||
|
|
||||||
|
impl fmt::Display for Underlying {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("underlying failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::error::Error for Underlying {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kind_is_reported() {
|
||||||
|
let error = Error::new(ErrorKind::Parse);
|
||||||
|
assert_eq!(error.kind(), ErrorKind::Parse);
|
||||||
|
assert_eq!(error.message(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_is_reported() {
|
||||||
|
let error = Error::with_message(ErrorKind::OutOfRange, "semi-major axis is negative");
|
||||||
|
assert_eq!(error.kind(), ErrorKind::OutOfRange);
|
||||||
|
assert_eq!(error.message(), Some("semi-major axis is negative"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_includes_kind_and_message() {
|
||||||
|
let error = Error::with_message(ErrorKind::Parse, "bad checksum");
|
||||||
|
assert_eq!(error.to_string(), "parse error: bad checksum");
|
||||||
|
assert_eq!(Error::new(ErrorKind::NotFound).to_string(), "not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_is_exposed() {
|
||||||
|
let error = Error::with_source(ErrorKind::Other, Underlying);
|
||||||
|
assert!(std::error::Error::source(&error).is_some());
|
||||||
|
assert_eq!(error.kind(), ErrorKind::Other);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_and_cause_replace_previous_values() {
|
||||||
|
let error = Error::new(ErrorKind::Other)
|
||||||
|
.with_context("first")
|
||||||
|
.with_context("second")
|
||||||
|
.caused_by(Underlying);
|
||||||
|
assert_eq!(error.message(), Some("second"));
|
||||||
|
assert!(std::error::Error::source(&error).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_from_kind() {
|
||||||
|
let error: Error = ErrorKind::Unsupported.into();
|
||||||
|
assert_eq!(error.kind(), ErrorKind::Unsupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn works_in_result_position() {
|
||||||
|
fn fail() -> Result<u32> {
|
||||||
|
Err(Error::new(ErrorKind::NotFinite))
|
||||||
|
}
|
||||||
|
|
||||||
|
let outcome = fail();
|
||||||
|
assert!(outcome.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
outcome.err().map(|error| error.kind()),
|
||||||
|
Some(ErrorKind::NotFinite)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,24 @@
|
||||||
//! Shared foundation types for the ApsisRS workspace: error traits, result aliases, and common physical constants.
|
//! Shared foundation types for the ApsisRS workspace: error handling,
|
||||||
|
//! result aliases, physical constants, and the unit marker trait.
|
||||||
|
//!
|
||||||
|
//! This crate sits at the bottom of the dependency graph. It has no
|
||||||
|
//! dependency on any other ApsisRS crate, so every other crate may use it.
|
||||||
|
//!
|
||||||
|
//! Three concerns live here:
|
||||||
|
//!
|
||||||
|
//! - [`Error`] and [`Result`] give the workspace a single error surface
|
||||||
|
//! without pulling in an external error handling crate.
|
||||||
|
//! - [`constant`] holds physical and astronomical constants expressed in
|
||||||
|
//! SI units.
|
||||||
|
//! - [`Quantity`] marks newtypes that carry a physical unit, so generic
|
||||||
|
//! code can accept any quantity and still recover its unit type.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
pub mod constant;
|
||||||
|
mod error;
|
||||||
|
mod quantity;
|
||||||
|
|
||||||
|
pub use error::{Error, ErrorKind, Result};
|
||||||
|
pub use quantity::{Quantity, Unit};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
//! Marker traits for quantities that carry a physical unit.
|
||||||
|
//!
|
||||||
|
//! A quantity newtype stores a numeric value together with its unit in the
|
||||||
|
//! type. The [`Quantity`] trait exposes that unit through an associated
|
||||||
|
//! type, so generic code can be written over any quantity and still recover
|
||||||
|
//! which unit it was given.
|
||||||
|
//!
|
||||||
|
//! The unit itself is any type implementing [`Unit`]. A unit knows how many
|
||||||
|
//! of its kind are in the corresponding base unit, which allows conversion
|
||||||
|
//! between units of the same dimension. The base unit of a dimension always
|
||||||
|
//! has a factor of one.
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
/// A unit of a single physical dimension.
|
||||||
|
///
|
||||||
|
/// Implementors are zero sized marker types. Two units that implement
|
||||||
|
/// `Unit` for the same dimension convert through
|
||||||
|
/// [`Quantity::to_unit`](crate::Quantity::to_unit).
|
||||||
|
pub trait Unit: Copy + fmt::Debug + fmt::Display + 'static {
|
||||||
|
/// The physical dimension this unit measures, such as length or time.
|
||||||
|
type Dimension: fmt::Debug + 'static;
|
||||||
|
|
||||||
|
/// The number of base units in one of this unit.
|
||||||
|
///
|
||||||
|
/// For example a kilometer has a factor of 1000 when the base unit is
|
||||||
|
/// the meter. The base unit of a dimension has a factor of exactly one.
|
||||||
|
fn factor_to_base() -> f64;
|
||||||
|
|
||||||
|
/// The symbol used to print the unit, such as `m` or `s`.
|
||||||
|
fn symbol() -> &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value paired with a unit.
|
||||||
|
///
|
||||||
|
/// Implementors are newtypes over `f64` and a [`Unit`]. The trait gives
|
||||||
|
/// generic code access to the stored value and its unit without assuming
|
||||||
|
/// which newtype it was handed.
|
||||||
|
pub trait Quantity: Copy {
|
||||||
|
/// The unit this quantity is expressed in.
|
||||||
|
type U: Unit;
|
||||||
|
|
||||||
|
/// Returns the stored numeric value in this quantity's own unit.
|
||||||
|
fn value(self) -> f64;
|
||||||
|
|
||||||
|
/// Builds a quantity from a numeric value in this quantity's own unit.
|
||||||
|
fn from_value(value: f64) -> Self;
|
||||||
|
|
||||||
|
/// Converts this quantity to another unit of the same dimension.
|
||||||
|
///
|
||||||
|
/// The value is scaled by the ratio of the two unit factors. The
|
||||||
|
/// conversion is exact only when the factors are exact powers of ten,
|
||||||
|
/// so callers that need a specific unit for output should convert once
|
||||||
|
/// at the boundary rather than repeatedly.
|
||||||
|
fn to_unit<U>(self) -> U
|
||||||
|
where
|
||||||
|
U: Quantity,
|
||||||
|
<U as Quantity>::U: Unit<Dimension = <Self::U as Unit>::Dimension>,
|
||||||
|
{
|
||||||
|
let base = self.value() * Self::U::factor_to_base();
|
||||||
|
U::from_value(base / <U as Quantity>::U::factor_to_base())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Quantity, Unit};
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
struct Length;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Meter;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Kilometer;
|
||||||
|
|
||||||
|
impl fmt::Display for Meter {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("m")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Kilometer {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("km")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Unit for Meter {
|
||||||
|
type Dimension = Length;
|
||||||
|
|
||||||
|
fn factor_to_base() -> f64 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol() -> &'static str {
|
||||||
|
"m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Unit for Kilometer {
|
||||||
|
type Dimension = Length;
|
||||||
|
|
||||||
|
fn factor_to_base() -> f64 {
|
||||||
|
1_000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol() -> &'static str {
|
||||||
|
"km"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
struct Meters(f64);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
struct Kilometers(f64);
|
||||||
|
|
||||||
|
impl Quantity for Meters {
|
||||||
|
type U = Meter;
|
||||||
|
|
||||||
|
fn value(self) -> f64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_value(value: f64) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Quantity for Kilometers {
|
||||||
|
type U = Kilometer;
|
||||||
|
|
||||||
|
fn value(self) -> f64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_value(value: f64) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_round_trips() {
|
||||||
|
let distance = Meters::from_value(1_500.0);
|
||||||
|
assert_eq!(distance.value(), 1_500.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_between_units_of_one_dimension() {
|
||||||
|
let distance = Meters::from_value(2_500.0);
|
||||||
|
let in_kilometers: Kilometers = distance.to_unit();
|
||||||
|
assert!((in_kilometers.value() - 2.5).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conversion_round_trips() {
|
||||||
|
let original = Kilometers::from_value(7.25);
|
||||||
|
let round_tripped: Kilometers = original.to_unit::<Meters>().to_unit();
|
||||||
|
assert!((round_tripped.value() - 7.25).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unit_metadata_is_available() {
|
||||||
|
assert_eq!(<Meter as Unit>::symbol(), "m");
|
||||||
|
assert_eq!(<Kilometer as Unit>::factor_to_base(), 1_000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue