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}