rustyfix_dictionary/
fix_datatype.rs

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