summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/.drawing.rs.swpbin0 -> 12288 bytes
-rw-r--r--src/.lib.rs.swpbin0 -> 20480 bytes
-rw-r--r--src/drawing.rs77
-rw-r--r--src/lib.rs144
-rw-r--r--src/main.rs10
-rw-r--r--src/record.rs72
6 files changed, 303 insertions, 0 deletions
diff --git a/src/.drawing.rs.swp b/src/.drawing.rs.swp
new file mode 100644
index 0000000..e2f78ef
--- /dev/null
+++ b/src/.drawing.rs.swp
Binary files differ
diff --git a/src/.lib.rs.swp b/src/.lib.rs.swp
new file mode 100644
index 0000000..f230d70
--- /dev/null
+++ b/src/.lib.rs.swp
Binary files 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<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"))
+ }
+}
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<String>,
+ date: Date,
+ time: Option<String>,
+ project: Option<String>,
+ note: Option<String>,
+ tags: Option<String>,
+ target: Option<String>,
+}
+
+#[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::<Vec<&str>>();
+ 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<String> {
+ self.project.clone()
+ }
+
+ pub fn currency(&self) -> String {
+ self.currency.clone()
+ }
+}