tune 0.32.0

Explore musical tunings and create synthesizer tuning files for microtonal scales.
Documentation
//! Linear and logarithmic operations on pitches, frequencies and frequency ratios.

use std::{
    cmp::Ordering,
    fmt::{self, Display, Formatter},
    ops::{Div, Mul},
    str::FromStr,
};

use crate::{
    math, parse,
    tuning::{Approximation, Tuning},
};

/// Struct representing the frequency of a pitch.
///
///
/// You can retrieve the absolute frequency of a [`Pitch`] in Hz via [`Pitch::as_hz`].
/// Alternatively, [`Pitch`]es can interact with [`Ratio`]s using [`Ratio::between_pitches`] or the [`Mul`]/[`Div`] operators.
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Pitch {
    hz: f64,
}

impl Pitch {
    /// A more intuitive replacement for [`Pitched::pitch`].
    ///
    /// # Examples
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::note::NoteLetter;
    /// # use tune::pitch::Pitch;
    /// use tune::pitch::Pitched;
    ///
    /// let note = NoteLetter::C.in_octave(4);
    /// assert_approx_eq!(Pitch::of(note).as_hz(), note.pitch().as_hz());
    /// ```
    pub fn of(pitched: impl Pitched) -> Pitch {
        pitched.pitch()
    }

    pub fn from_hz(hz: f64) -> Pitch {
        Pitch { hz }
    }

    pub fn as_hz(self) -> f64 {
        self.hz
    }
}

impl FromStr for Pitch {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.ends_with("Hz") || s.ends_with("hz") {
            let freq = &s[..s.len() - 2];
            let freq = freq
                .parse::<Ratio>()
                .map_err(|e| format!("Invalid frequency: '{}': {}", freq, e))?;
            Ok(Pitch::from_hz(freq.as_float()))
        } else {
            Err("Must end with Hz or hz".to_string())
        }
    }
}

/// Lower a [`Pitch`] by a given [`Ratio`].
///
/// # Examples
///
/// ```
/// # use assert_approx_eq::assert_approx_eq;
/// # use tune::pitch::Pitch;
/// # use tune::pitch::Ratio;
/// assert_approx_eq!((Pitch::from_hz(330.0) / Ratio::from_float(1.5)).as_hz(), 220.0);
/// ```
impl Div<Ratio> for Pitch {
    type Output = Pitch;

    fn div(self, rhs: Ratio) -> Self::Output {
        Pitch::from_hz(self.as_hz() / rhs.as_float())
    }
}

/// Raise a [`Pitch`] by a given [`Ratio`].
///
/// # Examples
///
/// ```
/// # use assert_approx_eq::assert_approx_eq;
/// # use tune::pitch::Pitch;
/// # use tune::pitch::Ratio;
/// assert_approx_eq!((Pitch::from_hz(220.0) * Ratio::from_float(1.5)).as_hz(), 330.0);
/// ```
impl Mul<Ratio> for Pitch {
    type Output = Pitch;

    fn mul(self, rhs: Ratio) -> Self::Output {
        Pitch::from_hz(self.as_hz() * rhs.as_float())
    }
}

/// Objects which have a [`Pitch`] assigned.
pub trait Pitched {
    /// Retrieves the [`Pitch`] of the [`Pitched`] object.
    ///
    /// # Examples
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::note::NoteLetter;
    /// # use tune::pitch::Pitch;
    /// use tune::pitch::Pitched;
    ///
    /// assert_approx_eq!(Pitch::from_hz(123.456).pitch().as_hz(), 123.456);
    /// assert_approx_eq!(NoteLetter::A.in_octave(5).pitch().as_hz(), 880.0);
    /// ```
    fn pitch(&self) -> Pitch;

    /// Finds a key or note for any [`Pitched`] object in the given `tuning`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::note::NoteLetter;
    /// # use tune::pitch::Pitch;
    /// # use tune::tuning::ConcertPitch;
    /// use tune::pitch::Pitched;
    ///
    /// let a4 = NoteLetter::A.in_octave(4);
    /// let tuning = ConcertPitch::from_a4_pitch(Pitch::from_hz(432.0));
    ///
    /// let approximation = a4.find_in_tuning(tuning);
    /// assert_eq!(approximation.approx_value, a4);
    /// assert_approx_eq!(approximation.deviation.as_cents(), 31.766654);
    /// ```
    fn find_in_tuning<K, T: Tuning<K>>(&self, tuning: T) -> Approximation<K> {
        tuning.find_by_pitch(self.pitch())
    }
}

impl Pitched for Pitch {
    fn pitch(&self) -> Pitch {
        *self
    }
}

/// Struct representing the relative distance between two [`Pitch`]es.
///
/// Mathematically, this distance can be interpreted as the factor between the two pitches in
/// linear frequency space or as the offset between them in logarithmic frequency space.
///
/// The [`Ratio`] struct offers both linear and logarithmic accessors to the encapsulated distance.
/// It is possible to convert between the different representations by using `from_<repr1>` and `as_<repr2>` in
/// combination where `<reprN>` can be a linear (`float`) or logarithmic (`cents`, `semitones`, `octaves`) quantity.
///
/// # Examples
///
/// ```
/// # use assert_approx_eq::assert_approx_eq;
/// # use tune::pitch::Ratio;
/// assert_approx_eq!(Ratio::from_float(1.5).as_cents(), 701.955);
/// assert_approx_eq!(Ratio::from_cents(400.0).as_semitones(), 4.0);
/// assert_approx_eq!(Ratio::from_semitones(3.0).as_octaves(), 0.25);
/// assert_approx_eq!(Ratio::from_octaves(3.0).as_float(), 8.0);
/// ```
///
/// # Invalid Values
///
/// [`Ratio`] can contain non-finite values if the *linear* value is not a finite positive number.
///
/// ```
/// # use tune::pitch::Ratio;
/// assert!(Ratio::from_cents(0.0).as_cents().is_finite());
/// assert!(Ratio::from_cents(-3.0).as_cents().is_finite());
/// assert!(Ratio::from_float(0.0).as_cents() == f64::NEG_INFINITY);
/// assert!(Ratio::from_float(-3.0).as_cents().is_nan());
/// ```
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Ratio {
    float_value: f64,
}

impl Ratio {
    pub fn from_float(float_value: f64) -> Self {
        Self { float_value }
    }

    pub fn from_cents(cents_value: f64) -> Self {
        Self::from_octaves(cents_value / 1200.0)
    }

    pub fn from_semitones(semitones: impl Into<f64>) -> Self {
        Self::from_octaves(semitones.into() / 12.0)
    }

    pub fn from_octaves(octaves: impl Into<f64>) -> Self {
        Self::from_float(octaves.into().exp2())
    }

    pub fn octave() -> Self {
        Self::from_float(2.0)
    }

    /// Creates a new [`Ratio`] instance based on the relative distance between two [`Pitched`] entities.
    ///
    /// # Examples
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Pitch;
    /// # use tune::pitch::Ratio;
    /// let pitch_330_hz = Pitch::from_hz(330.0);
    /// let pitch_440_hz = Pitch::from_hz(440.0);
    /// assert_approx_eq!(Ratio::between_pitches(pitch_330_hz, pitch_440_hz).as_float(), 4.0 / 3.0);
    /// ```
    pub fn between_pitches(pitch_a: impl Pitched, pitch_b: impl Pitched) -> Self {
        Ratio::from_float(pitch_b.pitch().as_hz() / pitch_a.pitch().as_hz())
    }

    /// Stretches `self` by the provided `stretch`.
    ///
    /// This reverses [`Ratio::deviation_from`].
    ///
    /// # Examples
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// assert_approx_eq!(Ratio::octave().stretched_by(Ratio::from_cents(10.0)).as_cents(), 1210.0);
    /// ```
    pub fn stretched_by(self, stretch: Ratio) -> Ratio {
        Ratio::from_float(self.as_float() * stretch.as_float())
    }

    /// Calculates the difference between the provided `reference` and `self`.
    ///
    /// This reverses [`Ratio::stretched_by`].
    ///
    /// # Examples
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// assert_approx_eq!(Ratio::from_cents(1210.0).deviation_from(Ratio::octave()).as_cents(), 10.0);
    /// ```
    pub fn deviation_from(self, reference: Ratio) -> Ratio {
        Ratio::from_float(self.as_float() / reference.as_float())
    }

    /// Creates a new [`Ratio`] instance by applying `self` `num_repetitions` times.
    ///
    /// This reverses [`Ratio::divided_into_equal_steps`] or [`Ratio::num_equal_steps_of_size`].
    ///
    /// # Examples
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// assert_approx_eq!(Ratio::from_semitones(2.0).repeated(3).as_semitones(), 6.0);
    /// ```
    pub fn repeated(self, num_repetitions: impl Into<f64>) -> Ratio {
        Ratio::from_octaves(self.as_octaves() * num_repetitions.into())
    }

    /// Returns the [`Ratio`] resulting from dividing `self` into `num_steps` equal steps.
    ///
    /// This reverses [`Ratio::repeated`].
    ///
    /// # Examples
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// assert_approx_eq!(Ratio::octave().divided_into_equal_steps(15).as_cents(), 80.0);
    /// ```
    pub fn divided_into_equal_steps(self, num_steps: impl Into<f64>) -> Ratio {
        Ratio::from_octaves(self.as_octaves() / num_steps.into())
    }

    /// Determines how many equal steps of size `step_size` fit into `self`.
    ///
    /// This reverses [`Ratio::repeated`].
    ///
    /// # Examples
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// assert_approx_eq!(Ratio::octave().num_equal_steps_of_size(Ratio::from_cents(80.0)), 15.0);
    /// ```
    pub fn num_equal_steps_of_size(self, step_size: Ratio) -> f64 {
        self.as_octaves() / step_size.as_octaves()
    }

    pub fn as_float(self) -> f64 {
        self.float_value
    }

    pub fn as_cents(self) -> f64 {
        self.as_semitones() * 100.0
    }

    pub fn as_semitones(self) -> f64 {
        self.as_octaves() * 12.0
    }

    pub fn as_octaves(self) -> f64 {
        self.float_value.log2()
    }

    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// assert_approx_eq!(Ratio::from_float(4.0).inv().as_float(), 0.25);
    /// assert_approx_eq!(Ratio::from_cents(150.0).inv().as_cents(), -150.0);
    /// ```
    pub fn inv(self) -> Ratio {
        Self {
            float_value: 1.0 / self.float_value,
        }
    }

    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// assert_eq!(Ratio::from_float(f64::INFINITY).abs().as_float(), f64::INFINITY);
    /// assert_approx_eq!(Ratio::from_float(2.0).abs().as_float(), 2.0);
    /// assert_approx_eq!(Ratio::from_float(1.0).abs().as_float(), 1.0);
    /// assert_approx_eq!(Ratio::from_float(0.5).abs().as_float(), 2.0);
    /// assert_eq!(Ratio::from_float(0.0).abs().as_float(), f64::INFINITY);
    ///
    /// // Pathological cases, documented for completeness
    /// assert_eq!(Ratio::from_float(-0.0).abs().as_float(), f64::NEG_INFINITY);
    /// assert_approx_eq!(Ratio::from_float(-0.5).abs().as_float(), -2.0);
    /// assert_approx_eq!(Ratio::from_float(-1.0).abs().as_float(), -1.0);
    /// assert_approx_eq!(Ratio::from_float(-2.0).abs().as_float(), -2.0);
    /// assert_eq!(Ratio::from_float(f64::NEG_INFINITY).abs().as_float(), f64::NEG_INFINITY);
    /// assert!(Ratio::from_float(f64::NAN).abs().as_float().is_nan());
    /// ```
    pub fn abs(self) -> Ratio {
        Self {
            float_value: if self.float_value > -1.0 && self.float_value < 1.0 {
                self.float_value.recip()
            } else {
                self.float_value
            },
        }
    }

    /// Check whether the given [`Ratio`] is negligible.
    ///
    /// The threshold is around a 500th of a cent.
    ///
    /// # Examples
    ///
    /// ```
    /// # use tune::pitch::Ratio;
    /// assert!(!Ratio::from_cents(0.002).is_negligible());
    /// assert!(Ratio::from_cents(0.001).is_negligible());
    /// assert!(Ratio::from_cents(0.000).is_negligible());
    /// assert!(Ratio::from_cents(-0.001).is_negligible());
    /// assert!(!Ratio::from_cents(-0.002).is_negligible());
    /// ```
    pub fn is_negligible(self) -> bool {
        (0.999999..1.000001).contains(&self.float_value)
    }

    /// `impl` stolen from <https://doc.rust-lang.org/std/primitive.f64.html#method.total_cmp>.
    pub fn total_cmp(&self, other: &Self) -> Ordering {
        let mut left = self.as_float().to_bits() as i64;
        let mut right = other.as_float().to_bits() as i64;
        left ^= (((left >> 63) as u64) >> 1) as i64;
        right ^= (((right >> 63) as u64) >> 1) as i64;
        left.cmp(&right)
    }

    /// Finds a rational number approximation of the current [`Ratio`] instance.
    ///
    /// The largest acceptable numerator or denominator can be controlled using the `odd_limit` parameter.
    /// Only odd factors are compared against the `odd_limit` which means that 12 is 3, effectively, while 11 stays 11.
    /// Read the documentation of [`math::odd_factors_u16`] for more examples.
    ///
    /// # Examples
    ///
    /// A minor seventh can be approximated by 16/9.
    ///
    ///```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// let minor_seventh = Ratio::from_semitones(10);
    /// let odd_limit = 9;
    /// let f = minor_seventh.nearest_fraction(odd_limit);
    /// assert_eq!((f.numer, f.denom), (16, 9));
    /// assert_eq!(f.num_octaves, 0);
    /// assert_approx_eq!(f.deviation.as_cents(), 3.910002); // Quite good!
    /// ```
    ///
    /// Reducing the `odd_limit` saves computation time but may lead to a bad approximation.
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// # let minor_seventh = Ratio::from_semitones(10);
    /// let odd_limit = 5;
    /// let f = minor_seventh.nearest_fraction(odd_limit);
    /// assert_eq!((f.numer, f.denom), (5, 3));
    /// assert_eq!(f.num_octaves, 0);
    /// assert_approx_eq!(f.deviation.as_cents(), 115.641287); // Pretty bad!
    /// ```
    ///
    /// The approximation is normalized to values within an octave. The number of octaves is reported separately.
    ///
    /// ```
    /// # use assert_approx_eq::assert_approx_eq;
    /// # use tune::pitch::Ratio;
    /// let lower_than_an_octave = Ratio::from_float(3.0 / 4.0);
    /// let odd_limit = 11;
    /// let f = lower_than_an_octave.nearest_fraction(odd_limit);
    /// assert_eq!((f.numer, f.denom), (3, 2));
    /// assert_eq!(f.num_octaves, -1);
    /// assert_approx_eq!(f.deviation.as_cents(), 0.0);
    /// ```
    pub fn nearest_fraction(self, odd_limit: u16) -> NearestFraction {
        NearestFraction::for_ratio(self, odd_limit)
    }
}

/// The default [`Ratio`] is the ratio that represents equivalence of two frequencies, i.e. no distance at all.
///
/// # Examples
///
/// ```
/// # use assert_approx_eq::assert_approx_eq;
/// # use tune::pitch::Ratio;
/// assert_approx_eq!(Ratio::default().as_float(), 1.0); // Neutral element for multiplication
/// assert_approx_eq!(Ratio::default().as_cents(), 0.0); // Neutral element for addition
/// ```
impl Default for Ratio {
    fn default() -> Self {
        Self::from_float(1.0)
    }
}

/// [`Ratio`]s can be formatted as float or cents.
///
/// # Examples
//
/// ```
/// # use tune::pitch::Ratio;
/// // As float
/// assert_eq!(format!("{}", Ratio::from_float(1.5)), "1.5000");
/// assert_eq!(format!("{}", Ratio::from_float(1.0 / 1.5)), "0.6667");
/// assert_eq!(format!("{:.2}", Ratio::from_float(1.0 / 1.5)), "0.67");
///
/// // As cents
/// assert_eq!(format!("{:#}", Ratio::from_float(1.5)), "+702.0c");
/// assert_eq!(format!("{:#}", Ratio::from_float(1.0 / 1.5)), "-702.0c");
/// assert_eq!(format!("{:#.2}", Ratio::from_float(1.0 / 1.5)), "-701.96c");
///
/// // With padding
/// assert_eq!(format!("{:=^#14.2}", Ratio::from_float(1.5)), "===+701.96c===");
/// ```
impl Display for Ratio {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        let formatted = if f.alternate() {
            format!(
                "{:+.precision$}c",
                self.as_cents(),
                precision = f.precision().unwrap_or(1)
            )
        } else {
            format!(
                "{:.precision$}",
                self.as_float(),
                precision = f.precision().unwrap_or(4)
            )
        };
        f.pad_integral(true, "", &formatted)
    }
}

/// [`Ratio`]s can be parsed using `tune`'s built-in expression language.
///
/// # Examples
///
/// ```
/// # use assert_approx_eq::assert_approx_eq;
/// # use tune::pitch::Ratio;
/// assert_approx_eq!("1.5".parse::<Ratio>().unwrap().as_float(), 1.5);
/// assert_approx_eq!("3/2".parse::<Ratio>().unwrap().as_float(), 1.5);
/// assert_approx_eq!("7:12:2".parse::<Ratio>().unwrap().as_semitones(), 7.0);
/// assert_approx_eq!("702c".parse::<Ratio>().unwrap().as_cents(), 702.0);
/// assert_eq!("foo".parse::<Ratio>().unwrap_err(), "Invalid expression \'foo\': Must be a float (e.g. 1.5), fraction (e.g. 3/2), interval fraction (e.g. 7:12:2) or cents value (e.g. 702c)");
impl FromStr for Ratio {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.parse::<RatioExpression>().map(RatioExpression::ratio)
    }
}

/// Target type for successfully parsed and validated ratio expressions.
#[derive(Copy, Clone, Debug)]
pub struct RatioExpression {
    ratio: Ratio,
    representation: RatioExpressionVariant,
}

impl RatioExpression {
    pub fn ratio(self) -> Ratio {
        self.ratio
    }

    pub fn variant(self) -> RatioExpressionVariant {
        self.representation
    }
}

/// The only way to construct a [`RatioExpression`] is via the [`FromStr`] trait.
impl FromStr for RatioExpression {
    type Err = String;

    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
        s = s.trim();
        parse_ratio(s)
            .and_then(|representation| {
                representation.as_ratio().map(|ratio| Self {
                    ratio,
                    representation,
                })
            })
            .map_err(|e| format!("Invalid expression '{}': {}", s, e))
    }
}

/// Type used to distinguish which particular outer expression was given as string input before parsing.
#[derive(Copy, Clone, Debug)]
pub enum RatioExpressionVariant {
    Float {
        float_value: f64,
    },
    Fraction {
        numer: f64,
        denom: f64,
    },
    IntervalFraction {
        numer: f64,
        denom: f64,
        interval: f64,
    },
    Cents {
        cents_value: f64,
    },
}

impl RatioExpressionVariant {
    pub fn as_ratio(self) -> Result<Ratio, String> {
        let float_value = self.as_float()?;
        if float_value > 0.0 {
            Ok(Ratio { float_value })
        } else {
            Err(format!(
                "Evaluates to {} but should be positive",
                float_value
            ))
        }
    }

    fn as_float(self) -> Result<f64, String> {
        let as_float = match self {
            Self::Float { float_value } => float_value,
            Self::Fraction { numer, denom } => numer / denom,
            Self::IntervalFraction {
                numer,
                denom,
                interval,
            } => interval.powf(numer / denom),
            Self::Cents { cents_value } => Ratio::from_cents(cents_value).as_float(),
        };
        if as_float.is_finite() {
            Ok(as_float)
        } else {
            Err(format!("Evaluates to {}", as_float))
        }
    }
}

fn parse_ratio(s: &str) -> Result<RatioExpressionVariant, String> {
    let s = s.trim();
    if let [numer, denom, interval] = parse::split_balanced(s, ':').as_slice() {
        Ok(RatioExpressionVariant::IntervalFraction {
            numer: parse_ratio_as_float(numer, "interval numerator")?,
            denom: parse_ratio_as_float(denom, "interval denominator")?,
            interval: parse_ratio_as_float(interval, "interval")?,
        })
    } else if let [numer, denom] = parse::split_balanced(s, '/').as_slice() {
        Ok(RatioExpressionVariant::Fraction {
            numer: parse_ratio_as_float(numer, "numerator")?,
            denom: parse_ratio_as_float(denom, "denominator")?,
        })
    } else if let [cents_value, ""] = parse::split_balanced(s, 'c').as_slice() {
        Ok(RatioExpressionVariant::Cents {
            cents_value: parse_ratio_as_float(cents_value, "cents value")?,
        })
    } else if s.starts_with('(') && s.ends_with(')') {
        parse_ratio(&s[1..s.len() - 1])
    } else {
        Ok(RatioExpressionVariant::Float {
            float_value: s.parse().map_err(|_| {
                "Must be a float (e.g. 1.5), fraction (e.g. 3/2), \
                 interval fraction (e.g. 7:12:2) or cents value (e.g. 702c)"
                    .to_string()
            })?,
        })
    }
}

fn parse_ratio_as_float(s: &str, name: &str) -> Result<f64, String> {
    parse_ratio(s)
        .and_then(RatioExpressionVariant::as_float)
        .map_err(|e| format!("Invalid {} '{}': {}", name, s, e))
}

/// An odd-limit nearest-fraction approximation fo a given [`Ratio`].
#[derive(Copy, Clone, Debug)]
pub struct NearestFraction {
    /// The numerator of the approximation.
    pub numer: u16,
    /// The denominator of the approximation.
    pub denom: u16,
    /// The deviation of the target value from the approximation.
    pub deviation: Ratio,
    /// The number of even factors that have been removed from the approximation to account for octave equivalence.
    pub num_octaves: i32,
}

impl NearestFraction {
    fn for_ratio(ratio: Ratio, odd_limit: u16) -> Self {
        let num_octaves = ratio.as_octaves().floor() as i32;
        let target_ratio = ratio.deviation_from(Ratio::from_octaves(num_octaves));

        let mut left = (0, 1);
        let mut right = (1, 0);

        let mut best = (0, 0);
        let mut best_deviation = Ratio::from_float(f64::INFINITY);

        while let Some(mid) =
            u16::checked_add(left.0, right.0).zip(u16::checked_add(left.1, right.1))
        {
            let odd_factors_numer = math::odd_factors_u16(mid.0);
            let odd_factors_denom = math::odd_factors_u16(mid.1);

            if odd_factors_numer > odd_limit && odd_factors_denom > odd_limit {
                break;
            }

            let mid_ratio = Ratio::from_float(f64::from(mid.0) / f64::from(mid.1));

            if odd_factors_numer <= odd_limit && odd_factors_denom <= odd_limit {
                let mid_deviation = target_ratio.deviation_from(mid_ratio);
                if mid_deviation.abs() < best_deviation.abs() {
                    best = mid;
                    best_deviation = mid_deviation;
                }
            }

            match target_ratio.partial_cmp(&mid_ratio) {
                Some(Ordering::Less) => {
                    right = mid;
                }
                Some(Ordering::Greater) => {
                    left = mid;
                }
                Some(Ordering::Equal) | None => break,
            }
        }

        NearestFraction {
            numer: best.0,
            denom: best.1,
            deviation: best_deviation,
            num_octaves,
        }
    }
}

impl Display for NearestFraction {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let formatted = format!(
            "{}/{} [{:+.0}c] ({:+}o)",
            self.numer,
            self.denom,
            self.deviation.as_cents(),
            self.num_octaves
        );
        f.pad(&formatted)
    }
}

#[cfg(test)]
mod test {
    use std::iter;

    use super::*;

    #[test]
    fn parses_successfully() {
        let test_cases = [
            ("1", 1.0000),
            ("99.9", 99.9000),
            ("(1.25)", 1.2500),
            ("(1.25)", 1.2500),
            ("10/3", 3.3333),
            ("10/(10/3)", 3.0000),
            ("(10/3)/10", 0.3333),
            ("(3/4)/(5/6)", 0.9000),
            ("(3/4)/(5/6)", 0.9000),
            ("0:12:2", 1.000),
            ("7:12:2", 1.4983),   // 2^(7/12) - 12-edo perfect fifth
            ("7/12:1:2", 1.4983), // 2^(7/12) - 12-edo perfect fifth
            ("12:12:2", 2.000),
            ("-12:12:2", 0.500),
            ("4:1:3/2", 5.0625),   // (3/2)^4 - pythagorean major third
            ("1:1/4:3/2", 5.0625), // (3/2)^4 - pythagorean major third
            ("1/2:3/2:(1:2:64)", 2.0000),
            ("((1/2):(3/2):(1:2:64))", 2.0000),
            (" (    (1 /2)  :(3 /2):   (1: 2:   64  ))     ", 2.0000),
            ("12:7:700c", 2.000),
            ("0c", 1.0000),
            ("(0/3)c", 1.0000),
            ("702c", 1.5000),  // 2^(702/1200) - pythagorean fifth
            ("-702c", 0.6666), // 2^(-702/1200) - pythagorean fifth downwards
            ("1200c", 2.0000),
            ("702c/3", 0.5000),    // 2^(702/1200)/3 - 702 cents divided by 3
            ("3/702c", 2.0000),    // 3/2^(702/1200) - 3 divided by 702 cents
            ("(1404/2)c", 1.5000), // 2^(702/1200) - 1402/2 cents
        ];

        for (input, expected) in test_cases.iter() {
            let parsed = input.parse::<Ratio>().unwrap().as_float();
            assert!(
                (parsed - expected).abs() < 0.0001,
                "`{}` should evaluate to {} but was {:.4}",
                input,
                expected,
                parsed
            );
        }
    }

    #[test]
    fn parses_with_error() {
        let test_cases = [
            (
                "0.0",
                "Invalid expression '0.0': Evaluates to 0 but should be positive",
            ),
            (
                "-1.2345",
                "Invalid expression '-1.2345': Evaluates to -1.2345 but should be positive",
            ),
            ("1/0", "Invalid expression '1/0': Evaluates to inf"),
            (
                "(1/0)c",
                "Invalid expression '(1/0)c': Invalid cents value '(1/0)': Evaluates to inf",
            ),
            (
                "(1/x)c",
                "Invalid expression '(1/x)c': Invalid cents value '(1/x)': Invalid denominator 'x': \
                 Must be a float (e.g. 1.5), fraction (e.g. 3/2), interval fraction (e.g. 7:12:2) or cents value (e.g. 702c)",
            ),
            (
                "   (1   /x )c ",
                "Invalid expression '(1   /x )c': Invalid cents value '(1   /x )': Invalid denominator 'x': \
                 Must be a float (e.g. 1.5), fraction (e.g. 3/2), interval fraction (e.g. 7:12:2) or cents value (e.g. 702c)",
            ),
        ];

        for (input, expected) in test_cases.iter() {
            let parse_error = input.parse::<Ratio>().unwrap_err();
            assert_eq!(parse_error, *expected);
        }
    }

    #[test]
    fn parse_variant() {
        assert!(matches!(
            "1".parse::<RatioExpression>().unwrap().variant(),
            RatioExpressionVariant::Float { .. }
        ));
        assert!(matches!(
            "10/3".parse::<RatioExpression>().unwrap().variant(),
            RatioExpressionVariant::Fraction { .. }
        ));
        assert!(matches!(
            "(3/4)/(5/6)".parse::<RatioExpression>().unwrap().variant(),
            RatioExpressionVariant::Fraction { .. }
        ));
        assert!(matches!(
            "12:7:700c".parse::<RatioExpression>().unwrap().variant(),
            RatioExpressionVariant::IntervalFraction { .. }
        ));
        assert!(matches!(
            "(0/3)c".parse::<RatioExpression>().unwrap().variant(),
            RatioExpressionVariant::Cents { .. }
        ));
    }

    #[test]
    fn find_nearest_fraction() {
        let nearest_fractions: Vec<_> = iter::successors(Some(0.5), |prev| Some(prev * 1.05))
            .take(50)
            .map(|ratio| {
                format!(
                    "ratio = {:.2}, nearest_fraction = {}",
                    ratio,
                    Ratio::from_float(ratio).nearest_fraction(11)
                )
            })
            .collect();

        assert_eq!(
            nearest_fractions,
            [
                "ratio = 0.50, nearest_fraction = 1/1 [+0c] (-1o)",
                "ratio = 0.53, nearest_fraction = 12/11 [-66c] (-1o)",
                "ratio = 0.55, nearest_fraction = 11/10 [+4c] (-1o)",
                "ratio = 0.58, nearest_fraction = 7/6 [-13c] (-1o)",
                "ratio = 0.61, nearest_fraction = 11/9 [-10c] (-1o)",
                "ratio = 0.64, nearest_fraction = 14/11 [+5c] (-1o)",
                "ratio = 0.67, nearest_fraction = 4/3 [+9c] (-1o)",
                "ratio = 0.70, nearest_fraction = 7/5 [+9c] (-1o)",
                "ratio = 0.74, nearest_fraction = 3/2 [-26c] (-1o)",
                "ratio = 0.78, nearest_fraction = 14/9 [-5c] (-1o)",
                "ratio = 0.81, nearest_fraction = 18/11 [-8c] (-1o)",
                "ratio = 0.86, nearest_fraction = 12/7 [-4c] (-1o)",
                "ratio = 0.90, nearest_fraction = 9/5 [-4c] (-1o)",
                "ratio = 0.94, nearest_fraction = 11/6 [+49c] (-1o)",
                "ratio = 0.99, nearest_fraction = 2/1 [-17c] (-1o)",
                "ratio = 1.04, nearest_fraction = 1/1 [+67c] (+0o)",
                "ratio = 1.09, nearest_fraction = 12/11 [+1c] (+0o)",
                "ratio = 1.15, nearest_fraction = 8/7 [+5c] (+0o)",
                "ratio = 1.20, nearest_fraction = 6/5 [+5c] (+0o)",
                "ratio = 1.26, nearest_fraction = 14/11 [-13c] (+0o)",
                "ratio = 1.33, nearest_fraction = 4/3 [-9c] (+0o)",
                "ratio = 1.39, nearest_fraction = 7/5 [-9c] (+0o)",
                "ratio = 1.46, nearest_fraction = 16/11 [+10c] (+0o)",
                "ratio = 1.54, nearest_fraction = 14/9 [-22c] (+0o)",
                "ratio = 1.61, nearest_fraction = 8/5 [+14c] (+0o)",
                "ratio = 1.69, nearest_fraction = 12/7 [-21c] (+0o)",
                "ratio = 1.78, nearest_fraction = 16/9 [+0c] (+0o)",
                "ratio = 1.87, nearest_fraction = 11/6 [+31c] (+0o)",
                "ratio = 1.96, nearest_fraction = 2/1 [-35c] (+0o)",
                "ratio = 2.06, nearest_fraction = 1/1 [+50c] (+1o)",
                "ratio = 2.16, nearest_fraction = 12/11 [-17c] (+1o)",
                "ratio = 2.27, nearest_fraction = 8/7 [-13c] (+1o)",
                "ratio = 2.38, nearest_fraction = 6/5 [-13c] (+1o)",
                "ratio = 2.50, nearest_fraction = 5/4 [+1c] (+1o)",
                "ratio = 2.63, nearest_fraction = 4/3 [-26c] (+1o)",
                "ratio = 2.76, nearest_fraction = 11/8 [+5c] (+1o)",
                "ratio = 2.90, nearest_fraction = 16/11 [-8c] (+1o)",
                "ratio = 3.04, nearest_fraction = 3/2 [+23c] (+1o)",
                "ratio = 3.19, nearest_fraction = 8/5 [-4c] (+1o)",
                "ratio = 3.35, nearest_fraction = 5/3 [+10c] (+1o)",
                "ratio = 3.52, nearest_fraction = 7/4 [+10c] (+1o)",
                "ratio = 3.70, nearest_fraction = 11/6 [+14c] (+1o)",
                "ratio = 3.88, nearest_fraction = 2/1 [-52c] (+1o)",
                "ratio = 4.07, nearest_fraction = 1/1 [+32c] (+2o)",
                "ratio = 4.28, nearest_fraction = 12/11 [-34c] (+2o)",
                "ratio = 4.49, nearest_fraction = 9/8 [-3c] (+2o)",
                "ratio = 4.72, nearest_fraction = 7/6 [+19c] (+2o)",
                "ratio = 4.95, nearest_fraction = 5/4 [-16c] (+2o)",
                "ratio = 5.20, nearest_fraction = 9/7 [+19c] (+2o)",
                "ratio = 5.46, nearest_fraction = 11/8 [-12c] (+2o)"
            ]
        );
    }
}