diff options
-rw-r--r-- | Cargo.lock | 308 | ||||
-rw-r--r-- | Cargo.toml | 10 | ||||
-rw-r--r-- | src/.drawing.rs.swp | bin | 0 -> 12288 bytes | |||
-rw-r--r-- | src/.lib.rs.swp | bin | 0 -> 20480 bytes | |||
-rw-r--r-- | src/drawing.rs | 77 | ||||
-rw-r--r-- | src/lib.rs | 144 | ||||
-rw-r--r-- | src/main.rs | 10 | ||||
-rw-r--r-- | src/record.rs | 72 |
8 files changed, 621 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1e2712c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,308 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "moze_analyzer" +version = "0.1.0" +dependencies = [ + "colored", + "console", + "csv", + "itertools", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2091015 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "moze_analyzer" +version = "0.1.0" +edition = "2021" + +[dependencies] +colored = "2.1.0" +console = "0.15.8" +csv = "1.3.0" +itertools = "0.13.0" diff --git a/src/.drawing.rs.swp b/src/.drawing.rs.swp Binary files differnew file mode 100644 index 0000000..e2f78ef --- /dev/null +++ b/src/.drawing.rs.swp diff --git a/src/.lib.rs.swp b/src/.lib.rs.swp Binary files differnew file mode 100644 index 0000000..f230d70 --- /dev/null +++ b/src/.lib.rs.swp 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() + } +} |