ireal-parser 0.1.0

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

use crate::{Chord, Error, Result, StaffText, TimeSignature};

/// Represents a single element of a chord progression.
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum ProgressionElement {
    SingleBarLine,
    OpeningDoubleBarLine,
    ClosingDoubleBarLine,
    OpeningRepeatBarLine,
    ClosingRepeatBarLine,
    FinalThickDoubleBarLine,
    SmallChord,
    LargeChord,
    Chord(Chord),
    AlternateChord(Chord),
    NoChord,
    RepeatOneMeasure,
    RepeatTwoMeasures,
    TimeSignature(TimeSignature),
    /// A, B, C, D sections
    Section(String),
    Verse,
    Intro,
    Segno,
    Coda,
    Fermata,
    /// N0, N1, N2.. ending
    Ending(u8),
    StaffText(StaffText),
    VerticalSpace(u8),
    Divider,
    Slash,
}

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

        match self {
            SingleBarLine => write!(f, "|"),
            OpeningDoubleBarLine => write!(f, "["),
            ClosingDoubleBarLine => write!(f, "]"),
            OpeningRepeatBarLine => write!(f, "{{"),
            ClosingRepeatBarLine => write!(f, "}}"),
            FinalThickDoubleBarLine => write!(f, "Z"),
            Chord(chord) => write!(f, "{} ", chord),
            AlternateChord(chord) => write!(f, "({}) ", chord),
            NoChord => write!(f, "n"),
            TimeSignature(ts) => write!(f, "{ts}"),
            Section(rm) => write!(f, "*{}", rm),
            Verse => write!(f, "*V"),
            Intro => write!(f, "*i"),
            Segno => write!(f, "S"),
            Coda => write!(f, "Q"),
            Fermata => write!(f, "f"),
            Ending(ending) => write!(f, "N{ending}"),
            StaffText(text) => write!(f, "{text}"),
            VerticalSpace(n) => write!(f, "{}", "Y".repeat((*n).into())),
            Divider => write!(f, ","),
            Slash => write!(f, "p"),
            SmallChord => write!(f, "s"),
            LargeChord => write!(f, "l"),
            RepeatOneMeasure => write!(f, " x "),
            RepeatTwoMeasures => write!(f, " r"),
        }
    }
}

/// Represents a chord progression as a sequence of [`ProgressionElement`]s.
#[derive(Debug, PartialEq, Clone)]
pub struct Progression(Vec<ProgressionElement>);

impl Progression {
    /// Create a new `Progression` from a `Vec<ProgressionElement>`.
    pub fn new(elements: Vec<ProgressionElement>) -> Self {
        Progression(elements)
    }

    /// Get a reference to the internal `Vec<ProgressionElement>`.
    pub fn elements(&self) -> &Vec<ProgressionElement> {
        &self.0
    }

    /// Get a mutable reference to the internal `Vec<ProgressionElement>`.
    pub fn elements_mut(&mut self) -> &mut Vec<ProgressionElement> {
        &mut self.0
    }
}

impl fmt::Display for Progression {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s: String = self.0.iter().map(|e| e.to_string()).collect();
        write!(f, "{s}")
    }
}

impl FromStr for Progression {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        parse_irealbook_progression(s)
    }
}

/// Parses an iReal Pro progression string and returns a [`Progression`].
///
/// # Arguments
///
/// * `progression` - A string representing an iReal Pro progression.
///
/// # Errors
///
/// Returns an error if the provided string contains an invalid progression.
pub fn parse_irealbook_progression(progression: &str) -> Result<Progression> {
    let chord_regex = Regex::new(
        r"(?x)
        T(?:44|34|24|54|64|74|22|32|58|68|78|98|12) | # Match optional time signature
        \| |           # single bar line
        \[ |           # opening double bar line
        \] |           # closing double bar line
        \{ |           # opening repeat bar line
        \} |           # closing repeat bar line
        Z |            # final thick double bar line
        \*[A-DVfi] |   # rehearsal marks / section
        S |            # Segno
        Q |            # Coda
        f |            # Fermata
        N[0-3] |       # Endings
        x |            # repeat one measure
        r |            # repeat two measures
        s | l |        # small / large chord
        n |            # 'No Chord' symbol
        (?:            # chord symbol
            \(?    # Match optional opening parenthesis for alternate chords
            [A-G](?:b|♭|\#|♯)? # Match root
            (?:5|2|add9|\+|o|h|sus|\^7|\-7|7sus|h7|o7|\^9|\^13|6|69|\^7\#11|\^9\#11|\^7\#5|
              \-6|\-69|\-\^7|\-\^9|-9|-11|-7b5|h9|-b6|-\#5|9|7b9|7\#9|7\#11|
              7b5|7\#5|9\#11|9b5|9\#5|7b13|7\#9\#5|7\#9b5|7\#9\#11|7b9\#11|7b9b5|7b9\#5|
              7b9\#9|7b9b13|7alt|13|13\#11|13b9|13\#9|7b9sus|7susadd3|9sus|13sus|7b13sus|11|\^|-|7)?
            (?:/[A-G](?:b|♭|\#|♯)?)? # Match optional inversion
            \)?    # Match optional closing parenthesis for alternate chords
        ) |
        <(?:\*\d{2})?[^>]*> | # staff text (including specific phrases and repeat count)
        YYY | YY | Y | # vertical space
        , |            # Divider
        p              # Slash
        ",
    )
    .unwrap();

    let mut result = Vec::new();

    for capture in chord_regex.captures_iter(progression) {
        use ProgressionElement::*;

        let symbol_str = capture.get(0).ok_or(Error::InvalidProgression)?.as_str();
        let progression_element = match symbol_str {
            _ if symbol_str.starts_with('T') => TimeSignature(symbol_str.parse()?),
            "|" => SingleBarLine,
            "[" => OpeningDoubleBarLine,
            "]" => ClosingDoubleBarLine,
            "{" => OpeningRepeatBarLine,
            "}" => ClosingRepeatBarLine,
            "Z" => FinalThickDoubleBarLine,
            "x" => RepeatOneMeasure,
            "r" => RepeatTwoMeasures,
            "s" => SmallChord,
            "l" => LargeChord,
            "n" => NoChord,
            _ if symbol_str.starts_with('*') => {
                let section = &symbol_str[1..];
                match section {
                    "V" => Verse,
                    "i" => Intro,
                    _ => Section(section.to_owned()),
                }
            }
            "S" => Segno,
            "Q" => Coda,
            "f" => Fermata,
            _ if symbol_str.starts_with('N') => {
                let n = symbol_str[1..].to_owned();
                Ending(n.parse().map_err(|_| Error::InvalidProgression)?)
            }
            _ if symbol_str.starts_with('<') => StaffText(symbol_str.parse()?),
            "YYY" => VerticalSpace(3),
            "YY" => VerticalSpace(2),
            "Y" => VerticalSpace(1),
            "," => Divider,
            "p" => Slash,
            _ if symbol_str.starts_with('(') => {
                let chord = symbol_str[1..symbol_str.len() - 1].to_owned();
                AlternateChord(chord.parse()?)
            }
            _ => Chord(symbol_str.parse()?),
        };
        result.push(progression_element);
    }

    Ok(Progression(result))
}