minimo 0.5.42

terminal ui library combining alot of things from here and there and making it slightly easier to play with
Documentation
use super::*;

#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Dot {
    datetime: DateTime,
    location: Location,
}

impl Default for Dot {
    fn default() -> Self {
        Self {
            datetime: DateTime::now(),
            location: Location::default(),
        }
    }
}

impl Dot {
    pub fn new(datetime: impl Into<DateTime>, location: impl Into<Location>) -> Self {
        Self {
            datetime: datetime.into(),
            location: location.into(),
        }
    }

    pub fn datetime(&self) -> DateTime {
        self.datetime
    }

    pub fn location(&self) -> Location {
        self.location.clone()
    }

    pub fn coordinate(&self) -> Coordinate {
        self.location.coordinate().clone()
    }

    pub fn with_datetime(&self, datetime: impl Into<DateTime>) -> Self {
        Self {
            datetime: datetime.into(),
            location: self.location.clone(),
        }
    }

    pub fn with_location(&self, location: Location) -> Self {
        Self {
            datetime: self.datetime,
            location,
        }
    }

    pub fn time_distance(&self, other: &Self) -> Duration {
        (self.datetime - other.datetime).abs()
    }

    pub fn spatial_distance(&self, other: &Self) -> f32 {
        self.location.coordinate().distance(&other.location.coordinate())
    }

   pub fn set_date_time(&self, datetime: DateTime) -> Self {
       Self { datetime, location: self.location.clone() }
   }

   pub fn set_location(&self, location: Location) -> Self {
        Self { datetime: self.datetime, location }
   }

   pub fn latitude(&self) -> f32 {
       self.location.coordinate().x()
   }

   pub fn longitude(&self) -> f32 {
       self.location.coordinate().y()
   }
   
   pub fn split(&self) -> (i32, u32, u32, f64) {
       (self.datetime.year(), self.datetime.month() as u32, self.datetime.day() as u32, self.datetime.hour_fraction())
   }

   pub fn add_duration(&self, duration: impl Into<Duration>) -> Self {
       let duration = duration.into();
       let datetime = self.datetime + duration;
       self.with_datetime(datetime)
   }

   pub fn sub_duration(&self, duration: impl Into<Duration>) -> Self {
       let duration = duration.into();
       let datetime = self.datetime - duration;
       self.with_datetime(datetime)
   }


}

impl std::fmt::Display for Dot {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "DOT({}, {})", self.datetime, self.location)
    }
}

fn is_valid_date(year: u16, month: u8, day: u8) -> bool {
    if year == 0 || month < 1 || month > 12 || day < 1 {
        return false;
    }

    let days_in_month = match month {
        4 | 6 | 9 | 11 => 30,
        2 => {
            if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
                29
            } else {
                28
            }
        }
        _ => 31,
    };

    day <= days_in_month
}

fn is_valid_latitude(latitude: f32) -> bool {
    latitude >= -90.0 && latitude <= 90.0
}

fn is_valid_longitude(longitude: f32) -> bool {
    longitude >= -180.0 && longitude <= 180.0
}

// Zeller's congruence for calculating day of week
fn day_of_week(y: i32, m: i32, d: i32) -> i32 {
    let y = if m < 3 { y - 1 } else { y };
    let m = if m < 3 { m + 12 } else { m };
    let k = y % 100;
    let j = y / 100;
    (d + 13 * (m + 1) / 5 + k + k / 4 + j / 4 + 5 * j) % 7
}

// Calculates the number of days since civil 1970-01-01. Negative values indicate days prior to 1970-01-01.
fn days_from_civil(y: i32, m: i32, d: i32) -> i64 {
    let y = y - (m <= 2) as i32;
    let era = (if y >= 0 { y } else { y - 399 }) / 400;
    let yoe = y - era * 400;
    let doy = (153 * (m + (if m > 2 { -3 } else { 9 })) + 2) / 5 + d - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    (era * 146097 + doe - 719468) as i64
}

// Calculates year/month/day from number of days since 1970-01-01.
fn civil_from_days(days: i64) -> (i32, i32, i32) {
    let days = days + 719468;
    let era = (if days >= 0 { days } else { days - 146096 }) / 146097;
    let doe = days - era * 146097;
    let yoe = ((doe - doe / 1460 + doe / 36524 - doe / 146096) / 365) as i32;
    let y = yoe + era as i32 * 400;
    let doy = doe - (365 * yoe as i64 + yoe as i64 / 4 - yoe as i64 / 100);
    let mp = (5 * doy + 2) / 153;
    let d = (doy - (153 * mp + 2) / 5 + 1) as i32;
    let m = (mp + (if mp < 10 { 3 } else { -9 })) as i32;
    match (y, m, d) {
        (y, m, d) if is_valid_date(y as u16, m as u8, d as u8) => (y, m, d),
        _ => (0, 0, 0),
    }
}

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

    #[test]
    fn test_date() {
        let date = Date::new(2023, 5, 17).unwrap();
        assert_eq!(date.year(), 2023);
        assert_eq!(date.month(), 5);
        assert_eq!(date.date(), 17);
        assert_eq!(date.to_string(), "2023-05-17");

        assert_eq!(date.next_day().to_string(), "2023-05-18");
        assert_eq!(date.previous_day().to_string(), "2023-05-16");

        assert!(Date::new(2023, 2, 29).is_none()); // Not a leap year
        assert!(Date::new(2024, 2, 29).is_some()); // Leap year
    }

    #[test]
    fn test_time() {
        let time = Time::new(14, 30, 0);
        assert_eq!(time.hour(), 14);
        assert_eq!(time.minute(), 30);
        assert_eq!(time.second(), 0);
        assert_eq!(time.to_string(), "14:30:00");

 
    }

    #[test]
    fn test_timezone() {
        let tz = TimeZone::new(5.50);
        assert_eq!(tz.to_string(), "+05:30");
        assert_eq!(tz.offset_seconds(), 19800);

        let utc = TimeZone::utc();
        assert_eq!(utc.to_string(), "+00:00");
        assert_eq!(utc.offset_seconds(), 0);
    }

    #[test]
    fn test_datetime() {
        let date = Date::new(2023, 5, 17).unwrap();
        let time = Time::new(14, 30, 0);
        let tz = TimeZone::new(5.50);
        let dt = DateTime::from_date_time(date, time, tz);
        assert_eq!(dt.to_string(), "2023-05-17T14:30:00+05:30");

        let utc_dt = dt.to_utc();
        assert_eq!(utc_dt.to_string(), "2023-05-17T09:00:00+00:00");

        assert_eq!(dt.date().to_string(), "2023-05-17");
        assert_eq!(dt.time().to_string(), "14:30:00");
    }

    #[test]
    fn test_datetime_arithmetic() {
        let dt = DateTime::from_date_time(
            Date::new(2023, 5, 17).unwrap(),
            Time::new(14, 30, 0),
            TimeZone::utc(),
        );
        let dt_plus_1day = dt + Duration::from_days(1);
        assert_eq!(dt_plus_1day.to_string(), "2023-05-18T14:30:00+00:00");

        let dt_minus_1hour = dt - Duration::from_seconds(3600);
        assert_eq!(dt_minus_1hour.to_string(), "2023-05-17T13:30:00+00:00");

        let duration = dt_plus_1day - dt;
        assert_eq!(duration.total_seconds(), 86400);
    }

    #[test]
    fn test_duration() {
        let d1 = Duration::new(3600, 500_000_000);
        let d2 = Duration::new(1800, 250_000_000);

        assert_eq!(d1.total_seconds(), 3600);
        assert_eq!(d1.to_string(), "1h 0.5s");

        let sum = d1 + d2;
        assert_eq!(sum.to_string(), "1h 30m 0.75s");

        let diff = d1 - d2;
        assert_eq!(diff.to_string(), "30m 0.25s");

        let mult = d2 * 3;
        assert_eq!(mult.to_string(), "1h 30m 0.75s");

        let div = d1 / 2;
        assert_eq!(div.to_string(), "30m 0.25s");

        let neg = -d1;
        assert_eq!(neg.to_string(), "-1h -0.5s");

        assert_eq!(Duration::from_days(2).to_string(), "2d");
    }

    #[test]
    fn test_coordinate() {
        let c1 = Coordinate::new(1.0, 2.0, 3.0);
        let c2 = Coordinate::new(4.0, 5.0, 6.0);

        assert_eq!(c1.x(), 1.0);
        assert_eq!(c1.y(), 2.0);
        assert_eq!(c1.z(), 3.0);

        assert_eq!(c1.to_string(), "(1.00, 2.00, 3.00)");

        let sum = c1 + c2;
        assert_eq!(sum.to_string(), "(5.00, 7.00, 9.00)");

        let diff = c2 - c1;
        assert_eq!(diff.to_string(), "(3.00, 3.00, 3.00)");

        let scaled = c1 * 2.0;
        assert_eq!(scaled.to_string(), "(2.00, 4.00, 6.00)");

        assert!((c1.distance(&c2) - 5.196152).abs() < 1e-6);
    }

    #[test]
    fn test_dot() {
        let dt = DateTime::from_date_time(
            Date::new(2023, 5, 17).unwrap(),
            Time::new(14, 30, 0),
            TimeZone::utc(),
        );
        let coord = Location::new("Test City".to_string(), (1.0, 2.0, 3.0), 0);
        let dot = Dot::new(dt, coord);

        assert_eq!(
            dot.to_string(),
            "DOT(2023-05-17T14:30:00+00:00, (1.00, 2.00, 3.00))"
        );

        let dt2 = dt + Duration::from_hours(1);
        let coord2 = Location::new("Test City".to_string(), (4.0, 5.0, 6.0), 0);
        let dot2 = Dot::new(dt2, coord2);

        assert_eq!(dot.time_distance(&dot2).to_string(), "1h");
        assert!((dot.spatial_distance(&dot2) - 5.196152).abs() < 1e-6);
    }
}