ireal-parser 0.1.0

iReal Pro song parser and manipulation library
Documentation
use std::{fmt, str::FromStr};

use crate::{Error, Result};

/// Represents a staff text
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum StaffTextKind {
    DCAlCoda,
    DCAlFine,
    DCAl1st,
    DCAl2nd,
    DCAl3rd,
    DSAlCoda,
    DSAlFine,
    DSAl1st,
    DSAl2nd,
    DSAl3rd,
    Fine,
    Free(String),
    Repeat(u32),
}

impl fmt::Display for StaffTextKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use StaffTextKind::*;

        let text = match self {
            DCAlCoda => "D.C. al Coda".into(),
            DCAlFine => "D.C. al Fine".into(),
            DCAl1st => "D.C. al 1st End".into(),
            DCAl2nd => "D.C. al 2nd End".into(),
            DCAl3rd => "D.C. al 3rd End".into(),
            DSAlCoda => "D.S. al Coda".into(),
            DSAlFine => "D.S. al Fine".into(),
            DSAl1st => "D.S. al 1st End".into(),
            DSAl2nd => "D.S. al 2nd End".into(),
            DSAl3rd => "D.S. al 3rd End".into(),
            Fine => "Fine".into(),
            Free(text) => text.clone(),
            Repeat(n) => format!("{n}x"),
        };

        write!(f, "{text}")
    }
}

impl FromStr for StaffTextKind {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        use StaffTextKind::*;

        let res = match s {
            "D.C. al Coda" => DCAlCoda,
            "D.C. al Fine" => DCAlFine,
            "D.C. al 1st End" => DCAl1st,
            "D.C. al 2nd End" => DCAl2nd,
            "D.C. al 3rd End" => DCAl3rd,
            "D.S. al Coda" => DSAlCoda,
            "D.S. al Fine" => DSAlFine,
            "D.S. al 1st End" => DSAl1st,
            "D.S. al 2nd End" => DSAl2nd,
            "D.S. al 3rd End" => DSAl3rd,
            "Fine" => Fine,
            _ => {
                if s.ends_with('x') {
                    let num_str = &s[0..s.len() - 1];
                    match num_str.parse() {
                        Ok(n) => Repeat(n),
                        _ => Free(s.into()),
                    }
                } else {
                    Free(s.into())
                }
            }
        };

        Ok(res)
    }
}

/// Represents some staff text
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct StaffText {
    pub text: StaffTextKind,
    /// You can move the text upwards relative to the current chord by adding a
    /// * followed by two digit number between 00 (below the system) and 74
    /// (above the system)
    pub position: Option<u8>,
}

impl StaffText {
    pub fn new(text: StaffTextKind) -> Self {
        Self {
            text,
            position: None,
        }
    }

    pub fn set_position(&mut self, position: Option<u8>) {
        self.position = position;
    }
}

impl fmt::Display for StaffText {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let pos = self.position.map(|p| format!("*{p}")).unwrap_or_default();
        let text = &self.text;

        write!(f, "<{pos}{text}>")
    }
}

impl FromStr for StaffText {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        if !s.starts_with('<') || !s.ends_with('>') {
            return Err(Error::InvalidStaffText);
        }

        let s = &s[1..s.len() - 1];

        let (pos, text) = if let Some(s) = s.strip_prefix('*') {
            let digit_count = s.chars().take_while(|c| c.is_ascii_digit()).count();
            let (number_str, trailing_str) = s.split_at(digit_count);
            (number_str.parse().ok(), trailing_str)
        } else {
            (None, s)
        };

        let mut res = Self::new(text.parse()?);
        res.set_position(pos);
        Ok(res)
    }
}

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

    #[test]
    fn test_to_string() {
        let mut s = StaffText::new(StaffTextKind::Free("Some text".into()));
        s.set_position(Some(12));
        assert_eq!(s.to_string(), "<*12Some text>");

        let st: StaffText = "<*12Some text>".parse().unwrap();
        assert_eq!(st, s);
    }
}