feat: add crabrl-fork to workspace and fix taxonomy loading
- Add crabrl-fork to workspace Cargo.toml - Update fiscal-xbrl-core to use local crabrl-fork - Fix SurfaceFormulaOp enum: add Divide variant - Fix SurfaceSignTransform enum: add Absolute variant - Implement divide_formula_values function for SurfaceFormulaOp::Divide - Implement Absolute sign transform handler - Fix all taxonomy_loader and related tests to pass
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::Reader;
|
||||
use regex::Regex;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -16,9 +18,6 @@ mod universal_income;
|
||||
|
||||
use taxonomy_loader::{ComputationSpec, ComputedDefinition};
|
||||
|
||||
#[cfg(feature = "with-crabrl")]
|
||||
use crabrl as _;
|
||||
|
||||
pub const PARSER_ENGINE: &str = "fiscal-xbrl";
|
||||
pub const PARSER_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -1062,24 +1061,79 @@ fn validate_xbrl_structure(xml: &str, source_file: Option<&str>) -> XbrlValidati
|
||||
};
|
||||
}
|
||||
|
||||
if !xml.contains("<xbrl") && !xml.contains("<xbrli:xbrl") {
|
||||
let mut reader = Reader::from_str(xml);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let mut has_xbrl_root = false;
|
||||
let mut depth: i32 = 0;
|
||||
let mut tag_stack: Vec<String> = Vec::new();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
|
||||
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
|
||||
|
||||
// Check for XBRL root element
|
||||
if depth == 0 && (name == "xbrl" || name.ends_with(":xbrl")) {
|
||||
has_xbrl_root = true;
|
||||
}
|
||||
|
||||
if matches!(reader.read_event_into(&mut buf), Ok(Event::Start(_))) {
|
||||
depth += 1;
|
||||
tag_stack.push(name);
|
||||
}
|
||||
}
|
||||
Ok(Event::End(e)) => {
|
||||
if depth > 0 {
|
||||
depth -= 1;
|
||||
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
|
||||
if let Some(expected) = tag_stack.pop() {
|
||||
if expected != name {
|
||||
return XbrlValidationResult {
|
||||
status: "warning".to_string(),
|
||||
message: Some(format!(
|
||||
"Mismatched tags in {:?}: expected </{}>, found </{}>",
|
||||
source_file.unwrap_or("unknown"),
|
||||
expected,
|
||||
name
|
||||
)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => {
|
||||
return XbrlValidationResult {
|
||||
status: "warning".to_string(),
|
||||
message: Some(format!(
|
||||
"XML parse error in {:?} at byte {}: {}",
|
||||
source_file.unwrap_or("unknown"),
|
||||
reader.error_position(),
|
||||
e
|
||||
)),
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
if !has_xbrl_root {
|
||||
return XbrlValidationResult {
|
||||
status: "error".to_string(),
|
||||
message: Some("Invalid XBRL: missing root element".to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let open_count = xml.matches('<').count();
|
||||
let close_count = xml.matches('>').count();
|
||||
|
||||
if open_count != close_count {
|
||||
if depth != 0 {
|
||||
return XbrlValidationResult {
|
||||
status: "warning".to_string(),
|
||||
message: Some(format!(
|
||||
"Malformed XML detected in {:?} ({} open, {} close tags)",
|
||||
"Unclosed tags in {:?}: {} unclosed element(s)",
|
||||
source_file.unwrap_or("unknown"),
|
||||
open_count,
|
||||
close_count
|
||||
depth
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -571,6 +571,7 @@ fn evaluate_formula_for_period(
|
||||
match formula.op {
|
||||
SurfaceFormulaOp::Sum => sum_formula_values(&values, formula.treat_null_as_zero),
|
||||
SurfaceFormulaOp::Subtract => subtract_formula_values(&values, formula.treat_null_as_zero),
|
||||
SurfaceFormulaOp::Divide => divide_formula_values(&values, formula.treat_null_as_zero),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,6 +613,33 @@ fn subtract_formula_values(values: &[Option<f64>], treat_null_as_zero: bool) ->
|
||||
Some(left - right)
|
||||
}
|
||||
|
||||
fn divide_formula_values(values: &[Option<f64>], treat_null_as_zero: bool) -> Option<f64> {
|
||||
if values.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let left = if treat_null_as_zero {
|
||||
values[0].unwrap_or(0.0)
|
||||
} else {
|
||||
values[0]?
|
||||
};
|
||||
let right = if treat_null_as_zero {
|
||||
values[1].unwrap_or(0.0)
|
||||
} else {
|
||||
values[1]?
|
||||
};
|
||||
|
||||
if right == 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !treat_null_as_zero && values.iter().all(|value| value.is_none()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(left / right)
|
||||
}
|
||||
|
||||
pub fn merge_mapping_assignments(
|
||||
primary: &mut HashMap<String, MappingAssignment>,
|
||||
secondary: HashMap<String, MappingAssignment>,
|
||||
@@ -831,6 +859,7 @@ fn transform_values(
|
||||
period_id.clone(),
|
||||
match sign_transform {
|
||||
Some(SurfaceSignTransform::Invert) => value.map(|amount| -amount),
|
||||
Some(SurfaceSignTransform::Absolute) => value.map(|amount| amount.abs()),
|
||||
None => *value,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ fn default_include_in_output() -> bool {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SurfaceSignTransform {
|
||||
Invert,
|
||||
Absolute,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@@ -73,6 +74,7 @@ pub struct SurfaceFormula {
|
||||
pub enum SurfaceFormulaOp {
|
||||
Sum,
|
||||
Subtract,
|
||||
Divide,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
||||
Reference in New Issue
Block a user