ireal_parser/
chord.rs

1use std::{fmt, str::FromStr};
2
3use crate::{Error, Result};
4
5/// Represents an iReal Pro note
6#[derive(Debug, PartialEq, Eq, Copy, Clone)]
7pub enum Note {
8    C,
9    Csharp,
10    Dflat,
11    D,
12    Dsharp,
13    Eflat,
14    E,
15    F,
16    Fsharp,
17    Gflat,
18    G,
19    Gsharp,
20    Aflat,
21    A,
22    Asharp,
23    Bflat,
24    B,
25}
26
27impl fmt::Display for Note {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        use Note::*;
30
31        let s = match self {
32            C => "C",
33            Csharp => "C#",
34            Dflat => "Db",
35            D => "D",
36            Dsharp => "D#",
37            Eflat => "Eb",
38            E => "E",
39            F => "F",
40            Fsharp => "F#",
41            Gflat => "Gb",
42            G => "G",
43            Gsharp => "G#",
44            Aflat => "Ab",
45            A => "A",
46            Asharp => "A#",
47            Bflat => "Bb",
48            B => "B",
49        };
50
51        write!(f, "{s}")
52    }
53}
54
55impl FromStr for Note {
56    type Err = Error;
57
58    fn from_str(s: &str) -> Result<Self> {
59        use Note::*;
60
61        let note = match s {
62            "C" => C,
63            "C#" => Csharp,
64            "Db" => Dflat,
65            "D" => D,
66            "D#" => Dsharp,
67            "Eb" => Eflat,
68            "E" => E,
69            "F" => F,
70            "F#" => Fsharp,
71            "Gb" => Gflat,
72            "G" => G,
73            "G#" => Gsharp,
74            "Ab" => Gflat,
75            "A" => A,
76            "A#" => Asharp,
77            "Bb" => Bflat,
78            "B" => B,
79            _ => return Err(Error::InvalidNote),
80        };
81
82        Ok(note)
83    }
84}
85
86/// Represents an iReal Pro chord quality
87#[derive(Debug, PartialEq, Eq, Copy, Clone)]
88pub enum ChordQuality {
89    Fifth,
90    Second,
91    Add9,
92    Aug,
93    Dim,
94    Half,
95    Sus,
96    Maj,
97    Min,
98    Maj7,
99    Min7,
100    Dom7,
101    Dom7sus,
102    Half7,
103    Dim7,
104    Maj9,
105    Maj13,
106    Sixth,
107    Sixth9,
108    Maj7Sharp11,
109    Maj9Sharp11,
110    Maj7Sharp5,
111    Min6,
112    Min69,
113    MinMaj7,
114    MinMaj9,
115    Min9,
116    Min11,
117    Min7Flat5,
118    Half9,
119    MinFlat6,
120    MinSharp5,
121    Ninth,
122    Dom7Flat9,
123    Dom7Sharp9,
124    Dom7Sharp11,
125    Dom7Flat5,
126    Dom7Sharp5,
127    NinthSharp11,
128    NinthFlat5,
129    NinthSharp5,
130    Dom7Flat13,
131    Dom7Sharp9Sharp5,
132    Dom7Sharp9Flat5,
133    Dom7Sharp9Sharp11,
134    Dom7Flat9Sharp11,
135    Dom7Flat9Flat5,
136    Dom7Flat9Sharp5,
137    Dom7Flat9Sharp9,
138    Dom7Flat9Flat13,
139    Dom7Alt,
140    Dom13,
141    Dom13Sharp11,
142    Dom13Flat9,
143    Dom13Sharp9,
144    Dom7Flat9sus,
145    Dom7susAdd3,
146    Dom9sus,
147    Dom13sus,
148    Dom7Flat13sus,
149    Dom11,
150}
151
152impl fmt::Display for ChordQuality {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        use ChordQuality::*;
155
156        let s = match self {
157            Fifth => "5",
158            Second => "2",
159            Add9 => "add9",
160            Aug => "+",
161            Dim => "o",
162            Half => "h",
163            Sus => "sus",
164            Maj => "^",
165            Min => "-",
166            Maj7 => "^7",
167            Min7 => "-7",
168            Dom7 => "7",
169            Dom7sus => "7sus",
170            Half7 => "h7",
171            Dim7 => "o7",
172            Maj9 => "^9",
173            Maj13 => "^13",
174            Sixth => "6",
175            Sixth9 => "69",
176            Maj7Sharp11 => "^7#11",
177            Maj9Sharp11 => "^9#11",
178            Maj7Sharp5 => "^7#5",
179            Min6 => "-6",
180            Min69 => "-69",
181            MinMaj7 => "-^7",
182            MinMaj9 => "-^9",
183            Min9 => "-9",
184            Min11 => "-11",
185            Min7Flat5 => "-7b5",
186            Half9 => "h9",
187            MinFlat6 => "-b6",
188            MinSharp5 => "-#5",
189            Ninth => "9",
190            Dom7Flat9 => "7b9",
191            Dom7Sharp9 => "7#9",
192            Dom7Sharp11 => "7#11",
193            Dom7Flat5 => "7b5",
194            Dom7Sharp5 => "7#5",
195            NinthSharp11 => "9#11",
196            NinthFlat5 => "9b5",
197            NinthSharp5 => "9#5",
198            Dom7Flat13 => "7b13",
199            Dom7Sharp9Sharp5 => "7#9#5",
200            Dom7Sharp9Flat5 => "7#9b5",
201            Dom7Sharp9Sharp11 => "7#9#11",
202            Dom7Flat9Sharp11 => "7b9#11",
203            Dom7Flat9Flat5 => "7b9b5",
204            Dom7Flat9Sharp5 => "7b9#5",
205            Dom7Flat9Sharp9 => "7b9#9",
206            Dom7Flat9Flat13 => "7b9b13",
207            Dom7Alt => "7alt",
208            Dom13 => "13",
209            Dom13Sharp11 => "13#11",
210            Dom13Flat9 => "13b9",
211            Dom13Sharp9 => "13#9",
212            Dom7Flat9sus => "7b9sus",
213            Dom7susAdd3 => "7susadd3",
214            Dom9sus => "9sus",
215            Dom13sus => "13sus",
216            Dom7Flat13sus => "7b13sus",
217            Dom11 => "11",
218        };
219
220        write!(f, "{s}")
221    }
222}
223
224impl FromStr for ChordQuality {
225    type Err = Error;
226
227    fn from_str(s: &str) -> Result<Self> {
228        use ChordQuality::*;
229
230        let q = match s {
231            "5" => Fifth,
232            "2" => Second,
233            "add9" => Add9,
234            "+" => Aug,
235            "o" => Dim,
236            "h" => Half,
237            "sus" => Sus,
238            "^" => Maj,
239            "-" => Min,
240            "^7" => Maj7,
241            "-7" => Min7,
242            "7" => Dom7,
243            "7sus" => Dom7sus,
244            "h7" => Half7,
245            "o7" => Dim7,
246            "^9" => Maj9,
247            "^13" => Maj13,
248            "6" => Sixth,
249            "69" => Sixth9,
250            "^7#11" => Maj7Sharp11,
251            "^9#11" => Maj9Sharp11,
252            "^7#5" => Maj7Sharp5,
253            "-6" => Min6,
254            "-69" => Min69,
255            "-^7" => MinMaj7,
256            "-^9" => MinMaj9,
257            "-9" => Min9,
258            "-11" => Min11,
259            "-7b5" => Min7Flat5,
260            "h9" => Half9,
261            "-b6" => MinFlat6,
262            "-#5" => MinSharp5,
263            "9" => Ninth,
264            "7b9" => Dom7Flat9,
265            "7#9" => Dom7Sharp9,
266            "7#11" => Dom7Sharp11,
267            "7b5" => Dom7Flat5,
268            "7#5" => Dom7Sharp5,
269            "9#11" => NinthSharp11,
270            "9b5" => NinthFlat5,
271            "9#5" => NinthSharp5,
272            "7b13" => Dom7Flat13,
273            "7#9#5" => Dom7Sharp9Sharp5,
274            "7#9b5" => Dom7Sharp9Flat5,
275            "7#9#11" => Dom7Sharp9Sharp11,
276            "7b9#11" => Dom7Flat9Sharp11,
277            "7b9b5" => Dom7Flat9Flat5,
278            "7b9#5" => Dom7Flat9Sharp5,
279            "7b9#9" => Dom7Flat9Sharp9,
280            "7b9b13" => Dom7Flat9Flat13,
281            "7alt" => Dom7Alt,
282            "13" => Dom13,
283            "13#11" => Dom13Sharp11,
284            "13b9" => Dom13Flat9,
285            "13#9" => Dom13Sharp9,
286            "7b9sus" => Dom7Flat9sus,
287            "7susadd3" => Dom7susAdd3,
288            "9sus" => Dom9sus,
289            "13sus" => Dom13sus,
290            "7b13sus" => Dom7Flat13sus,
291            "11" => Dom11,
292            _ => return Err(Error::InvalidChordQuality),
293        };
294
295        Ok(q)
296    }
297}
298
299/// Represents an iReal Pro chord
300///
301/// Chord symbol format: Root + an optional chord quality + an optional inversion
302///
303/// For example just a root:
304/// `C`
305///
306/// or a root plus a chord quality:
307/// `C-7`
308///
309/// or a root plus in inversion inversion:
310/// `C/E`
311///
312/// or a root plus a quality plus an inversion:
313/// `C-7/Bb`
314///
315/// All valid roots and inversions: `C, C#, Db, D, D#, Eb, E, F, F#, Gb, G, G#,
316/// Ab, A, A#, Bb, B`
317///
318/// All valid qualities: `5, 2, add9, +, o, h, sus, ^, -, ^7, -7, 7, 7sus, h7,
319/// o7, ^9, ^13, 6, 69, ^7#11, ^9#11, ^7#5, -6, -69, -^7, -^9, -9, -11, -7b5,
320/// h9, -b6, -#5, 9, 7b9, 7#9, 7#11, 7b5, 7#5, 9#11, 9b5, 9#5, 7b13, 7#9#5,
321/// 7#9b5, 7#9#11, 7b9#11, 7b9b5, 7b9#5, 7b9#9, 7b9b13, 7alt, 13, 13#11, 13b9,
322/// 13#9, 7b9sus, 7susadd3, 9sus, 13sus, 7b13sus, 11`
323#[derive(Debug, PartialEq, Eq, Copy, Clone)]
324pub struct Chord {
325    pub root: Note,
326    pub quality: Option<ChordQuality>,
327    pub inversion: Option<Note>,
328}
329
330impl Chord {
331    pub fn new(root: Note) -> Self {
332        Self {
333            root,
334            quality: None,
335            inversion: None,
336        }
337    }
338
339    pub fn set_quality(&mut self, quality: Option<ChordQuality>) {
340        self.quality = quality;
341    }
342
343    pub fn set_inversion(&mut self, inversion: Option<Note>) {
344        self.inversion = inversion;
345    }
346}
347
348impl fmt::Display for Chord {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        let root = self.root;
351        let quality = self.quality.map(|q| q.to_string()).unwrap_or_default();
352        let inversion = self.inversion.map(|i| format!("/{i}")).unwrap_or_default();
353
354        write!(f, "{root}{quality}{inversion}")
355    }
356}
357
358impl FromStr for Chord {
359    type Err = Error;
360
361    fn from_str(s: &str) -> Result<Self> {
362        let (chord, inv) = s
363            .split_once('/')
364            .map(|(c, i)| (c, Some(i)))
365            .unwrap_or((s, None));
366
367        let ext = chord
368            .chars()
369            .nth(1)
370            .map(|c| c == '#' || c == 'b')
371            .unwrap_or(false);
372        let (note, quality) = chord.split_at(if ext { 2 } else { 1 });
373
374        let mut c = Chord::new(note.parse()?);
375        if !quality.is_empty() {
376            c.set_quality(Some(quality.parse()?));
377        }
378        if let Some(inv) = inv {
379            c.set_inversion(Some(inv.parse()?));
380        }
381
382        Ok(c)
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_to_string() {
392        let mut chord = Chord::new(Note::Dsharp);
393        chord.set_quality(Some(ChordQuality::Min7Flat5));
394        chord.set_inversion(Some(Note::Bflat));
395        assert_eq!(chord.to_string(), "D#-7b5/Bb");
396
397        let c: Chord = "D#-7b5/Bb".parse().unwrap();
398        assert_eq!(c, chord);
399    }
400}