use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
const NANOS_PER_SECOND: i128 = 1_000_000_000;
const NANOS_PER_MINUTE: i128 = 60 * NANOS_PER_SECOND;
const NANOS_PER_HOUR: i128 = 60 * NANOS_PER_MINUTE;
const NANOS_PER_DAY: i128 = 24 * NANOS_PER_HOUR;
const NANOS_PER_WEEK: i128 = 7 * NANOS_PER_DAY;
const NANOS_PER_YEAR: i128 = 36525 * NANOS_PER_DAY / 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Timestamp {
pub seconds: i64,
pub nanos: i32,
}
impl Timestamp {
pub fn new(seconds: i64, nanos: i64) -> Option<Self> {
if !(0i64..NANOS_PER_SECOND as i64).contains(&nanos) {
return None;
}
Some(Self {
seconds,
nanos: nanos as i32,
})
}
pub fn from_ns(ns: i128) -> Option<Self> {
let seconds = (ns / NANOS_PER_SECOND) as i64;
let nanos = (ns % NANOS_PER_SECOND) as i32;
Some(Self { seconds, nanos })
}
pub fn to_ns(self) -> i128 {
(self.seconds as i128) * NANOS_PER_SECOND + (self.nanos as i128)
}
pub fn parse(input: &str) -> Result<Self, TimestampParseError> {
let input = input.trim();
if let Ok(dt) = DateTime::parse_from_rfc3339(input) {
return Ok(dt.with_timezone(&Utc).into());
}
if let Ok(dt) = NaiveDateTime::parse_from_str(input, "%Y-%m-%dT%H:%M:%S%.f") {
return Ok(Self::from_naivedt(dt));
}
if let Ok(dt) = NaiveDateTime::parse_from_str(input, "%Y-%m-%dT%H:%M:%S") {
return Ok(Self::from_naivedt(dt));
}
if let Ok(dt) = NaiveDateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S") {
return Ok(Self::from_naivedt(dt));
}
if let Ok(d) = NaiveDate::parse_from_str(input, "%Y-%m-%d") {
return Ok(Self::from_naivedate(d));
}
parse_unit_literal(input)
}
fn from_naivedt(dt: NaiveDateTime) -> Self {
Self {
seconds: dt.and_utc().timestamp(),
nanos: dt.and_utc().timestamp_subsec_nanos() as i32,
}
}
fn from_naivedate(d: NaiveDate) -> Self {
let dt = d.and_hms_opt(0, 0, 0).unwrap();
Self {
seconds: dt.and_utc().timestamp(),
nanos: 0,
}
}
pub fn epoch() -> Self {
Self {
seconds: 0,
nanos: 0,
}
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match Utc.timestamp_opt(self.seconds, self.nanos as u32) {
chrono::LocalResult::Single(dt) => {
if self.nanos == 0 {
write!(f, "{}", dt.format("%Y-%m-%dT%H:%M:%S"))
} else {
write!(f, "{}", dt.format("%Y-%m-%dT%H:%M:%S%.9f"))
}
}
_ => write!(f, "{}s {}ns", self.seconds, self.nanos),
}
}
}
impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Timestamp {
fn cmp(&self, other: &Self) -> Ordering {
match self.seconds.cmp(&other.seconds) {
Ordering::Equal => self.nanos.cmp(&other.nanos),
other => other,
}
}
}
impl From<DateTime<Utc>> for Timestamp {
fn from(dt: DateTime<Utc>) -> Self {
Self {
seconds: dt.timestamp(),
nanos: dt.timestamp_subsec_nanos() as i32,
}
}
}
impl From<Timestamp> for DateTime<Utc> {
fn from(ts: Timestamp) -> Self {
Utc.timestamp_opt(ts.seconds, ts.nanos as u32)
.single()
.unwrap_or_else(Utc::now)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Interval {
pub start: Timestamp,
pub end: Timestamp,
}
impl Interval {
pub fn new(start: Timestamp, end: Timestamp) -> Option<Self> {
if start <= end {
Some(Self { start, end })
} else {
None
}
}
pub fn from_ns(start_ns: i128, end_ns: i128) -> Option<Self> {
let start = Timestamp::from_ns(start_ns)?;
let end = Timestamp::from_ns(end_ns)?;
Self::new(start, end)
}
pub fn overlaps(self, other: Interval) -> bool {
self.start < other.end && other.start < self.end
}
pub fn before(self, other: Interval) -> bool {
self.end <= other.start
}
pub fn during(self, container: Interval) -> bool {
container.start <= self.start && self.end <= container.end
}
pub fn meets(self, other: Interval) -> bool {
self.end == other.start
}
}
impl fmt::Display for Interval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}, {}]", self.start, self.end)
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum TimestampParseError {
#[error("invalid unit '{unit}' in '{input}'")]
UnknownUnit {
input: String,
unit: String,
},
#[error("invalid number '{input}': {reason}")]
InvalidNumber {
input: String,
reason: String,
},
#[error("missing unit in '{input}'")]
MissingUnit {
input: String,
},
}
fn parse_unit_literal(input: &str) -> Result<Timestamp, TimestampParseError> {
let input = input.trim();
if input.is_empty() {
return Err(TimestampParseError::MissingUnit {
input: input.to_string(),
});
}
let mut num_end = 0;
let mut has_dot = false;
for (i, c) in input.char_indices() {
match c {
'0'..='9' => num_end = i + 1,
'-' if i == 0 => num_end = i + 1,
'.' if !has_dot => {
has_dot = true;
num_end = i + 1;
}
_ => break,
}
}
if num_end == 0 {
return Err(TimestampParseError::InvalidNumber {
input: input.to_string(),
reason: "no numeric characters found".to_string(),
});
}
let num_str = &input[..num_end];
let unit = &input[num_end..];
if unit.is_empty() {
return Err(TimestampParseError::MissingUnit {
input: input.to_string(),
});
}
let nanos_per_unit: i128 = match unit {
"ns" => 1,
"us" | "µs" => 1_000,
"ms" => 1_000_000,
"s" => NANOS_PER_SECOND,
"min" => NANOS_PER_MINUTE,
"h" => NANOS_PER_HOUR,
"d" => NANOS_PER_DAY,
"w" => NANOS_PER_WEEK,
"y" => NANOS_PER_YEAR,
_ => {
return Err(TimestampParseError::UnknownUnit {
input: input.to_string(),
unit: unit.to_string(),
})
}
};
let num: f64 = num_str.parse().map_err(|e: std::num::ParseFloatError| {
TimestampParseError::InvalidNumber {
input: num_str.to_string(),
reason: e.to_string(),
}
})?;
let total_ns = num as i128 * nanos_per_unit;
if total_ns / nanos_per_unit != num as i128 {
return Err(TimestampParseError::InvalidNumber {
input: input.to_string(),
reason: "overflow".to_string(),
});
}
Timestamp::from_ns(total_ns).ok_or_else(|| TimestampParseError::InvalidNumber {
input: input.to_string(),
reason: "out of range".to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timestamp_epoch() {
let ts = Timestamp::epoch();
assert_eq!(ts.seconds, 0);
assert_eq!(ts.nanos, 0);
}
#[test]
fn test_timestamp_from_ns() {
let ts = Timestamp::from_ns(1_500_000_000).unwrap();
assert_eq!(ts.seconds, 1);
assert_eq!(ts.nanos, 500_000_000);
}
#[test]
fn test_timestamp_to_ns() {
let ts = Timestamp {
seconds: 1,
nanos: 500_000_000,
};
assert_eq!(ts.to_ns(), 1_500_000_000);
}
#[test]
fn test_parse_unit_seconds() {
let ts = Timestamp::parse("100s").unwrap();
assert_eq!(ts.seconds, 100);
assert_eq!(ts.nanos, 0);
}
#[test]
fn test_parse_unit_milliseconds() {
let ts = Timestamp::parse("1500ms").unwrap();
assert_eq!(ts.seconds, 1);
assert_eq!(ts.nanos, 500_000_000);
}
#[test]
fn test_parse_negative() {
let ts = Timestamp::parse("-3600s").unwrap();
assert_eq!(ts.seconds, -3600);
assert_eq!(ts.nanos, 0);
}
#[test]
fn test_parse_iso_date() {
let ts = Timestamp::parse("2020-01-01").unwrap();
let expected = NaiveDate::from_ymd_opt(2020, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
assert_eq!(ts.seconds, expected.and_utc().timestamp());
}
#[test]
fn test_parse_iso_datetime() {
let ts = Timestamp::parse("2020-01-01T12:30:00").unwrap();
let expected = NaiveDate::from_ymd_opt(2020, 1, 1)
.unwrap()
.and_hms_opt(12, 30, 0)
.unwrap();
assert_eq!(ts.seconds, expected.and_utc().timestamp());
}
#[test]
fn test_timestamp_ordering() {
let ts1 = Timestamp::parse("100s").unwrap();
let ts2 = Timestamp::parse("200s").unwrap();
let ts3 = Timestamp::parse("100s").unwrap();
assert!(ts1 < ts2);
assert!(ts2 > ts1);
assert!(ts1 <= ts3);
assert!(ts1 >= ts3);
}
#[test]
fn test_interval_new_valid() {
let start = Timestamp::parse("100s").unwrap();
let end = Timestamp::parse("200s").unwrap();
let interval = Interval::new(start, end).unwrap();
assert_eq!(interval.start, start);
assert_eq!(interval.end, end);
}
#[test]
fn test_interval_new_invalid() {
let start = Timestamp::parse("200s").unwrap();
let end = Timestamp::parse("100s").unwrap();
assert!(Interval::new(start, end).is_none());
}
#[test]
fn test_interval_overlaps() {
let i1 = Interval::new(
Timestamp::parse("0s").unwrap(),
Timestamp::parse("100s").unwrap(),
)
.unwrap();
let i2 = Interval::new(
Timestamp::parse("50s").unwrap(),
Timestamp::parse("150s").unwrap(),
)
.unwrap();
let i3 = Interval::new(
Timestamp::parse("100s").unwrap(),
Timestamp::parse("200s").unwrap(),
)
.unwrap();
assert!(i1.overlaps(i2));
assert!(!i1.overlaps(i3));
}
#[test]
fn test_interval_before() {
let i1 = Interval::new(
Timestamp::parse("0s").unwrap(),
Timestamp::parse("100s").unwrap(),
)
.unwrap();
let i2 = Interval::new(
Timestamp::parse("100s").unwrap(),
Timestamp::parse("200s").unwrap(),
)
.unwrap();
let i3 = Interval::new(
Timestamp::parse("50s").unwrap(),
Timestamp::parse("150s").unwrap(),
)
.unwrap();
assert!(i1.before(i2));
assert!(!i1.before(i3));
}
#[test]
fn test_interval_meets() {
let i1 = Interval::new(
Timestamp::parse("0s").unwrap(),
Timestamp::parse("100s").unwrap(),
)
.unwrap();
let i2 = Interval::new(
Timestamp::parse("100s").unwrap(),
Timestamp::parse("200s").unwrap(),
)
.unwrap();
let i3 = Interval::new(
Timestamp::parse("50s").unwrap(),
Timestamp::parse("150s").unwrap(),
)
.unwrap();
assert!(i1.meets(i2));
assert!(!i1.meets(i3));
}
#[test]
fn test_interval_display() {
let interval = Interval::new(
Timestamp::parse("100s").unwrap(),
Timestamp::parse("200s").unwrap(),
)
.unwrap();
let s = interval.to_string();
assert!(s.starts_with('['), "should start with '[': {}", s);
assert!(s.ends_with(']'), "should end with ']': {}", s);
assert!(s.contains("T"), "should contain timestamp separator: {}", s);
}
}