use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use crate::{Accidental, Alteration, Interval, Key, Step};
#[derive(Clone, Copy, PartialOrd, Ord, Eq, Debug, PartialEq, FromPrimitive)]
#[must_use]
#[rustfmt::skip]
#[allow(missing_docs)]
pub enum Tpc {
Fbb = -15,
Cbb, Gbb, Dbb, Abb, Ebb, Bbb,
Fb, Cb, Gb, Db, Ab, Eb, Bb,
F, C, G, D, A, E, B,
Fs, Cs, Gs, Ds, As, Es, Bs,
Fss, Css, Gss, Dss, Ass, Ess, Bss,
}
impl Tpc {
pub const MAX: Tpc = Tpc::Bss;
pub const MIN: Tpc = Tpc::Fbb;
const DELTA_SEMITONE: i8 = 7;
const DELTA_ENHARMONIC: i8 = 12;
pub fn step(self) -> Step {
match (self as i8).rem_euclid(7) {
0 => Step::C,
1 => Step::G,
2 => Step::D,
3 => Step::A,
4 => Step::E,
5 => Step::B,
_ => Step::F,
}
}
#[must_use]
pub fn alteration(self, key: Key) -> Alteration {
let tpc = self as i8;
let key = key as i8;
(tpc - key - Self::MIN as i8 + Key::MAX as i8) / Self::DELTA_SEMITONE - 3
}
fn accidental(self) -> Accidental {
match (self as i8 + 1).div_euclid(7) {
-2 => Accidental::DblFlat,
-1 => Accidental::Flat,
0 => Accidental::Natural,
1 => Accidental::Sharp,
2 => Accidental::DblSharp,
_ => unreachable!("Tpc out of range"),
}
}
pub fn altered_step(self, key: Option<Key>) -> (Step, Option<Accidental>) {
let key = key.unwrap_or_default();
let step = self.step();
if step.with_key(key) == self {
(step, None)
} else {
(step, Some(self.accidental()))
}
}
#[must_use]
pub fn alter(self, by: Alteration) -> Option<Tpc> {
let new = self as i8 + by * Self::DELTA_SEMITONE;
num_traits::FromPrimitive::from_i8(new)
}
#[must_use]
pub fn enharmonic(self, other: Tpc) -> bool {
(self as i8 - other as i8) % Self::DELTA_ENHARMONIC == 0
}
}
impl std::ops::Add<Interval> for Tpc {
type Output = Option<Tpc>;
fn add(self, rhs: Interval) -> Self::Output {
let value = self as i8 + rhs as i8;
FromPrimitive::from_i8(value)
}
}
impl std::ops::Sub<Interval> for Tpc {
type Output = Option<Tpc>;
fn sub(self, rhs: Interval) -> Self::Output {
let value = self as i8 - rhs as i8;
FromPrimitive::from_i8(value)
}
}
impl std::ops::Sub<Tpc> for Tpc {
type Output = Option<Interval>;
fn sub(self, rhs: Tpc) -> Self::Output {
FromPrimitive::from_i8(rhs as i8 - self as i8)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_enharmonic_transpose() {
let c = Tpc::C;
let enharmonic = c as i8 + Tpc::DELTA_ENHARMONIC;
let enharmonic: Tpc = num_traits::FromPrimitive::from_i8(enharmonic).unwrap();
assert_eq!(enharmonic, Tpc::Bs);
}
#[test]
fn test_to_alter_with_key() {
assert_eq!(0, Tpc::A.alteration(Key::C));
assert_eq!(1, Tpc::Fs.alteration(Key::C));
assert_eq!(-1, Tpc::C.alteration(Key::D));
assert_eq!(-3, Tpc::Fbb.alteration(Key::Cs));
assert_eq!(0, Tpc::Eb.alteration(Key::Bb));
}
#[test]
fn test_to_accidental() {
assert_eq!(Accidental::DblFlat, Tpc::Fbb.accidental());
assert_eq!(Accidental::DblFlat, Tpc::Bbb.accidental());
assert_eq!(Accidental::Flat, Tpc::Cb.accidental());
assert_eq!(Accidental::Flat, Tpc::Eb.accidental());
assert_eq!(Accidental::Natural, Tpc::G.accidental());
assert_eq!(Accidental::Natural, Tpc::A.accidental());
assert_eq!(Accidental::Sharp, Tpc::Cs.accidental());
assert_eq!(Accidental::Sharp, Tpc::Es.accidental());
assert_eq!(Accidental::DblSharp, Tpc::Fss.accidental());
assert_eq!(Accidental::DblSharp, Tpc::Bss.accidental());
}
#[test]
fn add_interval() {
assert_eq!(Some(Tpc::E), Tpc::C + Interval::Maj3);
assert_eq!(Some(Tpc::C), Tpc::Fs + Interval::Dim5);
assert_eq!(None, Tpc::Fbb + Interval::Dim5);
assert_eq!(None, Tpc::Dss + Interval::Maj3);
}
}