use std::{
fs::File,
io::{Read, Write},
path::{Path, PathBuf},
};
use chrono::{DateTime, Duration, Utc};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::Event;
#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Sheet {
pub events: Vec<Event>,
}
impl Sheet {
pub fn load_default() -> Result<Sheet, SheetError> {
Self::load(Self::default_loc()?)
}
pub fn load<P>(path: P) -> Result<Sheet, SheetError>
where
P: AsRef<Path>,
{
let mut sheet_json = String::new();
{
let mut sheet_file = File::open(&path).map_err(SheetError::OpenSheet)?;
sheet_file
.read_to_string(&mut sheet_json)
.map_err(SheetError::ReadSheet)?;
}
if sheet_json.is_empty() {
Ok(Sheet::default())
} else {
serde_json::from_str(&sheet_json).map_err(SheetError::ParseSheet)
}
}
pub fn default_dir() -> Result<PathBuf, SheetError> {
ProjectDirs::from("dev", "neros", "PunchClock")
.ok_or(SheetError::FindSheet)
.map(|dirs| dirs.data_dir().to_owned())
}
pub fn default_loc() -> Result<PathBuf, SheetError> {
Self::default_dir().map(|mut dir| {
dir.push("sheet.json");
dir
})
}
pub fn write_default(&self) -> Result<(), SheetError> {
self.write(Self::default_loc()?)
}
pub fn write<P>(&self, path: P) -> Result<(), SheetError>
where
P: AsRef<Path>,
{
let new_sheet_json = serde_json::to_string(self).unwrap();
match File::create(&path) {
Ok(mut sheet_file) => {
write!(&mut sheet_file, "{}", new_sheet_json).map_err(SheetError::WriteSheet)
}
Err(e) => Err(SheetError::WriteSheet(e)),
}
}
pub fn punch_in(&mut self) -> Result<DateTime<Utc>, SheetError> {
self.punch_in_at(Utc::now())
}
pub fn punch_in_at(&mut self, time: DateTime<Utc>) -> Result<DateTime<Utc>, SheetError> {
match self.events.last() {
Some(Event { stop: Some(_), .. }) | None => {
let event = Event::new(time);
self.events.push(event);
Ok(time)
}
Some(Event {
start: start_time, ..
}) => Err(SheetError::PunchedIn(*start_time)),
}
}
pub fn punch_out(&mut self) -> Result<DateTime<Utc>, SheetError> {
self.punch_out_at(Utc::now())
}
pub fn punch_out_at(&mut self, time: DateTime<Utc>) -> Result<DateTime<Utc>, SheetError> {
match self.events.last_mut() {
Some(ref mut event @ Event { stop: None, .. }) => {
event.stop = Some(time);
Ok(time)
}
Some(Event {
stop: Some(stop_time),
..
}) => Err(SheetError::PunchedOut(*stop_time)),
None => Err(SheetError::NoPunches),
}
}
pub fn status(&self) -> SheetStatus {
match self.events.last() {
Some(Event {
stop: Some(stop), ..
}) => SheetStatus::PunchedOut(*stop),
Some(Event { start, .. }) => SheetStatus::PunchedIn(*start),
None => SheetStatus::Empty,
}
}
pub fn count_range(&self, begin: DateTime<Utc>, end: DateTime<Utc>) -> Duration {
self.events
.iter()
.map(|e| (e.start, e.stop.unwrap_or_else(Utc::now)))
.filter(|(start, stop)| {
let entirely_before = start < &begin && stop < &begin;
let entirely_after = start > &end && stop > &end;
!(entirely_before || entirely_after)
})
.map(|(start, stop)| {
let real_begin = std::cmp::max(begin, start);
let real_end = std::cmp::min(end, stop);
real_end - real_begin
})
.fold(Duration::zero(), |acc, next| acc + next)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SheetStatus {
PunchedIn(DateTime<Utc>),
PunchedOut(DateTime<Utc>),
Empty,
}
#[derive(Error, Debug)]
pub enum SheetError {
#[error("already punched in at {0}")]
PunchedIn(DateTime<Utc>),
#[error("not punched in, last punched out at {0}")]
PunchedOut(DateTime<Utc>),
#[error("not punched in, no punch-ins recorded")]
NoPunches,
#[error("unable to find sheet file")]
FindSheet,
#[error("unable to open sheet file")]
OpenSheet(#[source] std::io::Error),
#[error("unable to read sheet file")]
ReadSheet(#[source] std::io::Error),
#[error("unable to parse sheet")]
ParseSheet(#[source] serde_json::Error),
#[error("unable to write sheet to file")]
WriteSheet(#[source] std::io::Error),
}