Skip to main content

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