rtimelog 1.1.1

System for tracking time in a text-log-based format.
Documentation
//! Represention of a task event.
//!
//! # Examples
//!
//! ```rust
//! use std::time::Duration;
//! use timelog::{DateTime, TaskEvent};
//!
//! # fn main() {
//! let timestamp = DateTime::new((2022, 03, 14), (10, 0, 0)).expect("Invalid date or time");
//! let mut event = TaskEvent::new(
//!     timestamp,
//!     "project_1",
//!     Duration::from_secs(300)
//! );
//! println!("{}", event.project());
//! event.add_dur(Duration::from_secs(3000));
//! println!("{:?}", event.duration());
//! # }
//! ```
//!
//! # Description
//!
//! The [`TaskEvent`] type represents a single task event containing a
//! start time and duration.

use std::time::Duration;

#[doc(inline)]
use crate::date::DateTime;
#[doc(inline)]
use crate::entry::Entry;

/// Structure representing a single task event.
#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq)]
pub struct TaskEvent {
    /// Timestamp of the start of this event
    start: DateTime,
    /// Optional [`String`] representing the project
    proj:  Option<String>,
    /// Calculated total [`Duration`] of the event
    dur:   Duration
}

impl TaskEvent {
    /// Create a [`TaskEvent`] with the supplied parameters.
    pub fn new<'a, OS>(start: DateTime, proj: OS, dur: Duration) -> Self
    where
        OS: Into<Option<&'a str>>
    {
        Self { start, proj: proj.into().map(ToString::to_string), dur }
    }

    /// Create a [`TaskEvent`] from the supplied entry
    pub fn from_entry(entry: &Entry) -> Self {
        Self::new(entry.date_time(), entry.project(), Duration::default())
    }

    /// Return a tuple with two [`TaskEvent`]s, split at the supplied [`Duration`].
    /// The first starts at the same starting point as this [`TaskEvent`] and
    /// stopping at the supplied [`Duration`]. The second starts [`Duration`] after
    /// the start, containg the rest of the time.
    #[rustfmt::skip]
    pub fn split(&self, dur: Duration) -> Option<(Self, Self)> {
        Some((
            Self {
                start: self.start,
                proj:  self.proj(),
                dur
            },
            Self {
                start: (self.start + dur).ok()?,
                proj:  self.proj(),
                dur:   self.dur - dur
            }
        ))
    }

    /// Return the project as a String if one exists.
    pub fn proj(&self) -> Option<String> { self.proj.as_ref().cloned() }

    /// Return the project as a String defaulting to an empty string.
    pub fn project(&self) -> String { self.proj().unwrap_or_default() }

    /// Return the hour of the start time.
    pub fn hour(&self) -> usize { self.start.hour() as usize }

    /// Return the starting [`DateTime`].
    pub fn start(&self) -> &DateTime { &self.start }

    /// Extend the [`TaskEvent`]'s duration by the supplied amount.
    pub fn add_dur(&mut self, dur: Duration) { self.dur += dur; }

    /// Return the [`Duration`] of this [`TaskEvent`].
    pub fn duration(&self) -> Duration { self.dur }

    /// Return the number of seconds this [`TaskEvent`] has lasted.
    pub fn as_secs(&self) -> u64 { self.dur.as_secs() }

    /// Return the number of seconds after the hour of the starting time.
    pub fn second_offset(&self) -> u32 { self.start.second_offset() }
}

#[cfg(test)]
mod tests {
    use assert2::{assert, let_assert};

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

    #[test]
    fn test_new_with_project() {
        let_assert!(Ok(datetime) = DateTime::new((2022, 3, 20), (12, 34, 56)));
        let task = TaskEvent::new(
            datetime,
            "proja",
            Duration::from_secs(305)
        );
        let_assert!(Some(proj) = task.proj());
        assert!(proj == String::from("proja"));
        assert!(task.project() == String::from("proja"));
        let_assert!(Ok(expect_start) = DateTime::new((2022, 3, 20), (12, 34, 56)));
        assert!(task.start() == &expect_start);
        assert!(task.hour() == 12);
        assert!(task.duration() == Duration::from_secs(305));
    }

    #[test]
    fn test_new_without_project() {
        let_assert!(Ok(datetime) = DateTime::new((2022, 3, 20), (13, 24, 56)));
        let task = TaskEvent::new(
            datetime,
            None,
            Duration::from_secs(255)
        );
        assert!(task.proj() == None);
        assert!(task.project() == String::new());
        let_assert!(Ok(expect_start) = DateTime::new((2022, 3, 20), (13, 24, 56)));
        assert!(task.start() == &expect_start);
        assert!(task.hour() == 13);
        assert!(task.duration() == Duration::from_secs(255));
    }

    #[test]
    fn test_add_dur() {
        let_assert!(Ok(datetime) = DateTime::new((2022, 3, 20), (12, 34, 56)));
        let mut task = TaskEvent::new(
            datetime,
            "proja",
            Duration::from_secs(305)
        );

        assert!(task.as_secs() == 305, "Initial value");

        task.add_dur(Duration::from_secs(3600));
        assert!(task.as_secs() == 3905, "After add_dur");
        assert!(task.duration() == Duration::from_secs(3905));
    }

    #[test]
    fn test_second_offset() {
        let_assert!(Ok(datetime) = DateTime::new((2022, 3, 20), (12, 34, 56)));
        let task = TaskEvent::new(
            datetime,
            "proja",
            Duration::from_secs(305)
        );

        assert!(task.second_offset() == 34 * 60 + 56);
    }

    #[test]
    fn test_split() {
        let_assert!(Ok(datetime) = DateTime::new((2022, 3, 20), (12, 34, 56)));
        let task = TaskEvent::new(
            datetime,
            "proja",
            Duration::from_secs(600)
        );

        let_assert!(Some((first, second)) = task.split(Duration::from_secs(250)));

        let_assert!(Ok(expected) = DateTime::new((2022, 3, 20), (12, 34, 56)));
        assert!(first.start() == &expected);
        assert!(first.duration() == Duration::from_secs(250));

        let_assert!(Ok(expected) = DateTime::new((2022, 3, 20), (12, 39, 6)));
        assert!(second.start() == &expected);
        assert!(second.duration() == Duration::from_secs(350));
    }
}