use regex::Regex;
use std::{fmt, str::FromStr};
use crate::{Chord, Error, Result, StaffText, TimeSignature};
#[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),
Section(String),
Verse,
Intro,
Segno,
Coda,
Fermata,
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"),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct Progression(Vec<ProgressionElement>);
impl Progression {
pub fn new(elements: Vec<ProgressionElement>) -> Self {
Progression(elements)
}
pub fn elements(&self) -> &Vec<ProgressionElement> {
&self.0
}
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)
}
}
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))
}