euphony-units 0.1.1

Core types and traits for music composition
Documentation
use crate::pitch::interval::Interval;
use core::{cmp::Ordering, fmt};

#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub struct ModeIntervals {
    pub tones: &'static [Interval],
    pub steps: &'static [Interval],
    pub intervals: &'static [Interval],
}

impl fmt::Debug for ModeIntervals {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "ModeIntervals({:?})", self.steps)
    }
}

impl fmt::Display for ModeIntervals {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_list()
            .entries(self.intervals.iter().map(|step| step.as_ratio()))
            .finish()
    }
}

#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum RoundingStrategy {
    Down,
    Up,
    NearestDown,
    NearestUp,
    TowardsZero,
    AwayFromZero,
    Reject,
    Pass,
}

impl Default for RoundingStrategy {
    fn default() -> Self {
        RoundingStrategy::NearestDown
    }
}

impl ModeIntervals {
    pub const fn len(&self) -> usize {
        self.intervals.len()
    }

    pub const fn is_empty(&self) -> bool {
        self.intervals.is_empty()
    }

    pub fn collapse(&self, interval: Interval, rounding_strategy: RoundingStrategy) -> Interval {
        self.checked_collapse(interval, rounding_strategy)
            .expect("Interval could not be collapsed")
    }

    pub fn checked_collapse(
        &self,
        interval: Interval,
        rounding_strategy: RoundingStrategy,
    ) -> Option<Interval> {
        round_interval(self.tones, interval, rounding_strategy)
    }

    pub fn expand(&self, interval: Interval, rounding_strategy: RoundingStrategy) -> Interval {
        self.checked_expand(interval, rounding_strategy)
            .expect("Interval could not be expanded")
    }

    pub fn checked_expand(
        &self,
        interval: Interval,
        rounding_strategy: RoundingStrategy,
    ) -> Option<Interval> {
        round_interval(self.intervals, interval, rounding_strategy)
    }
}

fn round_interval(
    intervals: &[Interval],
    interval: Interval,
    rounding_strategy: RoundingStrategy,
) -> Option<Interval> {
    use RoundingStrategy::*;

    let scaled = (interval * intervals.len()).as_ratio();
    let scaled = match rounding_strategy {
        _ if scaled.is_whole() => scaled.whole(),
        Down => scaled.floor().whole(),
        Up => scaled.ceil().whole(),
        TowardsZero => scaled.truncate().whole(),
        AwayFromZero => scaled.round().whole(),
        Pass => return Some(interval),
        Reject => return None,
        NearestDown | NearestUp => {
            let lower = get_scaled_interval(intervals, scaled.floor().whole());
            let upper = get_scaled_interval(intervals, scaled.ceil().whole());
            return match lower.cmp(&upper) {
                Ordering::Equal if rounding_strategy == NearestDown => Some(lower),
                Ordering::Equal => Some(upper),
                Ordering::Greater => Some(upper),
                Ordering::Less => Some(lower),
            };
        }
    };

    Some(get_scaled_interval(intervals, scaled))
}

fn get_scaled_interval(intervals: &[Interval], scaled: i64) -> Interval {
    let len = intervals.len();

    if scaled < 0 {
        let index = (len - (scaled.unsigned_abs() as usize % len)) % len;
        let octave = (scaled.abs() - 1) as usize / len;
        let value = -(Interval(1, 1) - intervals[index]);
        value - Interval(1, 1) * octave
    } else {
        let index = scaled as usize % len;
        let octave = scaled as usize / len;
        intervals[index] + Interval(1, 1) * octave
    }
}

impl core::ops::Mul<Interval> for ModeIntervals {
    type Output = Interval;

    fn mul(self, interval: Interval) -> Self::Output {
        self.expand(interval, Default::default())
    }
}

impl core::ops::Div<ModeIntervals> for Interval {
    type Output = Interval;

    fn div(self, mode: ModeIntervals) -> Self::Output {
        mode.collapse(self, Default::default())
    }
}

#[test]
fn interval_mode_bounds_test() {
    use super::heptatonic::MAJOR;

    for i in -10000..10000 {
        let _ = MAJOR.expand(i.into(), Default::default());
    }
}