use crate::date::*;
use crate::expense::*;
use std::collections::HashSet;
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::prelude::*;
use std::path::Path;
use serde::{Serialize, Deserialize};
#[derive(Debug)]
struct DataError(String);
impl fmt::Display for DataError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for DataError {}
#[derive(Serialize, Deserialize)]
pub struct Datafile {
pub version: u64,
pub tags: HashSet<String>,
pub entries: Vec<Expense>,
}
impl Datafile {
fn new() -> Datafile {
Datafile {
version: 1,
tags: HashSet::new(),
entries: vec!(),
}
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Datafile, Box<dyn Error>> {
let file = File::open(path)?;
let reader = std::io::BufReader::new(file);
let d: Datafile = serde_json::from_reader(reader)?;
if d.version != 1 {
return Err(Box::new(DataError("unknown version in datafile!".into())));
}
Ok(d)
}
pub fn add_tag(&mut self, tag: String) {
self.tags.insert(tag);
}
pub fn insert(&mut self, expense: Expense) {
let mut insert_idx = 0;
for (idx, saved) in self.entries.iter().enumerate() {
match saved.compare_dates(&expense) {
std::cmp::Ordering::Greater => { insert_idx = idx; break; },
std::cmp::Ordering::Less => { insert_idx = idx + 1; },
std::cmp::Ordering::Equal => { insert_idx = idx + 1; },
}
}
if insert_idx > self.entries.len() {
self.entries.push(expense);
} else {
self.entries.insert(insert_idx, expense);
}
}
pub fn remove(&mut self, id: u64) -> Result<(), Box<dyn Error>> {
let mut rm_idx = 0;
for (idx, saved) in self.entries.iter().enumerate() {
if saved.compare_id(id) {
rm_idx = idx;
break;
}
}
if rm_idx > self.entries.len() {
return Err(Box::new(DataError("couldn't find item".into())));
}
self.entries.remove(rm_idx);
Ok(())
}
pub fn find(&self, id: u64) -> Option<&Expense> {
for expense in &self.entries {
if expense.compare_id(id) {
return Some(expense);
}
}
None
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn Error>> {
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(path)?;
let writer = std::io::BufWriter::new(file);
serde_json::to_writer(writer, &self)?;
Ok(())
}
pub fn expenses_between(&self, start: &SimpleDate, end: &SimpleDate) -> &[Expense] {
let mut start_idx = 0;
for (idx, expense) in self.entries.iter().enumerate() {
if let Some(end_date) = expense.get_end_date() {
if end_date > start {
start_idx = idx;
break;
}
} else {
start_idx = idx;
break;
}
}
let mut end_idx = self.entries.len();
for (idx, expense) in self.entries[start_idx..].iter().enumerate() {
if expense.get_start_date() > end {
end_idx = idx + start_idx;
break;
}
}
&self.entries[start_idx..end_idx]
}
}
pub fn initialise<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
let mut file = OpenOptions::new().write(true)
.create_new(true)
.open(path)?;
let contents = serde_json::to_string(&Datafile::new())?;
file.write_all(contents.as_bytes())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_single() {
let mut datafile = Datafile::new();
let expense = Expense::new(0, "test".into(), 100, SimpleDate::from_ymd(2020, 10, 14), None, None, vec!());
datafile.insert(expense);
assert_eq!(datafile.entries.len(), 1);
}
#[test]
fn insert_sorted() {
let mut datafile = Datafile::new();
let expense1 = Expense::new(0, "test".into(), 100, SimpleDate::from_ymd(2020, 10, 14), None, None, vec!());
let expense2 = Expense::new(1, "test".into(), 101, SimpleDate::from_ymd(2020, 10, 15), None, None, vec!());
datafile.insert(expense2);
datafile.insert(expense1);
assert_eq!(datafile.entries.len(), 2);
assert_eq!(datafile.entries[0].amount(), 100);
assert_eq!(datafile.entries[1].amount(), 101);
}
#[test]
fn remove() {
let mut datafile = Datafile::new();
let expense1 = Expense::new(0, "test".into(), 100, SimpleDate::from_ymd(2020, 10, 14), None, None, vec!());
let expense2 = Expense::new(1, "test".into(), 101, SimpleDate::from_ymd(2020, 10, 15), None, None, vec!());
datafile.insert(expense2);
datafile.insert(expense1);
assert_eq!(datafile.entries.len(), 2);
assert!(datafile.remove(1).is_ok());
assert_eq!(datafile.entries.len(), 1);
assert_eq!(datafile.entries[0].amount(), 100);
}
#[test]
fn find() {
let mut datafile = Datafile::new();
let expense1 = Expense::new(0, "test".into(), 100, SimpleDate::from_ymd(2020, 10, 14), None, None, vec!());
let expense2 = Expense::new(1, "test".into(), 101, SimpleDate::from_ymd(2020, 10, 15), None, None, vec!());
datafile.insert(expense2);
datafile.insert(expense1);
assert!(datafile.find(9999).is_none());
assert!(datafile.find(1).is_some());
assert_eq!(datafile.find(1).unwrap().amount(), 101);
}
}