Last active
July 23, 2018 09:05
-
-
Save ssokolow/0da765bebc79254c970d2f1e9e8bd076 to your computer and use it in GitHub Desktop.
Quick semi-hack to provide a means to serde-serialize file modification times prior to UNIX_EPOCH
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::time::{Duration, SystemTime, UNIX_EPOCH}; | |
use std::panic::catch_unwind; | |
/// Variation on `std::time::Duration` that sacrifices a bit of precision on the positive | |
/// side of the in order to provide a nicely serializable form for `SystemTime` values. | |
/// | |
/// (Workaround for https://github.com/serde-rs/json/issues/464) | |
#[derive(Eq, PartialEq, Debug, Serialize, Deserialize)] | |
pub struct Timestamp { | |
secs_since_epoch: i128, | |
nanos_since_epoch: u32, | |
} | |
impl Timestamp { | |
/// Construct a `Timestamp` from the provided `SystemTime` | |
/// | |
/// The use of `i128` inside `Timestamp` should make this operation infallible on all target | |
/// platforms. | |
pub fn from_system_time(stime: &SystemTime) -> Timestamp { | |
match stime.duration_since(UNIX_EPOCH) { | |
Ok(dur) => Timestamp { | |
secs_since_epoch: dur.as_secs() as i128, | |
nanos_since_epoch: dur.subsec_nanos(), | |
}, | |
Err(err) => { | |
let dur = err.duration(); | |
Timestamp { | |
secs_since_epoch: dur.as_secs() as i128 * -1, | |
nanos_since_epoch: dur.subsec_nanos(), | |
} | |
} | |
} | |
} | |
/// Construct a `SystemTime` corresponding to the timestamp or `None` if the conversion fails. | |
/// | |
/// **Note:** Some failure states are returned via `std::panic::catch_unwind` and will turn | |
/// into panics if not using unwinding-based panics. | |
#[allow(dead_code)] // Used in round-trip tests but not yet by main() | |
pub fn to_system_time(&self) -> Option<SystemTime> { | |
catch_unwind(|| { | |
let duration = match self.secs_since_epoch.checked_abs() { | |
Some(secs) => Duration::new(secs as u64, self.nanos_since_epoch), | |
None => Duration::new(i64::max_value() as u64 + 1, self.nanos_since_epoch), | |
}; | |
if self.secs_since_epoch < 0 { | |
UNIX_EPOCH - duration | |
} else { | |
UNIX_EPOCH + duration | |
} | |
}).ok() // TODO: Return a proper Result | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
use std::time::{Duration, SystemTime, UNIX_EPOCH}; | |
use super::Timestamp; | |
/// Test that SystemTime->Timestamp conversion works properly | |
#[test] | |
fn test_from_system_time() { | |
for (s, ns, val) in &[ | |
(-1 << 40, 1, UNIX_EPOCH - Duration::new(1 << 40, 1)), // Outside i32 range | |
(-1, 1, UNIX_EPOCH - Duration::new(1, 1)), // Negative case (outside u64 range) | |
(0, 0, UNIX_EPOCH), // Zero case | |
(1, 1, UNIX_EPOCH + Duration::new(1, 1)), // Positive case | |
(1 >> 40, 1, UNIX_EPOCH + Duration::new(1 >> 40, 1)) // Outside u32 range | |
] { | |
let ts = Timestamp::from_system_time(&val); | |
assert_eq!(Timestamp { secs_since_epoch: *s, nanos_since_epoch: *ns }, ts); | |
} | |
} | |
/// Test that SystemTime->Timestamp conversion works properly | |
#[test] | |
fn test_to_system_time() { | |
for (s, ns, val) in &[ | |
(-1 << 40, 1, UNIX_EPOCH - Duration::new(1 << 40, 1)), // Outside i32 range | |
(-1, 1, UNIX_EPOCH - Duration::new(1, 1)), // Negative case (outside u64 range) | |
(0, 0, UNIX_EPOCH), // Zero case | |
(1, 1, UNIX_EPOCH + Duration::new(1, 1)), // Positive case | |
(1 >> 40, 1, UNIX_EPOCH + Duration::new(1 >> 40, 1)) // Outside u32 range | |
] { | |
let ts = Timestamp { secs_since_epoch: *s, nanos_since_epoch: *ns }; | |
assert_eq!(ts.to_system_time().unwrap(), *val); | |
} | |
} | |
/// Test that SystemTime->Timestamp->SystemTime conversion results in equal values | |
#[test] | |
fn test_sts_round_tripping() { | |
for val in &[UNIX_EPOCH, SystemTime::now()] { | |
let ts = Timestamp::from_system_time(&val); | |
assert_eq!(&ts.to_system_time().unwrap(), val); | |
} | |
} | |
// TODO: Verify proper handling of nanos when the balance of nanos and secs may change | |
// during conversion. | |
/// Test that Timestamp->SystemTime->Timestamp conversion results in equal values | |
#[test] | |
fn test_tst_round_tripping() { | |
for val in &[ | |
// Minimum value of naive i64::abs() | |
(i64::min_value() as i128 + 1, u32::max_value() - 3_300_000_000), | |
// Basic negative case | |
(-1, 2), // (Note: abs(-1) != 2 to catch certain potential bugs) | |
// Zero case | |
(0, 0), | |
// Basic positive case | |
(1, 3), // (Note: abs(1) != 3 to catch certain potential bugs) | |
(i64::max_value() as i128, 0), // Maximum value of i64 | |
//(i64::max_value() as i128 + 1, 0), // Maximum value of i64 | |
//(u64::max_value() as i128, 0) // Maximum value of u64 | |
] { | |
let ts = Timestamp { | |
secs_since_epoch: val.0, | |
nanos_since_epoch: val.1 | |
}; | |
let st = ts.to_system_time().expect(&format!("{:?}", val)); | |
assert_eq!(Timestamp::from_system_time(&st), ts); | |
} | |
} | |
/// Test that out-of-range conditions don't panic | |
#[test] | |
fn test_out_of_range() { | |
for val in &[ | |
(i64::min_value() as i128, 0), // Minimum value of smart i64::abs() | |
(i64::max_value() as i128 + 1, 0), // One past maximum value of i64 | |
(u64::max_value() as i128, 0) // Maximum value of u64 | |
] { | |
let ts = Timestamp { | |
secs_since_epoch: val.0, | |
nanos_since_epoch: val.1 | |
}; | |
// Ensure the potential to panic can't be optimized away | |
println!("{:?}", ts.to_system_time()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment