mzdeisotoper 0.3.5

Deisotoping and charge state deconvolution of mass spectrometry files
Documentation
use std::{error::Error, fmt::Display, num::ParseFloatError, ops::Range, str::FromStr};

use serde::{Deserialize, Serialize};

use mzpeaks::coordinate::Span1D;

#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
pub struct TimeRange {
    pub start: f64,
    pub end: f64,
}

impl TimeRange {
    pub fn new(start: f64, end: f64) -> Self {
        Self { start, end }
    }
}

impl Span1D for TimeRange {
    type DimType = f64;

    fn start(&self) -> Self::DimType {
        self.start
    }

    fn end(&self) -> Self::DimType {
        self.end
    }
}

impl Default for TimeRange {
    fn default() -> Self {
        Self {
            start: 0.0,
            end: f64::INFINITY,
        }
    }
}

#[derive(Debug)]
pub enum TimeRangeParseError {
    MalformedStart(ParseFloatError),
    MalformedEnd(ParseFloatError),
}

impl Display for TimeRangeParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TimeRangeParseError::MalformedStart(e) => {
                write!(f, "Failed to parse time range start {e}")
            }
            TimeRangeParseError::MalformedEnd(e) => {
                write!(f, "Failed to parse time range end {e}")
            }
        }
    }
}

impl Error for TimeRangeParseError {}

impl FromStr for TimeRange {
    type Err = TimeRangeParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut tokens = if s.contains(' ') {
            s.split(' ')
        } else if s.contains(':') {
            s.split(':')
        } else if s.contains('-') {
            s.split('-')
        } else {
            s.split(' ')
        };
        let start_s = tokens.next().unwrap();
        let start_t = if start_s.is_empty() {
            0.0
        } else {
            match start_s.parse() {
                Ok(val) => val,
                Err(e) => return Err(TimeRangeParseError::MalformedStart(e)),
            }
        };
        let end_s = tokens.next().unwrap();
        let end_t = if end_s.is_empty() {
            f64::INFINITY
        } else {
            match end_s.parse() {
                Ok(val) => val,
                Err(e) => return Err(TimeRangeParseError::MalformedEnd(e)),
            }
        };
        Ok(TimeRange {
            start: start_t,
            end: end_t,
        })
    }
}

impl From<Range<f64>> for TimeRange {
    fn from(value: Range<f64>) -> Self {
        Self::new(value.start, value.end)
    }
}

impl From<(f64, f64)> for TimeRange {
    fn from(value: (f64, f64)) -> Self {
        Self::new(value.0, value.1)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_time_fromstr() -> Result<(), TimeRangeParseError> {
        let t: TimeRange = "52.0-".parse()?;
        assert_eq!(t.start(), 52.0);
        assert_eq!(t.end(), f64::INFINITY);

        let t: TimeRange = "-52.0".parse()?;
        assert_eq!(t.start(), 0.0);
        assert_eq!(t.end(), 52.0);

        let t: TimeRange = "32-52.0".parse()?;
        assert_eq!(t.start(), 32.0);
        assert_eq!(t.end(), 52.0);

        let t: TimeRange = "-".parse()?;
        assert_eq!(t.start(), 0.0);
        assert_eq!(t.end(), f64::INFINITY);

        Ok(())
    }

    #[test]
    fn test_time_fromstr_malformed() -> Result<(), TimeRangeParseError> {
        match "a-".parse::<TimeRange>() {
            Ok(_) => {
                panic!("Can't happen")
            }
            Err(e) => match e {
                TimeRangeParseError::MalformedStart(_) => {}
                TimeRangeParseError::MalformedEnd(_) => {
                    panic!("The problem is at the start")
                }
            },
        }

        match "-b".parse::<TimeRange>() {
            Ok(_) => {
                panic!("Can't happen")
            }
            Err(e) => match e {
                TimeRangeParseError::MalformedStart(_) => {
                    panic!("The problem is at the end")
                }
                TimeRangeParseError::MalformedEnd(_) => {}
            },
        }

        match "a-b".parse::<TimeRange>() {
            Ok(_) => {
                panic!("Can't happen")
            }
            Err(e) => match e {
                TimeRangeParseError::MalformedStart(_) => {}
                TimeRangeParseError::MalformedEnd(_) => {
                    panic!("The problem is at both ends, but err on the start first")
                }
            },
        }

        Ok(())
    }
}