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 CompoundInterval, DiatonicStepDistance, IntervalDirection, IntervalError, IntervalName,
10 IntervalNumber, IntervalQuality, SemitoneDistance, SimpleInterval,
11 };
12}
13
14#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub enum IntervalQuality {
16 Perfect,
17 Major,
18 Minor,
19 Augmented,
20 Diminished,
21 DoubleAugmented,
22 DoubleDiminished,
23}
24
25impl IntervalQuality {
26 pub const fn as_str(self) -> &'static str {
27 match self {
28 Self::Perfect => "perfect",
29 Self::Major => "major",
30 Self::Minor => "minor",
31 Self::Augmented => "augmented",
32 Self::Diminished => "diminished",
33 Self::DoubleAugmented => "double-augmented",
34 Self::DoubleDiminished => "double-diminished",
35 }
36 }
37}
38
39impl fmt::Display for IntervalQuality {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 formatter.write_str(self.as_str())
42 }
43}
44
45#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub enum IntervalDirection {
47 Ascending,
48 Descending,
49 Unison,
50}
51
52impl IntervalDirection {
53 pub const fn as_str(self) -> &'static str {
54 match self {
55 Self::Ascending => "ascending",
56 Self::Descending => "descending",
57 Self::Unison => "unison",
58 }
59 }
60}
61
62impl fmt::Display for IntervalDirection {
63 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64 formatter.write_str(self.as_str())
65 }
66}
67
68#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
69pub struct IntervalNumber(u8);
70
71impl IntervalNumber {
72 pub fn new(value: u8) -> Result<Self, IntervalError> {
73 if value == 0 || value > 64 {
74 return Err(IntervalError::OutOfRange);
75 }
76
77 Ok(Self(value))
78 }
79
80 pub const fn value(self) -> u8 {
81 self.0
82 }
83}
84
85#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub struct SemitoneDistance(i16);
87
88impl SemitoneDistance {
89 pub const fn new(value: i16) -> Self {
90 Self(value)
91 }
92
93 pub const fn value(self) -> i16 {
94 self.0
95 }
96}
97
98#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
99pub struct DiatonicStepDistance(i16);
100
101impl DiatonicStepDistance {
102 pub const fn new(value: i16) -> Self {
103 Self(value)
104 }
105
106 pub const fn value(self) -> i16 {
107 self.0
108 }
109}
110
111#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
112pub struct SimpleInterval {
113 quality: IntervalQuality,
114 number: IntervalNumber,
115 semitones: SemitoneDistance,
116}
117
118impl SimpleInterval {
119 pub fn new(
120 quality: IntervalQuality,
121 number: u8,
122 semitones: i16,
123 ) -> Result<Self, IntervalError> {
124 Ok(Self {
125 quality,
126 number: IntervalNumber::new(number)?,
127 semitones: SemitoneDistance::new(semitones),
128 })
129 }
130
131 pub fn perfect_unison() -> Self {
132 Self::new(IntervalQuality::Perfect, 1, 0).expect("valid interval")
133 }
134 pub fn minor_second() -> Self {
135 Self::new(IntervalQuality::Minor, 2, 1).expect("valid interval")
136 }
137 pub fn major_second() -> Self {
138 Self::new(IntervalQuality::Major, 2, 2).expect("valid interval")
139 }
140 pub fn minor_third() -> Self {
141 Self::new(IntervalQuality::Minor, 3, 3).expect("valid interval")
142 }
143 pub fn major_third() -> Self {
144 Self::new(IntervalQuality::Major, 3, 4).expect("valid interval")
145 }
146 pub fn perfect_fourth() -> Self {
147 Self::new(IntervalQuality::Perfect, 4, 5).expect("valid interval")
148 }
149 pub fn tritone() -> Self {
150 Self::new(IntervalQuality::Augmented, 4, 6).expect("valid interval")
151 }
152 pub fn perfect_fifth() -> Self {
153 Self::new(IntervalQuality::Perfect, 5, 7).expect("valid interval")
154 }
155 pub fn minor_sixth() -> Self {
156 Self::new(IntervalQuality::Minor, 6, 8).expect("valid interval")
157 }
158 pub fn major_sixth() -> Self {
159 Self::new(IntervalQuality::Major, 6, 9).expect("valid interval")
160 }
161 pub fn minor_seventh() -> Self {
162 Self::new(IntervalQuality::Minor, 7, 10).expect("valid interval")
163 }
164 pub fn major_seventh() -> Self {
165 Self::new(IntervalQuality::Major, 7, 11).expect("valid interval")
166 }
167 pub fn octave() -> Self {
168 Self::new(IntervalQuality::Perfect, 8, 12).expect("valid interval")
169 }
170
171 pub const fn quality(self) -> IntervalQuality {
172 self.quality
173 }
174 pub const fn number(self) -> IntervalNumber {
175 self.number
176 }
177 pub const fn semitones(self) -> SemitoneDistance {
178 self.semitones
179 }
180}
181
182#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
183pub struct CompoundInterval {
184 simple: SimpleInterval,
185 octaves: u8,
186}
187
188impl CompoundInterval {
189 pub const fn new(simple: SimpleInterval, octaves: u8) -> Self {
190 Self { simple, octaves }
191 }
192
193 pub const fn simple(self) -> SimpleInterval {
194 self.simple
195 }
196 pub const fn octaves(self) -> u8 {
197 self.octaves
198 }
199}
200
201#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
202pub struct IntervalName(String);
203
204impl IntervalName {
205 pub fn new(value: impl AsRef<str>) -> Result<Self, IntervalError> {
206 let trimmed = value.as_ref().trim();
207 if trimmed.is_empty() {
208 return Err(IntervalError::Empty);
209 }
210 Ok(Self(trimmed.to_string()))
211 }
212
213 pub fn as_str(&self) -> &str {
214 &self.0
215 }
216 pub fn value(&self) -> &str {
217 self.as_str()
218 }
219}
220
221impl fmt::Display for IntervalName {
222 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
223 formatter.write_str(self.as_str())
224 }
225}
226
227impl FromStr for IntervalName {
228 type Err = IntervalError;
229
230 fn from_str(value: &str) -> Result<Self, Self::Err> {
231 Self::new(value)
232 }
233}
234
235impl TryFrom<&str> for IntervalName {
236 type Error = IntervalError;
237
238 fn try_from(value: &str) -> Result<Self, Self::Error> {
239 Self::new(value)
240 }
241}
242
243#[derive(Clone, Copy, Debug, Eq, PartialEq)]
244pub enum IntervalError {
245 Empty,
246 OutOfRange,
247}
248
249impl fmt::Display for IntervalError {
250 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
251 match self {
252 Self::Empty => formatter.write_str("interval metadata text cannot be empty"),
253 Self::OutOfRange => formatter.write_str("interval number is out of range"),
254 }
255 }
256}
257
258impl Error for IntervalError {}
259
260#[cfg(test)]
261#[allow(
262 unused_imports,
263 clippy::unnecessary_wraps,
264 clippy::assertions_on_constants
265)]
266mod tests {
267 use super::{IntervalError, IntervalName, IntervalNumber, IntervalQuality, SimpleInterval};
268
269 #[test]
270 fn validates_interval_numbers_and_names() -> Result<(), IntervalError> {
271 assert_eq!(IntervalNumber::new(1)?.value(), 1);
272 assert_eq!(IntervalNumber::new(0), Err(IntervalError::OutOfRange));
273 assert_eq!(IntervalName::new(" octave ")?.as_str(), "octave");
274 Ok(())
275 }
276
277 #[test]
278 fn provides_common_intervals() {
279 assert_eq!(SimpleInterval::perfect_unison().semitones().value(), 0);
280 assert_eq!(SimpleInterval::minor_second().semitones().value(), 1);
281 assert_eq!(
282 SimpleInterval::major_third().quality(),
283 IntervalQuality::Major
284 );
285 assert_eq!(SimpleInterval::perfect_fifth().semitones().value(), 7);
286 assert_eq!(SimpleInterval::octave().number().value(), 8);
287 }
288}