1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
8 pub use crate::{
9 CircleOfFifthsPosition, KeyAccidentalCount, KeyError, KeyMode, KeyName, KeySignature,
10 ParallelKeyRelation, RelativeKeyRelation, Tonic,
11 };
12}
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct KeyName(String);
15
16impl KeyName {
17 pub fn new(value: impl AsRef<str>) -> Result<Self, KeyError> {
18 non_empty_text(value).map(Self)
19 }
20
21 pub fn as_str(&self) -> &str {
22 &self.0
23 }
24
25 pub fn value(&self) -> &str {
26 self.as_str()
27 }
28
29 pub fn into_string(self) -> String {
30 self.0
31 }
32}
33
34impl AsRef<str> for KeyName {
35 fn as_ref(&self) -> &str {
36 self.as_str()
37 }
38}
39
40impl fmt::Display for KeyName {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 formatter.write_str(self.as_str())
43 }
44}
45
46impl FromStr for KeyName {
47 type Err = KeyError;
48
49 fn from_str(value: &str) -> Result<Self, Self::Err> {
50 Self::new(value)
51 }
52}
53
54impl TryFrom<&str> for KeyName {
55 type Error = KeyError;
56
57 fn try_from(value: &str) -> Result<Self, Self::Error> {
58 Self::new(value)
59 }
60}
61#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
62pub struct Tonic(String);
63
64impl Tonic {
65 pub fn new(value: impl AsRef<str>) -> Result<Self, KeyError> {
66 non_empty_text(value).map(Self)
67 }
68
69 pub fn as_str(&self) -> &str {
70 &self.0
71 }
72
73 pub fn value(&self) -> &str {
74 self.as_str()
75 }
76
77 pub fn into_string(self) -> String {
78 self.0
79 }
80}
81
82impl AsRef<str> for Tonic {
83 fn as_ref(&self) -> &str {
84 self.as_str()
85 }
86}
87
88impl fmt::Display for Tonic {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 formatter.write_str(self.as_str())
91 }
92}
93
94impl FromStr for Tonic {
95 type Err = KeyError;
96
97 fn from_str(value: &str) -> Result<Self, Self::Err> {
98 Self::new(value)
99 }
100}
101
102impl TryFrom<&str> for Tonic {
103 type Error = KeyError;
104
105 fn try_from(value: &str) -> Result<Self, Self::Error> {
106 Self::new(value)
107 }
108}
109#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
110pub struct KeyAccidentalCount(i8);
111
112impl KeyAccidentalCount {
113 pub fn new(value: i8) -> Result<Self, KeyError> {
114 if !(-7..=7).contains(&value) {
115 return Err(KeyError::OutOfRange);
116 }
117
118 Ok(Self(value))
119 }
120
121 pub const fn value(self) -> i8 {
122 self.0
123 }
124}
125
126impl fmt::Display for KeyAccidentalCount {
127 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128 self.0.fmt(formatter)
129 }
130}
131
132impl FromStr for KeyAccidentalCount {
133 type Err = KeyError;
134
135 fn from_str(value: &str) -> Result<Self, Self::Err> {
136 let parsed = value
137 .trim()
138 .parse::<i8>()
139 .map_err(|_| KeyError::InvalidFormat)?;
140 Self::new(parsed)
141 }
142}
143
144impl TryFrom<i8> for KeyAccidentalCount {
145 type Error = KeyError;
146
147 fn try_from(value: i8) -> Result<Self, Self::Error> {
148 Self::new(value)
149 }
150}
151#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
152pub struct CircleOfFifthsPosition(i8);
153
154impl CircleOfFifthsPosition {
155 pub fn new(value: i8) -> Result<Self, KeyError> {
156 if !(-7..=7).contains(&value) {
157 return Err(KeyError::OutOfRange);
158 }
159
160 Ok(Self(value))
161 }
162
163 pub const fn value(self) -> i8 {
164 self.0
165 }
166}
167
168impl fmt::Display for CircleOfFifthsPosition {
169 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170 self.0.fmt(formatter)
171 }
172}
173
174impl FromStr for CircleOfFifthsPosition {
175 type Err = KeyError;
176
177 fn from_str(value: &str) -> Result<Self, Self::Err> {
178 let parsed = value
179 .trim()
180 .parse::<i8>()
181 .map_err(|_| KeyError::InvalidFormat)?;
182 Self::new(parsed)
183 }
184}
185
186impl TryFrom<i8> for CircleOfFifthsPosition {
187 type Error = KeyError;
188
189 fn try_from(value: i8) -> Result<Self, Self::Error> {
190 Self::new(value)
191 }
192}
193#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
194pub enum KeyMode {
195 Major,
196 Minor,
197 Modal,
198 Atonal,
199 Unknown,
200}
201
202impl KeyMode {
203 pub const ALL: &'static [Self] = &[
204 Self::Major,
205 Self::Minor,
206 Self::Modal,
207 Self::Atonal,
208 Self::Unknown,
209 ];
210
211 pub const fn as_str(self) -> &'static str {
212 match self {
213 Self::Major => "major",
214 Self::Minor => "minor",
215 Self::Modal => "modal",
216 Self::Atonal => "atonal",
217 Self::Unknown => "unknown",
218 }
219 }
220}
221
222impl fmt::Display for KeyMode {
223 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224 formatter.write_str(self.as_str())
225 }
226}
227
228impl FromStr for KeyMode {
229 type Err = KeyError;
230
231 fn from_str(value: &str) -> Result<Self, Self::Err> {
232 match normalized_label(value)?.as_str() {
233 "major" => Ok(Self::Major),
234 "minor" => Ok(Self::Minor),
235 "modal" => Ok(Self::Modal),
236 "atonal" => Ok(Self::Atonal),
237 "unknown" => Ok(Self::Unknown),
238 _ => Err(KeyError::UnknownLabel),
239 }
240 }
241}
242#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
243pub enum RelativeKeyRelation {
244 RelativeMajor,
245 RelativeMinor,
246}
247
248impl RelativeKeyRelation {
249 pub const ALL: &'static [Self] = &[Self::RelativeMajor, Self::RelativeMinor];
250
251 pub const fn as_str(self) -> &'static str {
252 match self {
253 Self::RelativeMajor => "relative-major",
254 Self::RelativeMinor => "relative-minor",
255 }
256 }
257}
258
259impl fmt::Display for RelativeKeyRelation {
260 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
261 formatter.write_str(self.as_str())
262 }
263}
264
265impl FromStr for RelativeKeyRelation {
266 type Err = KeyError;
267
268 fn from_str(value: &str) -> Result<Self, Self::Err> {
269 match normalized_label(value)?.as_str() {
270 "relative-major" => Ok(Self::RelativeMajor),
271 "relative-minor" => Ok(Self::RelativeMinor),
272 _ => Err(KeyError::UnknownLabel),
273 }
274 }
275}
276#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
277pub enum ParallelKeyRelation {
278 ParallelMajor,
279 ParallelMinor,
280}
281
282impl ParallelKeyRelation {
283 pub const ALL: &'static [Self] = &[Self::ParallelMajor, Self::ParallelMinor];
284
285 pub const fn as_str(self) -> &'static str {
286 match self {
287 Self::ParallelMajor => "parallel-major",
288 Self::ParallelMinor => "parallel-minor",
289 }
290 }
291}
292
293impl fmt::Display for ParallelKeyRelation {
294 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
295 formatter.write_str(self.as_str())
296 }
297}
298
299impl FromStr for ParallelKeyRelation {
300 type Err = KeyError;
301
302 fn from_str(value: &str) -> Result<Self, Self::Err> {
303 match normalized_label(value)?.as_str() {
304 "parallel-major" => Ok(Self::ParallelMajor),
305 "parallel-minor" => Ok(Self::ParallelMinor),
306 _ => Err(KeyError::UnknownLabel),
307 }
308 }
309}
310#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
311pub struct KeySignature {
312 accidental_count: KeyAccidentalCount,
313 mode: KeyMode,
314}
315
316impl KeySignature {
317 pub fn new(accidental_count: i8, mode: KeyMode) -> Result<Self, KeyError> {
318 Ok(Self {
319 accidental_count: KeyAccidentalCount::new(accidental_count)?,
320 mode,
321 })
322 }
323
324 pub const fn accidental_count(self) -> KeyAccidentalCount {
325 self.accidental_count
326 }
327 pub const fn mode(self) -> KeyMode {
328 self.mode
329 }
330 pub const fn is_sharp_key(self) -> bool {
331 self.accidental_count.value() > 0
332 }
333 pub const fn is_flat_key(self) -> bool {
334 self.accidental_count.value() < 0
335 }
336 pub const fn is_natural_key(self) -> bool {
337 self.accidental_count.value() == 0
338 }
339}
340#[derive(Clone, Copy, Debug, Eq, PartialEq)]
341pub enum KeyError {
342 Empty,
343 InvalidFormat,
344 OutOfRange,
345 NonFinite,
346 NonPositive,
347 UnknownLabel,
348}
349
350impl fmt::Display for KeyError {
351 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
352 match self {
353 Self::Empty => formatter.write_str("key metadata text cannot be empty"),
354 Self::InvalidFormat => formatter.write_str("key metadata has an invalid format"),
355 Self::OutOfRange => formatter.write_str("key metadata value is out of range"),
356 Self::NonFinite => formatter.write_str("key metadata value must be finite"),
357 Self::NonPositive => formatter.write_str("key metadata value must be positive"),
358 Self::UnknownLabel => formatter.write_str("unknown key metadata label"),
359 }
360 }
361}
362
363impl Error for KeyError {}
364
365#[allow(dead_code)]
366fn non_empty_text(value: impl AsRef<str>) -> Result<String, KeyError> {
367 let trimmed = value.as_ref().trim();
368 if trimmed.is_empty() {
369 Err(KeyError::Empty)
370 } else {
371 Ok(trimmed.to_string())
372 }
373}
374
375fn normalized_label(value: &str) -> Result<String, KeyError> {
376 let trimmed = value.trim();
377 if trimmed.is_empty() {
378 Err(KeyError::Empty)
379 } else {
380 Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
381 }
382}
383#[cfg(test)]
384#[allow(
385 unused_imports,
386 clippy::unnecessary_wraps,
387 clippy::assertions_on_constants
388)]
389mod tests {
390 use super::{
391 CircleOfFifthsPosition, KeyAccidentalCount, KeyError, KeyMode, KeyName, KeySignature,
392 ParallelKeyRelation, RelativeKeyRelation, Tonic,
393 };
394 use core::{fmt, str::FromStr};
395
396 fn assert_enum_family<T>(variants: &[T]) -> Result<(), KeyError>
397 where
398 T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = KeyError>,
399 {
400 for variant in variants {
401 let label = variant.to_string();
402 assert_eq!(label.parse::<T>()?, *variant);
403 assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
404 assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
405 }
406 Ok(())
407 }
408
409 #[test]
410 fn validates_text_newtypes() -> Result<(), KeyError> {
411 let value = KeyName::new(" example-value ")?;
412 assert_eq!(value.as_str(), "example-value");
413 assert_eq!(value.value(), "example-value");
414 assert_eq!(value.to_string(), "example-value");
415 assert_eq!(
416 <KeyName as TryFrom<&str>>::try_from("example-value")?,
417 value
418 );
419 let value = Tonic::new(" example-value ")?;
420 assert_eq!(value.as_str(), "example-value");
421 assert_eq!(value.value(), "example-value");
422 assert_eq!(value.to_string(), "example-value");
423 assert_eq!(<Tonic as TryFrom<&str>>::try_from("example-value")?, value);
424 Ok(())
425 }
426
427 #[test]
428 fn validates_numeric_newtypes() -> Result<(), KeyError> {
429 let value = KeyAccidentalCount::new(-7)?;
430 assert_eq!(value.value(), -7);
431 assert_eq!("-7".parse::<KeyAccidentalCount>()?, value);
432 assert_eq!(KeyAccidentalCount::new(8), Err(KeyError::OutOfRange));
433 let value = CircleOfFifthsPosition::new(-7)?;
434 assert_eq!(value.value(), -7);
435 assert_eq!("-7".parse::<CircleOfFifthsPosition>()?, value);
436 assert_eq!(CircleOfFifthsPosition::new(8), Err(KeyError::OutOfRange));
437 Ok(())
438 }
439
440 #[test]
441 fn displays_and_parses_enums() -> Result<(), KeyError> {
442 assert_enum_family(KeyMode::ALL)?;
443 assert_enum_family(RelativeKeyRelation::ALL)?;
444 assert_enum_family(ParallelKeyRelation::ALL)?;
445 Ok(())
446 }
447
448 #[test]
449 fn classifies_key_signatures() -> Result<(), KeyError> {
450 let c_major = KeySignature::new(0, KeyMode::Major)?;
451 let g_major = KeySignature::new(1, KeyMode::Major)?;
452 let f_major = KeySignature::new(-1, KeyMode::Major)?;
453 assert!(c_major.is_natural_key());
454 assert!(g_major.is_sharp_key());
455 assert!(f_major.is_flat_key());
456 assert_eq!(KeyAccidentalCount::new(8), Err(KeyError::OutOfRange));
457 Ok(())
458 }
459}