1use std::{slice::Iter, str::FromStr};
2
3use crate::Chord;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct ChordSequence {
7 chords: Vec<Chord>,
8}
9
10impl ChordSequence {
11 pub fn chords(&self) -> Iter<'_, Chord> {
12 self.chords.iter()
13 }
14
15 pub fn transpose(&self, semitones: i8) -> Self {
16 let chords = self.chords().map(|c| c.transpose(semitones)).collect();
17 Self { chords }
18 }
19}
20
21#[derive(Debug, thiserror::Error)]
22#[error("could not parse chord sequence")]
23pub struct ParseChordSequenceError;
24
25impl FromStr for ChordSequence {
26 type Err = ParseChordSequenceError;
27
28 fn from_str(s: &str) -> Result<Self, Self::Err> {
29 let res: Result<Vec<_>, _> = s.split_whitespace().map(Chord::from_str).collect();
30
31 if let Ok(chords) = res {
32 return Ok(Self { chords });
33 }
34
35 Err(ParseChordSequenceError)
36 }
37}
38
39#[cfg(test)]
40mod tests {
41 use rstest::rstest;
42
43 use super::*;
44
45 #[rstest(
46 chord_seq,
47 chords,
48 case("", &[]),
49 case("C", &["C"]),
50 case("C F G", &["C", "F", "G"]),
51 case("Dsus2 Am7 C#", &["Dsus2", "Am7", "C#"]),
52 )]
53 fn test_from_str(chord_seq: ChordSequence, chords: &[&str]) {
54 let chords1: Vec<Chord> = chord_seq.chords().cloned().collect();
55 let chords2: Vec<Chord> = chords.iter().map(|c| Chord::from_str(c).unwrap()).collect();
56 assert_eq!(chords1, chords2);
57 }
58
59 #[rstest(chord_seq, case("Z"), case("A Z"))]
60 fn test_from_str_fail(chord_seq: &str) {
61 assert!(ChordSequence::from_str(chord_seq).is_err());
62 }
63
64 #[rstest(
65 chord_seq1,
66 semitones,
67 chord_seq2,
68 case("", 0, ""),
69 case("C F G", 0, "C F G"),
70 case("C F G", 1, "C# F# G#"),
71 case("C F G", -1, "B E Gb"),
72 case("C F G", 12, "C F G"),
73 )]
74 fn test_transpose(chord_seq1: ChordSequence, semitones: i8, chord_seq2: ChordSequence) {
75 assert_eq!(chord_seq1.transpose(semitones), chord_seq2);
76 }
77}