1use std::{
4 fs::File,
5 io::{Read, Write},
6 path::{Path, PathBuf},
7};
8
9use chrono::{DateTime, Duration, Utc};
10use directories::ProjectDirs;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use crate::Event;
15
16#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
19pub struct Sheet {
20 pub events: Vec<Event>,
21}
22
23impl Sheet {
24 pub fn load_default() -> Result<Sheet, SheetError> {
29 Self::load(Self::default_loc()?)
30 }
31
32 pub fn load<P>(path: P) -> Result<Sheet, SheetError>
34 where
35 P: AsRef<Path>,
36 {
37 let mut sheet_json = String::new();
38
39 {
40 let mut sheet_file = File::open(&path).map_err(SheetError::OpenSheet)?;
41
42 sheet_file
43 .read_to_string(&mut sheet_json)
44 .map_err(SheetError::ReadSheet)?;
45 }
46
47 if sheet_json.is_empty() {
48 Ok(Sheet::default())
49 } else {
50 serde_json::from_str(&sheet_json).map_err(SheetError::ParseSheet)
51 }
52 }
53
54 pub fn default_dir() -> Result<PathBuf, SheetError> {
65 ProjectDirs::from("dev", "neros", "PunchClock")
66 .ok_or(SheetError::FindSheet)
67 .map(|dirs| dirs.data_dir().to_owned())
68 }
69
70 pub fn default_loc() -> Result<PathBuf, SheetError> {
77 Self::default_dir().map(|mut dir| {
78 dir.push("sheet.json");
79 dir
80 })
81 }
82
83 pub fn write_default(&self) -> Result<(), SheetError> {
88 self.write(Self::default_loc()?)
89 }
90
91 pub fn write<P>(&self, path: P) -> Result<(), SheetError>
93 where
94 P: AsRef<Path>,
95 {
96 let new_sheet_json = serde_json::to_string(self).unwrap();
97
98 match File::create(&path) {
99 Ok(mut sheet_file) => {
100 write!(&mut sheet_file, "{}", new_sheet_json).map_err(SheetError::WriteSheet)
101 }
102 Err(e) => Err(SheetError::WriteSheet(e)),
103 }
104 }
105
106 pub fn punch_in(&mut self) -> Result<DateTime<Utc>, SheetError> {
108 self.punch_in_at(Utc::now())
109 }
110
111 pub fn punch_in_at(&mut self, time: DateTime<Utc>) -> Result<DateTime<Utc>, SheetError> {
113 match self.events.last() {
114 Some(Event { stop: Some(_), .. }) | None => {
115 let event = Event::new(time);
116 self.events.push(event);
117 Ok(time)
118 }
119 Some(Event {
120 start: start_time, ..
121 }) => Err(SheetError::PunchedIn(*start_time)),
122 }
123 }
124
125 pub fn punch_out(&mut self) -> Result<DateTime<Utc>, SheetError> {
127 self.punch_out_at(Utc::now())
128 }
129
130 pub fn punch_out_at(&mut self, time: DateTime<Utc>) -> Result<DateTime<Utc>, SheetError> {
132 match self.events.last_mut() {
133 Some(ref mut event @ Event { stop: None, .. }) => {
134 event.stop = Some(time);
135 Ok(time)
136 }
137 Some(Event {
138 stop: Some(stop_time),
139 ..
140 }) => Err(SheetError::PunchedOut(*stop_time)),
141 None => Err(SheetError::NoPunches),
142 }
143 }
144
145 pub fn status(&self) -> SheetStatus {
148 match self.events.last() {
149 Some(Event {
150 stop: Some(stop), ..
151 }) => SheetStatus::PunchedOut(*stop),
152 Some(Event { start, .. }) => SheetStatus::PunchedIn(*start),
153 None => SheetStatus::Empty,
154 }
155 }
156
157 pub fn count_range(&self, begin: DateTime<Utc>, end: DateTime<Utc>) -> Duration {
160 self.events
161 .iter()
162 .map(|e| (e.start, e.stop.unwrap_or_else(Utc::now)))
163 .filter(|(start, stop)| {
164 let entirely_before = start < &begin && stop < &begin;
165 let entirely_after = start > &end && stop > &end;
166
167 !(entirely_before || entirely_after)
168 })
169 .map(|(start, stop)| {
170 let real_begin = std::cmp::max(begin, start);
171 let real_end = std::cmp::min(end, stop);
172
173 real_end - real_begin
174 })
175 .fold(Duration::zero(), |acc, next| acc + next)
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
181pub enum SheetStatus {
182 PunchedIn(DateTime<Utc>),
184 PunchedOut(DateTime<Utc>),
186 Empty,
188}
189
190#[derive(Error, Debug)]
194pub enum SheetError {
195 #[error("already punched in at {0}")]
196 PunchedIn(DateTime<Utc>),
197 #[error("not punched in, last punched out at {0}")]
198 PunchedOut(DateTime<Utc>),
199 #[error("not punched in, no punch-ins recorded")]
200 NoPunches,
201 #[error("unable to find sheet file")]
202 FindSheet,
203 #[error("unable to open sheet file")]
204 OpenSheet(#[source] std::io::Error),
205 #[error("unable to read sheet file")]
206 ReadSheet(#[source] std::io::Error),
207 #[error("unable to parse sheet")]
208 ParseSheet(#[source] serde_json::Error),
209 #[error("unable to write sheet to file")]
210 WriteSheet(#[source] std::io::Error),
211}