hapsi 0.0.3

A music theory package
Documentation
use std::{error::Error, fmt::Display, str::FromStr};

use crate::prelude::{AccidentalSymbol, Tone, ToneSymbol};

impl FromStr for Tone {
    type Err = ParseToneError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let tone = match s.get(0..1) {
            Some(tone) => tone.parse(),
            None => Err(ParseToneError::InvalidTone),
        };
        let acci = match s.get(1..) {
            Some(acci) => acci.parse(),
            None => Ok(AccidentalSymbol::Natural),
        };
        match (tone, acci) {
            (Ok(tone), Ok(acci)) => Ok(Tone::new(tone, acci)),
            (Err(e), _) => Err(e),
            (_, Err(e)) => Err(e),
        }
    }
}

impl FromStr for ToneSymbol {
    type Err = ParseToneError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "a" => Ok(ToneSymbol::A),
            "b" => Ok(ToneSymbol::B),
            "c" => Ok(ToneSymbol::C),
            "d" => Ok(ToneSymbol::D),
            "e" => Ok(ToneSymbol::E),
            "f" => Ok(ToneSymbol::F),
            "g" => Ok(ToneSymbol::G),
            _ => Err(ParseToneError::InvalidTone),
        }
    }
}

impl FromStr for AccidentalSymbol {
    type Err = ParseToneError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_ascii_lowercase().as_str() {
            "" => Ok(AccidentalSymbol::Sharp),
            "s" => Ok(AccidentalSymbol::Sharp),
            "" => Ok(AccidentalSymbol::Flat),
            "f" => Ok(AccidentalSymbol::Flat),
            "" => Ok(AccidentalSymbol::Natural),
            "" => Ok(AccidentalSymbol::Natural),
            _ => Err(ParseToneError::InvalidAccidential),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseToneError {
    InvalidTone,
    InvalidAccidential,
}

impl Display for ParseToneError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let message = match self {
            ParseToneError::InvalidTone => {
                "Invalid tone symbol: tone symbol {'a', 'b', 'c', 'd', 'e', 'f', 'g'} is accepted"
            }
            ParseToneError::InvalidAccidential => "Invalid accidental symbol",
        };
        write!(f, "{message}")
    }
}

impl Error for ParseToneError {}

#[cfg(test)]
mod tests {
    use crate::{
        parse::tone::ParseToneError,
        prelude::{AccidentalSymbol, Tone, ToneSymbol},
    };

    #[test]
    fn parse_tone_symbol() {
        assert_eq!("a".parse(), Ok(ToneSymbol::A));
        assert_eq!("ab".parse::<ToneSymbol>(), Err(ParseToneError::InvalidTone));
        assert_eq!("".parse::<ToneSymbol>(), Err(ParseToneError::InvalidTone));
    }

    #[test]
    fn parse_accidential() {
        assert_eq!("".parse(), Ok(AccidentalSymbol::Sharp));
        assert_eq!("s".parse(), Ok(AccidentalSymbol::Sharp));
        assert_eq!("".parse(), Ok(AccidentalSymbol::Flat));
        assert_eq!("f".parse(), Ok(AccidentalSymbol::Flat));
        assert_eq!("".parse(), Ok(AccidentalSymbol::Natural));
        assert_eq!("".parse(), Ok(AccidentalSymbol::Natural));
        assert_eq!(
            "ab".parse::<AccidentalSymbol>(),
            Err(ParseToneError::InvalidAccidential)
        );
        assert_eq!(
            "a".parse::<AccidentalSymbol>(),
            Err(ParseToneError::InvalidAccidential)
        );
    }

    #[test]
    fn parse_tone() {
        assert_eq!(
            "A".parse::<Tone>(),
            Ok(Tone::new(ToneSymbol::A, AccidentalSymbol::Natural))
        );
        assert_eq!(
            "A♯".parse::<Tone>(),
            Ok(Tone::new(ToneSymbol::A, AccidentalSymbol::Sharp))
        );
    }
}