use super::{KeySpan, lines};
pub(crate) const COMMA: char = ',';
pub(crate) const TAB: char = '\t';
pub(crate) fn keys(text: &str, delimiter: char) -> Vec<KeySpan> {
let mut spans = Vec::new();
for (row, (offset, line)) in lines(text).enumerate() {
if line.trim().is_empty() {
continue;
}
for (column, cell) in cells(line, delimiter).into_iter().enumerate() {
spans.push(KeySpan {
start: offset + cell.start,
end: offset + cell.end,
path: format!("[{row}][{column}]"),
});
}
}
spans
}
struct Cell {
start: usize,
end: usize,
}
fn cells(line: &str, delimiter: char) -> Vec<Cell> {
let mut start = 0;
let mut quoted = false;
let mut boundaries = Vec::new();
for (index, character) in line.char_indices() {
if character == '"' {
quoted = !quoted;
continue;
}
if character == delimiter && !quoted {
boundaries.push(Cell { start, end: index });
start = index + 1;
}
}
boundaries.push(Cell {
start,
end: line.len(),
});
boundaries
}
#[cfg(test)]
mod tests {
use super::*;
fn at(text: &str, needle: &str) -> Option<String> {
let at = text.find(needle).expect("the needle is in the text");
keys(text, COMMA)
.into_iter()
.find(|span| span.start <= at && at < span.end)
.map(|span| span.path)
}
#[test]
fn a_cell_is_named_by_its_coordinate() {
let text = "host,ip\nweb,10.0.0.5\n";
assert_eq!(at(text, "10.0.0.5").as_deref(), Some("[1][1]"));
}
#[test]
fn the_first_row_is_data_like_any_other() {
assert_eq!(at("10.0.0.1,x\n", "10.0.0.1").as_deref(), Some("[0][0]"));
}
#[test]
fn a_quoted_cell_may_hold_a_comma() {
let text = "a,\"10.0.0.1,10.0.0.2\",b\n";
assert_eq!(at(text, "10.0.0.1").as_deref(), Some("[0][1]"));
assert_eq!(at(text, "10.0.0.2").as_deref(), Some("[0][1]"));
}
#[test]
fn a_ragged_row_is_not_a_failure() {
let text = "a,b,c\n10.0.0.1\n";
assert_eq!(at(text, "10.0.0.1").as_deref(), Some("[1][0]"));
}
#[test]
fn a_tab_row_is_cells_under_tab_and_one_cell_under_comma() {
let text = "host\tmgmt\tgw\nweb01\t10.0.0.1\t10.0.0.254\n";
let tabbed: Vec<String> = keys(text, TAB).iter().map(|s| s.path.clone()).collect();
assert_eq!(
tabbed,
["[0][0]", "[0][1]", "[0][2]", "[1][0]", "[1][1]", "[1][2]"]
);
let comma: Vec<String> = keys(text, COMMA).iter().map(|s| s.path.clone()).collect();
assert_eq!(comma, ["[0][0]", "[1][0]"]);
}
#[test]
fn a_blank_line_has_no_cells() {
assert!(keys("\n \n", COMMA).is_empty());
}
}