quantities/
lib.rs

1// ---------------------------------------------------------------------------
2// Copyright:   (c) 2021 ff. Michael Amrhein (michael@adrhinum.de)
3// License:     This program is part of a larger application. For license
4//              details please read the file LICENSE.TXT provided together
5//              with the application.
6// ---------------------------------------------------------------------------
7// $Source:     src/lib.rs $
8// $Revision:   2024-07-26T21:35:21+02:00 $
9
10#![doc = include_str ! ("../README.md")]
11#![cfg_attr(not(feature = "std"), no_std)]
12// activate some rustc lints
13#![deny(non_ascii_idents)]
14#![deny(unsafe_code)]
15#![warn(missing_debug_implementations)]
16#![warn(missing_docs)]
17#![warn(trivial_casts)]
18#![warn(unused)]
19#![allow(dead_code)]
20// activate some clippy lints
21#![warn(clippy::cast_possible_truncation)]
22#![warn(clippy::cast_possible_wrap)]
23#![warn(clippy::cast_precision_loss)]
24#![warn(clippy::cast_sign_loss)]
25#![warn(clippy::cognitive_complexity)]
26#![warn(clippy::decimal_literal_representation)]
27#![warn(clippy::enum_glob_use)]
28#![warn(clippy::equatable_if_let)]
29#![warn(clippy::fallible_impl_from)]
30#![warn(clippy::if_not_else)]
31#![warn(clippy::if_then_some_else_none)]
32#![warn(clippy::implicit_clone)]
33#![warn(clippy::integer_division)]
34#![warn(clippy::manual_assert)]
35#![warn(clippy::match_same_arms)]
36#![warn(clippy::mismatching_type_param_order)]
37#![warn(clippy::missing_const_for_fn)]
38#![warn(clippy::missing_errors_doc)]
39#![warn(clippy::missing_panics_doc)]
40#![warn(clippy::multiple_crate_versions)]
41#![warn(clippy::multiple_inherent_impl)]
42#![warn(clippy::must_use_candidate)]
43#![warn(clippy::needless_pass_by_value)]
44#![warn(clippy::print_stderr)]
45#![warn(clippy::print_stdout)]
46#![warn(clippy::semicolon_if_nothing_returned)]
47#![warn(clippy::str_to_string)]
48#![warn(clippy::string_to_string)]
49#![warn(clippy::undocumented_unsafe_blocks)]
50#![warn(clippy::unicode_not_nfc)]
51#![warn(clippy::unimplemented)]
52#![warn(clippy::unseparated_literal_suffix)]
53#![warn(clippy::unused_self)]
54#![warn(clippy::unwrap_in_result)]
55#![warn(clippy::use_self)]
56#![warn(clippy::used_underscore_binding)]
57#![warn(clippy::wildcard_imports)]
58
59extern crate alloc;
60
61use alloc::{borrow::ToOwned, format, string::String};
62use core::{
63    cmp::Ordering,
64    fmt,
65    ops::{Add, Div, Mul, Sub},
66};
67
68#[cfg(feature = "fpdec")]
69pub use amnt_dec::{AmountT, Dec, Decimal, AMNT_ONE, AMNT_ZERO};
70#[cfg(all(not(feature = "fpdec"), target_pointer_width = "32"))]
71pub use amnt_f32::{AmountT, AMNT_ONE, AMNT_ZERO};
72#[cfg(all(not(feature = "fpdec"), target_pointer_width = "64"))]
73pub use amnt_f64::{AmountT, AMNT_ONE, AMNT_ZERO};
74pub use converter::{ConversionTable, Converter};
75pub use rate::Rate;
76pub use si_prefixes::SIPrefix;
77
78mod converter;
79pub mod prelude;
80mod rate;
81mod si_prefixes;
82
83#[cfg(feature = "fpdec")]
84#[doc(hidden)]
85pub mod amnt_dec;
86#[cfg(all(not(feature = "fpdec"), target_pointer_width = "32"))]
87#[doc(hidden)]
88pub mod amnt_f32;
89#[cfg(all(not(feature = "fpdec"), target_pointer_width = "64"))]
90#[doc(hidden)]
91pub mod amnt_f64;
92
93#[cfg(feature = "acceleration")]
94pub mod acceleration;
95#[cfg(feature = "area")]
96pub mod area;
97#[cfg(feature = "datathroughput")]
98pub mod datathroughput;
99#[cfg(feature = "datavolume")]
100pub mod datavolume;
101#[cfg(feature = "duration")]
102pub mod duration;
103#[cfg(feature = "energy")]
104pub mod energy;
105#[cfg(feature = "force")]
106pub mod force;
107#[cfg(feature = "frequency")]
108pub mod frequency;
109#[cfg(feature = "length")]
110pub mod length;
111#[cfg(feature = "mass")]
112pub mod mass;
113#[cfg(feature = "power")]
114pub mod power;
115#[cfg(feature = "speed")]
116pub mod speed;
117#[cfg(feature = "temperature")]
118pub mod temperature;
119#[cfg(feature = "volume")]
120pub mod volume;
121
122/// The abstract type of units used to define quantities.
123pub trait Unit:
124    Copy + Eq + PartialEq + Sized + Mul<AmountT> + fmt::Display
125{
126    /// Associated type of quantity
127    type QuantityType: Quantity<UnitType = Self>;
128
129    /// Returns an iterator over the variants of `Self`.
130    fn iter() -> impl Iterator<Item = Self>;
131
132    /// Returns `Some(unit)` where `unit.symbol()` == `symbol`, or `None` if
133    /// there is no such unit.
134    #[must_use]
135    fn from_symbol(symbol: &str) -> Option<Self> {
136        Self::iter().find(|&unit| unit.symbol() == symbol)
137    }
138
139    /// Returns the name of `self`.
140    fn name(&self) -> String;
141
142    /// Returns the symbol used to represent `self`.
143    fn symbol(&self) -> String;
144
145    /// Returns the SI prefix of `self`, or None is `self` is not a SI unit.
146    fn si_prefix(&self) -> Option<SIPrefix>;
147
148    /// Returns `1 * self`
149    fn as_qty(&self) -> Self::QuantityType {
150        Self::QuantityType::new(AMNT_ONE, *self)
151    }
152
153    /// Formats `self` using the given formatter.
154    ///
155    /// # Errors
156    ///
157    /// This function will only return an instance of `Error` returned from
158    /// the formatter.
159    fn fmt(&self, form: &mut fmt::Formatter<'_>) -> fmt::Result {
160        fmt::Display::fmt(&self.symbol(), form)
161    }
162}
163
164/// Type of units being linear scaled in terms of a reference unit.
165pub trait LinearScaledUnit: Unit {
166    /// Unit used as reference for scaling the units.
167    const REF_UNIT: Self;
168
169    /// Returns `Some(unit)` where `unit.scale()` == `Some(amnt)`, or `None`
170    /// if there is no such unit.
171    #[must_use]
172    fn from_scale(amnt: AmountT) -> Option<Self> {
173        Self::iter().find(|&unit| unit.scale() == amnt)
174    }
175
176    /// Returns `true` if `self` is the reference unit of its unit type.
177    #[inline(always)]
178    fn is_ref_unit(&self) -> bool {
179        *self == Self::REF_UNIT
180    }
181
182    /// Returns `factor` so that `factor` * `Self::REFUNIT` == 1 * `self`.
183    fn scale(&self) -> AmountT;
184
185    /// Returns `factor` so that `factor` * `other` == 1 * `self`.
186    #[inline(always)]
187    fn ratio(&self, other: &Self) -> AmountT {
188        self.scale() / other.scale()
189    }
190}
191
192/// The abstract type of quantities.
193pub trait Quantity: Copy + Sized + Mul<AmountT> {
194    /// Associated type of unit
195    type UnitType: Unit<QuantityType = Self>;
196
197    /// Returns an iterator over the variants of `Self::UnitType`.
198    #[must_use]
199    fn iter_units() -> impl Iterator<Item = Self::UnitType> {
200        Self::UnitType::iter()
201    }
202
203    /// Returns `Some(unit)` where `unit.symbol()` == `symbol`, or `None` if
204    /// there is no such unit.
205    #[must_use]
206    fn unit_from_symbol(symbol: &str) -> Option<Self::UnitType> {
207        Self::iter_units().find(|&unit| unit.symbol() == symbol)
208    }
209
210    /// Returns a new instance of the type implementing `Quantity`.
211    fn new(amount: AmountT, unit: Self::UnitType) -> Self;
212
213    /// Returns the amount of `self`.
214    fn amount(&self) -> AmountT;
215
216    /// Returns the unit of `self`.
217    fn unit(&self) -> Self::UnitType;
218
219    /// Return `true` if `self` and `other` have the same unit and their
220    /// amounts are equal, otherwise `false`.
221    #[inline(always)]
222    fn eq(&self, other: &Self) -> bool {
223        self.unit() == other.unit() && self.amount() == other.amount()
224    }
225
226    /// Returns the partial order of `self`s and `other`s amounts, if both
227    /// have the same unit, otherwise `None`.
228    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
229        if self.unit() == other.unit() {
230            PartialOrd::partial_cmp(&self.amount(), &other.amount())
231        } else {
232            None
233        }
234    }
235
236    /// Returns the sum of `self` and `other`, if both have the same unit.
237    ///
238    /// # Panics
239    ///
240    /// Panics if `self` and `other` have different units.
241    fn add(self, rhs: Self) -> Self {
242        if self.unit() == rhs.unit() {
243            return Self::new(self.amount() + rhs.amount(), self.unit());
244        }
245        panic!(
246            "Can't add '{}' and '{}'.",
247            self.unit().symbol(),
248            rhs.unit().symbol()
249        );
250    }
251
252    /// Returns the difference between `self` and `other`, if both have the
253    /// same unit.
254    ///
255    /// # Panics
256    ///
257    /// Panics if `self` and `other` have different units.
258    fn sub(self, rhs: Self) -> Self {
259        if self.unit() == rhs.unit() {
260            return Self::new(self.amount() - rhs.amount(), self.unit());
261        }
262        panic!(
263            "Can't subtract '{}' and '{}'.",
264            self.unit().symbol(),
265            rhs.unit().symbol(),
266        );
267    }
268
269    /// Returns the quotient `self` / `other`, if both have the same unit.
270    ///
271    /// # Panics
272    ///
273    /// Panics if `self` and `other` have different units.
274    fn div(self, rhs: Self) -> AmountT {
275        if self.unit() == rhs.unit() {
276            return self.amount() / rhs.amount();
277        }
278        panic!(
279            "Can't divide '{}' and '{}'.",
280            self.unit().symbol(),
281            rhs.unit().symbol()
282        );
283    }
284
285    /// Formats `self` using the given formatter.
286    ///
287    /// # Errors
288    ///
289    /// This function will only return an instance of `Error` returned from
290    /// the formatter.
291    fn fmt(&self, form: &mut fmt::Formatter<'_>) -> fmt::Result {
292        if self.unit().symbol().is_empty() {
293            fmt::Display::fmt(&self.amount(), form)
294        } else {
295            let tmp: String;
296            let amnt_non_neg = self.amount() >= AMNT_ZERO;
297            #[cfg(feature = "fpdec")]
298            let abs_amnt = self.amount().abs();
299            #[cfg(not(feature = "fpdec"))]
300            let abs_amnt = if amnt_non_neg {
301                self.amount()
302            } else {
303                -self.amount()
304            };
305            if let Some(prec) = form.precision() {
306                tmp = format!("{:.*} {}", prec, abs_amnt, self.unit());
307            } else {
308                tmp = format!("{} {}", abs_amnt, self.unit());
309            }
310            form.pad_integral(amnt_non_neg, "", &tmp)
311        }
312    }
313}
314
315/// Trait for quantities having a reference unit
316pub trait HasRefUnit: Quantity + Add<Self> + Sub<Self> + Div<Self>
317where
318    <Self as Quantity>::UnitType: LinearScaledUnit,
319{
320    /// Unit used as reference for scaling the units of `Self::UnitType`.
321    const REF_UNIT: <Self as Quantity>::UnitType;
322
323    /// Returns `Some(unit)` where `unit.scale()` == `amnt`, or `None` if
324    /// there is no such unit.
325    #[must_use]
326    fn unit_from_scale(amnt: AmountT) -> Option<Self::UnitType> {
327        Self::iter_units().find(|&unit| unit.scale() == amnt)
328    }
329
330    /// Returns `factor` so that `factor` * `unit` == `self`.
331    #[inline(always)]
332    fn equiv_amount(&self, unit: Self::UnitType) -> AmountT {
333        if self.unit() == unit {
334            self.amount()
335        } else {
336            self.unit().ratio(&unit) * self.amount()
337        }
338    }
339
340    /// Returns `qty` where `qty` == `self` and `qty.unit()` is `to_unit`.
341    fn convert(&self, to_unit: Self::UnitType) -> Self {
342        Self::new(self.equiv_amount(to_unit), to_unit)
343    }
344
345    /// Returns true, if `self` and `other` have equivalent amounts, otherwise
346    /// `false`.
347    #[inline(always)]
348    fn eq(&self, other: &Self) -> bool {
349        self.amount() == other.equiv_amount(self.unit())
350    }
351
352    /// Returns the partial order of `self`s amount and `other`s eqivalent
353    /// amount in `self`s unit.
354    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
355        if self.unit() == other.unit() {
356            PartialOrd::partial_cmp(&self.amount(), &other.amount())
357        } else {
358            PartialOrd::partial_cmp(
359                &self.amount(),
360                &other.equiv_amount(self.unit()),
361            )
362        }
363    }
364
365    /// Returns the sum of `self` and `other`
366    #[inline]
367    fn add(self, rhs: Self) -> Self {
368        Self::new(self.amount() + rhs.equiv_amount(self.unit()), self.unit())
369    }
370
371    /// Returns the difference between `self` and `other`
372    #[inline]
373    fn sub(self, rhs: Self) -> Self {
374        Self::new(self.amount() - rhs.equiv_amount(self.unit()), self.unit())
375    }
376
377    /// Returns the quotient `self` / `other`
378    #[inline]
379    fn div(self, rhs: Self) -> AmountT {
380        self.amount() / rhs.equiv_amount(self.unit())
381    }
382
383    #[doc(hidden)]
384    /// Returns a new instance of the type implementing `HasRefUnit`,
385    /// equivalent to `amount * Self::REF_UNIT`, converted to the unit
386    /// with the greatest scale less than or equal to `amount` or - if
387    /// there is no such unit - to the unit with the smallest scale
388    /// greater than `amount`, in any case taking only SI units into
389    /// account if Self::REF_UNIT is a SI unit.
390    #[must_use]
391    fn _fit(amount: AmountT) -> Self {
392        let take_all = Self::REF_UNIT.si_prefix().is_none();
393        let mut it = Self::iter_units()
394            .filter(|u| take_all || u.si_prefix().is_some());
395        // `it` returns atleast the reference unit, so its safe to unwrap here
396        let first = it.next().unwrap();
397        let last = it
398            .filter(|u| u.scale() > first.scale() && u.scale() <= amount)
399            .last();
400        match last {
401            Some(unit) => Self::new(amount / unit.scale(), unit),
402            None => Self::new(amount / first.scale(), first),
403        }
404    }
405}
406
407/// The "unit" of the "unitless" quantity.
408#[derive(Copy, Clone, Debug, Eq, PartialEq)]
409pub enum One {
410    /// Special singleton used as "unit" for the "unitless" quantity.
411    One,
412}
413
414impl One {
415    const VARIANTS: [Self; 1] = [ONE];
416}
417
418/// Special singleton used as "unit" for the "unitless" quantity.
419pub const ONE: One = One::One;
420
421impl Unit for One {
422    type QuantityType = AmountT;
423    fn iter() -> impl Iterator<Item = Self> {
424        Self::VARIANTS.iter().cloned()
425    }
426    fn name(&self) -> String {
427        "One".to_owned()
428    }
429    fn symbol(&self) -> String {
430        "".to_owned()
431    }
432    fn si_prefix(&self) -> Option<SIPrefix> {
433        None
434    }
435}
436
437impl fmt::Display for One {
438    #[inline(always)]
439    fn fmt(&self, form: &mut fmt::Formatter<'_>) -> fmt::Result {
440        <Self as Unit>::fmt(self, form)
441    }
442}
443
444impl LinearScaledUnit for One {
445    const REF_UNIT: Self = ONE;
446    fn scale(&self) -> AmountT {
447        AMNT_ONE
448    }
449}
450
451impl Mul<One> for AmountT {
452    type Output = Self;
453    #[inline(always)]
454    fn mul(self, _rhs: One) -> Self::Output {
455        self
456    }
457}
458
459impl Mul<AmountT> for One {
460    type Output = AmountT;
461    #[inline(always)]
462    fn mul(self, rhs: AmountT) -> Self::Output {
463        rhs
464    }
465}
466
467impl Quantity for AmountT {
468    type UnitType = One;
469
470    #[inline(always)]
471    fn new(amount: AmountT, _unit: Self::UnitType) -> Self {
472        amount
473    }
474
475    #[inline(always)]
476    fn amount(&self) -> AmountT {
477        *self
478    }
479
480    #[inline(always)]
481    fn unit(&self) -> Self::UnitType {
482        ONE
483    }
484}
485
486impl HasRefUnit for AmountT {
487    const REF_UNIT: One = ONE;
488
489    #[inline(always)]
490    fn _fit(amount: AmountT) -> Self {
491        amount
492    }
493}