Skip to main content

finance_query/models/quote/
formatted_value.rs

1/// Formatted value type used throughout Yahoo Finance API responses
2///
3/// Many numeric fields in Yahoo Finance responses follow this pattern:
4/// - `raw`: The actual numeric value
5/// - `fmt`: Human-readable formatted string (e.g., "276.97")
6/// - `longFmt`: Long format for large numbers (e.g., "14,776,353,000")
7///
8/// # Examples
9///
10/// ```json
11/// {
12///   "fmt": "276.97",
13///   "raw": 276.97
14/// }
15/// ```
16///
17/// ```json
18/// {
19///   "fmt": "14.78B",
20///   "longFmt": "14,776,353,000",
21///   "raw": 14776353000
22/// }
23/// ```
24use serde::Serialize;
25
26/// A generic type representing Yahoo Finance's formatted value pattern
27///
28/// Contains the raw numeric value along with optional formatted representations.
29/// Note: `raw` is optional because Yahoo sometimes returns empty objects `{}` for unavailable data.
30#[derive(Debug, Clone, PartialEq, Serialize, Default)]
31#[serde(rename_all = "camelCase")]
32pub struct FormattedValue<T> {
33    /// Human-readable formatted string
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub fmt: Option<String>,
36
37    /// Long format (for large numbers with full precision)
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub long_fmt: Option<String>,
40
41    /// Raw numeric value (None if data is unavailable)
42    #[serde(default)]
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub raw: Option<T>,
45}
46
47// Custom Deserialize that accepts both full objects AND bare values.
48// Bare values (from ValueFormat::Raw transform) become FormattedValue { raw: Some(v), .. }.
49impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for FormattedValue<T> {
50    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51    where
52        D: serde::Deserializer<'de>,
53    {
54        #[derive(serde::Deserialize)]
55        #[serde(rename_all = "camelCase")]
56        struct Full<T> {
57            fmt: Option<String>,
58            long_fmt: Option<String>,
59            raw: Option<T>,
60        }
61
62        #[derive(serde::Deserialize)]
63        #[serde(untagged)]
64        enum Helper<T> {
65            Full(Full<T>),
66            Bare(T),
67        }
68
69        match Helper::<T>::deserialize(deserializer)? {
70            Helper::Full(f) => Ok(FormattedValue {
71                fmt: f.fmt,
72                long_fmt: f.long_fmt,
73                raw: f.raw,
74            }),
75            Helper::Bare(v) => Ok(FormattedValue {
76                fmt: None,
77                long_fmt: None,
78                raw: Some(v),
79            }),
80        }
81    }
82}
83
84impl<T> FormattedValue<T> {
85    /// Create a new FormattedValue with just a raw value
86    pub fn new(raw: T) -> Self {
87        Self {
88            fmt: None,
89            long_fmt: None,
90            raw: Some(raw),
91        }
92    }
93
94    /// Create a FormattedValue with raw and formatted values
95    pub fn with_fmt(raw: T, fmt: String) -> Self {
96        Self {
97            fmt: Some(fmt),
98            long_fmt: None,
99            raw: Some(raw),
100        }
101    }
102
103    /// Create a FormattedValue with all fields
104    pub fn with_all(raw: T, fmt: String, long_fmt: String) -> Self {
105        Self {
106            fmt: Some(fmt),
107            long_fmt: Some(long_fmt),
108            raw: Some(raw),
109        }
110    }
111
112    /// Get the raw value
113    pub fn value(&self) -> Option<&T> {
114        self.raw.as_ref()
115    }
116
117    /// Get the formatted string, falling back to long format, then None
118    pub fn formatted(&self) -> Option<&str> {
119        self.fmt.as_deref().or(self.long_fmt.as_deref())
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_deserialize_simple() {
129        let json = r#"{"fmt": "276.97", "raw": 276.97}"#;
130        let value: FormattedValue<f64> = serde_json::from_str(json).unwrap();
131        assert_eq!(value.raw, Some(276.97));
132        assert_eq!(value.fmt.as_deref(), Some("276.97"));
133        assert_eq!(value.long_fmt, None);
134    }
135
136    #[test]
137    fn test_deserialize_with_long_fmt() {
138        let json = r#"{"fmt": "14.78B", "longFmt": "14,776,353,000", "raw": 14776353000}"#;
139        let value: FormattedValue<i64> = serde_json::from_str(json).unwrap();
140        assert_eq!(value.raw, Some(14776353000));
141        assert_eq!(value.fmt.as_deref(), Some("14.78B"));
142        assert_eq!(value.long_fmt.as_deref(), Some("14,776,353,000"));
143    }
144
145    #[test]
146    fn test_formatted_helper() {
147        let value = FormattedValue::with_fmt(100.5, "100.50".to_string());
148        assert_eq!(value.formatted(), Some("100.50"));
149
150        let value = FormattedValue::new(100.5);
151        assert_eq!(value.formatted(), None);
152    }
153}
154
155#[cfg(test)]
156mod test_empty_object {
157    use super::*;
158
159    #[test]
160    fn test_empty_object_deserializes() {
161        let json = "{}";
162        let result: Result<FormattedValue<f64>, _> = serde_json::from_str(json);
163        assert!(
164            result.is_ok(),
165            "Empty object should deserialize: {:?}",
166            result.err()
167        );
168
169        let fv = result.unwrap();
170        assert_eq!(fv.raw, None);
171        assert_eq!(fv.fmt, None);
172        assert_eq!(fv.long_fmt, None);
173    }
174}