Skip to content

feat(erational): extend parse to p/q, decimal, and scientific notation#866

Merged
Ravenwater merged 1 commit into
mainfrom
feat/issue-855-erational-parse-pq-decimal-sci
May 18, 2026
Merged

feat(erational): extend parse to p/q, decimal, and scientific notation#866
Ravenwater merged 1 commit into
mainfrom
feat/issue-855-erational-parse-pq-decimal-sci

Conversation

@Ravenwater

@Ravenwater Ravenwater commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

`erational::parse` previously matched only `[+-]*[0-9]+` via
`std::regex_match` and left the denominator unset (so reusing an
erational with a non-default denominator silently kept stale state).
This PR routes parse through `sw::universal::string_parse::scan_decimal_float`
(the foundation from #838) and now accepts every form a rational
literal can naturally take.

Design

A private static helper `parse_decimal_to_fraction(s, num, den, neg)`
tokenizes any decimal / scientific literal into an exact
(numerator, denominator) edecimal pair:

input numerator denominator
`"42"` 42 1
`"3.14"` 314 100
`"1.5e2"` 150 1
`"1.5e-1"` 15 10
`"3.14e+200"` 314 * 10^198 1

`parse()` itself first detects a `'/'` separator. If present, both
sides are parsed through the helper and combined as
`(p_num * q_den) / (p_den * q_num)` with sign = `p_neg ^ q_neg`.
`normalize()` then reduces to lowest terms via GCD.

`q_num.iszero()` (across all flavors: `"1/0"`, `"5/0.0"`,
`"1/0e10"`) is rejected -- erational has no NaR encoding and
silently representing infinity would mask downstream divide-by-zero
detection.

Defensive cap

The helper rejects inputs whose significand or denominator would
exceed 1,048,576 digits, mirroring the DoS protection that #854 added
to `edecimal::parse`. `scan_decimal_float` returns an int32
exponent, so without this guard `"1e2000000000"` would expand to a
~2 GB string before reaching edecimal.

Examples

```
"0" -> 0/1
"-1000" -> -1000/1
"1/2" -> 1/2
"4/8" -> 1/2 (GCD simplification)
"-22/7" -> -22/7
"22/-7" -> -22/7 (sign normalization)
"3.14" -> 157/50 (decimal -> rational)
"-0.5" -> -1/2
"1.5e-1" -> 3/20 (scientific -> rational)
"3.14/2" -> 157/100 (mixed: decimal numerator)
"1e2/2e1" -> 5/1
"-0.0" -> 0/1 (negative-zero collapse)
"1/0" -> REJECT
"1/2/3" -> REJECT
"" -> REJECT
"3.14.15" -> REJECT
```

Changes

  • `include/sw/universal/number/erational/erational_impl.hpp` --
    replace parse() with the routing logic; add the private
    parse_decimal_to_fraction helper; the integer path now also resets
    denominator to 1 (previously a latent bug when reusing an erational).
  • `elastic/rational/decimal/conversion/string_parse.cpp` -- extend the
    test from 2 groups to 11.

Test Results

Target gcc build gcc test clang build clang test
erat_string_parse OK PASS (11/11 groups) OK PASS (11/11 groups)
erat_api / logic / exceptions OK exit 0 OK exit 0
erat_addition / subtraction / multiplication / division / sqrt OK exit 0 OK exit 0

Test plan

  • Fast CI (gcc + clang CI_LITE) passes
  • Promote when satisfied: `gh pr ready`

Resolves #855

Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Tests

    • Significantly expanded regression test coverage for rational number parsing across integers, ratios, decimals, and scientific notation.
  • New Features

    • Enhanced parser to support scientific notation and mixed rational/decimal expressions.
  • Bug Fixes

    • Improved validation for invalid inputs including division-by-zero and malformed tokens.
    • Standardized negative-zero handling across all input forms.

Previously erational::parse only accepted integer literals matching
[+-]*[0-9]+ and left the denominator unset (relying on the prior
state, which defaulted to 1 only on a freshly constructed value).

Replace parse() with a routing function and a static helper
parse_decimal_to_fraction(s, num, den, neg) that uses scan_decimal_float
(the foundation from #838) to tokenize either an integer, a decimal,
or a scientific literal into an exact (numerator, denominator)
edecimal pair:

  "42"          -> num=42,  den=1
  "3.14"        -> num=314, den=100
  "1.5e2"       -> num=150, den=1
  "1.5e-1"      -> num=15,  den=10

For the p/q form, split on '/' and parse each half through the same
helper, then combine as (p_num * q_den) / (p_den * q_num).  Sign is
the XOR of the two sides.  Mixed forms like "3.14/2" and "1e2/2e1"
work because each side is independently a decimal/scientific literal.

  "1/2"        -> 1/2
  "-22/7"      -> -22/7
  "22/-7"      -> -22/7
  "4/8"        -> 1/2     (normalize() reduces via GCD)
  "3.14/2"     -> 157/100

Rejected forms:
  - q == 0 across all flavors: "1/0", "5/0.0", "0/0", "1/0e10"
    (erational has no NaR encoding, and silently representing
     infinity would mask downstream divide-by-zero detection)
  - Two slashes: "1/2/3"
  - Empty side: "1/", "/2", "/"
  - Malformed decimal: "3.14.15", "1e", ".", "42x"

Defensive cap: parse_decimal_to_fraction rejects any input whose
significand or denominator would exceed 2^20 (1,048,576) digits, the
same cap used by edecimal::parse (#854).

operator>> hygiene (failbit + extraction guard) was already shipped in
#858 (Phase E of #835); the test file now also pins it.

Test (elastic/rational/decimal/conversion/string_parse.cpp) extended
to 11 groups: integer, p/q with simplification, decimal-to-rational,
scientific, mixed p/q with decimal sides, q=0 rejection, malformed,
negative-zero collapse, operator>> failbit on bad, operator>> on a
p/q token in whitespace, operator>> empty stream.

Resolves #855

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Ravenwater Ravenwater self-assigned this May 18, 2026
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Extended erational::parse to accept integers, rational p/q form, decimal literals, scientific notation, and mixed formats. A new parse_decimal_to_fraction helper converts decimals and scientific notation to reduced fractions. Comprehensive regression tests validate all input formats, rejection paths, operator>> failbit behavior, and edge cases.

Changes

erational parser extension to p/q, decimal, and scientific notation

Layer / File(s) Summary
Core erational::parse rewrite
include/sw/universal/number/erational/erational_impl.hpp
erational::parse now parses integers, p/q form, decimals, scientific notation, and mixed rational/decimal inputs by trimming, splitting on '/', converting each side to an exact fraction, rejecting denominator-zero, normalizing to lowest terms via GCD, and ensuring negative-zero is eliminated.
Decimal and scientific notation to fraction conversion
include/sw/universal/number/erational/erational_impl.hpp
New private helper parse_decimal_to_fraction converts decimal or scientific-notation tokens into reduced (numerator, denominator) pairs using scan_decimal_float, handling positive and negative exponents separately with digit-expansion limits.
Test infrastructure and comprehensive parse coverage
elastic/rational/decimal/conversion/string_parse.cpp
Adds CheckParse and CheckReject helpers, expands test suite to cover integers, p/q simplification with sign handling, decimals, scientific notation (including negative exponents), mixed formats, division-by-zero rejection, malformed-token rejection, and negative-zero normalization. Updates file-level documentation to reflect expanded grammar.
operator>> regression tests and output hygiene
elastic/rational/decimal/conversion/string_parse.cpp
Replaces prior narrow operator>> tests with three new checks: failbit on bad tokens, success on p/q tokens with whitespace handling, and failbit on empty stream. Updates catch block output formatting to validate operator>> properly propagates parse failures.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • stillwater-sc/universal#838: The new erational::parse implementation uses string_parse::scan_decimal_float tokenization logic added in this PR.
  • stillwater-sc/universal#858: Overlaps on Phase E operator>> hygiene changes for erational (failbit behavior).
  • stillwater-sc/universal#865: Both extend rational/decimal string parsing to accept decimal and scientific notation via shared scan_decimal_float with operator>>/rejection coverage.

Suggested labels

enhancement

Poem

🐰 A rabbit hops through fractions bold—
Parse decimals, scientific gold!
From p/q to ratios neat,
erational skips, never beats.
Parsing magic, one hop complete! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main feature addition: extending erational::parse to handle p/q, decimal, and scientific notation formats.
Linked Issues check ✅ Passed The PR implementation fulfills all coding requirements from issue #855: p/q parsing with GCD reduction, decimal/scientific conversion, q=0 rejection, operator>> failbit behavior, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are scoped to the requirements in issue #855: parse logic enhancement, helper function addition, and test coverage expansion with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-855-erational-parse-pq-decimal-sci

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
include/sw/universal/number/erational/erational_impl.hpp (1)

340-342: 💤 Low value

Remove the empty public: section.

The public: access specifier on line 340 has no declarations before protected: on line 342, leaving a confusing empty section. This appears to be leftover from code reorganization.

Proposed fix
 	}
 
-public:
-
 protected:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/sw/universal/number/erational/erational_impl.hpp` around lines 340 -
342, Delete the stray empty access specifier by removing the standalone
"public:" that appears immediately before "protected:" in the erational
implementation (the empty section in erational_impl.hpp); ensure the class
continuity remains intact (leave the "protected:" block and any following
members unchanged) so there are no empty access specifier blocks left.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@include/sw/universal/number/erational/erational_impl.hpp`:
- Around line 340-342: Delete the stray empty access specifier by removing the
standalone "public:" that appears immediately before "protected:" in the
erational implementation (the empty section in erational_impl.hpp); ensure the
class continuity remains intact (leave the "protected:" block and any following
members unchanged) so there are no empty access specifier blocks left.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b3f67fb-daab-48eb-8e90-140f31d38f87

📥 Commits

Reviewing files that changed from the base of the PR and between 861e6e6 and 9eb9b55.

📒 Files selected for processing (2)
  • elastic/rational/decimal/conversion/string_parse.cpp
  • include/sw/universal/number/erational/erational_impl.hpp

@Ravenwater Ravenwater marked this pull request as ready for review May 18, 2026 02:53
@Ravenwater Ravenwater merged commit c302eb3 into main May 18, 2026
32 checks passed
@Ravenwater Ravenwater deleted the feat/issue-855-erational-parse-pq-decimal-sci branch May 18, 2026 03:14
@github-project-automation github-project-automation Bot moved this from In progress to Done in Universal Number Library May 18, 2026
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 26010974299

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.04%) to 84.208%

Details

  • Coverage increased (+0.04%) from the base build.
  • Patch coverage: 56 of 56 lines across 1 file are fully covered (100%).
  • 2 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

2 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
include/sw/universal/number/erational/erational_impl.hpp 2 74.44%

Coverage Stats

Coverage Status
Relevant Lines: 55523
Covered Lines: 46755
Line Coverage: 84.21%
Coverage Strength: 6429108.7 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

feat(erational): extend parse to p/q form and decimal/scientific notation

2 participants