Skip to content

Commit

Permalink
Implement Debug for Time
Browse files Browse the repository at this point in the history
  • Loading branch information
fabian-braun committed Oct 18, 2023
1 parent 26c12fa commit e56756d
Showing 1 changed file with 132 additions and 39 deletions.
171 changes: 132 additions & 39 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use std::cmp::max;
use std::cmp::min;
use std::cmp::Ordering;
use std::error::Error;
use std::fmt::Debug;
use std::fmt::{Debug, Formatter};
use std::fmt::Display;
use std::ops::Add;
use std::ops::AddAssign;
Expand Down Expand Up @@ -73,7 +73,7 @@ use serde::Serialize;
///
/// Low overhead time representation. Internally represented as milliseconds.
#[derive(
Eq, PartialEq, Hash, Ord, PartialOrd, Copy, Clone, Debug, Default, Serialize, Deref, From, Into,
Eq, PartialEq, Hash, Ord, PartialOrd, Copy, Clone, Default, Serialize, Deref, From, Into,
)]
pub struct Time(i64);
impl Time {
Expand Down Expand Up @@ -132,7 +132,7 @@ impl Time {
// casting to u32 is safe here because it is guaranteed that the value is in
// 0..1_000_000_000
#[allow(clippy::cast_possible_truncation)]
let nanos = if nanos.is_negative() {
let nanos = if nanos.is_negative() {
1_000_000_000 - nanos.unsigned_abs()
} else {
nanos.unsigned_abs()
Expand Down Expand Up @@ -263,8 +263,8 @@ impl Display for Time {
/// Allows deserializing from RFC 3339 strings and unsigned integers.
impl<'de> Deserialize<'de> for Time {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(TimeVisitor)
}
Expand All @@ -279,24 +279,24 @@ impl<'de> Visitor<'de> for TimeVisitor {
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
where
E: serde::de::Error,
{
Time::parse_from_rfc3339(v).map_err(|e| E::custom(e.to_string()))
}

fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
where
E: serde::de::Error,
{
i64::try_from(v)
.map_err(|e| E::custom(e.to_string()))
.map(Time::millis)
}

fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
where
D: serde::Deserializer<'de>,
{
// expecting an unsigned integer inside the newtype struct, but technically also
// allowing strings
Expand All @@ -317,6 +317,40 @@ impl From<Time> for std::time::SystemTime {
}
}

impl Debug for Time {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let positive = self.0 >= 0;
let mut total = if self.0 == i64::MIN {
9223372036854775808usize
} else {
self.0.abs() as usize
};
let millis_part = total % 1000;
total -= millis_part;
let seconds_part = (total % (1000 * 60)) / 1000;
total -= seconds_part;
let minutes_part = (total % (1000 * 60 * 60)) / (1000 * 60);
total -= minutes_part;
let hours_part = total / (1000 * 60 * 60);
if !positive {
f.write_str("-")?;
}
write!(f, "{:02}:", hours_part)?;
write!(f, "{:02}:", minutes_part)?;
write!(f, "{:02}", seconds_part)?;
if millis_part > 0 {
write!(f, ".{}", millis_part)?;
}
Ok(())
}
}

#[derive(Error, Debug, Eq, PartialEq)]
pub enum TimeWindowError {
#[error("time window start is after end")]
StartAfterEnd,
}

/// An interval or range of time: `[start,end)`.
#[derive(Clone, Debug, Eq, PartialEq, Default, Copy, Serialize, Deserialize, From, Into, Hash)]
pub struct TimeWindow {
Expand Down Expand Up @@ -738,21 +772,21 @@ impl TimeWindow {
/// Duration can be negative. Internally duration is represented as
/// milliseconds.
#[derive(
Eq,
PartialEq,
Ord,
PartialOrd,
Copy,
Clone,
Debug,
Default,
Hash,
Serialize,
Deref,
From,
Into,
Sum,
Neg,
Eq,
PartialEq,
Ord,
PartialOrd,
Copy,
Clone,
Debug,
Default,
Hash,
Serialize,
Deref,
From,
Into,
Sum,
Neg,
)]
pub struct Duration(i64);

Expand Down Expand Up @@ -838,8 +872,8 @@ impl Duration {
/// Allows deserializing from strings, unsigned integers, and signed integers.
impl<'de> Deserialize<'de> for Duration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(DurationVisitor)
}
Expand Down Expand Up @@ -868,31 +902,31 @@ impl<'de> Visitor<'de> for DurationVisitor {
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
where
E: serde::de::Error,
{
Duration::from_str(v).map_err(|e| E::custom(e.to_string()))
}

fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
where
E: serde::de::Error,
{
i64::try_from(v)
.map_err(|e| E::custom(e.to_string()))
.map(Duration::millis)
}

fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
where
E: serde::de::Error,
{
Ok(Duration::millis(v))
}

fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
where
D: serde::Deserializer<'de>,
{
// expecting a signed integer inside the newtype struct, but technically also
// allowing strings and signed integers
Expand Down Expand Up @@ -1189,11 +1223,11 @@ impl From<Duration> for std::time::Duration {
"Negative Duration {input} cannot be converted to std::time::Duration"
);
#[allow(clippy::cast_sign_loss)] // caught by the debug_assert above
let secs = (input.0 / 1000) as u64;
let secs = (input.0 / 1000) as u64;
// casting to u32 is safe here because it is guaranteed that the value is in
// 0..1_000_000_000. The sign loss is caught by the debug_assert above.
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let nanos = ((input.0 % 1000) * 1_000_000) as u32;
let nanos = ((input.0 % 1000) * 1_000_000) as u32;
std::time::Duration::new(secs, nanos)
}
}
Expand Down Expand Up @@ -1352,6 +1386,65 @@ mod time_test {
}
}

#[test]
fn test_debug() {
struct TestCase {
name: &'static str,
input: Time,
expected: String,
}
let tests = vec![
TestCase {
name: "EPOCH",
input: Time::EPOCH,
expected: "00:00:00".to_string(),
},
TestCase {
name: "i16::MAX + 1",
input: Time::seconds(i64::from(i16::MAX) + 1),
expected: "09:06:08".to_string(),
},
TestCase {
name: "i32::MAX + 1",
input: Time::seconds(i64::from(i32::MAX) + 1),
expected: "596523:14:08".to_string(),
},
TestCase {
name: "u32::MAX + 1",
input: Time::seconds(i64::from(u32::MAX) + 1),
expected: "1193046:28:16".to_string(),
},
TestCase {
name: "very large",
input: Time::seconds(i64::from(i32::MAX) * 3500),
expected: "2087831323:28:20".to_string(),
},
TestCase {
name: "MAX",
input: Time::MAX,
expected: "2562047788015:12:55.807".to_string(),
},
TestCase {
name: "i16::MIN",
input: Time::seconds(i64::from(i16::MIN)),
expected: "-09:06:08".to_string(),
},
TestCase {
name: "i64::MIN",
input: Time::millis(i64::MIN),
expected: "-2562047788015:12:55.808".to_string(),
},
];
for test in tests {
assert_eq!(
test.expected,
format!("{:?}", test.input),
"test '{}' failed",
test.name
);
}
}

#[test]
fn deserialize_time() {
// strings
Expand Down Expand Up @@ -1432,7 +1525,7 @@ mod duration_test {
+ Duration::minutes(3)
+ Duration::seconds(4)
+ Duration::millis(5))
.to_string()
.to_string()
);

assert_eq!("0ms", Duration::ZERO.to_string());
Expand Down

0 comments on commit e56756d

Please sign in to comment.