use std::fmt::{Display, Formatter};
use std::str::FromStr;
use bitcode::{Decode, Encode};
use chrono::{DateTime, Local, LocalResult, NaiveDate, NaiveDateTime, TimeZone, Utc};
use chrono_tz::Tz;
use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::error::SillokError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Encode, Decode)]
pub struct Timestamp(i64);
impl Timestamp {
pub fn now() -> Self {
Self(Utc::now().timestamp_millis())
}
pub fn from_datetime(value: DateTime<Utc>) -> Self {
Self(value.timestamp_millis())
}
pub fn from_millis(value: i64) -> Self {
Self(value)
}
pub fn as_millis(&self) -> i64 {
self.0
}
pub fn to_utc(self) -> Result<DateTime<Utc>, SillokError> {
match DateTime::<Utc>::from_timestamp_millis(self.0) {
Some(value) => Ok(value),
None => Err(SillokError::new(
"invalid_timestamp",
format!("timestamp milliseconds out of range: {}", self.0),
)),
}
}
pub fn to_rfc3339(self) -> String {
match self.to_utc() {
Ok(value) => value.to_rfc3339(),
Err(_) => self.0.to_string(),
}
}
}
impl Display for Timestamp {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_rfc3339())
}
}
impl Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_rfc3339())
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
match DateTime::parse_from_rfc3339(&raw) {
Ok(value) => Ok(Self::from_datetime(value.with_timezone(&Utc))),
Err(error) => Err(D::Error::custom(error.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Decode, Encode)]
pub struct DayKey {
pub date: String,
pub timezone: String,
}
#[derive(Debug, Clone)]
pub enum ZoneChoice {
Local,
Named(Tz),
}
impl ZoneChoice {
pub fn parse(value: Option<&str>) -> Result<Self, SillokError> {
match value {
Some(raw) => match Tz::from_str(raw) {
Ok(tz) => Ok(Self::Named(tz)),
Err(error) => Err(SillokError::new(
"invalid_timezone",
format!("invalid timezone `{raw}`: {error}"),
)),
},
None => Ok(Self::Local),
}
}
pub fn label(&self) -> String {
match self {
Self::Local => "local".to_string(),
Self::Named(tz) => tz.to_string(),
}
}
pub fn day_key(&self, timestamp: Timestamp) -> Result<DayKey, SillokError> {
let utc = timestamp.to_utc()?;
let date = match self {
Self::Local => utc.with_timezone(&Local).date_naive(),
Self::Named(tz) => utc.with_timezone(tz).date_naive(),
};
Ok(self.day_key_for_date(date))
}
pub fn day_key_for_date(&self, date: NaiveDate) -> DayKey {
DayKey {
date: date.format("%Y-%m-%d").to_string(),
timezone: self.label(),
}
}
pub fn parse_timestamp(&self, raw: &str) -> Result<Timestamp, SillokError> {
if let Ok(value) = DateTime::parse_from_rfc3339(raw) {
return Ok(Timestamp::from_datetime(value.with_timezone(&Utc)));
}
let naive = match NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S") {
Ok(value) => value,
Err(_) => match NaiveDateTime::parse_from_str(raw, "%Y-%m-%d %H:%M:%S") {
Ok(value) => value,
Err(error) => {
return Err(SillokError::new(
"invalid_timestamp",
format!("invalid timestamp `{raw}`: {error}"),
));
}
},
};
match self {
Self::Local => match Local.from_local_datetime(&naive) {
LocalResult::Single(value) => {
Ok(Timestamp::from_datetime(value.with_timezone(&Utc)))
}
LocalResult::Ambiguous(_, _) => Err(SillokError::new(
"ambiguous_timestamp",
format!("timestamp `{raw}` is ambiguous in {}", self.label()),
)),
LocalResult::None => Err(SillokError::new(
"invalid_timestamp",
format!("timestamp `{raw}` does not exist in {}", self.label()),
)),
},
Self::Named(tz) => match tz.from_local_datetime(&naive) {
LocalResult::Single(value) => {
Ok(Timestamp::from_datetime(value.with_timezone(&Utc)))
}
LocalResult::Ambiguous(_, _) => Err(SillokError::new(
"ambiguous_timestamp",
format!("timestamp `{raw}` is ambiguous in {}", self.label()),
)),
LocalResult::None => Err(SillokError::new(
"invalid_timestamp",
format!("timestamp `{raw}` does not exist in {}", self.label()),
)),
},
}
}
pub fn parse_date(&self, raw: &str) -> Result<DayKey, SillokError> {
match NaiveDate::parse_from_str(raw, "%Y-%m-%d") {
Ok(date) => Ok(self.day_key_for_date(date)),
Err(error) => Err(SillokError::new(
"invalid_date",
format!("invalid date `{raw}`: {error}"),
)),
}
}
}