use crate::error::{Error, Result};
use crate::kind::Shape;
use crate::ratio::Ratio;
use crate::unit::UnitExpr;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Conversion {
shape: Shape,
factor: Ratio,
offset: Ratio,
factor_f64: f64,
offset_f64: f64,
shift_f64: f64,
shift_lo_f64: f64,
}
impl Conversion {
pub fn linear(factor: Ratio) -> Result<Self> {
Self::new(factor, Ratio::ZERO)
}
pub fn affine(factor: Ratio, offset: Ratio) -> Result<Self> {
Self::new(factor, offset)
}
fn new(factor: Ratio, offset: Ratio) -> Result<Self> {
if factor.is_zero() {
return Err(Error::InvalidDefinition {
symbol: String::from("conversion"),
message: String::from("factor must not be zero"),
});
}
let shape = if offset.is_zero() {
Shape::Linear
} else {
Shape::Affine
};
let shift = offset.checked_div(factor)?;
let shift_f64 = shift.to_f64()?;
let shift_lo_f64 = Ratio::from_f64(shift_f64)
.and_then(|hi| shift.checked_sub(hi))
.and_then(Ratio::to_f64)
.unwrap_or(0.0);
Ok(Self {
shape,
factor,
offset,
factor_f64: factor.to_f64()?,
offset_f64: offset.to_f64()?,
shift_f64,
shift_lo_f64,
})
}
pub fn between(from: &UnitExpr, to: &UnitExpr) -> Result<Self> {
if from.dim() != to.dim() {
return Err(Error::DimensionMismatch {
from: from.dim(),
to: to.dim(),
});
}
if from.kind() != to.kind() {
let mut suggestions: Vec<String> = Vec::new();
for u in [from, to] {
if let Some(d) = u.difference_symbol() {
if !suggestions.iter().any(|s| s == d) {
suggestions.push(d.to_string());
}
}
}
return Err(Error::KindMismatch {
from: from.kind(),
to: to.kind(),
operation: "conversion",
suggestions,
});
}
let factor = from.factor().checked_div(to.factor())?;
let offset = from
.offset()
.checked_sub(to.offset())?
.checked_div(to.factor())?;
Self::new(factor, offset)
}
#[must_use]
pub const fn shape(&self) -> Shape {
self.shape
}
#[must_use]
pub const fn factor(&self) -> Ratio {
self.factor
}
#[must_use]
pub const fn offset(&self) -> Ratio {
self.offset
}
#[must_use]
pub const fn factor_f64(&self) -> f64 {
self.factor_f64
}
#[must_use]
pub const fn offset_f64(&self) -> f64 {
self.offset_f64
}
pub fn inverse(&self) -> Result<Self> {
let inv = self.factor.checked_recip()?;
let off = self.offset.checked_neg()?.checked_mul(inv)?;
Self::new(inv, off)
}
pub fn then(&self, next: &Self) -> Result<Self> {
let factor = self.factor.checked_mul(next.factor)?;
let offset = self
.offset
.checked_mul(next.factor)?
.checked_add(next.offset)?;
Self::new(factor, offset)
}
#[inline]
#[must_use]
pub fn apply(&self, x: f64) -> f64 {
match self.shape {
Shape::Affine => ((x + self.shift_f64) + self.shift_lo_f64) * self.factor_f64,
Shape::Linear => x * self.factor_f64,
}
}
#[inline]
pub fn apply_slice(&self, input: &[f64], output: &mut [f64]) -> Result<()> {
if input.len() != output.len() {
return Err(Error::LengthMismatch {
input: input.len(),
output: output.len(),
});
}
let a = self.factor_f64;
match self.shape {
Shape::Affine => {
let (s, lo) = (self.shift_f64, self.shift_lo_f64);
for (y, x) in output.iter_mut().zip(input) {
*y = ((*x + s) + lo) * a;
}
}
Shape::Linear => {
for (y, x) in output.iter_mut().zip(input) {
*y = *x * a;
}
}
}
Ok(())
}
#[inline]
pub fn apply_in_place(&self, data: &mut [f64]) {
let a = self.factor_f64;
match self.shape {
Shape::Affine => {
let (s, lo) = (self.shift_f64, self.shift_lo_f64);
for v in data.iter_mut() {
*v = ((*v + s) + lo) * a;
}
}
Shape::Linear => {
for v in data.iter_mut() {
*v *= a;
}
}
}
}
#[must_use]
pub fn apply_vec(&self, input: &[f64]) -> Vec<f64> {
let mut out = input.to_vec();
self.apply_in_place(&mut out);
out
}
pub fn apply_exact(&self, x: Ratio) -> Result<Ratio> {
self.factor.checked_mul(x)?.checked_add(self.offset)
}
pub fn apply_exact_f64(&self, x: f64) -> Result<f64> {
if !x.is_finite() {
return Err(Error::InvalidNumber {
input: x.to_string(),
});
}
if x == 0.0 && self.shape == Shape::Linear {
return Ok(x * self.factor_f64);
}
match Ratio::from_f64(x).and_then(|xr| self.apply_exact(xr)) {
Ok(y) => {
let y = y.to_f64()?;
Ok(if y == 0.0 { 0.0 } else { y })
}
Err(Error::Overflow { .. }) if self.shape == Shape::Linear => self.scaled_linear_f64(x),
Err(e) => Err(e),
}
}
fn scaled_linear_f64(&self, x: f64) -> Result<f64> {
let err = || Error::Overflow {
operation: "exact f64 result outside the normal range",
};
let bits = x.abs().to_bits();
let biased = i32::try_from(bits >> 52).map_err(|_| err())?;
let frac = bits & ((1_u64 << 52) - 1);
let (mut mant, mut exp2) = if biased == 0 {
(frac, -1074_i32)
} else {
(
frac | (1_u64 << 52),
biased.checked_sub(1075).ok_or_else(err)?,
)
};
let tz = mant.trailing_zeros();
mant = mant.checked_shr(tz).ok_or_else(err)?;
exp2 = exp2
.checked_add(i32::try_from(tz).map_err(|_| err())?)
.ok_or_else(err)?;
let signed = if x.is_sign_negative() {
i128::from(mant).checked_neg().ok_or_else(err)?
} else {
i128::from(mant)
};
let v = self.factor.checked_mul(Ratio::from_int(signed))?.to_f64()?;
let vbits = v.abs().to_bits();
let vbiased = i32::try_from(vbits >> 52).map_err(|_| err())?;
let new_biased = vbiased.checked_add(exp2).ok_or_else(err)?;
if !(1..=2046).contains(&new_biased) {
return Err(err());
}
let sign = v.to_bits() & (1_u64 << 63);
let frac = vbits & ((1_u64 << 52) - 1);
let new = u64::try_from(new_biased).map_err(|_| err())?;
Ok(f64::from_bits(sign | (new << 52) | frac))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn r(n: i128, d: i128) -> Ratio {
Ratio::new(n, d).unwrap()
}
#[test]
fn linear_and_inverse() {
let c = Conversion::linear(r(18, 5)).unwrap();
assert_eq!(c.shape(), Shape::Linear);
assert_eq!(c.apply(10.0), 36.0);
let inv = c.inverse().unwrap();
assert_eq!(inv.factor(), r(5, 18));
assert_eq!(
inv.apply_exact(c.apply_exact(r(7, 3)).unwrap()).unwrap(),
r(7, 3)
);
}
#[test]
fn fahrenheit_to_celsius_is_exact_at_zero_point() {
let c = Conversion::affine(r(5, 9), r(-160, 9)).unwrap();
assert_eq!(c.shape(), Shape::Affine);
assert_eq!(c.apply(32.0), 0.0);
assert_eq!(c.apply(212.0), 100.0);
assert_eq!(c.apply_exact(r(32, 1)).unwrap(), Ratio::ZERO);
let back = c.inverse().unwrap();
assert_eq!(back.apply_exact(r(100, 1)).unwrap(), r(212, 1));
}
#[test]
fn composition() {
let a = Conversion::linear(r(1000, 1)).unwrap();
let b = Conversion::affine(r(1, 1), r(1, 2)).unwrap();
let ab = a.then(&b).unwrap();
assert_eq!(
ab.apply_exact(r(2, 1)).unwrap(),
b.apply_exact(a.apply_exact(r(2, 1)).unwrap()).unwrap()
);
}
#[test]
fn slices() {
let c = Conversion::linear(r(3, 1)).unwrap();
let input = [1.0, 2.0, 3.0];
let mut out = [0.0; 3];
c.apply_slice(&input, &mut out).unwrap();
assert_eq!(out, [3.0, 6.0, 9.0]);
let mut short = [0.0; 2];
assert_eq!(
c.apply_slice(&input, &mut short),
Err(Error::LengthMismatch {
input: 3,
output: 2
})
);
assert_eq!(short, [0.0, 0.0]);
assert_eq!(c.apply_vec(&input), vec![3.0, 6.0, 9.0]);
c.apply_slice(&[], &mut []).unwrap();
}
#[test]
fn ieee_special_values_pass_through() {
let c = Conversion::linear(r(1, 3)).unwrap();
assert!(c.apply(f64::NAN).is_nan());
assert_eq!(c.apply(f64::INFINITY), f64::INFINITY);
assert_eq!(c.apply(f64::NEG_INFINITY), f64::NEG_INFINITY);
assert_eq!(c.apply(-0.0).to_bits(), (-0.0_f64).to_bits());
let big = Conversion::linear(r(1_000_000, 1)).unwrap();
assert_eq!(big.apply(f64::MAX), f64::INFINITY);
}
#[test]
fn rejects_zero_factor() {
assert!(Conversion::linear(Ratio::ZERO).is_err());
}
}