Skip to main content

finance_query/models/edgar/
company_facts.rs

1//! EDGAR Company Facts (XBRL) models.
2//!
3//! Models for structured XBRL financial data from
4//! `https://data.sec.gov/api/xbrl/companyfacts/CIK{padded}.json`.
5//!
6//! This data includes historical financial statement values (revenue, assets,
7//! liabilities, etc.) extracted from 10-K and 10-Q filings.
8
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Complete company facts response containing all XBRL financial data.
13///
14/// Facts are organized by taxonomy (e.g., `us-gaap`, `ifrs-full`, `dei`).
15/// Use the convenience methods to access common taxonomies.
16///
17/// # Example
18///
19/// ```no_run
20/// # use finance_query::CompanyFacts;
21/// # fn example(facts: CompanyFacts) {
22/// // Get US-GAAP revenue data
23/// if let Some(revenue) = facts.get_us_gaap_fact("Revenue") {
24///     for (unit, values) in &revenue.units {
25///         println!("Unit: {}, data points: {}", unit, values.len());
26///     }
27/// }
28/// # }
29/// ```
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[non_exhaustive]
32pub struct CompanyFacts {
33    /// CIK number
34    #[serde(default)]
35    pub cik: Option<u64>,
36
37    /// Company name
38    #[serde(default, rename = "entityName")]
39    pub entity_name: Option<String>,
40
41    /// Facts organized by taxonomy (e.g., "us-gaap", "ifrs-full", "dei")
42    #[serde(default)]
43    pub facts: HashMap<String, FactsByTaxonomy>,
44}
45
46impl CompanyFacts {
47    /// Get US-GAAP facts (most common for US-listed companies).
48    pub fn us_gaap(&self) -> Option<&FactsByTaxonomy> {
49        self.facts.get("us-gaap")
50    }
51
52    /// Get a specific fact concept from the US-GAAP taxonomy.
53    ///
54    /// Common concepts: `"Revenue"`, `"Assets"`, `"Liabilities"`,
55    /// `"NetIncomeLoss"`, `"EarningsPerShareBasic"`, `"StockholdersEquity"`.
56    pub fn get_us_gaap_fact(&self, concept: &str) -> Option<&FactConcept> {
57        self.us_gaap().and_then(|gaap| gaap.0.get(concept))
58    }
59
60    /// Get IFRS facts (for companies reporting under International Financial Reporting Standards).
61    pub fn ifrs(&self) -> Option<&FactsByTaxonomy> {
62        self.facts.get("ifrs-full")
63    }
64
65    /// Get DEI (Document and Entity Information) facts.
66    pub fn dei(&self) -> Option<&FactsByTaxonomy> {
67        self.facts.get("dei")
68    }
69}
70
71/// Facts within a single taxonomy (e.g., "us-gaap").
72///
73/// Maps concept names (e.g., "Revenue", "Assets") to their [`FactConcept`].
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[non_exhaustive]
76pub struct FactsByTaxonomy(pub HashMap<String, FactConcept>);
77
78/// A single XBRL concept (e.g., "Revenue") with all reported values.
79///
80/// Values are organized by unit of measure (e.g., "USD", "shares", "pure").
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[non_exhaustive]
83pub struct FactConcept {
84    /// Human-readable label
85    #[serde(default)]
86    pub label: Option<String>,
87
88    /// Description of this concept
89    #[serde(default)]
90    pub description: Option<String>,
91
92    /// Values organized by unit type (e.g., "USD" -> vec of data points)
93    #[serde(default)]
94    pub units: HashMap<String, Vec<FactUnit>>,
95}
96
97#[cfg(feature = "dataframe")]
98impl FactConcept {
99    /// Convert all data points from a specific unit to a polars DataFrame.
100    ///
101    /// # Arguments
102    ///
103    /// * `unit` - The unit of measure (e.g., "USD", "shares", "pure")
104    ///
105    /// # Example
106    ///
107    /// ```no_run
108    /// # #[cfg(feature = "dataframe")]
109    /// # use finance_query::CompanyFacts;
110    /// # #[cfg(feature = "dataframe")]
111    /// # fn example(facts: CompanyFacts) -> Result<(), Box<dyn std::error::Error>> {
112    /// if let Some(revenue) = facts.get_us_gaap_fact("Revenue") {
113    ///     // Convert USD revenue data to DataFrame
114    ///     if let Some(df) = revenue.to_dataframe_for_unit("USD")? {
115    ///         println!("Revenue in USD: {:?}", df);
116    ///     }
117    /// }
118    /// # Ok(())
119    /// # }
120    /// ```
121    pub fn to_dataframe_for_unit(
122        &self,
123        unit: &str,
124    ) -> ::polars::prelude::PolarsResult<Option<::polars::prelude::DataFrame>> {
125        if let Some(data_points) = self.units.get(unit) {
126            Ok(Some(FactUnit::vec_to_dataframe(data_points)?))
127        } else {
128            Ok(None)
129        }
130    }
131
132    /// Convert all data points from all units to a single polars DataFrame.
133    ///
134    /// Adds a "unit" column to distinguish between different units of measure.
135    ///
136    /// # Example
137    ///
138    /// ```no_run
139    /// # #[cfg(feature = "dataframe")]
140    /// # use finance_query::CompanyFacts;
141    /// # #[cfg(feature = "dataframe")]
142    /// # fn example(facts: CompanyFacts) -> Result<(), Box<dyn std::error::Error>> {
143    /// if let Some(revenue) = facts.get_us_gaap_fact("Revenue") {
144    ///     let df = revenue.to_dataframe()?;
145    ///     println!("All revenue data: {:?}", df);
146    /// }
147    /// # Ok(())
148    /// # }
149    /// ```
150    pub fn to_dataframe(&self) -> ::polars::prelude::PolarsResult<::polars::prelude::DataFrame> {
151        use ::polars::prelude::*;
152
153        // Collect all data points with their unit labels
154        let mut all_data: Vec<(String, FactUnit)> = Vec::new();
155        for (unit, data_points) in &self.units {
156            for point in data_points {
157                all_data.push((unit.clone(), point.clone()));
158            }
159        }
160
161        if all_data.is_empty() {
162            // Return empty DataFrame with correct schema
163            return Ok(DataFrame::empty());
164        }
165
166        // Extract unit column
167        let units: Vec<String> = all_data.iter().map(|(u, _)| u.clone()).collect();
168
169        // Extract fact units (without unit field)
170        let facts: Vec<FactUnit> = all_data.into_iter().map(|(_, f)| f).collect();
171
172        // Convert facts to DataFrame
173        let mut df = FactUnit::vec_to_dataframe(&facts)?;
174
175        // Add unit column at the beginning
176        let unit_series = Series::new("unit".into(), units);
177        df.insert_column(0, unit_series.into())?;
178
179        Ok(df)
180    }
181}
182
183/// A single data point for an XBRL fact.
184///
185/// Represents one reported value from a specific filing and period.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
188#[non_exhaustive]
189pub struct FactUnit {
190    /// Start date of the reporting period (for duration facts, e.g., revenue)
191    #[serde(default)]
192    pub start: Option<String>,
193
194    /// End date of the period (for duration facts) or instant date (for point-in-time facts)
195    #[serde(default)]
196    pub end: Option<String>,
197
198    /// The reported value
199    #[serde(default)]
200    pub val: Option<f64>,
201
202    /// Accession number of the filing that reported this value
203    #[serde(default)]
204    pub accn: Option<String>,
205
206    /// Fiscal year
207    #[serde(default)]
208    pub fy: Option<i32>,
209
210    /// Fiscal period (FY, Q1, Q2, Q3, Q4)
211    #[serde(default)]
212    pub fp: Option<String>,
213
214    /// Form type (10-K, 10-Q, etc.)
215    #[serde(default)]
216    pub form: Option<String>,
217
218    /// Date the filing was filed
219    #[serde(default)]
220    pub filed: Option<String>,
221
222    /// Frame identifier (e.g., "CY2023Q4I")
223    #[serde(default)]
224    pub frame: Option<String>,
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    #[cfg(feature = "dataframe")]
233    fn test_fact_concept_dataframe_conversion() {
234        let mut units = HashMap::new();
235        units.insert(
236            "USD".to_string(),
237            vec![FactUnit {
238                start: Some("2023-10-01".to_string()),
239                end: Some("2024-09-30".to_string()),
240                val: Some(391035000000.0),
241                accn: Some("0000320193-24-000123".to_string()),
242                fy: Some(2024),
243                fp: Some("FY".to_string()),
244                form: Some("10-K".to_string()),
245                filed: Some("2024-11-01".to_string()),
246                frame: Some("CY2024".to_string()),
247            }],
248        );
249
250        let concept = FactConcept {
251            label: Some("Revenue".to_string()),
252            description: Some("Total revenue".to_string()),
253            units,
254        };
255
256        // Test single unit conversion
257        let df = concept.to_dataframe_for_unit("USD").unwrap().unwrap();
258        assert_eq!(df.height(), 1);
259        let col_names = df.get_column_names_owned();
260        assert!(col_names.iter().any(|n| n.as_str() == "val"));
261        assert!(col_names.iter().any(|n| n.as_str() == "fy"));
262
263        // Test all units conversion (includes unit column)
264        let df = concept.to_dataframe().unwrap();
265        assert_eq!(df.height(), 1);
266        let col_names = df.get_column_names_owned();
267        assert!(col_names.iter().any(|n| n.as_str() == "unit"));
268        assert!(col_names.iter().any(|n| n.as_str() == "val"));
269    }
270
271    #[test]
272    fn test_deserialize_company_facts() {
273        let json = r#"{
274            "cik": 320193,
275            "entityName": "Apple Inc.",
276            "facts": {
277                "us-gaap": {
278                    "Revenue": {
279                        "label": "Revenue",
280                        "description": "Amount of revenue recognized.",
281                        "units": {
282                            "USD": [
283                                {
284                                    "start": "2023-10-01",
285                                    "end": "2024-09-28",
286                                    "val": 391035000000.0,
287                                    "accn": "0000320193-24-000123",
288                                    "fy": 2024,
289                                    "fp": "FY",
290                                    "form": "10-K",
291                                    "filed": "2024-11-01",
292                                    "frame": "CY2024"
293                                },
294                                {
295                                    "start": "2022-09-25",
296                                    "end": "2023-09-30",
297                                    "val": 383285000000.0,
298                                    "accn": "0000320193-23-000106",
299                                    "fy": 2023,
300                                    "fp": "FY",
301                                    "form": "10-K",
302                                    "filed": "2023-11-03"
303                                }
304                            ]
305                        }
306                    },
307                    "Assets": {
308                        "label": "Assets",
309                        "description": "Sum of the carrying amounts.",
310                        "units": {
311                            "USD": [
312                                {
313                                    "end": "2024-09-28",
314                                    "val": 364980000000.0,
315                                    "accn": "0000320193-24-000123",
316                                    "fy": 2024,
317                                    "fp": "FY",
318                                    "form": "10-K",
319                                    "filed": "2024-11-01"
320                                }
321                            ]
322                        }
323                    }
324                }
325            }
326        }"#;
327
328        let facts: CompanyFacts = serde_json::from_str(json).unwrap();
329        assert_eq!(facts.cik, Some(320193));
330        assert_eq!(facts.entity_name.as_deref(), Some("Apple Inc."));
331
332        // US-GAAP access
333        let gaap = facts.us_gaap().unwrap();
334        assert!(gaap.0.contains_key("Revenue"));
335        assert!(gaap.0.contains_key("Assets"));
336
337        // Convenience method
338        let revenue = facts.get_us_gaap_fact("Revenue").unwrap();
339        assert_eq!(revenue.label.as_deref(), Some("Revenue"));
340        let usd_values = revenue.units.get("USD").unwrap();
341        assert_eq!(usd_values.len(), 2);
342        assert_eq!(usd_values[0].val, Some(391035000000.0));
343        assert_eq!(usd_values[0].fy, Some(2024));
344        assert_eq!(usd_values[0].fp.as_deref(), Some("FY"));
345    }
346}