Решение на CSV Filter от Велин Клисуров

Обратно към всички решения

Към профила на Велин Клисуров

Резултати

  • 2 точки от тестове
  • 0 бонус точки
  • 2 точки общо
  • 2 успешни тест(а)
  • 13 неуспешни тест(а)

Код

/// Проверява че следващия символ във входния низ `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> {
let mut res = "";
let first = input.chars().next().unwrap();
if first == target {
return Some(&input[1..].trim());
}
return None;
}
/// Търси следващото срещане на символа `target` в низа `input`. Връща низа до този символ и низа
/// от този символ нататък, в двойка.
///
/// Ако не намери `target`, връща оригиналния низ и празен низ като втори елемент в двойката.
///
/// take_until(" foo/bar ", '/') //=> (" foo", "/bar ")
/// take_until("foobar", '/') //=> ("foobar", "")
///
pub fn take_until(input: &str, target: char) -> (&str, &str) {
let mut str1 = input;
let mut str2 = "";
for (i, c) in input.chars().enumerate() {
if c == target {
str1 = &input[..i].trim();
str2 = &input[i..].trim();
break;
}
}
return (str1, str2);
}
/// Комбинация от горните две функции -- взема символите до `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)> {
let mut str1 = input;
let mut str2 = "";
for (i, c) in input.chars().enumerate() {
if c == target {
str1 = &input[..i].trim();
str2 = &input[i+1..].trim();
return Some((str1, str2));
}
}
return 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, Error, ErrorKind};
pub struct Csv<R: BufRead> {
pub columns: Vec<String>,
reader: R,
selection: Option<Box<dyn Fn(&Row) -> Result<bool, CsvError>>>,
}
use std::io::Write;
use std::panic::resume_unwind;
impl<R: BufRead> Csv<R> {
/// Конструира нова стойност от подадения вход. Третира се като "нещо, от което може да се чете
/// ред по ред".
///
/// Очакваме да прочетете първия ред от входа и да го обработите като заглавна част ("header").
/// Това означава, че първия ред би трябвало да включва имена на колони, разделени със
/// запетайки и може би празни места. Примерно:
///
/// - name, age
/// - name,age,birth date
///
/// В случай, че има грешка от викане на методи на `reader`, тя би трябвало да е `io::Error`.
/// върнете `CsvError::IO`, което опакова въпросната грешка.
///
/// Ако първия ред е празен, прочитането ще ви върне 0 байта. Примерно, `read_line` връща
/// `Ok(0)` в такъв случай. Това означава, че нямаме валиден header -- нито една колона няма,
/// очакваме грешка `CsvError::InvalidHeader`.
///
/// Ако има дублиране на колони -- две колони с едно и също име -- също върнете
/// `CsvError::InvalidHeader`.
///
/// Ако всичко е наред, върнете конструирана стойност, на която `columns` е списък с колоните,
/// в същия ред, в който са подадени, без заобикалящите ги празни символи (използвайте
/// `.trim()`).
///
pub fn new(mut reader: R) -> Result<Self, CsvError> {
let mut res:Csv<R> = Csv {
columns: vec![],
reader,
selection: None
};
let mut line = String::from("");
let mut reader_res = res.reader.read_line(&mut line);
let mut rest;
match reader_res {
Ok(0) => return Err(CsvError::InvalidHeader(String::from("Empty row"))),
Err(_) => return Err(CsvError::IO(Error::new(ErrorKind::Other, "Error"))),
_ => {
let mut tmp = take_and_skip(&line, ',');
loop {
for i in &res.columns {
if i == tmp.unwrap().1 {
return Err(CsvError::InvalidHeader(String::from("Duplicated column")));
}
}
// if res.columns.iter().any(|i| i==tmp.unwrap().1) {
// return Err(CsvError::InvalidHeader(String::from("Duplicated column")));
// }
match tmp {
Some(_) => {
res.columns.push(String::from(tmp.unwrap().0.trim()));
rest = tmp.unwrap().1;
},
None => return Ok(res)
}
tmp = take_and_skip(tmp.unwrap().1, ',');
if tmp == None {
res.columns.push(String::from(rest));
break
};
}
}
}
return Ok(res);
}
/// Функцията приема следващия ред за обработка и конструира `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 res: Row = Default::default();
let mut trimmed = line.trim();
if skip_next(line, '"' ) == None{
return Err(CsvError::InvalidRow(String::from("Does not start with \"")));
}
let mut str1;
let mut str2 = trimmed;
let mut tmp;
let mut cnt = 0;
let mut rest = "";
while str2 != ""{
tmp = take_until(&str2[1..], '"');
str1 = tmp.0;
if take_and_skip(tmp.1, ',') == None && cnt != self.columns.len() - 1 {
return Err(CsvError::InvalidRow(String::from( "Row ended before columns")));
} else if take_and_skip(tmp.1, ',') == None {
res.insert(String::from(&self.columns[cnt]) , String::from(rest));
return Ok(res);
}
str2 = take_and_skip(tmp.1, ',').unwrap().1.trim();
rest = &str2[1..str2.len()-1];
res.insert(String::from(&self.columns[cnt]) , String::from(str1));
cnt+=1;
}
return if cnt == self.columns.len() - 1 {
Ok(res)
} else {
Err(CsvError::InvalidRow(String::from("Columns ended before 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> {
todo!();
}
// let mut row = String::from("");
// for col in self.columns.clone() {
// row = row + &*col;
// }
// row = row + "\n";
// writer.write(row.as_ref());
//
// let mut trimmed;
// let mut line;
// loop {
//
// line = String::from("");
// self.reader.read_line(&mut line);
// trimmed = line.trim();
//
// let mut str1;
// let mut str2 = trimmed;
// let mut tmp;
// let mut cnt = 0;
// let mut rest = "";
// while str2 != ""{
// tmp = take_until(&str2[1..], '"');
// str1 = tmp.0;
// if take_and_skip(tmp.1, ',') == None && cnt != self.columns.len() - 1 {
// return Err(CsvError::InvalidRow(String::from( "Row ended before columns")));
// } else if take_and_skip(tmp.1, ',') == None {
// row = row + rest;
// break;
// }
// str2 = take_and_skip(tmp.1, ',').unwrap().1.trim();
// rest = &str2[1..str2.len()-1];
// row = row + str1;
// cnt+=1;
// }
//
// row = row + "\n";
// println!("{:?}", row);
// writer.write(row.as_ref());
// row = String::from("");
// }
// return Ok(());
// }
}
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::from("");
let mut bytesRead;
loop {
bytesRead = self.reader.read_line(&mut line);
match bytesRead {
Ok(0) => return None,
Ok(_) => {
let mut row;
let mut tmp = self.parse_line(line.as_str());
match tmp{
Ok(_) => row = tmp.unwrap(),
Err(e) => return Some(Err(e))
}
match &self.selection {
Some(box_fn) => {
match box_fn(&row) {
Ok(true) => return Some(Ok(row)),
Ok(false) => {
line = String::from("");
continue
},
Err(e) => return Some(Err(e))
};
}
None => return Some(Err(CsvError::IO(Error::new(ErrorKind::Other, "Error"))))
};
},
Err(_) => return Some(Err(CsvError::IO(Error::new(ErrorKind::Other, "Error"))))
}
}
}
}

Лог от изпълнението

Compiling solution v0.1.0 (/tmp/d20210111-1538662-1pcr6ly/solution)
warning: unused import: `std::panic::resume_unwind`
  --> src/lib.rs:82:5
   |
82 | use std::panic::resume_unwind;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_imports)]` on by default

warning: unused variable: `res`
  --> src/lib.rs:11:9
   |
11 |     let mut res = "";
   |         ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_res`
   |
   = note: `#[warn(unused_variables)]` on by default

warning: value assigned to `str1` is never read
  --> src/lib.rs:47:9
   |
47 |     let mut str1 = input;
   |         ^^^^^^^^
   |
   = note: `#[warn(unused_assignments)]` on by default
   = help: maybe it is overwritten before being read?

warning: value assigned to `str2` is never read
  --> src/lib.rs:48:9
   |
48 |     let mut str2 = "";
   |         ^^^^^^^^
   |
   = help: maybe it is overwritten before being read?

warning: unused variable: `writer`
   --> src/lib.rs:233:41
    |
233 |     pub fn write_to<W: Write>(mut self, mut writer: W) -> Result<(), CsvError> {
    |                                         ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_writer`

warning: variable does not need to be mutable
  --> src/lib.rs:11:9
   |
11 |     let mut res = "";
   |         ----^^^
   |         |
   |         help: remove this `mut`
   |
   = note: `#[warn(unused_mut)]` on by default

warning: variable does not need to be mutable
   --> src/lib.rs:109:16
    |
109 |     pub fn new(mut reader: R) -> Result<Self, CsvError> {
    |                ----^^^^^^
    |                |
    |                help: remove this `mut`

warning: variable does not need to be mutable
   --> src/lib.rs:116:13
    |
116 |         let mut reader_res = res.reader.read_line(&mut line);
    |             ----^^^^^^^^^^
    |             |
    |             help: remove this `mut`

warning: variable does not need to be mutable
   --> src/lib.rs:180:13
    |
180 |         let mut trimmed = line.trim();
    |             ----^^^^^^^
    |             |
    |             help: remove this `mut`

warning: variable does not need to be mutable
   --> src/lib.rs:233:31
    |
233 |     pub fn write_to<W: Write>(mut self, mut writer: W) -> Result<(), CsvError> {
    |                               ----^^^^
    |                               |
    |                               help: remove this `mut`

warning: variable does not need to be mutable
   --> src/lib.rs:233:41
    |
233 |     pub fn write_to<W: Write>(mut self, mut writer: W) -> Result<(), CsvError> {
    |                                         ----^^^^^^
    |                                         |
    |                                         help: remove this `mut`

warning: variable does not need to be mutable
   --> src/lib.rs:310:25
    |
310 |                     let mut row;
    |                         ----^^^
    |                         |
    |                         help: remove this `mut`

warning: variable does not need to be mutable
   --> src/lib.rs:311:25
    |
311 |                     let mut tmp = self.parse_line(line.as_str());
    |                         ----^^^
    |                         |
    |                         help: remove this `mut`

warning: variable `bytesRead` should have a snake case name
   --> src/lib.rs:304:17
    |
304 |         let mut bytesRead;
    |                 ^^^^^^^^^ help: convert the identifier to snake case: `bytes_read`
    |
    = note: `#[warn(non_snake_case)]` on by default

warning: 14 warnings emitted

    Finished test [unoptimized + debuginfo] target(s) in 3.86s
     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 ... FAILED
test solution_test::test_csv_parse_line_with_commas ... ok
test solution_test::test_csv_selection_and_writing ... FAILED
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 ... FAILED
test solution_test::test_parsing_helpers_for_unicode ... FAILED
test solution_test::test_skip_next ... FAILED
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("Does not start with \"")', 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("Does not start with \"")', 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("Does not start with \"")', 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("Does not start with \"")', tests/solution_test.rs:195:48
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidRow("Does not start with \"")', tests/solution_test.rs:185:5

---- solution_test::test_csv_parse_line stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `("Name With  Spaces", "13", "0-0-0")`,
 right: `(" Name With  Spaces  ", " 13 ", "0-0-0")`', tests/solution_test.rs:151:5

---- solution_test::test_csv_selection_and_writing stdout ----
thread '<unnamed>' panicked at 'not yet implemented', /tmp/d20210111-1538662-1pcr6ly/solution/src/lib.rs:234:9
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', tests/solution_test.rs:251: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 'not yet implemented', /tmp/d20210111-1538662-1pcr6ly/solution/src/lib.rs:234:9
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 '<unnamed>' panicked at 'not yet implemented', /tmp/d20210111-1538662-1pcr6ly/solution/src/lib.rs:234:9
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', tests/solution_test.rs:278: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:14:22

---- solution_test::test_skip_next stdout ----
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/lib.rs:12:38

---- solution_test::test_take_and_skip stdout ----
thread 'main' panicked at 'byte index 5 is not a char boundary; it is inside 'б' (bytes 4..6) of `баба/яга`', src/lib.rs:52:21

---- 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
    solution_test::test_csv_selection_and_writing
    solution_test::test_csv_single_column_no_data
    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_skip_next
    solution_test::test_take_and_skip
    solution_test::test_take_until

test result: FAILED. 2 passed; 13 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--test solution_test'

История (1 версия и 0 коментара)

Велин качи първо решение на 11.01.2021 13:25 (преди над 4 години)