Add Rational.from-string - #4
Conversation
Parses a `Rational` from text — a fraction like `3/4` or `-2/7`, or a bare integer like `5` — the inverse of what `str` prints. Delegates to `new`, so the result is reduced to lowest terms with the sign normalized onto the numerator. Returns `Nothing` for malformed input, an empty numerator or denominator, or a zero denominator (which would otherwise divide by zero inside `new`). Implements the `from-string` interface. 14 new tests cover fractions, reduction, sign normalization, bare integers, and every rejection case.
There was a problem hiding this comment.
Build & Tests
Checked out claude/from-string (1c2de42) and ran carp -x tests/rational.carp locally: 64/64 pass, matching the PR. CI is green on both platforms. The 14 new tests cover every documented case (fractions, reduction, sign normalization on either part, zero numerator, bare integers, and each rejection path).
Findings
I read the implementation and probed a batch of inputs beyond the test suite. The core logic is correct: it delegates to new (so reduction + sign normalization are inherited, not re-implemented), guards empty numerator/denominator explicitly, and rejects a zero denominator before it can reach the division inside new. Malformed input is reliably rejected — "3abc/4", "3/4abc", "3.5", "0x10", "/", "3//4", "3/4/5" all → Nothing, because Int.from-string refuses trailing non-digits.
Everything I found that's mildly surprising is inherited from Int.from-string, not introduced here, and is shared with the existing Semver.from-string / the rest of the ecosystem:
- Leniency: leading whitespace and a leading
+are accepted (" 3/4"→3/4,"+5"→5/1,"3/+4"→3/4). Trailing whitespace is not ("3/4 "→Nothing), so there's a small asymmetry. Harmless. - Silent overflow (worth a mention): an out-of-range integer saturates instead of failing —
"9999999999/1"parses to2147483647/1(INT_MAX) rather thanNothing. The doc lists theNothingcases as malformed/empty/zero-denominator, and an overflowing value silently producing a wrong number is a footgun. But this is entirelyInt.from-string's behavior (the same thing happens today inSemver.from-string), so it's out of scope for this PR — flagging only for awareness, not as a change request.
No changelog file exists in the repo, and the generated docs/Rational.html is updated, so documentation is covered.
Verdict: merge
Correct, cleanly delegated, and thoroughly tested. from-string is the natural inverse of str and the rejection paths all hold up. The overflow note is an ecosystem-wide Int.from-string property, not a defect in this change.
Rationalhadfrom-int,from-float,from-double, andstr, but no way toparse a
Rationalback from its textual form.from-stringis the naturalinverse of
strand a common need (config values, CLI arguments, serializeddata).
Behavior
"3/4","-2/7","6/-8"(sign may sit on either part)"5","-5"→n/1Rational.new, so results are reduced to lowest terms with thesign normalized onto the numerator (
"6/-8"→-3/4,"6/8"→3/4)Nothingfor: the empty string, an empty numerator ("/4") ordenominator (
"3/"), a zero denominator ("3/0"— which would otherwisedivide by zero inside
new), non-numeric tokens ("a/b"), and too manycomponents (
"3/4/5")from-stringinterfaceTests
14 new assertions in
tests/rational.carpcover simple fractions, reduction,sign normalization, a zero numerator, bare integers, and each rejection case.
Full suite is 64/64;
carp-fmt --check,angler, andgendocsare cleanlocally.
Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.