summaryrefslogtreecommitdiff
path: root/src/lib.rs
diff options
context:
space:
mode:
authorgarhve <git@garhve.com>2024-08-01 02:02:43 +0800
committergarhve <git@garhve.com>2024-08-01 02:02:43 +0800
commitc926cd6eceaa2522b557c3dc1f9d7ebe8002702d (patch)
tree7f4adab86870dcb2fecf797d0b10bf0aa3a01759 /src/lib.rs
add function to print project summary
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs144
1 files changed, 144 insertions, 0 deletions
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<dyn error::Error>> {
+ 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<Record>) {
+ println!("placeholder");
+}
+
+fn print_summary_by_project(records: &Vec<Record>) {
+ let mut expenses: HashMap<String, HashMap<String, f64>> = 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<String, f64>| {
+ 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<Vec<Record>, Box<dyn error::Error>> {
+ let args = env::args().skip(1).collect::<Vec<String>>();
+
+ 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<csv::Reader<BufReader<File>>, Box<dyn error::Error>>
+{
+ 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<String>) -> Result<String, Box<dyn error::Error>> {
+ 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"))
+ }
+}