Initial commit
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Calculation parser for XBRL documents.
|
||||
|
||||
This module handles parsing of XBRL calculation linkbases and building
|
||||
calculation trees with weights for validation.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from edgar.xbrl.core import NAMESPACES, extract_element_id
|
||||
from edgar.xbrl.models import CalculationNode, CalculationTree, ElementCatalog, Fact, XBRLProcessingError
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class CalculationParser(BaseParser):
|
||||
"""Parser for XBRL calculation linkbases."""
|
||||
|
||||
def __init__(self, calculation_roles: Dict[str, Dict[str, Any]],
|
||||
calculation_trees: Dict[str, CalculationTree],
|
||||
element_catalog: Dict[str, ElementCatalog],
|
||||
facts: Dict[str, Fact]):
|
||||
"""
|
||||
Initialize calculation parser with data structure references.
|
||||
|
||||
Args:
|
||||
calculation_roles: Reference to calculation roles dictionary
|
||||
calculation_trees: Reference to calculation trees dictionary
|
||||
element_catalog: Reference to element catalog dictionary
|
||||
facts: Reference to facts dictionary
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# Store references to data structures
|
||||
self.calculation_roles = calculation_roles
|
||||
self.calculation_trees = calculation_trees
|
||||
self.element_catalog = element_catalog
|
||||
self.facts = facts
|
||||
|
||||
def parse_calculation(self, file_path: Union[str, Path]) -> None:
|
||||
"""Parse calculation linkbase file and build calculation trees."""
|
||||
try:
|
||||
content = Path(file_path).read_text()
|
||||
self.parse_calculation_content(content)
|
||||
except Exception as e:
|
||||
raise XBRLProcessingError(f"Error parsing calculation file {file_path}: {str(e)}") from e
|
||||
|
||||
def parse_calculation_content(self, content: str) -> None:
|
||||
"""Parse calculation linkbase content and build calculation trees."""
|
||||
try:
|
||||
# Use safe XML parsing method
|
||||
root = self._safe_parse_xml(content)
|
||||
|
||||
# Extract calculation links
|
||||
calculation_links = root.findall('.//{http://www.xbrl.org/2003/linkbase}calculationLink')
|
||||
|
||||
for link in calculation_links:
|
||||
role = link.get('{http://www.w3.org/1999/xlink}role')
|
||||
if not role:
|
||||
continue
|
||||
|
||||
# Store role information
|
||||
role_id = role.split('/')[-1] if '/' in role else role
|
||||
role_def = role_id.replace('_', ' ')
|
||||
|
||||
self.calculation_roles[role] = {
|
||||
'roleUri': role,
|
||||
'definition': role_def,
|
||||
'roleId': role_id
|
||||
}
|
||||
|
||||
# Extract arcs
|
||||
arcs = link.findall('.//{http://www.xbrl.org/2003/linkbase}calculationArc')
|
||||
|
||||
# Create relationships list
|
||||
relationships = []
|
||||
|
||||
for arc in arcs:
|
||||
from_ref = arc.get('{http://www.w3.org/1999/xlink}from')
|
||||
to_ref = arc.get('{http://www.w3.org/1999/xlink}to')
|
||||
order = self._parse_order_attribute(arc)
|
||||
weight = float(arc.get('weight', '1.0'))
|
||||
|
||||
if not from_ref or not to_ref:
|
||||
continue
|
||||
|
||||
# Find locators for from/to references
|
||||
from_loc = link.find(f'.//*[@{{{NAMESPACES["xlink"]}}}label="{from_ref}"]')
|
||||
to_loc = link.find(f'.//*[@{{{NAMESPACES["xlink"]}}}label="{to_ref}"]')
|
||||
|
||||
if from_loc is None or to_loc is None:
|
||||
continue
|
||||
|
||||
from_href = from_loc.get('{http://www.w3.org/1999/xlink}href')
|
||||
to_href = to_loc.get('{http://www.w3.org/1999/xlink}href')
|
||||
|
||||
if not from_href or not to_href:
|
||||
continue
|
||||
|
||||
# Extract element IDs
|
||||
from_element = extract_element_id(from_href)
|
||||
to_element = extract_element_id(to_href)
|
||||
|
||||
# Add relationship
|
||||
relationships.append({
|
||||
'from_element': from_element,
|
||||
'to_element': to_element,
|
||||
'order': order,
|
||||
'weight': weight
|
||||
})
|
||||
|
||||
# Build calculation tree for this role
|
||||
if relationships:
|
||||
self._build_calculation_tree(role, relationships)
|
||||
|
||||
except Exception as e:
|
||||
raise XBRLProcessingError(f"Error parsing calculation content: {str(e)}") from e
|
||||
|
||||
def _build_calculation_tree(self, role: str, relationships: List[Dict[str, Any]]) -> None:
|
||||
"""
|
||||
Build a calculation tree from relationships.
|
||||
|
||||
Args:
|
||||
role: Extended link role URI
|
||||
relationships: List of relationships (from_element, to_element, order, weight)
|
||||
"""
|
||||
# Group relationships by source element
|
||||
from_map = {}
|
||||
to_map = {}
|
||||
|
||||
for rel in relationships:
|
||||
from_element = rel['from_element']
|
||||
to_element = rel['to_element']
|
||||
|
||||
if from_element not in from_map:
|
||||
from_map[from_element] = []
|
||||
from_map[from_element].append(rel)
|
||||
|
||||
if to_element not in to_map:
|
||||
to_map[to_element] = []
|
||||
to_map[to_element].append(rel)
|
||||
|
||||
# Find root elements (appear as 'from' but not as 'to')
|
||||
root_elements = set(from_map.keys()) - set(to_map.keys())
|
||||
|
||||
if not root_elements:
|
||||
return # No root elements found
|
||||
|
||||
# Create calculation tree
|
||||
tree = CalculationTree(
|
||||
role_uri=role,
|
||||
definition=self.calculation_roles[role]['definition'],
|
||||
root_element_id=next(iter(root_elements)),
|
||||
all_nodes={}
|
||||
)
|
||||
|
||||
# Build tree recursively
|
||||
for root_id in root_elements:
|
||||
self._build_calculation_subtree(root_id, None, from_map, tree.all_nodes)
|
||||
|
||||
# Add tree to collection
|
||||
self.calculation_trees[role] = tree
|
||||
|
||||
def _build_calculation_subtree(self, element_id: str, parent_id: Optional[str],
|
||||
from_map: Dict[str, List[Dict[str, Any]]],
|
||||
all_nodes: Dict[str, CalculationNode]) -> None:
|
||||
"""
|
||||
Recursively build a calculation subtree.
|
||||
|
||||
Args:
|
||||
element_id: Current element ID
|
||||
parent_id: Parent element ID
|
||||
from_map: Map of relationships by source element
|
||||
all_nodes: Dictionary to store all nodes
|
||||
"""
|
||||
# Create node
|
||||
node = CalculationNode(
|
||||
element_id=element_id,
|
||||
parent=parent_id,
|
||||
children=[]
|
||||
)
|
||||
|
||||
# Add element information if available
|
||||
elem_info = None
|
||||
if element_id in self.element_catalog:
|
||||
elem_info = self.element_catalog[element_id]
|
||||
else:
|
||||
# Try alternative element ID formats (colon vs underscore)
|
||||
alt_element_id = element_id.replace(':', '_') if ':' in element_id else element_id.replace('_', ':')
|
||||
if alt_element_id in self.element_catalog:
|
||||
elem_info = self.element_catalog[alt_element_id]
|
||||
|
||||
if elem_info:
|
||||
node.balance_type = elem_info.balance
|
||||
node.period_type = elem_info.period_type
|
||||
|
||||
# Add to collection
|
||||
all_nodes[element_id] = node
|
||||
|
||||
# Process children
|
||||
if element_id in from_map:
|
||||
# Sort children by order
|
||||
children = sorted(from_map[element_id], key=lambda r: r['order'])
|
||||
|
||||
for rel in children:
|
||||
child_id = rel['to_element']
|
||||
|
||||
# Add child to parent's children list
|
||||
node.children.append(child_id)
|
||||
|
||||
# Set weight
|
||||
weight = rel['weight']
|
||||
|
||||
# Recursively build child subtree
|
||||
self._build_calculation_subtree(
|
||||
child_id, element_id, from_map, all_nodes
|
||||
)
|
||||
|
||||
# Update weight and order after child is built
|
||||
if child_id in all_nodes:
|
||||
all_nodes[child_id].weight = weight
|
||||
all_nodes[child_id].order = rel['order']
|
||||
Reference in New Issue
Block a user