klavier_core/
solfa.rs

1use std::{fmt, ops::{AddAssign, SubAssign}};
2
3/// Musical note names (solfège syllables).
4///
5/// Represents the seven natural notes in Western music: C, D, E, F, G, A, B.
6/// These correspond to the white keys on a piano.
7///
8/// # Examples
9///
10/// ```
11/// # use klavier_core::solfa::Solfa;
12/// let middle_c = Solfa::C;
13/// let a_note = Solfa::A;
14/// ```
15#[derive(serde::Deserialize, serde::Serialize)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum Solfa {
18    /// C note
19    C,
20    /// D note
21    D,
22    /// E note
23    E,
24    /// F note
25    F,
26    /// G note
27    G,
28    /// A note
29    A,
30    /// B note
31    B,
32}
33
34impl AddAssign<i32> for Solfa {
35    fn add_assign(&mut self, rhs: i32) {
36        let so = self.score_offset() + rhs;
37        if Solfa::B.score_offset() < so {
38            panic!("Solfa overflow");
39        }
40        *self = Solfa::from_score_offset(so);
41    }
42}
43
44impl SubAssign<i32> for Solfa {
45    fn sub_assign(&mut self, rhs: i32) {
46        let so = self.score_offset() - rhs;
47        if so < Solfa::C.score_offset() {
48            panic!("Solfa overflow");
49        }
50        *self = Solfa::from_score_offset(so);
51    }
52}
53
54impl Solfa {
55    /// Array of all seven natural notes.
56    pub const ALL: &'static [Solfa] = &[Self::C, Self::D, Self::E, Self::F, Self::G, Self::A, Self::B];
57
58    /// Returns the position on the musical staff (0-6).
59    ///
60    /// C=0, D=1, E=2, F=3, G=4, A=5, B=6
61    pub const fn score_offset(self) -> i32 {
62        match self {
63            Self::C => 0,
64            Self::D => 1,
65            Self::E => 2,
66            Self::F => 3,
67            Self::G => 4,
68            Self::A => 5,
69            Self::B => 6,
70        }
71    }
72
73    /// Returns the pitch offset in semitones from C (0-11).
74    ///
75    /// This represents the number of semitones above C within an octave.
76    /// C=0, D=2, E=4, F=5, G=7, A=9, B=11
77    pub const fn pitch_offset(self) -> i32 {
78        match self {
79            Self::C => 0,
80            Self::D => 2,
81            Self::E => 4,
82            Self::F => 5,
83            Self::G => 7,
84            Self::A => 9,
85            Self::B => 11,
86        }
87    }
88
89    /// Creates a solfa from a staff position offset.
90    ///
91    /// Values outside the valid range (0-6) are clamped to C or B.
92    ///
93    /// # Arguments
94    ///
95    /// * `offset` - The staff position (0-6).
96    pub fn from_score_offset(offset: i32) -> Solfa {
97        if offset < Self::C.score_offset() {
98            Self::C
99        } else if offset > Self::B.score_offset() {
100            Self::B
101        } else {
102            Self::ALL[offset as usize]
103        }
104    }
105}
106
107impl fmt::Display for Solfa {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        match *self {
110            Solfa::C => write!(f, "C"),
111            Solfa::D => write!(f, "D"),
112            Solfa::E => write!(f, "E"),
113            Solfa::F => write!(f, "F"),
114            Solfa::G => write!(f, "G"),
115            Solfa::A => write!(f, "A"),
116            Solfa::B => write!(f, "B"),
117        }
118    }
119}
120#[cfg(test)]
121mod tests {
122    use crate::solfa::Solfa;
123
124    #[test]
125    fn score_offset_is_valid() {
126        assert_eq!(Solfa::C.score_offset(), 0);
127        assert_eq!(Solfa::D.score_offset(), 1);
128        assert_eq!(Solfa::E.score_offset(), 2);
129        assert_eq!(Solfa::F.score_offset(), 3);
130        assert_eq!(Solfa::G.score_offset(), 4);
131        assert_eq!(Solfa::A.score_offset(), 5);
132        assert_eq!(Solfa::B.score_offset(), 6);
133    }
134
135    #[test]
136    fn pitch_offset_is_valid() {
137        assert_eq!(Solfa::C.pitch_offset(), 0);
138        assert_eq!(Solfa::D.pitch_offset(), 2);
139        assert_eq!(Solfa::E.pitch_offset(), 4);
140        assert_eq!(Solfa::F.pitch_offset(), 5);
141        assert_eq!(Solfa::G.pitch_offset(), 7);
142        assert_eq!(Solfa::A.pitch_offset(), 9);
143        assert_eq!(Solfa::B.pitch_offset(), 11);
144    }
145
146    #[test]
147    fn from_score_offset() {
148        assert_eq!(Solfa::from_score_offset(-1), Solfa::C);
149        assert_eq!(Solfa::from_score_offset(0), Solfa::C);
150        assert_eq!(Solfa::from_score_offset(1), Solfa::D);
151        assert_eq!(Solfa::from_score_offset(2), Solfa::E);
152        assert_eq!(Solfa::from_score_offset(3), Solfa::F);
153        assert_eq!(Solfa::from_score_offset(4), Solfa::G);
154        assert_eq!(Solfa::from_score_offset(5), Solfa::A);
155        assert_eq!(Solfa::from_score_offset(6), Solfa::B);
156        assert_eq!(Solfa::from_score_offset(7), Solfa::B);
157    }
158
159    #[test]
160    fn all() {
161        assert_eq!(Solfa::ALL[0], Solfa::C);
162        assert_eq!(Solfa::ALL[1], Solfa::D);
163        assert_eq!(Solfa::ALL[2], Solfa::E);
164        assert_eq!(Solfa::ALL[3], Solfa::F);
165        assert_eq!(Solfa::ALL[4], Solfa::G);
166        assert_eq!(Solfa::ALL[5], Solfa::A);
167        assert_eq!(Solfa::ALL[6], Solfa::B);
168        assert_eq!(Solfa::ALL.len(), 7);
169    }
170
171    #[test]
172    fn add_assign() {
173        let mut solfa = Solfa::C;
174        solfa += 1;
175        assert_eq!(solfa, Solfa::D);
176        solfa += 2;
177        assert_eq!(solfa, Solfa::F);
178    }
179
180    #[test]
181    #[should_panic]
182    fn add_assign_error() {
183        let mut solfa = Solfa::B;
184        solfa += 1;
185    }
186
187    #[test]
188    fn sub_assign() {
189        let mut solfa = Solfa::B;
190        solfa -= 1;
191        assert_eq!(solfa, Solfa::A);
192        solfa -= 2;
193        assert_eq!(solfa, Solfa::F);
194    }
195
196    #[test]
197    #[should_panic]
198    fn sub_assign_error() {
199        let mut solfa = Solfa::C;
200        solfa -= 1;
201    }
202}