From 15eff6083109fdda3a158823cc687e7b1057a7c6 Mon Sep 17 00:00:00 2001 From: Philip Linden <45497023+philiplinden@users.noreply.github.com> Date: Wed, 4 Sep 2024 03:31:56 -0400 Subject: [PATCH] `lofitime` rewrite to use From and Into instead of custom traits (#32) * lofitime: total rewrite from traits to From * lofitime: total rewrite using From impls --- Cargo.lock | 2 +- lofitime/Cargo.toml | 2 +- lofitime/README.md | 49 ++++++ lofitime/src/lib.rs | 313 ++++++++++++++++++++++++++--------- spacetime/src/ui/datetime.rs | 10 +- 5 files changed, 296 insertions(+), 80 deletions(-) create mode 100644 lofitime/README.md diff --git a/Cargo.lock b/Cargo.lock index b43434c..acdb1e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3328,7 +3328,7 @@ dependencies = [ [[package]] name = "lofitime" -version = "0.1.0" +version = "0.2.0" dependencies = [ "chrono", "hifitime", diff --git a/lofitime/Cargo.toml b/lofitime/Cargo.toml index a4b598f..5661205 100644 --- a/lofitime/Cargo.toml +++ b/lofitime/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lofitime" description = "Handy translations between Hifitime and Chrono datetimes and durations." -version = "0.1.0" +version = "0.2.0" authors.workspace = true edition.workspace = true diff --git a/lofitime/README.md b/lofitime/README.md new file mode 100644 index 0000000..aacc6fa --- /dev/null +++ b/lofitime/README.md @@ -0,0 +1,49 @@ +# lofitime + +`lofitime` is a Rust crate that provides wrapper types and conversion utilities +for working with both high-precision (`hifitime`) and low-precision (`chrono`) +time libraries. This crate aims to bridge the gap between these two popular +time-handling libraries in the Rust ecosystem. + +## Features + +- Wrapper types for `hifitime::Epoch`, `hifitime::Duration`, + `chrono::DateTime`, and `chrono::Duration` +- Low-friction conversions between `hifitime` and `chrono` types +- Implementation of common traits like `Deref`, `DerefMut`, and `Timelike` for + wrapper types +- Simplified `SimpleDateLike` trait for common date operations + +## Todos +- [ ] ✨Seamless✨ conversions between `hifitime` and `chrono` types +- [ ] Upstream into `hifitime`? + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +lofitime = "0.2.0" +``` + +Then, you can use the wrapper types in your code: + +```rust +use lofitime::{HifiEpoch, LofiDateTime}; + +let hifi_epoch = HifiEpoch(hifitime::Epoch::from_gregorian_utc(2023, 8, 12, 15, 30, 45, 0)); +let lofi_datetime: LofiDateTime = hifi_epoch.into(); + +assert_eq!(lofi_datetime.year(), 2023); +assert_eq!(lofi_datetime.month(), 8); +assert_eq!(lofi_datetime.day(), 12); +``` + +## Why use lofitime? + +- **Interoperability**: Easily convert between `hifitime` and `chrono` types. +- **Simplified API**: Use a consistent interface for both high-precision and + low-precision time operations. +- **Type Safety**: Wrapper types provide clear distinctions between different + time representations. diff --git a/lofitime/src/lib.rs b/lofitime/src/lib.rs index aae6b51..6583aad 100644 --- a/lofitime/src/lib.rs +++ b/lofitime/src/lib.rs @@ -1,105 +1,269 @@ -use chrono; +use chrono::{Datelike, Timelike}; use hifitime; +use std::ops::{Deref, DerefMut}; -/// Adds functions to a hifitime epoch so it can be represented as a chrono time. -pub trait HifiDateTime { - fn to_lofi_utc(&self) -> chrono::DateTime; - fn to_lofi_naive(&self) -> chrono::NaiveDateTime; +/// A wrapper type for `hifitime::Epoch`. +/// +/// This type allows for easy interoperability between `hifitime` and `chrono` libraries. +#[derive(Clone, Copy)] +pub struct HifiEpoch(pub hifitime::Epoch); + +/// A wrapper type for `hifitime::Duration`. +/// +/// This type allows for easy interoperability between `hifitime` and `chrono` libraries. +#[derive(Clone, Copy)] +pub struct HifiDuration(pub hifitime::Duration); + +/// A wrapper type for `chrono::DateTime`. +/// +/// This type allows for easy interoperability between `hifitime` and `chrono` libraries. +#[derive(Clone, Copy)] +pub struct LofiDateTime(pub chrono::DateTime); + +/// A wrapper type for `chrono::Duration`. +/// +/// This type allows for easy interoperability between `hifitime` and `chrono` libraries. +#[derive(Clone, Copy)] +pub struct LofiDuration(pub chrono::Duration); + +// Deref and DerefMut for wrapper types +impl Deref for HifiEpoch { + type Target = hifitime::Epoch; + fn deref(&self) -> &Self::Target { &self.0 } +} +impl DerefMut for HifiEpoch { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl Deref for HifiDuration { + type Target = hifitime::Duration; + fn deref(&self) -> &Self::Target { &self.0 } +} +impl DerefMut for HifiDuration { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl Deref for LofiDateTime { + type Target = chrono::DateTime; + fn deref(&self) -> &Self::Target { &self.0 } +} +impl DerefMut for LofiDateTime { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl Deref for LofiDuration { + type Target = chrono::Duration; + fn deref(&self) -> &Self::Target { &self.0 } +} +impl DerefMut for LofiDuration { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl HifiDateTime for hifitime::Epoch { - /// Represents an Epoch as a UTC date and time - fn to_lofi_utc(&self) -> chrono::DateTime { - chrono::DateTime::from_timestamp_millis(self.to_unix_milliseconds() as i64).unwrap() +/// A simplified trait for date-like operations. +/// +/// This trait provides a subset of the `chrono::Datelike` trait's functionality, +/// focusing on the most commonly used date operations. +pub trait SimpleDateLike { + /// Returns the year number in the calendar date. + fn year(&self) -> i32; + + /// Returns the month number starting from 1. + fn month(&self) -> u32; + + /// Returns the day of month starting from 1. + fn day(&self) -> u32; + + /// Returns the time-zone naive date. + fn date_naive(&self) -> chrono::NaiveDate; +} + +impl SimpleDateLike for LofiDateTime { + fn year(&self) -> i32 { self.0.year() } + fn month(&self) -> u32 { self.0.month() } + fn day(&self) -> u32 { self.0.day() } + fn date_naive(&self) -> chrono::NaiveDate { self.0.date_naive() } +} + +/// A simplified trait for time-like operations. +/// +/// This trait provides a subset of the `chrono::Timelike` trait's +/// functionality, focusing on the most commonly used operations. +impl Timelike for LofiDateTime { + fn hour(&self) -> u32 { self.0.hour() } + fn minute(&self) -> u32 { self.0.minute() } + fn second(&self) -> u32 { self.0.second() } + fn nanosecond(&self) -> u32 { self.0.nanosecond() } + fn with_hour(&self, hour: u32) -> Option { + self.0.with_hour(hour).map(LofiDateTime) } - /// Represents an Epoch in UTC then strips it down to a naive time - /// (no time zone). - fn to_lofi_naive(&self) -> chrono::NaiveDateTime { - self.to_lofi_utc().naive_utc() + fn with_minute(&self, min: u32) -> Option { + self.0.with_minute(min).map(LofiDateTime) + } + fn with_second(&self, sec: u32) -> Option { + self.0.with_second(sec).map(LofiDateTime) + } + fn with_nanosecond(&self, nano: u32) -> Option { + self.0.with_nanosecond(nano).map(LofiDateTime) } } -/// Adds functions to a hifitime duration so it can be represented as a chrono duration. -pub trait HifiDuration { - fn to_lofi_duration(&self) -> chrono::Duration; +/// # Conversions using `From` traits + +/// ## Converting between internal types + +/// Converts a `HifiEpoch` to a `LofiDateTime`. +/// +/// This conversion may lose some precision due to the differences in +/// internal representations between `hifitime` and `chrono`. +impl From for LofiDateTime { + fn from(epoch: HifiEpoch) -> Self { + LofiDateTime(chrono::DateTime::from_timestamp_millis(epoch.to_unix_milliseconds() as i64).unwrap()) + } } -impl HifiDuration for hifitime::Duration { - fn to_lofi_duration(&self) -> chrono::Duration { - let (centuries, nanos) = self.to_parts(); +/// Converts a `HifiDuration` to a `LofiDuration`. +/// +/// This conversion may lose some precision due to the differences in +/// internal representations between `hifitime` and `chrono`. +impl From for LofiDuration { + fn from(duration: HifiDuration) -> Self { + let (centuries, nanos) = duration.to_parts(); let centuries_as_days = centuries as i64 / hifitime::DAYS_PER_CENTURY_I64; let chrono_days = chrono::Duration::days(centuries_as_days); let chrono_nanos = chrono::Duration::nanoseconds(nanos as i64); - chrono_days + chrono_nanos + LofiDuration(chrono_days + chrono_nanos) } } -/// Adds functions to a chrono time so it can be represented as a hifitime epoch. -/// We only keep precision to the nearest millisecond from chrono. -pub trait LofiDateTime { - fn to_hifi_epoch(&self) -> hifitime::Epoch; +/// Converts a `LofiDateTime` to a `HifiEpoch`. +/// +/// This conversion preserves the precision of the `chrono::DateTime`. +impl From for HifiEpoch { + fn from(datetime: LofiDateTime) -> Self { + HifiEpoch(hifitime::Epoch::from_unix_duration(hifitime::Duration::from_milliseconds( + datetime.timestamp_millis() as f64, + ))) + } } -impl LofiDateTime for chrono::DateTime -where - Tz: chrono::TimeZone, -{ - fn to_hifi_epoch(&self) -> hifitime::Epoch { - hifitime::Epoch::from_unix_duration(hifitime::Duration::from_milliseconds( - self.to_utc().timestamp_millis() as f64, - )) +/// Converts a `LofiDuration` to a `HifiDuration`. +/// +/// This conversion preserves the precision of the `chrono::Duration`. +impl From for HifiDuration { + fn from(duration: LofiDuration) -> Self { + HifiDuration(hifitime::Duration::from_milliseconds(duration.num_milliseconds() as f64)) } } -/// Adds functions to a chrono duration so it can be represented as a hifitime duration. -/// We only keep precision to the nearest millisecond from chrono. -pub trait LofiDuration { - fn to_hifi_duration(&self) -> hifitime::Duration; +/// ## Conversions to and from external types + +/// Converts a `HifiEpoch` to a `hifitime::Epoch` +impl From for hifitime::Epoch { + fn from(epoch: HifiEpoch) -> Self { + epoch.0 + } +} + +/// Converts a `HifiEpoch` to a `chrono::DateTime` +impl From for chrono::DateTime { + fn from(epoch: HifiEpoch) -> Self { + let lofi: LofiDateTime = epoch.into(); + lofi.0 + } +} + +/// Converts a `HifiEpoch` to a `chrono::NaiveDateTime`. +/// +/// This conversion may lose some precision due to the differences in +/// internal representations between `hifitime` and `chrono`. +impl From for chrono::NaiveDateTime { + fn from(epoch: HifiEpoch) -> Self { + LofiDateTime::from(epoch).naive_utc() + } +} + +/// Converts a `HifiDuration` to a `hifitime::Duration` +impl From for hifitime::Duration { + fn from(hifi_duration: HifiDuration) -> Self { + hifi_duration.0 + } +} + +/// Converts a `HifiDuration` to a `chrono::Duration` +impl From for chrono::Duration { + fn from(hifi_duration: HifiDuration) -> Self { + let lofi: LofiDuration = hifi_duration.into(); + lofi.0 + } +} + +/// Converts a `LofiDateTime` to a `hifitime::Epoch` +impl From for hifitime::Epoch { + fn from(datetime: LofiDateTime) -> Self { + let hifi: HifiEpoch = datetime.into(); + hifi.0 + } +} + +/// Converts a `LofiDateTime` to a `chrono::DateTime` +impl From for chrono::DateTime { + fn from(datetime: LofiDateTime) -> Self { + datetime.0 + } } -impl LofiDuration for chrono::TimeDelta { - fn to_hifi_duration(&self) -> hifitime::Duration { - hifitime::Duration::from_milliseconds(self.num_milliseconds() as f64) +/// Converts a `LofiDuration` to a `hifitime::Duration` +impl From for hifitime::Duration { + fn from(lofi_duration: LofiDuration) -> Self { + let hifi: HifiDuration = lofi_duration.into(); + hifi.0 + } +} + +/// Converts a `LofiDuration` to a `chrono::Duration` +impl From for chrono::Duration { + fn from(lofi_duration: LofiDuration) -> Self { + lofi_duration.0 } } #[cfg(test)] mod tests { use super::*; - use chrono; - use hifitime; + use chrono::TimeZone; #[test] fn test_hifi_to_lofi_utc() { - let hifi_epoch = hifitime::Epoch::from_gregorian_utc(2023, 8, 12, 15, 30, 45, 0); - let lofi_utc = hifi_epoch.to_lofi_utc(); + let hifi_epoch = HifiEpoch(hifitime::Epoch::from_gregorian_utc(2023, 8, 12, 15, 30, 45, 0)); + let lofi_utc: LofiDateTime = hifi_epoch.into(); - assert_eq!(chrono::Datelike::year(&lofi_utc), 2023); - assert_eq!(chrono::Datelike::month(&lofi_utc), 8); - assert_eq!(chrono::Datelike::day(&lofi_utc), 12); - assert_eq!(chrono::Timelike::hour(&lofi_utc), 15); - assert_eq!(chrono::Timelike::minute(&lofi_utc), 30); - assert_eq!(chrono::Timelike::second(&lofi_utc), 45); + assert_eq!(lofi_utc.year(), 2023); + assert_eq!(lofi_utc.month(), 8); + assert_eq!(lofi_utc.day(), 12); + assert_eq!(lofi_utc.hour(), 15); + assert_eq!(lofi_utc.minute(), 30); + assert_eq!(lofi_utc.second(), 45); } #[test] fn test_hifi_to_lofi_naive() { - let hifi_epoch = hifitime::Epoch::from_gregorian_utc(2023, 8, 12, 15, 30, 45, 0); - let lofi_naive = hifi_epoch.to_lofi_naive(); + let hifi_epoch = HifiEpoch(hifitime::Epoch::from_gregorian_utc(2023, 8, 12, 15, 30, 45, 0)); + let lofi_naive: chrono::NaiveDateTime = hifi_epoch.into(); - assert_eq!(chrono::Datelike::year(&lofi_naive), 2023); - assert_eq!(chrono::Datelike::month(&lofi_naive), 8); - assert_eq!(chrono::Datelike::day(&lofi_naive), 12); - assert_eq!(chrono::Timelike::hour(&lofi_naive), 15); - assert_eq!(chrono::Timelike::minute(&lofi_naive), 30); - assert_eq!(chrono::Timelike::second(&lofi_naive), 45); + assert_eq!(lofi_naive.year(), 2023); + assert_eq!(lofi_naive.month(), 8); + assert_eq!(lofi_naive.day(), 12); + assert_eq!(lofi_naive.hour(), 15); + assert_eq!(lofi_naive.minute(), 30); + assert_eq!(lofi_naive.second(), 45); } #[test] fn test_hifi_duration_to_lofi_duration() { - let hifi_duration = - hifitime::Duration::from_days(365.) + hifitime::Duration::from_hours(6.); - let lofi_duration = hifi_duration.to_lofi_duration(); + let hifi_duration = HifiDuration( + hifitime::Duration::from_days(365.) + hifitime::Duration::from_hours(6.) + ); + let lofi_duration: LofiDuration = hifi_duration.into(); assert_eq!(lofi_duration.num_days(), 365); assert_eq!(lofi_duration.num_hours() % 24, 6); @@ -107,10 +271,8 @@ mod tests { #[test] fn test_lofi_to_hifi_epoch() { - use chrono::TimeZone; - - let lofi_datetime = chrono::Utc.with_ymd_and_hms(2023, 8, 12, 15, 30, 45).unwrap(); - let hifi_epoch = lofi_datetime.to_hifi_epoch(); + let lofi_datetime = LofiDateTime(chrono::Utc.with_ymd_and_hms(2023, 8, 12, 15, 30, 45).unwrap()); + let hifi_epoch: HifiEpoch = lofi_datetime.into(); let (y, m, d, h, min, s, _) = hifi_epoch.to_gregorian_utc(); assert_eq!(y, 2023); @@ -123,8 +285,8 @@ mod tests { #[test] fn test_lofi_duration_to_hifi_duration() { - let lofi_duration = chrono::Duration::days(365) + chrono::Duration::hours(6); - let hifi_duration = lofi_duration.to_hifi_duration(); + let lofi_duration = LofiDuration(chrono::Duration::days(365) + chrono::Duration::hours(6)); + let hifi_duration: HifiDuration = lofi_duration.into(); assert_eq!( hifi_duration.to_seconds(), @@ -134,26 +296,27 @@ mod tests { #[test] fn test_roundtrip_epoch_conversion() { - let original_epoch = - hifitime::Epoch::from_gregorian_utc(2023, 8, 12, 15, 30, 45, 123_456_789); - let roundtrip_epoch = original_epoch.to_lofi_utc().to_hifi_epoch(); + let original_epoch = HifiEpoch(hifitime::Epoch::from_gregorian_utc(2023, 8, 12, 15, 30, 45, 123_456_789)); + let roundtrip_epoch: HifiEpoch = LofiDateTime::from(original_epoch).into(); // Note: We lose some precision in the milliseconds because we don't trust Chrono to that precision assert!( - (original_epoch - roundtrip_epoch).abs() < hifitime::Duration::from_milliseconds(1.) + (*original_epoch - *roundtrip_epoch).abs() < hifitime::Duration::from_milliseconds(1.) ); } #[test] fn test_roundtrip_duration_conversion() { - let original_duration = hifitime::Duration::from_days(365.) - + hifitime::Duration::from_hours(6.) - + hifitime::Duration::from_nanoseconds(123_456_789.); - let roundtrip_duration = original_duration.to_lofi_duration().to_hifi_duration(); + let original_duration = HifiDuration( + hifitime::Duration::from_days(365.) + + hifitime::Duration::from_hours(6.) + + hifitime::Duration::from_nanoseconds(123_456_789.) + ); + let roundtrip_duration: HifiDuration = LofiDuration::from(original_duration).into(); // Note: We lose some precision in the milliseconds because we don't trust Chrono to that precision assert!( - (original_duration - roundtrip_duration).abs() + (*original_duration - *roundtrip_duration).abs() < hifitime::Duration::from_milliseconds(1.) ); } diff --git a/spacetime/src/ui/datetime.rs b/spacetime/src/ui/datetime.rs index 547d359..58e3717 100644 --- a/spacetime/src/ui/datetime.rs +++ b/spacetime/src/ui/datetime.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use bevy_egui::egui::{self, Widget}; use egui_extras::DatePickerButton; use hifitime::prelude::*; -use lofitime::{HifiDateTime, LofiDateTime}; +use lofitime::{HifiEpoch, LofiDateTime}; use crate::physics::time::CoordinateTime; @@ -16,11 +16,15 @@ pub fn set_time_menu(ui: &mut egui::Ui, coordinate_time: &mut ResMut