Решение на CSV Filter от Борис Петров
Резултати
- 4 точки от тестове
- 0 бонус точки
- 4 точки общо
- 4 успешни тест(а)
- 11 неуспешни тест(а)
Код
fn crop_letters(s: &str, pos: usize) -> &str {
match s.char_indices().skip(pos).next() {
Some((pos, _)) => &s[pos..],
None => "",
}
}
fn get_first_occurance(s: &str, target: char) -> Option<usize> {
for (i, c) in s.chars().enumerate() {
if c == target {
return Some(i);
}
}
None
}
/// Проверява че следващия символ във входния низ `input` е точно `target`.
///
/// Ако низа наистина започва с този символ, връща остатъка от низа без него, пакетиран във
/// `Some`. Иначе, връща `None`. Примерно:
///
/// skip_next("(foo", '(') //=> Some("foo")
/// skip_next("(foo", ')') //=> None
/// skip_next("", ')') //=> None
///
pub fn skip_next(input: &str, target: char) -> Option<&str> {
match input {
"" => None,
inp if inp.chars().nth(0).unwrap() == target => Some(crop_letters(input, 1)),
_ => None,
}
}
/// Търси следващото срещане на символа `target` в низа `input`. Връща низа до този символ и низа
/// от този символ нататък, в двойка.
///
/// Ако не намери `target`, връща оригиналния низ и празен низ като втори елемент в двойката.
///
/// take_until(" foo/bar ", '/') //=> (" foo", "/bar ")
/// take_until("foobar", '/') //=> ("foobar", "")
/// take_until("Barman\", \"asd\"") //=> ("Barman", "...")
///
pub fn take_until(input: &str, target: char) -> (&str, &str) {
let index = get_first_occurance(input, target);
if index != None {
return (
&input[..index.unwrap()],
crop_letters(input, index.unwrap()),
);
} else {
return (input, "");
}
}
/// Комбинация от горните две функции -- взема символите до `target` символа, и връща частта преди
/// символа и частта след, без самия символ. Ако символа го няма, връща `None`.
///
/// take_and_skip(" foo/bar ", '/') //=> Some((" foo", "bar "))
/// take_and_skip("foobar", '/') //=> None
///
pub fn take_and_skip(input: &str, target: char) -> Option<(&str, &str)> {
match take_until(input, target) {
tuple if tuple.1 != "" => Some((tuple.0, skip_next(tuple.1, target).unwrap())),
_ => None,
}
}
#[derive(Debug)]
pub enum CsvError {
IO(std::io::Error),
ParseError(String),
InvalidHeader(String),
InvalidRow(String),
InvalidColumn(String),
}
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 mut header = String::new();
match reader.read_line(&mut header) {
Ok(0) => {
return Err(CsvError::InvalidHeader("Empty header".to_string()));
}
Err(err) => {
return Err(CsvError::IO(err));
}
_ => {
let split = header.split(",");
let mut columns: Vec<String> = Vec::new();
for column in split {
if columns.contains(&column.to_string()) {
return Err(CsvError::InvalidHeader(
"Duplicate column names".to_string(),
));
}
columns.push(column.trim().to_string());
}
return Ok(Csv {
columns: columns,
reader: reader,
selection: None,
});
}
}
}
/// Функцията приема следващия ред за обработка и конструира `Row` стойност
/// (`HashMap<String, String>`) със колоните и съответсващите им стойности на този ред.
///
/// Алгоритъма е горе-долу:
///
/// 1. Изчистете реда с `.trim()`.
/// 2. Очаквате, че реда ще започне със `"`, иначе връщате грешка.
/// 3. Прочитате съдържанието от отварящата кавичка до следващата. Това е съдържанието на
/// стойността на текущата колона на този ред. Не го чистите от whitespace, просто го
/// приемате както е.
/// 4. Ако не намерите затваряща кавичка, това е грешка.
/// 5. Запазвате си стойността в един `Row` (`HashMap`) -- ключа е името на текущата колона,
/// до която сте стигнали, стойността е това, което току-що изпарсихте.
/// 6. Ако нямате оставащи колони за обработка и нямате оставащо съдържание от реда, всичко
/// е ок. Връщате реда.
/// 7. Ако нямате оставащи колони, но имате още от реда, или обратното, това е грешка.
///
/// За този процес, помощните функции, които дефинирахме по-горе може да ви свършат работа.
/// *Може* да използвате вместо тях `.split` по запетайки, но ще имаме поне няколко теста със
/// вложени запетайки. Бихте могли и с това да се справите иначе, разбира се -- ваш избор.
///
/// Внимавайте с празното пространство преди и след запетайки -- викайте `.trim()` на ключови
/// места. Всичко в кавички се взема както е, всичко извън тях се чисти от whitespace.
///
/// Всички грешки, които ще връщате, се очаква да бъдат `CsvError::InvalidRow`.
///
pub fn parse_line(&mut self, line: &str) -> Result<Row, CsvError> {
let mut l = line.trim();
let mut row: Row = HashMap::new();
for col in self.columns.iter() {
if skip_next(l, ',') != None {
l = skip_next(l, ',').unwrap();
}
if skip_next(l, '"') == None {
return Err(CsvError::InvalidRow("Missing quotation mark".to_string()));
}
// remove first "
l = skip_next(l, '"').unwrap();
// get first value without ending "
let line_tuple = take_and_skip(l, '"');
if line_tuple == None {
return Err(CsvError::InvalidRow(
"Mismatching amount of columns and values".to_string(),
));
}
l = line_tuple.unwrap().1;
row.insert(col.clone(), line_tuple.unwrap().0.to_string());
}
if skip_next(l, '"') != None {
return Err(CsvError::InvalidRow(
"Mismatching amount of columns and values".to_string(),
));
}
Ok(row)
}
/// Подадената функция, "callback", се очаква да се запази и да се използва по-късно за
/// филтриране -- при итерация, само редове, за които се връща `true` се очаква да се извадят.
///
/// Би трябвало `callback` да се вика от `.next()` и от `.write_to()`, вижте описанията на тези
/// методи за детайли.
///
pub fn apply_selection<F>(&mut self, callback: F)
where
F: Fn(&Row) -> Result<bool, CsvError> + 'static,
{
self.selection = Some(Box::new(callback));
}
/// Извикването на този метод консумира CSV-то и записва филтрираното съдържание в подадената
/// `Write` стойност. Вижте по-долу за пример и детайли.
///
/// Грешките, които се връщат са грешките, които идват от използваните други методи, плюс
/// грешките от писане във `writer`-а, опаковани в `CsvError::IO`.
///
/// В зависимост от това как си имплементирате метода, `mut` може би няма да ви трябва за
/// `self` -- ако имате warning-и, просто го махнете.
///
pub fn write_to<W: Write>(mut self, mut writer: W) -> Result<(), CsvError> {
let cols = self.columns.join(",");
match writer.write(cols.as_bytes()) {
Ok(_) => (),
Err(err) => {
return Err(CsvError::IO(err));
}
}
let mut line = String::new();
loop {
match self.reader.read_line(&mut line) {
Ok(0) => (),
Err(err) => {
return Err(CsvError::IO(err));
}
_ => match self.parse_line(&line) {
Err(err) => {
return Err(err);
}
row => {
let old_row = row.unwrap().clone();
match self.selection.as_ref().unwrap()(&old_row) {
Err(err) => {
return Err(err);
}
Ok(false) => continue,
Ok(true) => match writer.write(cols.as_bytes()) {
Ok(_) => (),
Err(err) => {
return Err(CsvError::IO(err));
}
},
}
}
},
}
}
}
}
impl<R: BufRead> Iterator for Csv<R> {
type Item = Result<Row, CsvError>;
/// Итерацията се състои от няколко стъпки:
///
/// 1. Прочитаме следващия ред от входа:
/// -> Ако има грешка при четене, връщаме Some(CsvError::IO(...))
/// -> Ако успешно се прочетат 0 байта, значи сме на края на входа, и няма какво повече да
/// четем -- връщаме `None`
/// -> Иначе, имаме успешно прочетен ред, продължаваме напред
/// 2. Опитваме се да обработим прочетения ред със `parse_line`:
/// -> Ако има грешка при парсене, връщаме Some(CsvError-а, който се връща от `parse_line`)
/// -> Ако успешно извикаме `parse_line`, вече имаме `Row` стойност.
/// 3. Проверяваме дали този ред изпълнява условието, запазено от `apply_selection`:
/// -> Ако условието върне грешка, връщаме тази грешка опакована във `Some`.
/// -> Ако условието върне Ok(false), *не* връщаме този ред, а пробваме следващия (обратно
/// към стъпка 1)
/// -> При Ok(true), връщаме този ред, опакован във `Some`
///
/// Да, тази функция връща `Option<Result<...>>` :). `Option` защото може да има, може да няма
/// следващ ред, `Result` защото четенето на реда (от примерно файл) може да не сработи.
///
fn next(&mut self) -> Option<Self::Item> {
let mut line = String::new();
match self.reader.read_line(&mut line) {
Ok(0) => {
return None;
}
Err(err) => {
return Some(Err(CsvError::IO(err)));
}
_ => match self.parse_line(&line) {
Err(err) => {
return Some(Err(err));
}
row => {
let old_row = row.unwrap().clone();
match self.selection.as_ref().unwrap()(&old_row) {
Err(err) => {
return Some(Err(err));
}
Ok(false) => self.next(),
Ok(true) => {
return Some(Ok(old_row));
}
}
}
},
}
}
}
Лог от изпълнението
Compiling solution v0.1.0 (/tmp/d20210111-1538662-pks6cs/solution) Finished test [unoptimized + debuginfo] target(s) in 4.11s 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 ... FAILED 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 ... FAILED test solution_test::test_csv_selection_and_writing ... FAILED test solution_test::test_csv_single_column_no_data ... ok test solution_test::test_csv_writing_without_a_selection ... FAILED test solution_test::test_csv_writing_without_any_rows ... FAILED 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 `Result::unwrap()` on an `Err` value: InvalidRow("Missing quotation mark")', tests/solution_test.rs:70:39 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidRow("Missing quotation mark")', tests/solution_test.rs:60:5 ---- 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 ---- solution_test::test_csv_iterating_with_a_selection stdout ---- thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidRow("Missing quotation mark")', tests/solution_test.rs:218:44 ---- solution_test::test_csv_iterating_with_no_selection stdout ---- thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidRow("Missing quotation mark")', tests/solution_test.rs:195:48 thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidRow("Missing quotation mark")', tests/solution_test.rs:185:5 ---- solution_test::test_csv_parse_line_with_commas stdout ---- thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidRow("Missing quotation mark")', tests/solution_test.rs:162:56 ---- solution_test::test_csv_selection_and_writing stdout ---- thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Timeout', tests/solution_test.rs:251:5 ---- solution_test::test_csv_writing_without_a_selection stdout ---- thread '<unnamed>' panicked at 'called `Option::unwrap()` on a `None` value', /tmp/d20210111-1538662-pks6cs/solution/src/lib.rs:221:55 thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', tests/solution_test.rs:224:5 ---- solution_test::test_csv_writing_without_any_rows stdout ---- thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Timeout', tests/solution_test.rs:278:5 ---- solution_test::test_parsing_helpers_for_unicode stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `("ба", "↓яга")`, right: `("баба", "↓яга")`', tests/solution_test.rs:135:5 ---- solution_test::test_take_and_skip stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `Some(("ба", "яга"))`, right: `Some(("баба", "яга"))`', tests/solution_test.rs:128:5 ---- 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_a_selection solution_test::test_csv_iterating_with_no_selection solution_test::test_csv_parse_line_with_commas solution_test::test_csv_selection_and_writing solution_test::test_csv_writing_without_a_selection solution_test::test_csv_writing_without_any_rows solution_test::test_parsing_helpers_for_unicode solution_test::test_take_and_skip solution_test::test_take_until test result: FAILED. 4 passed; 11 failed; 0 ignored; 0 measured; 0 filtered out error: test failed, to rerun pass '--test solution_test'