1664 lines
55 KiB
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"));
|
|
}
|