ireal-parser 0.1.0

iReal Pro song parser and manipulation library
Documentation
use std::{fmt, str::FromStr};

use crate::{Error, Result};

/// Represents an iReal Pro note
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Note {
    C,
    Csharp,
    Dflat,
    D,
    Dsharp,
    Eflat,
    E,
    F,
    Fsharp,
    Gflat,
    G,
    Gsharp,
    Aflat,
    A,
    Asharp,
    Bflat,
    B,
}

impl fmt::Display for Note {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use Note::*;

        let s = match self {
            C => "C",
            Csharp => "C#",
            Dflat => "Db",
            D => "D",
            Dsharp => "D#",
            Eflat => "Eb",
            E => "E",
            F => "F",
            Fsharp => "F#",
            Gflat => "Gb",
            G => "G",
            Gsharp => "G#",
            Aflat => "Ab",
            A => "A",
            Asharp => "A#",
            Bflat => "Bb",
            B => "B",
        };

        write!(f, "{s}")
    }
}

impl FromStr for Note {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        use Note::*;

        let note = match s {
            "C" => C,
            "C#" => Csharp,
            "Db" => Dflat,
            "D" => D,
            "D#" => Dsharp,
            "Eb" => Eflat,
            "E" => E,
            "F" => F,
            "F#" => Fsharp,
            "Gb" => Gflat,
            "G" => G,
            "G#" => Gsharp,
            "Ab" => Gflat,
            "A" => A,
            "A#" => Asharp,
            "Bb" => Bflat,
            "B" => B,
            _ => return Err(Error::InvalidNote),
        };

        Ok(note)
    }
}

/// Represents an iReal Pro chord quality
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum ChordQuality {
    Fifth,
    Second,
    Add9,
    Aug,
    Dim,
    Half,
    Sus,
    Maj,
    Min,
    Maj7,
    Min7,
    Dom7,
    Dom7sus,
    Half7,
    Dim7,
    Maj9,
    Maj13,
    Sixth,
    Sixth9,
    Maj7Sharp11,
    Maj9Sharp11,
    Maj7Sharp5,
    Min6,
    Min69,
    MinMaj7,
    MinMaj9,
    Min9,
    Min11,
    Min7Flat5,
    Half9,
    MinFlat6,
    MinSharp5,
    Ninth,
    Dom7Flat9,
    Dom7Sharp9,
    Dom7Sharp11,
    Dom7Flat5,
    Dom7Sharp5,
    NinthSharp11,
    NinthFlat5,
    NinthSharp5,
    Dom7Flat13,
    Dom7Sharp9Sharp5,
    Dom7Sharp9Flat5,
    Dom7Sharp9Sharp11,
    Dom7Flat9Sharp11,
    Dom7Flat9Flat5,
    Dom7Flat9Sharp5,
    Dom7Flat9Sharp9,
    Dom7Flat9Flat13,
    Dom7Alt,
    Dom13,
    Dom13Sharp11,
    Dom13Flat9,
    Dom13Sharp9,
    Dom7Flat9sus,
    Dom7susAdd3,
    Dom9sus,
    Dom13sus,
    Dom7Flat13sus,
    Dom11,
}

impl fmt::Display for ChordQuality {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use ChordQuality::*;

        let s = match self {
            Fifth => "5",
            Second => "2",
            Add9 => "add9",
            Aug => "+",
            Dim => "o",
            Half => "h",
            Sus => "sus",
            Maj => "^",
            Min => "-",
            Maj7 => "^7",
            Min7 => "-7",
            Dom7 => "7",
            Dom7sus => "7sus",
            Half7 => "h7",
            Dim7 => "o7",
            Maj9 => "^9",
            Maj13 => "^13",
            Sixth => "6",
            Sixth9 => "69",
            Maj7Sharp11 => "^7#11",
            Maj9Sharp11 => "^9#11",
            Maj7Sharp5 => "^7#5",
            Min6 => "-6",
            Min69 => "-69",
            MinMaj7 => "-^7",
            MinMaj9 => "-^9",
            Min9 => "-9",
            Min11 => "-11",
            Min7Flat5 => "-7b5",
            Half9 => "h9",
            MinFlat6 => "-b6",
            MinSharp5 => "-#5",
            Ninth => "9",
            Dom7Flat9 => "7b9",
            Dom7Sharp9 => "7#9",
            Dom7Sharp11 => "7#11",
            Dom7Flat5 => "7b5",
            Dom7Sharp5 => "7#5",
            NinthSharp11 => "9#11",
            NinthFlat5 => "9b5",
            NinthSharp5 => "9#5",
            Dom7Flat13 => "7b13",
            Dom7Sharp9Sharp5 => "7#9#5",
            Dom7Sharp9Flat5 => "7#9b5",
            Dom7Sharp9Sharp11 => "7#9#11",
            Dom7Flat9Sharp11 => "7b9#11",
            Dom7Flat9Flat5 => "7b9b5",
            Dom7Flat9Sharp5 => "7b9#5",
            Dom7Flat9Sharp9 => "7b9#9",
            Dom7Flat9Flat13 => "7b9b13",
            Dom7Alt => "7alt",
            Dom13 => "13",
            Dom13Sharp11 => "13#11",
            Dom13Flat9 => "13b9",
            Dom13Sharp9 => "13#9",
            Dom7Flat9sus => "7b9sus",
            Dom7susAdd3 => "7susadd3",
            Dom9sus => "9sus",
            Dom13sus => "13sus",
            Dom7Flat13sus => "7b13sus",
            Dom11 => "11",
        };

        write!(f, "{s}")
    }
}

impl FromStr for ChordQuality {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        use ChordQuality::*;

        let q = match s {
            "5" => Fifth,
            "2" => Second,
            "add9" => Add9,
            "+" => Aug,
            "o" => Dim,
            "h" => Half,
            "sus" => Sus,
            "^" => Maj,
            "-" => Min,
            "^7" => Maj7,
            "-7" => Min7,
            "7" => Dom7,
            "7sus" => Dom7sus,
            "h7" => Half7,
            "o7" => Dim7,
            "^9" => Maj9,
            "^13" => Maj13,
            "6" => Sixth,
            "69" => Sixth9,
            "^7#11" => Maj7Sharp11,
            "^9#11" => Maj9Sharp11,
            "^7#5" => Maj7Sharp5,
            "-6" => Min6,
            "-69" => Min69,
            "-^7" => MinMaj7,
            "-^9" => MinMaj9,
            "-9" => Min9,
            "-11" => Min11,
            "-7b5" => Min7Flat5,
            "h9" => Half9,
            "-b6" => MinFlat6,
            "-#5" => MinSharp5,
            "9" => Ninth,
            "7b9" => Dom7Flat9,
            "7#9" => Dom7Sharp9,
            "7#11" => Dom7Sharp11,
            "7b5" => Dom7Flat5,
            "7#5" => Dom7Sharp5,
            "9#11" => NinthSharp11,
            "9b5" => NinthFlat5,
            "9#5" => NinthSharp5,
            "7b13" => Dom7Flat13,
            "7#9#5" => Dom7Sharp9Sharp5,
            "7#9b5" => Dom7Sharp9Flat5,
            "7#9#11" => Dom7Sharp9Sharp11,
            "7b9#11" => Dom7Flat9Sharp11,
            "7b9b5" => Dom7Flat9Flat5,
            "7b9#5" => Dom7Flat9Sharp5,
            "7b9#9" => Dom7Flat9Sharp9,
            "7b9b13" => Dom7Flat9Flat13,
            "7alt" => Dom7Alt,
            "13" => Dom13,
            "13#11" => Dom13Sharp11,
            "13b9" => Dom13Flat9,
            "13#9" => Dom13Sharp9,
            "7b9sus" => Dom7Flat9sus,
            "7susadd3" => Dom7susAdd3,
            "9sus" => Dom9sus,
            "13sus" => Dom13sus,
            "7b13sus" => Dom7Flat13sus,
            "11" => Dom11,
            _ => return Err(Error::InvalidChordQuality),
        };

        Ok(q)
    }
}

/// Represents an iReal Pro chord
///
/// Chord symbol format: Root + an optional chord quality + an optional inversion
///
/// For example just a root:
/// `C`
///
/// or a root plus a chord quality:
/// `C-7`
///
/// or a root plus in inversion inversion:
/// `C/E`
///
/// or a root plus a quality plus an inversion:
/// `C-7/Bb`
///
/// All valid roots and inversions: `C, C#, Db, D, D#, Eb, E, F, F#, Gb, G, G#,
/// Ab, A, A#, Bb, B`
///
/// All valid qualities: `5, 2, add9, +, o, h, sus, ^, -, ^7, -7, 7, 7sus, h7,
/// o7, ^9, ^13, 6, 69, ^7#11, ^9#11, ^7#5, -6, -69, -^7, -^9, -9, -11, -7b5,
/// h9, -b6, -#5, 9, 7b9, 7#9, 7#11, 7b5, 7#5, 9#11, 9b5, 9#5, 7b13, 7#9#5,
/// 7#9b5, 7#9#11, 7b9#11, 7b9b5, 7b9#5, 7b9#9, 7b9b13, 7alt, 13, 13#11, 13b9,
/// 13#9, 7b9sus, 7susadd3, 9sus, 13sus, 7b13sus, 11`
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct Chord {
    pub root: Note,
    pub quality: Option<ChordQuality>,
    pub inversion: Option<Note>,
}

impl Chord {
    pub fn new(root: Note) -> Self {
        Self {
            root,
            quality: None,
            inversion: None,
        }
    }

    pub fn set_quality(&mut self, quality: Option<ChordQuality>) {
        self.quality = quality;
    }

    pub fn set_inversion(&mut self, inversion: Option<Note>) {
        self.inversion = inversion;
    }
}

impl fmt::Display for Chord {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let root = self.root;
        let quality = self.quality.map(|q| q.to_string()).unwrap_or_default();
        let inversion = self.inversion.map(|i| format!("/{i}")).unwrap_or_default();

        write!(f, "{root}{quality}{inversion}")
    }
}

impl FromStr for Chord {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        let (chord, inv) = s
            .split_once('/')
            .map(|(c, i)| (c, Some(i)))
            .unwrap_or((s, None));

        let ext = chord
            .chars()
            .nth(1)
            .map(|c| c == '#' || c == 'b')
            .unwrap_or(false);
        let (note, quality) = chord.split_at(if ext { 2 } else { 1 });

        let mut c = Chord::new(note.parse()?);
        if !quality.is_empty() {
            c.set_quality(Some(quality.parse()?));
        }
        if let Some(inv) = inv {
            c.set_inversion(Some(inv.parse()?));
        }

        Ok(c)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_to_string() {
        let mut chord = Chord::new(Note::Dsharp);
        chord.set_quality(Some(ChordQuality::Min7Flat5));
        chord.set_inversion(Some(Note::Bflat));
        assert_eq!(chord.to_string(), "D#-7b5/Bb");

        let c: Chord = "D#-7b5/Bb".parse().unwrap();
        assert_eq!(c, chord);
    }
}