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 ChordAlteration, ChordError, ChordExtension, ChordInversion, ChordName, ChordQuality,
10 ChordSymbol, ChordToneRole, ChordVoicingKind, SeventhChordKind, TriadKind,
11 };
12}
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct ChordName(String);
15
16impl ChordName {
17 pub fn new(value: impl AsRef<str>) -> Result<Self, ChordError> {
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 ChordName {
35 fn as_ref(&self) -> &str {
36 self.as_str()
37 }
38}
39
40impl fmt::Display for ChordName {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 formatter.write_str(self.as_str())
43 }
44}
45
46impl FromStr for ChordName {
47 type Err = ChordError;
48
49 fn from_str(value: &str) -> Result<Self, Self::Err> {
50 Self::new(value)
51 }
52}
53
54impl TryFrom<&str> for ChordName {
55 type Error = ChordError;
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 ChordSymbol(String);
63
64impl ChordSymbol {
65 pub fn new(value: impl AsRef<str>) -> Result<Self, ChordError> {
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 ChordSymbol {
83 fn as_ref(&self) -> &str {
84 self.as_str()
85 }
86}
87
88impl fmt::Display for ChordSymbol {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 formatter.write_str(self.as_str())
91 }
92}
93
94impl FromStr for ChordSymbol {
95 type Err = ChordError;
96
97 fn from_str(value: &str) -> Result<Self, Self::Err> {
98 Self::new(value)
99 }
100}
101
102impl TryFrom<&str> for ChordSymbol {
103 type Error = ChordError;
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 enum ChordQuality {
111 Major,
112 Minor,
113 Diminished,
114 Augmented,
115 Suspended2,
116 Suspended4,
117 Dominant,
118 HalfDiminished,
119 MajorMinor,
120 Power,
121 Custom,
122}
123
124impl ChordQuality {
125 pub const ALL: &'static [Self] = &[
126 Self::Major,
127 Self::Minor,
128 Self::Diminished,
129 Self::Augmented,
130 Self::Suspended2,
131 Self::Suspended4,
132 Self::Dominant,
133 Self::HalfDiminished,
134 Self::MajorMinor,
135 Self::Power,
136 Self::Custom,
137 ];
138
139 pub const fn as_str(self) -> &'static str {
140 match self {
141 Self::Major => "major",
142 Self::Minor => "minor",
143 Self::Diminished => "diminished",
144 Self::Augmented => "augmented",
145 Self::Suspended2 => "suspended-2",
146 Self::Suspended4 => "suspended-4",
147 Self::Dominant => "dominant",
148 Self::HalfDiminished => "half-diminished",
149 Self::MajorMinor => "major-minor",
150 Self::Power => "power",
151 Self::Custom => "custom",
152 }
153 }
154}
155
156impl fmt::Display for ChordQuality {
157 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158 formatter.write_str(self.as_str())
159 }
160}
161
162impl FromStr for ChordQuality {
163 type Err = ChordError;
164
165 fn from_str(value: &str) -> Result<Self, Self::Err> {
166 match normalized_label(value)?.as_str() {
167 "major" => Ok(Self::Major),
168 "minor" => Ok(Self::Minor),
169 "diminished" => Ok(Self::Diminished),
170 "augmented" => Ok(Self::Augmented),
171 "suspended-2" => Ok(Self::Suspended2),
172 "suspended-4" => Ok(Self::Suspended4),
173 "dominant" => Ok(Self::Dominant),
174 "half-diminished" => Ok(Self::HalfDiminished),
175 "major-minor" => Ok(Self::MajorMinor),
176 "power" => Ok(Self::Power),
177 "custom" => Ok(Self::Custom),
178 _ => Err(ChordError::UnknownLabel),
179 }
180 }
181}
182#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
183pub enum ChordToneRole {
184 Root,
185 Third,
186 Fifth,
187 Seventh,
188 Ninth,
189 Eleventh,
190 Thirteenth,
191 AddedTone,
192 AlteredTone,
193}
194
195impl ChordToneRole {
196 pub const ALL: &'static [Self] = &[
197 Self::Root,
198 Self::Third,
199 Self::Fifth,
200 Self::Seventh,
201 Self::Ninth,
202 Self::Eleventh,
203 Self::Thirteenth,
204 Self::AddedTone,
205 Self::AlteredTone,
206 ];
207
208 pub const fn as_str(self) -> &'static str {
209 match self {
210 Self::Root => "root",
211 Self::Third => "third",
212 Self::Fifth => "fifth",
213 Self::Seventh => "seventh",
214 Self::Ninth => "ninth",
215 Self::Eleventh => "eleventh",
216 Self::Thirteenth => "thirteenth",
217 Self::AddedTone => "added-tone",
218 Self::AlteredTone => "altered-tone",
219 }
220 }
221}
222
223impl fmt::Display for ChordToneRole {
224 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
225 formatter.write_str(self.as_str())
226 }
227}
228
229impl FromStr for ChordToneRole {
230 type Err = ChordError;
231
232 fn from_str(value: &str) -> Result<Self, Self::Err> {
233 match normalized_label(value)?.as_str() {
234 "root" => Ok(Self::Root),
235 "third" => Ok(Self::Third),
236 "fifth" => Ok(Self::Fifth),
237 "seventh" => Ok(Self::Seventh),
238 "ninth" => Ok(Self::Ninth),
239 "eleventh" => Ok(Self::Eleventh),
240 "thirteenth" => Ok(Self::Thirteenth),
241 "added-tone" => Ok(Self::AddedTone),
242 "altered-tone" => Ok(Self::AlteredTone),
243 _ => Err(ChordError::UnknownLabel),
244 }
245 }
246}
247#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
248pub enum ChordVoicingKind {
249 Closed,
250 Open,
251 Drop2,
252 Drop3,
253 Quartal,
254 Cluster,
255 Shell,
256 Custom,
257}
258
259impl ChordVoicingKind {
260 pub const ALL: &'static [Self] = &[
261 Self::Closed,
262 Self::Open,
263 Self::Drop2,
264 Self::Drop3,
265 Self::Quartal,
266 Self::Cluster,
267 Self::Shell,
268 Self::Custom,
269 ];
270
271 pub const fn as_str(self) -> &'static str {
272 match self {
273 Self::Closed => "closed",
274 Self::Open => "open",
275 Self::Drop2 => "drop-2",
276 Self::Drop3 => "drop-3",
277 Self::Quartal => "quartal",
278 Self::Cluster => "cluster",
279 Self::Shell => "shell",
280 Self::Custom => "custom",
281 }
282 }
283}
284
285impl fmt::Display for ChordVoicingKind {
286 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
287 formatter.write_str(self.as_str())
288 }
289}
290
291impl FromStr for ChordVoicingKind {
292 type Err = ChordError;
293
294 fn from_str(value: &str) -> Result<Self, Self::Err> {
295 match normalized_label(value)?.as_str() {
296 "closed" => Ok(Self::Closed),
297 "open" => Ok(Self::Open),
298 "drop-2" => Ok(Self::Drop2),
299 "drop-3" => Ok(Self::Drop3),
300 "quartal" => Ok(Self::Quartal),
301 "cluster" => Ok(Self::Cluster),
302 "shell" => Ok(Self::Shell),
303 "custom" => Ok(Self::Custom),
304 _ => Err(ChordError::UnknownLabel),
305 }
306 }
307}
308#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
309pub enum TriadKind {
310 Major,
311 Minor,
312 Diminished,
313 Augmented,
314 Suspended2,
315 Suspended4,
316 Power,
317}
318
319impl TriadKind {
320 pub const ALL: &'static [Self] = &[
321 Self::Major,
322 Self::Minor,
323 Self::Diminished,
324 Self::Augmented,
325 Self::Suspended2,
326 Self::Suspended4,
327 Self::Power,
328 ];
329
330 pub const fn as_str(self) -> &'static str {
331 match self {
332 Self::Major => "major",
333 Self::Minor => "minor",
334 Self::Diminished => "diminished",
335 Self::Augmented => "augmented",
336 Self::Suspended2 => "suspended-2",
337 Self::Suspended4 => "suspended-4",
338 Self::Power => "power",
339 }
340 }
341}
342
343impl fmt::Display for TriadKind {
344 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
345 formatter.write_str(self.as_str())
346 }
347}
348
349impl FromStr for TriadKind {
350 type Err = ChordError;
351
352 fn from_str(value: &str) -> Result<Self, Self::Err> {
353 match normalized_label(value)?.as_str() {
354 "major" => Ok(Self::Major),
355 "minor" => Ok(Self::Minor),
356 "diminished" => Ok(Self::Diminished),
357 "augmented" => Ok(Self::Augmented),
358 "suspended-2" => Ok(Self::Suspended2),
359 "suspended-4" => Ok(Self::Suspended4),
360 "power" => Ok(Self::Power),
361 _ => Err(ChordError::UnknownLabel),
362 }
363 }
364}
365#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
366pub enum SeventhChordKind {
367 MajorSeventh,
368 MinorSeventh,
369 Dominant,
370 HalfDiminished,
371 Diminished,
372 MinorMajor,
373}
374
375impl SeventhChordKind {
376 pub const ALL: &'static [Self] = &[
377 Self::MajorSeventh,
378 Self::MinorSeventh,
379 Self::Dominant,
380 Self::HalfDiminished,
381 Self::Diminished,
382 Self::MinorMajor,
383 ];
384
385 pub const fn as_str(self) -> &'static str {
386 match self {
387 Self::MajorSeventh => "major-seventh",
388 Self::MinorSeventh => "minor-seventh",
389 Self::Dominant => "dominant",
390 Self::HalfDiminished => "half-diminished",
391 Self::Diminished => "diminished",
392 Self::MinorMajor => "minor-major",
393 }
394 }
395}
396
397impl fmt::Display for SeventhChordKind {
398 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
399 formatter.write_str(self.as_str())
400 }
401}
402
403impl FromStr for SeventhChordKind {
404 type Err = ChordError;
405
406 fn from_str(value: &str) -> Result<Self, Self::Err> {
407 match normalized_label(value)?.as_str() {
408 "major-seventh" => Ok(Self::MajorSeventh),
409 "minor-seventh" => Ok(Self::MinorSeventh),
410 "dominant" => Ok(Self::Dominant),
411 "half-diminished" => Ok(Self::HalfDiminished),
412 "diminished" => Ok(Self::Diminished),
413 "minor-major" => Ok(Self::MinorMajor),
414 _ => Err(ChordError::UnknownLabel),
415 }
416 }
417}
418pub type ChordExtension = u8;
419pub type ChordAlteration = String;
420
421#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
422pub struct ChordInversion(u8);
423
424impl ChordInversion {
425 pub fn new(value: u8) -> Result<Self, ChordError> {
426 if value > 7 {
427 return Err(ChordError::OutOfRange);
428 }
429 Ok(Self(value))
430 }
431
432 pub const fn value(self) -> u8 {
433 self.0
434 }
435}
436
437impl TriadKind {
438 pub const MAJOR: Self = Self::Major;
439 pub const MINOR: Self = Self::Minor;
440 pub const DIMINISHED: Self = Self::Diminished;
441 pub const AUGMENTED: Self = Self::Augmented;
442}
443
444impl SeventhChordKind {
445 pub const DOMINANT: Self = Self::Dominant;
446 pub const MAJOR_SEVENTH: Self = Self::MajorSeventh;
447 pub const MINOR_SEVENTH: Self = Self::MinorSeventh;
448}
449#[derive(Clone, Copy, Debug, Eq, PartialEq)]
450pub enum ChordError {
451 Empty,
452 InvalidFormat,
453 OutOfRange,
454 NonFinite,
455 NonPositive,
456 UnknownLabel,
457}
458
459impl fmt::Display for ChordError {
460 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
461 match self {
462 Self::Empty => formatter.write_str("chord metadata text cannot be empty"),
463 Self::InvalidFormat => formatter.write_str("chord metadata has an invalid format"),
464 Self::OutOfRange => formatter.write_str("chord metadata value is out of range"),
465 Self::NonFinite => formatter.write_str("chord metadata value must be finite"),
466 Self::NonPositive => formatter.write_str("chord metadata value must be positive"),
467 Self::UnknownLabel => formatter.write_str("unknown chord metadata label"),
468 }
469 }
470}
471
472impl Error for ChordError {}
473
474#[allow(dead_code)]
475fn non_empty_text(value: impl AsRef<str>) -> Result<String, ChordError> {
476 let trimmed = value.as_ref().trim();
477 if trimmed.is_empty() {
478 Err(ChordError::Empty)
479 } else {
480 Ok(trimmed.to_string())
481 }
482}
483
484fn normalized_label(value: &str) -> Result<String, ChordError> {
485 let trimmed = value.trim();
486 if trimmed.is_empty() {
487 Err(ChordError::Empty)
488 } else {
489 Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
490 }
491}
492#[cfg(test)]
493#[allow(
494 unused_imports,
495 clippy::unnecessary_wraps,
496 clippy::assertions_on_constants
497)]
498mod tests {
499 use super::{
500 ChordAlteration, ChordError, ChordExtension, ChordInversion, ChordName, ChordQuality,
501 ChordSymbol, ChordToneRole, ChordVoicingKind, SeventhChordKind, TriadKind,
502 };
503 use core::{fmt, str::FromStr};
504
505 fn assert_enum_family<T>(variants: &[T]) -> Result<(), ChordError>
506 where
507 T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = ChordError>,
508 {
509 for variant in variants {
510 let label = variant.to_string();
511 assert_eq!(label.parse::<T>()?, *variant);
512 assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
513 assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
514 }
515 Ok(())
516 }
517
518 #[test]
519 fn validates_text_newtypes() -> Result<(), ChordError> {
520 let value = ChordName::new(" example-value ")?;
521 assert_eq!(value.as_str(), "example-value");
522 assert_eq!(value.value(), "example-value");
523 assert_eq!(value.to_string(), "example-value");
524 assert_eq!(
525 <ChordName as TryFrom<&str>>::try_from("example-value")?,
526 value
527 );
528 let value = ChordSymbol::new(" example-value ")?;
529 assert_eq!(value.as_str(), "example-value");
530 assert_eq!(value.value(), "example-value");
531 assert_eq!(value.to_string(), "example-value");
532 assert_eq!(
533 <ChordSymbol as TryFrom<&str>>::try_from("example-value")?,
534 value
535 );
536 Ok(())
537 }
538
539 #[test]
540 fn validates_numeric_newtypes() -> Result<(), ChordError> {
541 assert!(true);
542 Ok(())
543 }
544
545 #[test]
546 fn displays_and_parses_enums() -> Result<(), ChordError> {
547 assert_enum_family(ChordQuality::ALL)?;
548 assert_enum_family(ChordToneRole::ALL)?;
549 assert_enum_family(ChordVoicingKind::ALL)?;
550 assert_enum_family(TriadKind::ALL)?;
551 assert_enum_family(SeventhChordKind::ALL)?;
552 Ok(())
553 }
554
555 #[test]
556 fn validates_chord_metadata() -> Result<(), ChordError> {
557 let symbol = ChordSymbol::new(" Cmaj7 ")?;
558 assert_eq!(symbol.as_str(), "Cmaj7");
559 assert_eq!(ChordSymbol::new(" "), Err(ChordError::Empty));
560 assert_eq!(ChordInversion::new(1)?.value(), 1);
561 assert_eq!(TriadKind::MAJOR, TriadKind::Major);
562 assert_eq!(SeventhChordKind::DOMINANT, SeventhChordKind::Dominant);
563 Ok(())
564 }
565}