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}