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
}
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
}
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
}
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()); assert!(Date::new(2024, 2, 29).is_some()); }
#[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);
}
}