Решение на CSV Filter от Тихомир Каменов
Към профила на Тихомир Каменов
Резултати
- 10 точки от тестове
- 0 бонус точки
- 10 точки общо
- 10 успешни тест(а)
- 5 неуспешни тест(а)
Код
pub fn skip_next(input: &str, target: char) -> Option<&str> {
if input.is_empty() {
return None;
}
if input.chars().next().unwrap() == target {
return Some(&input[1..input.len()]);
} else {
return None;
}
}
pub fn take_until(input: &str, target: char) -> (&str, &str) {
for i in 0..input.len() {
match skip_next(&input[i..input.len()], target) {
None => continue,
Some(_) => return (&input[0..i], &input[i..input.len()]),
}
}
(input, "")
}
pub fn take_and_skip(input: &str, target: char) -> Option<(&str, &str)> {
match take_until(input, target) {
(_, "") => None,
(str1, str2) => Some((str1, &str2[1..str2.len()]))
}
}
#[derive(Debug)]
pub enum CsvError {
IO(std::io::Error),
ParseError(String),
InvalidHeader(String),
InvalidRow(String),
InvalidColumn(String),
}
impl From<std::io::Error> for CsvError {
fn from(error: std::io::Error) -> Self {
CsvError::IO(error)
}
}
use std::collections::HashMap;
type Row = HashMap<String, String>;
use std::io::BufRead;
pub struct Csv<R: BufRead> {
pub columns: Vec<String>,
reader: R,
selection: Option<Box<dyn Fn(&Row) -> Result<bool, CsvError>>>,
}
use std::io::Write;
impl<R: BufRead> Csv<R> {
pub fn new(mut reader: R) -> Result<Self, CsvError> {
let buf = &mut String::new();
let read_len = reader.read_line(buf).map_err(|e| CsvError::IO(e))?;
if read_len == 0 {
return Err(CsvError::InvalidHeader(String::from("no columns")));
}
let mut columns = Vec::<String>::new();
let mut res = take_and_skip(buf, ',');
loop {
match res {
Some((word, rest)) => {
for elem in columns.iter() {
if elem == word {
return Err(CsvError::InvalidHeader(String::from("duplicated columns")))
}
}
columns.push(String::from(word.trim()));
res = take_and_skip(rest, ',');
match res {
None => columns.push(String::from(rest.trim())),
Some(_) => continue,
}},
None => break,
}
}
Ok(Self {columns, reader, selection: None})
}
pub fn parse_line(&mut self, line: &str) -> Result<Row, CsvError> {
let mut res = line;
let mut row = Row::new();
for i in 0..self.columns.len() {
// trim leading whitespaces
let no_whitespaces_line = res.trim_start_matches(' ');
// if empty then end of line or last value is ""
if no_whitespaces_line == "" {
if i < self.columns.len() - 1 {
return Err(CsvError::InvalidRow(String::from("more columns than elements")));
} else { // i == self.columns.len() - 1
row.insert(self.columns[i].clone(), String::new());
break;
}
}
// if value in quotes is missing
if let Some(rest_line) = skip_next(no_whitespaces_line, ',') {
row.insert(self.columns[i].clone(), String::new());
res = rest_line;
} else {
// match leading quote
let elem_line = skip_next(no_whitespaces_line, '"').ok_or(CsvError::InvalidRow(String::from("no \"")))?;
// match value
let (elem, rest_line) = take_and_skip(elem_line, '"').ok_or(CsvError::InvalidRow(String::from("no \"")))?;
// add value
row.insert(self.columns[i].clone(), String::from(elem));
// match comma
let (_, next_elem_line) = take_and_skip(rest_line, ',').unwrap_or(("", ""));
res = next_elem_line;
}
}
if !res.is_empty() {
return Err(CsvError::InvalidRow(String::from("more elements than columns")))
}
Ok(row)
}
pub fn apply_selection<F>(&mut self, callback: F)
where F: Fn(&Row) -> Result<bool, CsvError> + 'static
{
self.selection = Some(Box::new(callback));
}
pub fn write_to<W: Write>(mut self, mut writer: W) -> Result<(), CsvError> {
for i in 0..self.columns.len() - 1 {
write!(writer, "{}", self.columns[i])?;
write!(writer, ", ")?;
}
write!(writer, "{}", self.columns[self.columns.len() - 1])?;
write!(writer, "\n")?;
while let Some(row) = self.next() {
println!("{:?}", row);
match row {
Ok(row) => {
for i in 0..self.columns.len() - 1 {
write!(writer, "\"{}\", ", row.get(&self.columns[i]).unwrap())?;
}
write!(writer, "\"{}\"\n", row.get(&self.columns[self.columns.len() - 1]).unwrap())?;
}
Err(e) => return Err(e),
}
}
Ok(())
}
}
impl<R: BufRead> Iterator for Csv<R> {
type Item = Result<Row, CsvError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let buf = &mut String::new();
match self.reader.read_line(buf) {
Ok(0) => return None,
Ok(_) => {},
Err(e) => return Some(Err(CsvError::IO(e))),
};
let row = match self.parse_line(buf) {
Ok(row) => row,
Err(e) => return Some(Err(e)),
};
match &self.selection {
Some(f) => match (*f)(&row) {
Ok(true) => return Some(Ok(row)),
Ok(false) => {},
Err(e) => return Some(Err(e)),
}
None => return Some(Ok(row))
}
}
}
}
Лог от изпълнението
Compiling solution v0.1.0 (/tmp/d20210111-1538662-njcb4m/solution) Finished test [unoptimized + debuginfo] target(s) in 4.27s Running target/debug/deps/solution_test-8916805fc40a2dab running 15 tests test solution_test::test_csv_basic ... ok test solution_test::test_csv_duplicate_columns ... FAILED test solution_test::test_csv_empty ... ok test solution_test::test_csv_iterating_with_a_selection ... ok test solution_test::test_csv_iterating_with_no_selection ... ok test solution_test::test_csv_parse_line ... ok test solution_test::test_csv_parse_line_with_commas ... ok test solution_test::test_csv_selection_and_writing ... ok test solution_test::test_csv_single_column_no_data ... FAILED test solution_test::test_csv_writing_without_a_selection ... ok test solution_test::test_csv_writing_without_any_rows ... ok test solution_test::test_parsing_helpers_for_unicode ... FAILED test solution_test::test_skip_next ... ok test solution_test::test_take_and_skip ... FAILED test solution_test::test_take_until ... FAILED failures: ---- solution_test::test_csv_duplicate_columns stdout ---- thread 'main' panicked at 'Expression None does not match the pattern "Some(CsvError::InvalidHeader(_))"', tests/solution_test.rs:92:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ---- solution_test::test_csv_single_column_no_data stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `0`, right: `1`', tests/solution_test.rs:178:5 ---- solution_test::test_parsing_helpers_for_unicode stdout ---- thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside '↓' (bytes 0..3) of `↓яга`', src/lib.rs:7:22 ---- solution_test::test_take_and_skip stdout ---- thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'б' (bytes 0..2) of `баба/яга`', src/lib.rs:15:26 ---- solution_test::test_take_until stdout ---- thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'б' (bytes 0..2) of `баба/яга`', src/lib.rs:15:26 failures: solution_test::test_csv_duplicate_columns solution_test::test_csv_single_column_no_data solution_test::test_parsing_helpers_for_unicode solution_test::test_take_and_skip solution_test::test_take_until test result: FAILED. 10 passed; 5 failed; 0 ignored; 0 measured; 0 filtered out error: test failed, to rerun pass '--test solution_test'