From c926cd6eceaa2522b557c3dc1f9d7ebe8002702d Mon Sep 17 00:00:00 2001 From: garhve Date: Thu, 1 Aug 2024 02:02:43 +0800 Subject: add function to print project summary --- src/.drawing.rs.swp | Bin 0 -> 12288 bytes src/.lib.rs.swp | Bin 0 -> 20480 bytes src/drawing.rs | 77 ++++++++++++++++++++++++++++ src/lib.rs | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 10 ++++ src/record.rs | 72 ++++++++++++++++++++++++++ 6 files changed, 303 insertions(+) create mode 100644 src/.drawing.rs.swp create mode 100644 src/.lib.rs.swp create mode 100644 src/drawing.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/record.rs (limited to 'src') diff --git a/src/.drawing.rs.swp b/src/.drawing.rs.swp new file mode 100644 index 0000000..e2f78ef Binary files /dev/null and b/src/.drawing.rs.swp differ diff --git a/src/.lib.rs.swp b/src/.lib.rs.swp new file mode 100644 index 0000000..f230d70 Binary files /dev/null and b/src/.lib.rs.swp differ diff --git a/src/drawing.rs b/src/drawing.rs new file mode 100644 index 0000000..f65ca8f --- /dev/null +++ b/src/drawing.rs @@ -0,0 +1,77 @@ +use console::Term; +use colored::Colorize; +use std::io::{self, Write}; + +pub enum Category<'a> { + Title(&'a str), + Line(&'a str), + Wrong(&'a str), + Menu, + Clear, +} + +pub fn ui(is: Category) -> char { + let term = Term::stdout; + let (_, cols) = term().size(); + + match is { + Category::Title(s) => drawing_title(&s, cols), + Category::Line(s) => drawing_line(&s), + Category::Wrong(s) => drawing_error(&s, cols), + Category::Menu => return drawing_menu(), + Category::Clear => clear_screen(), + } + + // return randomly if pattern is not menu + return 'a' +} + +fn drawing_menu() -> char { + let term = Term::stdout; + + ui(Category::Title("MOZE Analyzer")); + + println!("a. print summary by project\t\tb. print summary by time"); + println!("q. quit"); + print!("\nEnter your option: "); + io::stdout().flush().unwrap(); + let option = term().read_char().unwrap(); + + println!("{}\n", String::from(option).blue()); + + option +} + +fn drawing_title(s: &str, cols: u16) { + println!(""); + + for _ in 0..cols { + print!("-"); + } + println!(""); + for _ in 0..((cols - s.len() as u16) / 2) { + print!(" "); + } + + println!("{}", s.green()); + for _ in 0..cols { + print!("-"); + } + println!(""); +} + +fn drawing_line(s: &str) { + println!("{s}"); +} + +fn drawing_error(s: &str, cols: u16) { + for _ in 0..((cols - s.len() as u16) / 2) { + print!(" "); + } + println!("{}",s.red()); +} + +fn clear_screen() { + let term = Term::stdout; + let _ = term().clear_screen(); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a3a3522 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,144 @@ +use std::{ + env, + error, + fs::{File}, + io::BufReader, + collections::HashMap, +}; + +use itertools::Itertools; + +mod record; +mod drawing; + +use record::Record; + +pub fn run() -> Result<(), Box> { + let records = load_contents()?; + + drawing::ui(drawing::Category::Clear); + + loop { + match drawing::ui(drawing::Category::Menu) { + 'a' | 'A' => print_summary_by_project(&records), + 'b' | 'B' => print_summary_by_time(&records), + 'q' | 'Q' => { + drawing::ui(drawing::Category::Title("quit program")); + break; + }, + _ => { + drawing::ui(drawing::Category::Wrong("Unsupported Option, please try again.\n")); + continue; + }, + } + } + + Ok(()) +} + +fn print_summary_by_time(records: &Vec) { + println!("placeholder"); +} + +fn print_summary_by_project(records: &Vec) { + let mut expenses: HashMap> = HashMap::new(); + + let mut sum = HashMap::new(); + + /* group the project with separate currency */ + for r in records.iter() { + /* exclude transfer and other type */ + match r.record_type() { + "Expense" | "Fee" | "Refund" | "Income" => { + /* No project name will be ignore */ + if let Some(project) = r.project() { + if project.is_empty() { + continue; + } + + /* group projects with separate currency */ + expenses + .entry(project) + .and_modify(|expense: &mut HashMap| { + expense + .entry(r.currency()) + .and_modify(|v| *v += r.cal()) + .or_insert(r.cal()); + }) + .or_insert(HashMap::from([(r.currency(), r.cal())])); + + /* sum same project and currency amount */ + let element = sum.entry(r.currency()).or_insert(0.0 as f64); + *element += r.cal(); + } + }, + _ => continue, + } + } + + // print output + println!("print summary by project:"); + for project in expenses.keys().sorted() { + println!("\n\t{project}"); + + print!("\t\t\t|"); + + for currency in expenses[project].keys().sorted() { + let expense = expenses[project][currency]; + print!("\t{currency} {:.2}\t|", expense); + } + println!(""); + } + + println!("\n\n\tSummary"); + print!("\t\t\t|"); + for currency in sum.keys().sorted() { + print!("\t{currency} {:.2}\t|", sum[currency]); + } + + println!(""); +} + +fn load_contents() -> Result, Box> { + let args = env::args().skip(1).collect::>(); + + let csv_file = check_csv(&args)?; + + let mut reader = build_reader(csv_file)?; + + let mut records = Vec::new(); + + for r in reader.records() { + let r = r.unwrap(); + records.push(Record::new(r)); + } + Ok(records) +} + +fn build_reader(file_name: String) + -> Result>, Box> +{ + let file = File::open(file_name)?; + + let buf = BufReader::new(file); + + let ret = csv::ReaderBuilder::new() + .flexible(true) + .from_reader(buf); + + Ok(ret) +} + +fn check_csv(args: &Vec) -> Result> { + let n = args.len(); + + if n != 1 { + return Err(Box::from("Only 1 argument required\n\nUsage: moze_analyzer [csv_file]")); + } + + if args[0].contains(".csv") { + Ok(args[0].clone()) + } else { + Err(Box::from("Not csv file")) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..91b2f5b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,10 @@ +use moze_analyzer; +use std::process; + +fn main() { + if let Err(err) = moze_analyzer::run() { + eprintln!("{err}"); + process::exit(1); + } +} + diff --git a/src/record.rs b/src/record.rs new file mode 100644 index 0000000..841855e --- /dev/null +++ b/src/record.rs @@ -0,0 +1,72 @@ +#[derive(Debug)] +pub struct Record { + account: String, + currency: String, + record_type: String, + main_type: String, + subcategory: String, + price: f64, + fee: f64, + bonus: f64, + name: String, + store: Option, + date: Date, + time: Option, + project: Option, + note: Option, + tags: Option, + target: Option, +} + +#[derive(Debug)] +struct Date { + month: i32, + day: i32, + year: i32, +} + +impl Record { + pub fn new(r: csv::StringRecord) -> Self { + Record { + account: r[0].to_string(), + currency: r[1].to_string(), + record_type: r[2].to_string(), + main_type: r[3].to_string(), + subcategory: r[4].to_string(), + price: r[5].parse().unwrap(), + fee: r[6].parse().unwrap(), + bonus: r[7].parse().unwrap(), + name: r[8].to_string(), + store: r.get(9).map(|s| s.to_string()), + date: { + let d = r[10].split('/').collect::>(); + Date { + month: d[0].parse().unwrap(), + day: d[1].parse().unwrap(), + year: d[2].parse().unwrap(), + } + }, + time: r.get(11).map(|s| s.to_string()), + project: r.get(12).map(|s| s.to_string()), + note: r.get(13).map(|s| s.to_string()), + tags: r.get(14).map(|s| s.to_string()), + target: r.get(15).map(|s| s.to_string()), + } + } + + pub fn cal(&self) -> f64 { + self.price + self.fee + self.bonus + } + + pub fn record_type(&self) -> &str { + self.record_type.as_str() + } + + pub fn project(&self) -> Option { + self.project.clone() + } + + pub fn currency(&self) -> String { + self.currency.clone() + } +} -- cgit v1.2.3-70-g09d2