134 lines
3.1 KiB
Rust
134 lines
3.1 KiB
Rust
use std::{
|
|
fmt,
|
|
num::TryFromIntError,
|
|
ops::{Add, Deref, Sub},
|
|
};
|
|
|
|
use localtime::LocalTime;
|
|
use sqlite as sql;
|
|
|
|
/// Milliseconds since epoch.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct Timestamp(u64);
|
|
|
|
impl Add<u64> for Timestamp {
|
|
type Output = Timestamp;
|
|
|
|
fn add(self, millis: u64) -> Self::Output {
|
|
Self(self.0.saturating_add(millis))
|
|
}
|
|
}
|
|
|
|
impl Sub<u64> for Timestamp {
|
|
type Output = Timestamp;
|
|
|
|
fn sub(self, millis: u64) -> Self::Output {
|
|
Self(self.0.saturating_sub(millis))
|
|
}
|
|
}
|
|
|
|
impl Timestamp {
|
|
/// UNIX epoch.
|
|
pub const EPOCH: Self = Self(0);
|
|
/// Minimum value.
|
|
pub const MIN: Self = Self(0);
|
|
/// Maximum value.
|
|
// Nb. This is the maximum value that can fit in a signed 64-bit integer (`i64`).
|
|
// This makes it possible to store timestamps in sqlite.
|
|
pub const MAX: Self = Self(9223372036854775807);
|
|
|
|
/// Convert to local time.
|
|
pub fn to_local_time(&self) -> LocalTime {
|
|
(*self).into()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Timestamp {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
self.0.fmt(f)
|
|
}
|
|
}
|
|
|
|
impl Deref for Timestamp {
|
|
type Target = u64;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl From<LocalTime> for Timestamp {
|
|
fn from(t: LocalTime) -> Self {
|
|
Self(t.as_millis())
|
|
}
|
|
}
|
|
|
|
impl From<Timestamp> for LocalTime {
|
|
fn from(t: Timestamp) -> Self {
|
|
LocalTime::from_millis(t.0 as u128)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<u64> for Timestamp {
|
|
type Error = u64;
|
|
|
|
fn try_from(u: u64) -> Result<Self, u64> {
|
|
if u <= *Self::MAX {
|
|
Ok(Self(u))
|
|
} else {
|
|
Err(u)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<i64> for Timestamp {
|
|
type Error = TryFromIntError;
|
|
|
|
fn try_from(i: i64) -> Result<Self, Self::Error> {
|
|
i.try_into().map(Self)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&sql::Value> for Timestamp {
|
|
type Error = sql::Error;
|
|
|
|
fn try_from(value: &sql::Value) -> Result<Self, Self::Error> {
|
|
match value {
|
|
sql::Value::Integer(i) => match (*i).try_into() {
|
|
Ok(u) => Ok(Timestamp(u)),
|
|
Err(e) => Err(sql::Error {
|
|
code: None,
|
|
message: Some(format!("sql: invalid integer `{i}` for timestamp: {e}")),
|
|
}),
|
|
},
|
|
_ => Err(sql::Error {
|
|
code: None,
|
|
message: Some("sql: invalid type for timestamp".to_owned()),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl sql::BindableWithIndex for &Timestamp {
|
|
fn bind<I: sql::ParameterIndex>(self, stmt: &mut sql::Statement<'_>, i: I) -> sql::Result<()> {
|
|
match i64::try_from(*self.deref()) {
|
|
Ok(integer) => integer.bind(stmt, i),
|
|
Err(e) => Err(sql::Error {
|
|
code: None,
|
|
message: Some(format!("sql: invalid timestamp `{self}`: {e}")),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_timestamp_max() {
|
|
assert_eq!(i64::try_from(*Timestamp::MAX), Ok(i64::MAX));
|
|
}
|
|
}
|