ireal_parser/
progression.rs

1use regex::Regex;
2use std::{fmt, str::FromStr};
3
4use crate::{Chord, Error, Result, StaffText, TimeSignature};
5
6/// Represents a single element of a chord progression.
7#[derive(Debug, Eq, PartialEq, Clone)]
8pub enum ProgressionElement {
9    SingleBarLine,
10    OpeningDoubleBarLine,
11    ClosingDoubleBarLine,
12    OpeningRepeatBarLine,
13    ClosingRepeatBarLine,
14    FinalThickDoubleBarLine,
15    SmallChord,
16    LargeChord,
17    Chord(Chord),
18    AlternateChord(Chord),
19    NoChord,
20    RepeatOneMeasure,
21    RepeatTwoMeasures,
22    TimeSignature(TimeSignature),
23    /// A, B, C, D sections
24    Section(String),
25    Verse,
26    Intro,
27    Segno,
28    Coda,
29    Fermata,
30    /// N0, N1, N2.. ending
31    Ending(u8),
32    StaffText(StaffText),
33    VerticalSpace(u8),
34    Divider,
35    Slash,
36}
37
38impl fmt::Display for ProgressionElement {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        use ProgressionElement::*;
41
42        match self {
43            SingleBarLine => write!(f, "|"),
44            OpeningDoubleBarLine => write!(f, "["),
45            ClosingDoubleBarLine => write!(f, "]"),
46            OpeningRepeatBarLine => write!(f, "{{"),
47            ClosingRepeatBarLine => write!(f, "}}"),
48            FinalThickDoubleBarLine => write!(f, "Z"),
49            Chord(chord) => write!(f, "{} ", chord),
50            AlternateChord(chord) => write!(f, "({}) ", chord),
51            NoChord => write!(f, "n"),
52            TimeSignature(ts) => write!(f, "{ts}"),
53            Section(rm) => write!(f, "*{}", rm),
54            Verse => write!(f, "*V"),
55            Intro => write!(f, "*i"),
56            Segno => write!(f, "S"),
57            Coda => write!(f, "Q"),
58            Fermata => write!(f, "f"),
59            Ending(ending) => write!(f, "N{ending}"),
60            StaffText(text) => write!(f, "{text}"),
61            VerticalSpace(n) => write!(f, "{}", "Y".repeat((*n).into())),
62            Divider => write!(f, ","),
63            Slash => write!(f, "p"),
64            SmallChord => write!(f, "s"),
65            LargeChord => write!(f, "l"),
66            RepeatOneMeasure => write!(f, " x "),
67            RepeatTwoMeasures => write!(f, " r"),
68        }
69    }
70}
71
72/// Represents a chord progression as a sequence of [`ProgressionElement`]s.
73#[derive(Debug, PartialEq, Clone)]
74pub struct Progression(Vec<ProgressionElement>);
75
76impl Progression {
77    /// Create a new `Progression` from a `Vec<ProgressionElement>`.
78    pub fn new(elements: Vec<ProgressionElement>) -> Self {
79        Progression(elements)
80    }
81
82    /// Get a reference to the internal `Vec<ProgressionElement>`.
83    pub fn elements(&self) -> &Vec<ProgressionElement> {
84        &self.0
85    }
86
87    /// Get a mutable reference to the internal `Vec<ProgressionElement>`.
88    pub fn elements_mut(&mut self) -> &mut Vec<ProgressionElement> {
89        &mut self.0
90    }
91}
92
93impl fmt::Display for Progression {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        let s: String = self.0.iter().map(|e| e.to_string()).collect();
96        write!(f, "{s}")
97    }
98}
99
100impl FromStr for Progression {
101    type Err = Error;
102
103    fn from_str(s: &str) -> Result<Self> {
104        parse_irealbook_progression(s)
105    }
106}
107
108/// Parses an iReal Pro progression string and returns a [`Progression`].
109///
110/// # Arguments
111///
112/// * `progression` - A string representing an iReal Pro progression.
113///
114/// # Errors
115///
116/// Returns an error if the provided string contains an invalid progression.
117pub fn parse_irealbook_progression(progression: &str) -> Result<Progression> {
118    let chord_regex = Regex::new(
119        r"(?x)
120        T(?:44|34|24|54|64|74|22|32|58|68|78|98|12) | # Match optional time signature
121        \| |           # single bar line
122        \[ |           # opening double bar line
123        \] |           # closing double bar line
124        \{ |           # opening repeat bar line
125        \} |           # closing repeat bar line
126        Z |            # final thick double bar line
127        \*[A-DVfi] |   # rehearsal marks / section
128        S |            # Segno
129        Q |            # Coda
130        f |            # Fermata
131        N[0-3] |       # Endings
132        x |            # repeat one measure
133        r |            # repeat two measures
134        s | l |        # small / large chord
135        n |            # 'No Chord' symbol
136        (?:            # chord symbol
137            \(?    # Match optional opening parenthesis for alternate chords
138            [A-G](?:b|♭|\#|♯)? # Match root
139            (?:5|2|add9|\+|o|h|sus|\^7|\-7|7sus|h7|o7|\^9|\^13|6|69|\^7\#11|\^9\#11|\^7\#5|
140              \-6|\-69|\-\^7|\-\^9|-9|-11|-7b5|h9|-b6|-\#5|9|7b9|7\#9|7\#11|
141              7b5|7\#5|9\#11|9b5|9\#5|7b13|7\#9\#5|7\#9b5|7\#9\#11|7b9\#11|7b9b5|7b9\#5|
142              7b9\#9|7b9b13|7alt|13|13\#11|13b9|13\#9|7b9sus|7susadd3|9sus|13sus|7b13sus|11|\^|-|7)?
143            (?:/[A-G](?:b|♭|\#|♯)?)? # Match optional inversion
144            \)?    # Match optional closing parenthesis for alternate chords
145        ) |
146        <(?:\*\d{2})?[^>]*> | # staff text (including specific phrases and repeat count)
147        YYY | YY | Y | # vertical space
148        , |            # Divider
149        p              # Slash
150        ",
151    )
152    .unwrap();
153
154    let mut result = Vec::new();
155
156    for capture in chord_regex.captures_iter(progression) {
157        use ProgressionElement::*;
158
159        let symbol_str = capture.get(0).ok_or(Error::InvalidProgression)?.as_str();
160        let progression_element = match symbol_str {
161            _ if symbol_str.starts_with('T') => TimeSignature(symbol_str.parse()?),
162            "|" => SingleBarLine,
163            "[" => OpeningDoubleBarLine,
164            "]" => ClosingDoubleBarLine,
165            "{" => OpeningRepeatBarLine,
166            "}" => ClosingRepeatBarLine,
167            "Z" => FinalThickDoubleBarLine,
168            "x" => RepeatOneMeasure,
169            "r" => RepeatTwoMeasures,
170            "s" => SmallChord,
171            "l" => LargeChord,
172            "n" => NoChord,
173            _ if symbol_str.starts_with('*') => {
174                let section = &symbol_str[1..];
175                match section {
176                    "V" => Verse,
177                    "i" => Intro,
178                    _ => Section(section.to_owned()),
179                }
180            }
181            "S" => Segno,
182            "Q" => Coda,
183            "f" => Fermata,
184            _ if symbol_str.starts_with('N') => {
185                let n = symbol_str[1..].to_owned();
186                Ending(n.parse().map_err(|_| Error::InvalidProgression)?)
187            }
188            _ if symbol_str.starts_with('<') => StaffText(symbol_str.parse()?),
189            "YYY" => VerticalSpace(3),
190            "YY" => VerticalSpace(2),
191            "Y" => VerticalSpace(1),
192            "," => Divider,
193            "p" => Slash,
194            _ if symbol_str.starts_with('(') => {
195                let chord = symbol_str[1..symbol_str.len() - 1].to_owned();
196                AlternateChord(chord.parse()?)
197            }
198            _ => Chord(symbol_str.parse()?),
199        };
200        result.push(progression_element);
201    }
202
203    Ok(Progression(result))
204}