Skip to main content

music21_rs/key/
keysignature.rs

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