add rational number operations capability
This commit is contained in:
parent
690fa18e90
commit
83857cebbc
3 changed files with 1862 additions and 0 deletions
|
|
@ -13,6 +13,10 @@ pub const programmer = @import("programmer.zig");
|
|||
pub const formatter = @import("formatter.zig");
|
||||
pub const float_interp = @import("float_interp.zig");
|
||||
pub const units = @import("units.zig");
|
||||
// Exact numeric model (design.md 2.7). Not yet wired into the evaluator; see
|
||||
// Task 2.0b. Exported here so its tests run as part of `zig build test`.
|
||||
pub const rational = @import("rational.zig");
|
||||
pub const number = @import("number.zig");
|
||||
|
||||
// Re-export primary types for convenience
|
||||
pub const Value = types.Value;
|
||||
|
|
@ -39,6 +43,10 @@ pub const ConvertResult = units.ConvertResult;
|
|||
pub const convert = units.convert;
|
||||
pub const findUnit = units.findUnit;
|
||||
|
||||
// Exact numeric model
|
||||
pub const Rational = rational.Rational;
|
||||
pub const Number = number.Number;
|
||||
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
|
|
|||
656
engine/src/number.zig
Normal file
656
engine/src/number.zig
Normal file
|
|
@ -0,0 +1,656 @@
|
|||
//! The Tally numeric model: an exact tier over an inexact fallback.
|
||||
//!
|
||||
//! See design.md 2.7. A `Number` is either an exact `Rational` or an inexact
|
||||
//! `f64`. Arithmetic stays exact as long as it can, and falls back to floating
|
||||
//! point only where a result cannot be rational (transcendentals, irrational
|
||||
//! roots, non-integer powers).
|
||||
//!
|
||||
//! Two invariants make the `exact` tag trustworthy:
|
||||
//!
|
||||
//! 1. **Contagion.** Any operation with an inexact operand yields an inexact
|
||||
//! result. `exact` therefore means "no rounding has occurred anywhere in this
|
||||
//! value's history", not "happens to look clean right now".
|
||||
//! 2. **No re-exactification.** An inexact value is never converted back to
|
||||
//! exact, even when it looks like a whole number.
|
||||
//!
|
||||
//! Growth is bounded: exact results whose denominator exceeds
|
||||
//! `max_denominator_bits` are demoted to inexact rather than allowed to consume
|
||||
//! unbounded memory. Degradation, not failure.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const rational = @import("rational.zig");
|
||||
const Rational = rational.Rational;
|
||||
|
||||
pub const Error = rational.Error;
|
||||
|
||||
/// Denominator size at which an exact result is demoted to inexact.
|
||||
///
|
||||
/// Interactive single calculations do not approach this; the cap exists so that
|
||||
/// a pathological chain of divisions degrades gracefully instead of exhausting
|
||||
/// memory. Starting value, to be revisited with measurements.
|
||||
pub const max_denominator_bits: usize = 4096;
|
||||
|
||||
pub const Number = union(enum) {
|
||||
exact: Rational,
|
||||
inexact: f64,
|
||||
|
||||
// -- Construction --
|
||||
|
||||
pub fn fromRational(value: Rational) Number {
|
||||
return .{ .exact = value };
|
||||
}
|
||||
|
||||
pub fn fromFloat(value: f64) Number {
|
||||
return .{ .inexact = value };
|
||||
}
|
||||
|
||||
pub fn fromInt(allocator: Allocator, value: anytype) Error!Number {
|
||||
return .{ .exact = try Rational.initInt(allocator, value) };
|
||||
}
|
||||
|
||||
/// Parse a decimal literal exactly. `0.1` becomes `1/10`, not a float.
|
||||
pub fn parse(allocator: Allocator, text: []const u8) Error!Number {
|
||||
return .{ .exact = try Rational.parseDecimal(allocator, text) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Number) void {
|
||||
switch (self.*) {
|
||||
.exact => |*r| r.deinit(),
|
||||
.inexact => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone(self: Number) Error!Number {
|
||||
return switch (self) {
|
||||
.exact => |r| .{ .exact = try r.clone() },
|
||||
.inexact => |f| .{ .inexact = f },
|
||||
};
|
||||
}
|
||||
|
||||
// -- Queries --
|
||||
|
||||
pub fn isExact(self: Number) bool {
|
||||
return self == .exact;
|
||||
}
|
||||
|
||||
/// Collapse to f64 for display or for handing to a float-only operation.
|
||||
pub fn toFloat(self: Number, allocator: Allocator) f64 {
|
||||
return switch (self) {
|
||||
.exact => |r| r.toFloat(allocator),
|
||||
.inexact => |f| f,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isZero(self: Number) bool {
|
||||
return switch (self) {
|
||||
.exact => |r| r.isZero(),
|
||||
.inexact => |f| f == 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isNegative(self: Number) bool {
|
||||
return switch (self) {
|
||||
.exact => |r| r.isNegative(),
|
||||
.inexact => |f| f < 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// True for an exact whole number. Inexact values are never reported as
|
||||
/// integers, because we cannot know whether rounding produced the
|
||||
/// integer-looking value.
|
||||
pub fn isExactInteger(self: Number) bool {
|
||||
return switch (self) {
|
||||
.exact => |r| r.isInteger(),
|
||||
.inexact => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// The exact value as an i64, when it is a whole number that fits.
|
||||
/// Used for things like integer exponents and factorial arguments.
|
||||
pub fn asExactInt(self: Number, comptime T: type) ?T {
|
||||
return switch (self) {
|
||||
.exact => |r| blk: {
|
||||
if (!r.isInteger()) break :blk null;
|
||||
break :blk r.num.toInt(T) catch null;
|
||||
},
|
||||
.inexact => null,
|
||||
};
|
||||
}
|
||||
|
||||
// -- Demotion --
|
||||
|
||||
/// Apply the growth cap: an exact value with an oversized denominator is
|
||||
/// converted to inexact. Takes ownership of `value`.
|
||||
fn capped(allocator: Allocator, value: Rational) Number {
|
||||
if (value.denBitCount() <= max_denominator_bits) {
|
||||
return .{ .exact = value };
|
||||
}
|
||||
var v = value;
|
||||
const f = v.toFloat(allocator);
|
||||
v.deinit();
|
||||
return .{ .inexact = f };
|
||||
}
|
||||
|
||||
// -- Arithmetic (with contagion) --
|
||||
|
||||
/// Shape of a binary operation: exact when both operands are exact,
|
||||
/// otherwise fall back to floats.
|
||||
fn binary(
|
||||
allocator: Allocator,
|
||||
a: Number,
|
||||
b: Number,
|
||||
comptime exactOp: fn (Allocator, Rational, Rational) Error!Rational,
|
||||
comptime floatOp: fn (f64, f64) f64,
|
||||
) Error!Number {
|
||||
if (a == .exact and b == .exact) {
|
||||
const result = try exactOp(allocator, a.exact, b.exact);
|
||||
return capped(allocator, result);
|
||||
}
|
||||
return .{ .inexact = floatOp(a.toFloat(allocator), b.toFloat(allocator)) };
|
||||
}
|
||||
|
||||
fn addFloat(x: f64, y: f64) f64 {
|
||||
return x + y;
|
||||
}
|
||||
fn subFloat(x: f64, y: f64) f64 {
|
||||
return x - y;
|
||||
}
|
||||
fn mulFloat(x: f64, y: f64) f64 {
|
||||
return x * y;
|
||||
}
|
||||
fn divFloat(x: f64, y: f64) f64 {
|
||||
return x / y;
|
||||
}
|
||||
|
||||
pub fn add(allocator: Allocator, a: Number, b: Number) Error!Number {
|
||||
return binary(allocator, a, b, Rational.add, addFloat);
|
||||
}
|
||||
|
||||
pub fn sub(allocator: Allocator, a: Number, b: Number) Error!Number {
|
||||
return binary(allocator, a, b, Rational.sub, subFloat);
|
||||
}
|
||||
|
||||
pub fn mul(allocator: Allocator, a: Number, b: Number) Error!Number {
|
||||
return binary(allocator, a, b, Rational.mul, mulFloat);
|
||||
}
|
||||
|
||||
/// Division. Division by an exact zero is an error; division by an inexact
|
||||
/// zero follows IEEE semantics and yields infinity, matching the float tier.
|
||||
pub fn div(allocator: Allocator, a: Number, b: Number) Error!Number {
|
||||
if (b == .exact and b.exact.isZero()) return Error.DivisionByZero;
|
||||
return binary(allocator, a, b, Rational.div, divFloat);
|
||||
}
|
||||
|
||||
pub fn negate(allocator: Allocator, a: Number) Error!Number {
|
||||
return switch (a) {
|
||||
.exact => |r| .{ .exact = try Rational.negate(allocator, r) },
|
||||
.inexact => |f| .{ .inexact = -f },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn abs(allocator: Allocator, a: Number) Error!Number {
|
||||
return switch (a) {
|
||||
.exact => |r| .{ .exact = try Rational.abs(allocator, r) },
|
||||
.inexact => |f| .{ .inexact = @abs(f) },
|
||||
};
|
||||
}
|
||||
|
||||
/// Exponentiation. Stays exact only when both the base is exact and the
|
||||
/// exponent is an exact integer; a fractional exponent generally produces an
|
||||
/// irrational result, so it falls back.
|
||||
pub fn pow(allocator: Allocator, base: Number, exponent: Number) Error!Number {
|
||||
if (base == .exact) {
|
||||
if (exponent.asExactInt(i64)) |e| {
|
||||
if (base.exact.isZero() and e < 0) return Error.DivisionByZero;
|
||||
const result = try Rational.powInt(allocator, base.exact, e);
|
||||
return capped(allocator, result);
|
||||
}
|
||||
}
|
||||
return .{ .inexact = std.math.pow(f64, base.toFloat(allocator), exponent.toFloat(allocator)) };
|
||||
}
|
||||
|
||||
/// Square root. Exact for perfect rational squares (`sqrt(4)` is 2), inexact
|
||||
/// otherwise (`sqrt(2)`), per design.md 2.7.4.
|
||||
pub fn sqrt(allocator: Allocator, a: Number) Error!Number {
|
||||
if (a == .exact and !a.exact.isNegative()) {
|
||||
if (try Rational.sqrtExact(allocator, a.exact)) |root| {
|
||||
return capped(allocator, root);
|
||||
}
|
||||
}
|
||||
return .{ .inexact = @sqrt(a.toFloat(allocator)) };
|
||||
}
|
||||
|
||||
/// Apply a float-only function, always producing an inexact result. This is
|
||||
/// the single entry point for transcendentals, so the fallback boundary is
|
||||
/// visible in one place.
|
||||
pub fn applyFloatFn(allocator: Allocator, a: Number, comptime f: fn (f64) f64) Number {
|
||||
return .{ .inexact = f(a.toFloat(allocator)) };
|
||||
}
|
||||
|
||||
// -- Comparison --
|
||||
|
||||
pub fn order(allocator: Allocator, a: Number, b: Number) Error!std.math.Order {
|
||||
if (a == .exact and b == .exact) {
|
||||
return Rational.order(allocator, a.exact, b.exact);
|
||||
}
|
||||
const x = a.toFloat(allocator);
|
||||
const y = b.toFloat(allocator);
|
||||
if (x < y) return .lt;
|
||||
if (x > y) return .gt;
|
||||
return .eq;
|
||||
}
|
||||
|
||||
pub fn eql(allocator: Allocator, a: Number, b: Number) Error!bool {
|
||||
return (try order(allocator, a, b)) == .eq;
|
||||
}
|
||||
|
||||
// -- Rendering --
|
||||
|
||||
pub const Display = struct {
|
||||
/// Rendered text. Caller owns the memory.
|
||||
text: []u8,
|
||||
/// False when the text is a rounded approximation of the true value,
|
||||
/// either because the value is inexact or because an exact value has a
|
||||
/// non-terminating decimal expansion.
|
||||
exact: bool,
|
||||
};
|
||||
|
||||
/// Render for display. Exact values with terminating expansions print
|
||||
/// exactly; everything else is rounded to `max_digits` and flagged.
|
||||
pub fn toDecimalString(self: Number, allocator: Allocator, max_digits: usize) Error!Display {
|
||||
switch (self) {
|
||||
.exact => |r| {
|
||||
const d = try r.toDecimalString(allocator, max_digits);
|
||||
return .{ .text = d.text, .exact = d.exact };
|
||||
},
|
||||
.inexact => |f| {
|
||||
const text = std.fmt.allocPrint(allocator, "{d}", .{f}) catch
|
||||
return Error.OutOfMemory;
|
||||
return .{ .text = text, .exact = false };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Render an exact value as a fraction, or null when the value is inexact or
|
||||
/// a whole number (where a fraction adds nothing).
|
||||
pub fn toFractionString(self: Number, allocator: Allocator) Error!?[]u8 {
|
||||
return switch (self) {
|
||||
.exact => |r| if (r.isInteger()) null else try r.toFractionString(allocator),
|
||||
.inexact => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// -- Tests --
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
fn expectDecimal(expected: []const u8, expected_exact: bool, n: Number, digits: usize) !void {
|
||||
const d = try n.toDecimalString(alloc, digits);
|
||||
defer alloc.free(d.text);
|
||||
try testing.expectEqualStrings(expected, d.text);
|
||||
try testing.expectEqual(expected_exact, d.exact);
|
||||
}
|
||||
|
||||
test "parse produces an exact value" {
|
||||
var n = try Number.parse(alloc, "0.1");
|
||||
defer n.deinit();
|
||||
try testing.expect(n.isExact());
|
||||
try expectDecimal("0.1", true, n, 20);
|
||||
}
|
||||
|
||||
test "fromFloat produces an inexact value" {
|
||||
var n = Number.fromFloat(0.5);
|
||||
defer n.deinit();
|
||||
try testing.expect(!n.isExact());
|
||||
}
|
||||
|
||||
test "exact + exact stays exact: the 0.1 + 0.2 case" {
|
||||
var a = try Number.parse(alloc, "0.1");
|
||||
defer a.deinit();
|
||||
var b = try Number.parse(alloc, "0.2");
|
||||
defer b.deinit();
|
||||
var sum = try Number.add(alloc, a, b);
|
||||
defer sum.deinit();
|
||||
try testing.expect(sum.isExact());
|
||||
try expectDecimal("0.3", true, sum, 20);
|
||||
}
|
||||
|
||||
test "contagion: inexact operand makes the result inexact" {
|
||||
var a = try Number.parse(alloc, "0.1");
|
||||
defer a.deinit();
|
||||
var b = Number.fromFloat(0.2);
|
||||
defer b.deinit();
|
||||
|
||||
var sum = try Number.add(alloc, a, b);
|
||||
defer sum.deinit();
|
||||
try testing.expect(!sum.isExact());
|
||||
|
||||
var product = try Number.mul(alloc, b, a);
|
||||
defer product.deinit();
|
||||
try testing.expect(!product.isExact());
|
||||
}
|
||||
|
||||
test "contagion: an inexact value is never re-exactified" {
|
||||
// 0.5 * 2 == 1.0 exactly in f64, but the result must stay inexact because
|
||||
// we cannot know the history of the inexact operand.
|
||||
var a = Number.fromFloat(0.5);
|
||||
defer a.deinit();
|
||||
var two = try Number.fromInt(alloc, 2);
|
||||
defer two.deinit();
|
||||
var product = try Number.mul(alloc, a, two);
|
||||
defer product.deinit();
|
||||
try testing.expect(!product.isExact());
|
||||
try testing.expectEqual(@as(f64, 1.0), product.toFloat(alloc));
|
||||
try testing.expect(!product.isExactInteger());
|
||||
}
|
||||
|
||||
test "contagion propagates through a chain" {
|
||||
var exact = try Number.parse(alloc, "1.5");
|
||||
defer exact.deinit();
|
||||
var inexact = Number.fromFloat(2.0);
|
||||
defer inexact.deinit();
|
||||
|
||||
var step1 = try Number.add(alloc, exact, inexact);
|
||||
defer step1.deinit();
|
||||
var step2 = try Number.mul(alloc, step1, exact);
|
||||
defer step2.deinit();
|
||||
var step3 = try Number.sub(alloc, step2, exact);
|
||||
defer step3.deinit();
|
||||
try testing.expect(!step3.isExact());
|
||||
}
|
||||
|
||||
test "sub and mul stay exact" {
|
||||
var a = try Number.parse(alloc, "1.1");
|
||||
defer a.deinit();
|
||||
var b = try Number.parse(alloc, "2.2");
|
||||
defer b.deinit();
|
||||
var sum = try Number.add(alloc, a, b);
|
||||
defer sum.deinit();
|
||||
try expectDecimal("3.3", true, sum, 20);
|
||||
|
||||
var diff = try Number.sub(alloc, b, a);
|
||||
defer diff.deinit();
|
||||
try expectDecimal("1.1", true, diff, 20);
|
||||
}
|
||||
|
||||
test "div: exact thirds and the round trip back to one" {
|
||||
var one = try Number.fromInt(alloc, 1);
|
||||
defer one.deinit();
|
||||
var three = try Number.fromInt(alloc, 3);
|
||||
defer three.deinit();
|
||||
|
||||
var third = try Number.div(alloc, one, three);
|
||||
defer third.deinit();
|
||||
try testing.expect(third.isExact());
|
||||
|
||||
var back = try Number.mul(alloc, third, three);
|
||||
defer back.deinit();
|
||||
try testing.expect(back.isExact());
|
||||
try expectDecimal("1", true, back, 20);
|
||||
}
|
||||
|
||||
test "div by exact zero errors" {
|
||||
var one = try Number.fromInt(alloc, 1);
|
||||
defer one.deinit();
|
||||
var zero = try Number.fromInt(alloc, 0);
|
||||
defer zero.deinit();
|
||||
try testing.expectError(Error.DivisionByZero, Number.div(alloc, one, zero));
|
||||
}
|
||||
|
||||
test "div by inexact zero follows IEEE semantics" {
|
||||
var one = try Number.fromInt(alloc, 1);
|
||||
defer one.deinit();
|
||||
var zero = Number.fromFloat(0.0);
|
||||
defer zero.deinit();
|
||||
var result = try Number.div(alloc, one, zero);
|
||||
defer result.deinit();
|
||||
try testing.expect(std.math.isPositiveInf(result.toFloat(alloc)));
|
||||
}
|
||||
|
||||
test "negate and abs preserve exactness" {
|
||||
var a = try Number.parse(alloc, "0.25");
|
||||
defer a.deinit();
|
||||
var n = try Number.negate(alloc, a);
|
||||
defer n.deinit();
|
||||
try testing.expect(n.isExact());
|
||||
try expectDecimal("-0.25", true, n, 20);
|
||||
|
||||
var b = try Number.abs(alloc, n);
|
||||
defer b.deinit();
|
||||
try testing.expect(b.isExact());
|
||||
try expectDecimal("0.25", true, b, 20);
|
||||
}
|
||||
|
||||
test "negate and abs preserve inexactness" {
|
||||
var a = Number.fromFloat(-1.5);
|
||||
defer a.deinit();
|
||||
var b = try Number.abs(alloc, a);
|
||||
defer b.deinit();
|
||||
try testing.expect(!b.isExact());
|
||||
try testing.expectEqual(@as(f64, 1.5), b.toFloat(alloc));
|
||||
}
|
||||
|
||||
test "pow: integer exponent stays exact" {
|
||||
var two = try Number.fromInt(alloc, 2);
|
||||
defer two.deinit();
|
||||
var ten = try Number.fromInt(alloc, 10);
|
||||
defer ten.deinit();
|
||||
var p = try Number.pow(alloc, two, ten);
|
||||
defer p.deinit();
|
||||
try testing.expect(p.isExact());
|
||||
try expectDecimal("1024", true, p, 20);
|
||||
}
|
||||
|
||||
test "pow: 2^53 + 1 is exact, unlike f64" {
|
||||
var two = try Number.fromInt(alloc, 2);
|
||||
defer two.deinit();
|
||||
var fiftythree = try Number.fromInt(alloc, 53);
|
||||
defer fiftythree.deinit();
|
||||
var one = try Number.fromInt(alloc, 1);
|
||||
defer one.deinit();
|
||||
|
||||
var p = try Number.pow(alloc, two, fiftythree);
|
||||
defer p.deinit();
|
||||
var sum = try Number.add(alloc, p, one);
|
||||
defer sum.deinit();
|
||||
try expectDecimal("9007199254740993", true, sum, 0);
|
||||
}
|
||||
|
||||
test "pow: fractional exponent falls back to inexact" {
|
||||
var two = try Number.fromInt(alloc, 2);
|
||||
defer two.deinit();
|
||||
var half = try Number.parse(alloc, "0.5");
|
||||
defer half.deinit();
|
||||
var p = try Number.pow(alloc, two, half);
|
||||
defer p.deinit();
|
||||
try testing.expect(!p.isExact());
|
||||
try testing.expectApproxEqAbs(@as(f64, std.math.sqrt2), p.toFloat(alloc), 1e-15);
|
||||
}
|
||||
|
||||
test "pow: negative integer exponent stays exact" {
|
||||
var two = try Number.fromInt(alloc, 2);
|
||||
defer two.deinit();
|
||||
var neg = try Number.fromInt(alloc, -3);
|
||||
defer neg.deinit();
|
||||
var p = try Number.pow(alloc, two, neg);
|
||||
defer p.deinit();
|
||||
try testing.expect(p.isExact());
|
||||
try expectDecimal("0.125", true, p, 20);
|
||||
}
|
||||
|
||||
test "pow: zero to a negative power errors" {
|
||||
var zero = try Number.fromInt(alloc, 0);
|
||||
defer zero.deinit();
|
||||
var neg = try Number.fromInt(alloc, -1);
|
||||
defer neg.deinit();
|
||||
try testing.expectError(Error.DivisionByZero, Number.pow(alloc, zero, neg));
|
||||
}
|
||||
|
||||
test "sqrt: perfect squares stay exact, others fall back" {
|
||||
var four = try Number.fromInt(alloc, 4);
|
||||
defer four.deinit();
|
||||
var r = try Number.sqrt(alloc, four);
|
||||
defer r.deinit();
|
||||
try testing.expect(r.isExact());
|
||||
try expectDecimal("2", true, r, 20);
|
||||
|
||||
var two = try Number.fromInt(alloc, 2);
|
||||
defer two.deinit();
|
||||
var r2 = try Number.sqrt(alloc, two);
|
||||
defer r2.deinit();
|
||||
try testing.expect(!r2.isExact());
|
||||
try testing.expectApproxEqAbs(@as(f64, std.math.sqrt2), r2.toFloat(alloc), 1e-15);
|
||||
}
|
||||
|
||||
test "sqrt: negative input falls back to a float NaN" {
|
||||
var neg = try Number.fromInt(alloc, -4);
|
||||
defer neg.deinit();
|
||||
var r = try Number.sqrt(alloc, neg);
|
||||
defer r.deinit();
|
||||
try testing.expect(!r.isExact());
|
||||
try testing.expect(std.math.isNan(r.toFloat(alloc)));
|
||||
}
|
||||
|
||||
test "applyFloatFn always yields inexact" {
|
||||
var one = try Number.fromInt(alloc, 1);
|
||||
defer one.deinit();
|
||||
var r = Number.applyFloatFn(alloc, one, floatLog2);
|
||||
defer r.deinit();
|
||||
try testing.expect(!r.isExact());
|
||||
try testing.expectEqual(@as(f64, 0.0), r.toFloat(alloc));
|
||||
}
|
||||
|
||||
fn floatLog2(x: f64) f64 {
|
||||
return @log2(x);
|
||||
}
|
||||
|
||||
test "asExactInt" {
|
||||
var a = try Number.fromInt(alloc, 42);
|
||||
defer a.deinit();
|
||||
try testing.expectEqual(@as(?i64, 42), a.asExactInt(i64));
|
||||
|
||||
var frac = try Number.parse(alloc, "1.5");
|
||||
defer frac.deinit();
|
||||
try testing.expect(frac.asExactInt(i64) == null);
|
||||
|
||||
var inexact = Number.fromFloat(3.0);
|
||||
defer inexact.deinit();
|
||||
try testing.expect(inexact.asExactInt(i64) == null);
|
||||
}
|
||||
|
||||
test "isExactInteger" {
|
||||
var a = try Number.fromInt(alloc, 7);
|
||||
defer a.deinit();
|
||||
try testing.expect(a.isExactInteger());
|
||||
|
||||
var b = try Number.parse(alloc, "7.5");
|
||||
defer b.deinit();
|
||||
try testing.expect(!b.isExactInteger());
|
||||
}
|
||||
|
||||
test "order and eql across tiers" {
|
||||
var a = try Number.parse(alloc, "0.5");
|
||||
defer a.deinit();
|
||||
var b = Number.fromFloat(0.5);
|
||||
defer b.deinit();
|
||||
try testing.expect(try Number.eql(alloc, a, b));
|
||||
|
||||
var c = try Number.fromInt(alloc, 1);
|
||||
defer c.deinit();
|
||||
try testing.expectEqual(std.math.Order.lt, try Number.order(alloc, a, c));
|
||||
try testing.expectEqual(std.math.Order.gt, try Number.order(alloc, c, a));
|
||||
}
|
||||
|
||||
test "demotion: an oversized denominator degrades to inexact" {
|
||||
// Build 1/2^n with n past the cap by repeated halving.
|
||||
var value = try Number.fromInt(alloc, 1);
|
||||
defer value.deinit();
|
||||
var two = try Number.fromInt(alloc, 2);
|
||||
defer two.deinit();
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < max_denominator_bits + 64) : (i += 1) {
|
||||
var next = try Number.div(alloc, value, two);
|
||||
value.deinit();
|
||||
value = next;
|
||||
if (!value.isExact()) break;
|
||||
_ = &next;
|
||||
}
|
||||
try testing.expect(!value.isExact());
|
||||
}
|
||||
|
||||
test "demotion: normal values stay well under the cap" {
|
||||
var a = try Number.parse(alloc, "0.1");
|
||||
defer a.deinit();
|
||||
var b = try Number.parse(alloc, "1.0000001");
|
||||
defer b.deinit();
|
||||
var product = try Number.mul(alloc, a, b);
|
||||
defer product.deinit();
|
||||
try testing.expect(product.isExact());
|
||||
}
|
||||
|
||||
test "toDecimalString: inexact values are always flagged approximate" {
|
||||
var a = Number.fromFloat(0.5);
|
||||
defer a.deinit();
|
||||
const d = try a.toDecimalString(alloc, 20);
|
||||
defer alloc.free(d.text);
|
||||
try testing.expect(!d.exact);
|
||||
try testing.expectEqualStrings("0.5", d.text);
|
||||
}
|
||||
|
||||
test "toDecimalString: exact non-terminating values are flagged approximate" {
|
||||
var one = try Number.fromInt(alloc, 1);
|
||||
defer one.deinit();
|
||||
var three = try Number.fromInt(alloc, 3);
|
||||
defer three.deinit();
|
||||
var third = try Number.div(alloc, one, three);
|
||||
defer third.deinit();
|
||||
try expectDecimal("0.333", false, third, 3);
|
||||
}
|
||||
|
||||
test "toFractionString: exact non-integers only" {
|
||||
var one = try Number.fromInt(alloc, 1);
|
||||
defer one.deinit();
|
||||
var three = try Number.fromInt(alloc, 3);
|
||||
defer three.deinit();
|
||||
var third = try Number.div(alloc, one, three);
|
||||
defer third.deinit();
|
||||
|
||||
const frac = (try third.toFractionString(alloc)).?;
|
||||
defer alloc.free(frac);
|
||||
try testing.expectEqualStrings("1/3", frac);
|
||||
|
||||
// Integers and inexact values have no useful fraction form.
|
||||
try testing.expect((try one.toFractionString(alloc)) == null);
|
||||
var inexact = Number.fromFloat(0.25);
|
||||
defer inexact.deinit();
|
||||
try testing.expect((try inexact.toFractionString(alloc)) == null);
|
||||
}
|
||||
|
||||
test "toFloat: exact to float uses a single correct rounding" {
|
||||
var a = try Number.parse(alloc, "0.1");
|
||||
defer a.deinit();
|
||||
var b = try Number.parse(alloc, "0.2");
|
||||
defer b.deinit();
|
||||
var sum = try Number.add(alloc, a, b);
|
||||
defer sum.deinit();
|
||||
try testing.expectEqual(@as(f64, 0.3), sum.toFloat(alloc));
|
||||
}
|
||||
|
||||
test "clone preserves the tier" {
|
||||
var a = try Number.parse(alloc, "0.75");
|
||||
defer a.deinit();
|
||||
var b = try a.clone();
|
||||
defer b.deinit();
|
||||
try testing.expect(b.isExact());
|
||||
try testing.expect(try Number.eql(alloc, a, b));
|
||||
|
||||
var c = Number.fromFloat(1.25);
|
||||
defer c.deinit();
|
||||
var d = try c.clone();
|
||||
defer d.deinit();
|
||||
try testing.expect(!d.isExact());
|
||||
}
|
||||
1198
engine/src/rational.zig
Normal file
1198
engine/src/rational.zig
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue