1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! Measure accuracy error tolerance ranges
use std::{ops::{self, RangeInclusive}, fmt::Display, error::Error, str::FromStr};

#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};


/// A failure to parse a mass error tolerance quantity from a string
#[derive(Debug, PartialEq, Eq)]
pub enum ToleranceParsingError {
    /// The unit isn't empty, but not recognized
    UnknownUnit,
    /// The magnitude of the error tolerated couldn't be determined
    InvalidMagnitude
}

impl Display for ToleranceParsingError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&format!("{:?}", self))
    }
}

impl Error for ToleranceParsingError {}

impl FromStr for Tolerance {
    type Err = ToleranceParsingError;

    /// Parse a string of the form "<magnitude:f64><unit:da|ppm?>"
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let n = s.len();
        if n <= 2 {
            return Err(ToleranceParsingError::InvalidMagnitude)
        }
        let s = s.to_lowercase();
        if s.ends_with("da") {
            if let Ok(magnitude) = s[0..n-2].parse::<f64>() {
                Ok(Self::Da(magnitude))
            } else {
                Err(ToleranceParsingError::InvalidMagnitude)
            }
        } else if s.ends_with("ppm") {
            if let Ok(magnitude) = s[0..n-3].parse::<f64>() {
                Ok(Self::PPM(magnitude))
            } else {
                Err(ToleranceParsingError::InvalidMagnitude)
            }
        } else {
            Err(ToleranceParsingError::UnknownUnit)
        }
    }
}


#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Tolerance {
    PPM(f64),
    Da(f64)
}

impl ToString for Tolerance {
    fn to_string(&self) -> String {
        match self {
            Self::Da(tol) => format!("{}Da", tol),
            Self::PPM(tol) => format!("{}PPM", tol),
        }
    }
}

impl Tolerance {

    /// The interval around `query` which is within this `Tolerance`
    /// instance's range.
    pub fn bounds(&self, query: f64) -> (f64, f64) {
        match self {
            Tolerance::PPM(tol) => {
                let width = query * *tol / 1e6;
                (query - width, query + width)
            }
            Tolerance::Da(tol) => {
                (query - *tol, query + *tol)
            }
        }
    }

    /// Compute the error between the two masses, in the appropriate units
    pub fn call(&self, query: f64, reference: f64) -> f64 {
        match self {
            Self::PPM(_tol) => {
                (query - reference) / reference * 1e6
            },
            Self::Da(_tol) => {
                query - reference
            }
        }
    }

    /// Return the numeric value of the error threshold in its units
    pub fn tol(&self) -> f64 {
        match self {
            Self::PPM(tol) => *tol,
            Self::Da(tol) => *tol
        }
    }

    /// Check if `query` is within the tolerated error interval around `reference`
    pub fn test(&self, query: f64, reference: f64) -> bool {
        let (lower_bound, upper_bound) = self.bounds(reference);
        query >= lower_bound && query <= upper_bound
    }

    /// Format the error between two masses with the appropriate units
    pub fn format_error(&self, query: f64, reference: f64) -> String {
        match self {
            Self::PPM(_tol) => {
                let magnitude = (query - reference) / reference * 1e6;
                format!("{}PPM", magnitude).to_string()
            },
            Self::Da(_tol) => {
                let magnitude = query - reference;
                format!("{}Da", magnitude).to_string()
            }
        }
    }

    pub fn as_range(&self, query: f64) -> RangeInclusive<f64> {
        let (low, hi) = self.bounds(query);
        RangeInclusive::new(low, hi)
    }
}

/// Tolerance objects can by scaled up or down by a floating point value
impl ops::Mul<f64> for Tolerance {
    type Output = Tolerance;

    fn mul(self, rhs: f64) -> Self::Output {
        match self {
            Self::Da(val) => Self::Da(rhs * val),
            Self::PPM(val) => Self::PPM(rhs * val)
        }
    }
}

impl From<f64> for Tolerance {
    fn from(value: f64) -> Self {
        Self::PPM(value)
    }
}