tally/engine/src/rational.zig

1664 lines
55 KiB
Zig

//! 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"));
}