Skip to main content

libfmp/
types.rs

1//! Fundamental identifiers and wire-level value types.
2
3use std::{error::Error, fmt, str::FromStr};
4
5use chrono::{NaiveDate, NaiveDateTime};
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
7
8/// Why a string-backed identifier or code was rejected.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum StringValueError {
12    /// The value was empty or contained only whitespace.
13    Empty,
14    /// The value contained a control character.
15    ControlCharacter,
16    /// A ticker contained a comma, which would make list encoding ambiguous.
17    Comma,
18}
19
20impl fmt::Display for StringValueError {
21    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::Empty => formatter.write_str("value must not be empty or whitespace-only"),
24            Self::ControlCharacter => {
25                formatter.write_str("value must not contain control characters")
26            }
27            Self::Comma => formatter.write_str("ticker must not contain a comma"),
28        }
29    }
30}
31
32impl Error for StringValueError {}
33
34fn validate_string_value(value: &str, reject_comma: bool) -> Result<(), StringValueError> {
35    if value.trim().is_empty() {
36        return Err(StringValueError::Empty);
37    }
38    if value.chars().any(char::is_control) {
39        return Err(StringValueError::ControlCharacter);
40    }
41    if reject_comma && value.contains(',') {
42        return Err(StringValueError::Comma);
43    }
44    Ok(())
45}
46
47macro_rules! string_value {
48    ($name:ident, $description:literal, $reject_comma:literal) => {
49        #[doc = $description]
50        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
51        #[serde(transparent)]
52        pub struct $name(String);
53
54        impl $name {
55            /// Validates and constructs a value while preserving its representation.
56            pub fn new(value: impl Into<String>) -> Result<Self, StringValueError> {
57                let value = value.into();
58                validate_string_value(&value, $reject_comma)?;
59                Ok(Self(value))
60            }
61
62            /// Borrows the original, unnormalized representation.
63            pub fn as_str(&self) -> &str {
64                &self.0
65            }
66
67            /// Returns the original, unnormalized representation.
68            pub fn into_inner(self) -> String {
69                self.0
70            }
71        }
72
73        impl fmt::Display for $name {
74            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75                formatter.write_str(self.as_str())
76            }
77        }
78
79        impl FromStr for $name {
80            type Err = StringValueError;
81
82            fn from_str(value: &str) -> Result<Self, Self::Err> {
83                Self::new(value)
84            }
85        }
86
87        impl TryFrom<String> for $name {
88            type Error = StringValueError;
89
90            fn try_from(value: String) -> Result<Self, Self::Error> {
91                Self::new(value)
92            }
93        }
94
95        impl TryFrom<&str> for $name {
96            type Error = StringValueError;
97
98            fn try_from(value: &str) -> Result<Self, Self::Error> {
99                Self::new(value)
100            }
101        }
102
103        impl AsRef<str> for $name {
104            fn as_ref(&self) -> &str {
105                self.as_str()
106            }
107        }
108
109        impl<'de> Deserialize<'de> for $name {
110            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111            where
112                D: Deserializer<'de>,
113            {
114                let value = String::deserialize(deserializer)?;
115                Self::new(value).map_err(de::Error::custom)
116            }
117        }
118    };
119}
120
121string_value!(Ticker, "A provider ticker symbol.", true);
122string_value!(
123    TipRanksExpertUid,
124    "A representation-preserving stable TipRanks analyst identifier.",
125    false
126);
127string_value!(
128    BulkPart,
129    "An open, representation-preserving provider bulk partition.",
130    false
131);
132string_value!(
133    SearchTerm,
134    "A representation-preserving provider search term.",
135    false
136);
137string_value!(Cik, "A Central Index Key.", false);
138string_value!(
139    CongressionalMemberId,
140    "A representation-preserving congressional member identifier.",
141    false
142);
143string_value!(
144    FormType,
145    "An open, representation-preserving SEC filing form type.",
146    false
147);
148string_value!(
149    TransactionTypeCode,
150    "An open, representation-preserving insider transaction type code.",
151    false
152);
153string_value!(Cusip, "A CUSIP identifier.", false);
154string_value!(Isin, "An ISIN identifier.", false);
155string_value!(Lei, "A Legal Entity Identifier.", false);
156string_value!(ExchangeCode, "An open provider exchange code.", false);
157string_value!(
158    MarketHoursTimestamp,
159    "An opaque, representation-preserving market-hours query timestamp.",
160    false
161);
162string_value!(CurrencyCode, "An open provider currency code.", false);
163string_value!(CountryCode, "An open provider country code.", false);
164string_value!(Sector, "An open provider company sector.", false);
165string_value!(Industry, "An open provider company industry.", false);
166string_value!(
167    BenchmarkYear,
168    "An open, representation-preserving executive-compensation benchmark query year.",
169    false
170);
171
172/// Error returned when a query decimal is NaN or infinite.
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub struct NonFiniteDecimal;
175
176impl fmt::Display for NonFiniteDecimal {
177    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178        formatter.write_str("decimal value must be finite")
179    }
180}
181
182impl Error for NonFiniteDecimal {}
183
184/// A finite decimal suitable for inclusion in a request URL.
185///
186/// This deliberately imposes no sign or range restriction: endpoint
187/// documentation, rather than the shared scalar, owns those constraints.
188#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
189pub struct FiniteDecimal(f64);
190
191impl FiniteDecimal {
192    /// Validates and constructs a finite decimal.
193    pub fn new(value: f64) -> Result<Self, NonFiniteDecimal> {
194        if value.is_finite() {
195            Ok(Self(value))
196        } else {
197            Err(NonFiniteDecimal)
198        }
199    }
200
201    /// Returns the validated primitive value.
202    pub const fn get(self) -> f64 {
203        self.0
204    }
205}
206
207impl TryFrom<f64> for FiniteDecimal {
208    type Error = NonFiniteDecimal;
209
210    fn try_from(value: f64) -> Result<Self, Self::Error> {
211        Self::new(value)
212    }
213}
214
215impl From<FiniteDecimal> for f64 {
216    fn from(value: FiniteDecimal) -> Self {
217        value.get()
218    }
219}
220
221impl fmt::Display for FiniteDecimal {
222    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
223        self.0.fmt(formatter)
224    }
225}
226
227impl Serialize for FiniteDecimal {
228    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
229    where
230        S: Serializer,
231    {
232        serializer.serialize_f64(self.0)
233    }
234}
235
236impl<'de> Deserialize<'de> for FiniteDecimal {
237    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
238    where
239        D: Deserializer<'de>,
240    {
241        Self::new(f64::deserialize(deserializer)?).map_err(de::Error::custom)
242    }
243}
244
245/// Error returned when constructing an empty ticker list.
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub struct EmptyTickerList;
248
249impl fmt::Display for EmptyTickerList {
250    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
251        formatter.write_str("ticker list must contain at least one ticker")
252    }
253}
254
255impl Error for EmptyTickerList {}
256
257/// A non-empty sequence of tickers encoded as a comma-separated query value.
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct TickerList(Vec<Ticker>);
260
261impl TickerList {
262    pub fn new(tickers: Vec<Ticker>) -> Result<Self, EmptyTickerList> {
263        if tickers.is_empty() {
264            Err(EmptyTickerList)
265        } else {
266            Ok(Self(tickers))
267        }
268    }
269
270    pub fn as_slice(&self) -> &[Ticker] {
271        &self.0
272    }
273
274    pub fn into_inner(self) -> Vec<Ticker> {
275        self.0
276    }
277
278    pub fn iter(&self) -> impl ExactSizeIterator<Item = &Ticker> {
279        self.0.iter()
280    }
281
282    pub fn len(&self) -> usize {
283        self.0.len()
284    }
285
286    pub fn is_empty(&self) -> bool {
287        self.0.is_empty()
288    }
289}
290
291impl TryFrom<Vec<Ticker>> for TickerList {
292    type Error = EmptyTickerList;
293
294    fn try_from(tickers: Vec<Ticker>) -> Result<Self, Self::Error> {
295        Self::new(tickers)
296    }
297}
298
299impl fmt::Display for TickerList {
300    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
301        let mut tickers = self.0.iter();
302        if let Some(first) = tickers.next() {
303            first.fmt(formatter)?;
304        }
305        for ticker in tickers {
306            formatter.write_str(",")?;
307            ticker.fmt(formatter)?;
308        }
309        Ok(())
310    }
311}
312
313/// Error returned when a date or API datetime does not match its wire format.
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub struct InvalidTemporalValue {
316    expected: &'static str,
317}
318
319impl InvalidTemporalValue {
320    pub(crate) const fn new(expected: &'static str) -> Self {
321        Self { expected }
322    }
323
324    pub fn expected(&self) -> &'static str {
325        self.expected
326    }
327}
328
329impl fmt::Display for InvalidTemporalValue {
330    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
331        write!(formatter, "value must be a valid {}", self.expected)
332    }
333}
334
335impl Error for InvalidTemporalValue {}
336
337fn has_exact_ascii_shape(value: &str, len: usize, separators: &[(usize, u8)]) -> bool {
338    if value.len() != len || !value.is_ascii() {
339        return false;
340    }
341
342    value.bytes().enumerate().all(|(index, byte)| {
343        separators
344            .iter()
345            .find_map(|(position, expected)| (*position == index).then_some(*expected))
346            .map_or_else(|| byte.is_ascii_digit(), |expected| byte == expected)
347    })
348}
349
350/// A date represented on the wire as exactly `YYYY-MM-DD`.
351#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
352pub struct Date(NaiveDate);
353
354impl Date {
355    pub fn parse(value: &str) -> Result<Self, InvalidTemporalValue> {
356        const EXPECTED: &str = "YYYY-MM-DD date";
357        if !has_exact_ascii_shape(value, 10, &[(4, b'-'), (7, b'-')]) {
358            return Err(InvalidTemporalValue { expected: EXPECTED });
359        }
360        NaiveDate::parse_from_str(value, "%Y-%m-%d")
361            .map(Self)
362            .map_err(|_| InvalidTemporalValue { expected: EXPECTED })
363    }
364
365    /// Returns the inner calendar date.
366    pub const fn into_inner(self) -> NaiveDate {
367        self.0
368    }
369}
370
371impl FromStr for Date {
372    type Err = InvalidTemporalValue;
373
374    fn from_str(value: &str) -> Result<Self, Self::Err> {
375        Self::parse(value)
376    }
377}
378
379impl fmt::Display for Date {
380    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381        write!(formatter, "{}", self.0.format("%Y-%m-%d"))
382    }
383}
384
385impl Serialize for Date {
386    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
387    where
388        S: Serializer,
389    {
390        serializer.collect_str(self)
391    }
392}
393
394impl<'de> Deserialize<'de> for Date {
395    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
396    where
397        D: Deserializer<'de>,
398    {
399        let value = String::deserialize(deserializer)?;
400        Self::parse(&value).map_err(de::Error::custom)
401    }
402}
403
404/// A timezone-less API datetime represented as exactly `YYYY-MM-DD HH:MM:SS`.
405#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
406pub struct ApiDateTime(NaiveDateTime);
407
408impl ApiDateTime {
409    pub fn parse(value: &str) -> Result<Self, InvalidTemporalValue> {
410        const EXPECTED: &str = "YYYY-MM-DD HH:MM:SS datetime";
411        let separators = &[(4, b'-'), (7, b'-'), (10, b' '), (13, b':'), (16, b':')];
412        if !has_exact_ascii_shape(value, 19, separators) {
413            return Err(InvalidTemporalValue { expected: EXPECTED });
414        }
415        NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S")
416            .map(Self)
417            .map_err(|_| InvalidTemporalValue { expected: EXPECTED })
418    }
419
420    /// Returns the inner naive datetime.
421    pub const fn into_inner(self) -> NaiveDateTime {
422        self.0
423    }
424}
425
426impl FromStr for ApiDateTime {
427    type Err = InvalidTemporalValue;
428
429    fn from_str(value: &str) -> Result<Self, Self::Err> {
430        Self::parse(value)
431    }
432}
433
434impl fmt::Display for ApiDateTime {
435    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
436        write!(formatter, "{}", self.0.format("%Y-%m-%d %H:%M:%S"))
437    }
438}
439
440impl Serialize for ApiDateTime {
441    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
442    where
443        S: Serializer,
444    {
445        serializer.collect_str(self)
446    }
447}
448
449impl<'de> Deserialize<'de> for ApiDateTime {
450    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
451    where
452        D: Deserializer<'de>,
453    {
454        let value = String::deserialize(deserializer)?;
455        Self::parse(&value).map_err(de::Error::custom)
456    }
457}
458
459/// A Unix timestamp measured in seconds.
460#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
461#[serde(transparent)]
462pub struct UnixSeconds(pub i64);
463
464/// A Unix timestamp measured in milliseconds.
465#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
466#[serde(transparent)]
467pub struct UnixMilliseconds(pub i64);
468
469impl fmt::Display for UnixSeconds {
470    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
471        self.0.fmt(formatter)
472    }
473}
474
475impl fmt::Display for UnixMilliseconds {
476    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
477        self.0.fmt(formatter)
478    }
479}
480
481/// A numeric calendar year received in a response.
482///
483/// This is distinct from string-backed fiscal years and query-year units so
484/// the provider's JSON number representation remains part of the contract.
485#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
486#[serde(transparent)]
487pub struct CalendarYear(pub u32);
488
489impl CalendarYear {
490    /// Returns the primitive numeric year.
491    pub const fn get(self) -> u32 {
492        self.0
493    }
494}
495
496impl From<u32> for CalendarYear {
497    fn from(value: u32) -> Self {
498        Self(value)
499    }
500}
501
502impl From<CalendarYear> for u32 {
503    fn from(value: CalendarYear) -> Self {
504        value.get()
505    }
506}
507
508impl fmt::Display for CalendarYear {
509    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
510        self.0.fmt(formatter)
511    }
512}
513
514/// A numeric calendar quarter received in a response.
515///
516/// This is distinct from [`crate::query::Quarter`], whose textual `1` through
517/// `4` representations are query values. Response quarters are JSON integers.
518#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
519pub struct CalendarQuarter(u8);
520
521impl CalendarQuarter {
522    /// Validates and constructs a numeric response quarter.
523    pub const fn new(value: u8) -> Result<Self, InvalidCalendarQuarter> {
524        if value >= 1 && value <= 4 {
525            Ok(Self(value))
526        } else {
527            Err(InvalidCalendarQuarter)
528        }
529    }
530
531    /// Returns the primitive numeric quarter.
532    pub const fn get(self) -> u8 {
533        self.0
534    }
535}
536
537impl TryFrom<u8> for CalendarQuarter {
538    type Error = InvalidCalendarQuarter;
539
540    fn try_from(value: u8) -> Result<Self, Self::Error> {
541        Self::new(value)
542    }
543}
544
545impl From<CalendarQuarter> for u8 {
546    fn from(value: CalendarQuarter) -> Self {
547        value.get()
548    }
549}
550
551impl fmt::Display for CalendarQuarter {
552    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
553        self.0.fmt(formatter)
554    }
555}
556
557impl Serialize for CalendarQuarter {
558    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
559    where
560        S: Serializer,
561    {
562        serializer.serialize_u8(self.get())
563    }
564}
565
566impl<'de> Deserialize<'de> for CalendarQuarter {
567    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
568    where
569        D: Deserializer<'de>,
570    {
571        Self::new(u8::deserialize(deserializer)?).map_err(de::Error::custom)
572    }
573}
574
575/// Why a numeric response quarter was rejected.
576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
577pub struct InvalidCalendarQuarter;
578
579impl fmt::Display for InvalidCalendarQuarter {
580    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
581        formatter.write_str("calendar quarter must be an integer from 1 through 4")
582    }
583}
584
585impl Error for InvalidCalendarQuarter {}
586
587/// A representation-preserving provider page index.
588///
589/// Construction does not enforce endpoint-specific bounds. Documented bounds
590/// are exposed as advisory endpoint metadata with opt-in `accepts_page` checks.
591#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
592#[serde(transparent)]
593pub struct Page(pub u32);
594
595/// A representation-preserving provider result limit.
596///
597/// Construction does not enforce endpoint-specific bounds. Documented bounds
598/// are exposed as advisory endpoint metadata with opt-in `accepts_limit` checks.
599#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
600#[serde(transparent)]
601pub struct Limit(pub u32);
602
603impl fmt::Display for Page {
604    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
605        self.0.fmt(formatter)
606    }
607}
608
609impl fmt::Display for Limit {
610    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
611        self.0.fmt(formatter)
612    }
613}
614
615/// Error returned when an inclusive date range starts after it ends.
616#[derive(Debug, Clone, Copy, PartialEq, Eq)]
617pub struct InvalidDateRange;
618
619impl fmt::Display for InvalidDateRange {
620    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
621        formatter.write_str("date range start must not be later than its end")
622    }
623}
624
625impl Error for InvalidDateRange {}
626
627/// An inclusive date range whose start is not later than its end.
628#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
629pub struct DateRange {
630    from: Date,
631    to: Date,
632}
633
634impl DateRange {
635    pub fn new(from: Date, to: Date) -> Result<Self, InvalidDateRange> {
636        if from > to {
637            Err(InvalidDateRange)
638        } else {
639            Ok(Self { from, to })
640        }
641    }
642
643    pub fn from(&self) -> Date {
644        self.from
645    }
646
647    pub fn to(&self) -> Date {
648        self.to
649    }
650
651    /// Returns the elapsed calendar days between the inclusive endpoints.
652    ///
653    /// Equal endpoints have a span of zero days. This supports optional checks
654    /// against the advisory maximum date range in endpoint metadata.
655    pub fn span_days(&self) -> u64 {
656        self.to.0.signed_duration_since(self.from.0).num_days() as u64
657    }
658}
659
660/// A price represented by the provider as a JSON number.
661pub type Price = f64;
662/// A signed change represented by the provider as a JSON number.
663pub type Change = f64;
664/// A percentage represented by the provider as a JSON number.
665pub type Percentage = f64;
666/// A currency amount represented by the provider as a JSON number.
667///
668/// Covers market values and money totals such as assets under management,
669/// compensation, offering amounts and revenue. Amounts can be fractional or
670/// negative, so the type is `f64`; values above `2^53` round to the nearest
671/// `f64`. Response fields re-encode an integral value as a JSON integer
672/// through `crate::codecs::integral_f64`.
673pub type MarketValue = f64;
674/// A dimensionless ratio represented by the provider as a JSON number.
675///
676/// Covers coefficients such as a stock's beta. Response fields re-encode an
677/// integral value as a JSON integer through `crate::codecs::integral_f64`.
678pub type Ratio = f64;
679/// A per-share amount represented by the provider as a JSON number.
680///
681/// Covers the last dividend per share and the discounted-cash-flow value per
682/// share and its difference from the price. Response fields re-encode an
683/// integral value as a JSON integer through `crate::codecs::integral_f64`.
684pub type PerShareAmount = f64;
685/// A share or unit quantity represented by the provider as a JSON number.
686///
687/// Holdings, float, outstanding and transacted share amounts can be
688/// fractional, and position changes or short balances negative, so the type
689/// is `f64`; values above `2^53` round to the nearest `f64`. Response fields
690/// re-encode an integral value as a JSON integer through
691/// `crate::codecs::integral_f64`. Use [`Count`] for counts of things.
692pub type Quantity = f64;
693/// A traded volume represented by the provider as a JSON number.
694///
695/// The provider documents volume as a non-negative integer, but it sends a
696/// JSON integer or, intermittently, a fractional number (`20201922.82733` was
697/// observed on `quote-short`), so the type is `f64` and the SDK accepts any
698/// JSON number without enforcing a sign or integrality. Response fields
699/// re-encode an integral value as a JSON integer through
700/// the crate's `integral_f64` serde codec; the screener volume filters stay `u64`.
701pub type Volume = f64;
702/// A market capitalization represented by the provider as a JSON number.
703///
704/// Documented as an integer, but a fractional or exponent-form value decodes
705/// too, so the type is `f64`; values above `2^53` round to the nearest
706/// `f64`. Response fields re-encode an integral value as a JSON integer
707/// through `crate::codecs::integral_f64`; the screener market-cap filters
708/// stay `u64`.
709pub type MarketCapitalization = f64;
710/// A cryptocurrency supply (coins or tokens) represented as a JSON number.
711///
712/// Supplies can be fractional, so the type is `f64`; response fields
713/// re-encode an integral value as a JSON integer through
714/// `crate::codecs::integral_f64`.
715pub type TokenSupply = f64;
716/// One term (numerator or denominator) of a stock-split ratio.
717///
718/// Split terms can be fractional (for example a `1.5`-for-`1` split), so
719/// the type is `f64`; response fields re-encode an integral value as a JSON
720/// integer through `crate::codecs::integral_f64`.
721pub type SplitTerm = f64;
722/// A non-negative count represented by the provider as a JSON integer.
723///
724/// The provider has also sent integral floats such as `3.0`, so response
725/// fields decode through `crate::codecs::count`, which accepts a finite
726/// integral number and rejects a fractional, negative or non-finite one.
727pub type Count = u64;
728/// A signed currency amount reported in a financial statement.
729///
730/// Covers statement lines, estimates, DCF outputs and filing amounts. Values
731/// can be negative (expenses, cash outflows) or fractional, so the type is
732/// `f64`; values above `2^53` round to the nearest `f64`. Response fields
733/// re-encode an integral value as a JSON integer through
734/// `crate::codecs::integral_f64`.
735pub type StatementAmount = f64;