1use super::{
2 Context, ContextId, Decimals, Fact, FootnoteLink, InstanceDocument, ItemFact, TupleFact, Unit,
3 UnitId,
4};
5use crate::{
6 Concept, ExpandedName, NamespacePrefix, NamespaceUri, TaxonomySet, ValueError, XbrlError,
7 XbrlType,
8};
9use chrono::{NaiveDate, NaiveDateTime};
10use rust_decimal::Decimal;
11use std::{collections::HashMap, str::FromStr};
12
13#[derive(Debug, Clone)]
15pub struct TypedInstanceDocument {
16 namespaces: HashMap<NamespacePrefix, NamespaceUri>,
18 schema_refs: Vec<String>,
20 role_refs: Vec<String>,
22 arcrole_refs: Vec<String>,
24 contexts: HashMap<ContextId, Context>,
26 units: HashMap<UnitId, Unit>,
28 facts: Vec<TypedFact>,
30 footnote_links: Vec<FootnoteLink>,
32}
33
34impl TypedInstanceDocument {
35 pub fn from_instance(
38 instance: InstanceDocument,
39 taxonomy: &TaxonomySet,
40 ) -> Result<Self, XbrlError> {
41 let InstanceDocument {
42 namespaces,
43 schema_refs,
44 role_refs,
45 arcrole_refs,
46 contexts,
47 units,
48 facts,
49 footnote_links,
50 } = instance;
51
52 let facts = convert_facts(&facts, taxonomy, &contexts, &units, &namespaces)?;
53
54 Ok(Self {
55 namespaces,
56 schema_refs,
57 role_refs,
58 arcrole_refs,
59 contexts,
60 units,
61 facts,
62 footnote_links,
63 })
64 }
65
66 pub fn facts(&self) -> &[TypedFact] {
67 &self.facts
68 }
69
70 pub fn contexts(&self) -> &HashMap<ContextId, Context> {
71 &self.contexts
72 }
73
74 pub fn units(&self) -> &HashMap<UnitId, Unit> {
75 &self.units
76 }
77
78 pub fn schema_refs(&self) -> &[String] {
79 &self.schema_refs
80 }
81
82 pub fn role_refs(&self) -> &[String] {
83 &self.role_refs
84 }
85
86 pub fn arcrole_refs(&self) -> &[String] {
87 &self.arcrole_refs
88 }
89
90 pub fn namespaces(&self) -> &HashMap<NamespacePrefix, NamespaceUri> {
91 &self.namespaces
92 }
93
94 pub fn footnote_links(&self) -> &[FootnoteLink] {
95 &self.footnote_links
96 }
97}
98
99#[allow(clippy::large_enum_variant)]
101#[derive(Debug, Clone)]
102pub enum TypedFact {
103 Item(TypedItemFact),
104 Tuple(TypedTupleFact),
105}
106
107#[derive(Debug, Clone)]
108pub struct TypedItemFact {
109 pub concept: Concept,
111 pub id: Option<String>,
113 pub context: Context,
115 pub unit: Option<Unit>,
117 pub value: Option<FactValue>,
119 pub decimals: Option<Decimals>,
121 pub precision: Option<Decimals>,
123}
124
125#[derive(Debug, Clone)]
126pub struct TypedTupleFact {
127 pub concept: Concept,
128 pub id: Option<String>,
129 pub children: Vec<TypedFact>,
130}
131
132#[derive(Debug, Clone)]
133pub enum FactValue {
134 Text(String),
135 Boolean(bool),
136 Integer(i64),
137 Decimal(Decimal),
138 Date(NaiveDate),
139 DateTime(NaiveDateTime),
140 QName(ExpandedName),
141}
142
143fn convert_facts(
144 facts: &[Fact],
145 taxonomy: &TaxonomySet,
146 contexts: &HashMap<ContextId, Context>,
147 units: &HashMap<UnitId, Unit>,
148 namespaces: &HashMap<NamespacePrefix, NamespaceUri>,
149) -> Result<Vec<TypedFact>, XbrlError> {
150 facts
151 .iter()
152 .map(|fact| convert_fact(fact, taxonomy, contexts, units, namespaces))
153 .collect()
154}
155
156fn convert_fact(
157 fact: &Fact,
158 taxonomy: &TaxonomySet,
159 contexts: &HashMap<ContextId, Context>,
160 units: &HashMap<UnitId, Unit>,
161 namespaces: &HashMap<NamespacePrefix, NamespaceUri>,
162) -> Result<TypedFact, XbrlError> {
163 match fact {
164 Fact::Item(item) => Ok(TypedFact::Item(convert_item(
165 item, taxonomy, contexts, units, namespaces,
166 )?)),
167 Fact::Tuple(tuple) => Ok(TypedFact::Tuple(convert_tuple(
168 tuple, taxonomy, contexts, units, namespaces,
169 )?)),
170 }
171}
172
173fn convert_item(
174 item: &ItemFact,
175 taxonomy: &TaxonomySet,
176 contexts: &HashMap<ContextId, Context>,
177 units: &HashMap<UnitId, Unit>,
178 namespaces: &HashMap<NamespacePrefix, NamespaceUri>,
179) -> Result<TypedItemFact, XbrlError> {
180 let concept = taxonomy
181 .find_concept(item.concept_name())
182 .ok_or_else(|| ValueError::UnknownConcept {
183 concept: item.concept_name().to_string(),
184 })?
185 .clone();
186
187 let context =
188 contexts
189 .get(item.context_ref())
190 .cloned()
191 .ok_or_else(|| ValueError::MissingContext {
192 context_ref: item.context_ref().to_owned(),
193 })?;
194
195 let unit = match item.unit_ref() {
196 Some(unit_ref) => {
197 Some(
198 units
199 .get(unit_ref)
200 .cloned()
201 .ok_or_else(|| ValueError::MissingUnit {
202 unit_ref: unit_ref.to_owned(),
203 })?,
204 )
205 }
206 None => None,
207 };
208
209 let value = if item.is_nil() {
210 None
211 } else {
212 Some(parse_fact_value(item.value(), &concept, namespaces)?)
213 };
214
215 Ok(TypedItemFact {
216 concept: concept.to_owned(),
217 id: item.id().map(str::to_owned),
218 context,
219 unit,
220 value,
221 decimals: item.decimals().cloned(),
222 precision: item.precision().cloned(),
223 })
224}
225
226fn convert_tuple(
227 tuple: &TupleFact,
228 taxonomy: &TaxonomySet,
229 contexts: &HashMap<ContextId, Context>,
230 units: &HashMap<UnitId, Unit>,
231 namespaces: &HashMap<NamespacePrefix, NamespaceUri>,
232) -> Result<TypedTupleFact, XbrlError> {
233 let concept = taxonomy
234 .find_concept(tuple.concept_name())
235 .ok_or_else(|| ValueError::UnknownConcept {
236 concept: tuple.concept_name().to_string(),
237 })?
238 .clone();
239
240 let children = convert_facts(tuple.children(), taxonomy, contexts, units, namespaces)?;
241
242 Ok(TypedTupleFact {
243 concept,
244 id: tuple.id().map(str::to_owned),
245 children,
246 })
247}
248
249pub(crate) fn parse_fact_value(
250 raw: &str,
251 concept: &Concept,
252 namespaces: &HashMap<NamespacePrefix, NamespaceUri>,
253) -> Result<FactValue, XbrlError> {
254 let value = raw.trim();
255
256 match concept.data_type {
257 XbrlType::Boolean => parse_boolean(value).map(FactValue::Boolean),
258 XbrlType::Integer => parse_integer(value).map(FactValue::Integer),
259 XbrlType::Monetary
260 | XbrlType::Decimal
261 | XbrlType::Float
262 | XbrlType::Double
263 | XbrlType::Shares
264 | XbrlType::Pure
265 | XbrlType::Fraction
266 | XbrlType::Percent
267 | XbrlType::PerShare => parse_decimal_compatible(value).map(FactValue::Decimal),
268 XbrlType::Date => parse_xsd_date(value).map(FactValue::Date),
269 XbrlType::DateTime => parse_xsd_datetime(value).map(FactValue::DateTime),
270 XbrlType::QName => parse_qname_value(value, namespaces).map(FactValue::QName),
271 XbrlType::String | XbrlType::Simple(_) | XbrlType::Complex(_) => {
272 Ok(FactValue::Text(value.to_owned()))
273 }
274 }
275}
276
277fn parse_boolean(value: &str) -> Result<bool, XbrlError> {
278 if value.eq_ignore_ascii_case("true") || value == "1" {
279 return Ok(true);
280 }
281 if value.eq_ignore_ascii_case("false") || value == "0" {
282 return Ok(false);
283 }
284
285 Err(ValueError::InvalidBoolean {
286 raw: value.to_owned(),
287 }
288 .into())
289}
290
291fn parse_integer(value: &str) -> Result<i64, XbrlError> {
292 i64::from_str(value).map_err(|_| {
293 ValueError::InvalidInteger {
294 raw: value.to_owned(),
295 }
296 .into()
297 })
298}
299
300fn parse_decimal_compatible(value: &str) -> Result<Decimal, XbrlError> {
301 if value.is_empty() {
302 return Err(ValueError::InvalidDecimal {
303 raw: value.to_owned(),
304 }
305 .into());
306 }
307
308 if let Ok(decimal) = Decimal::from_str(value) {
309 return Ok(decimal);
310 }
311
312 Decimal::from_scientific(value).map_err(|_| {
313 ValueError::InvalidDecimal {
314 raw: value.to_owned(),
315 }
316 .into()
317 })
318}
319
320fn parse_xsd_date(value: &str) -> Result<NaiveDate, XbrlError> {
321 let normalized = strip_xsd_timezone(value);
322 NaiveDate::parse_from_str(normalized, "%Y-%m-%d").map_err(|_| {
323 ValueError::InvalidDate {
324 raw: value.to_owned(),
325 }
326 .into()
327 })
328}
329
330fn parse_xsd_datetime(value: &str) -> Result<NaiveDateTime, XbrlError> {
331 let normalized = strip_xsd_timezone(value);
332 NaiveDateTime::parse_from_str(normalized, "%Y-%m-%dT%H:%M:%S%.f")
333 .or_else(|_| NaiveDateTime::parse_from_str(normalized, "%Y-%m-%dT%H:%M:%S"))
334 .map_err(|_| {
335 ValueError::InvalidDateTime {
336 raw: value.to_owned(),
337 }
338 .into()
339 })
340}
341
342fn strip_xsd_timezone(value: &str) -> &str {
343 let trimmed = value.trim();
344 if let Some(without_z) = trimmed.strip_suffix('Z') {
345 return without_z;
346 }
347
348 if trimmed.len() >= 6 {
349 let tz_marker_index = trimmed.len() - 6;
350 let bytes = trimmed.as_bytes();
351 if (bytes[tz_marker_index] == b'+' || bytes[tz_marker_index] == b'-')
352 && bytes.get(tz_marker_index + 3) == Some(&b':')
353 {
354 return &trimmed[..tz_marker_index];
355 }
356 }
357
358 trimmed
359}
360
361fn parse_qname_value(
362 value: &str,
363 namespaces: &HashMap<NamespacePrefix, NamespaceUri>,
364) -> Result<ExpandedName, XbrlError> {
365 if value.is_empty() {
366 return Err(ValueError::InvalidQName {
367 raw: value.to_owned(),
368 reason: "QName value is empty".to_owned(),
369 }
370 .into());
371 }
372
373 let (prefix, local_name) = match value.split_once(':') {
374 Some((prefix, local_name)) => (Some(prefix), local_name),
375 None => (None, value),
376 };
377
378 if local_name.is_empty() {
379 return Err(ValueError::InvalidQName {
380 raw: value.to_owned(),
381 reason: "QName local name is empty".to_owned(),
382 }
383 .into());
384 }
385
386 let namespace_uri = match prefix {
387 Some(prefix) => {
388 namespaces
389 .get(prefix)
390 .cloned()
391 .ok_or_else(|| ValueError::InvalidQName {
392 raw: value.to_owned(),
393 reason: format!("Unknown QName prefix '{prefix}'"),
394 })?
395 }
396 None => namespaces
397 .get("")
398 .cloned()
399 .unwrap_or_else(|| NamespaceUri::from("")),
400 };
401
402 Ok(ExpandedName::new(namespace_uri, local_name.to_owned()))
403}
404
405#[cfg(test)]
406mod tests {
407 use super::{FactValue, parse_fact_value};
408 use crate::{
409 Balance, BaseSubstitutionGroup, Concept, ExpandedName, NamespaceUri, SubstitutionGroup,
410 XbrlType,
411 };
412 use std::collections::HashMap;
413
414 fn concept_with_type(data_type: XbrlType) -> Concept {
415 Concept {
416 id: Some("c1".to_owned()),
417 name: ExpandedName::new(
418 NamespaceUri::from("http://example.com"),
419 "Concept".to_owned(),
420 ),
421 data_type,
422 substitution_group: SubstitutionGroup {
423 base: BaseSubstitutionGroup::Item,
424 original: ExpandedName::new(
425 NamespaceUri::from("http://www.xbrl.org/2003/instance"),
426 "item".to_owned(),
427 ),
428 },
429 period_type: None,
430 balance: Some(Balance::Debit),
431 nillable: true,
432 is_abstract: false,
433 content_model: None,
434 }
435 }
436
437 #[test]
438 fn parse_integer_value() {
439 let concept = concept_with_type(XbrlType::Integer);
440 let value = parse_fact_value("42", &concept, &HashMap::new()).expect("integer parse");
441 assert!(matches!(value, FactValue::Integer(42)));
442 }
443
444 #[test]
445 fn parse_invalid_boolean_value() {
446 let concept = concept_with_type(XbrlType::Boolean);
447 let value = parse_fact_value("truth", &concept, &HashMap::new());
448 assert!(value.is_err());
449 }
450}