Skip to content

Commit b31b3e3

Browse files
committed
Format share amounts using their decimal values
1 parent 56159f1 commit b31b3e3

7 files changed

Lines changed: 2745 additions & 37 deletions

File tree

edgar/xbrl2/core.py

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,12 @@ def format_value(value: Union[int, float, str], is_monetary: bool, scale: int,
250250

251251
# Apply scaling
252252
scaled_value = value
253-
if is_monetary:
254-
if scale <= -9: # Billions
255-
scaled_value = value / 1_000_000_000
256-
elif scale <= -6: # Millions
257-
scaled_value = value / 1_000_000
258-
elif scale <= -3: # Thousands
259-
scaled_value = value / 1_000
253+
if scale <= -9: # Billions
254+
scaled_value = value / 1_000_000_000
255+
elif scale <= -6: # Millions
256+
scaled_value = value / 1_000_000
257+
elif scale <= -3: # Thousands
258+
scaled_value = value / 1_000
260259

261260
# Determine decimal places to show
262261
if isinstance(decimals, int):
@@ -265,23 +264,17 @@ def format_value(value: Union[int, float, str], is_monetary: bool, scale: int,
265264
decimal_places = min(2, decimals)
266265
else:
267266
# For negative decimals, adjust based on scaling
268-
if is_monetary:
269-
if scale <= -9: # Billions
270-
decimal_places = min(2, max(0, decimals + 9))
271-
elif scale <= -6: # Millions
272-
decimal_places = min(2, max(0, decimals + 6))
273-
elif scale <= -3: # Thousands
274-
decimal_places = min(2, max(0, decimals + 3))
275-
else:
276-
decimal_places = 0
267+
if scale <= -9: # Billions
268+
decimal_places = min(2, max(0, decimals + 9))
269+
elif scale <= -6: # Millions
270+
decimal_places = min(2, max(0, decimals + 6))
271+
elif scale <= -3: # Thousands
272+
decimal_places = min(2, max(0, decimals + 3))
277273
else:
278-
# For non-monetary values like share counts
279-
# Check if the value is effectively a whole number
280-
if abs(round(value) - value) < 0.001:
281-
decimal_places = 0 # Display as whole number
282-
else:
283-
# Otherwise use decimals attribute to determine precision
284-
decimal_places = max(0, -decimals)
274+
# For unscaled values, respect the decimals attribute
275+
# If decimals is negative, show that many zeros to the left of decimal
276+
# E.g., decimals=-2 means precision to hundreds place (two zeros after decimal)
277+
decimal_places = max(0, -decimals)
285278
else:
286279
# Default decimal places
287280
if is_monetary:

edgar/xbrl2/rendering.py

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
from edgar.files.html import Document
2121
from datetime import datetime
2222

23+
share_concepts = [
24+
'us-gaap_CommonStockSharesOutstanding',
25+
'us-gaap_WeightedAverageNumberOfSharesOutstandingBasic',
26+
'us-gaap_WeightedAverageNumberOfSharesOutstandingDiluted',
27+
'us-gaap_WeightedAverageNumberOfDilutedSharesOutstanding',
28+
'us-gaap_CommonStockSharesIssued'
29+
]
30+
2331

2432
def _is_html(text: str) -> bool:
2533
"""
@@ -353,7 +361,26 @@ def render_statement(
353361
# Determine the dominant scale for monetary values in this statement
354362
dominant_scale = determine_dominant_scale(statement_data, periods_to_display)
355363

356-
# Add the fiscal period indicator and units note as a subtitle if available
364+
# Determine the scale used for share amounts if present
365+
shares_scale = None
366+
367+
# Look for share-related concepts to determine their scaling from the decimals attribute
368+
for item in statement_data:
369+
concept = item.get('concept', '')
370+
if concept in share_concepts:
371+
# Check decimals attribute to determine proper scaling
372+
for period_key, _ in periods_to_display:
373+
decimals = item.get('decimals', {}).get(period_key)
374+
if isinstance(decimals, int) and decimals <= 0:
375+
# Use the decimals attribute to determine the scale
376+
# For shares, decimals is typically negative
377+
# -3 means thousands, -6 means millions, etc.
378+
shares_scale = decimals
379+
break
380+
if shares_scale is not None:
381+
break
382+
383+
# Add the fiscal period indicator and note as a subtitle if available
357384
if formatted_periods:
358385
subtitles = []
359386

@@ -363,12 +390,36 @@ def render_statement(
363390

364391
# Add units note
365392
if is_monetary_statement:
393+
monetary_scale_text = ""
366394
if dominant_scale == -3:
367-
units_note = "[italic](In thousands, except per share data)[/italic]"
395+
monetary_scale_text = "thousands"
368396
elif dominant_scale == -6:
369-
units_note = "[italic](In millions, except per share data)[/italic]"
397+
monetary_scale_text = "millions"
370398
elif dominant_scale == -9:
371-
units_note = "[italic](In billions, except per share data)[/italic]"
399+
monetary_scale_text = "billions"
400+
401+
shares_scale_text = ""
402+
if shares_scale is not None:
403+
if shares_scale == -3:
404+
shares_scale_text = "thousands"
405+
elif shares_scale == -6:
406+
shares_scale_text = "millions"
407+
elif shares_scale == -9:
408+
shares_scale_text = "billions"
409+
elif shares_scale == 0:
410+
shares_scale_text = "actual amounts"
411+
else:
412+
# For other negative scales (like -4, -5, -7, etc.)
413+
# Use a more generic description based on the scale
414+
scale_factor = 10 ** (-shares_scale)
415+
if scale_factor >= 1000:
416+
shares_scale_text = f"scaled by {scale_factor:,}"
417+
418+
# Construct appropriate units note
419+
if monetary_scale_text and shares_scale_text and shares_scale != dominant_scale:
420+
units_note = f"[italic](In {monetary_scale_text}, except shares in {shares_scale_text})[/italic]"
421+
elif monetary_scale_text:
422+
units_note = f"[italic](In {monetary_scale_text}, except per share data)[/italic]"
372423
else:
373424
units_note = ""
374425

@@ -595,8 +646,11 @@ def render_statement(
595646

596647
# Format the label based on level and abstract status
597648
level = item['level']
649+
598650
# Remove [Abstract] from label if present
599651
label = item['label'].replace(' [Abstract]', '')
652+
653+
concept = item['concept']
600654

601655
# Get values for each period
602656
period_values = []
@@ -608,22 +662,39 @@ def render_statement(
608662
# Check label to identify non-monetary items like shares, ratios, etc.
609663
is_monetary = is_monetary_statement
610664

611-
# Check for shares-related values by examining label
665+
# Check for shares-related values by examining concept names
612666
label_lower = label.lower()
613-
if any(keyword in label_lower for keyword in [
614-
'earnings per share', 'per common share', 'per share', 'in shares', 'shares outstanding'
615-
'per basic', 'per diluted'
616-
]):
617-
is_monetary = False
667+
is_share_value = False
618668

669+
if concept in ['us-gaap_EarningsPerShareBasic', 'us-gaap_EarningsPerShareDiluted']:
670+
is_monetary = False
671+
elif concept in share_concepts:
672+
is_monetary = False
673+
is_share_value = True
674+
619675
# Ratio-related items should not be monetary
620676
if any(keyword == word for keyword in ['ratio', 'percentage', 'per cent']
621677
for word in label_lower.split()):
622678
is_monetary = False
623679

624680
# Format numeric values
625681
if isinstance(value, (int, float)):
626-
formatted_value = Text(format_value(value, is_monetary, dominant_scale, fact_decimals), justify="right")
682+
# Handle share values differently
683+
if is_share_value and isinstance(fact_decimals, int):
684+
# Use fact_decimals to determine the appropriate scaling for share values
685+
# This ensures correct display for companies of all sizes
686+
if fact_decimals <= -3:
687+
# Apply appropriate scaling based on the actual decimals value
688+
scale_factor = 10 ** (-fact_decimals)
689+
scaled_value = value / scale_factor
690+
# Always display share amounts with 0 decimal places for cleaner presentation
691+
formatted_value = Text(f"{scaled_value:,.0f}", justify="right")
692+
else:
693+
# For smaller numbers or positive decimals, use unscaled values
694+
formatted_value = Text(f"{value:,.0f}", justify="right")
695+
else:
696+
# Format other values normally using the flexible format_value function
697+
formatted_value = Text(format_value(value, is_monetary, dominant_scale, fact_decimals), justify="right")
627698
else:
628699
# Non-numeric values - check if it's HTML and convert if needed
629700
if value and isinstance(value, str) and _is_html(value):

edgar/xbrl2/standardization.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import json
1010
import os
1111
from enum import Enum
12+
from json import JSONDecodeError
1213
from typing import Dict, List, Optional, Set, Tuple, Any
1314
from difflib import SequenceMatcher
1415

@@ -107,8 +108,10 @@ def _load_mappings(self) -> Dict[str, Set[str]]:
107108
else:
108109
# Flat structure
109110
return {k: set(v) for k, v in data.items()}
110-
111-
except (FileNotFoundError, json.JSONDecodeError):
111+
112+
except JSONDecodeError as e:
113+
raise
114+
except FileNotFoundError:
112115
# Return default mappings if file doesn't exist or is invalid
113116
return {
114117
"Revenue": {"us-gaap_SalesRevenueNet", "us-gaap_Revenue", "us-gaap_Revenues"},

edgartools.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Edgartools
2+
3+
4+
# Getting Started
5+
6+
Import the module and most useful functions with:
7+
```python
8+
from edgar import *
9+
```
10+
11+
To make requests to the SEC, you need to set your identity with:
12+
```python
13+
set_identity("user@domain.com")
14+
```
15+
16+
## Get a Company
17+
18+
### Get a company by ticker symbol
19+
20+
```python
21+
company = Company("AAPL")
22+
```
23+
24+
### Get a company by CIK
25+
26+
```python
27+
company = Company("0000320193")
28+
# OR
29+
company = Company(320193)
30+
```
31+
32+
### Get company filings
33+
34+
To get all filings for a company:
35+
```python
36+
filings = company.get_filings()
37+
```
38+
39+
### Get company filings by form type
40+
41+
To get all 10-K filings for a company:
42+
```python
43+
filings = company.get_filings(form="10-K")
44+
```

0 commit comments

Comments
 (0)