rtimelog 0.51.0

System for tracking time in a text-log-based format.
Documentation
//! Type holding the data for generating the hourly bar chart.
//!
//! # Description
//!
//! The [`DayHours`] type represents the time spent in events during each hour of a day.
//! The [`Hour`] type represents the work events applied to a particular hour.

use std::time::Duration;

use crate::TaskEvent;

fn hour() -> Duration { Duration::from_secs(3600) }

/// [`DayHours`] type tracks the hours containing [`TaskEvent`]s.
pub struct DayHours {
    // Starting hour
    start: usize,
    // Vector containing [`Hour`] object starting from `start` through all used hours.
    hours: Vec<Hour>
}

impl DayHours {
    /// Return the start hour
    pub fn start(&self) -> usize { self.start }

    /// Return the end hour
    pub fn end(&self) -> usize { self.start() + self.hours.len() - 1 }

    /// Return the number of hours covered by the [`DayHours`]
    pub fn num_hours(&self) -> usize { self.hours.len() }

    /// Add a new [`TaskEvent`]
    pub fn add(&mut self, event: TaskEvent) {
        let hour = event.hour();
        if self.hours.is_empty() {
            self.start = hour;
            self.hours.push(Hour::default());
        }
        if hour > self.end() {
            (0..(hour - self.end())).for_each(|_| self.hours.push(Hour::default()));
        }
        let start = self.start();
        if let Some(the_hour) = self.hours.get_mut(hour - start) {
            if let Some(ev) = the_hour.add(event) {
                self.add(ev);
            }
        }
    }

    /// Return an iterator over the [`Hour`]s
    pub fn iter(&self) -> impl Iterator<Item = &'_ Hour> { self.hours.iter() }
}

impl Default for DayHours {
    /// Create a [`DayHours`] type
    fn default() -> Self { Self { start: 0, hours: Vec::new() } }
}

/// A type representing the [`TaskEvent`]s in the hour.
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
pub struct Hour {
    events: Vec<TaskEvent>,
    offset: Duration
}

impl Hour {
    /// Return `true` if the [`Hour`] contains no [`TaskEvent`]s.
    pub fn is_empty(&self) -> bool { self.events.is_empty() }

    /// Return an iterator over the [`TaskEvent`]s
    pub fn iter(&self) -> impl Iterator<Item = &'_ TaskEvent> { self.events.iter() }

    /// Return a [`Duration`] representing the time remaining in the hour
    /// after the [`TaskEvent`]s are accounted for.
    pub fn remain(&self) -> Duration { hour() - self.offset }

    /// Add a new [`TaskEvent`], returning a new [`TaskEvent`] if there
    /// is any time left over after the hour is full.
    pub fn add(&mut self, event: TaskEvent) -> Option<TaskEvent> {
        self.offset = Duration::from_secs(u64::from(event.second_offset()));
        if event.duration() >= self.remain() {
            let (first, next) = event.split(self.remain())?;
            self.events.push(first);
            self.offset = hour();
            Some(next)
        }
        else {
            self.offset += event.duration();
            self.events.push(event);
            None
        }
    }
}

impl Default for Hour {
    /// Create a [`Hour`]
    fn default() -> Self { Self { events: vec![], offset: Duration::default() } }
}

#[cfg(test)]
mod tests {
    use spectral::prelude::*;

    use super::*;
    use crate::date::DateTime;

    fn task_event(time: (u32, u32, u32), proj: &str, secs: u64) -> TaskEvent {
        TaskEvent::new(
            DateTime::new((2022, 2, 17), time).unwrap(),
            proj,
            Duration::from_secs(secs)
        )
    }

    #[test]
    fn test_hour_new() {
        let hour = Hour::default();
        assert_that!(hour.is_empty()).is_equal_to(true);
        assert_that!(hour.iter().next()).is_none();
        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(3600));
    }

    #[test]
    fn test_hour_add_partial() {
        let mut hour = Hour::default();
        let dur = task_event((8, 0, 0), "foo", 300);

        assert_that!(hour.add(dur.clone())).is_none();

        assert_that!(hour.is_empty()).is_equal_to(false);
        assert_that!(hour.iter().next()).contains(&dur);
    }

    #[test]
    fn test_hour_add_partial_remain() {
        let mut hour = Hour::default();
        let dur = task_event((8, 0, 0), "foo", 300);

        assert_that!(hour.add(dur.clone())).is_none();

        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(3300));
        let used: Duration = hour.iter().map(|d| d.duration()).sum();
        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(3600) - used);
    }

    #[test]
    fn test_hour_add_partial_again() {
        let mut hour = Hour::default();
        let durs = [
            task_event((8, 0, 0), "foo", 300),
            task_event((8, 5, 0), "bar", 600)
        ];

        for dur in durs.iter() {
            assert_that!(hour.add(dur.clone())).is_none();
        }

        assert_that!(hour.is_empty()).is_equal_to(false);

        for (dur, hdur) in durs.iter().zip(hour.iter()) {
            assert_that!(hdur).is_equal_to(&dur);
        }
    }

    #[test]
    fn test_hour_add_partial_again_remain() {
        let mut hour = Hour::default();
        let durs = [
            task_event((8, 0, 0), "foo", 300),
            task_event((8, 5, 0), "bar", 600)
        ];

        for dur in durs.iter() {
            assert_that!(hour.add(dur.clone())).is_none();
        }

        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(2700));
        let used: Duration = hour.iter().map(|d| d.duration()).sum();
        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(3600) - used);
    }

    #[test]
    fn test_hour_add_overflow() {
        let mut hour = Hour::default();
        let dur1 = task_event((8, 0, 0), "foo", 3300);
        let dur2 = task_event((8, 55, 0), "bar", 650);

        assert_that!(hour.add(dur1.clone())).is_none();
        assert_that!(hour.add(dur2.clone())).is_some();

        let expect = [dur1, task_event((8, 55, 0), "bar", 300)];
        for (dur, hdur) in expect.iter().zip(hour.iter()) {
            assert_that!(hdur).is_equal_to(&dur);
        }
    }

    #[test]
    fn test_hour_add_overflow_remain() {
        let mut hour = Hour::default();
        let dur1 = task_event((8, 0, 0), "foo", 3300);
        let dur2 = task_event((8, 55, 0), "bar", 650);
        let remain = task_event((9, 0, 0), "bar", 350);

        assert_that!(hour.add(dur1.clone())).is_none();
        assert_that!(hour.add(dur2.clone())).contains(remain);

        assert_that!(hour.remain()).is_equal_to(Duration::from_secs(0));
        let used: Duration = hour.iter().map(|d| d.duration()).sum();
        assert_that!(used).is_equal_to(Duration::from_secs(3600));
    }
}