chord_mapper/
lib.rs

1//! # Chord Library
2//!
3//! This library provides functionality for working with musical chords.
4//!
5//! ## Example
6//!
7//! ```rust
8//! use my_chord_library::{Note, Chord, parse_root};
9//!
10//! // Parse a chord from a string
11//! let chord = parse_root("Cm")?;
12//!
13//! // Get the notes of the chord
14//! let notes = map_chord_to_notes(chord);
15//! ```
16//!
17//! ## Note Enum
18//!
19//! The `Note` enum represents musical notes:
20//!
21//! - A
22//! - B
23//! - C
24//! - D
25//! - E
26//! - F
27//! - G
28//! - ASHARP
29//! - CSHARP
30//! - DSHARP
31//! - FSHARP
32//! - GSHARP
33//! - DFLAT
34//! - EFLAT
35//! - GFLAT
36//! - AFLAT
37//! - BFLAT
38//!
39//! ## Chord Enum
40//!
41//! The `Chord` enum represents musical chords:
42//!
43//! - `MajorChord(Note)`
44//! - `MinorChord(Note)`
45//!
46//! ## Functions
47//!
48//! - `to_note(string_rep: &str) -> Option<Note>`: Converts a string representation to a `Note`.
49//! - `parse_root(input: &str) -> Result<Chord, Box<pest::error::Error<Rule>>>`: Parses a chord from a string.
50//! - `map_chord_to_notes(chord: Chord) -> Vec<Note>`: Maps a chord to its constituent notes.
51//! - `next_note(note: Note) -> Note`: Calculates the next note in the musical scale.
52//! - `plus_perfect_fifth(note: Note) -> Note`: Calculates the note a perfect fifth above the given note.
53//! - `plus_minor_third(note: Note) -> Note`: Calculates the note a minor third above the given note.
54//! - `plus_major_third(note: Note) -> Note`: Calculates the note a major third above the given note.
55//!
56//! ## Example Usage
57//!
58//! ```rust
59//! use my_chord_library::{Note, Chord};
60//!
61//! let note_a = Note::A;
62//! let note_c_sharp = Note::CSHARP;
63//!
64//! let chord_a_major = Chord::MajorChord(note_a);
65//! let chord_c_sharp_minor = Chord::MinorChord(note_c_sharp);
66//! ```
67
68use core::fmt;
69use pest::Parser;
70use pest_derive::Parser;
71
72#[derive(Parser)]
73#[grammar = "grammars/chord.pest"]
74pub struct ChordParser;
75
76#[derive(Debug, Clone, Copy)]
77pub enum Note {
78    A,
79    B,
80    C,
81    D,
82    E,
83    F,
84    G,
85    ASHARP,
86    CSHARP,
87    DSHARP,
88    FSHARP,
89    GSHARP,
90    DFLAT,
91    EFLAT,
92    GFLAT,
93    AFLAT,
94    BFLAT,
95}
96
97impl fmt::Display for Note {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Note::A => write!(f, "A"),
101            Note::B => write!(f, "B"),
102            Note::C => write!(f, "C"),
103            Note::D => write!(f, "D"),
104            Note::E => write!(f, "E"),
105            Note::F => write!(f, "F"),
106            Note::G => write!(f, "G"),
107            Note::ASHARP => write!(f, "A#"),
108            Note::CSHARP => write!(f, "C#"),
109            Note::DSHARP => write!(f, "D#"),
110            Note::FSHARP => write!(f, "F#"),
111            Note::GSHARP => write!(f, "G#"),
112            Note::DFLAT => write!(f, "Db"),
113            Note::EFLAT => write!(f, "Eb"),
114            Note::GFLAT => write!(f, "Gb"),
115            Note::AFLAT => write!(f, "Ab"),
116            Note::BFLAT => write!(f, "Bb"),
117        }
118    }
119}
120
121impl PartialEq for Note {
122    fn eq(&self, other: &Self) -> bool {
123        use Note::*;
124        matches!(
125            (self, other),
126            (A, A)
127                | (B, B)
128                | (C, C)
129                | (D, D)
130                | (E, E)
131                | (F, F)
132                | (G, G)
133                | (ASHARP, BFLAT)
134                | (BFLAT, ASHARP)
135                | (CSHARP, DFLAT)
136                | (DFLAT, CSHARP)
137                | (DSHARP, EFLAT)
138                | (EFLAT, DSHARP)
139                | (FSHARP, GFLAT)
140                | (GFLAT, FSHARP)
141                | (GSHARP, AFLAT)
142                | (AFLAT, GSHARP)
143        )
144    }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq)]
148pub enum Chord {
149    MajorChord(Note),
150    MinorChord(Note),
151}
152
153pub fn to_note(string_rep: &str) -> Option<Note> {
154    use Note::*;
155    match string_rep {
156        "A" => Some(A),
157        "B" => Some(B),
158        "C" => Some(C),
159        "D" => Some(D),
160        "E" => Some(E),
161        "F" => Some(F),
162        "G" => Some(G),
163
164        "A#" => Some(ASHARP),
165        "C#" => Some(CSHARP),
166        "D#" => Some(DSHARP),
167        "F#" => Some(FSHARP),
168        "G#" => Some(GSHARP),
169
170        "Db" => Some(DFLAT),
171        "Eb" => Some(EFLAT),
172        "Gb" => Some(GFLAT),
173        "Ab" => Some(AFLAT),
174        "Bb" => Some(BFLAT),
175
176        &_ => None,
177    }
178}
179
180fn next_note(note: Note) -> Note {
181    use Note::*;
182    match note {
183        A => ASHARP,
184        B => C,
185        C => CSHARP,
186        D => DSHARP,
187        E => F,
188        F => FSHARP,
189        G => GSHARP,
190        ASHARP => B,
191        CSHARP => D,
192        DSHARP => E,
193        FSHARP => G,
194        GSHARP => A,
195        DFLAT => D,
196        EFLAT => E,
197        GFLAT => G,
198        AFLAT => A,
199        BFLAT => B,
200    }
201}
202
203fn plus_perfect_fifth(note: Note) -> Note {
204    next_note(next_note(next_note(next_note(next_note(next_note(
205        next_note(note),
206    ))))))
207}
208
209fn plus_minor_third(note: Note) -> Note {
210    next_note(next_note(next_note(note)))
211}
212
213fn plus_major_third(note: Note) -> Note {
214    next_note(plus_minor_third(note))
215}
216
217pub fn parse_root(input: &str) -> Result<Chord, Box<pest::error::Error<Rule>>> {
218    let mut pairs = ChordParser::parse(Rule::root, input)?;
219
220    for pair in pairs.clone() {
221        for inner_pair in pair.into_inner() {
222            if inner_pair.as_rule() == Rule::chord {
223                let chord_pair = inner_pair.into_inner().next().unwrap();
224                match chord_pair.as_rule() {
225                    Rule::minor_chord => {
226                        let tonic = chord_pair.into_inner().next().unwrap().as_str();
227                        let tonic_note = to_note(tonic).unwrap();
228                        return Ok(Chord::MinorChord(tonic_note));
229                    }
230                    Rule::major_chord => {
231                        let tonic = chord_pair.into_inner().next().unwrap().as_str();
232                        let tonic_note = to_note(tonic).unwrap();
233                        return Ok(Chord::MajorChord(tonic_note));
234                    }
235                    _ => {
236                        return Err(Box::new(pest::error::Error::new_from_span(
237                            pest::error::ErrorVariant::CustomError {
238                                message: String::from("Unknown chord rule"),
239                            },
240                            chord_pair.as_span(),
241                        )));
242                    }
243                }
244            }
245        }
246    }
247
248    Err(Box::new(pest::error::Error::new_from_span(
249        pest::error::ErrorVariant::CustomError {
250            message: String::from("No chord rule found"),
251        },
252        pairs.next().unwrap().as_span(),
253    )))
254}
255
256pub fn map_chord_to_notes(chord: Chord) -> Vec<Note> {
257    match chord {
258        Chord::MinorChord(tonic) => {
259            let minor_third = plus_minor_third(tonic);
260            let perfect_fifth = plus_perfect_fifth(tonic);
261            vec![tonic, minor_third, perfect_fifth]
262        }
263        Chord::MajorChord(tonic) => {
264            let major_third = plus_major_third(tonic);
265            let perfect_fifth = plus_perfect_fifth(tonic);
266            vec![tonic, major_third, perfect_fifth]
267        }
268    }
269}