Решение на CSV Filter от Борис Ангелов
Резултати
- 11 точки от тестове
- 0 бонус точки
- 11 точки общо
- 11 успешни тест(а)
- 4 неуспешни тест(а)
Код
use std::collections::HashMap;
use std::io::Write;
use std::collections::HashSet;
use std::fmt;
type Row = HashMap<String, String>;
use std::io::BufRead;
#[derive(Debug)]
pub enum CsvError {
IO(std::io::Error),
ParseError(String),
InvalidHeader(String),
InvalidRow(String),
InvalidColumn(String),
}
impl fmt::Display for CsvError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
/// Проверява че следващия символ във входния низ `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 chs = input.chars();
chs.next().map_or_else(|| None, |ch| {
println!("[skip_next] Text |{}| current: |{}| target |{}| eq? |{}|", chs.as_str(), ch, target, ch == target);
return if ch == target {Some(chs.as_str())} else {None};
})
}
/// Търси следващото срещане на символа `target` в низа `input`. Връща низа до този символ и низа
/// от този символ нататък, в двойка.
///
/// Ако не намери `target`, връща оригиналния низ и празен низ като втори елемент в двойката.
///
/// take_until(" foo/bar ", '/') //=> (" foo", "/bar ")
/// take_until("foobar", '/') //=> ("foobar", "")
///
pub fn take_until(input: & str, target: char) -> (&str, &str) {
input.chars()
.position(|c| c == target)
.map_or_else(|| (input, ""), |id| input.split_at(id))
}
/// Комбинация от горните две функции -- взема символите до `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 (l, r) = take_until(input, target);
match r {
"" => None,
_ => skip_next(r, target).map(|val| {
println!("[take_and_skip] returning (|{}|, |{}|)", l, val);
(l, val)
})
}
}
pub struct Csv<R: BufRead> {
pub columns: Vec<String>,
reader: R,
selection: Option<Box<dyn Fn(&Row) -> Result<bool, CsvError>>>,
}
impl<R: BufRead> Csv<R> {
fn check_and_push(col: &mut Vec<String>, seen: &mut HashSet<String>, st: &str) -> Option<Result<Csv<R>, CsvError>> {
let trimmed = &st.trim().to_string();
if seen.contains(trimmed) {
return Some(Err(CsvError::InvalidHeader("Duplicate ".to_string() + st)));
}
col.push(trimmed.clone());
seen.insert(trimmed.clone());
return None;
}
/// Конструира нова стойност от подадения вход. Третира се като "нещо, от което може да се чете
/// ред по ред".
///
/// Очакваме да прочетете първия ред от входа и да го обработите като заглавна част ("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 buf: String = String::new();
match reader.read_line(&mut buf) {
Ok(0) => Err(CsvError::InvalidHeader(String::from("EOF"))),
Ok(_) => {
let mut err: Option<Result<Csv<R>, CsvError>> = None;
let mut col: Vec<String> = Vec::new();
let mut temp = take_and_skip(buf.as_str(), ',');
let mut prev: Option<(&str, &str)> = temp;
let mut seen: HashSet<String> = HashSet::new();
while temp != None && err.is_none() {
prev = temp;
temp.map(|(a, b)| {
println!("[Csv::new] (|{}|, |{}|)", a, b);
err = Csv::check_and_push(&mut col, &mut seen, a);
temp = take_and_skip(b, ',');
});
}
if err.is_none() {
prev.map(|(_, b)| {
println!("[Csv::new] last element |{}|)", b);
err = Csv::check_and_push(&mut col, &mut seen, b);
});
}
match err {
None => {
let ret = Csv {
columns: col,
reader: reader,
selection: None
};
Ok(ret)
},
Some(e) => e
}
},
Err(e) => Err(CsvError::IO(e))
}
}
/// Функцията приема следващия ред за обработка и конструира `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 label = "[parse_line]";
let mut trimmed = line.trim();
let mut err: Option<Result<Row, CsvError>> = None;
let mut result = Row::new();
for key in self.columns.iter() {
println!("=================");
println!("{} Text: {}", label, trimmed);
if trimmed == "" {
err = Some(Err(CsvError::InvalidRow("Should have more entries".to_string())));
break;
}
match take_and_skip(trimmed, '"').map(|(a, b)| {
if a != "" {
err = Some(Err(CsvError::InvalidRow("Should begin with \"".to_string())));
}
b
}) {
None => err = Some(Err(CsvError::InvalidRow("Should have closing quote".to_string()))),
Some(text) => {
if err.is_none() {
take_and_skip(text, '"').map(|(a, b)| {
trimmed = take_and_skip(b, ',').map_or_else(|| "", |(_, tx)| tx.trim());
result.insert(key.clone(), a.to_string());
});
}
}
}
}
if trimmed != "" && err.is_none() {
err = Some(Err(CsvError::InvalidRow("Should have less entries".to_string())));
}
match err {
Some(e) => e,
None => Ok(result)
}
}
/// Подадената функция, "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 label = "[Csv::write_to]";
println!("{} Entered", label);
let mut ret: Result<(), CsvError>;
let mut cont = true;
// Write header
ret = writer.write(self.format_header().as_bytes()).map_or_else(|e| {
cont = false;
Err(CsvError::IO(e))
}, |_| Ok(()));
while cont {
let mut buf = String::new();
ret = self.reader.read_line(&mut buf).map_or_else(|e| Err(CsvError::IO(e)),
|sz| {
if sz == 0 {
cont = false;
return Ok(());
}
println!("{} buffer read: {}", label, buf);
self.parse_line(buf.as_str()).map_or_else(|e| Err(e),
|mp| {
let mut error = Ok(());
println!("{} Parsed map: {:?}", label, mp);
let do_write = self.selection.as_ref().map_or_else(|| true,
|cond| {
println!("{} Aplying condition", label);
return (cond)(&mp).map_or_else(|e| {
error = Err(e);
false
}, |val| val);
});
if do_write && !error.is_err() {
return match writer.write(self.format_map(mp).as_bytes()) {
Err(e) => Err(CsvError::IO(e)),
_ => Ok(())
};
}
return error;
})
});
if ret.is_err() {
cont = false;
}
}
println!("{} exiting with {:?}", label, ret);
return ret;
}
fn format_header(&self) -> String {
let mut res = String::from("");
for col in self.columns.iter().rev() {
res = format!(", {}{}", col, res.clone());
}
res = take_and_skip(res.as_str(), ',').map_or_else(|| "".to_string(), |(_, a)| a.trim().to_string());
res = format!("{}\n", res);
return res;
}
fn format_map(&self, map: HashMap<String, String>) -> String {
let mut res = String::from("");
let it = self.columns.iter().rev();
for col in it {
res = format!(", \"{}\"{}", map[col], res.clone());
}
res = take_and_skip(res.as_str(), ',').map_or_else(|| "".to_string(), |(_, a)| a.trim().to_string());
res = format!("{}\n", res);
println!("[Csv::format_map] about to write {}", res);
return res;
}
}
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 label = "[Csv::next]";
println!("{} Entered", label);
let mut res: Option<Self::Item> = None;
let mut read = true;
while read {
read = false;
let mut buf = String::new();
// Read line
res = self.reader.read_line(&mut buf).map_or_else(|e| Some(Err(CsvError::IO(e))),
|sz| {
println!("{} Size read {}", label, sz);
// Check size of read buffer
if sz == 0 {
return None;
} else {
// Parse
self.parse_line(buf.as_str())
.map_or_else(|e| Some(Err(e)), |mp| {
println!("{} Row: {:?}", label, mp);
let filter = self.selection.as_ref().map(|cond| (cond)(&mp));
match filter {
None => Some(Ok(mp)),
Some(rs) => match rs {
Err(e) => Some(Err(e)),
Ok(true) => Some(Ok(mp)),
Ok(false) => {
// Continue to next row
read = true;
None
}
}
}
})
}
});
}
println!("{} exiting....", label);
return res;
}
}
Лог от изпълнението
Compiling solution v0.1.0 (/tmp/d20210111-1538662-1v8xkou/solution) Finished test [unoptimized + debuginfo] target(s) in 5.45s 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 ... 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_single_column_no_data stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `0`, right: `1`', tests/solution_test.rs:178:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ---- solution_test::test_parsing_helpers_for_unicode stdout ---- [skip_next] Text |яга| current: |↓| target |↓| eq? |true| thread 'main' panicked at 'assertion failed: `(left == right)` left: `("ба", "ба↓яга")`, right: `("баба", "↓яга")`', tests/solution_test.rs:135:5 ---- solution_test::test_take_and_skip stdout ---- [skip_next] Text |two| current: |/| target |/| eq? |true| [take_and_skip] returning (|one|, |two|) [skip_next] Text |а/яга| current: |б| target |/| eq? |false| thread 'main' panicked at 'assertion failed: `(left == right)` left: `None`, 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_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. 11 passed; 4 failed; 0 ignored; 0 measured; 0 filtered out error: test failed, to rerun pass '--test solution_test'