ukebox/
chord_sequence.rs

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}