use std::{fmt::Display, str::FromStr};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use uom::fmt::DisplayStyle;
use crate::{
quantities::Multi,
system::{Mass, MassOverCharge, OrderedMass, OrderedRatio, Ratio, da},
};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum Tolerance<T> {
Relative(OrderedRatio),
Absolute(T),
}
impl<T> Tolerance<T> {
pub fn new_ppm(value: f64) -> Self {
Self::Relative(Ratio::new::<crate::system::ratio::ppm>(value).into())
}
pub fn new_relative(value: Ratio) -> Self {
Self::Relative(value.into())
}
pub fn new_absolute(value: impl Into<T>) -> Self {
Self::Absolute(value.into())
}
pub fn convert<O: From<T>>(self) -> Tolerance<O> {
match self {
Self::Relative(r) => Tolerance::Relative(r),
Self::Absolute(a) => Tolerance::Absolute(a.into()),
}
}
}
impl<T> Tolerance<T>
where
T: std::ops::Mul<Ratio, Output = T>
+ std::ops::Sub<T, Output = T>
+ std::ops::Add<T, Output = T>
+ Copy,
{
pub fn bounds(&self, value: impl Into<T>) -> (T, T) {
let value = value.into();
match self {
Self::Relative(tolerance) => (
value
* (Ratio::new::<crate::system::ratio::fraction>(1.0) - tolerance.into_inner()),
value
* (Ratio::new::<crate::system::ratio::fraction>(1.0) + tolerance.into_inner()),
),
Self::Absolute(tolerance) => (value - *tolerance, value + *tolerance),
}
}
}
impl Tolerance<OrderedMass> {
pub fn bounds(&self, value: Mass) -> (Mass, Mass) {
match self {
Self::Relative(tolerance) => (
value
* (Ratio::new::<crate::system::ratio::fraction>(1.0) - tolerance.into_inner()),
value
* (Ratio::new::<crate::system::ratio::fraction>(1.0) + tolerance.into_inner()),
),
Self::Absolute(tolerance) => (value - **tolerance, value + **tolerance),
}
}
}
impl<T: Display> Display for Tolerance<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Absolute(value) => format!("{value} abs"),
Self::Relative(tolerance) => format!("{} rel", tolerance.value),
}
)
}
}
impl Display for Tolerance<Mass> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Absolute(value) => format!(
"{}",
value.into_format_args(crate::system::mass::dalton, DisplayStyle::Abbreviation)
),
Self::Relative(tolerance) => format!(
"{}",
tolerance
.into_format_args(crate::system::ratio::ppm, DisplayStyle::Abbreviation)
),
}
)
}
}
impl FromStr for Tolerance<Mass> {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let num_str = String::from_utf8(
s.bytes()
.take_while(|c| {
c.is_ascii_digit()
|| *c == b'.'
|| *c == b'-'
|| *c == b'+'
|| *c == b'e'
|| *c == b'E'
})
.collect::<Vec<_>>(),
)
.map_err(|_| ())?;
let num = num_str.parse::<f64>().map_err(|_| ())?;
match s[num_str.len()..].trim().to_ascii_lowercase().as_str() {
"ppm" => Ok(Self::Relative(
Ratio::new::<crate::system::ratio::ppm>(num).into(),
)),
"da" => Ok(Self::Absolute(da(num))),
_ => Err(()),
}
}
}
impl Display for Tolerance<MassOverCharge> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Absolute(value) => format!(
"{}",
value.into_format_args(
crate::system::mass_over_charge::thomson,
DisplayStyle::Abbreviation
)
),
Self::Relative(tolerance) => format!(
"{}",
tolerance
.into_format_args(crate::system::ratio::ppm, DisplayStyle::Abbreviation)
),
}
)
}
}
impl FromStr for Tolerance<MassOverCharge> {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let num_str = String::from_utf8(
s.bytes()
.take_while(|c| {
c.is_ascii_digit()
|| *c == b'.'
|| *c == b'-'
|| *c == b'+'
|| *c == b'e'
|| *c == b'E'
})
.collect::<Vec<_>>(),
)
.map_err(|_| ())?;
let num = num_str.parse::<f64>().map_err(|_| ())?;
match s[num_str.len()..].trim().to_ascii_lowercase().as_str() {
"ppm" => Ok(Self::Relative(
Ratio::new::<crate::system::ratio::ppm>(num).into(),
)),
"th" | "m/z" | "mz" => Ok(Self::Absolute(MassOverCharge::new::<
crate::system::mass_over_charge::thomson,
>(num))),
_ => Err(()),
}
}
}
impl<T> TryFrom<&str> for Tolerance<T>
where
Self: FromStr,
{
type Error = <Self as FromStr>::Err;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
pub trait WithinTolerance<A, B> {
fn within(&self, a: &A, b: &B) -> bool;
}
impl WithinTolerance<MassOverCharge, MassOverCharge> for Tolerance<MassOverCharge> {
fn within(&self, a: &MassOverCharge, b: &MassOverCharge) -> bool {
match self {
Self::Absolute(tol) => (a.value - b.value).abs() <= tol.value,
Self::Relative(tolerance) => a.ppm(*b) <= tolerance.into_inner(),
}
}
}
impl WithinTolerance<Mass, Mass> for Tolerance<Mass> {
fn within(&self, a: &Mass, b: &Mass) -> bool {
match self {
Self::Absolute(tol) => (a.value - b.value).abs() <= tol.value,
Self::Relative(tolerance) => a.ppm(*b) <= tolerance.into_inner(),
}
}
}
impl WithinTolerance<Multi<Mass>, Multi<Mass>> for Tolerance<Mass> {
fn within(&self, a: &Multi<Mass>, b: &Multi<Mass>) -> bool {
a.iter()
.cartesian_product(b.iter())
.any(|(a, b)| self.within(a, b))
}
}
impl WithinTolerance<Multi<Mass>, Mass> for Tolerance<Mass> {
fn within(&self, a: &Multi<Mass>, b: &Mass) -> bool {
a.iter().any(|a| self.within(a, b))
}
}
impl WithinTolerance<Mass, Multi<Mass>> for Tolerance<Mass> {
fn within(&self, a: &Mass, b: &Multi<Mass>) -> bool {
b.iter().any(|b| self.within(a, b))
}
}
impl WithinTolerance<Mass, Mass> for Tolerance<OrderedMass> {
fn within(&self, a: &Mass, b: &Mass) -> bool {
match self {
Self::Absolute(tol) => (a.value - b.value).abs() <= tol.value,
Self::Relative(tolerance) => a.ppm(*b) <= tolerance.into_inner(),
}
}
}
impl WithinTolerance<Multi<Mass>, Multi<Mass>> for Tolerance<OrderedMass> {
fn within(&self, a: &Multi<Mass>, b: &Multi<Mass>) -> bool {
a.iter()
.cartesian_product(b.iter())
.any(|(a, b)| self.within(a, b))
}
}
impl WithinTolerance<Multi<Mass>, Mass> for Tolerance<OrderedMass> {
fn within(&self, a: &Multi<Mass>, b: &Mass) -> bool {
a.iter().any(|a| self.within(a, b))
}
}
impl WithinTolerance<Mass, Multi<Mass>> for Tolerance<OrderedMass> {
fn within(&self, a: &Mass, b: &Multi<Mass>) -> bool {
b.iter().any(|b| self.within(a, b))
}
}