use std::fmt;
use std::hash::{Hash, Hasher};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::de::{self, Deserialize, Deserializer, Visitor};
use serde::ser::{Serialize, Serializer};
use crate::dates::{iso_time, js_time_clip, js_to_iso_string};
#[derive(Clone, Copy)]
pub struct JsDate(f64);
impl JsDate {
pub const INVALID: JsDate = JsDate(f64::NAN);
pub const UNIX_EPOCH: JsDate = JsDate(0.0);
#[must_use]
pub fn from_millis(ms: f64) -> JsDate {
JsDate(js_time_clip(ms))
}
#[must_use]
pub fn parse(s: &str) -> Option<JsDate> {
iso_time(s.as_bytes()).map(JsDate)
}
#[must_use]
pub fn now() -> JsDate {
SystemTime::now().into()
}
#[must_use]
pub const fn time_value(self) -> f64 {
self.0
}
#[must_use]
pub const fn is_valid(self) -> bool {
!self.0.is_nan()
}
#[must_use]
pub fn to_iso_string(self) -> Option<String> {
self.is_valid().then(|| js_to_iso_string(self.0))
}
}
impl PartialEq for JsDate {
fn eq(&self, other: &JsDate) -> bool {
self.0 == other.0 || (self.0.is_nan() && other.0.is_nan())
}
}
impl Eq for JsDate {}
impl Hash for JsDate {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
impl fmt::Debug for JsDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.to_iso_string() {
Some(iso) => write!(f, "JsDate({iso})"),
None => f.write_str("JsDate(Invalid Date)"),
}
}
}
impl From<SystemTime> for JsDate {
fn from(t: SystemTime) -> JsDate {
let ms = match t.duration_since(UNIX_EPOCH) {
Ok(after) => after.as_millis() as f64,
Err(before) => -(before.duration().as_nanos().div_ceil(1_000_000) as f64),
};
JsDate::from_millis(ms)
}
}
impl Serialize for JsDate {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self.to_iso_string() {
Some(iso) => serializer.serialize_str(&iso),
None => serializer.serialize_f64(f64::NAN),
}
}
}
struct DateVisitor;
impl Visitor<'_> for DateVisitor {
type Value = JsDate;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"a date in ECMAScript's date time string format with a time zone, or a number of \
milliseconds",
)
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<JsDate, E> {
JsDate::parse(s).ok_or_else(|| {
E::custom(format_args!(
"the date {s:?} is not in ECMAScript's date time string format with a time zone, \
which every browser reads alike"
))
})
}
fn visit_f64<E: de::Error>(self, ms: f64) -> Result<JsDate, E> {
Ok(JsDate::from_millis(ms))
}
fn visit_i64<E: de::Error>(self, ms: i64) -> Result<JsDate, E> {
Ok(JsDate::from_millis(ms as f64))
}
fn visit_u64<E: de::Error>(self, ms: u64) -> Result<JsDate, E> {
Ok(JsDate::from_millis(ms as f64))
}
}
impl<'de> Deserialize<'de> for JsDate {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<JsDate, D::Error> {
deserializer.deserialize_any(DateVisitor)
}
}
pub trait JsTime: Sized {
fn time_value(&self) -> f64;
fn from_time_value(t: f64) -> Option<Self>;
}
impl JsTime for JsDate {
fn time_value(&self) -> f64 {
self.0
}
fn from_time_value(t: f64) -> Option<JsDate> {
Some(JsDate::from_millis(t))
}
}
#[doc(hidden)]
#[must_use]
pub fn js_time_of<T: JsTime>(date: &T) -> f64 {
js_time_clip(date.time_value())
}
#[doc(hidden)]
#[must_use]
pub fn js_date_prop<T: JsTime>(t: f64) -> T {
T::from_time_value(t).unwrap_or_else(|| {
panic!(
"ferrovue: the date {:?} is passed to a `Date` prop whose Rust type cannot hold it",
JsDate::from_millis(t)
)
})
}
#[doc(hidden)]
pub mod date_prop {
use super::{JsDate, JsTime, de};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<T: JsTime, S: Serializer>(date: &T, serializer: S) -> Result<S::Ok, S::Error> {
JsDate::from_millis(date.time_value()).serialize(serializer)
}
pub fn deserialize<'de, T: JsTime, D: Deserializer<'de>>(
deserializer: D,
) -> Result<T, D::Error> {
held(JsDate::deserialize(deserializer)?)
}
pub(super) fn held<T: JsTime, E: de::Error>(date: JsDate) -> Result<T, E> {
T::from_time_value(date.time_value()).ok_or_else(|| {
E::custom(format_args!(
"the date {date:?} is beyond what the prop's Rust type holds"
))
})
}
}
#[doc(hidden)]
pub mod opt_date_prop {
use super::{JsDate, JsTime, date_prop};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<T: JsTime, S: Serializer>(
date: &Option<T>,
serializer: S,
) -> Result<S::Ok, S::Error> {
date.as_ref()
.map(|d| JsDate::from_millis(d.time_value()))
.serialize(serializer)
}
pub fn deserialize<'de, T: JsTime, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<T>, D::Error> {
Option::<JsDate>::deserialize(deserializer)?
.map(date_prop::held)
.transpose()
}
}
#[cfg(feature = "chrono")]
mod chrono_time {
use std::fmt;
use chrono::{DateTime, Utc};
use super::{JsDate, JsTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DateOutOfRange(JsDate);
impl DateOutOfRange {
#[must_use]
pub const fn date(self) -> JsDate {
self.0
}
}
impl fmt::Display for DateOutOfRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0.to_iso_string() {
Some(iso) => write!(f, "{iso} is beyond the dates chrono holds"),
None => f.write_str("an invalid date has no chrono date"),
}
}
}
impl std::error::Error for DateOutOfRange {}
impl From<DateTime<Utc>> for JsDate {
fn from(t: DateTime<Utc>) -> JsDate {
JsDate::from_millis(t.timestamp_millis() as f64)
}
}
impl TryFrom<JsDate> for DateTime<Utc> {
type Error = DateOutOfRange;
fn try_from(date: JsDate) -> Result<DateTime<Utc>, DateOutOfRange> {
if !date.is_valid() {
return Err(DateOutOfRange(date));
}
DateTime::from_timestamp_millis(date.time_value() as i64).ok_or(DateOutOfRange(date))
}
}
impl JsTime for DateTime<Utc> {
fn time_value(&self) -> f64 {
JsDate::from(*self).time_value()
}
fn from_time_value(t: f64) -> Option<DateTime<Utc>> {
DateTime::try_from(JsDate::from_millis(t)).ok()
}
}
}
#[cfg(feature = "chrono")]
pub use chrono_time::DateOutOfRange;
#[cfg(test)]
mod tests;