summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs54
-rw-r--r--src/qif.rs131
2 files changed, 113 insertions, 72 deletions
diff --git a/src/main.rs b/src/main.rs
index 8874ea9..37644c0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,42 +1,30 @@
-extern crate regex;
-#[macro_use] extern crate lazy_static;
-extern crate structopt;
-#[macro_use] extern crate structopt_derive;
-
pub mod qif;
-use structopt::StructOpt;
-
+use clap::Parser;
use qif::*;
-use std::io::prelude::*;
-use std::io::*;
-
-use std::ffi::{OsString, OsStr};
-use std::path::PathBuf;
-use std::result::Result;
-
-use std::fs::File;
-use std::error::Error;
-
-fn parse_filepath(str: &OsStr) -> Result<PathBuf, OsString> {
- let path: PathBuf = ::std::convert::From::from(str);
- if path.is_file() {
- Ok(path)
- } else {
- Err(str.to_os_string())
- }
-}
-
-#[derive(StructOpt, Debug)]
-#[structopt(name = "Qif Parser", about = "Qif file preprocessor to decrease duplication when importing to gnucash")]
+use std::{
+ error::Error,
+ fs::File,
+ io::{prelude::*, *},
+ path::PathBuf,
+ result::Result,
+};
+
+/// Qif file preprocessor to decrease duplication when importing to gnucash
+#[derive(Parser, Debug)]
+#[structopt(
+ name = "Qif Parser",
+ version, about, long_about = None
+)]
struct CliArgs {
- #[structopt(help = "Files to preprocess", required, parse(try_from_os_str="parse_filepath"))]
- files: Vec<PathBuf>
+ /// Files to preprocess
+ #[arg(value_hint = clap::ValueHint::FilePath)]
+ files: Vec<PathBuf>,
}
-fn main() -> Result<(), Box<Error>> {
- let args = CliArgs::from_args();
-
+fn main() -> Result<(), Box<dyn Error>> {
+ let args = CliArgs::parse();
+
for filepath in &args.files {
let file = File::open(filepath)?;
let file_reader = BufReader::new(file);
diff --git a/src/qif.rs b/src/qif.rs
index 5249677..f93dc66 100644
--- a/src/qif.rs
+++ b/src/qif.rs
@@ -1,18 +1,19 @@
-use std::fmt;
+use lazy_static::lazy_static;
use regex::Regex;
-use std::result::Result;
use std::error::Error;
+use std::fmt;
+use std::result::Result;
pub struct QifFile {
header: String,
- entries: Vec<QifEntry>
+ entries: Vec<QifEntry>,
}
impl QifFile {
pub fn new(header: String) -> QifFile {
QifFile {
- header: header,
- entries: Vec::new()
+ header,
+ entries: Vec::new(),
}
}
@@ -40,7 +41,7 @@ impl fmt::Display for QifFile {
pub struct QifEntry {
date: String,
amount: String,
- description: String
+ description: String,
}
const DATE_PREFIX: &str = "D";
@@ -56,7 +57,7 @@ impl QifEntry {
(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()
+ description: description.chars().skip(1).collect(),
}),
(None, _, _) => Err(QifParsingError::MissingDate),
(_, None, _) => Err(QifParsingError::MissingAmount),
@@ -65,65 +66,117 @@ impl QifEntry {
}
pub fn is_empty(&self) -> bool {
- self.amount == String::from("0")
+ &self.amount == "0" || &self.amount == "0.00"
}
pub fn clean_description(&self) -> String {
- let replaced_date = replace_date(&self.description);
- replace_common(&replaced_date)
+ let mut new_description = self.description.clone();
+ for algorithm in [
+ remove_date,
+ remove_card_number,
+ replace_common,
+ remove_payment_provider,
+ ] {
+ new_description = algorithm(&new_description);
+ }
+ new_description
}
}
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()
+ write!(
+ f,
+ "{}{}\n{}{}\n{}{}",
+ DATE_PREFIX,
+ self.date,
+ AMOUNT_PREFIX,
+ self.amount,
+ DESCRIPTION_PREFIX,
+ self.clean_description()
)
}
}
-fn replace_date(text: &str) -> String {
+fn remove_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();
+ 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 remove_card_number(text: &str) -> String {
+ lazy_static! {
+ static ref CARD_NUM_REGEX: Regex = Regex::new(r"(\d{6}\*+\d{4}|\d{16})").unwrap();
+ }
+ CARD_NUM_REGEX.replace_all(text, "").trim().to_string()
+}
+
+fn remove_payment_provider(text: &str) -> String {
+ lazy_static! {
+ static ref PURCH_REGEX: Regex =
+ Regex::new(r"(?i)^(purch( payfast\*)?|pp +\*|c\*|yoco +\*|ikh\*)").unwrap();
+ }
+ PURCH_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"),
- );
+ (r"(?i)(pick n pay|pnp)", "Pick n Pay"),
+ (r"(?i)checkers", "Checkers"),
+ (r"(?i)WOOLWORTHS", "Woolworths"),
+ (r"(?i)clicks", "Clicks"),
+ (r"(?i)dischem", "Dischem"),
+ (r"(?i)spar", "Spar"),
+ (r"(?i)(disc memb|disc prem)", "Discovery medical aid"),
+ (r"(?i)10XRA", "10X Retirement Annuity"),
+ (r"(?i)NEDABF/MFC", "Nedbank MFC"),
+ (r"(?i)SARSEFLNG", "SARS Efiling"),
+ (r"(?i)Crazy store", "Crazy Store"),
+ (r"^PNA", "PNA"),
+ (r"^BWH", "Builders Warehouse"),
+ (r"^MCD ", "McDonalds"),
+ (r"^MRP ", "Mr Price"),
+ (r"NakedIn", "Naked Insurance"),
+ (r"(?i)sahl", "SA Home Loans"),
+ (r"(?i)gautrain", "Gautrain"),
+ (r"(?i)BYC DEBIT", "TO SAVINGS POCKET"),
+ (r"(?i)AFRIHOST", "Afrihost"),
+ (r"(?i)DIALDIRECT", "Dialdirect"),
+ (r"(?i)STEERS", "Steers"),
+ (r"(?i)CELL C", "Cell C"),
+ (r"(?i)ELECTRICITY", "Electricity"),
+ (r"(?i)(COUNTRY VIEW|STAR STOP|Shell|Sasol|Engen)", "Petrol"),
+ (r"(?i)kung ?-?fu", "Kungfu Kitchen"),
+ (r"(?i)^atm cash", "Cash"),
+ (r"(?i)DRS CL & ME LA", "Medipark 24")
+ )
+ .into_iter()
+ .map(|(from, to)| (Regex::new(from).unwrap(), to))
+ .collect();
}
- COMMON_NAMES.iter().fold(
- text, |acc, next| if next.0.is_match(acc) {next.1} else {acc}
- ).to_string()
+ COMMON_NAMES
+ .iter()
+ .filter_map(|(rule, replacement)| {
+ if rule.is_match(text) {
+ Some(replacement.to_string())
+ } else {
+ None
+ }
+ })
+ .next()
+ .unwrap_or_else(|| text.to_owned())
+ .to_string()
}
-
#[derive(Debug)]
pub enum QifParsingError {
MissingDate,
MissingAmount,
- MissingDescription
+ MissingDescription,
}
impl fmt::Display for QifParsingError {