Skip to main content

music21/key/
keysignature.rs

1use crate::{
2    defaults::{IntegerType, UnsignedIntegerType},
3    error::{Error, Result},
4    interval::Interval,
5    pitch::{Accidental, Pitch},
6    scale::{FIFTHS_ORDER_SHARP, Scale, ScaleType},
7};
8
9use super::Key;
10
11use crate::interval::constants::{
12    PERFECT_FIFTH_DOWN, PERFECT_FIFTH_UP, PERFECT_FIFTH_UP as PERFECT_FIFTH, PERFECT_FOURTH_UP,
13    PERFECT_FOURTH_UP as PERFECT_FOURTH,
14};
15
16const MODE_SHARPS_ALTER: [(&str, IntegerType); 9] = [
17    ("major", 0),
18    ("ionian", 0),
19    ("minor", -3),
20    ("aeolian", -3),
21    ("dorian", -2),
22    ("phrygian", -4),
23    ("lydian", 1),
24    ("mixolydian", -1),
25    ("locrian", -5),
26];
27
28fn canonical_mode_for_offset(offset: IntegerType) -> Option<&'static str> {
29    match offset {
30        0 => Some("ionian"),
31        -1 => Some("mixolydian"),
32        -2 => Some("dorian"),
33        -3 => Some("aeolian"),
34        -4 => Some("phrygian"),
35        -5 => Some("locrian"),
36        1 => Some("lydian"),
37        _ => None,
38    }
39}
40
41/// Returns the circle-of-fifths sharp-count offset for a mode name.
42pub fn mode_sharps_alter(mode: &str) -> Option<IntegerType> {
43    MODE_SHARPS_ALTER
44        .iter()
45        .find_map(|(name, value)| (*name == mode.to_lowercase()).then_some(*value))
46}
47
48/// The most sharps or flats a signature is read with. music21 sets no
49/// limit, but every count is a walk round the circle of fifths, and past
50/// quadruple sharps its spellings only repeat.
51const MOST_SHARPS: IntegerType = 1000;
52
53/// How many fifths a count of sharps walks, refusing one past
54/// [`MOST_SHARPS`].
55fn fifths_walked(sharps: IntegerType) -> Result<UnsignedIntegerType> {
56    let walked = sharps.unsigned_abs();
57    if walked > MOST_SHARPS.unsigned_abs() {
58        return Err(Error::Key(format!(
59            "a signature of {sharps} sharps is more than {MOST_SHARPS} either way"
60        )));
61    }
62    Ok(walked)
63}
64
65/// Returns the major-key tonic pitch for a key-signature sharp count.
66pub fn sharps_to_pitch(sharp_count: IntegerType) -> Result<Pitch> {
67    if sharp_count == 0 {
68        return Pitch::from_name("C");
69    }
70
71    let walked = fifths_walked(sharp_count)?;
72    let mut pitch = Pitch::from_name("C")?;
73    pitch.octave_setter(None);
74
75    let interval = if sharp_count > 0 {
76        &*PERFECT_FIFTH
77    } else {
78        &*PERFECT_FIFTH_DOWN
79    };
80
81    for _ in 0..walked {
82        pitch = interval.transpose_pitch(&pitch)?;
83        pitch.octave_setter(None);
84    }
85    Ok(pitch)
86}
87
88/// Returns the key-signature sharp count for a tonic pitch and optional mode.
89pub fn pitch_to_sharps(pitch_value: &Pitch, mode: Option<&str>) -> Result<IntegerType> {
90    let step_index = FIFTHS_ORDER_SHARP
91        .iter()
92        .position(|step| *step == pitch_value.step())
93        .ok_or_else(|| Error::StepName("cannot map step to circle of fifths".to_string()))?;
94
95    let mut sharps = step_index as IntegerType - 1;
96    if !pitch_value.accidental().is_twelve_tone() {
97        return Err(Error::Key(
98            "Cannot determine sharps for quarter-tone keys! silly!".to_string(),
99        ));
100    }
101    sharps += 7 * pitch_value.accidental().alter() as IntegerType;
102
103    // A mode nobody has a signature alteration for leaves the signature
104    // where the major key put it, which is what music21 does: `C
105    // hypomixolydian` is written with no sharps and no flats.
106    if let Some(offset) = mode.and_then(mode_sharps_alter) {
107        sharps += offset;
108    }
109
110    Ok(sharps)
111}
112
113/// Returns the key-signature sharp count for a tonic pitch name and optional mode.
114pub fn pitch_name_to_sharps(pitch_name: &str, mode: Option<&str>) -> Result<IntegerType> {
115    let pitch = Pitch::from_name(pitch_name)?;
116    pitch_to_sharps(&pitch, mode)
117}
118
119#[derive(Clone, Debug)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121#[cfg_attr(feature = "serde", serde(from = "SavedKeySignature"))]
122/// A key signature represented by the number of sharps.
123///
124/// Flats are represented as negative sharps, so B-flat major has `-2`.
125/// A non-traditional signature, `E-` and `G#` together, is flagged as one
126/// and lists its pitches; it keeps a count as well, as music21's does
127/// since 14acfa0f2.
128#[must_use]
129pub struct KeySignature {
130    sharps: IntegerType,
131    /// music21's `isNonTraditional`, set on its own.
132    #[cfg_attr(feature = "serde", serde(default))]
133    non_traditional: bool,
134    /// Pitches written over the ones the count implies: music21's
135    /// `_alteredPitches`, which a traditional signature may carry too.
136    altered: Option<Vec<Pitch>>,
137    accidentals_apply_only_to_octave: bool,
138    #[cfg_attr(feature = "serde", serde(default))]
139    color: Option<String>,
140}
141
142/// A key signature as it was saved before the count was always a count:
143/// a missing count was a non-traditional signature.
144#[cfg(feature = "serde")]
145#[derive(serde::Deserialize)]
146struct SavedKeySignature {
147    sharps: Option<IntegerType>,
148    #[serde(default)]
149    non_traditional: bool,
150    altered: Option<Vec<Pitch>>,
151    accidentals_apply_only_to_octave: bool,
152    #[serde(default)]
153    color: Option<String>,
154}
155
156#[cfg(feature = "serde")]
157impl From<SavedKeySignature> for KeySignature {
158    fn from(saved: SavedKeySignature) -> Self {
159        Self {
160            sharps: saved.sharps.unwrap_or(0),
161            non_traditional: saved.non_traditional || saved.sharps.is_none(),
162            altered: saved.altered,
163            accidentals_apply_only_to_octave: saved.accidentals_apply_only_to_octave,
164            color: saved.color,
165        }
166    }
167}
168
169impl PartialEq for KeySignature {
170    /// music21's `equalityAttributes`: the count and the flag.
171    fn eq(&self, other: &Self) -> bool {
172        self.sharps == other.sharps && self.non_traditional == other.non_traditional
173    }
174}
175
176impl KeySignature {
177    /// Creates a key signature from a sharp count.
178    pub fn new(sharps: IntegerType) -> Self {
179        Self {
180            sharps,
181            non_traditional: false,
182            altered: None,
183            accidentals_apply_only_to_octave: false,
184            color: None,
185        }
186    }
187
188    /// The colour the signature is drawn in, if one was said.
189    pub fn color(&self) -> Option<&str> {
190        self.color.as_deref()
191    }
192
193    /// Sets the colour the signature is drawn in.
194    pub fn set_color(&mut self, color: Option<String>) {
195        self.color = color;
196    }
197
198    /// Creates a non-traditional key signature from the pitches it alters:
199    /// `E-` and `G#` together, say, which no count of sharps can express.
200    /// Its count is nought.
201    pub fn from_altered_pitches(pitches: Vec<Pitch>) -> Self {
202        let mut signature = Self::new(0);
203        signature.non_traditional = true;
204        signature.altered = Some(pitches);
205        signature
206    }
207
208    /// Returns the number of sharps, with flats as negative values.
209    pub fn sharps(&self) -> IntegerType {
210        self.sharps
211    }
212
213    /// Replaces the sharp count, leaving the flag and any pitches written
214    /// over it as they are, as music21's `sharps` setter does.
215    pub fn set_sharps(&mut self, sharps: IntegerType) {
216        self.sharps = sharps;
217    }
218
219    /// Writes pitches over the ones the count implies, as music21's
220    /// `alteredPitches` setter does. It does not make the signature
221    /// non-traditional; [`Self::set_non_traditional`] does that.
222    pub fn set_altered_pitches(&mut self, pitches: Vec<Pitch>) {
223        self.altered = Some(pitches);
224    }
225
226    /// Whether the signature is a list of altered pitches rather than a
227    /// count of sharps or flats: music21's `isNonTraditional`.
228    pub fn is_non_traditional(&self) -> bool {
229        self.non_traditional
230    }
231
232    /// Flags the signature non-traditional or traditional, its count and
233    /// pitches left as they are.
234    pub fn set_non_traditional(&mut self, non_traditional: bool) {
235        self.non_traditional = non_traditional;
236    }
237
238    /// Whether the altered pitches of a non-traditional signature carry
239    /// octaves that limit where they apply, as `F#4` altering only that
240    /// octave. music21 records the flag; applying it is a stream's job.
241    pub fn accidentals_apply_only_to_octave(&self) -> bool {
242        self.accidentals_apply_only_to_octave
243    }
244
245    /// Sets whether the altered pitches apply only in their own octave.
246    pub fn set_accidentals_apply_only_to_octave(&mut self, value: bool) {
247        self.accidentals_apply_only_to_octave = value;
248    }
249
250    /// The description music21 prints after `of`: `3 sharps`, `1 flat`,
251    /// `no sharps or flats`, or `pitches: [E-, G#]`.
252    pub fn description(&self) -> String {
253        if self.non_traditional {
254            let names: Vec<String> = self
255                .altered
256                .as_deref()
257                .unwrap_or_default()
258                .iter()
259                .map(Pitch::to_string)
260                .collect();
261            return format!("pitches: [{}]", names.join(", "));
262        }
263        match self.sharps {
264            0 => "no sharps or flats".to_string(),
265            1 => "1 sharp".to_string(),
266            -1 => "1 flat".to_string(),
267            n if n > 1 => format!("{n} sharps"),
268            n => format!("{} flats", -n),
269        }
270    }
271
272    /// Returns the pitches this signature alters, in circle-of-fifths order
273    /// and without octaves: `F# C# G#` for three sharps, `B- E- A-` for three
274    /// flats; a non-traditional signature answers the pitches it was given.
275    pub fn altered_pitches(&self) -> Result<Vec<Pitch>> {
276        if let Some(altered) = &self.altered {
277            return Ok(altered.clone());
278        }
279        if self.non_traditional {
280            return Ok(Vec::new());
281        }
282        let sharps = self.sharps;
283        let (start, interval) = if sharps > 0 {
284            ("B", &*PERFECT_FIFTH)
285        } else {
286            ("F", &*PERFECT_FOURTH)
287        };
288        let walked = fifths_walked(sharps)?;
289        let mut current = Pitch::from_name(start)?;
290        let mut altered = Vec::with_capacity(walked as usize);
291        for _ in 0..walked {
292            current = interval.transpose_pitch(&current)?;
293            current.octave_setter(None);
294            altered.push(current.clone());
295        }
296        Ok(altered)
297    }
298
299    /// Returns the accidental this signature puts on a step letter, if any.
300    pub fn accidental_by_step(&self, step: char) -> Result<Option<Accidental>> {
301        let step = crate::stepname::StepName::try_from(step)?;
302        Ok(self
303            .altered_pitches()?
304            .into_iter()
305            .rev()
306            .find(|pitch| pitch.step() == step)
307            .and_then(|pitch| pitch.written_accidental().cloned()))
308    }
309
310    /// Returns the signature of the major key this one's major tonic moves
311    /// to by the interval, drawn in the colour this one is.
312    pub fn transpose(&self, interval: &Interval) -> Result<Self> {
313        let tonic = self.try_as_key(Some("major"), None)?.tonic();
314        let mut transposed = interval.transpose_pitch(&tonic)?;
315        if interval.implicit_diatonic && pitch_to_sharps(&transposed, None)?.abs() > 6 {
316            transposed = transposed.get_enharmonic()?;
317        }
318        // music21 transposes the object, so only the count moves: its colour,
319        // its flag and any pitches written over it stay.
320        let mut moved = self.clone();
321        moved.sharps = pitch_to_sharps(&transposed, None)?;
322        Ok(moved)
323    }
324
325    /// Transposes a pitch spelled in C into this key signature the way
326    /// music21's `transposePitchFromC` does: up a fifth per sharp, or up a
327    /// fourth per flat, keeping the pitch's own octave, so `E4` in two sharps
328    /// is `F#4` and in three flats `G4`.
329    pub fn transpose_pitch_from_c(&self, pitch: &Pitch) -> Result<Pitch> {
330        let sharps = self.sharps;
331        if sharps == 0 {
332            return Ok(pitch.clone());
333        }
334        let step = if sharps < 0 {
335            &*PERFECT_FOURTH_UP
336        } else {
337            &*PERFECT_FIFTH_UP
338        };
339        let walked = fifths_walked(sharps)?;
340        let mut transposed = pitch.clone();
341        for _ in 0..walked {
342            transposed = step.transpose_pitch_with_options(&transposed, false, None)?;
343        }
344        transposed.octave_setter(pitch.octave());
345        Ok(transposed)
346    }
347
348    /// Returns the major or minor scale this signature implies.
349    pub fn scale(&self, mode: &str) -> Result<Scale> {
350        let scale_type = match mode {
351            "major" => ScaleType::Major,
352            "minor" => ScaleType::Minor,
353            other => {
354                return Err(Error::Key(format!(
355                    "no mapping to a scale exists for this mode yet: {other}"
356                )));
357            }
358        };
359        Ok(Scale::new(
360            scale_type,
361            self.try_as_key(Some(mode), None)?.tonic(),
362        ))
363    }
364
365    /// Converts this signature to a key in the given mode.
366    pub fn as_key(&self, mode: &str) -> Key {
367        self.try_as_key(Some(mode), None).unwrap_or_else(|_| {
368            Key::new(Pitch::from_name("C").expect("C is valid pitch"), "major", 0)
369        })
370    }
371
372    /// Converts this signature to a key, optionally inferring mode from tonic.
373    pub fn try_as_key(&self, mode: Option<&str>, tonic: Option<&str>) -> Result<Key> {
374        let our_sharps = self.sharps;
375
376        let resolved_mode = if mode.is_none() && tonic.is_none() {
377            "major".to_string()
378        } else if mode.is_none() && tonic.is_some() {
379            let tonic_name = tonic.expect("checked is_some above");
380            let major_sharps = pitch_name_to_sharps(tonic_name, None)?;
381            canonical_mode_for_offset(our_sharps.saturating_sub(major_sharps))
382                .ok_or_else(|| {
383                    Error::Key(format!(
384                        "Could not solve for mode from sharps={our_sharps}, tonic={tonic_name}"
385                    ))
386                })?
387                .to_string()
388        } else {
389            mode.expect("checked is_some above").to_lowercase()
390        };
391
392        let sharp_alteration_from_major = mode_sharps_alter(&resolved_mode)
393            .ok_or_else(|| Error::Key(format!("Mode {resolved_mode} is unknown")))?;
394
395        let tonic_pitch = sharps_to_pitch(our_sharps.saturating_sub(sharp_alteration_from_major))?;
396        Ok(Key::new(tonic_pitch, &resolved_mode, our_sharps))
397    }
398}
399
400impl std::fmt::Display for KeySignature {
401    /// `KeySignature of 3 sharps`, `KeySignature of pitches: [E-, G#]`.
402    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403        write!(f, "KeySignature of {}", self.description())
404    }
405}
406
407#[cfg(test)]
408mod non_traditional_tests {
409    use super::*;
410
411    /// The flag turns on and off again and the count stays through both.
412    #[test]
413    fn a_signature_can_be_made_non_traditional_and_back() {
414        let mut signature = KeySignature::new(3);
415        assert!(!signature.is_non_traditional());
416
417        signature.set_non_traditional(true);
418        assert!(signature.is_non_traditional());
419        assert_eq!(signature.sharps(), 3);
420
421        signature.set_non_traditional(false);
422        assert!(!signature.is_non_traditional());
423        assert_eq!(signature.sharps(), 3);
424    }
425
426    fn pitches(names: &[&str]) -> Vec<Pitch> {
427        names
428            .iter()
429            .map(|name| Pitch::from_name(*name).unwrap())
430            .collect()
431    }
432
433    /// music21 14acfa0f2: `sharps` is always a count, and `isNonTraditional`
434    /// is a flag of its own.
435    #[test]
436    fn a_pitch_list_makes_a_non_traditional_signature() {
437        let unusual = KeySignature::from_altered_pitches(pitches(&["Eb", "G#"]));
438        assert!(unusual.is_non_traditional());
439        assert_eq!(unusual.sharps(), 0);
440        assert_eq!(unusual.to_string(), "KeySignature of pitches: [Eb, G#]");
441        assert_eq!(
442            unusual.accidental_by_step('G').unwrap().unwrap().name(),
443            "sharp"
444        );
445        assert_eq!(unusual.accidental_by_step('A').unwrap(), None);
446        // Its count still names a key and still transposes.
447        assert_eq!(
448            unusual
449                .try_as_key(Some("major"), None)
450                .unwrap()
451                .tonic()
452                .name(),
453            "C"
454        );
455        assert_eq!(
456            unusual
457                .transpose_pitch_from_c(&Pitch::from_name("C4").unwrap())
458                .unwrap()
459                .name_with_octave(),
460            "C4"
461        );
462    }
463
464    /// Each of the three is set on its own, as music21's attributes are.
465    #[test]
466    fn sharps_flag_and_pitches_are_set_apart() {
467        // Pitches written over a traditional signature leave it traditional.
468        let mut signature = KeySignature::new(2);
469        signature.set_altered_pitches(pitches(&["Eb"]));
470        assert!(!signature.is_non_traditional());
471        assert_eq!(signature.sharps(), 2);
472        assert_eq!(signature.to_string(), "KeySignature of 2 sharps");
473        assert_eq!(signature.altered_pitches().unwrap(), pitches(&["Eb"]));
474        signature.set_sharps(3);
475        assert_eq!(signature.altered_pitches().unwrap(), pitches(&["Eb"]));
476
477        // The flag keeps the count; the count keeps the flag.
478        let mut flagged = KeySignature::new(2);
479        flagged.set_non_traditional(true);
480        assert_eq!(flagged.sharps(), 2);
481        assert_eq!(flagged.to_string(), "KeySignature of pitches: []");
482        assert_eq!(flagged.try_as_key(None, None).unwrap().tonic().name(), "D");
483        flagged.set_sharps(3);
484        assert!(flagged.is_non_traditional());
485        flagged.set_non_traditional(false);
486        assert_eq!(flagged.to_string(), "KeySignature of 3 sharps");
487
488        assert!(!signature.accidentals_apply_only_to_octave());
489        signature.set_accidentals_apply_only_to_octave(true);
490        assert!(signature.accidentals_apply_only_to_octave());
491        assert_eq!(
492            KeySignature::new(0).to_string(),
493            "KeySignature of no sharps or flats"
494        );
495        assert_eq!(KeySignature::new(-4).description(), "4 flats");
496    }
497
498    /// A transposed signature keeps its flag and its pitches; only the count
499    /// moves.
500    #[test]
501    fn transposing_moves_only_the_count() {
502        let mut unusual = KeySignature::new(2);
503        unusual.set_non_traditional(true);
504        unusual.set_altered_pitches(pitches(&["B-", "F#"]));
505        let moved = unusual
506            .transpose(&Interval::from_name("M2").unwrap())
507            .unwrap();
508        assert_eq!(moved.sharps(), 4);
509        assert!(moved.is_non_traditional());
510        assert_eq!(moved.altered_pitches().unwrap(), pitches(&["B-", "F#"]));
511    }
512
513    /// A non-traditional signature is not the C-major one it shares a count
514    /// with.
515    #[test]
516    fn the_flag_counts_in_equality() {
517        let mut flagged = KeySignature::new(2);
518        flagged.set_non_traditional(true);
519        assert_ne!(flagged, KeySignature::new(2));
520        assert_eq!(KeySignature::new(2), KeySignature::new(2));
521    }
522
523    #[test]
524    fn traditional_altered_pitches_carry_explicit_accidentals() {
525        let three_sharps = KeySignature::new(3);
526        let altered = three_sharps.altered_pitches().unwrap();
527        assert!(
528            altered
529                .iter()
530                .all(|pitch| pitch.written_accidental().is_some())
531        );
532        assert_eq!(
533            three_sharps
534                .accidental_by_step('F')
535                .unwrap()
536                .unwrap()
537                .name(),
538            "sharp"
539        );
540    }
541}
542
543#[cfg(test)]
544mod tests {
545
546    #[test]
547    fn a_signature_names_a_key_in_a_mode_or_from_a_tonic() {
548        let two_sharps = KeySignature::new(2);
549        assert_eq!(two_sharps.as_key("major").tonic().name(), "D");
550        assert_eq!(two_sharps.as_key("minor").tonic().name(), "B");
551        let from_tonic = two_sharps.try_as_key(None, Some("b")).unwrap();
552        assert_eq!(
553            (from_tonic.tonic().name(), from_tonic.mode()),
554            ("B".to_string(), "aeolian")
555        );
556        assert_eq!(
557            two_sharps.try_as_key(None, None).unwrap().tonic().name(),
558            "D"
559        );
560        // A mode given beside a tonic wins, and the tonic is not consulted.
561        assert_eq!(
562            two_sharps
563                .try_as_key(Some("major"), Some("C"))
564                .unwrap()
565                .tonic()
566                .name(),
567            "D"
568        );
569        let altered = KeySignature::from_altered_pitches(vec![Pitch::from_name("F#").unwrap()]);
570        assert_eq!(altered.as_key("major").tonic().name(), "C");
571        assert_eq!(
572            pitch_to_sharps(&Pitch::from_name("D").unwrap(), Some("major")).unwrap(),
573            2
574        );
575        assert_eq!(
576            pitch_to_sharps(&Pitch::from_name("D").unwrap(), Some("minor")).unwrap(),
577            -1
578        );
579        // A mode with no signature alteration leaves the major key's count.
580        assert_eq!(
581            pitch_to_sharps(&Pitch::from_name("D").unwrap(), Some("nonsense")).unwrap(),
582            2
583        );
584    }
585
586    #[test]
587    fn transpose_pitch_from_c_matches_music21() {
588        let cases: [(i32, [&str; 5]); 7] = [
589            (0, ["C4", "E4", "Bb3", "F#5", "G"]),
590            (1, ["G4", "B4", "F3", "C#5", "D"]),
591            (2, ["D4", "F#4", "C3", "G#5", "A"]),
592            (-1, ["F4", "A4", "Eb3", "B5", "C"]),
593            (-3, ["Eb4", "G4", "Db3", "A5", "Bb"]),
594            (7, ["C#4", "E#4", "B3", "F##5", "G#"]),
595            (-7, ["Cb4", "Eb4", "Bbb3", "F5", "Gb"]),
596        ];
597        for (sharps, expected) in cases {
598            let signature = KeySignature::new(sharps);
599            let actual = ["C4", "E4", "Bb3", "F#5", "G"].map(|name| {
600                signature
601                    .transpose_pitch_from_c(&Pitch::from_name(name).unwrap())
602                    .unwrap()
603                    .name_with_octave()
604            });
605            assert_eq!(actual, expected, "{sharps} sharps");
606        }
607    }
608    use super::*;
609
610    #[test]
611    fn altered_pitches_and_transpositions_match_music21() {
612        type Row = (
613            i32,
614            &'static [&'static str],
615            [Option<&'static str>; 3],
616            [i32; 3],
617            &'static str,
618            &'static str,
619        );
620        let cases: [Row; 15] = [
621            (
622                -7,
623                &["Bb", "Eb", "Ab", "Db", "Gb", "Cb", "Fb"],
624                [Some("flat"), Some("flat"), Some("flat")],
625                [-6, -8, -12],
626                "Cb",
627                "Ab",
628            ),
629            (
630                -6,
631                &["Bb", "Eb", "Ab", "Db", "Gb", "Cb"],
632                [None, Some("flat"), Some("flat")],
633                [-5, -7, -11],
634                "Gb",
635                "Eb",
636            ),
637            (
638                -5,
639                &["Bb", "Eb", "Ab", "Db", "Gb"],
640                [None, Some("flat"), None],
641                [-4, -6, -10],
642                "Db",
643                "Bb",
644            ),
645            (
646                -4,
647                &["Bb", "Eb", "Ab", "Db"],
648                [None, Some("flat"), None],
649                [-3, -5, -9],
650                "Ab",
651                "F",
652            ),
653            (
654                -3,
655                &["Bb", "Eb", "Ab"],
656                [None, Some("flat"), None],
657                [-2, -4, -8],
658                "Eb",
659                "C",
660            ),
661            (
662                -2,
663                &["Bb", "Eb"],
664                [None, Some("flat"), None],
665                [-1, -3, -7],
666                "Bb",
667                "G",
668            ),
669            (
670                -1,
671                &["Bb"],
672                [None, Some("flat"), None],
673                [0, -2, -6],
674                "F",
675                "D",
676            ),
677            (0, &[], [None, None, None], [1, -1, -5], "C", "A"),
678            (
679                1,
680                &["F#"],
681                [Some("sharp"), None, None],
682                [2, 0, -4],
683                "G",
684                "E",
685            ),
686            (
687                2,
688                &["F#", "C#"],
689                [Some("sharp"), None, Some("sharp")],
690                [3, 1, -3],
691                "D",
692                "B",
693            ),
694            (
695                3,
696                &["F#", "C#", "G#"],
697                [Some("sharp"), None, Some("sharp")],
698                [4, 2, -2],
699                "A",
700                "F#",
701            ),
702            (
703                4,
704                &["F#", "C#", "G#", "D#"],
705                [Some("sharp"), None, Some("sharp")],
706                [5, 3, -1],
707                "E",
708                "C#",
709            ),
710            (
711                5,
712                &["F#", "C#", "G#", "D#", "A#"],
713                [Some("sharp"), None, Some("sharp")],
714                [6, 4, 0],
715                "B",
716                "G#",
717            ),
718            (
719                6,
720                &["F#", "C#", "G#", "D#", "A#", "E#"],
721                [Some("sharp"), None, Some("sharp")],
722                [7, 5, 1],
723                "F#",
724                "D#",
725            ),
726            (
727                7,
728                &["F#", "C#", "G#", "D#", "A#", "E#", "B#"],
729                [Some("sharp"), Some("sharp"), Some("sharp")],
730                [8, 6, 2],
731                "C#",
732                "A#",
733            ),
734        ];
735        for (sharps, altered, by_step, transposed, major, minor) in cases {
736            let signature = KeySignature::new(sharps);
737            let names = signature
738                .altered_pitches()
739                .unwrap()
740                .iter()
741                .map(Pitch::name)
742                .collect::<Vec<_>>();
743            assert_eq!(names, altered, "{sharps} altered");
744            for (step, expected) in ['F', 'B', 'C'].into_iter().zip(by_step) {
745                let accidental = signature.accidental_by_step(step).unwrap();
746                assert_eq!(
747                    accidental.as_ref().map(Accidental::name),
748                    expected,
749                    "{sharps} {step}"
750                );
751            }
752            for (name, expected) in ["P5", "-P5", "m2"].into_iter().zip(transposed) {
753                let moved = signature
754                    .transpose(&Interval::from_name(name).unwrap())
755                    .unwrap();
756                assert_eq!(moved.sharps(), expected, "{sharps} by {name}");
757            }
758            assert_eq!(signature.scale("major").unwrap().tonic().name(), major);
759            assert_eq!(signature.scale("minor").unwrap().tonic().name(), minor);
760        }
761        assert!(KeySignature::new(0).scale("dorian").is_err());
762        assert!(KeySignature::new(0).accidental_by_step('H').is_err());
763    }
764    #[test]
765    fn keysignature_as_key_major_minor() {
766        let ks = KeySignature::new(2);
767        assert_eq!(ks.as_key("major").tonic().name(), "D");
768        assert_eq!(ks.as_key("minor").tonic().name(), "B");
769    }
770
771    #[test]
772    fn keysignature_mode_inference_from_tonic() {
773        let ks = KeySignature::new(0);
774        let key = ks.try_as_key(None, Some("D")).unwrap();
775        assert_eq!(key.mode(), "dorian");
776        assert_eq!(key.tonic().name(), "D");
777    }
778
779    #[test]
780    fn sharps_to_pitch_roundtrip() {
781        let f_sharp = sharps_to_pitch(6).unwrap();
782        assert_eq!(f_sharp.name(), "F#");
783        let b_flat = sharps_to_pitch(-2).unwrap();
784        assert_eq!(b_flat.name(), "Bb");
785        assert_eq!(sharps_to_pitch(-7).unwrap().name(), "Cb");
786        assert_eq!(sharps_to_pitch(7).unwrap().name(), "C#");
787        assert_eq!(KeySignature::new(-7).as_key("major").tonic().name(), "Cb");
788    }
789
790    #[test]
791    fn a_count_of_sharps_past_the_limit_is_refused() {
792        for sharps in [MOST_SHARPS + 1, IntegerType::MAX, IntegerType::MIN] {
793            let signature = KeySignature::new(sharps);
794            assert!(sharps_to_pitch(sharps).is_err(), "{sharps}");
795            assert!(signature.altered_pitches().is_err(), "{sharps}");
796            assert!(
797                signature.try_as_key(Some("minor"), None).is_err(),
798                "{sharps}"
799            );
800            let c = Pitch::from_name("C4").unwrap();
801            assert!(signature.transpose_pitch_from_c(&c).is_err(), "{sharps}");
802        }
803        assert!(sharps_to_pitch(MOST_SHARPS).is_ok());
804    }
805}