Skip to main content

brokapi_bcs_protocol/utils/
date_format.rs

1//! The date-time text format of the BCS API, for use with `#[serde(with = "...")]`.
2//!
3//! The broker sends and wants UTC date-times as ISO 8601 text with milliseconds and a `Z`, for example
4//! `2026-10-19T07:05:00.000Z`. All date-times of the API are UTC (broker docs). Use
5//! `#[serde(with = "brokapi_bcs_protocol::utils::date_format")]` on a `DateTime<Utc>` field,
6//! `date_format::option` on an `Option<DateTime<Utc>>`, and [`moex`] where the broker names the
7//! "Moscow Exchange format" (it is the same text format, see there).
8//!
9//! # Examples
10//!
11//! ```
12//! use brokapi_bcs_protocol::utils::date_format;
13//! use chrono::{DateTime, TimeZone, Utc};
14//! use serde::{Deserialize, Serialize};
15//!
16//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
17//! struct Bar {
18//!     #[serde(with = "date_format")]
19//!     time: DateTime<Utc>,
20//! }
21//!
22//! let bar = Bar { time: Utc.with_ymd_and_hms(2026, 10, 19, 7, 5, 0).unwrap() };
23//! let text = serde_json::to_string(&bar)?;
24//! assert_eq!(text, r#"{"time":"2026-10-19T07:05:00.000Z"}"#);
25//! assert_eq!(serde_json::from_str::<Bar>(&text)?, bar);
26//! # Ok::<(), serde_json::Error>(())
27//! ```
28
29use std::time::{SystemTime, UNIX_EPOCH};
30
31use chrono::{DateTime, Utc};
32use serde::{self, Deserialize, Deserializer, Serializer};
33
34/// ISO 8601 with milliseconds and a literal `Z`, the format of the API.
35const FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.3fZ";
36
37/// Writes a UTC date-time as text with milliseconds and a `Z`, for example `2026-10-19T07:05:00.000Z`.
38///
39/// Use it through `#[serde(with = "...")]`. Time below a millisecond is cut off.
40///
41/// # Errors
42///
43/// Only the errors of the serializer. Formatting itself cannot fail.
44pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
45where
46    S: Serializer,
47{
48    let s = date.format(FORMAT).to_string();
49    serializer.serialize_str(&s)
50}
51
52/// Reads a date-time text into a UTC `DateTime`.
53///
54/// It accepts any RFC 3339 text (with `Z` or with an offset, which is converted to UTC) and the plain
55/// `YYYY-MM-DDTHH:MM:SS.mmmZ` text.
56///
57/// # Errors
58///
59/// A serde error if the text is not a date-time in one of those forms. An empty text is an error here
60/// (the [`option`] module treats it as no value).
61pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
62where
63    D: Deserializer<'de>,
64{
65    let s = String::deserialize(deserializer)?;
66    DateTime::parse_from_rfc3339(&s)
67        .map(|dt| dt.with_timezone(&Utc))
68        .or_else(|_| chrono::NaiveDateTime::parse_from_str(&s, FORMAT).map(|ndt| ndt.and_utc()))
69        .map_err(serde::de::Error::custom)
70}
71
72/// The same format for an optional date-time (`Option<DateTime<Utc>>`).
73///
74/// Reading is more forgiving than [`deserialize`]: `null`, an empty text and the epoch text
75/// `1970-01-01T00:00:00.000Z` all read as `None`. The broker uses the epoch as "no time" in some
76/// order fields. Writing `None` gives `null`. Use it with `#[serde(default, with = "...option")]`.
77///
78/// # Examples
79///
80/// ```
81/// use brokapi_bcs_protocol::utils::date_format;
82/// use chrono::{DateTime, Utc};
83/// use serde::Deserialize;
84///
85/// #[derive(Deserialize)]
86/// struct Event {
87///     #[serde(default, with = "date_format::option")]
88///     at: Option<DateTime<Utc>>,
89/// }
90///
91/// let set: Event = serde_json::from_str(r#"{"at":"2026-10-19T07:05:00.000Z"}"#)?;
92/// assert!(set.at.is_some());
93/// let epoch: Event = serde_json::from_str(r#"{"at":"1970-01-01T00:00:00.000Z"}"#)?;
94/// assert!(epoch.at.is_none());
95/// let missing: Event = serde_json::from_str("{}")?;
96/// assert!(missing.at.is_none());
97/// # Ok::<(), serde_json::Error>(())
98/// ```
99pub mod option {
100    use super::*;
101
102    /// Writes `Some` as in [`super::serialize`] and `None` as `null`.
103    ///
104    /// # Errors
105    ///
106    /// Only the errors of the serializer.
107    pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
108    where
109        S: Serializer,
110    {
111        match date {
112            Some(d) => super::serialize(d, serializer),
113            None => serializer.serialize_none(),
114        }
115    }
116
117    /// Reads `null`, an empty text and the epoch text as `None`, and any other date-time as `Some`.
118    ///
119    /// # Errors
120    ///
121    /// A serde error if the text is not a date-time in the forms [`super::deserialize`] accepts.
122    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
123    where
124        D: Deserializer<'de>,
125    {
126        let s: Option<String> = Option::deserialize(deserializer)?;
127        match s {
128            Some(ref s) if s.is_empty() || s == "1970-01-01T00:00:00.000Z" => Ok(None),
129            Some(s) => DateTime::parse_from_rfc3339(&s)
130                .map(|dt| Some(dt.with_timezone(&Utc)))
131                .or_else(|_| {
132                    chrono::NaiveDateTime::parse_from_str(&s, FORMAT).map(|ndt| Some(ndt.and_utc()))
133                })
134                .map_err(serde::de::Error::custom),
135            None => Ok(None),
136        }
137    }
138}
139
140/// The format the broker docs call the Moscow Exchange format: `yyyy-MM-dd'T'HH:mm:ss.SSSX`.
141///
142/// In the code it is the same text as the main module (UTC, milliseconds, `Z`). It is a separate module
143/// because the order search request names its period fields in this format. Use it with
144/// `#[serde(with = "...::moex")]`.
145///
146/// # Examples
147///
148/// ```
149/// use brokapi_bcs_protocol::utils::date_format;
150/// use chrono::{DateTime, TimeZone, Utc};
151/// use serde::Serialize;
152///
153/// #[derive(Serialize)]
154/// struct Period {
155///     #[serde(with = "date_format::moex")]
156///     from: DateTime<Utc>,
157/// }
158///
159/// let p = Period { from: Utc.with_ymd_and_hms(2026, 1, 26, 0, 0, 0).unwrap() };
160/// assert_eq!(serde_json::to_string(&p)?, r#"{"from":"2026-01-26T00:00:00.000Z"}"#);
161/// # Ok::<(), serde_json::Error>(())
162/// ```
163pub mod moex {
164    use chrono::{DateTime, Utc};
165    use serde::{Deserialize, Deserializer, Serializer};
166
167    const MOEX_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.3fZ";
168
169    /// Writes a UTC date-time as `2026-10-19T07:05:00.000Z`.
170    ///
171    /// # Errors
172    ///
173    /// Only the errors of the serializer.
174    pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
175    where
176        S: Serializer,
177    {
178        let s = date.format(MOEX_FORMAT).to_string();
179        serializer.serialize_str(&s)
180    }
181
182    /// Reads an RFC 3339 text or the plain `YYYY-MM-DDTHH:MM:SS.mmmZ` text into a UTC date-time.
183    ///
184    /// # Errors
185    ///
186    /// A serde error if the text is not a date-time in one of those forms.
187    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
188    where
189        D: Deserializer<'de>,
190    {
191        let s = String::deserialize(deserializer)?;
192        DateTime::parse_from_rfc3339(&s)
193            .map(|dt| dt.with_timezone(&Utc))
194            .or_else(|_| {
195                chrono::NaiveDateTime::parse_from_str(&s, MOEX_FORMAT).map(|ndt| ndt.and_utc())
196            })
197            .map_err(serde::de::Error::custom)
198    }
199}
200
201/// The current time as milliseconds since the Unix epoch (UTC).
202///
203/// Returns 0 if the system clock is before the epoch. The WebSocket session uses it for its pong
204/// watchdog.
205///
206/// # Examples
207///
208/// ```
209/// use brokapi_bcs_protocol::utils::date_format::now_millis;
210///
211/// assert!(now_millis() > 1_700_000_000_000); // later than November 2023
212/// ```
213pub fn now_millis() -> u64 {
214    SystemTime::now()
215        .duration_since(UNIX_EPOCH)
216        .unwrap_or_default()
217        .as_millis() as u64
218}