time-rs / time

The most used Rust library for date and time handling.
https://time-rs.github.io
Apache License 2.0
1.06k stars 261 forks source link

[feature request] Serde support unix timestamps in millisecond precision #643

Closed sedrik closed 6 months ago

sedrik commented 6 months ago

Hi

For an API I am working with I get Unix timestamps in millisecond precision as integers so I wrote the following code to handle that case (as I could not find support for it in the time crate). While my code is far from ideal (and not ready to be brought into the crate itself) I figured it might be a good starting point for future support.

A lot of things could probably be improved (I have never been good at understanding the Serializer/Deserializer implementations of Serde), especially the error handling is really bad and should be improved before this could be added as a feature.

/// Serialize/deserialize unix timestamp strings in millisecond precision
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};

use time::OffsetDateTime;

/// Serialize an `OffsetDateTime` as its Unix timestamp (in milliseconds)
pub fn serialize<S: Serializer>(
    datetime: &OffsetDateTime,
    serializer: S,
) -> Result<S::Ok, S::Error> {
    let ts = (datetime.unix_timestamp() * 1000) + datetime.millisecond() as i64;
    ts.serialize(serializer)
}

/// Deserialize an `OffsetDateTime` from its Unix timestamp (in milliseconds)
pub fn deserialize<'a, D: Deserializer<'a>>(deserializer: D) -> Result<OffsetDateTime, D::Error> {
    let value = <i64>::deserialize(deserializer)?.to_string();
    let (ts, millis) = value.split_at(value.len() - 3);
    let dt = OffsetDateTime::from_unix_timestamp(ts.parse().unwrap())
        .map_err(|err| de::Error::invalid_value(de::Unexpected::Str(err.name()), &err))?;
    let dt = dt
        .replace_millisecond(millis.parse().unwrap())
        .map_err(|err| de::Error::invalid_value(de::Unexpected::Str(err.name()), &err))?;
    Ok(dt)
}

#[cfg(test)]
mod test {
    use time::OffsetDateTime;

    use super::*;

    #[derive(Serialize, Deserialize, Debug, PartialEq)]
    struct Test {
        #[serde(with = "crate::timestamp_millis")]
        dt: OffsetDateTime,
    }

    #[test]
    fn roundtrip() {
        let json = r#"{"dt":1704879629555}"#;

        let s: Test = serde_json::from_str(json).unwrap();

        assert_eq!(s.dt.millisecond(), 555);

        let s_str = serde_json::to_string(&s).unwrap();

        assert_eq!(json, s_str);
    }
}
/// Serialize/deserialize optional unix timestamp strings in millisecond precision
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};

use time::OffsetDateTime;

/// Serialize an `OffsetDateTime` as its Unix timestamp (in milliseconds)
pub fn serialize<S: Serializer>(
    option: &Option<OffsetDateTime>,
    serializer: S,
) -> Result<S::Ok, S::Error> {
    option
        .map(|ts| (ts.unix_timestamp() * 1000) + ts.millisecond() as i64)
        .serialize(serializer)
}

/// Deserialize an `OffsetDateTime` from its Unix timestamp (in milliseconds)
pub fn deserialize<'a, D: Deserializer<'a>>(
    deserializer: D,
) -> Result<Option<OffsetDateTime>, D::Error> {
    Ok(Option::deserialize(deserializer)?.map(|value: i64| {
        let value = value.to_string();
        let (ts, millis) = value.split_at(value.len() - 3);
        let dt = OffsetDateTime::from_unix_timestamp(ts.parse().unwrap()).unwrap();
        dt.replace_millisecond(millis.parse().unwrap()).unwrap()
    }))
}

#[cfg(test)]
mod test {
    use time::OffsetDateTime;

    use super::*;

    #[derive(Serialize, Deserialize, Debug, PartialEq)]
    struct Test {
        #[serde(with = "crate::timestamp_millis_option")]
        dt: Option<OffsetDateTime>,
    }

    #[test]
    fn roundtrip() {
        let json = r#"{"dt":1704879629555}"#;

        let s: Test = serde_json::from_str(json).unwrap();

        assert!(s.dt.is_some());
        assert_eq!(s.dt.unwrap().millisecond(), 555);

        let s_str = serde_json::to_string(&s).unwrap();

        assert_eq!(json, s_str);
    }
}

Feel free to close this issues if this is not interesting.

jhpratt commented 6 months ago

You can add support for any format description, including the unix_timestamp component.

time::serde::format_description!(timestamp_millis, OffsetDateTime, "[unix_timestamp precision:millisecond]");
sedrik commented 6 months ago

Oh I had completely missed that. Awesome stuff! :)

Here is a link to the documentation for anyone from the future that stumbles upon this. https://docs.rs/time/0.3.31/time/serde/macro.format_description.html