feat: slash chord support (G/B parses+transposes bass note)
All checks were successful
CI / Check / Test (push) Successful in 11m54s
All checks were successful
CI / Check / Test (push) Successful in 11m54s
This commit is contained in:
@@ -15,9 +15,14 @@ pub enum TransposeError {
|
||||
impl ChordTransposer {
|
||||
pub fn transpose_chord(&self, chord: &Chord, semitones: i8) -> Chord {
|
||||
let new_semitone = (chord.root.semitone() as i16 + semitones as i16).rem_euclid(12) as u8;
|
||||
let bass = chord.bass.map(|b| {
|
||||
let s = (b.semitone() as i16 + semitones as i16).rem_euclid(12) as u8;
|
||||
Note::from_semitone(s)
|
||||
});
|
||||
Chord {
|
||||
root: Note::from_semitone(new_semitone),
|
||||
descriptor: chord.descriptor.clone(),
|
||||
bass,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ fn parse_simple() {
|
||||
let c = Chord::parse("Em").unwrap();
|
||||
assert_eq!(c.root, crate::value_objects::Note::E);
|
||||
assert_eq!(c.descriptor.as_deref(), Some("m"));
|
||||
assert!(c.bass.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -12,6 +13,7 @@ fn parse_no_descriptor() {
|
||||
let c = Chord::parse("G").unwrap();
|
||||
assert_eq!(c.root, crate::value_objects::Note::G);
|
||||
assert!(c.descriptor.is_none());
|
||||
assert!(c.bass.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -26,6 +28,7 @@ fn name_sharp() {
|
||||
let c = Chord {
|
||||
root: crate::value_objects::Note::FSharpGFlat,
|
||||
descriptor: Some("m".into()),
|
||||
bass: None,
|
||||
};
|
||||
assert_eq!(c.name(true), "F#m");
|
||||
}
|
||||
@@ -35,6 +38,7 @@ fn name_flat() {
|
||||
let c = Chord {
|
||||
root: crate::value_objects::Note::ASharpBFlat,
|
||||
descriptor: None,
|
||||
bass: None,
|
||||
};
|
||||
assert_eq!(c.name(false), "Bb");
|
||||
}
|
||||
@@ -45,3 +49,39 @@ fn parse_flat_with_descriptor() {
|
||||
assert_eq!(c.root, crate::value_objects::Note::ASharpBFlat);
|
||||
assert_eq!(c.descriptor.as_deref(), Some("m"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_slash_chord() {
|
||||
let c = Chord::parse("G/B").unwrap();
|
||||
assert_eq!(c.root, crate::value_objects::Note::G);
|
||||
assert!(c.descriptor.is_none());
|
||||
assert_eq!(c.bass, Some(crate::value_objects::Note::B));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_slash_chord_with_descriptor() {
|
||||
let c = Chord::parse("Am7/G").unwrap();
|
||||
assert_eq!(c.root, crate::value_objects::Note::A);
|
||||
assert_eq!(c.descriptor.as_deref(), Some("m7"));
|
||||
assert_eq!(c.bass, Some(crate::value_objects::Note::G));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_slash_chord_with_sharp_bass() {
|
||||
let c = Chord::parse("D/F#").unwrap();
|
||||
assert_eq!(c.root, crate::value_objects::Note::D);
|
||||
assert_eq!(c.bass, Some(crate::value_objects::Note::FSharpGFlat));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_slash_chord() {
|
||||
let c = Chord::parse("G/B").unwrap();
|
||||
assert_eq!(c.name(true), "G/B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_slash_chord_transpose_display() {
|
||||
let c = Chord::parse("D/F#").unwrap();
|
||||
assert_eq!(c.name(true), "D/F#");
|
||||
assert_eq!(c.name(false), "D/Gb");
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ fn lyric_line_chord_positions() {
|
||||
chord: Chord {
|
||||
root: Note::E,
|
||||
descriptor: Some("m".into()),
|
||||
bass: None,
|
||||
},
|
||||
},
|
||||
ChordPosition {
|
||||
@@ -18,6 +19,7 @@ fn lyric_line_chord_positions() {
|
||||
chord: Chord {
|
||||
root: Note::C,
|
||||
descriptor: None,
|
||||
bass: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -66,3 +66,17 @@ fn transpose_to_key() {
|
||||
let result = t.transpose_to_key(&song, "A").unwrap();
|
||||
assert_eq!(result.sections.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_slash_chord() {
|
||||
let t = ChordTransposer;
|
||||
let result = t.transpose_chord(&chord("G/B"), 2);
|
||||
assert_eq!(result.name(true), "A/C#");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_slash_chord_down() {
|
||||
let t = ChordTransposer;
|
||||
let result = t.transpose_chord(&chord("D/F#"), -2);
|
||||
assert_eq!(result.name(true), "C/E");
|
||||
}
|
||||
|
||||
@@ -6,17 +6,33 @@ use serde::{Deserialize, Serialize};
|
||||
pub struct Chord {
|
||||
pub root: Note,
|
||||
pub descriptor: Option<String>,
|
||||
pub bass: Option<Note>,
|
||||
}
|
||||
|
||||
impl Chord {
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
let (root, consumed) = Note::parse_prefix(s)?;
|
||||
let descriptor = if consumed < s.len() {
|
||||
Some(s[consumed..].to_string())
|
||||
let (main, bass_str) = match s.rsplit_once('/') {
|
||||
Some((m, b)) if !b.is_empty() => (m, Some(b)),
|
||||
_ => (s, None),
|
||||
};
|
||||
|
||||
let (root, consumed) = Note::parse_prefix(main)?;
|
||||
let descriptor = if consumed < main.len() {
|
||||
Some(main[consumed..].to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(Chord { root, descriptor })
|
||||
|
||||
let bass = match bass_str {
|
||||
Some(b) => Some(Note::parse(b)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Some(Chord {
|
||||
root,
|
||||
descriptor,
|
||||
bass,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn name(&self, use_sharps: bool) -> String {
|
||||
@@ -25,9 +41,20 @@ impl Chord {
|
||||
} else {
|
||||
self.root.to_flat_str()
|
||||
};
|
||||
match &self.descriptor {
|
||||
let base = match &self.descriptor {
|
||||
Some(d) => format!("{}{}", root_str, d),
|
||||
None => root_str.to_string(),
|
||||
};
|
||||
match &self.bass {
|
||||
Some(b) => {
|
||||
let bass_str = if use_sharps {
|
||||
b.to_sharp_str()
|
||||
} else {
|
||||
b.to_flat_str()
|
||||
};
|
||||
format!("{}/{}", base, bass_str)
|
||||
}
|
||||
None => base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user