diff --git a/engine/src/Rational.zig b/engine/src/Rational.zig index a49c886..eeac848 100644 --- a/engine/src/Rational.zig +++ b/engine/src/Rational.zig @@ -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, .{}); +}