use crate::Key;
#[cfg(feature = "logging")]
use tracing::debug;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Alphabetic {
A,
B,
C,
D,
E,
F,
G,
M,
}
impl TryFrom<char> for Alphabetic {
type Error = ();
fn try_from(value: char) -> Result<Self, Self::Error> {
Ok(match value {
'A' | 'a' => Self::A,
'B' | 'b' => Self::B,
'C' | 'c' => Self::C,
'D' | 'd' => Self::D,
'E' | 'e' => Self::E,
'F' | 'f' => Self::F,
'G' | 'g' => Self::G,
'M' | 'm' => Self::M,
_ => return Err(()),
})
}
}
#[derive(Debug, PartialEq)]
pub enum Token {
Number(i32), Alphabetic(Alphabetic), Flat, Sharp, }
impl Token {
fn try_from_char(c: char) -> Option<Self> {
if c.is_ascii_digit() {
Some(Self::Number(
c.to_digit(10).expect("Char::to_digit() failed?") as i32,
))
} else if c == '#' {
Some(Self::Sharp)
} else if c == '♭' {
Some(Self::Flat)
} else if let Ok(alphabetic) = Alphabetic::try_from(c) {
Some(Self::Alphabetic(alphabetic))
} else {
None
}
}
fn from_string(s: &str) -> Vec<Token> {
let mut tokens = vec![];
for c in s.chars() {
match Token::try_from_char(c) {
Some(token) => tokens.push(token),
None => break,
}
}
tokens
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum NotationType {
Traditional,
OpenKey,
Lancelot,
}
#[derive(Debug, Clone, Copy)]
enum ParserState {
Init,
Number(i32), Alphabetic(Alphabetic),
AlphabeticFlat(Alphabetic),
AlphabeticSharp(Alphabetic),
}
pub struct KeyParser {
notation_type: NotationType,
key: Key,
}
impl KeyParser {
pub fn try_from_string<'a>(s: &str) -> Result<Self, &'a str> {
Self::try_from_tokens(Token::from_string(s))
}
fn try_from_tokens<'a>(tokens: Vec<Token>) -> Result<Self, &'a str> {
#[cfg(feature = "logging")]
debug!("from_tokens tokens={tokens:?}");
let mut state = ParserState::Init;
macro_rules! switch_state {
($new_state:expr) => {{
let new_state = $new_state;
#[cfg(feature = "logging")]
debug!("Switch from old_state={state:?} to new_state={new_state:?}");
state = new_state;
}};
}
macro_rules! not_implemented {
() => {
return Err("Not implemented yet :/")
};
}
macro_rules! ok_chromatic {
($chromatic:ident) => {
return Ok(Self {
notation_type: NotationType::Traditional,
key: Key::$chromatic,
})
};
}
for t in tokens {
match state {
ParserState::Init => match t {
Token::Number(number) => switch_state!(ParserState::Number(number)),
Token::Alphabetic(alphabetic) => {
switch_state!(ParserState::Alphabetic(alphabetic))
}
Token::Flat => return Err("Should not start with flat (♭)"),
Token::Sharp => return Err("Should not start with sharp (#)"),
},
ParserState::Number(number) => match t {
Token::Number(another_number) => {
let combined_number = number * 10 + another_number;
if combined_number > 12 {
return Err("Number > 12");
}
switch_state!(ParserState::Number(combined_number))
}
Token::Alphabetic(alphabetic) => {
match alphabetic {
Alphabetic::A => {
return Ok(Self {
notation_type: NotationType::Lancelot,
key: Key::from_lancelot_numeric(number, false)
.map_err(|_| "Not a key")?,
});
}
Alphabetic::B => {
return Ok(Self {
notation_type: NotationType::Lancelot,
key: Key::from_lancelot_numeric(number, true)
.map_err(|_| "Not a key")?,
});
}
Alphabetic::M => {
return Ok(Self {
notation_type: NotationType::OpenKey,
key: Key::from_open_key_numeric(number, false)
.map_err(|_| "Not a key")?,
});
}
Alphabetic::D => {
return Ok(Self {
notation_type: NotationType::OpenKey,
key: Key::from_open_key_numeric(number, true)
.map_err(|_| "Not a key")?,
});
}
_ => return Err("Illegal alphabetic character following number"),
}
}
Token::Flat => return Err("Illegal flat character following number"),
Token::Sharp => return Err("Illegal sharp character following number"),
},
ParserState::Alphabetic(alphabetic) => match t {
Token::Number(_) => return Err("Illegal number following alphabetic"),
Token::Alphabetic(Alphabetic::M) => match alphabetic {
Alphabetic::A => ok_chromatic!(AMinor),
Alphabetic::B => ok_chromatic!(BMinor),
Alphabetic::C => ok_chromatic!(CMinor),
Alphabetic::D => ok_chromatic!(DMinor),
Alphabetic::E => ok_chromatic!(EMinor),
Alphabetic::F => ok_chromatic!(FMinor),
Alphabetic::G => ok_chromatic!(GMinor),
Alphabetic::M => not_implemented!(),
},
Token::Alphabetic(Alphabetic::B) => {
switch_state!(ParserState::AlphabeticFlat(alphabetic))
}
Token::Alphabetic(_) => return Err("Illegal alphabetic following alphabetic"),
Token::Flat => {
switch_state!(ParserState::AlphabeticFlat(alphabetic))
}
Token::Sharp => {
switch_state!(ParserState::AlphabeticSharp(alphabetic))
}
},
ParserState::AlphabeticFlat(key) => match t {
Token::Number(_) => return Err("Illegal Number following alphabetic flat"),
Token::Alphabetic(Alphabetic::M) => match key {
Alphabetic::A => ok_chromatic!(GSharpMinor), Alphabetic::B => ok_chromatic!(BFlatMinor), Alphabetic::C => not_implemented!(), Alphabetic::D => ok_chromatic!(CSharpMinor), Alphabetic::E => ok_chromatic!(EFlatMinor), Alphabetic::F => not_implemented!(), Alphabetic::G => ok_chromatic!(FSharpMinor), Alphabetic::M => unreachable!(), },
Token::Alphabetic(_) => {
return Err("Illegal alphabetic following Alphabetic Flat")
}
Token::Flat => return Err("Illegal Flat following Alphabetic Flat"),
Token::Sharp => return Err("Illegal Sharp following Alphabetic Flat"),
},
ParserState::AlphabeticSharp(key) => match t {
Token::Number(_) => return Err("Illegal Number following Alphabetic Sharp"),
Token::Alphabetic(Alphabetic::M) => match key {
Alphabetic::A => not_implemented!(),
Alphabetic::B => not_implemented!(),
Alphabetic::C => ok_chromatic!(CSharpMinor), Alphabetic::D => ok_chromatic!(FSharpMajor), Alphabetic::E => not_implemented!(),
Alphabetic::F => ok_chromatic!(FSharpMinor),
Alphabetic::G => ok_chromatic!(GSharpMinor), Alphabetic::M => not_implemented!(),
},
Token::Alphabetic(_) => {
return Err("Illegal Alphabetic following Alphabetic Sharp")
}
Token::Flat => return Err("Illegal Flat following Alphabetic sharp"),
Token::Sharp => return Err("Illegal sharp following Alphabetic sharp"),
},
}
}
match state {
ParserState::Init => Err("No tokens"),
ParserState::Alphabetic(alphabetic) => match alphabetic {
Alphabetic::A => ok_chromatic!(AMajor),
Alphabetic::B => ok_chromatic!(BMajor),
Alphabetic::C => ok_chromatic!(CMajor),
Alphabetic::D => ok_chromatic!(DMajor),
Alphabetic::E => ok_chromatic!(EMajor),
Alphabetic::F => ok_chromatic!(FMajor),
Alphabetic::G => ok_chromatic!(GMajor),
Alphabetic::M => Err("M is not a valid key notation"),
},
ParserState::Number(_) => Err("Missing major or minor indicator (A,B,D,M)"),
ParserState::AlphabeticFlat(alphabetic) => match alphabetic {
Alphabetic::A => ok_chromatic!(AFlatMajor), Alphabetic::B => ok_chromatic!(BFlatMajor), Alphabetic::C => ok_chromatic!(BMinor),
Alphabetic::D => ok_chromatic!(DFlatMajor),
Alphabetic::E => ok_chromatic!(EFlatMajor),
Alphabetic::F => not_implemented!(),
Alphabetic::G => ok_chromatic!(FSharpMajor),
Alphabetic::M => Err("M is not a valid key notation"),
},
ParserState::AlphabeticSharp(alphabetic) => match alphabetic {
Alphabetic::A => not_implemented!(),
Alphabetic::B => not_implemented!(),
Alphabetic::C => not_implemented!(),
Alphabetic::D => not_implemented!(),
Alphabetic::E => not_implemented!(),
Alphabetic::F => ok_chromatic!(FSharpMajor),
Alphabetic::G => not_implemented!(),
Alphabetic::M => not_implemented!(),
},
}
}
pub fn notation_type(&self) -> NotationType {
self.notation_type
}
pub fn key(&self) -> Key {
self.key
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Key;
#[cfg(logging)]
use test_log::test;
#[test]
fn test_token_parsing() {
assert_eq!(
Token::try_from_char('a'),
Some(Token::Alphabetic(Alphabetic::A))
);
assert_eq!(Token::try_from_char('x'), None);
assert_eq!(
Token::try_from_char('m'),
Some(Token::Alphabetic(Alphabetic::M))
);
assert_eq!(Token::try_from_char('♭'), Some(Token::Flat));
assert_eq!(
Token::from_string("10m"),
vec![
Token::Number(1),
Token::Number(0),
Token::Alphabetic(Alphabetic::M)
]
);
assert_eq!(
Token::from_string("F#m"),
vec![
Token::Alphabetic(Alphabetic::F),
Token::Sharp,
Token::Alphabetic(Alphabetic::M)
]
);
assert_eq!(
Token::from_string("F#/G♭"),
vec![Token::Alphabetic(Alphabetic::F), Token::Sharp,]
)
}
#[test]
fn test_parse_from_string() {
macro_rules! test_case {
($str:expr, NotationType::$t:ident, Key::$l:ident) => {{
let notation = KeyParser::try_from_string($str).expect("Failed parsing");
assert_eq!(notation.notation_type(), NotationType::$t);
assert_eq!(notation.key(), Key::$l);
}};
}
test_case!("1A", NotationType::Lancelot, Key::GSharpMinor);
test_case!("1A", NotationType::Lancelot, Key::GSharpMinor);
test_case!("1B", NotationType::Lancelot, Key::BMajor);
test_case!("F#m", NotationType::Traditional, Key::FSharpMinor);
test_case!("1D", NotationType::OpenKey, Key::CMajor);
test_case!("Abm", NotationType::Traditional, Key::GSharpMinor);
test_case!("A♭m", NotationType::Traditional, Key::GSharpMinor);
test_case!("G#m", NotationType::Traditional, Key::GSharpMinor);
test_case!("B", NotationType::Traditional, Key::BMajor);
test_case!("Ebm", NotationType::Traditional, Key::EFlatMinor);
test_case!("E♭m", NotationType::Traditional, Key::EFlatMinor);
test_case!("F#", NotationType::Traditional, Key::FSharpMajor);
test_case!("Gb", NotationType::Traditional, Key::FSharpMajor);
test_case!("G♭", NotationType::Traditional, Key::FSharpMajor);
test_case!("Bbm", NotationType::Traditional, Key::BFlatMinor);
test_case!("Db", NotationType::Traditional, Key::DFlatMajor);
test_case!("D♭", NotationType::Traditional, Key::DFlatMajor);
test_case!("Fm", NotationType::Traditional, Key::FMinor);
test_case!("Ab", NotationType::Traditional, Key::AFlatMajor);
test_case!("A♭", NotationType::Traditional, Key::AFlatMajor);
test_case!("Cm", NotationType::Traditional, Key::CMinor);
test_case!("Eb", NotationType::Traditional, Key::EFlatMajor);
test_case!("E♭", NotationType::Traditional, Key::EFlatMajor);
test_case!("Gm", NotationType::Traditional, Key::GMinor);
test_case!("Bb", NotationType::Traditional, Key::BFlatMajor);
test_case!("B♭", NotationType::Traditional, Key::BFlatMajor);
test_case!("Dm", NotationType::Traditional, Key::DMinor);
test_case!("F", NotationType::Traditional, Key::FMajor);
test_case!("Am", NotationType::Traditional, Key::AMinor);
test_case!("C", NotationType::Traditional, Key::CMajor);
test_case!("Em", NotationType::Traditional, Key::EMinor);
test_case!("G", NotationType::Traditional, Key::GMajor);
test_case!("Bm", NotationType::Traditional, Key::BMinor);
test_case!("Cb", NotationType::Traditional, Key::BMinor);
test_case!("C♭", NotationType::Traditional, Key::BMinor);
test_case!("D", NotationType::Traditional, Key::DMajor);
test_case!("F#m", NotationType::Traditional, Key::FSharpMinor);
test_case!("A", NotationType::Traditional, Key::AMajor);
test_case!("Dbm", NotationType::Traditional, Key::CSharpMinor);
test_case!("D♭m", NotationType::Traditional, Key::CSharpMinor);
test_case!("C#m", NotationType::Traditional, Key::CSharpMinor);
test_case!("E", NotationType::Traditional, Key::EMajor);
test_case!("1A (Abm)", NotationType::Lancelot, Key::GSharpMinor);
test_case!("D#m", NotationType::Traditional, Key::FSharpMajor);
test_case!("D#m/E♭m", NotationType::Traditional, Key::FSharpMajor);
test_case!("Gbm", NotationType::Traditional, Key::FSharpMinor);
test_case!("F#", NotationType::Traditional, Key::FSharpMajor);
test_case!("F#/G♭", NotationType::Traditional, Key::FSharpMajor);
}
#[test]
fn test_parse_lancelot_string() {
let test_cases: Vec<String> = (1..13)
.flat_map(|i| [format!("{}A", i), format!("{}B", i)])
.collect();
for t in &test_cases {
let parsed = KeyParser::try_from_string(t).expect("Failed parsing");
assert_eq!(parsed.notation_type(), NotationType::Lancelot);
assert_eq!(parsed.key().lancelot().to_string(), t.to_string());
}
}
#[test]
fn test_parse_open_key_string() {
assert_eq!(
KeyParser::try_from_string("1m").unwrap().key().open_key(),
"1m"
);
let test_cases: Vec<String> = (1..13)
.flat_map(|i| [format!("{}m", i), format!("{}d", i)])
.collect();
for t in &test_cases {
println!("testing {}", t);
let parsed = KeyParser::try_from_string(t).expect("Failed parsing key");
assert_eq!(parsed.notation_type(), NotationType::OpenKey);
assert_eq!(parsed.key().open_key(), t.to_string());
}
}
}