1use anyhow::{bail, ensure, Result};
2use chrono::{DateTime, Datelike, Duration, Local, LocalResult, NaiveDateTime, TimeZone, Utc, Weekday, NaiveDate};
3use regex::Regex;
4use std::cmp::Ordering;
5use std::fmt::{Display, Formatter};
6use std::fs::{read_dir, File};
7use std::io;
8use std::io::BufRead;
9use std::ops::Add;
10use std::path::Path;
11use std::str::FromStr;
12
13use nom::branch::alt;
14use nom::bytes::complete::{tag, take_while1};
15use nom::character::complete::{alphanumeric0, char as nom_char, char};
16use nom::combinator::{map, map_opt, map_res};
17use nom::multi::separated_list0;
18use nom::sequence::{delimited, preceded, separated_pair};
19use nom::IResult as NomResult;
20
21fn parse_tags(text: &str) -> NomResult<&str, Vec<&str>> {
22 let sep = ' ';
23 let quote = '"';
24 let x = separated_list0(
25 nom_char(sep),
26 alt((
27 delimited(
28 nom_char(quote),
29 take_while1(|c| c != quote),
30 nom_char(quote),
31 ),
32 take_while1(|c| c != quote && c != sep),
33 )),
34 )(text);
35
36 x
37}
38
39fn parse_date(input: &str) -> NomResult<&str, DateTime<Utc>> {
40 map_opt(take_while1(|c| c != ' '), |v: &str| {
41 let parsed = NaiveDateTime::parse_from_str(v.trim(), "%Y%m%dT%H%M%SZ");
42 return if let Ok(d) = parsed {
43 Some(DateTime::<Utc>::from_utc(d, Utc))
44 } else {
45 None
46 };
47 })(input)
48}
49
50fn parse_range(input: &str) -> NomResult<&str, Range> {
67 alt((
68 map_res(
69 separated_pair(parse_date, tag(" - "), parse_date),
70 |(from, to)| Range::new(from, Some(to)),
71 ),
72 map_res(parse_date, |r| Range::new(r, None)),
73 preceded(char(':'), map_res(alphanumeric0, Range::from_period_str)),
74 ))(input)
75}
76
77fn parse_entry(input: &str) -> NomResult<&str, TimeEntry> {
78 preceded(
79 tag("inc "),
80 map(separated_pair(parse_range, tag(" # "), parse_tags), |t| {
81 TimeEntry {
82 range: t.0,
83 tags: t
84 .1
85 .into_iter()
86 .map(|s| s.to_string().clone())
87 .collect::<Vec<String>>(),
88 id: 0,
89 }
90 }),
91 )(input)
92}
93#[derive(Copy, Clone, PartialEq, Debug)]
96pub struct Range {
97 from: DateTime<Utc>,
98 to: Option<DateTime<Utc>>,
99}
100
101impl Range {
102 fn from_period_str(period: &str) -> Result<Range> {
103 match period {
104 "today" => Range::today(),
105 "yesterday" => Range::yesterday(),
106 "week" => Range::current_week(),
107 "lastweek" => Range::last_week(),
108 "month" => Range::current_month(),
109 "lastmonth" => Range::last_month(),
110 _ => bail!(""),
111 }
112 }
113
114 pub fn pretty_duration(d: &Duration) -> String {
116 format!(
117 "{:02}:{:02}:{:02}",
118 d.num_hours(),
119 d.num_minutes() % 60,
120 d.num_seconds() % 60
121 )
122 }
123
124 pub fn new(from: DateTime<Utc>, to: Option<DateTime<Utc>>) -> Result<Range> {
126 if to.is_none() {
127 return Ok(Range { from, to });
128 }
129
130 return if from + Duration::seconds(1) <= to.unwrap() {
131 Ok(Range { from, to })
132 } else {
133 bail!("Range is invalid: from cannot be greater that to");
134 };
135 }
136
137 pub fn day(day: &DateTime<Local>) -> Result<Range> {
139 let morning = match Utc.from_local_datetime(&day.naive_utc().date().and_hms(0, 0, 0)) {
140 LocalResult::Single(t) => t,
141 _ => bail!("Cannot determine morning"),
142 };
143 let evening = match Utc.from_local_datetime(&day.naive_utc().date().and_hms(23, 59, 59)) {
144 LocalResult::Single(t) => Some(t),
145 _ => bail!("Cannot determine evening"),
146 };
147
148 Range::new(morning, evening)
149 }
150
151 pub fn today() -> Result<Range> {
153 Self::day(&Local::now())
154 }
155
156 pub fn yesterday() -> Result<Range> {
158 let day = Local::today() - Duration::days(1);
159 Self::day(&day.and_hms(0,0,0))
160 }
161
162 pub fn week(day: &DateTime<Local>) -> Result<Range> {
164 let mut current = day.clone();
165 while current.weekday() != Weekday::Mon {
166 current = current - Duration::days(1);
167 }
168
169 let monday = match Utc.from_local_datetime(¤t.naive_utc().date().and_hms(0, 0, 0)) {
170 LocalResult::Single(t) => t,
171 _ => bail!("Cannot determine morning"),
172 };
173
174 let sunday = match Utc.from_local_datetime(
175 &(current + Duration::days(6)).naive_utc().date().and_hms(23, 59, 59),
176 ) {
177 LocalResult::Single(t) => Some(t),
178 _ => bail!("Cannot determine morning"),
179 };
180
181 Range::new(monday, sunday)
182 }
183
184 pub fn current_week() -> Result<Range> {
186 Self::week(&Local::today().and_hms(0,0,0))
187 }
188
189 pub fn last_week() -> Result<Range> {
191 let day = Local::today() - Duration::days(7);
192 Self::week(&day.and_hms(0,0,0))
193 }
194
195 pub fn month(day: &DateTime<Local>) -> Result<Range> {
197 let mut current = day.clone();
198 while current.day() != 1 {
199 current = current - Duration::days(1);
200 }
201
202 let first = match Utc.from_local_datetime(¤t.naive_utc().date().and_hms(0, 0, 0)) {
203 LocalResult::Single(t) => t,
204 _ => bail!("Cannot determine morning"),
205 };
206
207 current = current + Duration::days(26);
208 while current.day() != 1 {
209 current = current + Duration::days(1);
210 }
211
212 let last = match Utc.from_local_datetime(
213 &(current - Duration::days(1)).naive_utc().date()
214 .and_hms(23, 59, 59),
215 ) {
216 LocalResult::Single(t) => Some(t),
217 _ => bail!("Cannot determine morning"),
218 };
219
220 Range::new(first, last)
221 }
222
223 pub fn current_month() -> Result<Range> {
225 Self::month(&Local::today().and_hms(0,0,0))
226 }
227
228 pub fn last_month() -> Result<Range> {
230 let mut current = Local::today();
231 let this_month = current.month();
232 while current.month() != this_month - 1 {
233 current = current - Duration::days(15);
234 }
235 Self::month(¤t.and_hms(0,0,0))
236 }
237
238 pub fn is_open(&self) -> bool {
240 self.to.is_none()
241 }
242
243 pub fn intersection(&self, other: &Range) -> Option<Range> {
245 if self.to.is_none() && other.to.is_none() {
246 let from = if self.from < other.from {
247 other.from
248 } else {
249 self.from
250 };
251 return match Range::new(from, None) {
252 Ok(r) => Some(r),
253 Err(_) => None,
254 };
255 }
256
257 let from_a = self.from;
258 let to_a = self.to;
259
260 let from_b = other.from;
261 let to_b = other.to;
262
263 if let Some(to) = to_a {
264 if from_b > to {
265 return None;
266 }
267 } else {
268 let from = if self.from < other.from {
269 other.from
270 } else {
271 self.from
272 };
273 return match Range::new(from, to_b) {
274 Ok(r) => Some(r),
275 Err(_) => None,
276 };
277 }
278
279 if let Some(to) = to_b {
280 if from_a > to {
281 return None;
282 }
283 } else {
284 let from = if self.from < other.from {
285 other.from
286 } else {
287 self.from
288 };
289 return match Range::new(from, to_a) {
290 Ok(r) => Some(r),
291 Err(_) => None,
292 };
293 }
294
295 if let Some(to) = to_b {
296 if from_a > to {
297 return None;
298 }
299 }
300
301 let from = if from_a > from_b { from_a } else { from_b };
302 let to = if to_a.unwrap() > to_b.unwrap() {
303 to_b
304 } else {
305 to_a
306 };
307
308 match Range::new(from, to) {
309 Ok(r) => Some(r),
310 Err(_) => None,
311 }
312 }
313
314 pub fn duration(&self) -> Duration {
316 let to = match self.to {
317 Some(t) => t,
318 None => Utc::now(),
319 };
320
321 to - self.from
322 }
323
324 pub fn days(&self) -> Vec<DateTime<Utc>> {
326 let mut current = self.from;
327 let end = self.to.unwrap_or(Utc::now());
328 let mut days = vec![];
329 while current <= end {
330 days.push(current);
331 current = current.add(Duration::days(1));
332 }
333
334 days
335 }
336
337 pub fn split_at(&self, date: DateTime<Utc>) -> Result<(Range, Range)> {
339 let to = match self.to {
340 Some(t) => t,
341 None => Utc::now(),
342 };
343
344 let duration_from = date - self.from;
345 let duration_to = to - date;
346
347 Ok((
348 Range::new(self.from, Some(self.from + duration_from))?,
349 Range::new(to - duration_to, self.to)?,
350 ))
351 }
352
353 pub fn split(&self) -> Result<(Range, Range)> {
355 let to = match self.to {
356 Some(t) => t,
357 None => Utc::now(),
358 };
359
360 let duration = (to - self.from) / 2;
361
362 self.split_at(self.from + duration)
363 }
364}
365
366impl FromStr for Range {
367 type Err = anyhow::Error;
368
369 fn from_str(s: &str) -> Result<Self> {
370 let range = match parse_range(s) {
371 Ok((st, r)) => {
372 if !st.is_empty() {
373 bail!("Cannot parse range {}", s)
374 }
375 r
376 }
377 Err(_) => bail!("Cannot parse range {}", s),
378 };
379
380 if let None = range.to {
381 ensure!(
382 Utc::now() - range.from >= Duration::seconds(1),
383 "From must be less than \"now - 1s\""
384 )
385 }
386
387 Ok(range)
388 }
389}
390
391impl Display for Range {
392 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
393 let from = Local.from_utc_datetime(&self.from.naive_utc());
394 if self.is_open() {
395 write!(
396 f,
397 "{} - ... [{}]",
398 from,
399 Range::pretty_duration(&self.duration())
400 )
401 } else {
402 let to = Local.from_utc_datetime(&self.to.unwrap().naive_utc());
403 write!(
404 f,
405 "{} - {} [{}]",
406 from,
407 to,
408 Range::pretty_duration(&self.duration())
409 )
410 }
411 }
412}
413
414#[derive(Clone)]
417pub struct TimeEntry {
418 range: Range,
419 tags: Vec<String>,
420 id: usize,
421}
422
423impl TimeEntry {
424 pub fn range(&self) -> &Range {
426 &self.range
427 }
428
429 pub fn tags(&self) -> &[String] {
431 &self.tags
432 }
433
434 pub fn day(&self) -> NaiveDate {
436 self.range.from.naive_local().date()
437 }
438
439 pub fn id(&self) -> usize {
442 self.id
443 }
444}
445
446impl FromStr for TimeEntry {
447 type Err = anyhow::Error;
448
449 fn from_str(s: &str) -> Result<Self> {
450 match parse_entry(s) {
451 Ok((_, e)) => Ok(e),
452 Err(_) => bail!("Cannot parse \"{}\"", s),
453 }
454 }
455}
456
457impl Display for TimeEntry {
458 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
459 write!(f, "{}: {:?}", self.range(), self.tags())
460 }
461}
462
463pub struct Work {
465 entries: Vec<TimeEntry>,
466}
467
468impl Work {
469 fn load_entries_from_file(file: &Path) -> Result<Vec<TimeEntry>> {
470 let mut entries = vec![];
471 let data = File::open(file)?;
472
473 for line in io::BufReader::new(data).lines() {
474 entries.push(line?.parse()?);
475 }
476
477 Ok(entries)
478 }
479
480 pub fn load_range(data_path: &Path, range: Option<Range>) -> Result<Work> {
483 let mut entries = vec![];
484
485 let file_re = Regex::new(r"^(?P<y>\d{4})-(?P<m>\d{2}).data$").unwrap();
486
487 for file in read_dir(data_path)? {
488 let file = file?;
489 for _ in file_re.captures_iter(&file.file_name().to_string_lossy()) {
490 entries.append(&mut Work::load_entries_from_file(&file.path())?);
491 }
492 }
493
494 entries.sort_by(|a, b| {
495 if a.range.from > b.range.from {
496 Ordering::Less
497 } else if a.range.from < b.range.from {
498 Ordering::Greater
499 } else {
500 Ordering::Equal
501 }
502 });
503
504 let mut i = 1;
505 for e in &mut entries {
506 e.id = i;
507 i += 1;
508 }
509
510 return if let Some(r) = range {
511 Ok(Work {
512 entries: entries
513 .into_iter()
514 .filter(|e| e.range.intersection(&r).is_some())
515 .collect(),
516 })
517 } else {
518 Ok(Work { entries })
519 };
520 }
521
522 pub fn load_all(data_path: &Path) -> Result<Work> {
524 Work::load_range(data_path, None)
525 }
526
527 pub fn entries(&self) -> &[TimeEntry] {
529 &self.entries
530 }
531
532 pub fn duration(&self) -> Duration {
534 self.entries()
535 .iter()
536 .fold(Duration::zero(), |a, t| a + t.range.duration())
537 }
538}
539
540impl Display for Work {
541 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
542 write!(f, "{} entries loaded", self.entries.len())
543 }
544}
545
546#[cfg(test)]
547mod range_tests {
548 use crate::data::Range;
549 use chrono::{DateTime, Duration, NaiveDateTime, Utc};
550
551 fn parse_date_time(date: &str) -> DateTime<Utc> {
552 let d = NaiveDateTime::parse_from_str(date, "%Y%m%dT%H%M%SZ").unwrap();
553 DateTime::<Utc>::from_utc(d, Utc)
554 }
555
556 #[test]
557 fn test_range_parse() {
558 assert_eq!(
560 "20220101T120000Z - 20220101T130000Z"
561 .parse::<Range>()
562 .unwrap(),
563 Range {
564 from: parse_date_time("20220101T120000Z"),
565 to: Some(parse_date_time("20220101T130000Z"))
566 }
567 );
568
569 assert!("20220101T130000Z - 20220101T120000Z"
571 .parse::<Range>()
572 .is_err());
573
574 assert_eq!(
576 "20220101T120000Z".parse::<Range>().unwrap(),
577 Range {
578 from: parse_date_time("20220101T120000Z"),
579 to: None
580 }
581 );
582
583 let now = Utc::now();
585 let tomorrow = now + Duration::days(1);
586 let range = tomorrow.format("%Y%m%dT%H%M%SZ").to_string();
587 assert!(range.parse::<Range>().is_err(),);
588
589 assert!("1日1月2022年".parse::<Range>().is_err())
591 }
592
593 #[test]
594 fn test_range_split() {
595 let input1: Range = "20220101T120000Z - 20220101T130000Z".parse().unwrap();
596 let output1: Range = "20220101T120000Z - 20220101T123000Z".parse().unwrap();
597 let output2: Range = "20220101T123000Z - 20220101T130000Z".parse().unwrap();
598
599 assert_eq!(input1.split().unwrap(), (output1, output2));
600
601 let input1: Range = "20220101T120000Z - 20220101T120001Z".parse().unwrap();
603
604 assert!(input1.split().is_err());
605 }
606
607 #[test]
608 fn test_range_split_at() {
609 let input1: Range = "20220101T120000Z - 20220101T130000Z".parse().unwrap();
610 let output1: Range = "20220101T120000Z - 20220101T123000Z".parse().unwrap();
611 let output2: Range = "20220101T123000Z - 20220101T130000Z".parse().unwrap();
612
613 assert_eq!(
615 input1
616 .split_at(parse_date_time("20220101T123000Z"))
617 .unwrap(),
618 (output1, output2)
619 );
620
621 assert!(input1
623 .split_at(parse_date_time("20220101T120000Z"))
624 .is_err());
625 assert!(input1
626 .split_at(parse_date_time("20220101T130000Z"))
627 .is_err());
628
629 assert!(input1
631 .split_at(parse_date_time("20220101T110000Z"))
632 .is_err());
633 assert!(input1
634 .split_at(parse_date_time("20220101T150000Z"))
635 .is_err());
636 }
637
638 #[test]
639 fn test_range_intersect() {
640 let input1: Range = "20220101T120000Z - 20220101T124500Z".parse().unwrap();
642 let input2: Range = "20220101T123000Z - 20220101T130000Z".parse().unwrap();
643 let output2: Range = "20220101T123000Z - 20220101T124500Z".parse().unwrap();
644
645 assert_eq!(input1.intersection(&input2), Some(output2));
646
647 let input1: Range = "20220101T120000Z - 20220101T123000Z".parse().unwrap();
649 let input2: Range = "20220101T123000Z - 20220101T130000Z".parse().unwrap();
650
651 assert!(input1.intersection(&input2).is_none());
652
653 let input1: Range = "20220101T120000Z - 20220101T121500Z".parse().unwrap();
655 let input2: Range = "20220101T124500Z - 20220101T130000Z".parse().unwrap();
656
657 assert!(input1.intersection(&input2).is_none());
658
659 let input1: Range = "20220711T120000Z".parse().unwrap();
661 let input2: Range = "20220711T000000Z - 20220712T000000Z".parse().unwrap();
662 let output: Range = "20220711T120000Z - 20220712T000000Z".parse().unwrap();
663
664 assert_eq!(input1.intersection(&input2).unwrap(), output);
665 assert_eq!(input2.intersection(&input1).unwrap(), output);
666
667 let input1: Range = "20220718T095832Z".parse().unwrap();
669 let input2: Range = "20220711T000000Z - 20220717T235959Z".parse().unwrap();
670
671 assert!(input1.intersection(&input2).is_none());
672 assert!(input2.intersection(&input1).is_none());
673
674 let input1: Range = "20220718T104206Z".parse().unwrap();
676 let input2: Range = "20220717T000000Z".parse().unwrap();
677
678 assert_eq!(input1.intersection(&input2).unwrap(), input1);
679 assert_eq!(input2.intersection(&input1).unwrap(), input1);
680 }
681
682 #[test]
683 fn test_range_duration() {
684 let input1: Range = "20220101T120000Z - 20220101T124500Z".parse().unwrap();
685
686 assert_eq!(input1.duration(), Duration::minutes(45));
687 }
688}
689
690#[cfg(test)]
691mod timeentry_tests {
692 use crate::data::TimeEntry;
693 use chrono::{DateTime, Duration, NaiveDateTime, Utc};
694
695 fn parse_date_time(date: &str) -> DateTime<Utc> {
696 let d = NaiveDateTime::parse_from_str(date, "%Y%m%dT%H%M%SZ").unwrap();
697 DateTime::<Utc>::from_utc(d, Utc)
698 }
699
700 #[test]
701 fn test_timeentry_parse() {
702 let input1: TimeEntry = "inc 20220101T120000Z - 20220101T124500Z # tag1 \"tag 2\" tag3"
703 .parse()
704 .unwrap();
705
706 assert_eq!(input1.range().duration(), Duration::minutes(45));
707 assert_eq!(input1.tags(), vec!["tag1", "tag 2", "tag3"]);
708 assert!(!input1.range().is_open());
709
710 let input1: TimeEntry = "inc 20220101T120000Z # tag1 \"tag 2 \" \" t a g 3 \""
711 .parse()
712 .unwrap();
713
714 assert_eq!(input1.range().from, parse_date_time("20220101T120000Z"));
715 assert_eq!(input1.tags(), vec!["tag1", "tag 2 ", " t a g 3 "]);
716 assert!(input1.range().is_open());
717 }
718}