Решение на CSV Filter от Цветелина Стоянова
Към профила на Цветелина Стоянова
Резултати
- 7 точки от тестове
- 0 бонус точки
- 7 точки общо
- 7 успешни тест(а)
- 8 неуспешни тест(а)
Код
use std::collections::HashMap;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
#[derive(Debug)]
pub enum CsvError {
IO(std::io::Error),
ParseError(String),
InvalidHeader(String),
InvalidRow(String),
InvalidColumn(String),
}
type Row = HashMap<String, String>;
pub struct Csv<R: BufRead> {
pub columns: Vec<String>,
reader: R,
selection: Option<Box<dyn Fn(&Row) -> Result<bool, CsvError>>>,
}
pub fn read_records(header: String) -> Result<Vec<String>, CsvError> {
let mut column_names = Vec::new();
let mut remainder = header;
while let Some((first_word, rest_words)) = take_and_skip(&remainder, ',') {
let first_trimmed = first_word.trim();
let column_already_added_error = |column: String| {
Err(CsvError::InvalidHeader(format!(
"The column: {} is already added.",
column
)))
};
if column_names.contains(&String::from(first_trimmed)) {
return column_already_added_error(String::from(first_trimmed));
} else {
column_names.push(String::from(first_trimmed));
}
let second_part_copy = String::from(rest_words);
// handle the last column name
if take_and_skip(&second_part_copy, ',').is_none() {
let last_word_trimmed = rest_words.trim();
if column_names.contains(&String::from(last_word_trimmed)) {
return column_already_added_error(String::from(last_word_trimmed));
} else {
column_names.push(String::from(last_word_trimmed))
}
}
remainder = String::from(rest_words);
}
return Ok(column_names);
}
impl<R: BufRead> Csv<R> {
pub fn new(mut reader: R) -> Result<Self, CsvError> {
let mut header = String::new();
return match reader.read_line(&mut header) {
Ok(n) => match n {
0 => Err(CsvError::InvalidHeader(String::from(
"Your header is not valid.",
))),
_ => Ok(Csv {
columns: read_records(header).unwrap(),
reader: reader,
selection: None,
}),
},
Err(error) => Err(CsvError::IO(error)),
};
}
pub fn parse_line(&mut self, line: &str) -> Result<Row, CsvError> {
let separator = '"';
let trimmed_line = line.trim();
let mut row = Row::new();
let not_valid_row_error = |message: String| {
Err(CsvError::InvalidRow(format!(
"Your row is not valid. {}",
message
)))
};
if trimmed_line.is_empty() {
return not_valid_row_error(String::from(""));
}
let mut remainder = trimmed_line;
for (i, column) in self.columns.iter().enumerate() {
if remainder.chars().nth(0).unwrap() != separator {
return not_valid_row_error(String::from("It does not start with a \"."));
}
let remainder_after_first_quote = skip_next(remainder, separator);
if remainder_after_first_quote.is_none() {
return not_valid_row_error(String::from(""));
}
let first_word_and_rest =
take_and_skip(remainder_after_first_quote.unwrap(), separator);
if first_word_and_rest.is_none() {
return not_valid_row_error(String::from("There is not found a second quote."));
}
let (first_word, rest_words) = first_word_and_rest.unwrap();
row.insert(column.to_string(), String::from(first_word));
let rest_words_trimmed_with_leading_quote = rest_words.trim();
let second_word_trimmed = skip_next(rest_words_trimmed_with_leading_quote, ',');
let is_last_iteration = i == (self.columns.len() - 1);
if !is_last_iteration && second_word_trimmed.is_none() {
return not_valid_row_error(String::from(
"The count of the records in the row is smaller than the count of the columns.",
));
}
if second_word_trimmed.is_none() {
remainder = rest_words_trimmed_with_leading_quote;
} else {
remainder = second_word_trimmed.unwrap().trim();
}
}
return match remainder {
"" => Ok(row),
_ => not_valid_row_error(String::from(
"The count of the records in the row is greater than the count of the columns.",
)),
};
}
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> {
let columns = self.columns.join(", ");
let mut rows: Vec<String> = Vec::new();
while let Some(row) = self.next() {
let row_unwrapped = row.unwrap();
let row_records: String = self
.columns
.iter()
.map(|c| format!("\"{}\"", row_unwrapped.get(c).unwrap()))
.collect::<Vec<String>>()
.join(", ");
rows.push(String::from(row_records));
}
let formatted_rows = rows.join("\n");
let result = [columns, formatted_rows].join("\n");
return match write!(&mut writer, "{}", result) {
Ok(_) => Ok(()),
Err(error) => Err(CsvError::IO(error)),
};
}
}
impl<R: BufRead> Iterator for Csv<R> {
type Item = Result<Row, CsvError>;
fn next(&mut self) -> Option<Self::Item> {
let mut was_found_next_item = false;
while !was_found_next_item {
let mut line = String::new();
match self.reader.read_line(&mut line) {
Ok(n) => {
if n == 0 {
return None;
}
}
Err(error) => return Some(Err(CsvError::IO(error))),
}
let row_value;
match self.parse_line(&line) {
Ok(n) => {
row_value = n;
}
Err(error) => return Some(Err(error)),
}
match (self.selection.as_ref().unwrap())(&row_value) {
Ok(n) => {
was_found_next_item = n;
if n {
return Some(Ok(row_value));
}
}
Err(error) => return Some(Err(error)),
}
}
return None;
}
}
pub fn skip_next(input: &str, target: char) -> Option<&str> {
if input.is_empty() {
return None;
}
let next_character = input.chars().next().unwrap();
return if next_character == target {
Some(&input[1..])
} else {
None
};
}
pub fn take_until(input: &str, target: char) -> (&str, &str) {
for (i, c) in input.chars().enumerate() {
if c == target {
return input.split_at(i);
}
}
return (input, "");
}
pub fn take_and_skip(input: &str, target: char) -> Option<(&str, &str)> {
let (first, second) = take_until(input, target);
return if second.is_empty() {
None
} else {
Some((first, skip_next(&second, target).unwrap()))
};
}
Лог от изпълнението
Compiling solution v0.1.0 (/tmp/d20210111-1538662-1hjsajx/solution) warning: unused import: `std::io::BufReader` --> src/lib.rs:3:5 | 3 | use std::io::BufReader; | ^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default warning: 1 warning emitted Finished test [unoptimized + debuginfo] target(s) in 4.19s Running target/debug/deps/solution_test-8916805fc40a2dab running 15 tests test solution_test::test_csv_basic ... FAILED 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 ... FAILED 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 ... FAILED 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_basic stdout ---- thread '<unnamed>' panicked at 'called `Option::unwrap()` on a `None` value', /tmp/d20210111-1538662-1hjsajx/solution/src/lib.rs:199:44 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', tests/solution_test.rs:60:5 ---- solution_test::test_csv_duplicate_columns stdout ---- thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidHeader("The column: age is already added.")', /tmp/d20210111-1538662-1hjsajx/solution/src/lib.rs:70:51 ---- solution_test::test_csv_iterating_with_no_selection stdout ---- thread '<unnamed>' panicked at 'called `Option::unwrap()` on a `None` value', /tmp/d20210111-1538662-1hjsajx/solution/src/lib.rs:199:44 thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', tests/solution_test.rs:185:5 ---- 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_csv_writing_without_a_selection stdout ---- thread '<unnamed>' panicked at 'called `Option::unwrap()` on a `None` value', /tmp/d20210111-1538662-1hjsajx/solution/src/lib.rs:199:44 thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', tests/solution_test.rs:224: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:221:15 ---- solution_test::test_take_and_skip stdout ---- thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/lib.rs:242:49 ---- solution_test::test_take_until stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `("ба", "ба/яга")`, right: `("баба", "/яга")`', tests/solution_test.rs:121:5 failures: solution_test::test_csv_basic solution_test::test_csv_duplicate_columns solution_test::test_csv_iterating_with_no_selection solution_test::test_csv_single_column_no_data solution_test::test_csv_writing_without_a_selection solution_test::test_parsing_helpers_for_unicode solution_test::test_take_and_skip solution_test::test_take_until test result: FAILED. 7 passed; 8 failed; 0 ignored; 0 measured; 0 filtered out error: test failed, to rerun pass '--test solution_test'