use clap::Parser;
use std::io;
use std::io::Write;
use std::process::exit;
use std::collections::HashMap;
use std::fmt::Debug;
macro_rules! flush {
() => {
match io::stdout().flush() {
Ok(_) => (),
Err(_) => {
eprintln!("Unable to write terminal output.");
exit(1);
},
}
};
}
macro_rules! input {
($input:ident) => {
let mut $input = String::new();
match io::stdin().read_line(&mut $input) {
Ok(_) => (),
Err(_) => {
eprintln!("Unable to read terminal input.");
exit(1);
},
}
};
}
#[derive(Parser)]
struct Args {
domestic_total: Option<f64>,
foreign_total: Option<f64>,
}
impl Args {
pub fn new() -> Self {
Self::parse()
}
fn get_total(desired_total: Option<f64>, prompt: &str) -> f64 {
match desired_total {
Some(total) => total,
None => {
print!("{}", prompt);
flush!();
input_float()
},
}
}
fn domestic_total(&self) -> f64 {
Self::get_total(self.domestic_total, "Please enter domestic total: ")
}
fn foreign_total(&self) -> f64 {
Self::get_total(self.foreign_total, "Please enter foreign total: ")
}
}
fn input_float() -> f64 {
let parsed_input = loop {
input!(input);
match input
.trim()
.parse::<f64>() {
Ok(parsed_input) => break parsed_input,
Err(_) => {
print!("Please try again. Enter a valid number: ");
flush!();
continue;
},
}
};
parsed_input
}
#[derive(Debug)]
struct CategoryList {
conversion_factor: f64,
remaining_foreign_total: f64,
categories: HashMap<String, f64>,
}
impl CategoryList {
pub fn new(args: Args) -> Self {
let domestic_total = args.domestic_total();
let foreign_total = args.foreign_total();
Self {
conversion_factor: domestic_total / &foreign_total,
remaining_foreign_total: foreign_total,
categories: HashMap::new(),
}
}
fn assign_to_category(&mut self, category_name: String, value_to_assign: f64) -> () {
match &self.categories.get(&category_name) {
&Some(&value) => &self.categories.insert(category_name, &value + value_to_assign),
None => &self.categories.insert(category_name, value_to_assign),
};
}
pub fn input_loop(&mut self) -> () {
type CounterType = u64;
let mut category_counter: CounterType = 0;
loop {
print!("Enter subtotal in foreign currency (category subtotal) (remaining money to categorize: {:.2}): ", &self.remaining_foreign_total);
flush!();
input!(input);
match input
.trim()
.parse::<String>() {
Ok(parsed_input) => {
let mut split_input = parsed_input
.split_whitespace()
.collect::<Vec<_>>();
let subtotal = match split_input.last() {
Some(subtotal) => {
match subtotal.parse::<f64>() {
Ok(subtotal) => subtotal,
Err(_) => {
eprintln!("Please enter the subtotal as the last element on the line, preceeded by a space. Try again.");
continue;
}
}
},
None => {
self.assign_to_category("Other".to_string(), self.remaining_foreign_total);
break;
}
};
if self.remaining_foreign_total - subtotal < 0f64 {
eprintln!("The entered subtotal exceeds the remaining total. Try again or just press enter to assign the entire remaining total to a category named Other.");
continue;
}
split_input.remove(split_input.len() - 1);
let mut category_name = split_input.join(" ");
if category_name == "" {
category_counter = match category_counter.checked_add(1) {
Some(new_counter_value) => new_counter_value,
None => {
println!("A maximum of {} unnamed categories are supported. Please name category explicitly. Try again.", CounterType::MAX);
continue;
}
};
category_name = format!("Unnamed category {}", category_counter);
}
self.assign_to_category(category_name, subtotal);
self.remaining_foreign_total -= subtotal;
if self.remaining_foreign_total == 0f64 {
break;
}
},
Err(_) => {
eprintln!("Unable to parse terminal input. Try again.");
continue;
},
}
}
}
fn ordered_category_list(&self) -> Vec<&String> {
let mut named_categories = Vec::new();
let mut unnamed_categories = Vec::new();
let mut other_category = Vec::new();
for category in self.categories.keys().collect::<Vec<_>>() {
if *category == "Other" {
other_category.push(category);
continue;
} else if category.starts_with("Unnamed category") {
unnamed_categories.push(category);
continue;
} else {
named_categories.push(category);
continue;
}
}
named_categories.sort();
unnamed_categories.sort();
let mut ordered_categories = named_categories;
ordered_categories.append(&mut unnamed_categories);
ordered_categories.append(&mut other_category);
ordered_categories
}
pub fn print_split_table(&self) -> () {
println!("{:<20}{:>20}{:>20}", "Category", "Foreign subtotal", "Domestic subtotal");
for category in self.ordered_category_list() {
let foreign_amount = match self.categories.get(category) {
Some(foreign_amount) => foreign_amount,
None => {
eprint!("Tried to read a category that does not exist from memory. Exiting.");
exit(1);
}
};
println!("{:<20}{:>20.2}{:>20.2}", category, foreign_amount, foreign_amount * self.conversion_factor);
}
}
}
pub fn run() -> () {
let args = Args::new();
let mut category_list = CategoryList::new(args);
category_list.input_loop();
category_list.print_split_table();
}