tempus_cli/
utils.rs

1use crate::times::DateRange;
2
3use chrono::{Date, DateTime, Datelike, FixedOffset, Local, TimeZone};
4
5use std::env;
6
7use std::io::{ErrorKind, Read};
8
9use std::fs::{self, File, Metadata, OpenOptions};
10use std::path::Path;
11use std::time::SystemTime;
12
13use regex::Regex;
14
15/// Get the created time or panic
16pub fn get_metadata_created(metadata: Metadata) -> DateTime<FixedOffset> {
17    match metadata.created() {
18        Ok(created_at) => system_time_to_datetime(&created_at),
19        Err(e) => panic!("err getting session metadata: {:?}", e),
20    }
21}
22
23/// Convert a SystemTime to chrono::DateTime
24pub fn system_time_to_datetime(time: &SystemTime) -> DateTime<FixedOffset> {
25    match time.duration_since(SystemTime::UNIX_EPOCH) {
26        Ok(duration) => local_to_fixed_offset(Local.timestamp(duration.as_secs() as i64, 0)),
27        Err(e) => panic!("error getting SystemTime seconds: {}", e),
28    }
29}
30
31pub fn format_datetime(time: &DateTime<FixedOffset>) -> String {
32    time.to_rfc3339()
33}
34
35pub fn datetime_from_str(time: &str) -> DateTime<FixedOffset> {
36    DateTime::parse_from_rfc3339(time)
37        .unwrap_or_else(|e| panic!("failed to parse datetime {}: {}", time, e))
38}
39
40/// Return the value of $HOME or panic if it doesn't exist
41pub fn get_home_dir() -> String {
42    env::var("HOME").unwrap_or_else(|e| panic!("error getting $HOME env variable: {}", e))
43}
44
45/// Create a directory & all parent directories if they don't exist
46/// & return the name. Panic if an error occurs while creating the dir
47pub fn create_dir(dir: &str) {
48    fs::create_dir_all(&dir).unwrap_or_else(|e| {
49        // if it already exists, no problem
50        if e.kind() != ErrorKind::AlreadyExists {
51            panic!("could not create {} directory: {}", dir, e);
52        }
53    });
54}
55
56/// Open a file for appending or create it if it doesn't exist
57/// Panic on error, return the file handle
58pub fn create_or_open_file(path: &str) -> File {
59    OpenOptions::new()
60        .create(true)
61        .append(true)
62        .open(&path)
63        .expect(&format!("Error opening {}", &path))
64}
65
66/// Returns the length in hours between the start & end time
67pub fn get_length_hours(start: &DateTime<FixedOffset>, end: &DateTime<FixedOffset>) -> f64 {
68    ((end.timestamp() - start.timestamp()) as f64) / 3600.0
69}
70
71pub fn get_file_contents(path: &Path) -> String {
72    let mut file = match File::open(&path) {
73        Ok(file) => file,
74        Err(e) => {
75            eprintln!("error opening {}: {}", path.display(), e);
76            std::process::exit(1);
77        }
78    };
79
80    let mut contents = String::new();
81    if let Err(e) = file.read_to_string(&mut contents) {
82        eprintln!("error reading {}: {}", path.display(), e);
83        std::process::exit(1);
84    }
85
86    contents
87}
88
89// TODO may be able to change this to a Tz: TimeZone generic param
90// instead of fixedoffset
91pub fn datetime_to_readable_str(date: &DateTime<FixedOffset>) -> String {
92    date.format("%Y-%m-%d %H:%M:%S").to_string()
93}
94
95pub fn get_start_date() -> DateTime<FixedOffset> {
96    local_to_fixed_offset(Local.ymd(1970, 1, 1).and_hms(0, 0, 0))
97}
98
99/// Returns a DateTime<FixedOffset> given a date string
100/// If `end` == true, the date returned has a time of 23:59:59
101/// to support the range being inclusive
102/// so that 12-01..12-10 would include all sessions started on 12-10
103pub fn get_date_from_arg(date_arg: &str, end: bool) -> DateTime<FixedOffset> {
104    let get_time = |dt: Date<Local>| {
105        if !end {
106            dt.and_hms(0, 0, 0)
107        } else {
108            dt.and_hms(23, 59, 59)
109        }
110    };
111
112    if date_arg == "today" {
113        return local_to_fixed_offset(get_time(Local::today()));
114    }
115
116    let re = Regex::new(r"^(\d{4}-)?(\d{1,2})-(\d{1,2})$").unwrap();
117
118    let caps = re
119        .captures(date_arg)
120        .expect(&format!("{} is not a valid date", date_arg));
121
122    let year = match caps.get(1) {
123        // 0..4 is safe because if it exists, it _must_
124        // look like `YYYY-` -- just remove the dash
125        Some(_) => caps[1][0..4].parse().unwrap(),
126        // if no year is provided, use this year
127        None => Local::today().year(),
128    };
129
130    let month: u32 = caps[2].parse().unwrap();
131    let day: u32 = caps[3].parse().unwrap();
132
133    local_to_fixed_offset(get_time(Local.ymd(year, month, day)))
134}
135
136pub fn local_to_fixed_offset(date: DateTime<Local>) -> DateTime<FixedOffset> {
137    // why not just DateTime::from(date), you ask?
138    // converting a date to a fixed offset using the from trait
139    // also converts the timezone to utc, but we want to conserve
140    // the timezone. There's probably an easier way to do this,
141    // but this seems like the quickest to me after spending
142    // a few hours reading the docs
143    DateTime::parse_from_rfc3339(&date.to_rfc3339()).unwrap()
144}
145
146/// parses string in <date>(..(<date>)?)? format
147/// where date -> 'today' | yyyy-mm-dd | mm-dd
148/// <date> returns the range (<earliest_tempus_date>, <date>), inclusive
149/// <date>.. returns the range (<date>, <today>), inclusive
150/// <date1>..<date2> returns the range (<date1>, <date2>), inclusive
151/// 'today' can be used in place of a date instead of typing today's date
152/// a date without the year will search for this year
153pub fn parse_date_range(date_range: &str) -> Result<DateRange, &str> {
154    let dates = date_range.split("..").collect::<Vec<&str>>();
155
156    let start_date = get_start_date();
157
158    if dates.len() == 1 {
159        // no dots (-d <date>), so this is the end date
160        Ok(DateRange(start_date, get_date_from_arg(dates[0], true)))
161    } else if dates.len() == 2 {
162        match (dates[0], dates[1]) {
163            ("", "") => Err("Invalid date-range provided"),
164            ("", _) => Ok(DateRange(start_date, get_date_from_arg(dates[1], true))),
165            (_, "") => Ok(DateRange(
166                get_date_from_arg(dates[0], false),
167                get_date_from_arg("today", true),
168            )),
169            (_, _) => Ok(DateRange(
170                get_date_from_arg(dates[0], false),
171                get_date_from_arg(dates[1], true),
172            )),
173        }
174    } else {
175        Err("Invalid date-range provided")
176    }
177}
178
179#[cfg(test)]
180mod test {
181    use super::*;
182
183    #[test]
184    fn test_date_range() {
185        let DateRange(start, end) = parse_date_range("2021-12-01..2021-12-13").unwrap();
186
187        assert_eq!(
188            Local.ymd(2021, 12, 1).and_hms(0, 0, 0).timestamp(),
189            start.timestamp()
190        );
191
192        assert_eq!(
193            Local.ymd(2021, 12, 13).and_hms(23, 59, 59).timestamp(),
194            end.timestamp()
195        );
196    }
197
198    #[test]
199    fn test_date_range_end_only() {
200        let DateRange(start, end) = parse_date_range("2021-12-01").unwrap();
201
202        assert_eq!(
203            Local.ymd(1970, 1, 1).and_hms(0, 0, 0).timestamp(),
204            start.timestamp()
205        );
206
207        assert_eq!(
208            Local.ymd(2021, 12, 1).and_hms(23, 59, 59).timestamp(),
209            end.timestamp()
210        );
211    }
212
213    #[test]
214    fn test_date_range_start_only() {
215        let DateRange(start, end) = parse_date_range("2021-12-01..").unwrap();
216
217        assert_eq!(
218            Local.ymd(2021, 12, 1).and_hms(0, 0, 0).timestamp(),
219            start.timestamp()
220        );
221
222        assert_eq!(
223            Local::today().and_hms(23, 59, 59).timestamp(),
224            end.timestamp()
225        );
226    }
227
228    #[test]
229    fn test_date_range_end_only_with_dots() {
230        // ..12-01 should be identical to 12-01
231        let DateRange(start, end) = parse_date_range("..12-01").unwrap();
232
233        assert_eq!(
234            Local.ymd(1970, 1, 1).and_hms(0, 0, 0).timestamp(),
235            start.timestamp()
236        );
237
238        assert_eq!(
239            Local.ymd(2021, 12, 1).and_hms(23, 59, 59).timestamp(),
240            end.timestamp()
241        );
242    }
243
244    #[test]
245    fn test_date_range_today() {
246        let DateRange(start, end) = parse_date_range("today").unwrap();
247
248        assert_eq!(
249            Local.ymd(1970, 1, 1).and_hms(0, 0, 0).timestamp(),
250            start.timestamp()
251        );
252
253        assert_eq!(
254            Local::today().and_hms(23, 59, 59).timestamp(),
255            end.timestamp()
256        );
257    }
258
259    #[test]
260    fn test_date_range_today_2() {
261        let DateRange(start, end) = parse_date_range("today..2021-12-31").unwrap();
262
263        assert_eq!(
264            Local::today().and_hms(0, 0, 0).timestamp(),
265            start.timestamp()
266        );
267
268        assert_eq!(
269            Local.ymd(2021, 12, 31).and_hms(23, 59, 59).timestamp(),
270            end.timestamp()
271        );
272    }
273
274    #[test]
275    fn test_date_range_today_3() {
276        let DateRange(start, end) = parse_date_range("2021-12-1..today").unwrap();
277
278        assert_eq!(
279            Local.ymd(2021, 12, 1).and_hms(0, 0, 0).timestamp(),
280            start.timestamp()
281        );
282
283        assert_eq!(
284            Local::today().and_hms(23, 59, 59).timestamp(),
285            end.timestamp()
286        );
287    }
288
289    #[test]
290    fn test_get_date_from_arg() {
291        let d = get_date_from_arg("2021-12-01", false);
292
293        assert_eq!(
294            Local.ymd(2021, 12, 1).and_hms(0, 0, 0).timestamp(),
295            d.timestamp()
296        );
297    }
298
299    #[test]
300    fn test_get_date_from_arg_end() {
301        let d = get_date_from_arg("2021-12-01", true);
302
303        assert_eq!(
304            Local.ymd(2021, 12, 1).and_hms(23, 59, 59).timestamp(),
305            d.timestamp()
306        );
307    }
308
309    #[test]
310    fn test_get_date_from_arg_one_digit_day() {
311        let d = get_date_from_arg("2021-12-1", false);
312
313        assert_eq!(
314            Local.ymd(2021, 12, 1).and_hms(0, 0, 0).timestamp(),
315            d.timestamp()
316        );
317    }
318
319    #[test]
320    fn test_get_date_from_arg_one_digit_month() {
321        let d = get_date_from_arg("2021-1-10", false);
322
323        assert_eq!(
324            Local.ymd(2021, 1, 10).and_hms(0, 0, 0).timestamp(),
325            d.timestamp()
326        );
327    }
328
329    #[test]
330    fn test_get_date_from_arg_one_digit_month_and_day() {
331        let d = get_date_from_arg("2021-1-1", false);
332
333        assert_eq!(
334            Local.ymd(2021, 1, 1).and_hms(0, 0, 0).timestamp(),
335            d.timestamp()
336        );
337    }
338
339    #[test]
340    fn test_get_date_from_arg_no_year() {
341        let d = get_date_from_arg("12-01", false);
342
343        assert_eq!(
344            Local
345                .ymd(Local::today().year(), 12, 1)
346                .and_hms(0, 0, 0)
347                .timestamp(),
348            d.timestamp()
349        );
350    }
351
352    #[test]
353    fn test_get_date_from_arg_no_year_one_digit_day() {
354        let d = get_date_from_arg("12-5", false);
355
356        assert_eq!(
357            Local
358                .ymd(Local::today().year(), 12, 5)
359                .and_hms(0, 0, 0)
360                .timestamp(),
361            d.timestamp()
362        );
363    }
364
365    #[test]
366    fn test_get_date_from_arg_today() {
367        let d = get_date_from_arg("today", false);
368
369        assert_eq!(Local::today().and_hms(0, 0, 0).timestamp(), d.timestamp());
370    }
371}