CSV Filter

Предадени решения

Краен срок:
11.01.2021 17:00
Точки:
15

Срокът за предаване на решения е отминал

use solution::*;
use std::io::{self, Read, BufRead, BufReader};
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));
}
}
}
macro_rules! timeout {
($time:expr, $body:block) => {
use std::panic::catch_unwind;
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
if let Err(e) = catch_unwind(|| $body) {
sender.send(Err(e)).unwrap();
return;
}
match sender.send(Ok(())) {
Ok(()) => {}, // everything good
Err(_) => {}, // we have been released, don't panic
}
});
if let Err(any) = receiver.recv_timeout(std::time::Duration::from_millis($time)).unwrap() {
panic!("{}", any.downcast_ref::<String>().unwrap());
}
}
}
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) { }
}
#[test]
fn test_csv_basic() {
timeout!(1000, {
let reader = BufReader::new(r#"
name, age, birth date
"Douglas Adams", "42", "1952-03-11"
"#.trim().as_bytes());
let mut csv = Csv::new(reader).unwrap();
assert_eq!(csv.columns, &["name", "age", "birth date"]);
let row = csv.next().unwrap().unwrap();
assert_eq!(row["name"], "Douglas Adams");
assert_eq!(row["age"].parse::<u32>().unwrap(), 42);
assert_eq!(row["birth date"], "1952-03-11");
assert!(csv.next().is_none());
});
}
#[test]
fn test_csv_empty() {
assert_match!(Csv::new(BufReader::new("".as_bytes())).err(), Some(CsvError::InvalidHeader(_)));
}
#[test]
fn test_csv_duplicate_columns() {
let data = r#"
name, age, age
"Douglas Adams", "42", "Douglas Adams"
"#.trim().as_bytes();
assert_match!(Csv::new(BufReader::new(data)).err(), Some(CsvError::InvalidHeader(_)));
let data = r#"
name, age, name
"Douglas Adams", "42", "Douglas Adams"
"#.trim().as_bytes();
assert_match!(Csv::new(BufReader::new(data)).err(), Some(CsvError::InvalidHeader(_)));
let data = r#"
foo, foo, bar
"Douglas Adams", "42", "Douglas Adams"
"#.trim().as_bytes();
assert_match!(Csv::new(BufReader::new(data)).err(), Some(CsvError::InvalidHeader(_)));
}
#[test]
fn test_skip_next() {
assert_eq!(skip_next("[test]", '['), Some("test]"));
assert_eq!(skip_next("<test>", '<'), Some("test>"));
assert_eq!(skip_next("[test]", '<'), None);
assert_eq!(skip_next("", '<'), None);
}
#[test]
fn test_take_until() {
assert_eq!(take_until("one/two", '/'), ("one", "/two"));
assert_eq!(take_until("баба/яга", '/'), ("баба", "/яга"));
assert_eq!(take_until("", '/'), ("", ""));
}
#[test]
fn test_take_and_skip() {
assert_eq!(take_and_skip("one/two", '/'), Some(("one", "two")));
assert_eq!(take_and_skip("баба/яга", '/'), Some(("баба", "яга")));
assert_eq!(take_until("", '/'), ("", ""));
}
#[test]
fn test_parsing_helpers_for_unicode() {
assert_eq!(skip_next("↓яга", '↓'), Some("яга"));
assert_eq!(take_until("баба↓яга", '↓'), ("баба", "↓яга"));
assert_eq!(take_and_skip("баба↓яга", '↓'), Some(("баба", "яга")));
}
#[test]
fn test_csv_parse_line() {
let reader = BufReader::new("name, age, birth date".trim().as_bytes());
let mut csv = Csv::new(reader).unwrap();
let row = csv.parse_line(r#""Basic Name","13","2020-01-01""#).unwrap();
assert_eq! {
(row["name"].as_str(), row["age"].as_str(), row["birth date"].as_str()),
("Basic Name", "13", "2020-01-01"),
};
let row = csv.parse_line(r#"" Name With Spaces "," 13 ","0-0-0""#).unwrap();
assert_eq! {
(row["name"].as_str(), row["age"].as_str(), row["birth date"].as_str()),
(" Name With Spaces ", " 13 ", "0-0-0"),
};
}
#[test]
fn test_csv_parse_line_with_commas() {
let reader = BufReader::new("age, name".trim().as_bytes());
let mut csv = Csv::new(reader).unwrap();
let row = csv.parse_line(r#""13", "Name, Basic""#).unwrap();
assert_eq! {
(row["name"].as_str(), row["age"].as_str()),
("Name, Basic", "13"),
};
let row = csv.parse_line(r#""13, or maybe 14","Basic Name""#).unwrap();
assert_eq! {
(row["name"].as_str(), row["age"].as_str()),
("Basic Name", "13, or maybe 14"),
};
}
#[test]
fn test_csv_single_column_no_data() {
let mut csv = Csv::new(BufReader::new("singe column".as_bytes())).unwrap();
assert_eq!(csv.columns.len(), 1);
assert!(csv.next().is_none());
}
#[test]
fn test_csv_iterating_with_no_selection() {
timeout!(1000, {
let reader = BufReader::new(r#"
name, age, birth date
"Douglas Adams", "42", "1952-03-11"
"Gen Z. Person", "20", "2000-01-01"
"Ada Lovelace", "36", "1815-12-10"
"#.trim().as_bytes());
let csv = Csv::new(reader).unwrap();
let filtered_names = csv.map(|row| row.unwrap()["name"].clone()).collect::<Vec<_>>();
assert_eq!(filtered_names, &["Douglas Adams", "Gen Z. Person", "Ada Lovelace"]);
});
}
#[test]
fn test_csv_iterating_with_a_selection() {
let reader = BufReader::new(r#"
name, age, birth date
"Douglas Adams", "42", "1952-03-11"
"Gen Z. Person", "20", "2000-01-01"
"Ada Lovelace", "36", "1815-12-10"
"#.trim().as_bytes());
let mut csv = Csv::new(reader).unwrap();
csv.apply_selection(|row| {
let age = row.get("age").unwrap();
let age = age.parse::<u32>().unwrap();
Ok(age > 30)
});
let filtered_names = csv.map(|row| row.unwrap()["name"].clone()).collect::<Vec<_>>();
assert_eq!(filtered_names, &["Douglas Adams", "Ada Lovelace"]);
}
#[test]
fn test_csv_writing_without_a_selection() {
timeout!(1000, {
let reader = BufReader::new(r#"
name, age ,birth date
"Douglas Adams","42","1952-03-11"
"Gen Z. Person", "20" , "2000-01-01"
"Ada Lovelace","36","1815-12-10"
"#.trim().as_bytes());
let csv = Csv::new(reader).unwrap();
let mut output = Vec::new();
csv.write_to(&mut output).unwrap();
let output_lines = output.lines().
map(Result::unwrap).
collect::<Vec<String>>();
assert_eq!(output_lines, &[
"name, age, birth date",
"\"Douglas Adams\", \"42\", \"1952-03-11\"",
"\"Gen Z. Person\", \"20\", \"2000-01-01\"",
"\"Ada Lovelace\", \"36\", \"1815-12-10\"",
]);
});
}
#[test]
fn test_csv_selection_and_writing() {
timeout!(1000, {
let reader = BufReader::new(r#"
name, age ,birth date
"Douglas Adams","42","1952-03-11"
"Gen Z. Person", "20" , "2000-01-01"
"Ada Lovelace","36","1815-12-10"
"#.trim().as_bytes());
let mut csv = Csv::new(reader).unwrap();
csv.apply_selection(|row| Ok(row["name"].contains(".")));
let mut output = Vec::new();
csv.write_to(&mut output).unwrap();
let output_lines = output.lines().
map(Result::unwrap).
collect::<Vec<String>>();
assert_eq!(output_lines, &[
"name, age, birth date",
"\"Gen Z. Person\", \"20\", \"2000-01-01\"",
]);
});
}
#[test]
fn test_csv_writing_without_any_rows() {
timeout!(1000, {
let reader = BufReader::new(r#"
name, age, birth date
"#.trim().as_bytes());
let csv = Csv::new(reader).unwrap();
let mut output = Vec::new();
csv.write_to(&mut output).unwrap();
let output_lines = output.lines().
map(Result::unwrap).
collect::<Vec<String>>();
assert_eq!(output_lines, &[
"name, age, birth date",
]);
});
}

В това домашно ще прочетем файл с Comma-Separated Value (CSV) формат, или поне някаква негова форма. Няма да спазваме стандартите кой знае колко стриктно, просто ще имплементираме четене и обработка на нещо подобно на CSV.

Общи неща

Примерен файл ще изглежда горе-долу така:

name, age, birth date
"Douglas Adams", "42", "1952-03-11"
"Ada, Countess of Lovelace", "36", "1815-12-10"

Първия ред винаги ще очакваме, че е поредица от имената на колоните, разделени със запетайки и може би с интервали. Самите имена е забранено да съдържат запетайки (няма как да се изпарси) и няма да имат символи за нов ред (иначе няма да е "първия ред"), но иначе може да съдържат каквото си искате (стига да е валиден UTF8). "Header"-а или заглавната част на това конкретно CSV ще включва "name", "age", и "birth date". Не може да има дублиране на колони.

Оттам нататък, всеки следващ ред съдържа стойностите, които съответстват на въпросната колона. Забележете, че те могат да съдържат запетайки, затова, за всеки случай, всяка стойност е оградена с двойни кавички.

Между елементите може да има интервали, може и да няма, но стойността на елемента е винаги UTF8 низа между двойните кавички. Няма да има нови редове в низовете и няма да има никакви escape-нати ("екранирани") кавички.

Парсене на низове

Започваме с някои помощни функции за обработка на низове.

/// Проверява че следващия символ във входния низ `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> {
    todo!()
}

/// Търси следващото срещане на символа `target` в низа `input`. Връща низа до този символ и низа
/// от този символ нататък, в двойка.
///
/// Ако не намери `target`, връща оригиналния низ и празен низ като втори елемент в двойката.
///
/// take_until(" foo/bar ", '/') //=> (" foo", "/bar ")
/// take_until("foobar", '/')    //=> ("foobar", "")
///
pub fn take_until(input: &str, target: char) -> (&str, &str) {
    todo!()
}

/// Комбинация от горните две функции -- взема символите до `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)> {
    todo!()
}

Забележете, че символа който търсим, или по който разбиваме, е char, тоест е utf-8 стойност. Възможно е да е примерно ѝ. От друга страна, за CSV-та вероятно тези функции ще ги използвате (ако искате) само със запетайка и кавичка, които са си валиден ASCII.

Тоест, ако искате да имплементирате и използвате тези функции, така че да работят с байтове и индексиране на низове, можете. Ще се погрижим да има само един тест, който изисква UTF8 от тях, така че най-много ще изгубите 1 точка.

Все пак, не е твърде трудно да ползвате Chars, така че ви съветваме да го направите. Функциите char_indices, len_utf8, split_at също могат да ви свършат работа -- както прецените.

Грешки

#[derive(Debug)]
pub enum CsvError {
    IO(std::io::Error),
    ParseError(String),
    InvalidHeader(String),
    InvalidRow(String),
    InvalidColumn(String),
}

Възможните грешки, които могат да се върнат от различните функции в домашното -- ще има конкретни инструкции по-надолу. Няма да проверяваме конкретните съдържания на грешките, само дали са правилния вариант на enum-a -- но ако слагате полезна информация, може да ви е по-лесно да дебъгвате. (Примерно, ParseError което съдържа какво сте се опитали да изпарсите, или InvalidRow, което съдържа къде парсенето е ударило на камък.)

Грешките ParseError и InvalidColumn всъщност не се използват никъде в самата имплементация, но са удобни за "selection" функцията. Ако ги махнете, нищо лошо няма да се случи, отвъд това, че един от по-долните примери няма да ви се компилира.

Може да ви е удобно за по-долу да имплементирате From на някои типове грешки, както обяснихме в лекцията за error handling. Можете и да ползвате и map_err, и разнообразни други неща. Този cheatsheet също може да ви бъде удобен, за да видите какви опции имате с Option.

Не е задължително да имплементирате std::error::Error и std::fmt::Display за грешките -- може да го направите за completeness, ако искате.

CSV редове

Започваме с конкретните за CSV неща. Първо, удобна дефиниция за един "ред":

use std::collections::HashMap;

type Row = HashMap<String, String>;

Забележете, че този тип не е публичен -- никъде в тестовете няма да го използваме, просто е удобно име за нещо, което ще се среща по-долу. Спокойно можете просто да ползвате HashMap директно.

В случай, че не сте ползвали подобна структура от данни досега ("асоциативен масив" е името, което се използва обикновено на български), би трябвало да ви е достатъчно да знаете как да пишете в него и как да четете от него:

let mut map: HashMap<String, String> = HashMap::new();
map.insert(String::from("name"), String::from("Billy"));
map.insert(String::from("name"), String::from("Mandy"));

println!("{:?}", map.get("name"));
// => Some("Mandy")
println!("{:?}", map.get("Grim"));
// => None

За разлика от масив/вектор, тук ключовете не са числа, а са низове (в случая). Първия от generic типовете е този на ключа, а втория -- този на стойността. Структурата не позволява дубликати -- ако вкарате "name" за втори път, само втората стойност се запазва.

Имайте предвид, че реда на ключовете е произволен, така че като минавате по колони, винаги използвайте columns полето. Ако итерирате по всички ключове на map-а, те няма да са в ред на записване.

Метода contains_key може да ви е полезен.

Забележете, че функцията get ще приеме &str, дори и типа на ключа да е String. Ако ви е любопитно, ето документацията за метода get -- входа трябва да имплементира Borrow trait-a.

CSV

Декларацията на типа очакваме да изглежда горе-долу така:

use std::io::BufRead;

pub struct Csv<R: BufRead> {
    pub columns: Vec<String>,
    reader: R,
    selection: Option<Box<dyn Fn(&Row) -> Result<bool, CsvError>>>,
}

Както обикновено, ако едно поле е pub, тогава очакваме да е дефинирано точно както е дадено -- тестовете ни ще го достъпват. За останалите полета, в случая reader и selection, приемете ги като препоръка.

Идеята е, че ще конструираме Csv стойност с подаден вход нещо, от което да чете редове -- файл, сокет, или просто вектор от байтове. Ще можем да поискаме следващия ред, обработен спрямо някакви правила, които ще се съхраняват в selection. Ето как очакваме да изглежда имплементацията на методите на типа:

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> {
        todo!()
    }

    /// Функцията приема следващия ред за обработка и конструира `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> {
        todo!()
    }

    /// Подадената функция, "callback", се очаква да се запази и да се използва по-късно за
    /// филтриране -- при итерация, само редове, за които се връща `true` се очаква да се извадят.
    ///
    /// Би трябвало `callback` да се вика от `.next()` и от `.write_to()`, вижте описанията на тези
    /// методи за детайли.
    ///
    pub fn apply_selection<F>(&mut self, callback: F)
        where F: Fn(&Row) -> Result<bool, CsvError> + 'static
    {
        todo!()
    }

    /// Извикването на този метод консумира CSV-то и записва филтрираното съдържание в подадената
    /// `Write` стойност. Вижте по-долу за пример и детайли.
    ///
    /// Грешките, които се връщат са грешките, които идват от използваните други методи, плюс
    /// грешките от писане във `writer`-а, опаковани в `CsvError::IO`.
    ///
    /// В зависимост от това как си имплементирате метода, `mut` може би няма да ви трябва за
    /// `self` -- ако имате warning-и, просто го махнете.
    ///
    pub fn write_to<W: Write>(mut self, mut writer: W) -> Result<(), CsvError> {
        todo!()
    }
}

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> {
        todo!()
    }
}

Итерация с филтриране

Нашия Csv тип ще си има подадена функция за филтриране, която ще се наложи да запазите като поле, препоръчваме в Box както сме го направили по-горе. При всяко извикване на next() ще искаме функцията да се извиква, за да проверим дали да върнем обработения ред или не. Ето един пример, който би трябвало да обясни каква е идеята:

// Подготвяме данните:
let reader = BufReader::new(r#"
    name, age, birth date
    "Douglas Adams", "42", "1952-03-11"
    "Gen Z. Person", "20", "2000-01-01"
    "Ada Lovelace", "36", "1815-12-10"
"#.trim().as_bytes());

// Конструираме си CSV-то:
let mut csv = Csv::new(reader).unwrap();

// Инсталираме условието -- само редове с възраст над 30 ще останат:
csv.apply_selection(|row| {
    let age = row.get("age").ok_or_else(|| CsvError::InvalidColumn(String::from("age")))?;
    let age = age.parse::<u32>().map_err(|_| CsvError::ParseError(String::from(age)))?;

    Ok(age > 30)
});

// Итерираме през резултата:
while let Some(row) = csv.next() {
    println!("{:?}", row.unwrap().get("name"));
    // => Some("Douglas Adams")
    // => Some("Ada Lovelace")
}

Както виждате, анонимната функция за условието става малко досадна, но има начини, по които може да се опрости, особено в тестове. При всички положения, се надяваме примера да е достатъчно ясен за това как се очаква да работи итерацията.

Писане на нов CSV

Всичко това съществува с цел в крайна сметка да използвате write_to метода, за да запишете прочетения текст в нов. Формата се очаква да е подобен на входа:

  • Всяка една от колоните трябва да бъде обвита с кавички.
  • Отделните колони са разделени със ", " (запетайка и точно 1 интервал)
  • Реда на стойностите е същия като реда на колоните в оригиналния текст
  • Реда на редовете е същия като реда на редовете в оригиналния текст, само че са филтрирани по начина, по който сме указали.

Пример:

let reader = BufReader::new(r#"
    name,  age    ,birth date
    "Douglas Adams","42","1952-03-11"
    "Gen Z. Person",    "20"   ,   "2000-01-01"
    "Ada Lovelace","36","1815-12-10"
"#.trim().as_bytes());

let mut csv = Csv::new(reader).unwrap();
csv.apply_selection(|row| {
    Ok(row["age"].parse::<u32>().unwrap() < 40)
});

let mut output = Vec::new();
csv.write_to(&mut output).unwrap();

println!("{}", String::from_utf8(output).unwrap());
// name, age, birth date
// "Gen Z. Person", "20", "2000-01-01"
// "Ada Lovelace", "36", "1815-12-10"

Както виждате, входа този път има разнообразно количество разделящи интервали около запетайките, но изхода си има точно 1. При вход е все тая дали ги има или не -- лесно може да ги trim-нем. При изход избираме един конкретен формат и той е гореописания -- с допълнителния интервал.

Също така може да видите как използваме малко по-кратък и по-смел метод за бърникане в HashMap-a: [] вместо .get(), което би panic-нало, ако има несъответствие. И също .unwrap() ;).

Базов тест: 03/test_basic.rs

Задължително прочетете (или си припомнете): Указания за предаване на домашни