hotfix_dictionary/datatype.rs
1use strum::IntoEnumIterator;
2use strum_macros::{EnumIter, IntoStaticStr};
3
4use crate::Dictionary;
5
6/// A FIX data type defined as part of a [`Dictionary`].
7#[derive(Debug)]
8#[allow(dead_code)]
9pub struct Datatype<'a>(pub(crate) &'a Dictionary, pub(crate) &'a DatatypeData);
10
11#[derive(Clone, Debug, PartialEq)]
12pub(crate) struct DatatypeData {
13 /// **Primary key.** Identifier of the datatype.
14 pub(crate) datatype: FixDatatype,
15 /// Human readable description of this Datatype.
16 pub(crate) description: String,
17 /// A string that contains examples values for a datatype
18 pub(crate) examples: Vec<String>,
19 // TODO: 'XML'.
20}
21
22impl<'a> Datatype<'a> {
23 /// Returns the name of `self`. This is also guaranteed to be a valid Rust
24 /// identifier.
25 pub fn name(&self) -> &str {
26 self.1.datatype.name()
27 }
28
29 /// Returns `self` as an `enum`.
30 pub fn basetype(&self) -> FixDatatype {
31 self.1.datatype
32 }
33}
34
35/// Sum type for all possible FIX data types ever defined across all FIX
36/// application versions.
37#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, EnumIter, IntoStaticStr)]
38#[repr(u8)]
39#[non_exhaustive]
40pub enum FixDatatype {
41 /// Single character value, can include any alphanumeric character or
42 /// punctuation except the delimiter. All char fields are case sensitive
43 /// (i.e. m != M). The following fields are based on char.
44 Char,
45 /// char field containing one of two values: 'Y' = True/Yes 'N' = False/No.
46 Boolean,
47 /// Sequence of digits with optional decimal point and sign character (ASCII
48 /// characters "-", "0" - "9" and "."); the absence of the decimal point
49 /// within the string will be interpreted as the float representation of an
50 /// integer value. All float fields must accommodate up to fifteen
51 /// significant digits. The number of decimal places used should be a factor
52 /// of business/market needs and mutual agreement between counterparties.
53 /// Note that float values may contain leading zeros (e.g. "00023.23" =
54 /// "23.23") and may contain or omit trailing zeros after the decimal point
55 /// (e.g. "23.0" = "23.0000" = "23" = "23."). Note that fields which are
56 /// derived from float may contain negative values unless explicitly
57 /// specified otherwise. The following data types are based on float.
58 Float,
59 /// float field typically representing a Price times a Qty.
60 Amt,
61 /// float field representing a price. Note the number of decimal places may
62 /// vary. For certain asset classes prices may be negative values. For
63 /// example, prices for options strategies can be negative under certain
64 /// market conditions. Refer to Volume 7: FIX Usage by Product for asset
65 /// classes that support negative price values.
66 Price,
67 /// float field representing a price offset, which can be mathematically
68 /// added to a "Price". Note the number of decimal places may vary and some
69 /// fields such as LastForwardPoints may be negative.
70 PriceOffset,
71 /// float field capable of storing either a whole number (no decimal places)
72 /// of "shares" (securities denominated in whole units) or a decimal value
73 /// containing decimal places for non-share quantity asset classes
74 /// (securities denominated in fractional units).
75 Qty,
76 /// float field representing a percentage (e.g. 0.05 represents 5% and 0.9525
77 /// represents 95.25%). Note the number of decimal places may vary.
78 Percentage,
79 /// Sequence of digits without commas or decimals and optional sign character
80 /// (ASCII characters "-" and "0" - "9" ). The sign character utilizes one
81 /// byte (i.e. positive int is "99999" while negative int is "-99999"). Note
82 /// that int values may contain leading zeros (e.g. "00023" = "23").
83 /// Examples: 723 in field 21 would be mapped int as |21=723|. -723 in field
84 /// 12 would be mapped int as |12=-723| The following data types are based on
85 /// int.
86 Int,
87 /// int field representing a day during a particular monthy (values 1 to 31).
88 DayOfMonth,
89 /// int field representing the length in bytes. Value must be positive.
90 Length,
91 /// int field representing the number of entries in a repeating group. Value
92 /// must be positive.
93 NumInGroup,
94 /// int field representing a message sequence number. Value must be positive.
95 SeqNum,
96 /// `int` field representing a field's tag number when using FIX "Tag=Value"
97 /// syntax. Value must be positive and may not contain leading zeros.
98 TagNum,
99 /// Alpha-numeric free format strings, can include any character or
100 /// punctuation except the delimiter. All String fields are case sensitive
101 /// (i.e. morstatt != Morstatt).
102 String,
103 /// string field containing raw data with no format or content restrictions.
104 /// Data fields are always immediately preceded by a length field. The length
105 /// field should specify the number of bytes of the value of the data field
106 /// (up to but not including the terminating SOH). Caution: the value of one
107 /// of these fields may contain the delimiter (SOH) character. Note that the
108 /// value specified for this field should be followed by the delimiter (SOH)
109 /// character as all fields are terminated with an "SOH".
110 Data,
111 /// string field representing month of a year. An optional day of the month
112 /// can be appended or an optional week code. Valid formats: YYYYMM YYYYMMDD
113 /// YYYYMMWW Valid values: YYYY = 0000-9999; MM = 01-12; DD = 01-31; WW = w1,
114 /// w2, w3, w4, w5.
115 MonthYear,
116 /// string field containing one or more space delimited single character
117 /// values (e.g. |18=2 A F| ).
118 MultipleCharValue,
119 /// string field representing a currency type using ISO 4217 Currency code (3
120 /// character) values (see Appendix 6-A).
121 Currency,
122 /// string field representing a market or exchange using ISO 10383 Market
123 /// Identifier Code (MIC) values (see"Appendix 6-C).
124 Exchange,
125 /// Identifier for a national language - uses ISO 639-1 standard.
126 Language,
127 /// string field represening a Date of Local Market (as oppose to UTC) in
128 /// YYYYMMDD format. This is the "normal" date field used by the FIX
129 /// Protocol. Valid values: YYYY = 0000-9999, MM = 01-12, DD = 01-31.
130 LocalMktDate,
131 /// string field containing one or more space delimited multiple character
132 /// values (e.g. |277=AV AN A| ).
133 MultipleStringValue,
134 /// string field representing Date represented in UTC (Universal Time
135 /// Coordinated, also known as "GMT") in YYYYMMDD format. This
136 /// special-purpose field is paired with UTCTimeOnly to form a proper
137 /// UTCTimestamp for bandwidth-sensitive messages. Valid values: YYYY =
138 /// 0000-9999, MM = 01-12, DD = 01-31.
139 UtcDateOnly,
140 /// string field representing Time-only represented in UTC (Universal Time
141 /// Coordinated, also known as "GMT") in either HH:MM:SS (whole seconds) or
142 /// HH:MM:SS.sss (milliseconds) format, colons, and period required. This
143 /// special-purpose field is paired with UTCDateOnly to form a proper
144 /// UTCTimestamp for bandwidth-sensitive messages. Valid values: HH = 00-23,
145 /// MM = 00-60 (60 only if UTC leap second), SS = 00-59. (without
146 /// milliseconds) HH = 00-23, MM = 00-59, SS = 00-60 (60 only if UTC leap
147 /// second), sss=000-999 (indicating milliseconds).
148 UtcTimeOnly,
149 /// string field representing Time/date combination represented in UTC
150 /// (Universal Time Coordinated, also known as "GMT") in either
151 /// YYYYMMDD-HH:MM:SS (whole seconds) or YYYYMMDD-HH:MM:SS.sss (milliseconds)
152 /// format, colons, dash, and period required. Valid values: * YYYY =
153 /// 0000-9999, MM = 01-12, DD = 01-31, HH = 00-23, MM = 00-59, SS = 00-60 (60
154 /// only if UTC leap second) (without milliseconds). * YYYY = 0000-9999, MM =
155 /// 01-12, DD = 01-31, HH = 00-23, MM = 00-59, SS = 00-60 (60 only if UTC
156 /// leap second), sss=000-999 (indicating milliseconds). Leap Seconds: Note
157 /// that UTC includes corrections for leap seconds, which are inserted to
158 /// account for slowing of the rotation of the earth. Leap second insertion
159 /// is declared by the International Earth Rotation Service (IERS) and has,
160 /// since 1972, only occurred on the night of Dec. 31 or Jun 30. The IERS
161 /// considers March 31 and September 30 as secondary dates for leap second
162 /// insertion, but has never utilized these dates. During a leap second
163 /// insertion, a UTCTimestamp field may read "19981231-23:59:59",
164 /// "19981231-23:59:60", "19990101-00:00:00". (see
165 /// <http://tycho.usno.navy.mil/leapsec.html>)
166 UtcTimestamp,
167 /// Contains an XML document raw data with no format or content restrictions.
168 /// XMLData fields are always immediately preceded by a length field. The
169 /// length field should specify the number of bytes of the value of the data
170 /// field (up to but not including the terminating SOH).
171 XmlData,
172 /// string field representing a country using ISO 3166 Country code (2
173 /// character) values (see Appendix 6-B).
174 Country,
175}
176
177impl FixDatatype {
178 /// Compares `name` to the set of strings commonly used by QuickFIX's custom
179 /// specification format and returns its associated
180 /// [`Datatype`](super::Datatype) if a match
181 /// was found. The query is case-insensitive.
182 ///
183 /// # Examples
184 ///
185 /// ```
186 /// use hotfix_dictionary::FixDatatype;
187 ///
188 /// assert_eq!(FixDatatype::from_quickfix_name("AMT"), Some(FixDatatype::Amt));
189 /// assert_eq!(FixDatatype::from_quickfix_name("Amt"), Some(FixDatatype::Amt));
190 /// assert_eq!(FixDatatype::from_quickfix_name("MONTHYEAR"), Some(FixDatatype::MonthYear));
191 /// assert_eq!(FixDatatype::from_quickfix_name(""), None);
192 /// ```
193 pub fn from_quickfix_name(name: &str) -> Option<Self> {
194 // https://github.com/quickfix/quickfix/blob/b6760f55ac6a46306b4e081bb13b65e6220ab02d/src/C%2B%2B/DataDictionary.cpp#L646-L680
195 Some(match name.to_ascii_uppercase().as_str() {
196 "AMT" => FixDatatype::Amt,
197 "BOOLEAN" => FixDatatype::Boolean,
198 "CHAR" => FixDatatype::Char,
199 "COUNTRY" => FixDatatype::Country,
200 "CURRENCY" => FixDatatype::Currency,
201 "DATA" => FixDatatype::Data,
202 "DATE" => FixDatatype::UtcDateOnly, // FIXME?
203 "DAYOFMONTH" => FixDatatype::DayOfMonth,
204 "EXCHANGE" => FixDatatype::Exchange,
205 "FLOAT" => FixDatatype::Float,
206 "INT" => FixDatatype::Int,
207 "LANGUAGE" => FixDatatype::Language,
208 "LENGTH" => FixDatatype::Length,
209 "LOCALMKTDATE" => FixDatatype::LocalMktDate,
210 "MONTHYEAR" => FixDatatype::MonthYear,
211 "MULTIPLECHARVALUE" | "MULTIPLEVALUESTRING" => FixDatatype::MultipleCharValue,
212 "MULTIPLESTRINGVALUE" => FixDatatype::MultipleStringValue,
213 "NUMINGROUP" => FixDatatype::NumInGroup,
214 "PERCENTAGE" => FixDatatype::Percentage,
215 "PRICE" => FixDatatype::Price,
216 "PRICEOFFSET" => FixDatatype::PriceOffset,
217 "QTY" => FixDatatype::Qty,
218 "STRING" => FixDatatype::String,
219 "TZTIMEONLY" => FixDatatype::UtcTimeOnly, // FIXME
220 "TZTIMESTAMP" => FixDatatype::UtcTimestamp, // FIXME
221 "UTCDATE" => FixDatatype::UtcDateOnly,
222 "UTCDATEONLY" => FixDatatype::UtcDateOnly,
223 "UTCTIMEONLY" => FixDatatype::UtcTimeOnly,
224 "UTCTIMESTAMP" => FixDatatype::UtcTimestamp,
225 "SEQNUM" => FixDatatype::SeqNum,
226 "TIME" => FixDatatype::UtcTimestamp,
227 "XMLDATA" => FixDatatype::XmlData,
228 _ => {
229 return None;
230 }
231 })
232 }
233
234 /// Returns the name adopted by QuickFIX for `self`.
235 pub fn to_quickfix_name(&self) -> &str {
236 match self {
237 FixDatatype::Int => "INT",
238 FixDatatype::Length => "LENGTH",
239 FixDatatype::Char => "CHAR",
240 FixDatatype::Boolean => "BOOLEAN",
241 FixDatatype::Float => "FLOAT",
242 FixDatatype::Amt => "AMT",
243 FixDatatype::Price => "PRICE",
244 FixDatatype::PriceOffset => "PRICEOFFSET",
245 FixDatatype::Qty => "QTY",
246 FixDatatype::Percentage => "PERCENTAGE",
247 FixDatatype::DayOfMonth => "DAYOFMONTH",
248 FixDatatype::NumInGroup => "NUMINGROUP",
249 FixDatatype::Language => "LANGUAGE",
250 FixDatatype::SeqNum => "SEQNUM",
251 FixDatatype::TagNum => "TAGNUM",
252 FixDatatype::String => "STRING",
253 FixDatatype::Data => "DATA",
254 FixDatatype::MonthYear => "MONTHYEAR",
255 FixDatatype::Currency => "CURRENCY",
256 FixDatatype::Exchange => "EXCHANGE",
257 FixDatatype::LocalMktDate => "LOCALMKTDATE",
258 FixDatatype::MultipleStringValue => "MULTIPLESTRINGVALUE",
259 FixDatatype::UtcTimeOnly => "UTCTIMEONLY",
260 FixDatatype::UtcTimestamp => "UTCTIMESTAMP",
261 FixDatatype::UtcDateOnly => "UTCDATEONLY",
262 FixDatatype::Country => "COUNTRY",
263 FixDatatype::MultipleCharValue => "MULTIPLECHARVALUE",
264 FixDatatype::XmlData => "XMLDATA",
265 }
266 }
267
268 /// Returns the name of `self`, character by character identical to the name
269 /// that appears in the official guidelines. **Generally** primitive datatypes
270 /// will use `snake_case` and non-primitive ones will have `PascalCase`, but
271 /// that's not true for every [`Datatype`](super::Datatype).
272 ///
273 /// # Examples
274 ///
275 /// ```
276 /// use hotfix_dictionary::FixDatatype;
277 ///
278 /// assert_eq!(FixDatatype::Qty.name(), "Qty");
279 /// assert_eq!(FixDatatype::Float.name(), "float");
280 /// assert_eq!(FixDatatype::String.name(), "String");
281 /// ```
282 pub fn name(&self) -> &'static str {
283 // 1. Most primitive data types have `snake_case` names.
284 // 2. Most derivative data types have `PascalCase` names.
285 // 3. `data` and `String` ruin the party and mess it up.
286 // Why, you ask? Oh, you sweet summer child. You'll learn soon enough
287 // that nothing makes sense in FIX land.
288 match self {
289 FixDatatype::Int => "int",
290 FixDatatype::Length => "Length",
291 FixDatatype::Char => "char",
292 FixDatatype::Boolean => "Boolean",
293 FixDatatype::Float => "float",
294 FixDatatype::Amt => "Amt",
295 FixDatatype::Price => "Price",
296 FixDatatype::PriceOffset => "PriceOffset",
297 FixDatatype::Qty => "Qty",
298 FixDatatype::Percentage => "Percentage",
299 FixDatatype::DayOfMonth => "DayOfMonth",
300 FixDatatype::NumInGroup => "NumInGroup",
301 FixDatatype::Language => "Language",
302 FixDatatype::SeqNum => "SeqNum",
303 FixDatatype::TagNum => "TagNum",
304 FixDatatype::String => "String",
305 FixDatatype::Data => "data",
306 FixDatatype::MonthYear => "MonthYear",
307 FixDatatype::Currency => "Currency",
308 FixDatatype::Exchange => "Exchange",
309 FixDatatype::LocalMktDate => "LocalMktDate",
310 FixDatatype::MultipleStringValue => "MultipleStringValue",
311 FixDatatype::UtcTimeOnly => "UTCTimeOnly",
312 FixDatatype::UtcTimestamp => "UTCTimestamp",
313 FixDatatype::UtcDateOnly => "UTCDateOnly",
314 FixDatatype::Country => "Country",
315 FixDatatype::MultipleCharValue => "MultipleCharValue",
316 FixDatatype::XmlData => "XMLData",
317 }
318 }
319
320 /// Returns `true` if and only if `self` is a "base type", i.e. a primitive;
321 /// returns `false` otherwise.
322 ///
323 /// # Examples
324 ///
325 /// ```
326 /// use hotfix_dictionary::FixDatatype;
327 ///
328 /// assert_eq!(FixDatatype::Float.is_base_type(), true);
329 /// assert_eq!(FixDatatype::Price.is_base_type(), false);
330 /// ```
331 pub fn is_base_type(&self) -> bool {
332 matches!(self, Self::Char | Self::Float | Self::Int | Self::String)
333 }
334
335 /// Returns the primitive [`Datatype`](super::Datatype) from which `self` is derived. If
336 /// `self` is primitive already, returns `self` unchanged.
337 ///
338 /// # Examples
339 ///
340 /// ```
341 /// use hotfix_dictionary::FixDatatype;
342 ///
343 /// assert_eq!(FixDatatype::Float.base_type(), FixDatatype::Float);
344 /// assert_eq!(FixDatatype::Price.base_type(), FixDatatype::Float);
345 /// ```
346 pub fn base_type(&self) -> Self {
347 match self {
348 Self::Char | Self::Boolean => Self::Char,
349 Self::Float
350 | Self::Amt
351 | Self::Price
352 | Self::PriceOffset
353 | Self::Qty
354 | Self::Percentage => Self::Float,
355 Self::Int
356 | Self::DayOfMonth
357 | Self::Length
358 | Self::NumInGroup
359 | Self::SeqNum
360 | Self::TagNum => Self::Int,
361 _ => Self::String,
362 }
363 }
364
365 /// Returns an [`Iterator`] over all variants of
366 /// [`Datatype`](super::Datatype).
367 pub fn iter_all() -> impl Iterator<Item = Self> {
368 <Self as IntoEnumIterator>::iter()
369 }
370}
371
372#[cfg(test)]
373mod test {
374 use super::*;
375 use std::collections::HashSet;
376
377 #[test]
378 fn iter_all_unique() {
379 let as_vec = FixDatatype::iter_all().collect::<Vec<FixDatatype>>();
380 let as_set = FixDatatype::iter_all().collect::<HashSet<FixDatatype>>();
381 assert_eq!(as_vec.len(), as_set.len());
382 }
383
384 #[test]
385 fn more_than_20_datatypes() {
386 // According to the official documentation, FIX has "about 20 data
387 // types". Including recent revisions, we should well exceed that
388 // number.
389 assert!(FixDatatype::iter_all().count() > 20);
390 }
391
392 #[test]
393 fn names_are_unique() {
394 let as_vec = FixDatatype::iter_all()
395 .map(|dt| dt.name())
396 .collect::<Vec<&str>>();
397 let as_set = FixDatatype::iter_all()
398 .map(|dt| dt.name())
399 .collect::<HashSet<&str>>();
400 assert_eq!(as_vec.len(), as_set.len());
401 }
402
403 #[test]
404 fn base_type_is_itself() {
405 for dt in FixDatatype::iter_all() {
406 if dt.is_base_type() {
407 assert_eq!(dt.base_type(), dt);
408 } else {
409 assert_ne!(dt.base_type(), dt);
410 }
411 }
412 }
413
414 #[test]
415 fn base_type_is_actually_base_type() {
416 for dt in FixDatatype::iter_all() {
417 assert!(dt.base_type().is_base_type());
418 }
419 }
420}