1pub use keysignature::{
2 KeySignature, mode_sharps_alter, pitch_name_to_sharps, pitch_to_sharps, sharps_to_pitch,
3};
4
5use crate::{
6 chord::Chord,
7 defaults::IntegerType,
8 error::{Error, Result},
9 pitch::Pitch,
10 scale::diatonicscale::DiatonicScale,
11};
12use std::str::FromStr;
13
14pub mod keysignature;
16
17#[derive(Clone, Debug)]
18pub struct Key {
20 tonic_pitch: Pitch,
21 mode: String,
22 sharps: IntegerType,
23}
24
25impl Key {
26 pub(crate) fn new(tonic_pitch: Pitch, mode: &str, sharps: IntegerType) -> Self {
27 Self {
28 tonic_pitch,
29 mode: mode.to_string(),
30 sharps,
31 }
32 }
33
34 pub fn from_tonic_mode<'a, M>(tonic: &str, mode: M) -> Result<Self>
39 where
40 M: Into<Option<&'a str>>,
41 {
42 let tonic_pitch = Pitch::from_name(tonic.to_string())?;
43
44 let mode = mode.into();
45 let resolved_mode = match mode {
46 Some(mode) => mode.to_lowercase(),
47 None => {
48 if tonic.chars().all(|ch| !ch.is_ascii_uppercase()) {
49 "minor".to_string()
50 } else {
51 "major".to_string()
52 }
53 }
54 };
55
56 let sharps = pitch_to_sharps(&tonic_pitch, Some(&resolved_mode))?;
57 Ok(Self::new(tonic_pitch, &resolved_mode, sharps))
58 }
59
60 pub fn from_tonic(tonic: &str) -> Result<Self> {
62 Self::from_tonic_mode(tonic, None::<&str>)
63 }
64
65 pub fn tonic(&self) -> Pitch {
67 self.tonic_pitch.clone()
68 }
69
70 pub fn tonic_pitch(&self) -> &Pitch {
72 &self.tonic_pitch
73 }
74
75 pub fn mode(&self) -> &str {
77 &self.mode
78 }
79
80 pub fn sharps(&self) -> IntegerType {
82 self.sharps
83 }
84
85 pub fn key_signature(&self) -> KeySignature {
87 KeySignature::new(self.sharps)
88 }
89
90 pub fn scale(&self) -> DiatonicScale {
92 DiatonicScale::new(self.tonic_pitch.clone(), self.sharps, &self.mode)
93 }
94
95 pub fn pitch_from_degree(&self, degree: usize) -> Result<Pitch> {
97 self.scale().pitch_from_degree(degree)
98 }
99
100 pub fn pitches(&self) -> Result<Vec<Pitch>> {
102 self.scale().pitches()
103 }
104
105 pub fn triad_from_degree(&self, degree: usize) -> Result<Chord> {
107 self.scale().triad_from_degree(degree)
108 }
109
110 pub fn seventh_chord_from_degree(&self, degree: usize) -> Result<Chord> {
112 self.scale().seventh_chord_from_degree(degree)
113 }
114
115 pub fn harmonized_triads(&self) -> Result<Vec<Chord>> {
117 (1..=7)
118 .map(|degree| self.triad_from_degree(degree))
119 .collect()
120 }
121
122 pub fn harmonized_sevenths(&self) -> Result<Vec<Chord>> {
124 (1..=7)
125 .map(|degree| self.seventh_chord_from_degree(degree))
126 .collect()
127 }
128
129 pub fn relative(&self) -> Result<Self> {
131 match self.mode.as_str() {
132 "major" => self.key_signature().try_as_key(Some("minor"), None),
133 "minor" => self.key_signature().try_as_key(Some("major"), None),
134 _ => Ok(self.clone()),
135 }
136 }
137
138 pub fn parallel(&self) -> Result<Self> {
140 match self.mode.as_str() {
141 "major" => Self::from_tonic_mode(&self.tonic_pitch.name(), "minor"),
142 "minor" => Self::from_tonic_mode(&self.tonic_pitch.name(), "major"),
143 _ => Ok(self.clone()),
144 }
145 }
146}
147
148impl FromStr for Key {
149 type Err = Error;
150
151 fn from_str(value: &str) -> Result<Self> {
152 parse_key(value)
153 }
154}
155
156impl TryFrom<&str> for Key {
157 type Error = Error;
158
159 fn try_from(value: &str) -> Result<Self> {
160 value.parse()
161 }
162}
163
164impl TryFrom<String> for Key {
165 type Error = Error;
166
167 fn try_from(value: String) -> Result<Self> {
168 value.parse()
169 }
170}
171
172fn parse_key(value: &str) -> Result<Key> {
173 let trimmed = value.trim();
174 if trimmed.is_empty() {
175 return Err(Error::Analysis("key cannot be empty".to_string()));
176 }
177
178 let parts = trimmed.split_whitespace().collect::<Vec<_>>();
179 match parts.as_slice() {
180 [tonic] => {
181 if let Some((tonic, mode)) = split_compact_key_token(tonic) {
182 Key::from_tonic_mode(tonic, Some(mode.as_str()))
183 } else {
184 Key::from_tonic(tonic)
185 }
186 }
187 [tonic, mode] => {
188 let mode = canonical_key_mode(mode);
189 Key::from_tonic_mode(tonic, Some(mode.as_str()))
190 }
191 _ => Err(Error::Analysis(format!(
192 "invalid key {value:?}; use a tonic and optional mode, such as \"C\", \"C major\", or \"Am\""
193 ))),
194 }
195}
196
197fn split_compact_key_token(token: &str) -> Option<(&str, String)> {
198 let lower = token.to_ascii_lowercase();
199 for suffix in ["major", "minor", "maj", "min", "m"] {
200 if lower.ends_with(suffix) && lower.len() > suffix.len() {
201 let tonic_end = token.len() - suffix.len();
202 return Some((&token[..tonic_end], canonical_key_mode(suffix)));
203 }
204 }
205 None
206}
207
208fn canonical_key_mode(mode: &str) -> String {
209 match mode.to_ascii_lowercase().as_str() {
210 "maj" | "major" => "major".to_string(),
211 "m" | "min" | "minor" => "minor".to_string(),
212 other => other.to_string(),
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::key::keysignature::pitch_name_to_sharps;
220
221 #[test]
222 fn key_from_tonic_mode() {
223 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
224 assert_eq!(c_major.sharps(), 0);
225 let g_major = Key::from_tonic_mode("G", Some("major")).unwrap();
226 assert_eq!(g_major.sharps(), 1);
227 let a_minor = Key::from_tonic_mode("A", Some("minor")).unwrap();
228 assert_eq!(a_minor.sharps(), 0);
229 let e_phrygian = Key::from_tonic_mode("E", Some("phrygian")).unwrap();
230 assert_eq!(e_phrygian.sharps(), 0);
231 }
232
233 #[test]
234 fn key_from_string_accepts_common_notation() {
235 let c_major: Key = "C major".parse().unwrap();
236 assert_eq!(c_major.tonic().name(), "C");
237 assert_eq!(c_major.mode(), "major");
238
239 let a_minor: Key = "Am".parse().unwrap();
240 assert_eq!(a_minor.tonic().name(), "A");
241 assert_eq!(a_minor.mode(), "minor");
242
243 let b_flat_minor: Key = "Bb minor".parse().unwrap();
244 assert_eq!(b_flat_minor.tonic().name(), "B-");
245 assert_eq!(b_flat_minor.mode(), "minor");
246 }
247
248 #[test]
249 fn key_scale_degree_and_chords() {
250 let d_major = Key::from_tonic_mode("D", Some("major")).unwrap();
251 assert_eq!(
252 d_major.pitch_from_degree(7).unwrap().name_with_octave(),
253 "C#5"
254 );
255 assert_eq!(
256 d_major.triad_from_degree(1).unwrap().pitched_common_name(),
257 "D-major triad"
258 );
259 assert_eq!(
260 d_major
261 .seventh_chord_from_degree(5)
262 .unwrap()
263 .pitched_common_name(),
264 "A-dominant seventh chord"
265 );
266 }
267
268 #[test]
269 fn key_harmonized_triads() {
270 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
271 let triads = c_major.harmonized_triads().unwrap();
272 assert_eq!(triads.len(), 7);
273 assert_eq!(triads[0].pitched_common_name(), "C-major triad");
274 assert_eq!(triads[4].pitched_common_name(), "G-major triad");
275 }
276
277 #[test]
278 fn key_relative_and_parallel() {
279 let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
280 let relative = c_major.relative().unwrap();
281 assert_eq!(relative.mode(), "minor");
282 assert_eq!(relative.tonic().name(), "A");
283
284 let parallel = c_major.parallel().unwrap();
285 assert_eq!(parallel.mode(), "minor");
286 assert_eq!(parallel.tonic().name(), "C");
287 }
288
289 #[test]
290 fn pitch_name_to_sharps_modes() {
291 assert_eq!(pitch_name_to_sharps("C", Some("major")).unwrap(), 0);
292 assert_eq!(pitch_name_to_sharps("E", Some("minor")).unwrap(), 1);
293 assert_eq!(pitch_name_to_sharps("D", Some("dorian")).unwrap(), 0);
294 assert_eq!(pitch_name_to_sharps("A", Some("mixolydian")).unwrap(), 2);
295 }
296}