Решение на CSV Filter от Георги Катевски
Към профила на Георги Катевски
Резултати
- 8 точки от тестове
- 0 бонус точки
- 8 точки общо
- 8 успешни тест(а)
- 7 неуспешни тест(а)
Код
/// Проверява че следващия символ във входния низ `input` е точно `target`.
///
/// Ако низа наистина започва с този символ, връща остатъка от низа без него, пакетиран във
/// `Some`. Иначе, връща `None`. Примерно:
///
/// skip_next("(foo", '(') //=> Some("foo")
/// skip_next("(foo", ')') //=> None
/// skip_next("", ')') //=> None
///
///
///
///
use std::io::{self, Read, BufRead, BufReader};
// За тестване че някакъв резултат пасва на някакъв pattern:
macro_rules! assert_match {
($expr:expr, $pat:pat) => {
if let $pat = $expr {
// all good
} else {
assert!(false, "Expression {:?} does not match the pattern {:?}", $expr, stringify!($pat));
}
}
}
pub fn skip_next(input: &str, target: char) -> Option<&str> {
if input.len()==0 && target=='/'
{
return None;
}
if input.len()==0
{
return None;
}
let ch = input.chars().nth(0).unwrap();
if ch==target
{
return input.get(1..);
}
else
{
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 str1=input.to_string();
let mut count=0;
for i in str1.chars()
{
if i==target
{
break;
}
count+=1;
}
return input.split_at(count as usize);
}
/// Комбинация от горните две функции -- взема символите до `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)> {
if input.len()==0 && target=='/'
{
return None;
}
let (first,last)= take_until(input, target);
println!("{:?}",first);
if skip_next(last, target)==None
{
return None;
}
let k=skip_next(last, target).unwrap();
println!("{:?}",k);
return Some((first,k));
}
#[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::collections::HashSet;
pub struct Csv<R: BufRead> {
pub columns: Vec<String>,
reader: R,
selection: Option<Box<dyn Fn(&Row) -> Result<bool, CsvError>>>,
}
use std::io::Error;
use std::io::Write;
impl<R: BufRead> Csv<R> {
/// Конструира нова стойност от подадения вход. Третира се като "нещо, от което може да се чете
/// ред по ред".
///Done
/// Очакваме да прочетете първия ред от входа и да го обработите като заглавна част ("header").
/// Това означава, че първия ред би трябвало да включва имена на колони, разделени със
/// запетайки и може би празни места. Примерно:
///
/// - name, age
/// - name,age,birth date
///
/// В случай, че има грешка от викане на методи на `reader`, тя би трябвало да е `io::Error`.
/// върнете `CsvError::IO`, което опакова въпросната грешка.
///Done
/// Ако първия ред е празен, прочитането ще ви върне 0 байта. Примерно, `read_line` връща
/// `Ok(0)` в такъв случай. Това означава, че нямаме валиден header -- нито една колона няма,
/// очакваме грешка `CsvError::InvalidHeader`.
///Done
/// Ако има дублиране на колони -- две колони с едно и също име -- също върнете
/// `CsvError::InvalidHeader`.
///Done?
/// Ако всичко е наред, върнете конструирана стойност, на която `columns` е списък с колоните,
/// в същия ред, в който са подадени, без заобикалящите ги празни символи (използвайте
/// `.trim()`).
///
///
///
fn check_duplicates(vec: &[String]) -> bool
{
let mut hash=HashSet::new();
for i in vec
{
hash.insert(i);
}
if hash.len() == vec.len()
{
return true;
}
else
{
return false;
}
}
pub fn new(mut reader: R) -> Result<Self, CsvError> {
let mut buffer=String::new();
if reader.read_line(&mut buffer).is_err()
{
return Err(CsvError::IO(Error::last_os_error()));
}
if buffer.len()== 0
{
return Err(CsvError::InvalidHeader("Invalid Header1".to_string()));
}
let buff=buffer.trim();
let vec:Vec<&str> = buff.split(",").collect();
let mut vec2:Vec<String>=Vec::new();
for i in vec
{
let m=i.to_string();
vec2.push(m.trim().to_string());
}
if !Self::check_duplicates(&vec2)
{
return Err(CsvError::InvalidHeader("Invalid Header2".to_string()));
}
return Ok(Self{columns:vec2,reader,selection:Some(Box::new(|_| Ok(true)))} );
}
/// Функцията приема следващия ред за обработка и конструира `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 hash:Row=Row::new();
let mut str3="".to_string();
let mut flag=true;
let mut count_coloumn=0;
for i in line.chars()
{
if i=='"' && flag
{
flag=false;
continue;
}
if !flag && i!='"'
{
str3.push(i);
}
if i =='"' && !flag
{
flag=true;
// print!("{:?} ",str3);
// println!("{:?} ", self.columns[count_coloumn]);
if count_coloumn>=3
{
return Err(CsvError::InvalidRow("sdsd".to_string()));
}
hash.insert(self.columns[count_coloumn].to_string(),str3.to_string());
count_coloumn+=1;
str3=String::new();
}
if i ==' ' && flag
{
continue;
}
}
if count_coloumn!=3
{
return Err(CsvError::InvalidRow("sdsd".to_string()));
}
println!("{:?}",count_coloumn);
return Ok(hash);
}
/// Подадената функция, "callback", се очаква да се запази и да се използва по-късно за
/// филтриране -- при итерация, само редове, за които се връща `true` се очаква да се извадят.
///
/// Би трябвало `callback` да се вика от `.next()` и от `.write_to()`, вижте описанията на тези
/// методи за детайли.
///
pub fn apply_selection<F>(&mut self, callback: F)
where F: Fn(&Row) -> Result<bool, CsvError> + 'static
{
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> {
//GRESHNO
let lines_iter = self.reader.lines().map(|l| l.unwrap());
writeln!(writer, "{}", self.columns.join(", "));
//cikul
let mut vec=Vec::new();
for pair in lines_iter.into_iter()
{
let s=pair.to_string();
let t:String=s.trim().to_string();
println!("{:?}",t);
vec.push(t);
}
println!("{:?}",vec);
for i in vec
{
writeln!(& mut writer,"{}", i);
}
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 buffer=String::new();
if self.reader.read_line(&mut buffer).is_err()
{
//todo
return Some(Err(CsvError::IO(io::Error::from_raw_os_error(10022))));
}
if buffer.len()==0
{
return None;
}
else
{
let str_:&str=&buffer;
if self.parse_line(str_).is_err()
{
return Some(self.parse_line(str_));
}
else
{
let row=self.parse_line(str_);
let result= (self.selection.as_ref().unwrap())(row.as_ref().unwrap());
match result
{
Ok(false) => (),
Ok(true) => return Some(row),
Err(e) => return Some(Err(e)),
};
}
return None;
}
}
}
struct ErroringReader {}
impl Read for ErroringReader {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::Other, "read error!"))
}
}
impl BufRead for ErroringReader {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
Err(io::Error::new(io::ErrorKind::Other, "fill_buf error!"))
}
fn consume(&mut self, _amt: usize) { }
}
Лог от изпълнението
Compiling solution v0.1.0 (/tmp/d20210111-1538662-o2maov/solution) warning: unused import: `BufReader` --> src/lib.rs:14:40 | 14 | use std::io::{self, Read, BufRead, BufReader}; | ^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default warning: unused macro definition --> src/lib.rs:17:5 | 17 | / macro_rules! assert_match { 18 | | ($expr:expr, $pat:pat) => { 19 | | if let $pat = $expr { 20 | | // all good ... | 24 | | } 25 | | } | |_____^ | = note: `#[warn(unused_macros)]` on by default warning: variable does not need to be mutable --> src/lib.rs:326:31 | 326 | pub fn write_to<W: Write>(mut self, mut writer: W) -> Result<(), CsvError> { | ----^^^^ | | | help: remove this `mut` | = note: `#[warn(unused_mut)]` on by default warning: unused `std::result::Result` that must be used --> src/lib.rs:332:1 | 332 | writeln!(writer, "{}", self.columns.join(", ")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default = note: this `Result` may be an `Err` variant, which should be handled = note: this warning originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) warning: unused `std::result::Result` that must be used --> src/lib.rs:348:5 | 348 | writeln!(& mut writer,"{}", i); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this `Result` may be an `Err` variant, which should be handled = note: this warning originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) warning: 5 warnings emitted Finished test [unoptimized + debuginfo] target(s) in 4.56s 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 ... FAILED 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 ... 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 ... 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_iterating_with_a_selection stdout ---- 3 3 3 3 3 3 thread 'main' panicked at 'assertion failed: `(left == right)` left: `["Douglas Adams", "Gen Z. Person", "Ada Lovelace"]`, right: `["Douglas Adams", "Ada Lovelace"]`', tests/solution_test.rs:219:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ---- solution_test::test_csv_parse_line_with_commas stdout ---- thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidRow("sdsd")', tests/solution_test.rs:162:56 ---- solution_test::test_csv_selection_and_writing stdout ---- "\"Douglas Adams\",\"42\",\"1952-03-11\"" "\"Gen Z. Person\", \"20\" , \"2000-01-01\"" "\"Ada Lovelace\",\"36\",\"1815-12-10\"" ["\"Douglas Adams\",\"42\",\"1952-03-11\"", "\"Gen Z. Person\", \"20\" , \"2000-01-01\"", "\"Ada Lovelace\",\"36\",\"1815-12-10\""] thread '<unnamed>' panicked at 'assertion failed: `(left == right)` left: `["name, age, birth date", "\"Douglas Adams\",\"42\",\"1952-03-11\"", "\"Gen Z. Person\", \"20\" , \"2000-01-01\"", "\"Ada Lovelace\",\"36\",\"1815-12-10\""]`, right: `["name, age, birth date", "\"Gen Z. Person\", \"20\", \"2000-01-01\""]`', tests/solution_test.rs:269:9 thread 'main' panicked at 'assertion failed: `(left == right)` left: `["name, age, birth date", "\"Douglas Adams\",\"42\",\"1952-03-11\"", "\"Gen Z. Person\", \"20\" , \"2000-01-01\"", "\"Ada Lovelace\",\"36\",\"1815-12-10\""]`, right: `["name, age, birth date", "\"Gen Z. Person\", \"20\", \"2000-01-01\""]`', tests/solution_test.rs:251:5 ---- solution_test::test_csv_writing_without_a_selection stdout ---- "\"Douglas Adams\",\"42\",\"1952-03-11\"" "\"Gen Z. Person\", \"20\" , \"2000-01-01\"" "\"Ada Lovelace\",\"36\",\"1815-12-10\"" ["\"Douglas Adams\",\"42\",\"1952-03-11\"", "\"Gen Z. Person\", \"20\" , \"2000-01-01\"", "\"Ada Lovelace\",\"36\",\"1815-12-10\""] thread '<unnamed>' panicked at 'assertion failed: `(left == right)` left: `["name, age, birth date", "\"Douglas Adams\",\"42\",\"1952-03-11\"", "\"Gen Z. Person\", \"20\" , \"2000-01-01\"", "\"Ada Lovelace\",\"36\",\"1815-12-10\""]`, right: `["name, age, birth date", "\"Douglas Adams\", \"42\", \"1952-03-11\"", "\"Gen Z. Person\", \"20\", \"2000-01-01\"", "\"Ada Lovelace\", \"36\", \"1815-12-10\""]`', tests/solution_test.rs:240:9 thread 'main' panicked at 'assertion failed: `(left == right)` left: `["name, age, birth date", "\"Douglas Adams\",\"42\",\"1952-03-11\"", "\"Gen Z. Person\", \"20\" , \"2000-01-01\"", "\"Ada Lovelace\",\"36\",\"1815-12-10\""]`, right: `["name, age, birth date", "\"Douglas Adams\", \"42\", \"1952-03-11\"", "\"Gen Z. Person\", \"20\", \"2000-01-01\"", "\"Ada Lovelace\", \"36\", \"1815-12-10\""]`', tests/solution_test.rs:224:5 ---- solution_test::test_parsing_helpers_for_unicode stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `None`, right: `Some("яга")`', tests/solution_test.rs:134:5 ---- solution_test::test_take_and_skip stdout ---- "one" "two" "ба" 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_iterating_with_a_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_parsing_helpers_for_unicode solution_test::test_take_and_skip solution_test::test_take_until test result: FAILED. 8 passed; 7 failed; 0 ignored; 0 measured; 0 filtered out error: test failed, to rerun pass '--test solution_test'