budget_tracker/
expense.rs1use chrono::{Local, NaiveDate};
4use log::{error, trace};
5use std::io::{self, BufRead, BufReader, Write};
6use std::{env, process::Command};
7use std::{fs, path::PathBuf};
8
9pub fn capitalize(string: String) -> String {
10 if string.is_empty() {
11 return String::new();
12 }
13
14 let mut chars = string.chars();
15 let first_char = chars.next().unwrap().to_uppercase().to_string();
16 let rest: String = chars.collect();
17
18 first_char + &rest
19}
20
21#[derive(Debug, Clone)]
23pub struct Expense {
24 pub date: String,
25 pub description: String,
26 pub expense_type: String,
27 pub amount: f64,
28}
29
30impl Expense {
31 pub fn new(date: String, description: String, expense_type: String, amount: f64) -> Self {
32 Self {
33 date,
34 description,
35 expense_type: capitalize(expense_type),
36 amount,
37 }
38 }
39
40 pub fn add_expense() -> Result<(), Box<dyn std::error::Error>> {
48 trace!("Adding expense ...");
49 let date = Self::input_date()?;
50 let description = Self::input("Enter description:")?;
51 let expense_type = capitalize(Self::input(
52 "Enter expense type (Food, Travel, Fun, Medical, Personal or Other): ",
53 )?);
54 let amount = Self::input_amount()?;
55 let expense = Self::new(date, description, expense_type, amount);
56
57 Self::append_to_csv("expenses.csv", &expense)?;
58 println!("Added your data to the db!");
59 trace!("Added expense: {:?}", expense);
60
61 Ok(())
62 }
63
64 fn input(prompt: &str) -> Result<String, Box<dyn std::error::Error>> {
66 let mut input = String::new();
67 print!("{}", prompt);
68 io::stdout().flush()?;
69 io::stdin().read_line(&mut input)?;
70 Ok(input.trim().to_string())
71 }
72
73 fn input_date() -> Result<String, Box<dyn std::error::Error>> {
75 loop {
76 let input = Self::input(
77 "Enter date (YYYY-MM-DD or YYYY/MM/DD, leave empty for today's date): ",
78 )?;
79 if input.is_empty() {
80 return Ok(Local::now().format("%Y-%m-%d").to_string());
81 } else if let Ok(date) = NaiveDate::parse_from_str(&input, "%Y-%m-%d") {
82 return Ok(date.to_string());
83 } else if let Ok(date) = NaiveDate::parse_from_str(&input, "%Y/%m/%d") {
84 return Ok(date.to_string());
85 } else {
86 println!("Invalid date format. Please enter the date in YYYY-MM-DD or YYYY/MM/DD format.");
87 }
88 }
89 }
90
91 fn input_amount() -> Result<f64, Box<dyn std::error::Error>> {
93 loop {
94 let input = Self::input("Enter amount: ")?;
95 match input.trim().parse() {
96 Ok(amount) => return Ok(amount),
97 Err(_) => println!("Invalid amount. Please enter a valid number."),
98 }
99 }
100 }
101
102 pub fn edit_expenses(file_name: &str) -> Result<(), Box<dyn std::error::Error>> {
104 trace!("Editing the expenses file ...");
105 let editor = env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
106 trace!("Choosing '{}' as the editor", editor);
107 Command::new(editor)
108 .arg(Expense::get_database_file_path(file_name)?)
109 .status()?;
110
111 Ok(())
112 }
113
114 pub fn append_to_csv(
116 file_name: &str,
117 expense: &Expense,
118 ) -> Result<(), Box<dyn std::error::Error>> {
119 trace!("Appending to db ... ");
120 let file_path = Expense::get_database_file_path(file_name)?;
121 let mut file = fs::OpenOptions::new().append(true).open(file_path)?;
122 let data = format!(
123 "{},{},{},{}\n",
124 expense.date, expense.description, expense.expense_type, expense.amount
125 );
126 file.write_all(data.as_bytes())?;
127
128 Ok(())
129 }
130
131 pub fn read_csv(file_name: &str) -> Result<Vec<Expense>, Box<dyn std::error::Error>> {
134 trace!("Reading the db ... ");
135 let file_path = Expense::get_database_file_path(file_name)?;
136 let file = fs::File::open(file_path)?;
137
138 let reader = BufReader::new(file);
139 let mut expenses = Vec::new();
140
141 for (index, line) in reader.lines().enumerate() {
142 let line = line?;
143 if index == 0 {
144 continue; }
146 let fields: Vec<&str> = line.split(',').collect();
147 if fields.len() == 4 {
148 let expense_type: String = fields[2].parse()?;
149 let expense = Expense::new(
150 fields[0].to_string(),
151 fields[1].to_string(),
152 expense_type,
153 fields[3].parse::<f64>()?,
154 );
155 expenses.push(expense);
156 }
157 }
158 Ok(expenses)
159 }
160
161 pub fn create_expenses_csv() -> Result<(), Box<dyn std::error::Error>> {
163 trace!("Creating the db ... ");
164 let budget_tracker_dir = Expense::get_database_file_path("")?;
165 if let Err(err) = fs::create_dir_all(&budget_tracker_dir) {
166 error!(
167 "Error creating directory {}: {}",
168 budget_tracker_dir.display(),
169 err
170 );
171 return Err(err.into());
172 }
173
174 let expenses_file = budget_tracker_dir.join("expenses.csv");
175 if let Err(err) = fs::File::create(&expenses_file) {
176 error!("Error creating file {}: {}", expenses_file.display(), err);
177 return Err(err.into());
178 }
179 Ok(())
180 }
181
182 fn get_database_file_path(file_name: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
183 let home_dir = dirs::home_dir().ok_or("Unable to determine user's home directory")?;
184 Ok(home_dir
185 .join(".local")
186 .join("share")
187 .join("budget-tracker")
188 .join(file_name))
189 }
190}