use std::{
fmt,
ops::{Add, Sub},
str::FromStr,
};
use crate::{Interval, PitchClass, Semitones, StaffPosition};
#[derive(Debug, thiserror::Error)]
#[error("could not parse note name '{name}'")]
pub struct ParseNoteError {
name: String,
}
#[derive(Debug, Clone, Copy, Eq, PartialOrd, Ord)]
pub struct Note {
pub pitch_class: PitchClass,
staff_position: StaffPosition,
}
impl Note {
pub fn new(pitch_class: PitchClass, staff_position: StaffPosition) -> Self {
Self {
pitch_class,
staff_position,
}
}
pub fn is_white_note(&self) -> bool {
use PitchClass::*;
matches!(self.pitch_class, C | D | E | F | G | A | B)
}
}
impl PartialEq for Note {
fn eq(&self, other: &Self) -> bool {
self.pitch_class == other.pitch_class
}
}
impl fmt::Display for Note {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use PitchClass::*;
use StaffPosition::*;
let s = match (self.staff_position, self.pitch_class) {
(CPos, ASharp) => "Bb", (CPos, B) => "B", (CPos, C) => "C",
(CPos, CSharp) => "C#",
(CPos, D) => "D", (DPos, C) => "C", (DPos, CSharp) => "Db",
(DPos, D) => "D",
(DPos, DSharp) => "D#",
(DPos, E) => "E", (EPos, D) => "D", (EPos, DSharp) => "Eb",
(EPos, E) => "E",
(EPos, F) => "F", (EPos, FSharp) => "F#", (FPos, DSharp) => "Eb", (FPos, E) => "E", (FPos, F) => "F",
(FPos, FSharp) => "F#",
(FPos, G) => "G", (GPos, F) => "F", (GPos, FSharp) => "Gb",
(GPos, G) => "G",
(GPos, GSharp) => "G#",
(GPos, A) => "A", (APos, G) => "G", (APos, GSharp) => "Ab",
(APos, A) => "A",
(APos, ASharp) => "A#",
(APos, B) => "B", (BPos, A) => "A", (BPos, ASharp) => "Bb",
(BPos, B) => "B",
(BPos, C) => "C", (BPos, CSharp) => "C#", _ => panic!("Impossible combination of PitchClass and StaffPosition"),
};
write!(f, "{s}")
}
}
impl FromStr for Note {
type Err = ParseNoteError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use PitchClass::*;
use StaffPosition::*;
let name = s.to_string();
let (pitch_class, staff_position) = match s {
"C" => (C, CPos),
"C#" => (CSharp, CPos),
"Db" => (CSharp, DPos),
"D" => (D, DPos),
"D#" => (DSharp, DPos),
"Eb" => (DSharp, EPos),
"E" => (E, EPos),
"F" => (F, FPos),
"F#" => (FSharp, FPos),
"Gb" => (FSharp, GPos),
"G" => (G, GPos),
"G#" => (GSharp, GPos),
"Ab" => (GSharp, APos),
"A" => (A, APos),
"A#" => (ASharp, APos),
"Bb" => (ASharp, BPos),
"B" => (B, BPos),
_ => return Err(ParseNoteError { name }),
};
Ok(Self::new(pitch_class, staff_position))
}
}
impl From<PitchClass> for Note {
fn from(pitch_class: PitchClass) -> Self {
use PitchClass::*;
use StaffPosition::*;
let staff_position = match pitch_class {
C | CSharp => CPos,
D | DSharp => DPos,
E => EPos,
F | FSharp => FPos,
G | GSharp => GPos,
A | ASharp => APos,
B => BPos,
};
Self::new(pitch_class, staff_position)
}
}
impl Add<Interval> for Note {
type Output = Self;
fn add(self, interval: Interval) -> Self {
let pitch_class = self.pitch_class + interval.to_semitones();
let staff_position = self.staff_position + (interval.to_number() - 1);
Self::new(pitch_class, staff_position)
}
}
impl Add<Semitones> for Note {
type Output = Self;
fn add(self, n: Semitones) -> Self {
let note = Self::from(self.pitch_class + n);
if note.pitch_class == self.pitch_class {
return Self::new(self.pitch_class, self.staff_position);
}
note
}
}
impl Sub<Semitones> for Note {
type Output = Self;
fn sub(self, n: Semitones) -> Self {
let note = Self::from(self.pitch_class - n);
if note.pitch_class == self.pitch_class {
return Self::new(self.pitch_class, self.staff_position);
}
let staff_position = match note {
n if n.is_white_note() => note.staff_position,
_ => note.staff_position + 1,
};
Self::new(note.pitch_class, staff_position)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use Interval::*;
use PitchClass::*;
use super::*;
#[rstest(
s,
case("C"),
case("C#"),
case("Db"),
case("D"),
case("D#"),
case("Eb"),
case("E"),
case("F"),
case("F#"),
case("Gb"),
case("G"),
case("G#"),
case("Ab"),
case("A"),
case("A#"),
case("Bb"),
case("B")
)]
fn test_from_and_to_str(s: &str) {
let note = Note::from_str(s).unwrap();
assert_eq!(format!("{note}"), s);
}
#[rstest(
note,
is_white_note,
case("C", true),
case("C#", false),
case("Db", false),
case("D", true),
case("D#", false),
case("Eb", false),
case("E", true),
case("F", true),
case("F#", false),
case("Gb", false),
case("G", true),
case("G#", false),
case("Ab", false),
case("A", true),
case("A#", false),
case("Bb", false),
case("B", true)
)]
fn test_is_white_note(note: Note, is_white_note: bool) {
assert_eq!(note.is_white_note(), is_white_note);
}
#[rstest(
pitch_class,
note,
case(C, "C"),
case(CSharp, "C#"),
case(D, "D"),
case(DSharp, "D#"),
case(E, "E"),
case(F, "F"),
case(FSharp, "F#"),
case(G, "G"),
case(GSharp, "G#"),
case(A, "A"),
case(ASharp, "A#"),
case(B, "B")
)]
fn test_from_pitch_class(pitch_class: PitchClass, note: Note) {
assert_eq!(Note::from(pitch_class), note);
}
#[rstest(
note1,
interval,
note2,
case("C", PerfectUnison, "C"),
case("C", MinorThird, "Eb"),
case("C", MajorThird, "E"),
case("C", PerfectFifth, "G"),
case("C#", PerfectUnison, "C#"),
case("C#", MajorThird, "F")
)]
fn test_add_interval(note1: Note, interval: Interval, note2: Note) {
assert_eq!(note1 + interval, note2);
}
#[rstest(
note1,
n,
note2,
case("C", 0, "C"),
case("C#", 0, "C#"),
case("Db", 0, "Db"),
case("C", 1, "C#"),
case("C#", 1, "D"),
case("Db", 1, "D"),
case("C", 3, "D#"),
case("C", 4, "E"),
case("C", 7, "G"),
case("A", 3, "C"),
case("A", 12, "A"),
case("A#", 12, "A#"),
case("Ab", 12, "Ab")
)]
fn test_add_semitones(note1: Note, n: Semitones, note2: Note) {
assert_eq!(note1 + n, note2);
}
#[rstest(
note1,
n,
note2,
case("C", 0, "C"),
case("C#", 0, "C#"),
case("Db", 0, "Db"),
case("C", 1, "B"),
case("C", 2, "Bb"),
case("C#", 3, "Bb"),
case("Db", 3, "Bb"),
case("A", 3, "Gb"),
case("A", 12, "A")
)]
fn test_subtract_semitones(note1: Note, n: Semitones, note2: Note) {
assert_eq!(note1 - n, note2);
}
}