use std::fmt; use regex::Regex; use std::result::Result; use std::error::Error; pub struct QifFile { header: String, entries: Vec } impl QifFile { pub fn new(header: String) -> QifFile { QifFile { header: header, entries: Vec::new() } } pub fn push(&mut self, entry: QifEntry) { if !entry.is_empty() { self.entries.push(entry); } } } impl fmt::Display for QifFile { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut hat_separated = String::new(); for entry in &self.entries { hat_separated.push_str("\n"); hat_separated.push_str(&entry.to_string()); hat_separated.push_str("\n^"); } write!(f, "{}{}", self.header, hat_separated) } } pub struct QifEntry { date: String, amount: String, description: String } const DATE_PREFIX: &str = "D"; const AMOUNT_PREFIX: &str = "T"; const DESCRIPTION_PREFIX: &str = "M"; #[derive(Debug)] pub enum QifParsingError { MissingDate, MissingAmount, MissingDescription } impl fmt::Display for QifParsingError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } } impl Error for QifParsingError {} impl QifEntry { pub fn new(lines: &Vec) -> Result { let date = lines.iter().find(|l| l.starts_with(DATE_PREFIX)); let amount = lines.iter().find(|l| l.starts_with(AMOUNT_PREFIX)); let description = lines.iter().find(|l| l.starts_with(DESCRIPTION_PREFIX)); match (date, amount, description) { (Some(date), Some(amount), Some(description)) => Ok(QifEntry { date: date.chars().skip(1).collect(), amount: amount.chars().skip(1).collect(), description: description.chars().skip(1).collect() }), (None, _, _) => Err(QifParsingError::MissingDate), (_, None, _) => Err(QifParsingError::MissingAmount), (_, _, None) => Err(QifParsingError::MissingDescription), } } pub fn is_empty(&self) -> bool { self.amount == String::from("0") } pub fn clean_description(&self) -> String { let replaced_date = replace_date(&self.description); replace_common(&replaced_date) } } impl fmt::Display for QifEntry { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}\n{}{}\n{}{}", DATE_PREFIX, self.date, AMOUNT_PREFIX, self.amount, DESCRIPTION_PREFIX, self.clean_description() ) } } fn replace_date(text: &str) -> String { lazy_static! { static ref DATE_REGEX: Regex = Regex::new(r"(?i)(\d{2} )?(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)( \d{4})?").unwrap(); } DATE_REGEX.replace_all(text, "").trim().to_string() } fn replace_common(text: &str) -> String { lazy_static! { static ref COMMON_NAMES: Vec<(Regex, &'static str)> = vec!( (Regex::new(r"(?i)(pick n pay|pnp)").unwrap(), "Pick n Pay"), (Regex::new(r"(?i)checkers").unwrap(), "Checkers"), (Regex::new(r"(?i)WOOLWORTHS").unwrap(), "Woolworths"), (Regex::new(r"(?i)clicks").unwrap(), "Clicks"), (Regex::new(r"(?i)spar").unwrap(), "Spar"), (Regex::new(r"(?i)Crazy store").unwrap(), "Crazy Store"), (Regex::new(r"^PNA").unwrap(), "PNA"), (Regex::new(r"(?i)sahl").unwrap(), "SA Home Loans"), (Regex::new(r"(?i)gautrain").unwrap(), "Gautrain"), (Regex::new(r"(?i)BANK YOUR CHANGE DEBI").unwrap(), "TO SAVINGS POCKET"), (Regex::new(r"(?i)AFRIHOST").unwrap(), "Afrihost"), (Regex::new(r"(?i)DIALDIRECT").unwrap(), "Dialdirect"), (Regex::new(r"(?i)STEERS").unwrap(), "Steers"), (Regex::new(r"(?i)CELL C").unwrap(), "Cell C"), (Regex::new(r"(?i)ELECTRICITY").unwrap(), "Electricity"), (Regex::new(r"(?i)(COUNTRY VIEW|STAR STOP|Shell|Sasol)").unwrap(), "Petrol"), (Regex::new(r"(?i)kung ?-?fu").unwrap(), "Kungfu Kitchen"), ); } COMMON_NAMES.iter().fold( text, |acc, next| if next.0.is_match(acc) {next.1} else {acc} ).to_string() }