add parseFraction to Rational struct (round trip toFractionString)

This commit is contained in:
Emil Lerch 2026-10-03 11:46:28 -07:00
parent bcdf36bd48
commit d36cf386ac
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -622,6 +622,45 @@ pub fn toFractionString(self: Rational, allocator: Allocator) Error![]u8 {
Error.OutOfMemory;
}
/// The inverse of `toFractionString`, at any size: `"-781250/12573"`, `"7"`.
///
/// `parse` caps literals at 512 digits, which is right for what a person types and
/// wrong for a value the engine produced itself: `factorial(1000)` has 2,568 digits and
/// has to come back exactly when a saved session is loaded. Only the canonical form is
/// accepted - an optional minus, digits, and optionally a slash and more digits - and
/// the result is reduced, so an unreduced fraction is still read as the right value.
pub fn parseFraction(allocator: Allocator, text: []const u8) Error!Rational {
const slash = std.mem.indexOfScalar(u8, text, '/');
const num_text = if (slash) |s| text[0..s] else text;
const den_text = if (slash) |s| text[s + 1 ..] else "1";
const magnitude = if (num_text.len > 0 and num_text[0] == '-') num_text[1..] else num_text;
if (!allDigits(magnitude) or !allDigits(den_text)) return Error.InvalidNumber;
var num = try Managed.init(allocator);
errdefer num.deinit();
try setDecimal(&num, num_text);
var den = try Managed.init(allocator);
errdefer den.deinit();
try setDecimal(&den, den_text);
return finish(allocator, &num, &den);
}
fn allDigits(text: []const u8) bool {
if (text.len == 0) return false;
for (text) |c| {
if (!std.ascii.isDigit(c)) return false;
}
return true;
}
fn setDecimal(value: *Managed, text: []const u8) Error!void {
value.setString(10, text) catch |err| return switch (err) {
error.OutOfMemory => Error.OutOfMemory,
// Unreachable after `allDigits`, and reported rather than asserted.
else => Error.InvalidNumber,
};
}
pub const DecimalResult = struct {
/// Decimal text. Caller owns the memory.
text: []u8,
@ -2124,3 +2163,43 @@ fn bodyScientificRendering(a: Allocator) anyerror!void {
test "OOM safety: scientific rendering" {
try oomSweep(bodyScientificRendering);
}
// -- parseFraction --
test "parseFraction reads back what toFractionString writes, at any size" {
for ([_][]const u8{ "0", "7", "-7", "1/3", "-1/3", "781250/12573" }) |text| {
var r = try parseFraction(alloc, text);
defer r.deinit();
try expectFrac(text, r);
}
// Far past `parse`'s 512-digit limit: a value the engine can produce must load.
const digits = "9" ** 3000;
// Over 2, because 10^3000 - 1 is odd: over 7 it would reduce, since 7 divides it.
var big = try parseFraction(alloc, digits ++ "/2");
defer big.deinit();
const back = try big.toFractionString(alloc);
defer alloc.free(back);
try testing.expectEqualStrings(digits ++ "/2", back);
try testing.expectError(Error.TooManyDigits, parse(alloc, digits));
}
test "parseFraction reduces, and refuses anything but the canonical form" {
var half = try parseFraction(alloc, "2/4");
defer half.deinit();
try expectFrac("1/2", half);
try testing.expectError(Error.DivisionByZero, parseFraction(alloc, "1/0"));
for ([_][]const u8{ "", "-", "1/", "/2", "1/-2", "+1", "1.5", "1e3", "1 / 2", "0x10", "--1" }) |text| {
try testing.expectError(Error.InvalidNumber, parseFraction(alloc, text));
}
}
test "parseFraction releases everything on any allocation failure" {
try testing.checkAllAllocationFailures(testing.allocator, struct {
fn run(a: Allocator) !void {
var r = try parseFraction(a, "-123456789012345678901234567890/98765432109876543210");
r.deinit();
}
}.run, .{});
}