use-music-interval 0.0.1

Musical interval metadata primitives for RustUse.
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

use core::{fmt, str::FromStr};
use std::error::Error;

pub mod prelude {
    pub use crate::{
        CompoundInterval, DiatonicStepDistance, IntervalDirection, IntervalError, IntervalName,
        IntervalNumber, IntervalQuality, SemitoneDistance, SimpleInterval,
    };
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum IntervalQuality {
    Perfect,
    Major,
    Minor,
    Augmented,
    Diminished,
    DoubleAugmented,
    DoubleDiminished,
}

impl IntervalQuality {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Perfect => "perfect",
            Self::Major => "major",
            Self::Minor => "minor",
            Self::Augmented => "augmented",
            Self::Diminished => "diminished",
            Self::DoubleAugmented => "double-augmented",
            Self::DoubleDiminished => "double-diminished",
        }
    }
}

impl fmt::Display for IntervalQuality {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum IntervalDirection {
    Ascending,
    Descending,
    Unison,
}

impl IntervalDirection {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Ascending => "ascending",
            Self::Descending => "descending",
            Self::Unison => "unison",
        }
    }
}

impl fmt::Display for IntervalDirection {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct IntervalNumber(u8);

impl IntervalNumber {
    pub fn new(value: u8) -> Result<Self, IntervalError> {
        if value == 0 || value > 64 {
            return Err(IntervalError::OutOfRange);
        }

        Ok(Self(value))
    }

    pub const fn value(self) -> u8 {
        self.0
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SemitoneDistance(i16);

impl SemitoneDistance {
    pub const fn new(value: i16) -> Self {
        Self(value)
    }

    pub const fn value(self) -> i16 {
        self.0
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DiatonicStepDistance(i16);

impl DiatonicStepDistance {
    pub const fn new(value: i16) -> Self {
        Self(value)
    }

    pub const fn value(self) -> i16 {
        self.0
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SimpleInterval {
    quality: IntervalQuality,
    number: IntervalNumber,
    semitones: SemitoneDistance,
}

impl SimpleInterval {
    pub fn new(
        quality: IntervalQuality,
        number: u8,
        semitones: i16,
    ) -> Result<Self, IntervalError> {
        Ok(Self {
            quality,
            number: IntervalNumber::new(number)?,
            semitones: SemitoneDistance::new(semitones),
        })
    }

    pub fn perfect_unison() -> Self {
        Self::new(IntervalQuality::Perfect, 1, 0).expect("valid interval")
    }
    pub fn minor_second() -> Self {
        Self::new(IntervalQuality::Minor, 2, 1).expect("valid interval")
    }
    pub fn major_second() -> Self {
        Self::new(IntervalQuality::Major, 2, 2).expect("valid interval")
    }
    pub fn minor_third() -> Self {
        Self::new(IntervalQuality::Minor, 3, 3).expect("valid interval")
    }
    pub fn major_third() -> Self {
        Self::new(IntervalQuality::Major, 3, 4).expect("valid interval")
    }
    pub fn perfect_fourth() -> Self {
        Self::new(IntervalQuality::Perfect, 4, 5).expect("valid interval")
    }
    pub fn tritone() -> Self {
        Self::new(IntervalQuality::Augmented, 4, 6).expect("valid interval")
    }
    pub fn perfect_fifth() -> Self {
        Self::new(IntervalQuality::Perfect, 5, 7).expect("valid interval")
    }
    pub fn minor_sixth() -> Self {
        Self::new(IntervalQuality::Minor, 6, 8).expect("valid interval")
    }
    pub fn major_sixth() -> Self {
        Self::new(IntervalQuality::Major, 6, 9).expect("valid interval")
    }
    pub fn minor_seventh() -> Self {
        Self::new(IntervalQuality::Minor, 7, 10).expect("valid interval")
    }
    pub fn major_seventh() -> Self {
        Self::new(IntervalQuality::Major, 7, 11).expect("valid interval")
    }
    pub fn octave() -> Self {
        Self::new(IntervalQuality::Perfect, 8, 12).expect("valid interval")
    }

    pub const fn quality(self) -> IntervalQuality {
        self.quality
    }
    pub const fn number(self) -> IntervalNumber {
        self.number
    }
    pub const fn semitones(self) -> SemitoneDistance {
        self.semitones
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct CompoundInterval {
    simple: SimpleInterval,
    octaves: u8,
}

impl CompoundInterval {
    pub const fn new(simple: SimpleInterval, octaves: u8) -> Self {
        Self { simple, octaves }
    }

    pub const fn simple(self) -> SimpleInterval {
        self.simple
    }
    pub const fn octaves(self) -> u8 {
        self.octaves
    }
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct IntervalName(String);

impl IntervalName {
    pub fn new(value: impl AsRef<str>) -> Result<Self, IntervalError> {
        let trimmed = value.as_ref().trim();
        if trimmed.is_empty() {
            return Err(IntervalError::Empty);
        }
        Ok(Self(trimmed.to_string()))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
    pub fn value(&self) -> &str {
        self.as_str()
    }
}

impl fmt::Display for IntervalName {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for IntervalName {
    type Err = IntervalError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        Self::new(value)
    }
}

impl TryFrom<&str> for IntervalName {
    type Error = IntervalError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IntervalError {
    Empty,
    OutOfRange,
}

impl fmt::Display for IntervalError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("interval metadata text cannot be empty"),
            Self::OutOfRange => formatter.write_str("interval number is out of range"),
        }
    }
}

impl Error for IntervalError {}

#[cfg(test)]
#[allow(
    unused_imports,
    clippy::unnecessary_wraps,
    clippy::assertions_on_constants
)]
mod tests {
    use super::{IntervalError, IntervalName, IntervalNumber, IntervalQuality, SimpleInterval};

    #[test]
    fn validates_interval_numbers_and_names() -> Result<(), IntervalError> {
        assert_eq!(IntervalNumber::new(1)?.value(), 1);
        assert_eq!(IntervalNumber::new(0), Err(IntervalError::OutOfRange));
        assert_eq!(IntervalName::new(" octave ")?.as_str(), "octave");
        Ok(())
    }

    #[test]
    fn provides_common_intervals() {
        assert_eq!(SimpleInterval::perfect_unison().semitones().value(), 0);
        assert_eq!(SimpleInterval::minor_second().semitones().value(), 1);
        assert_eq!(
            SimpleInterval::major_third().quality(),
            IntervalQuality::Major
        );
        assert_eq!(SimpleInterval::perfect_fifth().semitones().value(), 7);
        assert_eq!(SimpleInterval::octave().number().value(), 8);
    }
}