feat(erational): extend parse to p/q, decimal, and scientific notation#866
Conversation
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>
📝 WalkthroughWalkthroughExtended Changeserational parser extension to p/q, decimal, and scientific notation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
include/sw/universal/number/erational/erational_impl.hpp (1)
340-342: 💤 Low valueRemove the empty
public:section.The
public:access specifier on line 340 has no declarations beforeprotected: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
📒 Files selected for processing (2)
elastic/rational/decimal/conversion/string_parse.cppinclude/sw/universal/number/erational/erational_impl.hpp
Coverage Report for CI Build 26010974299Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.04%) to 84.208%Details
Uncovered ChangesNo uncovered changes found. Coverage Regressions2 previously-covered lines in 1 file lost coverage.
Coverage Stats
💛 - Coveralls |
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:
`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
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).
test from 2 groups to 11.
Test Results
Test plan
Resolves #855
Generated with Claude Code
Summary by CodeRabbit
Release Notes
Tests
New Features
Bug Fixes