use std::fmt;
use std::ops;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Notation {
#[serde(alias = "western")]
#[serde(alias = "dutch")]
English,
#[serde(alias = "czech")]
German,
Nashville,
Roman,
}
#[allow(clippy::derivable_impls)] impl Default for Notation {
fn default() -> Notation {
Notation::English
}
}
impl FromStr for Notation {
type Err = ();
fn from_str(s: &str) -> Result<Notation, ()> {
use self::Notation::*;
let lower = s.to_ascii_lowercase();
match lower.as_str() {
"english" | "western" | "dutch" => Ok(English),
"german" | "czech" => Ok(German),
"nashville" => Ok(Nashville),
"roman" => Ok(Roman),
_ => Err(()),
}
}
}
impl fmt::Display for Notation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
Notation::English => "english",
Notation::German => "german",
Notation::Nashville => "nashville",
Notation::Roman => "roman",
};
write!(f, "{}", name)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub struct Chromatic(u8);
macro_rules! impl_from {
(u $t:ty) => {
impl From<$t> for Chromatic {
fn from(u: $t) -> Chromatic {
Chromatic((u % 12) as u8)
}
}
};
(i $t:ty) => {
impl From<$t> for Chromatic {
fn from(i: $t) -> Chromatic {
Chromatic(i.rem_euclid(12) as u8)
}
}
};
}
macro_rules! impl_into {
($t:ty) => {
impl From<Chromatic> for $t {
fn from(c: Chromatic) -> $t {
c.0 as _
}
}
};
}
impl_from!(u u8);
impl_from!(u u16);
impl_from!(u u32);
impl_from!(u u64);
impl_from!(u usize);
impl_from!(i i8);
impl_from!(i i16);
impl_from!(i i32);
impl_from!(i i64);
impl_from!(i isize);
impl_into!(u8);
impl_into!(u16);
impl_into!(u32);
impl_into!(u64);
impl_into!(usize);
impl_into!(i8);
impl_into!(i16);
impl_into!(i32);
impl_into!(i64);
impl_into!(isize);
impl FromStr for Chromatic {
type Err = ();
fn from_str(s: &str) -> Result<Chromatic, ()> {
match i32::from_str(s) {
Ok(i) => Ok(i.into()),
Err(_) => Err(()),
}
}
}
impl ops::Add for Chromatic {
type Output = Chromatic;
fn add(self, other: Chromatic) -> Chromatic {
Chromatic((self.0 + other.0) % 12)
}
}
impl ops::AddAssign for Chromatic {
fn add_assign(&mut self, other: Chromatic) {
*self = *self + other;
}
}
impl ops::Sub for Chromatic {
type Output = Chromatic;
fn sub(self, other: Chromatic) -> Chromatic {
Chromatic((12 + self.0 - other.0) % 12)
}
}
impl ops::SubAssign for Chromatic {
fn sub_assign(&mut self, other: Chromatic) {
*self = *self - other;
}
}
impl Chromatic {
pub fn new(i: i32) -> Chromatic {
i.into()
}
pub fn num(&self) -> u8 {
self.0
}
fn parse_halftone(from: &str, base: Chromatic, base_size: usize) -> (Chromatic, usize) {
let c = from[base_size..].chars().next();
let (delta, size) = match c {
Some('b') => (-1, 1),
Some('♭') => (-1, '♭'.len_utf8()),
Some('#') => (1, 1),
Some('♯') => (1, '♯'.len_utf8()),
Some(_) | None => (0, 0),
};
(base + delta.into(), base_size + size)
}
fn parse_western(from: &str, german: bool) -> Option<(Chromatic, usize)> {
let base = match from.chars().next().unwrap() {
'C' | 'c' => 0,
'D' | 'd' => 2,
'E' | 'e' => 4,
'F' | 'f' => 5,
'G' | 'g' => 7,
'A' | 'a' => 9,
'B' | 'b' if !german => 11,
'B' | 'b' if german => 10,
'H' | 'h' if german => 11,
_ => return None,
};
Some(Self::parse_halftone(from, base.into(), 1))
}
fn parse_nashvile(from: &str) -> Option<(Chromatic, usize)> {
let base = match from.chars().next().unwrap() {
'1' => 0,
'2' => 2,
'3' => 4,
'4' => 5,
'5' => 7,
'6' => 9,
'7' => 11,
_ => return None,
};
Some(Self::parse_halftone(from, base.into(), 1))
}
fn parse_roman(from: &str) -> Option<(Chromatic, usize)> {
let mut chars = from.chars();
let c1 = chars.next().map(|c| c.to_ascii_uppercase()).unwrap();
let c2 = chars.next().map(|c| c.to_ascii_uppercase());
let c3 = chars.next().map(|c| c.to_ascii_uppercase());
let (base, base_size) = match (c1, c2, c3) {
('I', Some('I'), Some('I')) => (4, 3),
('V', Some('I'), Some('I')) => (11, 3),
('I', Some('I'), _) => (2, 2),
('I', Some('V'), _) => (5, 2),
('V', Some('I'), _) => (9, 2),
('I', _, _) => (0, 1),
('V', _, _) => (7, 1),
_ => return None,
};
Some(Self::parse_halftone(from, base.into(), base_size))
}
pub fn parse_span(from: &str, notation: Notation) -> Option<(Chromatic, usize)> {
use self::Notation::*;
if from.is_empty() {
None
} else {
match notation {
English => Self::parse_western(from, false),
German => Self::parse_western(from, true),
Nashville => Self::parse_nashvile(from),
Roman => Self::parse_roman(from),
}
}
}
pub fn parse(from: &str, notation: Notation) -> Option<Chromatic> {
Self::parse_span(from, notation).map(|(chromatic, _)| chromatic)
}
fn as_str_western(&self, german: bool, uppercase: bool) -> &'static str {
const TONES_UPPER: &[&str] = &[
"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B",
];
const TONES_UPPER_G: &[&str] = &[
"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "B", "H",
];
const TONES_LOWER: &[&str] = &[
"c", "c#", "d", "eb", "e", "f", "f#", "g", "ab", "a", "bb", "b",
];
const TONES_LOWER_G: &[&str] = &[
"c", "c#", "d", "eb", "e", "f", "f#", "g", "ab", "a", "b", "h",
];
let i = self.0 as usize;
match (german, uppercase) {
(false, true) => TONES_UPPER[i],
(true, true) => TONES_UPPER_G[i],
(false, false) => TONES_LOWER[i],
(true, false) => TONES_LOWER_G[i],
}
}
fn as_str_nashville(&self) -> &'static str {
match self.0 {
0 => "1",
1 => "1#",
2 => "2",
3 => "3b",
4 => "3",
5 => "4",
6 => "4#",
7 => "5",
8 => "6b",
9 => "6",
10 => "7b",
11 => "7",
_ => unreachable!(),
}
}
fn as_str_roman(&self, uppercase: bool) -> &'static str {
const TONES_UPPER: &[&str] = &[
"I", "I#", "II", "IIIb", "III", "IV", "IV#", "V", "VIb", "VI", "VIIb", "VII",
];
const TONES_LOWER: &[&str] = &[
"i", "i#", "ii", "iiib", "iii", "iv", "iv#", "v", "vib", "vi", "viib", "vii",
];
let i = self.0 as usize;
if uppercase {
TONES_UPPER[i]
} else {
TONES_LOWER[i]
}
}
fn as_str(&self, notation: Notation, uppercase: bool) -> &'static str {
use self::Notation::*;
match notation {
English => self.as_str_western(false, uppercase),
German => self.as_str_western(true, uppercase),
Nashville => self.as_str_nashville(),
Roman => self.as_str_roman(uppercase),
}
}
pub fn transposed<C>(self, by: C) -> Chromatic
where
C: Into<Chromatic>,
{
self + by.into()
}
}
impl fmt::Display for Chromatic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.as_str(Notation::English, true))
}
}
#[derive(Debug)]
struct Chord<'s> {
base: Chromatic,
uppercase: bool,
suffix: &'s str,
}
impl<'s> Chord<'s> {
fn parse(src: &'s str, notation: Notation) -> Result<Self, &'s str> {
let (base, base_size) = Chromatic::parse_span(src, notation).ok_or(src)?;
Ok(Self {
base,
uppercase: src.chars().next().unwrap().is_uppercase(),
suffix: &src[base_size..],
})
}
fn transposed(self, by: impl Into<Chromatic>) -> Self {
Self {
base: self.base.transposed(by),
uppercase: self.uppercase,
suffix: self.suffix,
}
}
fn str_len(&self, notation: Notation) -> usize {
self.base.as_str(notation, self.uppercase).len() + self.suffix.len()
}
fn write_string(&self, mut to: String, notation: Notation) -> String {
let base = self.base.as_str(notation, self.uppercase);
to.push_str(base);
to.push_str(self.suffix);
to
}
}
fn is_chord_separator(c: char) -> bool {
match c {
'/' | ',' | '\\' | '|' => true,
c if c.is_whitespace() => true,
_ => false,
}
}
#[derive(Debug)]
struct ChordIter<'s> {
rest: &'s str,
notation: Notation,
}
impl<'s> ChordIter<'s> {
fn new(src: &'s str, src_notation: Notation) -> Self {
Self {
rest: src,
notation: src_notation,
}
}
}
impl<'s> Iterator for ChordIter<'s> {
type Item = Result<Chord<'s>, &'s str>;
fn next(&mut self) -> Option<Self::Item> {
if self.rest.is_empty() {
return None;
}
let mut split_found = false;
let split = self
.rest
.find(|c| {
if !split_found {
split_found = is_chord_separator(c);
false
} else {
!is_chord_separator(c)
}
})
.unwrap_or(self.rest.len());
let (next, rest) = self.rest.split_at(split);
self.rest = rest;
Some(Chord::parse(next, self.notation))
}
}
pub fn transpose(
chord_set: &str,
by: impl Into<Chromatic>,
src_notation: Notation,
to_notation: Notation,
) -> Result<String, &str> {
let by = by.into();
let prefix_at = chord_set
.find(|c: char| !is_chord_separator(c))
.unwrap_or(0);
let (prefix, rest) = chord_set.split_at(prefix_at);
let mut transposed_len = prefix.len();
for chord in ChordIter::new(rest, src_notation) {
transposed_len += chord?.transposed(by).str_len(to_notation);
}
let mut res = String::with_capacity(transposed_len);
res.push_str(prefix);
Ok(ChordIter::new(rest, src_notation).fold(res, |res, chord| {
chord.unwrap().transposed(by).write_string(res, to_notation)
}))
}
#[cfg(test)]
mod tests {
use super::*;
use Notation::*;
#[test]
fn notation_parse() {
let names = [
"english",
"western",
"dutch",
"german",
"nashville",
"roman",
];
let expected = vec![English, English, English, German, Nashville, Roman];
let parsed: Vec<_> = names
.iter()
.map(|s| Notation::from_str(*s).unwrap())
.collect();
assert_eq!(&parsed, &expected);
let mut json: String = "[".to_owned();
for name in &names {
json.push('"');
json.push_str(name);
json.push_str("\",");
}
json.pop(); json.push(']');
let parsed: Vec<Notation> = serde_json::from_str(&json).unwrap();
assert_eq!(&parsed, &expected);
let json_expected = r#"["english","english","english","german","nashville","roman"]"#;
let serialized = serde_json::to_string(&parsed).unwrap();
assert_eq!(serialized, json_expected);
}
#[test]
fn chromatic_basic() {
let c = Chromatic::parse("C", English).unwrap();
assert_eq!(format!("{}", c), "C");
assert_eq!(Chromatic::parse("", English), None);
assert_eq!(Chromatic::parse("", German), None);
assert_eq!(Chromatic::parse("", Nashville), None);
assert_eq!(Chromatic::parse("", Roman), None);
}
#[test]
fn chromatic_transposition() {
let c: Chromatic = 0.into();
let transposed = c.transposed(-1);
assert_eq!(transposed.as_str(German, false), "h");
let transposed = c.transposed(3);
assert_eq!(transposed.as_str(German, true), "Eb");
}
#[test]
fn chromatic_english() {
let c: Chromatic = 0.into();
let fsharp: Chromatic = 6.into();
assert_eq!(Chromatic::parse("C", English).unwrap(), c);
assert_eq!(Chromatic::parse("F#", English).unwrap(), fsharp);
assert_eq!(Chromatic::parse("F♯", English).unwrap(), fsharp);
assert_eq!(Chromatic::parse("Gb", English).unwrap(), fsharp);
assert_eq!(Chromatic::parse("G♭", English).unwrap(), fsharp);
assert_eq!(Chromatic::parse("X", English), None);
}
#[test]
fn chromatic_german() {
let c: Chromatic = 0.into();
let fsharp: Chromatic = 6.into();
assert_eq!(Chromatic::parse("C", German).unwrap(), c);
assert_eq!(
Chromatic::parse("H", German).unwrap(),
Chromatic::parse("B", English).unwrap()
);
assert_eq!(
Chromatic::parse("B", German).unwrap(),
Chromatic::parse("Bb", English).unwrap()
);
assert_eq!(Chromatic::parse("F#", German).unwrap(), fsharp);
assert_eq!(Chromatic::parse("F♯", German).unwrap(), fsharp);
assert_eq!(Chromatic::parse("Gb", German).unwrap(), fsharp);
assert_eq!(Chromatic::parse("G♭", German).unwrap(), fsharp);
assert_eq!(Chromatic::parse("X", German), None);
}
#[test]
fn chromatic_nashville() {
let c: Chromatic = 0.into();
let fsharp: Chromatic = 6.into();
assert_eq!(Chromatic::parse("1", Nashville).unwrap(), c);
assert_eq!(
Chromatic::parse("2", Nashville).unwrap(),
Chromatic::parse("D", German).unwrap()
);
assert_eq!(Chromatic::parse("4#", Nashville).unwrap(), fsharp);
assert_eq!(Chromatic::parse("4♯", Nashville).unwrap(), fsharp);
assert_eq!(Chromatic::parse("5b", Nashville).unwrap(), fsharp);
assert_eq!(Chromatic::parse("5♭", Nashville).unwrap(), fsharp);
assert_eq!(Chromatic::parse("0", Nashville), None);
assert_eq!(Chromatic::parse("8", Nashville), None);
assert_eq!(Chromatic::parse("X", Nashville), None);
}
#[test]
fn chromatic_roman() {
let c: Chromatic = 0.into();
let fsharp: Chromatic = 6.into();
assert_eq!(Chromatic::parse("I", Roman).unwrap(), c);
assert_eq!(
Chromatic::parse("II", Roman).unwrap(),
Chromatic::parse("D", German).unwrap()
);
assert_eq!(Chromatic::parse("IV#", Roman).unwrap(), fsharp);
assert_eq!(Chromatic::parse("IV♯", Roman).unwrap(), fsharp);
assert_eq!(Chromatic::parse("Vb", Roman).unwrap(), fsharp);
assert_eq!(Chromatic::parse("V♭", Roman).unwrap(), fsharp);
assert_eq!(Chromatic::parse("C", Roman), None);
assert_eq!(Chromatic::parse("X", Roman), None);
}
#[test]
fn chromatic_span() {
assert_eq!(Chromatic::parse_span("A", English).unwrap().1, 1);
assert_eq!(Chromatic::parse_span("D#", English).unwrap().1, 2);
assert_eq!(Chromatic::parse_span("H#", German).unwrap().1, 2);
assert_eq!(Chromatic::parse_span("1#", Nashville).unwrap().1, 2);
}
#[test]
fn transpose_basic() {
let t = transpose("C", 2, English, English).unwrap();
assert_eq!(t, "D");
}
#[test]
fn transpose_multiple() {
let t = transpose("C/D,E", 2, English, English).unwrap();
assert_eq!(t, "D/E,F#");
let t = transpose("C / D , E", 2, English, English).unwrap();
assert_eq!(t, "D / E , F#");
}
#[test]
fn transpose_suffixes() {
let t = transpose("Cm/D°,Emaj7", 2, English, English).unwrap();
assert_eq!(t, "Dm/E°,F#maj7");
}
#[test]
fn transpose_multiple_separators() {
let t = transpose("C/|\\/D,, ,,E,,,", 2, English, English).unwrap();
assert_eq!(t, "D/|\\/E,, ,,F#,,,");
}
#[test]
fn transpose_leading_separators() {
let t = transpose(",C", 2, English, English).unwrap();
assert_eq!(t, ",D");
}
#[test]
fn transpose_whitespace() {
let t = transpose(" /C ", 2, English, English).unwrap();
assert_eq!(t, " /D ");
}
#[test]
fn transpose_german() {
let t = transpose("H/B", 0, German, English).unwrap();
assert_eq!(t, "B/Bb");
}
#[test]
fn transpose_roman() {
let t = transpose("C/D,E", 5, English, Roman).unwrap();
assert_eq!(t, "IV/V,VI");
let t = transpose("C/D,E", 5, English, Roman).unwrap();
assert_eq!(t, "IV/V,VI");
}
#[test]
fn transpose_nashville() {
let t = transpose("I/II,III", 0, Roman, Nashville).unwrap();
assert_eq!(t, "1/2,3");
}
#[test]
fn transpose_lowercase() {
let t = transpose("c", 2, English, Roman).unwrap();
assert_eq!(t, "ii");
let t = transpose("c,d,e,", 2, English, Roman).unwrap();
assert_eq!(t, "ii,iii,iv#,");
}
}