#![allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct TabNote {
pub string_idx: usize,
pub fret: u8,
}
impl TabNote {
pub fn new(string_idx: usize, fret: u8) -> Self {
Self { string_idx, fret }
}
}
#[derive(Debug, Clone, Default)]
pub struct TabColumn {
pub frets: Vec<Option<u8>>,
pub is_bar_line: bool,
}
impl TabColumn {
pub fn new_beat(num_strings: usize) -> Self {
Self {
frets: vec![None; num_strings],
is_bar_line: false,
}
}
pub fn bar_line(num_strings: usize) -> Self {
Self {
frets: vec![None; num_strings],
is_bar_line: true,
}
}
pub fn set_fret(&mut self, string_idx: usize, fret: u8) {
if string_idx < self.frets.len() {
self.frets[string_idx] = Some(fret);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TabStaff {
pub string_names: Vec<String>,
pub columns: Vec<TabColumn>,
}
impl TabStaff {
pub fn new(string_names: Vec<String>) -> Self {
Self {
string_names,
columns: Vec::new(),
}
}
pub fn guitar_standard() -> Self {
let names = vec!["e", "B", "G", "D", "A", "E"]
.into_iter()
.map(str::to_string)
.collect();
Self::new(names)
}
pub fn add_column(&mut self, col: TabColumn) {
self.columns.push(col);
}
pub fn add_bar_line(&mut self) {
let n = self.string_names.len();
self.columns.push(TabColumn::bar_line(n));
}
pub fn num_strings(&self) -> usize {
self.string_names.len()
}
}
pub fn render_tab_ascii(staff: &TabStaff) -> String {
let mut rows: Vec<String> = staff
.string_names
.iter()
.map(|s| format!("{}-", s))
.collect();
for col in &staff.columns {
if col.is_bar_line {
for row in &mut rows {
row.push('|');
}
} else {
for (i, row) in rows.iter_mut().enumerate() {
match col.frets.get(i).copied().flatten() {
Some(f) => {
if f >= 10 {
row.push_str(&format!("{}", f));
} else {
row.push_str(&format!("{}-", f));
}
}
None => row.push_str("--"),
}
}
}
}
for row in &mut rows {
row.push('|');
}
rows.join("\n") + "\n"
}
pub fn count_tab_beats(staff: &TabStaff) -> usize {
staff.columns.iter().filter(|c| !c.is_bar_line).count()
}
pub fn count_tab_notes(staff: &TabStaff) -> usize {
staff
.columns
.iter()
.flat_map(|c| c.frets.iter())
.filter(|f| f.is_some())
.count()
}
pub fn pentatonic_riff_tab() -> TabStaff {
let mut staff = TabStaff::guitar_standard();
let n = staff.num_strings();
for &fret in &[0u8, 3, 5] {
let mut col = TabColumn::new_beat(n);
col.set_fret(5, fret);
staff.add_column(col);
}
staff.add_bar_line();
for &fret in &[2u8, 5] {
let mut col = TabColumn::new_beat(n);
col.set_fret(4, fret);
staff.add_column(col);
}
staff
}
pub fn tab_has_correct_string_count(src: &str, expected: usize) -> bool {
src.lines().filter(|l| !l.is_empty()).count() == expected
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_guitar_standard_strings() {
let staff = TabStaff::guitar_standard();
assert_eq!(staff.num_strings(), 6 );
}
#[test]
fn test_tab_column_set_fret() {
let mut col = TabColumn::new_beat(6);
col.set_fret(0, 5);
assert_eq!(col.frets[0], Some(5) );
}
#[test]
fn test_tab_column_out_of_bounds() {
let mut col = TabColumn::new_beat(2);
col.set_fret(10, 3);
assert_eq!(col.frets.len(), 2 );
}
#[test]
fn test_render_tab_ascii_lines() {
let staff = TabStaff::guitar_standard();
let rendered = render_tab_ascii(&staff);
assert!(tab_has_correct_string_count(&rendered, 6) );
}
#[test]
fn test_count_tab_beats() {
let staff = pentatonic_riff_tab();
assert_eq!(count_tab_beats(&staff), 5 );
}
#[test]
fn test_count_tab_notes() {
let staff = pentatonic_riff_tab();
assert_eq!(count_tab_notes(&staff), 5 );
}
#[test]
fn test_render_contains_string_name() {
let staff = TabStaff::guitar_standard();
let rendered = render_tab_ascii(&staff);
assert!(rendered.contains('e') );
}
#[test]
fn test_bar_line_renders() {
let mut staff = TabStaff::guitar_standard();
staff.add_bar_line();
let rendered = render_tab_ascii(&staff);
assert!(rendered.contains('|') );
}
#[test]
fn test_pentatonic_riff_has_bar_line() {
let staff = pentatonic_riff_tab();
let has_bar = staff.columns.iter().any(|c| c.is_bar_line);
assert!(has_bar );
}
}