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 Accidental, EnharmonicSpelling, NaturalNote, NoteClass, NoteError, NoteLetter, NoteName,
10 NoteSpelling, Octave, ScientificPitchNotation,
11 };
12}
13
14#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub enum NoteLetter {
16 A,
17 B,
18 C,
19 D,
20 E,
21 F,
22 G,
23}
24
25impl NoteLetter {
26 pub const ALL: &'static [Self] = &[
27 Self::A,
28 Self::B,
29 Self::C,
30 Self::D,
31 Self::E,
32 Self::F,
33 Self::G,
34 ];
35
36 pub const fn as_str(self) -> &'static str {
37 match self {
38 Self::A => "A",
39 Self::B => "B",
40 Self::C => "C",
41 Self::D => "D",
42 Self::E => "E",
43 Self::F => "F",
44 Self::G => "G",
45 }
46 }
47}
48
49impl fmt::Display for NoteLetter {
50 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51 formatter.write_str(self.as_str())
52 }
53}
54
55impl FromStr for NoteLetter {
56 type Err = NoteError;
57
58 fn from_str(value: &str) -> Result<Self, Self::Err> {
59 match value.trim().to_ascii_uppercase().as_str() {
60 "A" => Ok(Self::A),
61 "B" => Ok(Self::B),
62 "C" => Ok(Self::C),
63 "D" => Ok(Self::D),
64 "E" => Ok(Self::E),
65 "F" => Ok(Self::F),
66 "G" => Ok(Self::G),
67 _ => Err(NoteError::InvalidFormat),
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
73pub enum Accidental {
74 Natural,
75 Sharp,
76 Flat,
77 DoubleSharp,
78 DoubleFlat,
79}
80
81impl Accidental {
82 pub const ALL: &'static [Self] = &[
83 Self::Natural,
84 Self::Sharp,
85 Self::Flat,
86 Self::DoubleSharp,
87 Self::DoubleFlat,
88 ];
89
90 pub const fn as_str(self) -> &'static str {
91 match self {
92 Self::Natural => "",
93 Self::Sharp => "#",
94 Self::Flat => "b",
95 Self::DoubleSharp => "##",
96 Self::DoubleFlat => "bb",
97 }
98 }
99}
100
101impl fmt::Display for Accidental {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 formatter.write_str(self.as_str())
104 }
105}
106
107#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub struct Octave(i8);
109
110impl Octave {
111 pub const MIN: i8 = -1;
112 pub const MAX: i8 = 10;
113
114 pub fn new(value: i8) -> Result<Self, NoteError> {
115 if !(Self::MIN..=Self::MAX).contains(&value) {
116 return Err(NoteError::OutOfRange);
117 }
118
119 Ok(Self(value))
120 }
121
122 pub const fn value(self) -> i8 {
123 self.0
124 }
125}
126
127impl fmt::Display for Octave {
128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129 self.0.fmt(formatter)
130 }
131}
132
133impl FromStr for Octave {
134 type Err = NoteError;
135
136 fn from_str(value: &str) -> Result<Self, Self::Err> {
137 let parsed = value
138 .trim()
139 .parse::<i8>()
140 .map_err(|_| NoteError::InvalidFormat)?;
141 Self::new(parsed)
142 }
143}
144
145impl TryFrom<i8> for Octave {
146 type Error = NoteError;
147
148 fn try_from(value: i8) -> Result<Self, Self::Error> {
149 Self::new(value)
150 }
151}
152
153#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
154pub struct NoteName {
155 value: String,
156 letter: NoteLetter,
157 accidental: Accidental,
158 octave: Option<Octave>,
159}
160
161pub type NoteSpelling = NoteName;
162pub type ScientificPitchNotation = NoteName;
163pub type NaturalNote = NoteLetter;
164
165#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
166pub struct EnharmonicSpelling {
167 preferred: NoteName,
168 alternate: NoteName,
169}
170
171impl EnharmonicSpelling {
172 pub const fn new(preferred: NoteName, alternate: NoteName) -> Self {
173 Self {
174 preferred,
175 alternate,
176 }
177 }
178
179 pub const fn preferred(&self) -> &NoteName {
180 &self.preferred
181 }
182
183 pub const fn alternate(&self) -> &NoteName {
184 &self.alternate
185 }
186}
187
188impl NoteName {
189 pub fn new(value: impl AsRef<str>) -> Result<Self, NoteError> {
190 value.as_ref().parse()
191 }
192
193 pub fn as_str(&self) -> &str {
194 &self.value
195 }
196
197 pub fn value(&self) -> &str {
198 self.as_str()
199 }
200
201 pub const fn letter(&self) -> NoteLetter {
202 self.letter
203 }
204
205 pub const fn accidental(&self) -> Accidental {
206 self.accidental
207 }
208
209 pub const fn octave(&self) -> Option<Octave> {
210 self.octave
211 }
212
213 pub const fn note_class(&self) -> NoteClass {
214 match self.accidental {
215 Accidental::Natural => NoteClass::Natural,
216 Accidental::Sharp | Accidental::Flat => NoteClass::Chromatic,
217 Accidental::DoubleSharp | Accidental::DoubleFlat => NoteClass::Enharmonic,
218 }
219 }
220}
221
222impl AsRef<str> for NoteName {
223 fn as_ref(&self) -> &str {
224 self.as_str()
225 }
226}
227
228impl fmt::Display for NoteName {
229 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
230 formatter.write_str(self.as_str())
231 }
232}
233
234impl FromStr for NoteName {
235 type Err = NoteError;
236
237 fn from_str(value: &str) -> Result<Self, Self::Err> {
238 let trimmed = value.trim();
239 if trimmed.is_empty() {
240 return Err(NoteError::Empty);
241 }
242
243 let mut chars = trimmed.chars();
244 let letter_char = chars.next().ok_or(NoteError::Empty)?;
245 let letter = NoteLetter::from_str(&letter_char.to_string())?;
246 let rest: String = chars.collect();
247 let (accidental_text, octave_text) = split_accidental_and_octave(&rest)?;
248 let accidental = parse_accidental(accidental_text)?;
249 let octave = if octave_text.is_empty() {
250 None
251 } else {
252 Some(octave_text.parse::<Octave>()?)
253 };
254
255 Ok(Self {
256 value: format!(
257 "{}{}{}",
258 letter,
259 accidental,
260 octave.map_or_else(String::new, |value| value.to_string())
261 ),
262 letter,
263 accidental,
264 octave,
265 })
266 }
267}
268
269impl TryFrom<&str> for NoteName {
270 type Error = NoteError;
271
272 fn try_from(value: &str) -> Result<Self, Self::Error> {
273 value.parse()
274 }
275}
276
277#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
278pub enum NoteClass {
279 Natural,
280 Chromatic,
281 Enharmonic,
282}
283
284fn split_accidental_and_octave(value: &str) -> Result<(&str, &str), NoteError> {
285 let octave_start = value
286 .char_indices()
287 .find_map(|(index, character)| {
288 (character == '-' || character.is_ascii_digit()).then_some(index)
289 })
290 .unwrap_or(value.len());
291 let accidental = &value[..octave_start];
292 let octave = &value[octave_start..];
293
294 if !octave.is_empty() && octave == "-" {
295 return Err(NoteError::InvalidFormat);
296 }
297
298 Ok((accidental, octave))
299}
300
301fn parse_accidental(value: &str) -> Result<Accidental, NoteError> {
302 match value {
303 "" => Ok(Accidental::Natural),
304 "#" => Ok(Accidental::Sharp),
305 "b" => Ok(Accidental::Flat),
306 "##" | "x" => Ok(Accidental::DoubleSharp),
307 "bb" => Ok(Accidental::DoubleFlat),
308 _ => Err(NoteError::InvalidFormat),
309 }
310}
311
312#[derive(Clone, Copy, Debug, Eq, PartialEq)]
313pub enum NoteError {
314 Empty,
315 InvalidFormat,
316 OutOfRange,
317}
318
319impl fmt::Display for NoteError {
320 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
321 match self {
322 Self::Empty => formatter.write_str("note spelling cannot be empty"),
323 Self::InvalidFormat => formatter.write_str("note spelling has an invalid format"),
324 Self::OutOfRange => formatter.write_str("octave is out of range"),
325 }
326 }
327}
328
329impl Error for NoteError {}
330
331#[cfg(test)]
332#[allow(
333 unused_imports,
334 clippy::unnecessary_wraps,
335 clippy::assertions_on_constants
336)]
337mod tests {
338 use super::{Accidental, NoteClass, NoteError, NoteLetter, NoteName, Octave};
339
340 #[test]
341 fn parses_common_note_spellings() -> Result<(), NoteError> {
342 for spelling in ["C", "C#", "Db", "F##", "Gbb", "A4", "C#4"] {
343 assert_eq!(spelling.parse::<NoteName>()?.to_string(), spelling);
344 }
345
346 let note = NoteName::new(" C#4 ")?;
347 assert_eq!(note.letter(), NoteLetter::C);
348 assert_eq!(note.accidental(), Accidental::Sharp);
349 assert_eq!(note.octave(), Some(Octave::new(4)?));
350 assert_eq!(note.note_class(), NoteClass::Chromatic);
351 Ok(())
352 }
353
354 #[test]
355 fn validates_octave_range() {
356 assert_eq!(Octave::new(-1).map(Octave::value), Ok(-1));
357 assert_eq!(Octave::new(10).map(Octave::value), Ok(10));
358 assert_eq!(Octave::new(-2), Err(NoteError::OutOfRange));
359 assert_eq!(Octave::new(11), Err(NoteError::OutOfRange));
360 }
361
362 #[test]
363 fn rejects_invalid_spellings() {
364 assert_eq!(NoteName::new(""), Err(NoteError::Empty));
365 assert_eq!(NoteName::new("H"), Err(NoteError::InvalidFormat));
366 assert_eq!(NoteName::new("C###"), Err(NoteError::InvalidFormat));
367 assert_eq!(NoteName::new("C11"), Err(NoteError::OutOfRange));
368 }
369}