use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
fn deserialize_cik<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<u64>, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum CikValue {
Num(u64),
Str(String),
}
match Option::<CikValue>::deserialize(deserializer)? {
Some(CikValue::Num(n)) => Ok(Some(n)),
Some(CikValue::Str(s)) => s
.trim_start_matches('0')
.parse::<u64>()
.map(Some)
.map_err(serde::de::Error::custom),
None => Ok(None),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CompanyFacts {
#[serde(default, deserialize_with = "deserialize_cik")]
pub cik: Option<u64>,
#[serde(default, rename = "entityName")]
pub entity_name: Option<String>,
#[serde(default)]
pub facts: HashMap<String, FactsByTaxonomy>,
}
impl CompanyFacts {
pub fn us_gaap(&self) -> Option<&FactsByTaxonomy> {
self.facts.get("us-gaap")
}
pub fn get_us_gaap_fact(&self, concept: &str) -> Option<&FactConcept> {
self.us_gaap().and_then(|gaap| gaap.0.get(concept))
}
pub fn ifrs(&self) -> Option<&FactsByTaxonomy> {
self.facts.get("ifrs-full")
}
pub fn dei(&self) -> Option<&FactsByTaxonomy> {
self.facts.get("dei")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FactsByTaxonomy(pub HashMap<String, FactConcept>);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FactConcept {
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub units: HashMap<String, Vec<FactUnit>>,
}
#[cfg(feature = "dataframe")]
impl FactConcept {
pub fn to_dataframe_for_unit(
&self,
unit: &str,
) -> ::polars::prelude::PolarsResult<Option<::polars::prelude::DataFrame>> {
if let Some(data_points) = self.units.get(unit) {
Ok(Some(FactUnit::vec_to_dataframe(data_points)?))
} else {
Ok(None)
}
}
pub fn to_dataframe(&self) -> ::polars::prelude::PolarsResult<::polars::prelude::DataFrame> {
use ::polars::prelude::*;
let mut units: Vec<String> = Vec::new();
let mut facts: Vec<FactUnit> = Vec::new();
for (unit, data_points) in &self.units {
for point in data_points {
units.push(unit.clone());
facts.push(point.clone());
}
}
if facts.is_empty() {
return Ok(DataFrame::empty());
}
let mut df = FactUnit::vec_to_dataframe(&facts)?;
let unit_series = Series::new("unit".into(), units);
df.insert_column(0, unit_series.into())?;
Ok(df)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
#[non_exhaustive]
pub struct FactUnit {
#[serde(default)]
pub start: Option<String>,
#[serde(default)]
pub end: Option<String>,
#[serde(default)]
pub val: Option<f64>,
#[serde(default)]
pub accn: Option<String>,
#[serde(default)]
pub fy: Option<i32>,
#[serde(default)]
pub fp: Option<String>,
#[serde(default)]
pub form: Option<String>,
#[serde(default)]
pub filed: Option<String>,
#[serde(default)]
pub frame: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "dataframe")]
fn test_fact_concept_dataframe_conversion() {
let mut units = HashMap::new();
units.insert(
"USD".to_string(),
vec![FactUnit {
start: Some("2023-10-01".to_string()),
end: Some("2024-09-30".to_string()),
val: Some(391035000000.0),
accn: Some("0000320193-24-000123".to_string()),
fy: Some(2024),
fp: Some("FY".to_string()),
form: Some("10-K".to_string()),
filed: Some("2024-11-01".to_string()),
frame: Some("CY2024".to_string()),
}],
);
let concept = FactConcept {
label: Some("Revenue".to_string()),
description: Some("Total revenue".to_string()),
units,
};
let df = concept.to_dataframe_for_unit("USD").unwrap().unwrap();
assert_eq!(df.height(), 1);
let col_names = df.get_column_names_owned();
assert!(col_names.iter().any(|n| n.as_str() == "val"));
assert!(col_names.iter().any(|n| n.as_str() == "fy"));
let df = concept.to_dataframe().unwrap();
assert_eq!(df.height(), 1);
let col_names = df.get_column_names_owned();
assert!(col_names.iter().any(|n| n.as_str() == "unit"));
assert!(col_names.iter().any(|n| n.as_str() == "val"));
}
#[test]
fn test_deserialize_company_facts() {
let json = r#"{
"cik": 320193,
"entityName": "Apple Inc.",
"facts": {
"us-gaap": {
"Revenue": {
"label": "Revenue",
"description": "Amount of revenue recognized.",
"units": {
"USD": [
{
"start": "2023-10-01",
"end": "2024-09-28",
"val": 391035000000.0,
"accn": "0000320193-24-000123",
"fy": 2024,
"fp": "FY",
"form": "10-K",
"filed": "2024-11-01",
"frame": "CY2024"
},
{
"start": "2022-09-25",
"end": "2023-09-30",
"val": 383285000000.0,
"accn": "0000320193-23-000106",
"fy": 2023,
"fp": "FY",
"form": "10-K",
"filed": "2023-11-03"
}
]
}
},
"Assets": {
"label": "Assets",
"description": "Sum of the carrying amounts.",
"units": {
"USD": [
{
"end": "2024-09-28",
"val": 364980000000.0,
"accn": "0000320193-24-000123",
"fy": 2024,
"fp": "FY",
"form": "10-K",
"filed": "2024-11-01"
}
]
}
}
}
}
}"#;
let facts: CompanyFacts = serde_json::from_str(json).unwrap();
assert_eq!(facts.cik, Some(320193));
assert_eq!(facts.entity_name.as_deref(), Some("Apple Inc."));
let gaap = facts.us_gaap().unwrap();
assert!(gaap.0.contains_key("Revenue"));
assert!(gaap.0.contains_key("Assets"));
let revenue = facts.get_us_gaap_fact("Revenue").unwrap();
assert_eq!(revenue.label.as_deref(), Some("Revenue"));
let usd_values = revenue.units.get("USD").unwrap();
assert_eq!(usd_values.len(), 2);
assert_eq!(usd_values[0].val, Some(391035000000.0));
assert_eq!(usd_values[0].fy, Some(2024));
assert_eq!(usd_values[0].fp.as_deref(), Some("FY"));
}
}