Skip to content

Implement abs, ceil, copysign, Rem, to_radians, to_degrees, and trunc#6

Closed
TimTheBig wants to merge 4 commits into
timschmidt:mainfrom
TimTheBig:main
Closed

Implement abs, ceil, copysign, Rem, to_radians, to_degrees, and trunc#6
TimTheBig wants to merge 4 commits into
timschmidt:mainfrom
TimTheBig:main

Conversation

@TimTheBig
Copy link
Copy Markdown

@TimTheBig TimTheBig commented May 21, 2026

Most of the diff is tests

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several mathematical utility methods to the Rational and Real types, including ceil, trunc, abs, copysign, and atan2. It also implements the remainder operator (%) for both types and adds specialized conversion methods between radians and degrees (to_degrees, to_radians). Feedback highlights critical issues regarding mathematical correctness for irrational values in ceil, trunc, and abs, which currently only round the rational scale and cause cache inconsistencies. Furthermore, the to_degrees and to_radians implementations violate internal invariants by failing to ensure that rational scales remain reduced after direct modification.

Comment thread src/real/arithmetic.rs
Comment on lines +2357 to +2363
pub fn ceil(mut self) -> Self {
self.rational = self.rational.ceil();

map_cache!(self.primitive_approx_cache, num::Float::ceil);

self
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The implementation of ceil on Real is mathematically incorrect for non-rational values (classes other than One). It currently performs a "scale-wise" ceiling, which does not correspond to the ceiling of the represented value. For example, Real::pi().ceil() returns pi (approx 3.14) instead of 4, and (Real::pi() * 0.5).ceil() returns pi instead of 2. If an exact ceiling cannot be computed for irrationals, this method should either be restricted to rational Reals or return a Computable representing the ceiling. Furthermore, the map_cache! call here introduces a critical bug: it updates the cache with the float ceiling of the entire value, while the Real itself is only updated scale-wise, leading to an inconsistent state where the cached float does not match the symbolic value.

Comment thread src/real/arithmetic.rs
Comment on lines +2398 to +2416
pub fn trunc(&self) -> Self {
use PrimitiveApproxCache::*;

// ? do computable need to be run first
Self {
rational: self.rational.trunc(),
// it's over one (int) and rational
class: Class::One,
computable: None,
signal: None,
primitive_approx_cache: Cell::new(match self.primitive_approx_cache.get() {
#[cfg(feature = "cached-f32-approx")]
F32(f) => F32(f.map(|f| f.trunc())),
#[cfg(feature = "cached-f64-approx")]
F64(f) => F64(f.map(|f| f.trunc())),
_ => Empty,
}),
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The implementation of trunc on Real suffers from the same mathematical incorrectness as ceil for non-rational values. For example, Real::pi().trunc() returns 1 instead of 3. Additionally, it is inconsistent with ceil because it forces the result to Class::One, effectively discarding the symbolic part entirely after truncating the scale. Like ceil, the use of trunc() on the primitive cache while only truncating the scale in the symbolic representation leads to cache inconsistency (e.g., trunc(pi) would have a rational value of 1 but a cached float of 3.0).

Comment thread src/real/arithmetic.rs
Comment on lines +2435 to +2441
pub fn abs(mut self) -> Self {
self.rational = self.rational.abs();

map_cache!(self.primitive_approx_cache, num::Float::abs);

self
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The abs method is incorrect for the Irrational class. It only ensures the rational scale is positive, but for an opaque irrational, the underlying Computable might be negative. While the docstring acknowledges this, providing a method named abs that returns a negative value is highly misleading and likely to cause bugs in generic code. This should at least attempt to use best_sign() to determine if a negation is needed, or return a Result if the sign is unknown.

Comment thread src/real/arithmetic.rs
Comment on lines +4827 to +4832
self.rational.numerator *= BigUint::from(180u64);
self.class = One;
self.computable = None;
self.signal = None;
self.primitive_approx_cache = Cell::new(PrimitiveApproxCache::Empty);
return self;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Directly multiplying the numerator of the internal Rational breaks the reduced fraction invariant. If self.rational was 1/2, it becomes 180/2, which is not reduced. This causes predicates like is_integer() (which checks if the denominator is 1) to return incorrect results. You should use the * operator or call .reduce() after the modification.

            self.rational = self.rational * Rational::new(180);
            self.class = One;
            self.computable = None;
            self.signal = None;
            self.primitive_approx_cache = Cell::new(PrimitiveApproxCache::Empty);
            return self;

Comment thread src/real/arithmetic.rs
Comment on lines +4858 to +4862
self.rational.denominator *= BigUint::from(180u64);
self.class = Pi;
self.computable = Some(Computable::pi());
self.primitive_approx_cache = Cell::new(PrimitiveApproxCache::Empty);
return self;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Directly multiplying the denominator breaks the Rational reduced invariant. For example, to_radians(180) would result in a rational scale of 180/180, which is not reduced to 1/1. This will cause is_one() and other structural checks to fail. Use the / operator instead.

            self.rational = self.rational / Rational::new(180);
            self.class = Pi;
            self.computable = Some(Computable::pi());
            self.primitive_approx_cache = Cell::new(PrimitiveApproxCache::Empty);
            return self;

@timschmidt timschmidt closed this May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants