diff --git a/Cargo.toml b/Cargo.toml index fffe8ff..ed39054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ exclude = ["fuzz/"] arrayvec = { version = "0.7", default-features = false, optional = true } bitcode_derive = { version = "=0.6.7", path = "./bitcode_derive", optional = true } bytemuck = { version = "1.14", features = [ "min_const_generics", "must_cast" ] } +chrono = { version = ">=0.4", default-features = false, optional = true } glam = { version = ">=0.21", default-features = false, optional = true } rust_decimal = { version = "1.36", default-features = false, optional = true } serde = { version = "1.0", default-features = false, features = [ "alloc" ], optional = true } @@ -43,6 +44,7 @@ zstd = "0.13.0" derive = [ "dep:bitcode_derive" ] std = [ "serde?/std", "glam?/std", "arrayvec?/std" ] default = [ "derive", "std" ] +chrono_datetime_fixedoffset = ["dep:chrono"] [package.metadata.docs.rs] features = [ "derive", "serde", "std" ] diff --git a/src/ext/date.rs b/src/ext/date.rs new file mode 100644 index 0000000..45bf24c --- /dev/null +++ b/src/ext/date.rs @@ -0,0 +1,28 @@ +use crate::int::ranged_int; +#[cfg(feature = "chrono")] +mod chrono; +#[cfg(feature = "time")] +mod time; + +ranged_int!(Hour, u8, 0, 23); +ranged_int!(Minute, u8, 0, 59); +ranged_int!(Second, u8, 0, 59); +ranged_int!(Nanosecond, u32, 0, 999_999_999); + +type TimeEncode = (u8, u8, u8, u32); +type TimeDecode = (Hour, Minute, Second, Nanosecond); + +#[cfg(feature = "chrono")] +type DateEncode = i32; +#[cfg(feature = "chrono")] +type DateDecode = i32; + +#[cfg(feature = "chrono")] +type DateTimeEncode = (DateEncode, TimeEncode); +#[cfg(feature = "chrono")] +type DateTimeDecode = (DateEncode, TimeEncode); + +#[cfg(feature = "chrono")] +pub type DateTimeWithOffsetEncode = (DateTimeEncode, i32); +#[cfg(feature = "chrono")] +pub type DateTimeWithOffsetDecode = (DateTimeDecode, i32); diff --git a/src/ext/date/chrono.rs b/src/ext/date/chrono.rs new file mode 100644 index 0000000..7aa036e --- /dev/null +++ b/src/ext/date/chrono.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "chrono_datetime_fixedoffset")] +mod date_time_fixed_offset; +mod date_time_utc; +mod naive_date; +mod naive_date_time; +mod naive_time; diff --git a/src/ext/date/chrono/date_time_fixed_offset.rs b/src/ext/date/chrono/date_time_fixed_offset.rs new file mode 100644 index 0000000..bebe6f7 --- /dev/null +++ b/src/ext/date/chrono/date_time_fixed_offset.rs @@ -0,0 +1,107 @@ +use chrono::{DateTime, FixedOffset, NaiveDateTime}; + +use crate::{ + convert::{impl_convert, ConvertFrom}, + ext::date::{DateTimeEncode, DateTimeWithOffsetDecode, DateTimeWithOffsetEncode}, +}; + +impl_convert!( + DateTime, + DateTimeWithOffsetEncode, + DateTimeWithOffsetDecode +); + +impl ConvertFrom<&DateTime> for DateTimeWithOffsetEncode { + fn convert_from(x: &DateTime) -> Self { + let naive_enc = DateTimeEncode::convert_from(&x.naive_utc()); + let offset_sec = x.offset().local_minus_utc(); + + (naive_enc, offset_sec) + } +} + +impl ConvertFrom for DateTime { + fn convert_from(enc: DateTimeWithOffsetEncode) -> Self { + let naive = NaiveDateTime::convert_from(enc.0); + let offset = + FixedOffset::east_opt(enc.1).unwrap_or_else(|| FixedOffset::east_opt(0).unwrap()); + + DateTime::::from_naive_utc_and_offset(naive, offset) + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + use chrono::{DateTime, FixedOffset, NaiveDate}; + + #[test] + fn test_chrono_datetime_fixedoffset() { + let dates = [ + (1, 1, 1), + (1970, 1, 1), // epoch + (2025, 10, 6), + (-44, 3, 15), // BCE + (9999, 12, 31), + ]; + + let offsets = [ + -12 * 3600, // UTC-12, Baker Island Time + -11 * 3600, // UTC-11, Niue / Samoa + -5 * 3600, // UTC-5, EST (Eastern Standard Time, 美东冬令时) + -3 * 3600, // UTC-3, BRT (Brasilia Time) + 0, // UTC+0, GMT + 3600, // UTC+1, CET (Central European Time) + 3 * 3600, // UTC+3, MSK (Moscow Time) + 5 * 3600 + 1800, // UTC+5:30, IST (India Standard Time) + 8 * 3600, // UTC+8, CST (China Standard Time) + 14 * 3600, // UTC+14, Line Islands Time + ]; + + let times = [(0, 0, 0), (12, 34, 56), (23, 59, 59)]; + + for &(y, m, d) in &dates { + for &(h, mi, s) in × { + let naive = NaiveDate::from_ymd_opt(y, m, d) + .unwrap() + .and_hms_opt(h, mi, s) + .unwrap(); + + for &offset_sec in &offsets { + let offset = FixedOffset::east_opt(offset_sec).unwrap(); + let dt_fixed = + DateTime::::from_naive_utc_and_offset(naive, offset); + + let enc = crate::encode(&dt_fixed); + let decoded: DateTime = crate::decode(&enc).unwrap(); + + assert_eq!( + dt_fixed, decoded, + "Failed for datetime {:?} with offset {}", + dt_fixed, offset + ); + } + } + } + } + + fn bench_data() -> Vec> { + crate::random_data(1000) + .into_iter() + .map( + |(y, m, d, h, mi, s, n, offset_sec): (i32, u32, u32, u32, u32, u32, u32, i32)| { + let naive = + NaiveDate::from_ymd_opt((y % 9999).max(1), (m % 12).max(1), (d % 28) + 1) + .unwrap() + .and_hms_nano_opt(h % 24, mi % 60, s % 60, n % 1_000_000_000) + .unwrap(); + let offset = FixedOffset::east_opt(offset_sec % 86_400) + .unwrap_or(FixedOffset::east_opt(0).unwrap()); + DateTime::::from_naive_utc_and_offset(naive, offset) + }, + ) + .collect() + } + + crate::bench_encode_decode!(data: Vec>); +} diff --git a/src/ext/date/chrono/date_time_utc.rs b/src/ext/date/chrono/date_time_utc.rs new file mode 100644 index 0000000..665b0ee --- /dev/null +++ b/src/ext/date/chrono/date_time_utc.rs @@ -0,0 +1,70 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; + +use crate::{ + convert::{impl_convert, ConvertFrom}, + ext::date::{DateTimeDecode, DateTimeEncode}, +}; + +impl_convert!(DateTime, DateTimeEncode, DateTimeDecode); + +impl ConvertFrom<&DateTime> for DateTimeEncode { + fn convert_from(x: &DateTime) -> Self { + DateTimeEncode::convert_from(&x.naive_utc()) + } +} + +impl ConvertFrom for DateTime { + fn convert_from(enc: DateTimeEncode) -> Self { + let naive = NaiveDateTime::convert_from(enc); + + DateTime::from_naive_utc_and_offset(naive, Utc) + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + use chrono::{DateTime, NaiveDate, Utc}; + + #[test] + fn test_chrono_datetime_utc() { + let ymds = [ + (1970, 1, 1), // epoch + (2025, 10, 6), + (1, 1, 1), + (-44, 3, 15), // BCE + (9999, 12, 31), + ]; + + for &(y, m, d) in ymds.iter() { + let naive = NaiveDate::from_ymd_opt(y, m, d) + .unwrap() + .and_hms_opt(12, 34, 56) + .unwrap(); + let dt_utc = DateTime::::from_naive_utc_and_offset(naive, Utc); + + let enc = crate::encode(&dt_utc); + let decoded: DateTime = crate::decode(&enc).unwrap(); + + assert_eq!(dt_utc, decoded, "failed for datetime {:?}", dt_utc); + } + } + + fn bench_data() -> Vec> { + crate::random_data(1000) + .into_iter() + .map( + |(y, m, d, h, mi, s, n, _offset_sec): (i32, u32, u32, u32, u32, u32, u32, i32)| { + let naive = + NaiveDate::from_ymd_opt((y % 9999).max(1), (m % 12).max(1), (d % 28) + 1) + .unwrap() + .and_hms_nano_opt(h % 24, mi % 60, s % 60, n % 1_000_000_000) + .unwrap(); + DateTime::::from_naive_utc_and_offset(naive, Utc) + }, + ) + .collect() + } + + crate::bench_encode_decode!(utc_vec: Vec>); +} diff --git a/src/ext/date/chrono/naive_date.rs b/src/ext/date/chrono/naive_date.rs new file mode 100644 index 0000000..0ca61a7 --- /dev/null +++ b/src/ext/date/chrono/naive_date.rs @@ -0,0 +1,57 @@ +use chrono::{Datelike, NaiveDate}; + +use crate::{ + convert::{impl_convert, ConvertFrom}, + ext::date::{DateDecode, DateEncode}, +}; + +impl_convert!(NaiveDate, DateEncode, DateDecode); + +impl ConvertFrom<&NaiveDate> for DateEncode { + fn convert_from(days: &NaiveDate) -> Self { + days.num_days_from_ce() - 719_163 // 1970-1-1 + } +} + +impl ConvertFrom for NaiveDate { + fn convert_from(days: DateDecode) -> Self { + NaiveDate::from_num_days_from_ce_opt(days + 719_163).unwrap() // 1970-1-1 + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_chrono_naive_date() { + let dates = [ + NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), // epoch + NaiveDate::from_ymd_opt(2025, 10, 6).unwrap(), + NaiveDate::from_ymd_opt(1, 1, 1).unwrap(), + NaiveDate::from_ymd_opt(-44, 3, 15).unwrap(), // BCE + NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(), + ]; + + for x in dates { + let enc = crate::encode(&x); + let date: NaiveDate = crate::decode(&enc).unwrap(); + + assert_eq!(x, date, "failed for date {:?}", x); + } + } + + use alloc::vec::Vec; + use chrono::NaiveDate; + + fn bench_data() -> Vec { + crate::random_data(1000) + .into_iter() + .map(|(y, m, d): (i32, u32, u32)| { + let year = (y % 9999).max(1); // 1 ~ 9998 + let month = (m % 12).max(1); // 1 ~ 12 + let day = (d % 28) + 1; // 1 ~ 28 + NaiveDate::from_ymd_opt(year, month, day).unwrap() + }) + .collect() + } + crate::bench_encode_decode!(data: Vec<_>); +} diff --git a/src/ext/date/chrono/naive_date_time.rs b/src/ext/date/chrono/naive_date_time.rs new file mode 100644 index 0000000..e0ebb3a --- /dev/null +++ b/src/ext/date/chrono/naive_date_time.rs @@ -0,0 +1,87 @@ +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + +use crate::{ + convert::{impl_convert, ConvertFrom}, + ext::date::{DateEncode, DateTimeDecode, DateTimeEncode, TimeEncode}, +}; + +impl_convert!(NaiveDateTime, DateTimeEncode, DateTimeDecode); + +impl ConvertFrom<&NaiveDateTime> for DateTimeEncode { + fn convert_from(x: &NaiveDateTime) -> Self { + ( + DateEncode::convert_from(&x.date()), + TimeEncode::convert_from(&x.time()), + ) + } +} + +impl ConvertFrom for NaiveDateTime { + fn convert_from((date, time): DateTimeEncode) -> Self { + NaiveDateTime::new( + NaiveDate::convert_from(date), + NaiveTime::from_hms_nano_opt(time.0 as u32, time.1 as u32, time.2 as u32, time.3) + .unwrap(), + ) + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + + use crate::decode; + use crate::encode; + + #[test] + fn test_chrono_naive_datetime() { + let dt = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 10, 6).unwrap(), + NaiveTime::from_hms_nano_opt(12, 34, 56, 123_456_789).unwrap(), + ); + + let encoded = encode(&dt); + let decoded: NaiveDateTime = decode(&encoded).unwrap(); + + assert_eq!(dt, decoded); + + let dt2 = NaiveDateTime::new( + NaiveDate::from_ymd_opt(1, 1, 1).unwrap(), + NaiveTime::from_hms_nano_opt(0, 0, 0, 0).unwrap(), + ); + let encoded2 = encode(&dt2); + let decoded2: NaiveDateTime = decode(&encoded2).unwrap(); + assert_eq!(dt2, decoded2); + } + + fn bench_data() -> Vec { + crate::random_data(1000) + .into_iter() + .map( + |(y, m, d, h, min, s, n): (i32, u32, u32, u8, u8, u8, u32)| { + let year = (y % 9999).max(1); + let month = (m % 12).max(1); + let day = (d % 28) + 1; + let date = NaiveDate::from_ymd_opt(year, month, day).unwrap(); + + let hour = h % 24; + let minute = min % 60; + let second = s % 60; + let nano = n % 1_000_000_000; + let time = NaiveTime::from_hms_nano_opt( + hour as u32, + minute as u32, + second as u32, + nano, + ) + .unwrap(); + + NaiveDateTime::new(date, time) + }, + ) + .collect() + } + + crate::bench_encode_decode!(data_vec: Vec<_>); +} diff --git a/src/ext/date/chrono/naive_time.rs b/src/ext/date/chrono/naive_time.rs new file mode 100644 index 0000000..7df1389 --- /dev/null +++ b/src/ext/date/chrono/naive_time.rs @@ -0,0 +1,73 @@ +use crate::{ + convert::{impl_convert, ConvertFrom}, + ext::date::{TimeDecode, TimeEncode}, +}; +use chrono::{NaiveTime, Timelike}; + +impl_convert!(NaiveTime, TimeEncode, TimeDecode); + +impl ConvertFrom<&NaiveTime> for TimeEncode { + fn convert_from(value: &NaiveTime) -> Self { + ( + value.hour() as u8, + value.minute() as u8, + value.second() as u8, + value.nanosecond(), + ) + } +} + +impl ConvertFrom for NaiveTime { + fn convert_from(value: TimeDecode) -> Self { + NaiveTime::from_hms_nano_opt( + value.0.into_inner() as u32, + value.1.into_inner() as u32, + value.2.into_inner() as u32, + value.3.into_inner(), + ) + .unwrap() + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_chrono_naive_time() { + assert!(crate::decode::(&crate::encode( + &NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap() + )) + .is_ok()); + assert!( + crate::decode::(&crate::encode(&(23u8, 59u8, 59u8, 999_999_999u32))).is_ok() + ); + assert!( + crate::decode::(&crate::encode(&(24u8, 59u8, 59u8, 999_999_999u32))) + .is_err() + ); + assert!( + crate::decode::(&crate::encode(&(23u8, 60u8, 59u8, 999_999_999u32))) + .is_err() + ); + assert!( + crate::decode::(&crate::encode(&(23u8, 59u8, 60u8, 999_999_999u32))) + .is_err() + ); + assert!( + crate::decode::(&crate::encode(&(23u8, 59u8, 59u8, 1_000_000_000u32))) + .is_err() + ); + } + + use alloc::vec::Vec; + use chrono::NaiveTime; + use time::Time; + fn bench_data() -> Vec