Skip to main content

use_interval/
lib.rs

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}