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);
}
if let Some(y) = self.exact_f64_unreduced(x) {
return Ok(if y == 0.0 { 0.0 } else { y });
}
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 exact_f64_unreduced(&self, x: f64) -> Option<f64> {
let bits = x.to_bits();
let biased = i32::try_from((bits >> 52) & 0x7ff).ok()?;
let frac = bits & ((1_u64 << 52) - 1);
let (mut mant, mut exp) = if biased == 0 {
(frac, -1074_i32)
} else {
(frac | (1_u64 << 52), biased.checked_sub(1075)?)
};
if mant == 0 {
exp = 0;
} else {
let tz = mant.trailing_zeros();
mant = mant.checked_shr(tz)?;
exp = exp.checked_add(i32::try_from(tz).ok()?)?;
}
let mut smant = i128::from(mant);
if bits >> 63 == 1 {
smant = smant.checked_neg()?;
}
let pow = |scale: i32| -> Option<i128> {
if (0..=126).contains(&scale) {
1_i128.checked_shl(u32::try_from(scale).ok()?)
} else {
None
}
};
let (fnum, fden) = (self.factor.num(), self.factor.den());
let prod = fnum.checked_mul(smant)?;
let (num, den) = match (self.shape, exp >= 0) {
(Shape::Linear, true) => (prod.checked_mul(pow(exp)?)?, fden),
(Shape::Linear, false) => (prod, fden.checked_mul(pow(exp.checked_neg()?)?)?),
(Shape::Affine, true) => {
let (bn, bd) = (self.offset.num(), self.offset.den());
let total = prod
.checked_mul(pow(exp)?)?
.checked_mul(bd)?
.checked_add(bn.checked_mul(fden)?)?;
(total, fden.checked_mul(bd)?)
}
(Shape::Affine, false) => {
let (bn, bd) = (self.offset.num(), self.offset.den());
let scale = pow(exp.checked_neg()?)?;
let total = prod
.checked_mul(bd)?
.checked_add(bn.checked_mul(fden)?.checked_mul(scale)?)?;
(total, fden.checked_mul(bd)?.checked_mul(scale)?)
}
};
Ratio::frac_to_f64(num, den).ok()
}
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()
}
fn exact_f64_reference(c: &Conversion, x: f64) -> Result<f64> {
if !x.is_finite() {
return Err(Error::InvalidNumber {
input: x.to_string(),
});
}
if x == 0.0 && c.shape == Shape::Linear {
return Ok(x * c.factor_f64);
}
match Ratio::from_f64(x).and_then(|xr| c.apply_exact(xr)) {
Ok(y) => {
let y = y.to_f64()?;
Ok(if y == 0.0 { 0.0 } else { y })
}
Err(Error::Overflow { .. }) if c.shape == Shape::Linear => c.scaled_linear_f64(x),
Err(e) => Err(e),
}
}
#[test]
fn unreduced_exact_f64_is_identical_to_the_reduced_path() {
let convs = [
Conversion::linear(r(18, 5)).unwrap(),
Conversion::linear(r(381, 1250)).unwrap(),
Conversion::linear(r(1, 1_000_000_007)).unwrap(),
Conversion::linear(r(3_048, 10_000)).unwrap(),
Conversion::linear(r(1_000_000_000_000_000_000_000, 7)).unwrap(),
Conversion::affine(r(5, 9), r(-160, 9)).unwrap(),
Conversion::affine(r(9, 5), r(32, 1)).unwrap(),
Conversion::affine(r(1, 1), r(-546_300, 2_000)).unwrap(),
Conversion::affine(
r(1_000_000_000_000_000_000, 3),
r(1, 1_000_000_000_000_000_000),
)
.unwrap(),
];
let mut state = 0x2545_f491_4f6c_dd1d_u64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let mut xs = vec![
0.0,
-0.0,
1.0,
-1.0,
32.0,
98.6,
273.15,
f64::MIN_POSITIVE,
5e-324,
1e300,
-1e300,
1e-30,
1e30,
1e-310,
];
for _ in 0..20_000 {
let bits = next();
let exp = 1023 - 80 + (next() % 200);
let x = f64::from_bits((bits & 0x800f_ffff_ffff_ffff) | (exp << 52));
xs.push(x);
xs.push(f64::from(i32::try_from(next() % 2_000_000).unwrap() - 1_000_000) * 0.25);
}
for c in &convs {
for &x in &xs {
let got = c.apply_exact_f64(x);
let want = exact_f64_reference(c, x);
match (got, want) {
(Ok(g), Ok(w)) => assert_eq!(g.to_bits(), w.to_bits(), "{c:?} x={x:e}"),
(Err(g), Err(w)) => assert_eq!(g.code(), w.code(), "{c:?} x={x:e}"),
(g, w) => panic!("{c:?} x={x:e}: {g:?} vs {w:?}"),
}
}
}
}
#[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());
}
}