1use crate::{
18 error::{Error, Result},
19 key::Key,
20 roman::RomanNumeral,
21};
22
23const MINOR: &str = "minor";
25
26#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum HarmonicFunction {
30 TonicMajor,
32 TonicMajorParallelklangMinor,
34 TonicMajorGegenklangMinor,
36 TonicMinor,
38 TonicMinorParallelklangMajor,
40 TonicMinorGegenklangMajor,
42 SubdominantMajor,
44 SubdominantMajorParallelklangMinor,
46 SubdominantMajorGegenklangMinor,
48 SubdominantMinor,
50 SubdominantMinorParallelklangMajor,
52 SubdominantMinorGegenklangMajor,
54 DominantMajor,
56 DominantMajorParallelklangMinor,
58 DominantMajorGegenklangMinor,
60 DominantMinor,
62 DominantMinorParallelklangMajor,
64 DominantMinorGegenklangMajor,
66}
67
68use HarmonicFunction::*;
69
70const KEY_NEUTRAL: [(HarmonicFunction, &str); 11] = [
72 (TonicMajor, "I"),
73 (TonicMinor, "i"),
74 (SubdominantMinorGegenklangMajor, "bII"),
75 (SubdominantMajorParallelklangMinor, "ii"),
76 (SubdominantMajor, "IV"),
77 (SubdominantMinor, "iv"),
78 (DominantMajor, "V"),
79 (DominantMinor, "v"),
80 (TonicMajorParallelklangMinor, "vi"),
81 (SubdominantMajorGegenklangMinor, "vi"),
82 (DominantMajorGegenklangMinor, "bvii"),
83];
84
85const MAJOR_ONLY: [(HarmonicFunction, &str); 7] = [
89 (TonicMinorParallelklangMajor, "bIII"),
90 (DominantMinorGegenklangMajor, "bIII"),
91 (DominantMajorParallelklangMinor, "iii"),
92 (TonicMajorGegenklangMinor, "iii"),
93 (SubdominantMinorParallelklangMajor, "bVI"),
94 (TonicMinorGegenklangMajor, "bVI"),
95 (DominantMinorParallelklangMajor, "bVII"),
96];
97
98const MINOR_ONLY: [(HarmonicFunction, &str); 7] = [
100 (TonicMinorParallelklangMajor, "III"),
101 (DominantMinorGegenklangMajor, "III"),
102 (DominantMajorParallelklangMinor, "#iii"),
103 (TonicMajorGegenklangMinor, "#iii"),
104 (SubdominantMinorParallelklangMajor, "VI"),
105 (TonicMinorGegenklangMajor, "VI"),
106 (DominantMinorParallelklangMajor, "VII"),
107];
108
109#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub enum FunctionDetail {
113 #[default]
115 Full,
116 MainOnly,
119}
120
121impl HarmonicFunction {
122 pub const ALL: [Self; 18] = [
124 TonicMajor,
125 TonicMajorParallelklangMinor,
126 TonicMajorGegenklangMinor,
127 TonicMinor,
128 TonicMinorParallelklangMajor,
129 TonicMinorGegenklangMajor,
130 SubdominantMajor,
131 SubdominantMajorParallelklangMinor,
132 SubdominantMajorGegenklangMinor,
133 SubdominantMinor,
134 SubdominantMinorParallelklangMajor,
135 SubdominantMinorGegenklangMajor,
136 DominantMajor,
137 DominantMajorParallelklangMinor,
138 DominantMajorGegenklangMinor,
139 DominantMinor,
140 DominantMinorParallelklangMajor,
141 DominantMinorGegenklangMajor,
142 ];
143
144 pub fn symbol(self) -> &'static str {
146 match self {
147 TonicMajor => "T",
148 TonicMajorParallelklangMinor => "Tp",
149 TonicMajorGegenklangMinor => "Tg",
150 TonicMinor => "t",
151 TonicMinorParallelklangMajor => "tP",
152 TonicMinorGegenklangMajor => "tG",
153 SubdominantMajor => "S",
154 SubdominantMajorParallelklangMinor => "Sp",
155 SubdominantMajorGegenklangMinor => "Sg",
156 SubdominantMinor => "s",
157 SubdominantMinorParallelklangMajor => "sP",
158 SubdominantMinorGegenklangMajor => "sG",
159 DominantMajor => "D",
160 DominantMajorParallelklangMinor => "Dp",
161 DominantMajorGegenklangMinor => "Dg",
162 DominantMinor => "d",
163 DominantMinorParallelklangMajor => "dP",
164 DominantMinorGegenklangMajor => "dG",
165 }
166 }
167
168 pub fn music21_name(self) -> &'static str {
170 match self {
171 TonicMajor => "TONIC_MAJOR",
172 TonicMajorParallelklangMinor => "TONIC_MAJOR_PARALLELKLANG_MINOR",
173 TonicMajorGegenklangMinor => "TONIC_MAJOR_GEGENKLANG_MINOR",
174 TonicMinor => "TONIC_MINOR",
175 TonicMinorParallelklangMajor => "TONIC_MINOR_PARALLELKLANG_MAJOR",
176 TonicMinorGegenklangMajor => "TONIC_MINOR_GEGENKLANG_MAJOR",
177 SubdominantMajor => "SUBDOMINANT_MAJOR",
178 SubdominantMajorParallelklangMinor => "SUBDOMINANT_MAJOR_PARALLELKLANG_MINOR",
179 SubdominantMajorGegenklangMinor => "SUBDOMINANT_MAJOR_GEGENKLANG_MINOR",
180 SubdominantMinor => "SUBDOMINANT_MINOR",
181 SubdominantMinorParallelklangMajor => "SUBDOMINANT_MINOR_PARALLELKLANG_MAJOR",
182 SubdominantMinorGegenklangMajor => "SUBDOMINANT_MINOR_GEGENKLANG_MAJOR",
183 DominantMajor => "DOMINANT_MAJOR",
184 DominantMajorParallelklangMinor => "DOMINANT_MAJOR_PARALLELKLANG_MINOR",
185 DominantMajorGegenklangMinor => "DOMINANT_MAJOR_GEGENKLANG_MINOR",
186 DominantMinor => "DOMINANT_MINOR",
187 DominantMinorParallelklangMajor => "DOMINANT_MINOR_PARALLELKLANG_MAJOR",
188 DominantMinorGegenklangMajor => "DOMINANT_MINOR_GEGENKLANG_MAJOR",
189 }
190 }
191
192 pub fn from_symbol(symbol: &str) -> Result<Self> {
198 Self::ALL
199 .into_iter()
200 .find(|function| function.symbol() == symbol)
201 .ok_or_else(|| Error::Value(format!("'{symbol}' is not a valid HarmonicFunction")))
202 }
203
204 pub fn main_function(self) -> Self {
206 match self.symbol().chars().next() {
207 Some('T') => TonicMajor,
208 Some('t') => TonicMinor,
209 Some('S') => SubdominantMajor,
210 Some('s') => SubdominantMinor,
211 Some('D') => DominantMajor,
212 _ => DominantMinor,
213 }
214 }
215
216 pub fn figure(self, mode: &str) -> &'static str {
219 table(mode)
220 .find(|(function, _)| *function == self)
221 .map(|(_, figure)| figure)
222 .unwrap_or_default()
223 }
224
225 pub fn to_roman(self, key: &Key) -> Result<RomanNumeral> {
242 RomanNumeral::new(self.figure(key.mode()), key.clone())
243 }
244
245 pub fn from_roman(numeral: &RomanNumeral, detail: FunctionDetail) -> Option<Self> {
251 let written = numeral.roman_numeral();
252 let (found, _) = table(numeral.key().mode()).find(|(_, figure)| *figure == written)?;
253
254 match detail {
255 FunctionDetail::Full => Some(found),
256 FunctionDetail::MainOnly => Some(found.main_function()),
257 }
258 }
259}
260
261fn table(mode: &str) -> impl Iterator<Item = (HarmonicFunction, &'static str)> {
264 let own = if mode == MINOR {
265 MINOR_ONLY
266 } else {
267 MAJOR_ONLY
268 };
269 own.into_iter().chain(KEY_NEUTRAL)
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 fn key(tonic: &str, mode: &str) -> Key {
277 Key::from_tonic_mode(tonic, mode).unwrap()
278 }
279
280 fn numeral(figure: &str, tonic: &str, mode: &str) -> RomanNumeral {
281 RomanNumeral::new(figure, key(tonic, mode)).unwrap()
282 }
283
284 #[test]
285 fn every_function_has_a_figure_in_either_mode() {
286 for function in HarmonicFunction::ALL {
287 assert!(!function.figure("major").is_empty(), "{function:?}");
288 assert!(!function.figure(MINOR).is_empty(), "{function:?}");
289 assert_eq!(
290 HarmonicFunction::from_symbol(function.symbol()).unwrap(),
291 function
292 );
293 }
294 assert!(HarmonicFunction::from_symbol("TPG").is_err());
295 }
296
297 #[test]
298 fn functions_read_as_music21s_numerals() {
299 let c = key("C", "major");
300 assert_eq!(TonicMajor.to_roman(&c).unwrap().figure(), "I");
301 assert_eq!(TonicMinor.to_roman(&c).unwrap().figure(), "i");
302 assert_eq!(
303 SubdominantMinorParallelklangMajor
304 .to_roman(&c)
305 .unwrap()
306 .figure(),
307 "bVI"
308 );
309 assert_eq!(
310 SubdominantMinorParallelklangMajor
311 .to_roman(&key("a", MINOR))
312 .unwrap()
313 .figure(),
314 "VI"
315 );
316 assert_eq!(
317 TonicMajorParallelklangMinor.to_roman(&c).unwrap().figure(),
318 "vi"
319 );
320 assert_eq!(
321 SubdominantMajorGegenklangMinor
322 .to_roman(&c)
323 .unwrap()
324 .figure(),
325 "vi"
326 );
327 }
328
329 #[test]
330 fn numerals_read_as_music21s_functions() {
331 let full = FunctionDetail::Full;
332 let vi = numeral("vi", "C", "major");
333 assert_eq!(
334 HarmonicFunction::from_roman(&vi, full),
335 Some(TonicMajorParallelklangMinor)
336 );
337
338 let sixth = numeral("VI", "a", MINOR);
339 assert_eq!(
340 HarmonicFunction::from_roman(&sixth, full),
341 Some(SubdominantMinorParallelklangMajor)
342 );
343 assert_eq!(
344 HarmonicFunction::from_roman(&sixth, FunctionDetail::MainOnly),
345 Some(SubdominantMinor)
346 );
347
348 let neapolitan = numeral("bII6", "g", MINOR);
349 assert_eq!(
350 HarmonicFunction::from_roman(&neapolitan, full),
351 Some(SubdominantMinorGegenklangMajor)
352 );
353
354 let mediant = numeral("III", "f", MINOR);
355 assert_eq!(
356 HarmonicFunction::from_roman(&mediant, full),
357 Some(TonicMinorParallelklangMajor)
358 );
359 assert_eq!(
360 HarmonicFunction::from_roman(&numeral("i6", "C", "major"), full),
361 Some(TonicMinor)
362 );
363 assert_eq!(
364 HarmonicFunction::from_roman(&numeral("#iv", "C", "major"), full),
365 None
366 );
367 }
368
369 #[test]
370 fn each_label_names_its_main_function() {
371 let mains: Vec<&str> = HarmonicFunction::ALL
372 .into_iter()
373 .map(|function| function.main_function().symbol())
374 .collect();
375 assert_eq!(
376 mains,
377 [
378 "T", "T", "T", "t", "t", "t", "S", "S", "S", "s", "s", "s", "D", "D", "D", "d",
379 "d", "d"
380 ]
381 );
382 assert_eq!(
383 DominantMinorGegenklangMajor.music21_name(),
384 "DOMINANT_MINOR_GEGENKLANG_MAJOR"
385 );
386 }
387}