//! Exact rational arithmetic for Tally. //! //! A `Rational` is `num / den` over arbitrary-precision integers, always kept //! reduced with `den > 0`. Addition, subtraction, multiplication, division and //! integer powers are closed and exact: no rounding occurs anywhere. //! //! This is the "exact" tier of the numeric model described in design.md 2.7. //! Rationals cannot represent irrational results (`sqrt(2)`, `pi`), which is why //! `number.zig` layers a float fallback on top. //! //! Why rationals rather than a decimal type: every decimal value is a rational, //! so rationals strictly subsume decimal128 in expressiveness, and unlike decimal //! they never round (`1/3` is exact, and `(1/3) * 3` is exactly 1). See //! design.md 2.7.3. //! //! API style: operations return a fresh `Rational` and take an allocator, which //! suits the evaluator's arena. Callers not using an arena must `deinit`. const std = @import("std"); const Allocator = std.mem.Allocator; const Managed = std.math.big.int.Managed; const Order = std.math.Order; pub const Error = error{ OutOfMemory, DivisionByZero, InvalidNumber, /// The exponent of an integer power did not fit the supported range. ExponentTooLarge, }; pub const Rational = struct { /// Numerator. Carries the sign of the value. num: Managed, /// Denominator. Always strictly positive. den: Managed, // -- Construction -- /// Zero (`0/1`). pub fn initZero(allocator: Allocator) Error!Rational { return initInt(allocator, 0); } /// An integer value (`value/1`). pub fn initInt(allocator: Allocator, value: anytype) Error!Rational { // Built separately rather than inside a struct literal: if the second // allocation fails, the first must still be released. var num = try Managed.initSet(allocator, value); errdefer num.deinit(); const den = try Managed.initSet(allocator, 1); return .{ .num = num, .den = den }; } /// A ratio, reduced on construction. Errors if `den` is zero. pub fn initRatio(allocator: Allocator, num: anytype, den: anytype) Error!Rational { // Built separately rather than inside a struct literal: if the second // allocation fails, the first must still be released, and an errdefer // cannot cover a value that does not exist yet. var n = try Managed.initSet(allocator, num); errdefer n.deinit(); var d = try Managed.initSet(allocator, den); errdefer d.deinit(); return finish(allocator, &n, &d); } /// Assemble a reduced `Rational` from a numerator and denominator. /// /// Ownership transfers ONLY on success. Both parts are taken by pointer and /// normalized in place, so on failure the caller's `errdefer` still sees /// live, current values and frees them exactly once. /// /// Taking them by value would be unsound twice over: the caller's errdefer /// and this function would both free them (a double free), and normalization /// can reallocate limbs, which would leave the caller's copy dangling. fn finish(allocator: Allocator, num: *Managed, den: *Managed) Error!Rational { if (den.eqlZero()) return Error.DivisionByZero; try normalizeParts(allocator, num, den); return .{ .num = num.*, .den = den.* }; } pub fn deinit(self: *Rational) void { self.num.deinit(); self.den.deinit(); } pub fn clone(self: Rational) Error!Rational { return self.cloneWith(self.num.allocator); } /// Copy into a different allocator. /// /// Needed because a `Rational` carries its allocator inside its limbs: a /// value stored in the environment must be copied into an evaluation's /// scratch arena (and vice versa) rather than shared, or one side will free /// memory the other still refers to. pub fn cloneWith(self: Rational, allocator: Allocator) Error!Rational { var num = try self.num.cloneWithDifferentAllocator(allocator); errdefer num.deinit(); const den = try self.den.cloneWithDifferentAllocator(allocator); return .{ .num = num, .den = den }; } /// Reduce by the GCD and force the sign onto the numerator, in place. fn normalizeParts(allocator: Allocator, num: *Managed, den: *Managed) Error!void { if (num.eqlZero()) { try den.set(1); num.setSign(true); return; } // Move the sign to the numerator. if (!den.isPositive()) { num.negate(); den.negate(); } var g = try Managed.init(allocator); defer g.deinit(); try g.gcd(num, den); // Nothing to do when already coprime. if (g.toConst().orderAgainstScalar(1) == .eq) return; var q = try Managed.init(allocator); defer q.deinit(); var r = try Managed.init(allocator); defer r.deinit(); try q.divFloor(&r, num, &g); try num.copy(q.toConst()); try q.divFloor(&r, den, &g); try den.copy(q.toConst()); } // -- Parsing -- /// Parse exact numeric text, accepting either a decimal literal or a /// fraction: `"0.0254"`, `"-1.25e3"`, `"5/9"`, `"463/900"`. /// /// The fraction form exists because several exact conversion factors have no /// terminating decimal expansion: Fahrenheit's is exactly 5/9 and a knot's is /// exactly 463/900. Writing those as rounded decimals would defeat the point. pub fn parse(allocator: Allocator, text: []const u8) Error!Rational { const slash = std.mem.indexOfScalar(u8, text, '/') orelse return parseDecimal(allocator, text); var numerator = try parseDecimal(allocator, text[0..slash]); defer numerator.deinit(); var denominator = try parseDecimal(allocator, text[slash + 1 ..]); defer denominator.deinit(); return div(allocator, numerator, denominator); } /// Parse a decimal numeric literal exactly. /// /// Accepts an optional sign, digits with an optional fractional part, and an /// optional decimal exponent: `42`, `-1.25`, `1e5`, `1.5e-3`. Underscores, /// commas and spaces are ignored as digit separators, matching the /// tokenizer's accepted forms. /// /// The result is exact: `0.1` becomes `1/10`, not the nearest binary float. pub fn parseDecimal(allocator: Allocator, text: []const u8) Error!Rational { var buf: [512]u8 = undefined; var len: usize = 0; for (text) |c| { if (c == '_' or c == ',' or c == ' ') continue; if (len >= buf.len) return Error.InvalidNumber; buf[len] = c; len += 1; } var s = buf[0..len]; if (s.len == 0) return Error.InvalidNumber; var negative = false; if (s[0] == '+' or s[0] == '-') { negative = s[0] == '-'; s = s[1..]; if (s.len == 0) return Error.InvalidNumber; } // Split off the exponent. var exponent: i64 = 0; var mantissa = s; if (std.mem.indexOfAny(u8, s, "eE")) |e_idx| { mantissa = s[0..e_idx]; const exp_text = s[e_idx + 1 ..]; if (exp_text.len == 0) return Error.InvalidNumber; exponent = std.fmt.parseInt(i64, exp_text, 10) catch return Error.InvalidNumber; if (exponent > 100_000 or exponent < -100_000) return Error.ExponentTooLarge; } if (mantissa.len == 0) return Error.InvalidNumber; // Split the mantissa on the decimal point. var digits_buf: [512]u8 = undefined; var digits_len: usize = 0; var frac_digits: usize = 0; var seen_dot = false; for (mantissa) |c| { if (c == '.') { if (seen_dot) return Error.InvalidNumber; seen_dot = true; continue; } if (c < '0' or c > '9') return Error.InvalidNumber; if (digits_len >= digits_buf.len) return Error.InvalidNumber; digits_buf[digits_len] = c; digits_len += 1; if (seen_dot) frac_digits += 1; } if (digits_len == 0) return Error.InvalidNumber; var num = try Managed.init(allocator); errdefer num.deinit(); num.setString(10, digits_buf[0..digits_len]) catch return Error.InvalidNumber; var den = try Managed.initSet(allocator, 1); errdefer den.deinit(); // value = digits / 10^frac_digits * 10^exponent const net_exp: i64 = exponent - @as(i64, @intCast(frac_digits)); if (net_exp != 0) { const magnitude: u64 = @intCast(@abs(net_exp)); if (magnitude > std.math.maxInt(u32)) return Error.ExponentTooLarge; var scale = try Managed.initSet(allocator, 10); defer scale.deinit(); try scale.pow(&scale, @intCast(magnitude)); if (net_exp > 0) { try num.mul(&num, &scale); } else { try den.mul(&den, &scale); } } if (negative) num.negate(); return finish(allocator, &num, &den); } // -- Predicates -- pub fn isZero(self: Rational) bool { return self.num.eqlZero(); } pub fn isNegative(self: Rational) bool { return !self.num.isPositive() and !self.num.eqlZero(); } /// True when the denominator is 1, i.e. the value is a whole number. pub fn isInteger(self: Rational) bool { return self.den.toConst().orderAgainstScalar(1) == .eq; } /// Size of the denominator in bits, used for the demotion policy. pub fn denBitCount(self: Rational) usize { return self.den.bitCountAbs(); } /// True when the value has a terminating decimal expansion, i.e. the /// denominator's only prime factors are 2 and 5. Such values print exactly. pub fn isTerminating(self: Rational, allocator: Allocator) Error!bool { var d = try self.den.clone(); defer d.deinit(); var q = try Managed.init(allocator); defer q.deinit(); var r = try Managed.init(allocator); defer r.deinit(); for ([_]u8{ 2, 5 }) |factor| { var f = try Managed.initSet(allocator, factor); defer f.deinit(); while (true) { try q.divFloor(&r, &d, &f); if (!r.eqlZero()) break; try d.copy(q.toConst()); } } return d.toConst().orderAgainstScalar(1) == .eq; } // -- Comparison -- /// Compare two rationals: `a <=> b`. pub fn order(allocator: Allocator, a: Rational, b: Rational) Error!Order { // a/b vs c/d -> a*d vs c*b (denominators are positive, so the // inequality direction is preserved) var left = try Managed.init(allocator); defer left.deinit(); var right = try Managed.init(allocator); defer right.deinit(); try left.mul(&a.num, &b.den); try right.mul(&b.num, &a.den); return left.order(right); } pub fn eql(allocator: Allocator, a: Rational, b: Rational) Error!bool { return (try order(allocator, a, b)) == .eq; } // -- Arithmetic -- pub fn add(allocator: Allocator, a: Rational, b: Rational) Error!Rational { // a/b + c/d = (a*d + c*b) / (b*d) var left = try Managed.init(allocator); defer left.deinit(); var right = try Managed.init(allocator); defer right.deinit(); try left.mul(&a.num, &b.den); try right.mul(&b.num, &a.den); var num = try Managed.init(allocator); errdefer num.deinit(); try num.add(&left, &right); var den = try Managed.init(allocator); errdefer den.deinit(); try den.mul(&a.den, &b.den); return finish(allocator, &num, &den); } pub fn sub(allocator: Allocator, a: Rational, b: Rational) Error!Rational { var left = try Managed.init(allocator); defer left.deinit(); var right = try Managed.init(allocator); defer right.deinit(); try left.mul(&a.num, &b.den); try right.mul(&b.num, &a.den); var num = try Managed.init(allocator); errdefer num.deinit(); try num.sub(&left, &right); var den = try Managed.init(allocator); errdefer den.deinit(); try den.mul(&a.den, &b.den); return finish(allocator, &num, &den); } pub fn mul(allocator: Allocator, a: Rational, b: Rational) Error!Rational { var num = try Managed.init(allocator); errdefer num.deinit(); try num.mul(&a.num, &b.num); var den = try Managed.init(allocator); errdefer den.deinit(); try den.mul(&a.den, &b.den); return finish(allocator, &num, &den); } pub fn div(allocator: Allocator, a: Rational, b: Rational) Error!Rational { if (b.isZero()) return Error.DivisionByZero; var num = try Managed.init(allocator); errdefer num.deinit(); try num.mul(&a.num, &b.den); var den = try Managed.init(allocator); errdefer den.deinit(); try den.mul(&a.den, &b.num); return finish(allocator, &num, &den); } pub fn negate(allocator: Allocator, a: Rational) Error!Rational { var result = try a.clone(); errdefer result.deinit(); result.num.negate(); _ = allocator; return result; } pub fn abs(allocator: Allocator, a: Rational) Error!Rational { var result = try a.clone(); errdefer result.deinit(); result.num.abs(); _ = allocator; return result; } /// Raise to an integer power. A negative exponent takes the reciprocal, /// which is exact for rationals. pub fn powInt(allocator: Allocator, a: Rational, exponent: i64) Error!Rational { if (exponent == 0) return initInt(allocator, 1); const magnitude: u64 = @intCast(@abs(exponent)); // Guard against absurd exponents that would exhaust memory. 2^(2^20) // is already a 128 KiB integer. if (magnitude > 1_000_000) return Error.ExponentTooLarge; const e: u32 = @intCast(magnitude); if (a.isZero()) { if (exponent < 0) return Error.DivisionByZero; return initInt(allocator, 0); } var num = try a.num.clone(); errdefer num.deinit(); var den = try a.den.clone(); errdefer den.deinit(); try num.pow(&num, e); try den.pow(&den, e); if (exponent < 0) { // Reciprocal: swap, then let normalize fix the sign. return finish(allocator, &den, &num); } return finish(allocator, &num, &den); } /// Exact square root, or null when the value is not a perfect square of a /// rational. Used to keep `sqrt(4)` exact while `sqrt(2)` falls back. pub fn sqrtExact(allocator: Allocator, a: Rational) Error!?Rational { if (a.isNegative()) return null; if (a.isZero()) return try initInt(allocator, 0); var num_root = try Managed.init(allocator); errdefer num_root.deinit(); var den_root = try Managed.init(allocator); errdefer den_root.deinit(); // Guarded by the isNegative check above, so a negative input is impossible. num_root.sqrt(&a.num) catch |err| switch (err) { error.SqrtOfNegativeNumber => unreachable, else => |e| return e, }; den_root.sqrt(&a.den) catch |err| switch (err) { error.SqrtOfNegativeNumber => unreachable, else => |e| return e, }; // Verify: big.int sqrt truncates, so square the roots and compare. var check = try Managed.init(allocator); defer check.deinit(); try check.mul(&num_root, &num_root); if (!check.eql(a.num)) { num_root.deinit(); den_root.deinit(); return null; } try check.mul(&den_root, &den_root); if (!check.eql(a.den)) { num_root.deinit(); den_root.deinit(); return null; } return try finish(allocator, &num_root, &den_root); } /// Largest integer not greater than the value. pub fn floor(allocator: Allocator, a: Rational) Error!Rational { if (a.isInteger()) return a.clone(); var q = try Managed.init(allocator); errdefer q.deinit(); var r = try Managed.init(allocator); defer r.deinit(); // divFloor rounds the quotient toward negative infinity, which is // exactly floor for a positive denominator (an invariant here). try q.divFloor(&r, &a.num, &a.den); var den = try Managed.initSet(allocator, 1); errdefer den.deinit(); return finish(allocator, &q, &den); } /// Smallest integer not less than the value. pub fn ceil(allocator: Allocator, a: Rational) Error!Rational { if (a.isInteger()) return a.clone(); var f = try floor(allocator, a); errdefer f.deinit(); // Not an integer, so ceil is always floor + 1. try f.num.addScalar(&f.num, 1); return f; } /// Round to the nearest integer, halves away from zero (matching `@round`). pub fn round(allocator: Allocator, a: Rational) Error!Rational { if (a.isInteger()) return a.clone(); var half = try initRatio(allocator, 1, 2); defer half.deinit(); if (a.isNegative()) { var shifted = try sub(allocator, a, half); defer shifted.deinit(); return ceil(allocator, shifted); } var shifted = try add(allocator, a, half); defer shifted.deinit(); return floor(allocator, shifted); } /// Exact integer remainder matching `@mod`: the result takes the sign of /// the divisor, and equals `a - b * floor(a / b)`. pub fn mod(allocator: Allocator, a: Rational, b: Rational) Error!Rational { if (b.isZero()) return Error.DivisionByZero; var quotient = try div(allocator, a, b); defer quotient.deinit(); var floored = try floor(allocator, quotient); defer floored.deinit(); var scaled = try mul(allocator, floored, b); defer scaled.deinit(); return sub(allocator, a, scaled); } /// Exact factorial. Unbounded, unlike the f64 version which overflows past /// 170. pub fn factorial(allocator: Allocator, n: u64) Error!Rational { // A guard against absurd inputs that would take effectively forever; // 20000! is already a ~78000-digit number. if (n > 20_000) return Error.ExponentTooLarge; var acc = try Managed.initSet(allocator, 1); errdefer acc.deinit(); var i: u64 = 2; while (i <= n) : (i += 1) { var factor = try Managed.initSet(allocator, i); defer factor.deinit(); try acc.mul(&acc, &factor); } var den = try Managed.initSet(allocator, 1); errdefer den.deinit(); return finish(allocator, &acc, &den); } // -- Conversion -- /// Convert to the nearest f64. /// /// Scales the numerator so the quotient carries enough bits to round /// correctly, rather than dividing two separately-rounded floats (which /// would lose precision twice and overflow for large values). pub fn toFloat(self: Rational, allocator: Allocator) f64 { if (self.isZero()) return 0; const negative = self.isNegative(); var num = self.num.clone() catch return std.math.nan(f64); defer { var n = num; n.deinit(); } num.abs(); const num_bits: i64 = @intCast(num.bitCountAbs()); const den_bits: i64 = @intCast(self.den.bitCountAbs()); // Aim for ~72 significant bits in the quotient, comfortably more than // f64's 53, so a single final rounding is correct. const target: i64 = 72; const shift: i64 = target - (num_bits - den_bits); var scaled = Managed.init(allocator) catch return std.math.nan(f64); defer scaled.deinit(); if (shift > 0) { if (shift > 1 << 20) return if (negative) -0.0 else 0.0; scaled.shiftLeft(&num, @intCast(shift)) catch return std.math.nan(f64); } else { scaled.shiftRight(&num, @intCast(-shift)) catch return std.math.nan(f64); } var q = Managed.init(allocator) catch return std.math.nan(f64); defer q.deinit(); var r = Managed.init(allocator) catch return std.math.nan(f64); defer r.deinit(); q.divFloor(&r, &scaled, &self.den) catch return std.math.nan(f64); const quotient, _ = q.toFloat(f64, .nearest_even); const exponent: i32 = @intCast(-shift); const result = std.math.ldexp(quotient, exponent); return if (negative) -result else result; } /// Render as an exact fraction, e.g. "781250/12573" or "7" for integers. /// Caller owns the returned memory. pub fn toFractionString(self: Rational, allocator: Allocator) Error![]u8 { const num_str = try decimalDigits(self.num, allocator); defer allocator.free(num_str); if (self.isInteger()) return allocator.dupe(u8, num_str); const den_str = try decimalDigits(self.den, allocator); defer allocator.free(den_str); return std.fmt.allocPrint(allocator, "{s}/{s}", .{ num_str, den_str }) catch Error.OutOfMemory; } pub const DecimalResult = struct { /// Decimal text. Caller owns the memory. text: []u8, /// False when the expansion was truncated at `max_digits` and rounded, /// i.e. the text is an approximation of the exact value. exact: bool, }; /// Render as decimal text by long division. /// /// Terminating expansions print exactly and report `exact = true`. Repeating /// expansions are rounded half-up at `max_digits` fractional digits and /// report `exact = false`, so a frontend can mark them as approximate. pub fn toDecimalString(self: Rational, allocator: Allocator, max_digits: usize) Error!DecimalResult { var out = std.ArrayList(u8).empty; errdefer out.deinit(allocator); if (self.isNegative()) try out.append(allocator, '-'); var work = try self.num.clone(); defer work.deinit(); work.abs(); var q = try Managed.init(allocator); defer q.deinit(); var rem = try Managed.init(allocator); defer rem.deinit(); // Integer part. try q.divFloor(&rem, &work, &self.den); const int_str = try decimalDigits(q, allocator); defer allocator.free(int_str); try out.appendSlice(allocator, int_str); if (rem.eqlZero()) { return .{ .text = try out.toOwnedSlice(allocator), .exact = true }; } try out.append(allocator, '.'); var ten = try Managed.initSet(allocator, 10); defer ten.deinit(); var digits: usize = 0; var exact = true; while (digits < max_digits) { try rem.mul(&rem, &ten); try q.divFloor(&rem, &rem, &self.den); const digit = q.toInt(u8) catch 0; try out.append(allocator, '0' + digit); digits += 1; if (rem.eqlZero()) break; } else { // Ran out of digits with a remainder left: the text is truncated. exact = false; } if (!exact) { // Round half-up: compare 2*remainder against the denominator. var doubled = try Managed.init(allocator); defer doubled.deinit(); var two = try Managed.initSet(allocator, 2); defer two.deinit(); try doubled.mul(&rem, &two); if (doubled.order(self.den) != .lt) { roundUpDecimal(out.items); } } // Trim any trailing zeros produced by an exact expansion. if (exact) { var end = out.items.len; while (end > 0 and out.items[end - 1] == '0') end -= 1; if (end > 0 and out.items[end - 1] == '.') end -= 1; out.shrinkRetainingCapacity(end); } return .{ .text = try out.toOwnedSlice(allocator), .exact = exact }; } }; /// Base-10 digits of a big integer. Base 10 is always valid, so the only real /// failure mode is allocation. fn decimalDigits(m: Managed, allocator: Allocator) Error![]u8 { return m.toString(allocator, 10, .lower) catch |err| switch (err) { error.InvalidBase => unreachable, else => |e| return e, }; } /// Propagate a half-up rounding carry through a decimal string in place. /// Only called when the digits produced can absorb the carry, which is /// guaranteed here because the integer part is present. fn roundUpDecimal(text: []u8) void { var i = text.len; while (i > 0) { i -= 1; const c = text[i]; if (c == '.' or c == '-') continue; if (c != '9') { text[i] = c + 1; return; } text[i] = '0'; } } // -- Tests -- const testing = std.testing; const alloc = testing.allocator; fn expectFrac(expected: []const u8, r: Rational) !void { const s = try r.toFractionString(alloc); defer alloc.free(s); try testing.expectEqualStrings(expected, s); } fn expectDecimal(expected: []const u8, expected_exact: bool, r: Rational, digits: usize) !void { const d = try r.toDecimalString(alloc, digits); defer alloc.free(d.text); try testing.expectEqualStrings(expected, d.text); try testing.expectEqual(expected_exact, d.exact); } test "initInt and fraction rendering" { var a = try Rational.initInt(alloc, 42); defer a.deinit(); try expectFrac("42", a); try testing.expect(a.isInteger()); try testing.expect(!a.isZero()); } test "initZero" { var z = try Rational.initZero(alloc); defer z.deinit(); try testing.expect(z.isZero()); try testing.expect(z.isInteger()); try expectFrac("0", z); } test "initRatio reduces on construction" { var a = try Rational.initRatio(alloc, 6, 8); defer a.deinit(); try expectFrac("3/4", a); } test "initRatio normalizes a negative denominator onto the numerator" { var a = try Rational.initRatio(alloc, 1, -2); defer a.deinit(); try expectFrac("-1/2", a); try testing.expect(a.isNegative()); } test "initRatio rejects a zero denominator" { try testing.expectError(Error.DivisionByZero, Rational.initRatio(alloc, 1, 0)); } test "initRatio reduces to an integer" { var a = try Rational.initRatio(alloc, 10, 5); defer a.deinit(); try expectFrac("2", a); try testing.expect(a.isInteger()); } test "parseDecimal: plain integer" { var a = try Rational.parseDecimal(alloc, "1234"); defer a.deinit(); try expectFrac("1234", a); } test "parseDecimal: 0.1 is exactly one tenth" { var a = try Rational.parseDecimal(alloc, "0.1"); defer a.deinit(); try expectFrac("1/10", a); } test "parseDecimal: negative decimal" { var a = try Rational.parseDecimal(alloc, "-1.25"); defer a.deinit(); try expectFrac("-5/4", a); } test "parseDecimal: exponent forms" { var a = try Rational.parseDecimal(alloc, "1e5"); defer a.deinit(); try expectFrac("100000", a); var b = try Rational.parseDecimal(alloc, "1.5e-3"); defer b.deinit(); try expectFrac("3/2000", b); var c = try Rational.parseDecimal(alloc, "2.5E2"); defer c.deinit(); try expectFrac("250", c); } test "parseDecimal: separators are ignored" { var a = try Rational.parseDecimal(alloc, "1_000_000"); defer a.deinit(); try expectFrac("1000000", a); var b = try Rational.parseDecimal(alloc, "1,000"); defer b.deinit(); try expectFrac("1000", b); } test "parseDecimal: integer beyond f64 precision stays exact" { // This is the value standard mode currently mangles. var a = try Rational.parseDecimal(alloc, "9007199254740993"); defer a.deinit(); try expectFrac("9007199254740993", a); } test "parseDecimal: very large integer" { var a = try Rational.parseDecimal(alloc, "99999999999999999999999999"); defer a.deinit(); try expectFrac("99999999999999999999999999", a); } test "parseDecimal: rejects malformed input" { try testing.expectError(Error.InvalidNumber, Rational.parseDecimal(alloc, "")); try testing.expectError(Error.InvalidNumber, Rational.parseDecimal(alloc, "1.2.3")); try testing.expectError(Error.InvalidNumber, Rational.parseDecimal(alloc, "abc")); try testing.expectError(Error.InvalidNumber, Rational.parseDecimal(alloc, "-")); try testing.expectError(Error.InvalidNumber, Rational.parseDecimal(alloc, "1e")); } test "add: the classic binary float failure is exact here" { var a = try Rational.parseDecimal(alloc, "0.1"); defer a.deinit(); var b = try Rational.parseDecimal(alloc, "0.2"); defer b.deinit(); var sum = try Rational.add(alloc, a, b); defer sum.deinit(); try expectFrac("3/10", sum); try expectDecimal("0.3", true, sum, 20); } test "add: 1.1 + 2.2" { var a = try Rational.parseDecimal(alloc, "1.1"); defer a.deinit(); var b = try Rational.parseDecimal(alloc, "2.2"); defer b.deinit(); var sum = try Rational.add(alloc, a, b); defer sum.deinit(); try expectFrac("33/10", sum); try expectDecimal("3.3", true, sum, 20); } test "add: unlike denominators" { var a = try Rational.initRatio(alloc, 1, 3); defer a.deinit(); var b = try Rational.initRatio(alloc, 1, 6); defer b.deinit(); var sum = try Rational.add(alloc, a, b); defer sum.deinit(); try expectFrac("1/2", sum); } test "sub" { var a = try Rational.initRatio(alloc, 1, 2); defer a.deinit(); var b = try Rational.initRatio(alloc, 1, 3); defer b.deinit(); var d = try Rational.sub(alloc, a, b); defer d.deinit(); try expectFrac("1/6", d); } test "sub to zero normalizes the denominator" { var a = try Rational.initRatio(alloc, 3, 7); defer a.deinit(); var d = try Rational.sub(alloc, a, a); defer d.deinit(); try testing.expect(d.isZero()); try expectFrac("0", d); } test "mul" { var a = try Rational.initRatio(alloc, 2, 3); defer a.deinit(); var b = try Rational.initRatio(alloc, 3, 4); defer b.deinit(); var p = try Rational.mul(alloc, a, b); defer p.deinit(); try expectFrac("1/2", p); } test "mul: one third times three is exactly one" { var a = try Rational.initRatio(alloc, 1, 3); defer a.deinit(); var three = try Rational.initInt(alloc, 3); defer three.deinit(); var p = try Rational.mul(alloc, a, three); defer p.deinit(); try expectFrac("1", p); try testing.expect(p.isInteger()); } test "mul: 0.1 * 3" { var a = try Rational.parseDecimal(alloc, "0.1"); defer a.deinit(); var three = try Rational.initInt(alloc, 3); defer three.deinit(); var p = try Rational.mul(alloc, a, three); defer p.deinit(); try expectDecimal("0.3", true, p, 20); } test "div" { var a = try Rational.initInt(alloc, 1); defer a.deinit(); var b = try Rational.initInt(alloc, 3); defer b.deinit(); var q = try Rational.div(alloc, a, b); defer q.deinit(); try expectFrac("1/3", q); } test "div by zero errors" { var a = try Rational.initInt(alloc, 1); defer a.deinit(); var z = try Rational.initZero(alloc); defer z.deinit(); try testing.expectError(Error.DivisionByZero, Rational.div(alloc, a, z)); } test "div by a negative keeps the sign on the numerator" { var a = try Rational.initInt(alloc, 1); defer a.deinit(); var b = try Rational.initInt(alloc, -3); defer b.deinit(); var q = try Rational.div(alloc, a, b); defer q.deinit(); try expectFrac("-1/3", q); } test "negate and abs" { var a = try Rational.initRatio(alloc, 3, 4); defer a.deinit(); var n = try Rational.negate(alloc, a); defer n.deinit(); try expectFrac("-3/4", n); var b = try Rational.abs(alloc, n); defer b.deinit(); try expectFrac("3/4", b); } test "powInt: positive exponent" { var a = try Rational.initInt(alloc, 2); defer a.deinit(); var p = try Rational.powInt(alloc, a, 10); defer p.deinit(); try expectFrac("1024", p); } test "powInt: 2^53 + 1 territory stays exact" { var two = try Rational.initInt(alloc, 2); defer two.deinit(); var p = try Rational.powInt(alloc, two, 53); defer p.deinit(); var one = try Rational.initInt(alloc, 1); defer one.deinit(); var sum = try Rational.add(alloc, p, one); defer sum.deinit(); try expectFrac("9007199254740993", sum); } test "powInt: negative exponent takes the reciprocal" { var a = try Rational.initInt(alloc, 2); defer a.deinit(); var p = try Rational.powInt(alloc, a, -3); defer p.deinit(); try expectFrac("1/8", p); } test "powInt: zero exponent is one" { var a = try Rational.initRatio(alloc, 7, 9); defer a.deinit(); var p = try Rational.powInt(alloc, a, 0); defer p.deinit(); try expectFrac("1", p); } test "powInt: rational base" { var a = try Rational.initRatio(alloc, 2, 3); defer a.deinit(); var p = try Rational.powInt(alloc, a, 3); defer p.deinit(); try expectFrac("8/27", p); } test "powInt: zero base" { var z = try Rational.initZero(alloc); defer z.deinit(); var p = try Rational.powInt(alloc, z, 5); defer p.deinit(); try testing.expect(p.isZero()); try testing.expectError(Error.DivisionByZero, Rational.powInt(alloc, z, -1)); } test "powInt: rejects absurd exponents" { var a = try Rational.initInt(alloc, 2); defer a.deinit(); try testing.expectError(Error.ExponentTooLarge, Rational.powInt(alloc, a, 2_000_000)); } test "powInt: negative base with odd and even exponents" { var a = try Rational.initInt(alloc, -2); defer a.deinit(); var odd = try Rational.powInt(alloc, a, 3); defer odd.deinit(); try expectFrac("-8", odd); var even = try Rational.powInt(alloc, a, 2); defer even.deinit(); try expectFrac("4", even); } test "sqrtExact: perfect squares stay exact" { var four = try Rational.initInt(alloc, 4); defer four.deinit(); var r = (try Rational.sqrtExact(alloc, four)).?; defer r.deinit(); try expectFrac("2", r); } test "sqrtExact: perfect rational square" { var a = try Rational.initRatio(alloc, 9, 16); defer a.deinit(); var r = (try Rational.sqrtExact(alloc, a)).?; defer r.deinit(); try expectFrac("3/4", r); } test "sqrtExact: non-squares return null so the caller can fall back" { var two = try Rational.initInt(alloc, 2); defer two.deinit(); try testing.expect((try Rational.sqrtExact(alloc, two)) == null); var a = try Rational.initRatio(alloc, 1, 3); defer a.deinit(); try testing.expect((try Rational.sqrtExact(alloc, a)) == null); } test "sqrtExact: zero and negatives" { var z = try Rational.initZero(alloc); defer z.deinit(); var r = (try Rational.sqrtExact(alloc, z)).?; defer r.deinit(); try testing.expect(r.isZero()); var neg = try Rational.initInt(alloc, -4); defer neg.deinit(); try testing.expect((try Rational.sqrtExact(alloc, neg)) == null); } test "order and eql" { var a = try Rational.initRatio(alloc, 1, 3); defer a.deinit(); var b = try Rational.initRatio(alloc, 1, 2); defer b.deinit(); try testing.expectEqual(Order.lt, try Rational.order(alloc, a, b)); try testing.expectEqual(Order.gt, try Rational.order(alloc, b, a)); try testing.expectEqual(Order.eq, try Rational.order(alloc, a, a)); try testing.expect(try Rational.eql(alloc, a, a)); try testing.expect(!try Rational.eql(alloc, a, b)); } test "order across signs" { var neg = try Rational.initInt(alloc, -1); defer neg.deinit(); var pos = try Rational.initRatio(alloc, 1, 1000); defer pos.deinit(); try testing.expectEqual(Order.lt, try Rational.order(alloc, neg, pos)); } test "order recognizes equal values with different representations" { var a = try Rational.initRatio(alloc, 2, 4); defer a.deinit(); var b = try Rational.initRatio(alloc, 50, 100); defer b.deinit(); try testing.expect(try Rational.eql(alloc, a, b)); } test "isTerminating" { var tenth = try Rational.parseDecimal(alloc, "0.1"); defer tenth.deinit(); try testing.expect(try tenth.isTerminating(alloc)); var eighth = try Rational.initRatio(alloc, 1, 8); defer eighth.deinit(); try testing.expect(try eighth.isTerminating(alloc)); var third = try Rational.initRatio(alloc, 1, 3); defer third.deinit(); try testing.expect(!try third.isTerminating(alloc)); var seventh = try Rational.initRatio(alloc, 1, 7); defer seventh.deinit(); try testing.expect(!try seventh.isTerminating(alloc)); var whole = try Rational.initInt(alloc, 5); defer whole.deinit(); try testing.expect(try whole.isTerminating(alloc)); } test "toDecimalString: terminating expansions are exact" { var a = try Rational.initRatio(alloc, 1, 8); defer a.deinit(); try expectDecimal("0.125", true, a, 20); var b = try Rational.initInt(alloc, 7); defer b.deinit(); try expectDecimal("7", true, b, 20); var c = try Rational.initRatio(alloc, -3, 4); defer c.deinit(); try expectDecimal("-0.75", true, c, 20); } test "toDecimalString: repeating expansions round and report inexact" { var third = try Rational.initRatio(alloc, 1, 3); defer third.deinit(); try expectDecimal("0.333", false, third, 3); var twothirds = try Rational.initRatio(alloc, 2, 3); defer twothirds.deinit(); // 0.6666... rounds up at the cutoff try expectDecimal("0.667", false, twothirds, 3); } test "toDecimalString: rounding carries across digits" { // 1/1000 with 2 digits: 0.001 -> rounds to 0.00 var a = try Rational.initRatio(alloc, 999, 1000); defer a.deinit(); try expectDecimal("1.00", false, a, 2); } test "toDecimalString: the unit conversion case is exact" { // 12 inches in feet: 12 * (127/5000) / (381/1250) = exactly 1 var twelve = try Rational.initInt(alloc, 12); defer twelve.deinit(); var inch = try Rational.initRatio(alloc, 127, 5000); defer inch.deinit(); var foot = try Rational.initRatio(alloc, 381, 1250); defer foot.deinit(); var meters = try Rational.mul(alloc, twelve, inch); defer meters.deinit(); var feet = try Rational.div(alloc, meters, foot); defer feet.deinit(); try expectFrac("1", feet); try expectDecimal("1", true, feet, 20); } test "toDecimalString: non-terminating conversion renders approximately" { // 100 km in miles = 100000 / 1609.344 = 781250/12573 var hundred_km = try Rational.initInt(alloc, 100000); defer hundred_km.deinit(); var mile = try Rational.parseDecimal(alloc, "1609.344"); defer mile.deinit(); var miles = try Rational.div(alloc, hundred_km, mile); defer miles.deinit(); try expectFrac("781250/12573", miles); const d = try miles.toDecimalString(alloc, 9); defer alloc.free(d.text); try testing.expect(!d.exact); try testing.expect(std.mem.startsWith(u8, d.text, "62.13711922")); } test "toFloat: simple values" { var half = try Rational.initRatio(alloc, 1, 2); defer half.deinit(); try testing.expectEqual(@as(f64, 0.5), half.toFloat(alloc)); var z = try Rational.initZero(alloc); defer z.deinit(); try testing.expectEqual(@as(f64, 0.0), z.toFloat(alloc)); var neg = try Rational.initInt(alloc, -7); defer neg.deinit(); try testing.expectEqual(@as(f64, -7.0), neg.toFloat(alloc)); } test "toFloat: a single rounding fixes the accumulated-error bug" { // Exactly 3/10 rounded once is the f64 nearest 0.3, which prints as "0.3". var a = try Rational.parseDecimal(alloc, "0.1"); defer a.deinit(); var b = try Rational.parseDecimal(alloc, "0.2"); defer b.deinit(); var sum = try Rational.add(alloc, a, b); defer sum.deinit(); try testing.expectEqual(@as(f64, 0.3), sum.toFloat(alloc)); // Whereas the f64 route accumulates error and misses. The values must be // runtime-known: Zig folds float literals at comptime as `comptime_float`, // which would not exercise f64 arithmetic at all. var x: f64 = 0.1; var y: f64 = 0.2; _ = &x; _ = &y; try testing.expect(x + y != @as(f64, 0.3)); } test "toFloat: thirds round correctly" { var third = try Rational.initRatio(alloc, 1, 3); defer third.deinit(); try testing.expectApproxEqAbs(@as(f64, 1.0 / 3.0), third.toFloat(alloc), 1e-18); } test "toFloat: large integers" { var a = try Rational.parseDecimal(alloc, "9007199254740992"); defer a.deinit(); try testing.expectEqual(@as(f64, 9007199254740992.0), a.toFloat(alloc)); } test "toFloat: magnitudes far beyond the scaling window" { // When the numerator has far more bits than the denominator, toFloat must // shift the numerator DOWN rather than up. Exercises the negative-shift // branch of the scaling logic. var big = try Rational.parseDecimal(alloc, "1e30"); defer big.deinit(); try testing.expectEqual(@as(f64, 1e30), big.toFloat(alloc)); var huge = try Rational.parseDecimal(alloc, "1e100"); defer huge.deinit(); try testing.expectEqual(@as(f64, 1e100), huge.toFloat(alloc)); var neg = try Rational.parseDecimal(alloc, "-1e40"); defer neg.deinit(); try testing.expectEqual(@as(f64, -1e40), neg.toFloat(alloc)); // 2^200 as an exact integer, converted down to the nearest f64. var two = try Rational.initInt(alloc, 2); defer two.deinit(); var p = try Rational.powInt(alloc, two, 200); defer p.deinit(); try testing.expectEqual(std.math.ldexp(@as(f64, 1.0), 200), p.toFloat(alloc)); } test "toFloat: a large numerator over a large denominator" { // Both sides big, ratio small: the shift stays near zero. var a = try Rational.parseDecimal(alloc, "1e40"); defer a.deinit(); var b = try Rational.parseDecimal(alloc, "1e39"); defer b.deinit(); var q = try Rational.div(alloc, a, b); defer q.deinit(); try testing.expectEqual(@as(f64, 10.0), q.toFloat(alloc)); } test "toFloat: round trip through parseDecimal for a range of values" { const cases = [_][]const u8{ "1", "0.5", "0.25", "2.75", "-3.125", "1024", "0.0625" }; for (cases) |c| { var r = try Rational.parseDecimal(alloc, c); defer r.deinit(); const f = r.toFloat(alloc); const expected = try std.fmt.parseFloat(f64, c); try testing.expectEqual(expected, f); } } test "denBitCount grows with the denominator" { var small = try Rational.initRatio(alloc, 1, 2); defer small.deinit(); try testing.expectEqual(@as(usize, 2), small.denBitCount()); var big = try Rational.initRatio(alloc, 1, 1024); defer big.deinit(); try testing.expectEqual(@as(usize, 11), big.denBitCount()); var whole = try Rational.initInt(alloc, 99); defer whole.deinit(); try testing.expectEqual(@as(usize, 1), whole.denBitCount()); } test "clone is independent" { var a = try Rational.initRatio(alloc, 3, 7); defer a.deinit(); var b = try a.clone(); defer b.deinit(); try testing.expect(try Rational.eql(alloc, a, b)); try expectFrac("3/7", b); } test "chained arithmetic stays exact where f64 would drift" { // (0.1 + 0.2) * 10 - 3 == 0 exactly var a = try Rational.parseDecimal(alloc, "0.1"); defer a.deinit(); var b = try Rational.parseDecimal(alloc, "0.2"); defer b.deinit(); var ten = try Rational.initInt(alloc, 10); defer ten.deinit(); var three = try Rational.initInt(alloc, 3); defer three.deinit(); var sum = try Rational.add(alloc, a, b); defer sum.deinit(); var scaled = try Rational.mul(alloc, sum, ten); defer scaled.deinit(); var result = try Rational.sub(alloc, scaled, three); defer result.deinit(); try testing.expect(result.isZero()); // The f64 route does not reach zero. Runtime-known values are required so // the arithmetic actually happens in f64 rather than being comptime-folded. var x: f64 = 0.1; var y: f64 = 0.2; _ = &x; _ = &y; try testing.expect((x + y) * 10.0 - 3.0 != 0.0); } test "floor" { const cases = [_]struct { in: []const u8, out: []const u8 }{ .{ .in = "3.7", .out = "3" }, .{ .in = "3.2", .out = "3" }, .{ .in = "3", .out = "3" }, .{ .in = "-3.2", .out = "-4" }, .{ .in = "-3.7", .out = "-4" }, .{ .in = "-3", .out = "-3" }, .{ .in = "0", .out = "0" }, .{ .in = "0.5", .out = "0" }, .{ .in = "-0.5", .out = "-1" }, }; for (cases) |c| { var a = try Rational.parseDecimal(alloc, c.in); defer a.deinit(); var f = try Rational.floor(alloc, a); defer f.deinit(); try expectFrac(c.out, f); } } test "ceil" { const cases = [_]struct { in: []const u8, out: []const u8 }{ .{ .in = "3.2", .out = "4" }, .{ .in = "3.7", .out = "4" }, .{ .in = "3", .out = "3" }, .{ .in = "-3.2", .out = "-3" }, .{ .in = "-3.7", .out = "-3" }, .{ .in = "0.5", .out = "1" }, .{ .in = "-0.5", .out = "0" }, }; for (cases) |c| { var a = try Rational.parseDecimal(alloc, c.in); defer a.deinit(); var r = try Rational.ceil(alloc, a); defer r.deinit(); try expectFrac(c.out, r); } } test "round: halves go away from zero, matching @round" { const cases = [_]struct { in: []const u8, out: []const u8 }{ .{ .in = "3.5", .out = "4" }, .{ .in = "3.4", .out = "3" }, .{ .in = "3.6", .out = "4" }, .{ .in = "2.5", .out = "3" }, .{ .in = "-3.5", .out = "-4" }, .{ .in = "-3.4", .out = "-3" }, .{ .in = "-2.5", .out = "-3" }, .{ .in = "7", .out = "7" }, .{ .in = "0", .out = "0" }, }; for (cases) |c| { var a = try Rational.parseDecimal(alloc, c.in); defer a.deinit(); var r = try Rational.round(alloc, a); defer r.deinit(); try expectFrac(c.out, r); // Cross-check against the f64 builtin for the same input. const f = try std.fmt.parseFloat(f64, c.in); try testing.expectEqual(@round(f), r.toFloat(alloc)); } } test "mod: matches the a - b*floor(a/b) definition, sign following the divisor" { // Expected values are written out rather than derived from `@mod`: Zig's // float `@mod` with a NEGATIVE divisor gave different answers in different // builds of this suite (-2 normally, 1 under the instrumented coverage // build, i.e. comptime folding and the runtime path disagree). An oracle // that changes with optimize mode cannot verify anything, so these are the // values the definition requires. const cases = [_]struct { a: i64, b: i64, expected: i64 }{ .{ .a = 10, .b = 3, .expected = 1 }, // floor(10/3)=3 -> 10-9 .{ .a = -10, .b = 3, .expected = 2 }, // floor(-10/3)=-4 -> -10+12 .{ .a = 10, .b = -3, .expected = -2 }, // floor(10/-3)=-4 -> 10-12 .{ .a = -10, .b = -3, .expected = -1 }, // floor(-10/-3)=3 -> -10+9 .{ .a = 7, .b = 7, .expected = 0 }, .{ .a = 0, .b = 5, .expected = 0 }, .{ .a = 7, .b = 5, .expected = 2 }, .{ .a = -7, .b = 5, .expected = 3 }, }; for (cases) |c| { var a = try Rational.initInt(alloc, c.a); defer a.deinit(); var b = try Rational.initInt(alloc, c.b); defer b.deinit(); var m = try Rational.mod(alloc, a, b); defer m.deinit(); var expected = try Rational.initInt(alloc, c.expected); defer expected.deinit(); if (!try Rational.eql(alloc, m, expected)) { const got = try m.toFractionString(alloc); defer alloc.free(got); std.debug.print("mod({d}, {d}): expected {d}, got {s}\n", .{ c.a, c.b, c.expected, got }); return error.ModMismatch; } // The result must carry the sign of the divisor (or be zero). if (c.expected != 0) { try testing.expectEqual(c.b < 0, m.isNegative()); } } } test "mod: fractional operands" { var a = try Rational.parseDecimal(alloc, "7.5"); defer a.deinit(); var b = try Rational.parseDecimal(alloc, "2"); defer b.deinit(); var m = try Rational.mod(alloc, a, b); defer m.deinit(); try expectFrac("3/2", m); } test "mod: by zero errors" { var a = try Rational.initInt(alloc, 1); defer a.deinit(); var z = try Rational.initZero(alloc); defer z.deinit(); try testing.expectError(Error.DivisionByZero, Rational.mod(alloc, a, z)); } test "factorial: small values" { const cases = [_]struct { n: u64, out: []const u8 }{ .{ .n = 0, .out = "1" }, .{ .n = 1, .out = "1" }, .{ .n = 5, .out = "120" }, .{ .n = 10, .out = "3628800" }, }; for (cases) |c| { var f = try Rational.factorial(alloc, c.n); defer f.deinit(); try expectFrac(c.out, f); } } test "factorial: unbounded past the f64 limit of 170" { // 171! overflows f64 to infinity; exactly this is why the old evaluator // rejected it. Exact arithmetic has no such wall. var f = try Rational.factorial(alloc, 171); defer f.deinit(); try testing.expect(f.isInteger()); const s = try f.toFractionString(alloc); defer alloc.free(s); try testing.expect(s.len > 300); // 171! has 310 digits try testing.expect(std.math.isPositiveInf(f.toFloat(alloc))); } test "factorial: 20 is exact where f64 is already lossy" { var f = try Rational.factorial(alloc, 20); defer f.deinit(); try expectFrac("2432902008176640000", f); } test "factorial: absurd inputs are rejected" { try testing.expectError(Error.ExponentTooLarge, Rational.factorial(alloc, 20_001)); } // -- Allocation-failure safety -- // // Every `errdefer` in this file exists to release a partially-constructed value // when a LATER allocation fails. Merely executing those lines proves nothing; // what matters is that no memory leaks when the failure happens. // // `std.testing.FailingAllocator` fails after exactly N allocations, so sweeping // N across an operation's whole allocation sequence drives every intermediate // failure point. `testing.allocator` sits underneath and reports a leak at the // end of the test, which is what actually verifies the errdefer chain. /// Run `body` repeatedly, failing the 0th allocation, then the 1st, and so on, /// until the operation completes without needing to fail. Any error other than /// OutOfMemory is a real failure. fn oomSweep(comptime body: fn (Allocator) anyerror!void) !void { var fail_index: usize = 0; while (fail_index < 512) : (fail_index += 1) { var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = fail_index }); if (body(failing.allocator())) |_| { // Completed without exhausting the budget: the sweep is done. return; } else |err| { if (err != error.OutOfMemory) return err; } } return error.OomSweepNeverCompleted; } fn bodyParseDecimal(a: Allocator) anyerror!void { var x = try Rational.parseDecimal(a, "-1.25e3"); defer x.deinit(); var y = try Rational.parseDecimal(a, "0.1"); defer y.deinit(); } fn bodyInitRatio(a: Allocator) anyerror!void { var x = try Rational.initRatio(a, 6, -8); defer x.deinit(); var y = try x.clone(); defer y.deinit(); } fn bodyArithmetic(a: Allocator) anyerror!void { var x = try Rational.parseDecimal(a, "1.5"); defer x.deinit(); var y = try Rational.parseDecimal(a, "2.25"); defer y.deinit(); var sum = try Rational.add(a, x, y); defer sum.deinit(); var diff = try Rational.sub(a, x, y); defer diff.deinit(); var prod = try Rational.mul(a, x, y); defer prod.deinit(); var quot = try Rational.div(a, x, y); defer quot.deinit(); var neg = try Rational.negate(a, x); defer neg.deinit(); var magnitude = try Rational.abs(a, neg); defer magnitude.deinit(); _ = try Rational.order(a, x, y); } fn bodyPowAndSqrt(a: Allocator) anyerror!void { var base = try Rational.initRatio(a, 9, 16); defer base.deinit(); var p = try Rational.powInt(a, base, 3); defer p.deinit(); var q = try Rational.powInt(a, base, -2); defer q.deinit(); if (try Rational.sqrtExact(a, base)) |root| { var r = root; r.deinit(); } var two = try Rational.initInt(a, 2); defer two.deinit(); // A non-square: exercises the path that allocates roots, discovers they do // not square back, and releases them before returning null. try std.testing.expect((try Rational.sqrtExact(a, two)) == null); } fn bodyRounding(a: Allocator) anyerror!void { var x = try Rational.parseDecimal(a, "-3.7"); defer x.deinit(); var f = try Rational.floor(a, x); defer f.deinit(); var c = try Rational.ceil(a, x); defer c.deinit(); var r = try Rational.round(a, x); defer r.deinit(); var three = try Rational.initInt(a, 3); defer three.deinit(); var m = try Rational.mod(a, x, three); defer m.deinit(); } fn bodyFactorial(a: Allocator) anyerror!void { var f = try Rational.factorial(a, 12); defer f.deinit(); } fn bodyRendering(a: Allocator) anyerror!void { var x = try Rational.initRatio(a, 1, 7); defer x.deinit(); const frac = try x.toFractionString(a); a.free(frac); const repeating = try x.toDecimalString(a, 12); a.free(repeating.text); var terminating = try Rational.parseDecimal(a, "0.125"); defer terminating.deinit(); const exact = try terminating.toDecimalString(a, 12); a.free(exact.text); _ = try x.isTerminating(a); _ = x.toFloat(a); } test "OOM safety: parseDecimal leaks nothing at any failure point" { try oomSweep(bodyParseDecimal); } test "OOM safety: initRatio and clone" { try oomSweep(bodyInitRatio); } test "OOM safety: the four arithmetic operations plus negate, abs and order" { try oomSweep(bodyArithmetic); } test "OOM safety: powInt and sqrtExact" { try oomSweep(bodyPowAndSqrt); } test "OOM safety: floor, ceil, round and mod" { try oomSweep(bodyRounding); } test "OOM safety: factorial" { try oomSweep(bodyFactorial); } test "OOM safety: decimal and fraction rendering" { try oomSweep(bodyRendering); } test "parse: accepts decimal text" { var a = try Rational.parse(alloc, "0.0254"); defer a.deinit(); try expectFrac("127/5000", a); } test "parse: accepts fraction text" { var a = try Rational.parse(alloc, "5/9"); defer a.deinit(); try expectFrac("5/9", a); var b = try Rational.parse(alloc, "463/900"); defer b.deinit(); try expectFrac("463/900", b); } test "parse: fraction text is reduced" { var a = try Rational.parse(alloc, "10/4"); defer a.deinit(); try expectFrac("5/2", a); } test "parse: fraction reduces to lowest terms" { // 101325/760 is the exact torr factor; gcd is 5. var a = try Rational.parse(alloc, "101325/760"); defer a.deinit(); try expectFrac("20265/152", a); } test "parse: negative fractions" { var a = try Rational.parse(alloc, "-160/9"); defer a.deinit(); try expectFrac("-160/9", a); var b = try Rational.parse(alloc, "160/-9"); defer b.deinit(); try expectFrac("-160/9", b); } test "parse: fraction with a zero denominator errors" { try testing.expectError(Error.DivisionByZero, Rational.parse(alloc, "1/0")); } test "parse: malformed fractions error" { try testing.expectError(Error.InvalidNumber, Rational.parse(alloc, "1/")); try testing.expectError(Error.InvalidNumber, Rational.parse(alloc, "/2")); try testing.expectError(Error.InvalidNumber, Rational.parse(alloc, "a/b")); }