Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I add a chrono feature here? Why isn’t there a time feature ?

Copy link
Member

@finnbear finnbear Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a "time" feature, and you already did add a "chrono" feature.

[dependencies]
cool_crate = { optional = true }

creates a "cool_crate" feature.

If the cool_crate feature needs to enable other features, then you have to write

[dependencies]
cool_crate = { optional = true }

[features]
cool_crate = ["dep:cool_crate", "other_feature"]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, thanks for this PR! We'll review it later. 🚀


[package.metadata.docs.rs]
features = [ "derive", "serde", "std" ]
Expand Down
28 changes: 28 additions & 0 deletions src/ext/date.rs
Original file line number Diff line number Diff line change
@@ -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);
6 changes: 6 additions & 0 deletions src/ext/date/chrono.rs
Original file line number Diff line number Diff line change
@@ -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;
107 changes: 107 additions & 0 deletions src/ext/date/chrono/date_time_fixed_offset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use chrono::{DateTime, FixedOffset, NaiveDateTime};

use crate::{
convert::{impl_convert, ConvertFrom},
ext::date::{DateTimeEncode, DateTimeWithOffsetDecode, DateTimeWithOffsetEncode},
};

impl_convert!(
DateTime<FixedOffset>,
DateTimeWithOffsetEncode,
DateTimeWithOffsetDecode
);

impl ConvertFrom<&DateTime<FixedOffset>> for DateTimeWithOffsetEncode {
fn convert_from(x: &DateTime<FixedOffset>) -> Self {
let naive_enc = DateTimeEncode::convert_from(&x.naive_utc());
let offset_sec = x.offset().local_minus_utc();

(naive_enc, offset_sec)
}
}

impl ConvertFrom<DateTimeWithOffsetEncode> for DateTime<FixedOffset> {
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::<FixedOffset>::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 &times {
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::<FixedOffset>::from_naive_utc_and_offset(naive, offset);

let enc = crate::encode(&dt_fixed);
let decoded: DateTime<FixedOffset> = crate::decode(&enc).unwrap();

assert_eq!(
dt_fixed, decoded,
"Failed for datetime {:?} with offset {}",
dt_fixed, offset
);
}
}
}
}

fn bench_data() -> Vec<DateTime<FixedOffset>> {
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::<FixedOffset>::from_naive_utc_and_offset(naive, offset)
},
)
.collect()
}

crate::bench_encode_decode!(data: Vec<DateTime<FixedOffset>>);
}
70 changes: 70 additions & 0 deletions src/ext/date/chrono/date_time_utc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use chrono::{DateTime, NaiveDateTime, Utc};

use crate::{
convert::{impl_convert, ConvertFrom},
ext::date::{DateTimeDecode, DateTimeEncode},
};

impl_convert!(DateTime<Utc>, DateTimeEncode, DateTimeDecode);

impl ConvertFrom<&DateTime<Utc>> for DateTimeEncode {
fn convert_from(x: &DateTime<Utc>) -> Self {
DateTimeEncode::convert_from(&x.naive_utc())
}
}

impl ConvertFrom<DateTimeEncode> for DateTime<Utc> {
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::<Utc>::from_naive_utc_and_offset(naive, Utc);

let enc = crate::encode(&dt_utc);
let decoded: DateTime<Utc> = crate::decode(&enc).unwrap();

assert_eq!(dt_utc, decoded, "failed for datetime {:?}", dt_utc);
}
}

fn bench_data() -> Vec<DateTime<Utc>> {
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::<Utc>::from_naive_utc_and_offset(naive, Utc)
},
)
.collect()
}

crate::bench_encode_decode!(utc_vec: Vec<DateTime<Utc>>);
}
57 changes: 57 additions & 0 deletions src/ext/date/chrono/naive_date.rs
Original file line number Diff line number Diff line change
@@ -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<DateDecode> 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<NaiveDate> {
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<_>);
}
87 changes: 87 additions & 0 deletions src/ext/date/chrono/naive_date_time.rs
Original file line number Diff line number Diff line change
@@ -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<DateTimeEncode> 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<NaiveDateTime> {
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<_>);
}
Loading
Loading