#![doc = include_str!("../README.md")]
#![cfg_attr(
feature = "document-features",
cfg_attr(doc, doc = ::document_features::document_features!())
)]
use std::cmp::Ordering;
use num_traits::{checked_pow, ConstZero, PrimInt, ToPrimitive};
mod string;
pub use string::{DisplayAdapter, EngineeringRepr};
#[cfg(feature = "serde")]
mod serde_support;
#[derive(Debug, Clone, Copy, Default)]
pub struct EngineeringQuantity<T: EQSupported<T>> {
significand: T,
exponent: i8,
}
pub trait EQSupported<T: PrimInt>:
PrimInt
+ std::fmt::Display
+ ConstZero
+ SignHelper<T>
+ TryInto<i64>
+ TryInto<i128>
+ TryInto<u64>
+ TryInto<u128>
{
const EXPONENT_BASE: T;
}
macro_rules! supported_types {
{$($t:ty),+} => {$(
impl<> EQSupported<$t> for $t {
const EXPONENT_BASE: $t = 1000;
}
)+}
}
supported_types!(i16, i32, i64, i128, isize, u16, u32, u64, u128, usize);
#[derive(Debug, Clone)]
pub struct AbsAndSign<T: PrimInt> {
abs: T,
negative: bool,
}
pub trait SignHelper<T: PrimInt> {
fn abs_and_sign(&self) -> AbsAndSign<T>;
}
macro_rules! impl_unsigned_helpers {
{$($t:ty),+} => {$(
impl<> SignHelper<$t> for $t {
fn abs_and_sign(&self) -> AbsAndSign<$t> {
AbsAndSign { abs: *self, negative: false }
}
}
)+}
}
macro_rules! impl_signed_helpers {
{$($t:ty),+} => {$(
impl<> SignHelper<$t> for $t {
fn abs_and_sign(&self) -> AbsAndSign<$t> {
AbsAndSign { abs: self.abs(), negative: self.is_negative() }
}
}
)+}
}
impl_unsigned_helpers!(u16, u32, u64, u128, usize);
impl_signed_helpers!(i16, i32, i64, i128, isize);
impl<T: EQSupported<T>> EngineeringQuantity<T> {
pub fn from_raw(significand: T, exponent: i8) -> Result<Self, Error> {
Self::from_raw_unchecked(significand, exponent).check_for_int_overflow()
}
#[must_use]
pub fn to_raw(self) -> (T, i8) {
(self.significand, self.exponent)
}
fn from_raw_unchecked(significand: T, exponent: i8) -> Self {
Self {
significand,
exponent,
}
}
}
impl<T: EQSupported<T> + From<EngineeringQuantity<T>>> PartialEq for EngineeringQuantity<T> {
fn eq(&self, other: &Self) -> bool {
if self.exponent == other.exponent {
return self.significand == other.significand;
}
let cmp = self.partial_cmp(other);
matches!(cmp, Some(Ordering::Equal))
}
}
impl<T: EQSupported<T> + From<EngineeringQuantity<T>>> Eq for EngineeringQuantity<T> {}
impl<T: EQSupported<T> + From<EngineeringQuantity<T>>> PartialOrd for EngineeringQuantity<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<T: EQSupported<T> + From<EngineeringQuantity<T>>> Ord for EngineeringQuantity<T> {
fn cmp(&self, other: &Self) -> Ordering {
let v1 = <T as From<EngineeringQuantity<T>>>::from(*self);
let v2 = <T as From<EngineeringQuantity<T>>>::from(*other);
v1.cmp(&v2)
}
}
impl<T: EQSupported<T>> EngineeringQuantity<T> {
pub fn convert<U: EQSupported<U> + From<T>>(&self) -> EngineeringQuantity<U> {
let (sig, exp) = self.to_raw();
EngineeringQuantity::<U>::from_raw_unchecked(sig.into(), exp)
}
pub fn try_convert<U: EQSupported<U> + TryFrom<T>>(
&self,
) -> Result<EngineeringQuantity<U>, Error> {
let (sig, exp) = self.to_raw();
EngineeringQuantity::<U>::from_raw(sig.try_into().map_err(|_| Error::Overflow)?, exp)
}
}
impl<T: EQSupported<T>> EngineeringQuantity<T> {
#[must_use]
pub fn normalise(self) -> Self {
let mut working = self;
loop {
let (div, rem) = (
working.significand / T::EXPONENT_BASE,
working.significand % T::EXPONENT_BASE,
);
if rem != T::ZERO {
break;
}
working.significand = div;
working.exponent += 1;
}
working
}
}
impl<T: EQSupported<T>, U: EQSupported<U>> From<T> for EngineeringQuantity<U>
where
U: From<T>,
{
fn from(value: T) -> Self {
Self {
significand: value.into(),
exponent: 0,
}
}
}
impl<T: EQSupported<T>> EngineeringQuantity<T> {
fn check_for_int_overflow(self) -> Result<Self, Error> {
if self.exponent < 0 {
return Ok(self);
}
let exp: usize = self.exponent.unsigned_abs().into();
let Some(factor) = checked_pow(T::EXPONENT_BASE, exp) else {
return Err(Error::Overflow);
};
let result: T = factor * self.significand;
let _ = std::convert::TryInto::<T>::try_into(result).map_err(|_| Error::Overflow)?;
Ok(self)
}
}
macro_rules! impl_from {
{$($t:ty),+} => {$(
impl<T: EQSupported<T>> From<EngineeringQuantity<T>> for $t
where $t: From<T>,
{
#[doc = concat!("\
Conversion to the same storage type (or a larger type)
is infallible due to the checks at construction time.
")]
fn from(eq: EngineeringQuantity<T>) -> Self {
let abs_exp: usize = eq.exponent.unsigned_abs().into();
let factor: Self = num_traits::pow(T::EXPONENT_BASE.into(), abs_exp);
if eq.exponent > 0 {
Self::from(eq.significand) * factor
} else {
Self::from(eq.significand) / factor
}
}
}
)+}
}
impl_from!(u16, u32, u64, u128, usize, i16, i32, i64, i128, isize);
impl<T: EQSupported<T>> EngineeringQuantity<T> {
fn apply_factor<U: EQSupported<U>>(self, sig: U) -> U {
let abs_exp: usize = self.exponent.unsigned_abs().into();
let factor: U = num_traits::pow(U::EXPONENT_BASE, abs_exp);
if self.exponent >= 0 {
sig * factor
} else {
sig / factor
}
}
}
impl<T: EQSupported<T>> ToPrimitive for EngineeringQuantity<T> {
fn to_i64(&self) -> Option<i64> {
let i: i64 = match self.significand.try_into() {
Ok(ii) => ii,
Err(_) => return None,
};
Some(self.apply_factor(i))
}
fn to_u64(&self) -> Option<u64> {
let i: u64 = match self.significand.try_into() {
Ok(ii) => ii,
Err(_) => return None,
};
Some(self.apply_factor(i))
}
fn to_i128(&self) -> Option<i128> {
let i: i128 = match self.significand.try_into() {
Ok(ii) => ii,
Err(_) => return None,
};
Some(self.apply_factor(i))
}
fn to_u128(&self) -> Option<u128> {
let i: u128 = match self.significand.try_into() {
Ok(ii) => ii,
Err(_) => return None,
};
Some(self.apply_factor(i))
}
}
#[derive(Clone, Copy, Debug, PartialEq, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Numeric overflow")]
Overflow,
#[error("Numeric underflow")]
Underflow,
#[error("The string could not be parsed")]
ParseError,
}
#[cfg(test)]
mod test {
use super::EngineeringQuantity as EQ;
use super::Error as EQErr;
#[test]
fn integers() {
for i in &[1i64, -1, 100, -100, 1000, 4000, -4000, 4_000_000] {
let ee = EQ::from_raw(*i, 0).unwrap();
assert_eq!(i64::from(ee), *i);
let ee2 = EQ::from_raw(*i, 1).unwrap();
assert_eq!(i64::from(ee2), *i * 1000, "input is {}", *i);
}
}
#[test]
fn equality() {
for (a, b, c, d) in &[
(1i64, 0, 1i64, 0),
(1, 1, 1000, 0),
(2000, 0, 2, 1),
(123_000_000, 0, 123_000, 1),
(123_000_000, 0, 123, 2),
(456_000_000_000_000, 0, 456_000, 3),
(456_000_000_000_000, 0, 456, 4),
] {
let e1 = EQ::from_raw(*a, *b);
let e2 = EQ::from_raw(*c, *d);
assert_eq!(e1, e2);
}
}
#[test]
fn conversion() {
let t = EQ::<u32>::from_raw(12345, 0).unwrap();
let u = t.convert::<u64>();
assert_eq!(u.to_raw().0, <u32 as Into<u64>>::into(t.to_raw().0));
assert_eq!(t.to_raw().1, u.to_raw().1);
}
#[test]
fn overflow() {
let t = EQ::<u32>::from_raw(100_000, 0).unwrap();
let _ = t.try_convert::<u16>().expect_err("TryFromIntError");
assert_eq!(EQ::<u32>::from_raw(1, 5), Err(EQErr::Overflow));
}
#[test]
fn normalise() {
let q = EQ::from_raw(1_000_000, 0).unwrap();
let q2 = q.normalise();
assert_eq!(q, q2);
assert_eq!(q2.to_raw(), (1, 2));
}
#[test]
fn to_primitive() {
use num_traits::ToPrimitive as _;
let e = EQ::<i128>::from_raw(1234, 0).unwrap();
assert_eq!(e.to_i8(), None);
assert_eq!(e.to_i16(), Some(1234));
assert_eq!(e.to_i32(), Some(1234));
assert_eq!(e.to_i64(), Some(1234));
assert_eq!(e.to_i128(), Some(1234));
assert_eq!(e.to_isize(), Some(1234));
assert_eq!(e.to_u8(), None);
assert_eq!(e.to_u16(), Some(1234));
assert_eq!(e.to_u32(), Some(1234));
assert_eq!(e.to_u64(), Some(1234));
assert_eq!(e.to_u128(), Some(1234));
assert_eq!(e.to_usize(), Some(1234));
let e = EQ::<i128>::from_raw(-1, 0).unwrap();
assert_eq!(e.to_u64(), None);
assert_eq!(e.to_u128(), None);
let e = EQ::<u128>::from_raw(u128::MAX, 0).unwrap();
assert_eq!(e.to_i64(), None);
assert_eq!(e.to_i128(), None);
let e = EQ::from_raw(1, -1).unwrap();
assert_eq!(e.to_i32(), Some(0));
let e = EQ::from_raw(1001, -1).unwrap();
assert_eq!(e.to_i32(), Some(1));
let e = EQ::from_raw(-1, -1).unwrap();
assert_eq!(e.to_i32(), Some(0));
let e = EQ::from_raw(-1001, -1).unwrap();
assert_eq!(e.to_i32(), Some(-1));
}
}