Repository navigation
Conversation
dgunning#1766) Root cause: a fractionItemType fact has no text of its own. The instance parser fell back to "".join(element.itertext()), and the XML parser drops blank text, so the whitespace between numerator and denominator disappeared and 1/3 became "13". float() then stored 13.0, and the missing decimals ranked as precision 0. XBRL 2.1 §5.1.1.3.2 defines the value as the quotient and §4.6.6 defines the precision as INF. Change: a fact with direct xbrli:numerator and xbrli:denominator children stores the quotient and sets decimals to INF. A zero denominator, a non-decimal, a nil fact, or a quotient that does not fit in a float leaves numeric_value unset and keeps the two parts separated by "/", the same way float() leaves any other non-numeric text unset. Nested text that is not a fraction is still joined. Verification: inline instance, no SEC filing. No US-GAAP concept uses fractionItemType, and the issue's sample of 80 SEC instances had none, so the ground truth is the spec's 1/3. tests/issues/regression/test_issue_1766.py fails before the fix (value '13', numeric_value 13.0) and passes after (value '0.3333333333333333', numeric_value == 1/3, decimals 'INF'). hatch run test-fast: 3 failed, 49959 passed, 9 skipped, 3419 deselected, 1 xfailed. The three failures are tests/search/test_textsearch.py highlight assertions, which require ANSI color; this environment sets NO_COLOR=1, TERM=dumb and FORCE_COLOR=0. The same three pass with color enabled. They do not touch instance parsing.
Root cause: the quotient was formatted from float(), so 9007199254740993/1 was stored as 9007199254740992 and 1.0000000000000001/1 as 1. The division used the caller's decimal context, so prec=2 rounded it and an Inexact trap aborted the instance parse. Operand .text stops at a comment or processing instruction, so 1<!--c-->2 was read as 1. Change: value is the Decimal quotient in fixed-point form, and float() is used only for numeric_value. A float that overflows, underflows to zero, or is non-finite leaves numeric_value unset while value stays the exact decimal. The division runs in a local context (precision max(50, the operands' significant digits), traps cleared). Each operand is the element's full character content. Verification: regression tests cover the two float-mantissa cases, a caller context of prec=2 with the Inexact trap enabled, and 1<!--c-->2 / 4 == 3. Apple Inc. FY2023 10-K, accession 0000320193-23-000106, instance aapl-20230930_htm.xml, has no fraction item (no numerator element). Ordinary control, read from that instance: us-gaap:Assets, context c-22, instant 2023-09-30, 352583000000 (id f-172, decimals -6); context c-23, instant 2022-09-24, 352755000000. Cassette recorded from live SEC and not edited. hatch run pytest tests/issues/regression/test_issue_1766.py: 11 passed. hatch run check-regression-provenance: 556 regression file(s). hatch run check-cassettes: 272 YAML file(s), no disallowed tags. hatch run test-fast: 3 failed, 49963 passed, 9 skipped, 3419 deselected, 1 xfailed. The three failures are the text-search highlight tests under NO_COLOR; the same tests pass with color enabled. hatch run lint: 1061 findings, the same three pre-existing instance.py findings (N812, two S110) and no new ones.
…xact (GH dgunning#1766) Root cause: localcontext() copied the caller's rounding and exponent limits, so ROUND_UP changed the last digit of 1/3, Emax=2 overflowed 1000/1, and Emin=-9 turned 10^-100 into zero. Precision was also capped near 50 significant digits, which truncates the terminating quotient 1/2**100 (70 digits) while still marking it INF. decimals was set to INF only after float() succeeded, so 10**400/1 and 1/10**400 kept decimals None. Change: divide in an explicit Context (ROUND_HALF_EVEN, Emin/Emax at the decimal limits, traps cleared). A denominator of 2^a*5^b is stored at its exact digit count. A non-terminating quotient stays half-even at 50 significant digits, so 1/3 is fifty 3s. decimals is INF whenever the quotient is a finite decimal, including when it does not fit in a float. Verification: the caller-context test now sets ROUND_UP, Emax=2 and Emin=-9. 1/2**100 is the 70-digit expansion of 5**100/10**100. 10**400/1 and 1/10**400 keep decimals INF. hatch run pytest tests/issues/regression/test_issue_1766.py: 12 passed. check-regression-provenance: 556. check-cassettes: 272. hatch run lint: 1061 findings, the same three pre-existing instance.py findings (N812, two S110).
…ning#1766) Root cause: the significand's digit count used len(str(coefficient)). Under the default 4300-digit int-to-str limit, 1/2**6200 (5**6200 has 4334 digits) raises ValueError, and that handler stored slash text with no numeric value. Change: the count is floor(log10(n))+1 from bit_length, corrected against a power of ten, so the int is never converted to a string. The changelog no longer calls a recurring one third exact: terminating quotients are exact, recurring ones are half-even to 50 significant digits, and decimals stays INF. Verification: tests/issues/regression/test_issue_1766.py 13 passed, including 1/2**6200 while str(5**6200) still raises under the default limit. check-regression-provenance 556. check-cassettes 272. lint 1061, the same three pre-existing instance.py findings.
…nning#1766) The 1/2**6200 regression assumed the process still had Python's default 4300-digit limit. PYTHONINTMAXSTRDIGITS=0 turns that limit off, so str(5**6200) would succeed and the test would fail for the wrong reason. The test now sets the limit to 4300 and restores the previous value. The fraction docstring no longer says rounding clears numeric_value: that field is unset only on overflow or a nonzero underflow, and the exact text stays in value. Verification: tests/issues/regression/test_issue_1766.py 13 passed, and the same 13 passed with PYTHONINTMAXSTRDIGITS=0. hatch run lint: 1061 findings, the same three pre-existing instance.py findings.
|
One edge case in _exact_terminating_digits(Decimal("1"), Decimal("3")) # None, 0.0 s
_exact_terminating_digits(Decimal("1E+300000"), Decimal("3")) # None, 0.05 s
_exact_terminating_digits(Decimal("1"), Decimal("1E-300000")) # 1, 81.7 sWhen the shift ( A check against the decimal lexical form before any arithmetic would close this. For example, Smaller point: the regression test file is 413 lines, and none of the cases I could see in the diff cover an operand with an exponent or an underscore. Those are the inputs where |
…1766) Root cause: Decimal() accepts an exponent, underscores, and non-ASCII digits, which the xsd:decimal lexical space does not include. A negative exponent then makes _exact_terminating_digits scale a coefficient by 10**n, so the cost follows the exponent rather than the text. _exact_terminating_digits(Decimal('1'), Decimal('1E-300000')) takes about 82 seconds. Change: both operands are checked against the xsd:decimal production, ASCII digits only, before any Decimal arithmetic. Surrounding whitespace is still stripped, matching the operand text that is stored, so " 2 " / " 4 " stays one half. A mismatch keeps the slash form and leaves numeric_value unset. Verification: hatch run pytest tests/issues/regression/test_issue_1766.py 14 passed in 0.18s, including 1/1E-300000, an exponent numerator, 1_0, and an Arabic-Indic digit. hatch run lint: 1061 findings, the same three pre-existing instance.py findings (N812, two S110).
|
Read the new commit against the case I timed: One input still scales with text length rather than value: a long plain-decimal denominator such as |
dgunning
left a comment
There was a problem hiding this comment.
Thanks for this. The bug is real (1/3 read as 13) and the main change is in the right place. I'd like two changes before merge, plus a note on CI.
1. Check that each operand is an xsd:decimal before calling Decimal(). @arhancanli's finding reproduces at the PR head: a denominator of 1E-20000 takes 0.16 s and 1E-60000 takes 1.47 s, so the time grows with the square of the exponent. Decimal() also accepts 1_0 (read as 10) and Arabic-Indic digits (١/2 gives 0.5), and neither is valid in an instance document. A gate like re.fullmatch(r'[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)', text) on both operands, falling through to the existing slash-form path, closes all three. Please add tests for an exponent, an underscore and a non-ASCII digit.
2. With that gate in place, most of the exactness code can go. Once the operands have no exponent, a terminating n/d has at most sig(n) + 4*sig(d) + 1 significant digits, since log2(d) < 3.33 * digits(d). So you can divide once at that precision and read the context's Inexact flag. If it's set, the quotient recurs, and you divide again at your current 50-digit rule. This replaces _coefficient, _decimal_digit_count and _exact_terminating_digits (about 75 lines). I compared the two versions on 40,006 cases (random integers and decimals over denominators of the form 2^a*5^b, random values, 3*2^k and 10^k, plus 1/2**6200 and 9007199254740993/1) and got 0 differences:
ctx = Context(prec=max(50, sig(n) + 4 * sig(d) + 1), rounding=ROUND_HALF_EVEN,
Emin=MIN_EMIN, Emax=MAX_EMAX, traps=[])
q = ctx.divide(N, D)
if ctx.flags[Inexact]: # recurring: round to the 50-digit rule
q = _decimal_context(max(50, sig(n) + sig(d))).divide(N, D)CI: the one test-fast (3.13) failure, tests/documents/test_markdown.py::test_obscure_filing_to_markdown, has nothing to do with this change. It passes at your head commit locally and on main, so the next push should clear it.
Parse time is fine. A 21,393-fact instance (GBDC 10-Q fixture) takes 15.3 s on this branch and 16.1 s on main, which is within noise.
) Root cause: a plain decimal is legal xsd:decimal, so the lexical gate admits 0. followed by 300,000 zeros and a 1. Decimal stores that as coefficient 1 and exponent -300001, and _exact_terminating_digits then scales a coefficient by 10**300001. Parsing that fraction took 29.4 seconds and stored a 300,002-digit integer. Change: drop _coefficient, _decimal_digit_count and _exact_terminating_digits. Divide once at sig(n) + 4*sig(d) + 1 digits, since log2(d) < 3.33 * digits(d). If the context's Inexact flag is set, divide again at the 50-digit rule. The lexical pattern is the maintainer's non-capturing form. Operands longer than 10000 characters keep the slash form. A cap of 100 would reject 2**6200 (1867 digits) and 10**400, which still have to divide exactly. Verification: the 300,000-zero denominator was 29.37s before the cap and 0.0016s after, value 1/0.000...001 with numeric_value None. hatch run pytest tests/issues/regression/test_issue_1766.py: 15 passed in 0.23s. hatch run pytest tests/xbrl/test_xbrl_parser.py tests/issues/regression/test_jj77_instance_fact_identity.py -k 'not xoxo': 14 passed, 1 deselected, in 15.30s. hatch run ruff check edgar/xbrl/parsers/instance.py: the same three pre-existing findings (N812, two S110).
…unning#1766) The 1/3 case uses 50 digits on both passes, so deleting the Inexact fallback would still pass. 10**60/3 recurs: sig(n)+sig(d) is 62 and the first pass is 66 digits. Half-even, the stored text is sixty 3s, a point, and two 3s. The cap is checked at 10000 and 10001 characters on both operands. The 300,000-zero denominator is spied, so a capped operand never reaches Decimal. A one-second clock assert stays beside the spy. Verification: hatch run pytest tests/issues/regression/test_issue_1766.py 17 passed in 0.24s. hatch run ruff check tests/issues/regression/test_issue_1766.py: all checks passed.
|
Thanks both. dgunning: point 1 is in (79c3ac1), with the gate matching your pattern and tests for an exponent, arhancanli: good call on long plain decimals. |
|
Read the new commit. The One case I'd want a test for: the operands pass the gate, the quotient is exact, but it is too small or large for a float. For example |
…#1766) 1/10**9999 is an exact decimal, the digit 1 in the 9,999th place, and float() of it is 0.0. A 9,999-digit numerator over 1 is exact and float() of it is inf on Python 3.12 (it does not raise). Both operands stay within the 10,000-character cap. The parser already returns the exact text, numeric_value None and decimals INF for both: underflow through numeric == 0.0 and quotient != 0, overflow through the non-finite check. No parser change. Verification: pytest tests/issues/regression/test_issue_1766.py 19 passed in 0.25s. pytest tests/xbrl/test_xbrl_parser.py tests/issues/regression/test_jj77_instance_fact_identity.py 15 passed in 16.83s. ruff check on the test file: all checks passed. check-regression-provenance: 556. check-regression-skips: 556.
|
Read c61c6e7 (not run). Both tests cover the case I asked about: One band is between those two and the normal range: a quotient like |
…ing#1766) 1/10**320 is the subnormal 1e-320 (hex 0x0.00000000007e8p-1022), finite and nonzero, so numeric_value keeps that float. 1/10**400 underflows to 0.0 and -1/10**400 underflows to signed zero; both keep the exact decimal with numeric_value unset. That is the existing out-of-range rule: unset only for a non-finite float or a nonzero quotient that collapses to 0.0. A subnormal is the same kind of rounded float a value past the mantissa already gets. Split the division and the float conversion out of _fraction_item. CodeFactor flags that method as Complex Method at complexity 16. After the split, radon reports 13 for _fraction_item, 7 for _fraction_quotient and 4 for _fraction_numeric_value. Verification: the new test passed against the parser at c61c6e7 (1 passed in 0.12s) and again after the split. hatch run pytest tests/issues/regression/test_issue_1766.py: 20 passed in 0.20s. hatch run pytest tests/xbrl/test_xbrl_parser.py tests/issues/regression/test_jj77_instance_fact_identity.py tests/issues/regression/test_issue_1766.py: 35 passed in 16.02s. hatch run test-fast: 49887 passed, 88 failed, 9 skipped, 3419 deselected, 1 xfailed, 88 rerun in 1510.56s. The 88 failures are share-scale captions, two Schedule 13D rich-table rows, three text-search highlights, and the VCR null-reason test. None are in test_issue_1766.py. hatch run ruff check tests/issues/regression/test_issue_1766.py: all checks passed. ruff check edgar/xbrl/parsers/instance.py: the same three pre-existing findings (N812, two S110). check-regression-provenance and check-regression-skips: 556.
…unning#1766) A one-second bound fails when the machine is busy, even if the parser is right. The exponent 1E-300000, the underscore 1_0, the Arabic-Indic digit, and the 300,000-zero denominator are spied instead. Each is absent from Decimal's constructor arguments. The fixture's 1/3 is present, so an empty spy cannot pass the check. Verification: hatch run pytest tests/issues/regression/test_issue_1766.py 20 passed in 0.35s. hatch run pytest tests/xbrl/test_xbrl_parser.py tests/issues/regression/test_jj77_instance_fact_identity.py 15 passed in 15.40s. hatch run ruff check tests/issues/regression/test_issue_1766.py: all checks passed.
What this changes
Before: an
xbrli:fractionItemTypefact with numerator 1 and denominator 3 was parsed asvalue='13',numeric_value=13.0, because the instance parser joined the children's text. After: the fact is the quotient withdecimals='INF'. Terminating quotients are stored exactly as decimal text (9007199254740993/1stays9007199254740993, and1/2**6200keeps all 4,334 digits). The quotient is computed with one division atsig(n) + 4*sig(d) + 1digits; if that is inexact (recurring, such as one third) it is rounded half-even to 50 significant digits. Operands must match the xsd:decimal lexical form (ASCII digits, no exponent or underscore) and be at most 10,000 characters; anything else keeps its slash text withnumeric_value=None. The division ignores the caller's decimal context. Non-fraction facts are unchanged.Fixes #1766
Verification
0000320193-23-000106, instanceaapl-20230930_htm.xml:us-gaap:Assets, contextc-22(instant 2023-09-30) =352583000000. SEC full-text search forfractionItemTypeandxbrli:numeratorreturned no hits, so no real filing with a fraction item was found. This is an ordinary-fact control, and the fraction cases use synthetic fixtures.tests/issues/regression/test_issue_1766.py; its docstring links XBRL fraction items are parsed as their numerator and denominator digits joined (1/3 reads as 13) #1766. 17 tests pass (including exponent,1_0, non-ASCII digit, 10,000/10,001-character boundary, and10**60/3fallback cases), also withPYTHONINTMAXSTRDIGITS=0.tests/cassettes/test_apple_fy2023_10k_ordinary_assets_are_unchanged.yamlwas recorded from live SEC and not edited afterwards. Recorded on: 2026-10-24 (SEC response date 03:33:24 GMT)1/0,0/0,abc/3) keeps its text withnumeric_value=None, the same as an existing non-numeric fact, and the tests assert that.changelog.d/1766.fixed.md,changelog.d/1766-xsd-decimal.fixed.md,changelog.d/1766-operand-length.fixed.mdOther checks: regression provenance OK (556), cassettes OK (272), lint shows no new findings.
test-fastwas last run on2f7d1574, with 49963 passed and 3 failed. The 3 failures are textsearch highlight tests that fail underNO_COLORand pass with color enabled. It was not re-run after the later commits, which touch onlyedgar/xbrl/parsers/instance.py, the regression test and the changelog fragments. XBRL parser tests (tests/xbrl/test_xbrl_parser.py,test_jj77_instance_fact_identity.py) pass at9898334.6.0
docs/upgrade/6.0.mdsays what users see now and what to do insteadNot a 6.0 change.
AI assistance