Решение на CSV Filter от Ана Иванова

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

Към профила на Ана Иванова

Резултати

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

Код

///////////////////////////////////////////////////
/// Проверява че следващия символ във входния низ `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> {
if input.starts_with(target) {
return Some(&input[1usize..]);
}
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 index = input.find(target).unwrap_or(input.len());
(&input[0..index], &input[index..])
}
/// Комбинация от горните две функции -- взема символите до `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 (head,tail) = take_until(input, target);
if tail.is_empty() {
return None;
}
return Some((head, skip_next(tail, target).unwrap_or("")))
}
///////////////////////////////////////////////////
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>>>,
}
///////////////////////////////////////////////////
#[derive(Debug)]
pub enum CsvError {
IO(std::io::Error),
ParseError(String),
InvalidHeader(String),
InvalidRow(String),
InvalidColumn(String),
}
use std::io::Write;
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 line = String::new();
let header = reader.read_line(&mut line).map_err(|io_err| CsvError::IO(io_err))?;
if header == 0 {
return Err(CsvError::InvalidHeader(String::new()));
}
let mut csv = Csv { columns: Vec::new(), reader: reader, selection: None };
let mut column_header = line.trim();
while !column_header.is_empty() {
column_header = csv.add_column(column_header)?;
}
Ok(csv)
}
/// Функцията приема следващия ред за обработка и конструира `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 row = Row::new();
let mut parsed_line = line.trim();
for (index, column_header) in self.columns.iter().enumerate() {
if let Some(tail) = skip_next(parsed_line, '"') {
if let Some((value, tail)) = take_and_skip(tail, '"') {
parsed_line = tail.trim_start();
row.insert(String::from(column_header), String::from(value));
}
else {
break;
}
if index != self.columns.len() {
parsed_line = skip_next(parsed_line.trim_start(),',').unwrap_or("").trim_start();
}
} else {
break;
}
}
if !parsed_line.trim().is_empty() || row.len() != self.columns.len() {
return Err(CsvError::InvalidRow(String::from(line)));
}
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::from(callback))
}
/// Извикването на този метод консумира CSV-то и записва филтрираното съдържание в подадената
/// `Write` стойност. Вижте по-долу за пример и детайли.
///
/// Грешките, които се връщат са грешките, които идват от използваните други методи, плюс
/// грешките от писане във `writer`-а, опаковани в `CsvError::IO`.
///
/// В зависимост от това как си имплементирате метода, `mut` може би няма да ви трябва за
/// `self` -- ако имате warning-и, просто го махнете.
///
pub fn write_to<W: Write>(mut self, mut writer: W) -> Result<(), CsvError> {
let column_header = self.columns.join(", ");
if let Err(err) = writer.write_all(column_header.as_bytes()) {
return Err(CsvError::IO(err));
}
while let Some(value) = self.next() {
let row = value?;
let mut row_values = Vec::new();
for column in &self.columns {
if let None = row.get(column) {
return Err(CsvError::ParseError(String::from("Failed to get column")));
}
row_values.push(row[column].to_string());
}
writer.write_all("\n".as_bytes());
let formatted_row = row_values.iter().map(|x| format!("\"{}\"", x)).collect::<Vec<String>>().join(", ");
if let Err(err) = writer.write_all(formatted_row.as_bytes()) {
return Err(CsvError::IO(err));
}
}
Ok(())
}
fn add_column<'a>(&mut self, header : &'a str) -> Result<&'a str, CsvError> {
let header = header.trim();
let (column, tail) = take_until(header, ',');
let column = String::from(column.trim());
if column.is_empty() || self.columns.contains(&column) {
return Err(CsvError::InvalidHeader(String::from(header)));
}
self.columns.push(column);
let tail = tail.trim();
if tail.starts_with(',') {
return Ok(tail[1..].trim_start());
} else if !tail.is_empty() {
return Err(CsvError::InvalidHeader(String::from(header)));
}
Ok(tail)
}
fn apply_selection_to_line(&mut self, line : &str) -> Result<Option<Row>, CsvError> {
let parsed_row = self.parse_line(&line)?;
if self.selection.is_some() {
return match self.selection.as_ref().unwrap()(&parsed_row)? {
true => Ok(Some(parsed_row)),
false => Ok(None),
};
}
Ok(Some(parsed_row))
}
}
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();
let mut read_result = self.reader.read_line(&mut line);
while let Ok(bytes_read) = read_result {
if bytes_read == 0 {
return None
}
let selection_result = self.apply_selection_to_line(&line);
println!("---selection result={:?}, line={:?}---\n", selection_result, line);
match selection_result{
Ok(Some(row)) => return Some(Ok(row)),
Err(csv_error) => return Some(Err(csv_error)),
_ => ()
}
line = String::new();
read_result = self.reader.read_line(&mut line);
}
match(read_result) {
Ok(_) => None,
Err(error) => Some(Err(CsvError::IO(error))),
}
}
}

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

Compiling solution v0.1.0 (/tmp/d20210111-1538662-1aguo7d/solution)
warning: unnecessary parentheses around `match` scrutinee expression
   --> src/lib.rs:286:14
    |
286 |         match(read_result) {
    |              ^^^^^^^^^^^^^ help: remove these parentheses
    |
    = note: `#[warn(unused_parens)]` on by default

warning: unused `std::result::Result` that must be used
   --> src/lib.rs:202:13
    |
202 |             writer.write_all("\n".as_bytes());
    |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: `#[warn(unused_must_use)]` on by default
    = note: this `Result` may be an `Err` variant, which should be handled

warning: 2 warnings emitted

    Finished test [unoptimized + debuginfo] target(s) in 4.69s
     Running target/debug/deps/solution_test-8916805fc40a2dab

running 15 tests
test solution_test::test_csv_basic ... ok
test solution_test::test_csv_duplicate_columns ... ok
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 ... ok
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 ... ok
test solution_test::test_take_until ... ok

failures:

---- 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:13:22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    solution_test::test_parsing_helpers_for_unicode

test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

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

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

Ана качи първо решение на 11.01.2021 15:54 (преди над 4 години)