use crate::{Duration, Timestamp};
#[cfg(not(feature = "chrono"))]
impl crate::Timestamp {
pub fn format(&self) -> crate::String {
use crate::ToString;
self.to_string()
}
}
#[cfg(feature = "chrono")]
mod chrono {
use chrono::Utc;
use crate::{String, Timestamp, ToString, timestamp::TimestampError};
impl Timestamp {
pub fn format(&self, string: &str) -> Result<String, TimestampError> {
let chrono_timestamp: chrono::DateTime<Utc> = (*self).try_into()?;
Ok(chrono_timestamp.format(string).to_string())
}
#[inline]
pub fn as_datetime_utc(&self) -> Result<chrono::DateTime<Utc>, TimestampError> {
(*self).try_into()
}
}
}
impl Timestamp {
#[must_use]
#[inline]
pub const fn new(seconds: i64, nanos: i32) -> Self {
Self { seconds, nanos }
}
#[must_use]
pub fn from_unix_millis(millis: i64) -> Self {
let seconds = millis / 1000;
#[allow(clippy::cast_possible_truncation)]
let nanos = ((millis % 1000) * 1_000_000) as i32;
let ts = Self { seconds, nanos };
ts.normalized()
}
#[must_use]
pub fn checked_total_i64_millis(&self) -> Option<i64> {
let ts = (*self).try_normalize().ok()?;
let seconds_part = ts.seconds.checked_mul(1000)?;
let nanos_part = i64::from(ts.nanos / 1_000_000);
seconds_part.checked_add(nanos_part)
}
}
#[cfg(all(not(feature = "std"), feature = "chrono-wasm"))]
impl Timestamp {
#[must_use]
#[inline]
pub fn now() -> Self {
::chrono::Utc::now().into()
}
}
#[cfg(feature = "std")]
impl Timestamp {
#[must_use]
#[inline]
pub fn now() -> Self {
std::time::SystemTime::now().into()
}
}
#[cfg(any(feature = "std", feature = "chrono-wasm"))]
impl Timestamp {
#[must_use]
#[inline]
pub fn is_within_range_from_now(&self, range: Duration) -> bool {
let now = Self::now();
(now + range) >= *self && (now - range) <= *self
}
#[must_use]
#[inline]
pub fn is_within_future_range(&self, range: Duration) -> bool {
let now = Self::now();
let max = now + range;
*self <= max && *self >= now
}
#[must_use]
#[inline]
pub fn is_within_past_range(&self, range: Duration) -> bool {
let now = Self::now();
let min = now - range;
*self >= min && *self <= now
}
#[must_use]
#[inline]
pub fn is_future(&self) -> bool {
*self > Self::now()
}
#[must_use]
#[inline]
pub fn is_past(&self) -> bool {
*self < Self::now()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn offset_seconds(base: &Timestamp, s: i64) -> Timestamp {
Timestamp {
seconds: base.seconds + s,
nanos: base.nanos,
}
}
#[test]
fn test_is_future() {
let now = Timestamp::now();
let future_point = offset_seconds(&now, 5);
let past_point = offset_seconds(&now, -5);
assert!(future_point.is_future(), "T + 5s should be in the future");
assert!(
!past_point.is_future(),
"T - 5s should NOT be in the future"
);
}
#[test]
fn test_is_past() {
let now = Timestamp::now();
let future_point = offset_seconds(&now, 5);
let past_point = offset_seconds(&now, -5);
assert!(past_point.is_past(), "T - 5s should be in the past");
assert!(!future_point.is_past(), "T + 5s should NOT be in the past");
}
#[test]
fn test_is_within_future_range() {
let now = Timestamp::now();
let range = Duration::new(10, 0);
let inside = offset_seconds(&now, 5);
assert!(
inside.is_within_future_range(range),
"5s is within 10s range"
);
let outside_far = offset_seconds(&now, 15);
assert!(
!outside_far.is_within_future_range(range),
"15s is outside 10s range"
);
let outside_past = offset_seconds(&now, -1);
assert!(
!outside_past.is_within_future_range(range),
"Past value is not in future range"
);
}
#[test]
fn test_is_within_past_range() {
let now = Timestamp::now();
let range = Duration::new(10, 0);
let inside = offset_seconds(&now, -5);
assert!(
inside.is_within_past_range(range),
"-5s is within 10s past range"
);
let outside_old = offset_seconds(&now, -15);
assert!(
!outside_old.is_within_past_range(range),
"-15s is too old for 10s range"
);
let outside_future = offset_seconds(&now, 1);
assert!(
!outside_future.is_within_past_range(range),
"Future value is not in past range"
);
}
#[test]
fn test_from_unix_millis_positive() {
let ts = Timestamp::from_unix_millis(1_000);
assert_eq!(ts.seconds, 1);
assert_eq!(ts.nanos, 0);
let ts = Timestamp::from_unix_millis(1_500);
assert_eq!(ts.seconds, 1);
assert_eq!(ts.nanos, 500_000_000);
}
#[test]
fn test_from_unix_millis_zero() {
let ts = Timestamp::from_unix_millis(0);
assert_eq!(ts.seconds, 0);
assert_eq!(ts.nanos, 0);
}
#[test]
fn test_from_unix_millis_negative() {
let ts = Timestamp::from_unix_millis(-1);
assert_eq!(ts.seconds, -1);
assert_eq!(ts.nanos, 999_000_000);
let ts = Timestamp::from_unix_millis(-100);
assert_eq!(ts.seconds, -1);
assert_eq!(ts.nanos, 900_000_000);
let ts = Timestamp::from_unix_millis(-1_500);
assert_eq!(ts.seconds, -2);
assert_eq!(ts.nanos, 500_000_000);
}
#[test]
fn test_round_trip_millis() {
let inputs = std::vec![0, 100, -100, 1_500, -1_500, 999, -999];
for input in inputs {
let ts = Timestamp::from_unix_millis(input);
let result = ts.checked_total_i64_millis().unwrap();
assert_eq!(input, result, "Round trip failed for {input}ms");
}
}
#[test]
fn test_total_millis_basic() {
let ts = Timestamp {
seconds: 1,
nanos: 500_000_000,
};
assert_eq!(ts.checked_total_i64_millis(), Some(1_500));
}
#[test]
fn test_total_millis_negative_normalization() {
let ts = Timestamp {
seconds: -1,
nanos: 500_000_000,
};
assert_eq!(ts.checked_total_i64_millis(), Some(-500));
}
#[test]
fn test_total_millis_overflow() {
let ts = Timestamp {
seconds: i64::MAX,
nanos: 0,
};
assert_eq!(ts.checked_total_i64_millis(), None);
let ts = Timestamp {
seconds: i64::MIN,
nanos: 0,
};
assert_eq!(ts.checked_total_i64_millis(), None);
}
#[test]
fn test_total_millis_boundary() {
let max_safe_seconds = i64::MAX / 1000;
let ts = Timestamp {
seconds: max_safe_seconds,
nanos: 0,
};
assert!(ts.checked_total_i64_millis().is_some());
let ts_overflow = Timestamp {
seconds: max_safe_seconds + 1,
nanos: 0,
};
assert!(ts_overflow.checked_total_i64_millis().is_none());
}
}