rtimelog 1.1.1

System for tracking time in a text-log-based format.
Documentation
//! Module representing the kind of an entry.
//!
//! # Examples
//!
//! ```rust
//! use timelog::entry::EntryKind;
//!
//! # fn main() {
//!     let ek = EntryKind::new(Some('!'));
//!     println!("Is ignored: {}", ek.is_ignored());
//!
//!     let ek = EntryKind::from_entry_line("2022-06-20 21:15:34^+happening Something");
//!     println!("Is event: {}", ek.is_event());
//! # }
//! ```
//!
//! # Description
//!
//! Objects of this type represent the kind of an [`Entry`].

#[cfg(doc)]
use crate::entry::Entry;
use crate::entry::{EntryError, MARKER_RE};

/// Enumeration of different kinds of task entries
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum EntryKind {
    /// An entry that starts a task.
    Start,
    /// Stop the most current task.
    Stop,
    /// An entry that is ignored.
    Ignored,
    /// An entry that marks a zero duration event.
    Event
}

impl From<EntryKind> for char {
    /// Return the matching mark character for the supplied kind.
    fn from(k: EntryKind) -> char {
        match k {
            EntryKind::Start | EntryKind::Stop => ' ',
            EntryKind::Ignored => '!',
            EntryKind::Event => '^'
        }
    }
}

impl EntryKind {
    /// Create a new [`EntryKind`] from an optional character, defaulting to [`EntryKind::Start`]
    /// if the character is not recognized.
    pub fn new<KC>(mark: KC) -> Self
    where
        KC: Into<Option<char>>
    {
        Self::try_new(mark).unwrap_or(Self::Start)
    }

    /// Create a new [`EntryKind`] from an optional character.
    ///
    /// # Errors
    ///
    /// Return [`EntryError::InvalidMarker`] if character not recognized.
    pub fn try_new<KC>(mark: KC) -> Result<Self, EntryError>
    where
        KC: Into<Option<char>>
    {
        match mark.into() {
            Some(' ') | None => Ok(Self::Start),
            Some('!') => Ok(Self::Ignored),
            Some('^') => Ok(Self::Event),
            Some(_) => Err(EntryError::InvalidMarker)
        }
    }

    /// Extract the appropriate [`EntryKind`] from supplied entry line.
    pub fn from_entry_line(line: &str) -> Self {
        let marker = MARKER_RE.captures(line)
            .map(|cap| cap.get(1).and_then(|m| m.as_str().chars().next()));
        Self::new(marker.flatten())
    }

    /// Is a start marker
    pub fn is_start(&self) -> bool { *self == Self::Start }

    /// Is an ignored entry marker
    pub fn is_ignored(&self) -> bool { *self == Self::Ignored }

    /// Is an event marker
    pub fn is_event(&self) -> bool { *self == Self::Event }

    /// Is a stop marker
    pub fn is_stop(&self) -> bool { *self == Self::Stop }
}

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

    use super::*;

    #[rstest]
    #[case(None, EntryKind::Start, "none")]
    #[case(Some(' '), EntryKind::Start, "space")]
    #[case(Some('!'), EntryKind::Ignored, "bang")]
    #[case(Some('^'), EntryKind::Event, "caret")]
    fn try_new_success_with_option(
        #[case]input: Option<char>,
        #[case]expected: EntryKind,
        #[case]msg: &str
    ) {
        let_assert!(Ok(actual) = EntryKind::try_new(input), "{msg}");
        assert!(actual == expected);
    }

    #[rstest]
    #[case(' ', EntryKind::Start, "space")]
    #[case('!', EntryKind::Ignored, "bang")]
    #[case('^', EntryKind::Event, "caret")]
    fn try_new_success_with_char(
        #[case]input: char,
        #[case]expected: EntryKind,
        #[case]msg: &str
    ) {
        let_assert!(Ok(actual) = EntryKind::try_new(input), "{msg}");
        assert!(actual == expected);
    }

    #[rstest]
    #[case('a')]
    #[case('8')]
    #[case('*')]
    fn try_new_fails(#[case]input: char) {
        assert!(EntryKind::try_new(input).is_err());
    }

    #[test]
    fn new_succeeds_on_none() {
        assert!(EntryKind::new(None) == EntryKind::Start);
    }

    #[rstest]
    #[case(' ', EntryKind::Start, "space")]
    #[case('!', EntryKind::Ignored, "bang")]
    #[case('^', EntryKind::Event, "caret")]
    #[case('a', EntryKind::Start, "invalid letter")]
    #[case('8', EntryKind::Start, "invalid number")]
    #[case('*', EntryKind::Start, "invalid special")]
    fn new_never_fails(#[case]input: char, #[case]expected: EntryKind, #[case]msg: &str) {
        assert!(EntryKind::new(input) == expected, "{msg}");
    }

    #[rstest]
    #[case(EntryKind::Start,   ' ', "start")]
    #[case(EntryKind::Stop,    ' ', "stop")]
    #[case(EntryKind::Ignored, '!', "ignored")]
    #[case(EntryKind::Event,   '^', "event")]
    fn char_from_kind(#[case]kind: EntryKind, #[case]out: char, #[case]msg: &str) {
        assert!(char::from(kind) == out, "{msg}");
    }

    #[rstest]
    #[case("2013-06-05 10:00:02 +test @Commented",  EntryKind::Start,   "start line")]
    #[case("2013-06-05 10:00:02!+test @Ignored",    EntryKind::Ignored, "ignored line")]
    #[case("2013-06-05 10:00:02^+test @Event",      EntryKind::Event,   "pinned line")]
    #[case("#2013-06-05 10:00:02 +test @Commented", EntryKind::Start,   "commented line")]
    fn from_entry_line(#[case]input: &str, #[case]expected: EntryKind, #[case]msg: &str) {
        assert!(EntryKind::from_entry_line(input) == expected, "{msg}");
    }
}