1use itertools::Itertools;
17
18use crate::{
19 defaults::IntegerType,
20 error::{Error, Result},
21 pitch::Pitch,
22};
23
24const ALTER_LIMIT: IntegerType = 1;
26
27const PENALTY_OFF: IntegerType = 1;
29
30const BASE40_STEPS: [(char, IntegerType); 7] = [
34 ('C', 3),
35 ('D', 9),
36 ('E', 15),
37 ('F', 20),
38 ('G', 26),
39 ('A', 32),
40 ('B', 38),
41];
42
43const BASE40_MODIFIERS: [(&str, IntegerType); 5] =
45 [("bb", -2), ("b", -1), ("", 0), ("#", 1), ("##", 2)];
46
47const BASE40_OCTAVE: IntegerType = 40;
49
50const BASE40_INTERVALS: [(IntegerType, &str); 25] = [
54 (0, "P1"),
55 (1, "A1"),
56 (4, "d2"),
57 (5, "m2"),
58 (6, "M2"),
59 (7, "A2"),
60 (10, "d3"),
61 (11, "m3"),
62 (12, "M3"),
63 (13, "A3"),
64 (16, "d4"),
65 (17, "P4"),
66 (18, "A4"),
67 (22, "d5"),
68 (23, "P5"),
69 (24, "A5"),
70 (27, "d6"),
71 (28, "m6"),
72 (29, "M6"),
73 (30, "A6"),
74 (33, "d7"),
75 (34, "m7"),
76 (35, "M7"),
77 (36, "A7"),
78 (39, "d8"),
79];
80
81const BASE40_UNKNOWN: &str = "ddd";
83
84#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct EnharmonicRules {
90 alteration: Option<IntegerType>,
91 aug_dim: Option<IntegerType>,
92 mixture: Option<IntegerType>,
93}
94
95impl Default for EnharmonicRules {
96 fn default() -> Self {
97 Self::MELODIC
98 }
99}
100
101impl EnharmonicRules {
102 pub const MELODIC: Self = Self {
105 alteration: Some(4),
106 aug_dim: Some(2),
107 mixture: None,
108 };
109
110 pub const CHORDAL: Self = Self {
113 mixture: Some(2),
114 ..Self::MELODIC
115 };
116
117 pub fn new(
120 alteration: Option<IntegerType>,
121 aug_dim: Option<IntegerType>,
122 mixture: Option<IntegerType>,
123 ) -> Self {
124 let on = |penalty: Option<IntegerType>| penalty.filter(|penalty| *penalty != 0);
125 Self {
126 alteration: on(alteration),
127 aug_dim: on(aug_dim),
128 mixture: on(mixture),
129 }
130 }
131
132 pub fn alteration_penalty(self) -> Option<IntegerType> {
134 self.alteration
135 }
136
137 pub fn aug_dim_penalty(self) -> Option<IntegerType> {
139 self.aug_dim
140 }
141
142 pub fn mixture_penalty(self) -> Option<IntegerType> {
144 self.mixture
145 }
146
147 pub fn alteration_score(self, spelling: &[Pitch]) -> IntegerType {
150 let Some(penalty) = self.alteration else {
151 return PENALTY_OFF;
152 };
153
154 let (flats, sharps) = accidental_counts(spelling);
155 (flats + sharps + 1) * penalty
156 }
157
158 pub fn mixture_score(self, spelling: &[Pitch]) -> IntegerType {
161 let Some(penalty) = self.mixture else {
162 return PENALTY_OFF;
163 };
164
165 let (flats, sharps) = accidental_counts(spelling);
166 flats.min(sharps) * penalty
167 }
168
169 pub fn aug_dim_score(self, spelling: &[Pitch]) -> Result<IntegerType> {
178 let Some(penalty) = self.aug_dim else {
179 return Ok(PENALTY_OFF);
180 };
181
182 let mut odd_steps = 0;
183 for pair in spelling.windows(2) {
184 let low = base40(&pair[0])?;
185 let high = base40(&pair[1])?;
186
187 let name = base40_interval((high - low).rem_euclid(BASE40_OCTAVE));
190 odd_steps += name.chars().filter(|c| matches!(c, 'A' | 'd')).count() as IntegerType;
191 }
192
193 Ok((odd_steps + 1) * penalty)
194 }
195
196 fn score(self, spelling: &[Pitch]) -> Result<IntegerType> {
198 Ok(self.aug_dim_score(spelling)?
199 + self.alteration_score(spelling)
200 + self.mixture_score(spelling))
201 }
202}
203
204pub fn spelling_options(pitches: &[Pitch]) -> Vec<Vec<Pitch>> {
208 pitches
209 .iter()
210 .map(|pitch| {
211 let mut options = vec![pitch.clone()];
212 options.extend(pitch.all_common_enharmonics(ALTER_LIMIT));
213 options
214 })
215 .collect()
216}
217
218pub fn best_choice(options: &[Vec<Pitch>], rules: EnharmonicRules) -> Result<Vec<usize>> {
227 let positions = options.iter().map(|option| 0..option.len());
228
229 let mut best: Option<(IntegerType, Vec<usize>)> = None;
230 for choice in positions.multi_cartesian_product() {
231 let spelling: Vec<Pitch> = choice
232 .iter()
233 .zip(options)
234 .map(|(index, option)| option[*index].clone())
235 .collect();
236 let score = rules.score(&spelling)?;
237 if best.as_ref().is_some_and(|(lowest, _)| score >= *lowest) {
238 continue;
239 }
240 best = Some((score, choice));
241 }
242
243 Ok(best.map(|(_, choice)| choice).unwrap_or_default())
244}
245
246pub fn best_spelling(pitches: &[Pitch], rules: EnharmonicRules) -> Result<Vec<Pitch>> {
265 if pitches.is_empty() {
266 return Err(Error::Analysis(
267 "an enharmonic spelling needs at least one pitch".to_string(),
268 ));
269 }
270
271 let options = spelling_options(pitches);
272 let choice = best_choice(&options, rules)?;
273 Ok(choice
274 .into_iter()
275 .zip(options)
276 .map(|(index, mut option)| option.swap_remove(index))
277 .collect())
278}
279
280fn accidental_counts(spelling: &[Pitch]) -> (IntegerType, IntegerType) {
282 spelling.iter().fold((0, 0), |(flats, sharps), pitch| {
283 let modifier = pitch.accidental().modifier();
284 let count = |mark: char| modifier.chars().filter(|c| *c == mark).count() as IntegerType;
285 (flats + count('b'), sharps + count('#'))
286 })
287}
288
289fn base40(pitch: &Pitch) -> Result<IntegerType> {
291 let name = pitch.name();
292 let mut chars = name.chars();
293 let step = chars.next();
294 let modifier = chars.as_str();
295
296 let natural = BASE40_STEPS
297 .iter()
298 .find(|(letter, _)| Some(*letter) == step);
299 let shift = BASE40_MODIFIERS
300 .iter()
301 .find(|(written, _)| *written == modifier);
302 let (Some((_, natural)), Some((_, shift))) = (natural, shift) else {
303 return Err(Error::Analysis(format!("{name} has no place in base 40")));
304 };
305
306 Ok(natural + shift)
307}
308
309fn base40_interval(distance: IntegerType) -> &'static str {
311 BASE40_INTERVALS
312 .iter()
313 .find(|(known, _)| *known == distance)
314 .map_or(BASE40_UNKNOWN, |(_, name)| name)
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 fn pitches(names: &[&str]) -> Vec<Pitch> {
322 names.iter().map(|name| name.parse().unwrap()).collect()
323 }
324
325 fn names(pitches: &[Pitch]) -> Vec<String> {
326 pitches.iter().map(Pitch::name_with_octave).collect()
327 }
328
329 #[test]
330 fn a_plain_spelling_stays() {
331 let best = best_spelling(&pitches(&["C", "D", "E"]), EnharmonicRules::MELODIC).unwrap();
332 assert_eq!(names(&best), ["C", "D", "E"]);
333 }
334
335 #[test]
336 fn double_accidentals_are_simplified() {
337 let best = best_spelling(&pitches(&["Dbb", "E", "F##"]), EnharmonicRules::MELODIC).unwrap();
338 assert_eq!(names(&best), ["C", "E", "G"]);
339 }
340
341 #[test]
342 fn an_octave_moves_with_its_respelling() {
343 let best =
344 best_spelling(&pitches(&["B#3", "F-4", "G4"]), EnharmonicRules::MELODIC).unwrap();
345 assert_eq!(names(&best), ["C4", "E4", "G4"]);
346 }
347
348 #[test]
349 fn no_pitches_are_refused() {
350 assert!(best_spelling(&[], EnharmonicRules::MELODIC).is_err());
351 }
352
353 #[test]
354 fn scores_add_up_as_music21_adds_them() {
355 let spelling = pitches(&["C", "E", "G#"]);
357 assert_eq!(EnharmonicRules::MELODIC.alteration_score(&spelling), 8);
358 assert_eq!(
359 EnharmonicRules::MELODIC.aug_dim_score(&spelling).unwrap(),
360 2
361 );
362 let augmented = pitches(&["C", "E-", "G#"]);
363 assert_eq!(
364 EnharmonicRules::MELODIC.aug_dim_score(&augmented).unwrap(),
365 4
366 );
367 assert_eq!(
368 EnharmonicRules::MELODIC.mixture_score(&spelling),
369 PENALTY_OFF
370 );
371
372 let mixed = pitches(&["C#", "E-", "G#"]);
373 assert_eq!(EnharmonicRules::CHORDAL.mixture_score(&mixed), 2);
374 }
375
376 #[test]
377 fn a_distance_base40_cannot_spell_reads_as_three_odd_steps() {
378 let odd = pitches(&["C", "E##"]);
380 assert_eq!(EnharmonicRules::MELODIC.aug_dim_score(&odd).unwrap(), 8);
381 }
382
383 #[test]
384 fn a_penalty_that_is_off_scores_one() {
385 let off = EnharmonicRules::new(Some(0), None, Some(0));
386 let spelling = pitches(&["C###", "E-", "G#"]);
387 assert_eq!(off.alteration_score(&spelling), PENALTY_OFF);
388 assert_eq!(off.aug_dim_score(&spelling).unwrap(), PENALTY_OFF);
389 assert_eq!(off.mixture_score(&spelling), PENALTY_OFF);
390 }
391
392 #[test]
393 fn the_best_choice_indexes_each_position_s_options() {
394 let options = spelling_options(&pitches(&["Dbb", "E"]));
395 assert_eq!(names(&options[0])[0], "Dbb");
396 let choice = best_choice(&options, EnharmonicRules::MELODIC).unwrap();
397 assert_eq!(options[0][choice[0]].name(), "C");
398 assert_eq!(choice[1], 0);
399 }
400
401 #[test]
402 fn the_presets_are_music21s_rule_classes() {
403 assert_eq!(EnharmonicRules::default(), EnharmonicRules::MELODIC);
404
405 let melodic = EnharmonicRules::MELODIC;
406 assert_eq!(melodic.alteration_penalty(), Some(4));
407 assert_eq!(melodic.aug_dim_penalty(), Some(2));
408 assert_eq!(melodic.mixture_penalty(), None);
409 assert_eq!(EnharmonicRules::CHORDAL.mixture_penalty(), Some(2));
410
411 assert_eq!(EnharmonicRules::new(Some(4), Some(2), Some(0)), melodic);
412 }
413
414 #[test]
415 fn a_triple_sharp_has_no_base40_place() {
416 let spelling = pitches(&["C###", "E"]);
417 assert!(EnharmonicRules::MELODIC.aug_dim_score(&spelling).is_err());
418 }
419}