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 {
|
impl ChordTransposer {
|
||||||
pub fn transpose_chord(&self, chord: &Chord, semitones: i8) -> Chord {
|
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 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 {
|
Chord {
|
||||||
root: Note::from_semitone(new_semitone),
|
root: Note::from_semitone(new_semitone),
|
||||||
descriptor: chord.descriptor.clone(),
|
descriptor: chord.descriptor.clone(),
|
||||||
|
bass,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ fn parse_simple() {
|
|||||||
let c = Chord::parse("Em").unwrap();
|
let c = Chord::parse("Em").unwrap();
|
||||||
assert_eq!(c.root, crate::value_objects::Note::E);
|
assert_eq!(c.root, crate::value_objects::Note::E);
|
||||||
assert_eq!(c.descriptor.as_deref(), Some("m"));
|
assert_eq!(c.descriptor.as_deref(), Some("m"));
|
||||||
|
assert!(c.bass.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -12,6 +13,7 @@ fn parse_no_descriptor() {
|
|||||||
let c = Chord::parse("G").unwrap();
|
let c = Chord::parse("G").unwrap();
|
||||||
assert_eq!(c.root, crate::value_objects::Note::G);
|
assert_eq!(c.root, crate::value_objects::Note::G);
|
||||||
assert!(c.descriptor.is_none());
|
assert!(c.descriptor.is_none());
|
||||||
|
assert!(c.bass.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -26,6 +28,7 @@ fn name_sharp() {
|
|||||||
let c = Chord {
|
let c = Chord {
|
||||||
root: crate::value_objects::Note::FSharpGFlat,
|
root: crate::value_objects::Note::FSharpGFlat,
|
||||||
descriptor: Some("m".into()),
|
descriptor: Some("m".into()),
|
||||||
|
bass: None,
|
||||||
};
|
};
|
||||||
assert_eq!(c.name(true), "F#m");
|
assert_eq!(c.name(true), "F#m");
|
||||||
}
|
}
|
||||||
@@ -35,6 +38,7 @@ fn name_flat() {
|
|||||||
let c = Chord {
|
let c = Chord {
|
||||||
root: crate::value_objects::Note::ASharpBFlat,
|
root: crate::value_objects::Note::ASharpBFlat,
|
||||||
descriptor: None,
|
descriptor: None,
|
||||||
|
bass: None,
|
||||||
};
|
};
|
||||||
assert_eq!(c.name(false), "Bb");
|
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.root, crate::value_objects::Note::ASharpBFlat);
|
||||||
assert_eq!(c.descriptor.as_deref(), Some("m"));
|
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 {
|
chord: Chord {
|
||||||
root: Note::E,
|
root: Note::E,
|
||||||
descriptor: Some("m".into()),
|
descriptor: Some("m".into()),
|
||||||
|
bass: None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ChordPosition {
|
ChordPosition {
|
||||||
@@ -18,6 +19,7 @@ fn lyric_line_chord_positions() {
|
|||||||
chord: Chord {
|
chord: Chord {
|
||||||
root: Note::C,
|
root: Note::C,
|
||||||
descriptor: None,
|
descriptor: None,
|
||||||
|
bass: None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -66,3 +66,17 @@ fn transpose_to_key() {
|
|||||||
let result = t.transpose_to_key(&song, "A").unwrap();
|
let result = t.transpose_to_key(&song, "A").unwrap();
|
||||||
assert_eq!(result.sections.len(), 0);
|
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 struct Chord {
|
||||||
pub root: Note,
|
pub root: Note,
|
||||||
pub descriptor: Option<String>,
|
pub descriptor: Option<String>,
|
||||||
|
pub bass: Option<Note>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chord {
|
impl Chord {
|
||||||
pub fn parse(s: &str) -> Option<Self> {
|
pub fn parse(s: &str) -> Option<Self> {
|
||||||
let (root, consumed) = Note::parse_prefix(s)?;
|
let (main, bass_str) = match s.rsplit_once('/') {
|
||||||
let descriptor = if consumed < s.len() {
|
Some((m, b)) if !b.is_empty() => (m, Some(b)),
|
||||||
Some(s[consumed..].to_string())
|
_ => (s, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (root, consumed) = Note::parse_prefix(main)?;
|
||||||
|
let descriptor = if consumed < main.len() {
|
||||||
|
Some(main[consumed..].to_string())
|
||||||
} else {
|
} else {
|
||||||
None
|
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 {
|
pub fn name(&self, use_sharps: bool) -> String {
|
||||||
@@ -25,9 +41,20 @@ impl Chord {
|
|||||||
} else {
|
} else {
|
||||||
self.root.to_flat_str()
|
self.root.to_flat_str()
|
||||||
};
|
};
|
||||||
match &self.descriptor {
|
let base = match &self.descriptor {
|
||||||
Some(d) => format!("{}{}", root_str, d),
|
Some(d) => format!("{}{}", root_str, d),
|
||||||
None => root_str.to_string(),
|
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