dotago 0.1.0

syntactic sugar for time calculations
Documentation
//!
//! This crate is a rustlang port of [dotago.js](https://github.com/alexpusch/dotago.js) npm package written by
//! [@alexpusch](https://github.com/alexpusch).
//!
//!
//! ## 📦 Cargo.toml
//!
//! ```toml
//! [dependencies]
//! dotago = "0.1"
//! ```
//!
//! ## 🔧 Example
//!
//! ### Work with dates
//! ```rust
//! use dotago::Dotago;
//! // now: 2023-03-15T10:00:00Z
//! 1.minute().ago().as_date(); // 2023-03-15T09:59:00Z
//! 2.hours().ago().as_date(); // 2023-03-15T08:00:00Z
//! 3.hours().from_now().as_date(); // 2023-03-15T13:00:00Z
//! ```
//!
//! ### Work with timestamps
//! ```rust
//! use dotago::Dotago;
//! // now: 2020-06-18T10:00:00Z
//! 1.minute().ago(); // 1592474340000
//! 1.hour().ago(); // 1592470800000
//! 1.hour().from_now(); // 1592478000000
//! ```
//!
//! ### Duration in milliseconds
//! ```rust
//! use dotago::Dotago;
//! 1.second(); // 1000
//! 1.minute(); // 6000
//! 1.hour(); // 3600000
//! ```
//!
//! ### Do math
//! ```rust
//! use dotago::Dotago;
//! // now: 2020-06-18T10:00:00Z
//! (1.hour().ago() + 2.minutes()).as_date(); // 2020-06-18T09:02:00Z
//! ```

use chrono::prelude::*;
use chrono::Duration;
use chrono::LocalResult;
use chrono::Utc;
use num_traits::cast::FromPrimitive;
use num_traits::cast::ToPrimitive;
use num_traits::NumOps;

pub trait Dotago {
    fn second(self) -> Self;
    fn seconds(self) -> Self;
    fn minute(self) -> Self;
    fn minutes(self) -> Self;
    fn hour(self) -> Self;
    fn hours(self) -> Self;
    fn day(self) -> Self;
    fn days(self) -> Self;
    fn week(self) -> Self;
    fn weeks(self) -> Self;
    fn ago(&self) -> i64;
    fn from_now(&self) -> i64;
    fn as_date(self) -> Option<DateTime<Utc>>;
}

impl<T> Dotago for T
where
    T: NumOps + FromPrimitive + ToPrimitive,
{
    fn second(self) -> Self {
        self * T::from_u32(1000).unwrap()
    }

    fn seconds(self) -> Self {
        self * T::from_u32(1000).unwrap()
    }

    fn minute(self) -> Self {
        self.seconds() * T::from_u32(60).unwrap()
    }

    fn minutes(self) -> Self {
        self.seconds() * T::from_u32(60).unwrap()
    }

    fn hour(self) -> Self {
        self.minutes() * T::from_u32(60).unwrap()
    }

    fn hours(self) -> Self {
        self.minutes() * T::from_u32(60).unwrap()
    }

    fn day(self) -> Self {
        self.hours() * T::from_u32(24).unwrap()
    }

    fn days(self) -> Self {
        self.hours() * T::from_u32(24).unwrap()
    }

    fn week(self) -> Self {
        self.days() * T::from_u32(7).unwrap()
    }

    fn weeks(self) -> Self {
        self.days() * T::from_u32(7).unwrap()
    }

    fn ago(&self) -> i64 {
        Utc::now()
            .checked_sub_signed(Duration::milliseconds(self.to_i64().unwrap()))
            .unwrap()
            .timestamp_millis()
    }

    fn from_now(&self) -> i64 {
        Utc::now()
            .checked_add_signed(Duration::milliseconds(self.to_i64().unwrap()))
            .unwrap()
            .timestamp_millis()
    }

    fn as_date(self) -> Option<DateTime<Utc>> {
        match Utc.timestamp_millis_opt(self.to_i64().unwrap()) {
            LocalResult::Single(dt) => Some(dt),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use claims::assert_ge;
    use claims::assert_le;

    #[test]
    fn second() {
        assert_eq!(1.second(), 1000);
    }

    #[test]
    fn seconds() {
        assert_eq!(2.seconds(), 2 * 1000);
    }

    #[test]
    fn minute() {
        assert_eq!(1.minute(), 1000 * 60);
    }

    #[test]
    fn minutes() {
        assert_eq!(2.minutes(), 2 * 1000 * 60);
    }

    #[test]
    fn hour() {
        assert_eq!(1.hour(), 1000 * 60 * 60);
    }

    #[test]
    fn hours() {
        assert_eq!(2.hours(), 1000 * 60 * 60 * 2);
    }

    #[test]
    fn day() {
        assert_eq!(1.day(), 1000 * 60 * 60 * 24);
    }

    #[test]
    fn days() {
        assert_eq!(2.days(), 1000 * 60 * 60 * 24 * 2);
    }

    #[test]
    fn week() {
        assert_eq!(1.week(), 1000 * 60 * 60 * 24 * 7);
    }

    #[test]
    fn weeks() {
        assert_eq!(2.weeks(), 1000 * 60 * 60 * 24 * 7 * 2);
    }

    #[test]
    fn floating_numbers() {
        assert_eq!(1.5.minutes(), 1000.0 * 60.0 * 1.5);
    }

    #[test]
    fn negative_numbers() {
        assert_eq!((-2.0).minutes(), 1000.0 * 60.0 * -2.0);
    }

    #[test]
    fn five_minutes_ago() {
        let ts = 5.minutes().ago().as_date().unwrap().timestamp();

        let expected = Utc::now()
            .checked_sub_signed(Duration::minutes(5))
            .unwrap()
            .timestamp();
        assert_le!(ts, expected);
        assert_ge!(ts, expected - 10);
    }
}