ako 0.0.3

Ako is a Rust crate that offers a practical and human-friendly approach to creating, manipulating, formatting and converting dates, times and timestamps.
Documentation
use core::fmt::{self, Debug, Formatter};
use core::ops::Sub;

use crate::{AsTime, Calendar, Date, PlainDateTime, TimeInterval};

/// The **time of day**, without a time-zone.
///
/// This class does not store or represent a date or time-zone. `PlainTime`
/// can be thought of as the time from the clock on your wall.
///
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct PlainTime {
    pub(crate) secs: u32,
    pub(crate) nsec: u32,
}

// Constants
impl PlainTime {
    /// The time at midnight, `00:00`.
    pub const MIDNIGHT: Self = Self::of(0, 0, 0);
}

// Construction
impl PlainTime {
    /// Obtains the time from an hour, minute, and second.
    #[must_use]
    pub const fn of(hour: u8, minute: u8, second: u8) -> Self {
        let mut secs = (hour as u32) * 3_600;
        secs += (minute as u32) * 60;
        secs += second as u32;

        Self { secs, nsec: 0 }
    }

    /// Obtains the time from an hour, minute, second, and nanosecond.
    #[must_use]
    pub const fn of_nanosecond(hour: u8, minute: u8, second: u8, nanoseconds: u32) -> Self {
        let mut time = Self::of(hour, minute, second);
        time.nsec = nanoseconds;

        time
    }
}

// Conversion From
impl PlainTime {
    /// Creates a [`PlainTime`] from the number of seconds elapsed since midnight.
    #[must_use]
    pub const fn from_seconds_since_midnight(seconds: u32) -> Self {
        Self {
            secs: seconds.rem_euclid(86_400),
            nsec: 0,
        }
    }

    /// Creates a [`PlainTime`] from the number of nanoseconds elapsed since midnight.
    #[must_use]
    pub const fn from_nanoseconds_since_midnight(nanoseconds: u64) -> Self {
        let secs = nanoseconds.div_euclid(1_000_000_000) as u32;
        let nsec = nanoseconds.rem_euclid(1_000_000_000) as u32;

        let mut time = Self::from_seconds_since_midnight(secs);
        time.nsec = nsec;

        time
    }

    /// Creates a [`PlainTime`] from the number of seconds and nanoseconds since the Unix epoch.
    #[must_use]
    pub const fn from_unix_timestamp(seconds: i64, nanoseconds: u32) -> Self {
        Self {
            secs: seconds.rem_euclid(86_400) as u32,
            nsec: nanoseconds,
        }
    }
}

// Components
impl PlainTime {
    /// Gets the hour within the day.
    #[must_use]
    pub const fn hour(self) -> u8 {
        ((self.secs % 86_400) / 3_600) as u8
    }

    /// Gets the minute within the hour.
    #[must_use]
    pub const fn minute(self) -> u8 {
        ((self.secs % 3_600) / 60) as u8
    }

    /// Gets the second within the minute.
    #[must_use]
    pub const fn second(self) -> u8 {
        (self.secs % 60) as u8
    }

    /// Gets the millisecond within the second.
    #[must_use]
    pub const fn millisecond(self) -> u16 {
        (self.nsec / 1_000_000) as u16
    }

    /// Gets the microsecond within the second.
    #[must_use]
    pub const fn microsecond(self) -> u32 {
        self.nsec / 1_000
    }

    /// Gets the nanosecond within the second.
    #[must_use]
    pub const fn nanosecond(self) -> u32 {
        self.nsec
    }

    /// Returns the hour, minute, second, and nanosecond within the day.
    #[must_use]
    pub const fn components(self) -> (u8, u8, u8, u32) {
        let mut second = self.secs % 86_400;

        let hour = second / 3_600;
        second -= hour * 3_600;

        let minute = second / 60;
        second -= minute * 60;

        (hour as u8, minute as u8, second as u8, self.nsec)
    }
}

// Composition
impl PlainTime {
    /// Combines this date with a time to create a `PlainDateTime`.
    #[must_use]
    pub const fn with_date<C: Calendar>(self, date: Date<C>) -> PlainDateTime<C> {
        PlainDateTime { date, time: self }
    }
}

// Conversion To
impl PlainTime {
    /// Returns the number of seconds elapsed since midnight.
    #[must_use]
    pub const fn as_seconds_since_midnight(self) -> u32 {
        self.secs
    }

    /// Returns the number of nanoseconds elapsed since midnight.
    #[must_use]
    pub const fn as_nanoseconds_since_midnight(self) -> u64 {
        let mut nanos = (self.secs as u64) * 1_000_000_000;
        nanos += self.nsec as u64;

        nanos
    }

    /// Returns the time in seconds and nanoseconds since midnight.
    pub(crate) const fn as_timestamp(self) -> (i64, u32) {
        // returns the internal clock time as a timestamp
        // this is intended only for measurement
        (self.secs as i64, self.nsec)
    }
}

impl Debug for PlainTime {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.pad(&self.format_rfc3339())
    }
}

impl From<(u8, u8, u8)> for PlainTime {
    fn from((hour, minute, second): (u8, u8, u8)) -> Self {
        Self::of(hour, minute, second)
    }
}

impl From<(u8, u8, u8, u32)> for PlainTime {
    fn from((hour, minute, second, nanosecond): (u8, u8, u8, u32)) -> Self {
        Self::of_nanosecond(hour, minute, second, nanosecond)
    }
}

impl AsTime for PlainTime {
    fn as_time(&self) -> PlainTime {
        // return a copy of `self`
        // intended only for usage in generic functions
        *self
    }

    fn as_timestamp(&self) -> (i64, u32) {
        (*self).as_timestamp()
    }
}

impl Sub<Self> for PlainTime {
    type Output = TimeInterval;

    fn sub(self, rhs: Self) -> Self::Output {
        TimeInterval::between(rhs, self)
    }
}

#[cfg(test)]
mod tests {
    use alloc::format;

    use test_case::test_case;

    use crate::PlainTime;

    #[test]
    fn expect_clock_valid() {
        // check all valid permutations of the clock to ensure we can create from
        // and convert to clock
        for hour in 0..24 {
            for minute in 0..60 {
                for second in 0..60 {
                    let time = PlainTime::of(hour, minute, second);

                    // assert components
                    assert_eq!(time.components(), (hour, minute, second, 0));

                    // assert timestamp
                    assert_eq!(
                        PlainTime::from_seconds_since_midnight(time.as_seconds_since_midnight()),
                        time
                    );

                    // assert RFC-3339
                    assert_eq!(
                        time.format_rfc3339(),
                        format!("{:02}:{:02}:{:02}", hour, minute, second)
                    );
                }
            }
        }
    }

    #[test_case((0, 0, 0), 0)] // 00:00:00
    #[test_case((4, 0, 10), 14410)] // 04:00:10
    #[test_case((23, 59, 59), 86399)] // 23:59:59
    fn expect_time_as_timestamp(clock: (u8, u8, u8), timestamp: i64) {
        assert_eq!(
            PlainTime::of(clock.0, clock.1, clock.2).as_timestamp().0,
            timestamp
        );
    }

    #[test_case((5, 10, 0), (12, 3, 0), (6, 53, 0))]
    fn time_sub_time(start: (u8, u8, u8), end: (u8, u8, u8), expected: (i8, i8, i8)) {
        let time_start = PlainTime::of(start.0, start.1, start.2);
        let time_end = PlainTime::of(end.0, end.1, end.2);
        let ti = time_end - time_start;

        assert_eq!(ti.hours(), expected.0);
        assert_eq!(ti.minutes(), expected.1);
        assert_eq!(ti.seconds(), expected.2);
    }
}