From 8c037c4afb06303146ed7af183c8a330a2d919a6 Mon Sep 17 00:00:00 2001 From: Eric Lavault <39483232+lvlte@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:13:07 +0100 Subject: [PATCH 1/8] Fix `rationalize([T,] x::Rational; ...)` - Fix `tol` ignored when `T` is not specified. - Specific implementation for rationals (vs floats): return `x` unless the given `tol` is significant enough to reduce the magnitude of its components, in which case rationalize `float(x)` instead (see #60768). --- base/rational.jl | 15 +++++++++++++-- test/rational.jl | 13 +++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/base/rational.jl b/base/rational.jl index b48e8a359e346..a2bca6f0079d3 100644 --- a/base/rational.jl +++ b/base/rational.jl @@ -244,7 +244,7 @@ julia> typeof(numerator(a)) BigInt ``` """ -function rationalize(::Type{T}, x::Union{AbstractFloat, Rational}, tol::Real) where T<:Integer +function rationalize(::Type{T}, x::AbstractFloat, tol::Real) where T<:Integer if tol < 0 throw(ArgumentError("negative tolerance $tol")) end @@ -309,7 +309,18 @@ rationalize(x::Real; kvs...) = rationalize(Int, x; kvs...) rationalize(::Type{T}, x::Complex; kvs...) where {T<:Integer} = Complex(rationalize(T, x.re; kvs...), rationalize(T, x.im; kvs...)) rationalize(x::Complex; kvs...) = Complex(rationalize(Int, x.re; kvs...), rationalize(Int, x.im; kvs...)) rationalize(::Type{T}, x::Rational; tol::Real = 0) where {T<:Integer} = rationalize(T, x, tol) -rationalize(x::Rational; kvs...) = x +rationalize(x::Rational{T}; kvs...) where {T<:Integer} = rationalize(T, x; kvs...) +function rationalize(::Type{T}, x::Rational, tol::Real) where {T<:Integer} + T<:Unsigned && x < 0 && __throw_negate_unsigned() + if 0 ≤ tol ≤ eps(float(x)) + try + return Rational{T}(x) + catch e + isa(e,InexactError) || rethrow() + end + end + return rationalize(T, float(x), tol) +end rationalize(x::Integer; kvs...) = Rational(x) function rationalize(::Type{T}, x::Integer; kvs...) where {T<:Integer} if Base.hastypemax(T) # BigInt doesn't diff --git a/test/rational.jl b/test/rational.jl index 93b049f22f465..4aa1419d7a429 100644 --- a/test/rational.jl +++ b/test/rational.jl @@ -879,3 +879,16 @@ end @test 1.0 != big(1//0) @test Inf == big(1//0) end + +@testset "rationalize(Rational) (issue #60768)" begin + x = float(pi) + r = rationalize(x) + @test rationalize(Int32, r, tol=0.0) === Rational{Int32}(r) == r + @test rationalize(r) === rationalize(r, tol=0) === r + @test rationalize(r, tol=eps(float(r))) === r + @test rationalize(r, tol=0.1) == 16//5 + for n=1:10 + @test rationalize(r, tol=1/10^n) == rationalize(float(r), tol=1/10^n) + end + @test_throws OverflowError rationalize(UInt, -r) +end From e97d914caede677ec3209553375da6d8f16e59da Mon Sep 17 00:00:00 2001 From: Eric Lavault <39483232+lvlte@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:16:49 +0100 Subject: [PATCH 2/8] Fix `rationalize(x)` accuracy for tiny `x` Convert `x` to a float type of higher precision when needed, so that `a, r = divrem(x,y)` remain exact in the while loop (this adds type instability and breaks inference so the function return type needs to be specified). --- base/rational.jl | 18 +++++++++++------- test/rational.jl | 8 ++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/base/rational.jl b/base/rational.jl index a2bca6f0079d3..6bcfb37782c05 100644 --- a/base/rational.jl +++ b/base/rational.jl @@ -244,21 +244,25 @@ julia> typeof(numerator(a)) BigInt ``` """ -function rationalize(::Type{T}, x::AbstractFloat, tol::Real) where T<:Integer - if tol < 0 - throw(ArgumentError("negative tolerance $tol")) - end - +function rationalize(::Type{T}, x::AbstractFloat, tol::Real)::Rational{T} where T<:Integer + tol < 0 && throw(ArgumentError("negative tolerance $tol")) T<:Unsigned && x < 0 && __throw_negate_unsigned() isnan(x) && return T(x)//one(T) isinf(x) && return unsafe_rational(x < 0 ? -one(T) : one(T), zero(T)) + r, a = modf(abs(x)) + if r > tol && 1/r ≥ maxintfloat(x) + p = exponent(1/r) + 1 + if p > precision(Float64) + return setprecision(() -> rationalize(T, BigFloat(x), tol), BigFloat, p) + end + x, a, r = Float64.((x, a, r)) + end + p, q = (x < 0 ? -one(T) : one(T)), zero(T) pp, qq = zero(T), one(T) x = abs(x) - a = trunc(x) - r = x-a y = one(x) tolx = oftype(x, tol) nt, t, tt = tolx, zero(tolx), tolx diff --git a/test/rational.jl b/test/rational.jl index 4aa1419d7a429..59b8ed05903c6 100644 --- a/test/rational.jl +++ b/test/rational.jl @@ -892,3 +892,11 @@ end end @test_throws OverflowError rationalize(UInt, -r) end + +@testset "rationalize(x) with tiny x (issue #49803, #49848)" begin + for (T, U) in ((Int32, Float16), (Int64, Float32), (BigInt, Float64), (BigInt, BigFloat)) + x = prevfloat(1/maxintfloat(U)) + r = rationalize(T, x, tol=0) + @test abs(r - x) == 0 + end +end From 66e616aa261996633a9e731b84e6d6a0cc27af1d Mon Sep 17 00:00:00 2001 From: Eric Lavault <39483232+lvlte@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:26:24 +0100 Subject: [PATCH 3/8] Fix `rationalize(T, x)` with `x` negative integer and `T` unsigned --- base/rational.jl | 1 + test/rational.jl | 1 + 2 files changed, 2 insertions(+) diff --git a/base/rational.jl b/base/rational.jl index 6bcfb37782c05..25e53ecb151a1 100644 --- a/base/rational.jl +++ b/base/rational.jl @@ -327,6 +327,7 @@ function rationalize(::Type{T}, x::Rational, tol::Real) where {T<:Integer} end rationalize(x::Integer; kvs...) = Rational(x) function rationalize(::Type{T}, x::Integer; kvs...) where {T<:Integer} + T<:Unsigned && x < 0 && __throw_negate_unsigned() if Base.hastypemax(T) # BigInt doesn't x < typemin(T) && return unsafe_rational(-one(T), zero(T)) x > typemax(T) && return unsafe_rational(one(T), zero(T)) diff --git a/test/rational.jl b/test/rational.jl index 59b8ed05903c6..831d655305cda 100644 --- a/test/rational.jl +++ b/test/rational.jl @@ -39,6 +39,7 @@ using Test @test @inferred(rationalize(Int8, 1000//3)) === Rational{Int8}(1//0) @test @inferred(rationalize(Int8, 1000)) === Rational{Int8}(1//0) @test_throws OverflowError rationalize(UInt, -2.0) + @test_throws OverflowError rationalize(UInt, -2) @test_throws ArgumentError rationalize(Int, big(3.0), -1.) # issue 26823 @test_throws InexactError rationalize(Int, NaN) From 9045610fa34262fa82f380e6a5848981c81b734e Mon Sep 17 00:00:00 2001 From: Eric Lavault <39483232+lvlte@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:16:49 +0100 Subject: [PATCH 4/8] More descriptive error message Co-authored-by: Oscar Smith --- base/rational.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/rational.jl b/base/rational.jl index 25e53ecb151a1..8bf3cb560d80b 100644 --- a/base/rational.jl +++ b/base/rational.jl @@ -245,7 +245,7 @@ BigInt ``` """ function rationalize(::Type{T}, x::AbstractFloat, tol::Real)::Rational{T} where T<:Integer - tol < 0 && throw(ArgumentError("negative tolerance $tol")) + tol < 0 && throw(ArgumentError("Tolerance can not be negative. tol=$tol")) T<:Unsigned && x < 0 && __throw_negate_unsigned() isnan(x) && return T(x)//one(T) isinf(x) && return unsafe_rational(x < 0 ? -one(T) : one(T), zero(T)) From 0f0f4695c03d2343f8bface3c55c0d60a8a1ab6c Mon Sep 17 00:00:00 2001 From: Eric Lavault <39483232+lvlte@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:19:19 +0100 Subject: [PATCH 5/8] This will be faster since `inv(maxintfloat(x))` is a constant Co-authored-by: Oscar Smith --- base/rational.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/rational.jl b/base/rational.jl index 8bf3cb560d80b..d2a19cf2e82e3 100644 --- a/base/rational.jl +++ b/base/rational.jl @@ -251,7 +251,7 @@ function rationalize(::Type{T}, x::AbstractFloat, tol::Real)::Rational{T} where isinf(x) && return unsafe_rational(x < 0 ? -one(T) : one(T), zero(T)) r, a = modf(abs(x)) - if r > tol && 1/r ≥ maxintfloat(x) + if r > tol && r ≤ inv(maxintfloat(x)) p = exponent(1/r) + 1 if p > precision(Float64) return setprecision(() -> rationalize(T, BigFloat(x), tol), BigFloat, p) From 0276a685e02f071d17a400d79cb0a88e067263e8 Mon Sep 17 00:00:00 2001 From: Eric Lavault <39483232+lvlte@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:05:53 +0100 Subject: [PATCH 6/8] Fix computation of the precision required Co-authored-by: Oscar Smith --- base/rational.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/rational.jl b/base/rational.jl index d2a19cf2e82e3..e643972aa2b49 100644 --- a/base/rational.jl +++ b/base/rational.jl @@ -252,7 +252,7 @@ function rationalize(::Type{T}, x::AbstractFloat, tol::Real)::Rational{T} where r, a = modf(abs(x)) if r > tol && r ≤ inv(maxintfloat(x)) - p = exponent(1/r) + 1 + p = 1 - exponent(r) if p > precision(Float64) return setprecision(() -> rationalize(T, BigFloat(x), tol), BigFloat, p) end From 9a5ed9f2a9990401d15f9732ba7bb7bc12069753 Mon Sep 17 00:00:00 2001 From: Eric Lavault <39483232+lvlte@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:20:29 +0100 Subject: [PATCH 7/8] Add tests for subnormal numbers --- test/rational.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/rational.jl b/test/rational.jl index 831d655305cda..65a5d917f2666 100644 --- a/test/rational.jl +++ b/test/rational.jl @@ -895,9 +895,19 @@ end end @testset "rationalize(x) with tiny x (issue #49803, #49848)" begin - for (T, U) in ((Int32, Float16), (Int64, Float32), (BigInt, Float64), (BigInt, BigFloat)) + for (T, U) in ((Int32, Float16), (Int64, Float32), (Int128, Float64), (BigInt, BigFloat)) x = prevfloat(1/maxintfloat(U)) r = rationalize(T, x, tol=0) @test abs(r - x) == 0 + if U != BigFloat + # x subnormal + x = prevfloat(floatmin(U)) + r = rationalize(widen(T), x, tol=0) + @test abs(r - x) == 0 + # x subnormal, inv(x) infinite + x = inv(floatmax(U)) + r = rationalize(BigInt, x, tol=0) + @test abs(r - x) == 0 + end end end From 106d53b95e0cbaf32691c615cb5dfb4d6f7353b9 Mon Sep 17 00:00:00 2001 From: Eric Lavault <39483232+lvlte@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:26:48 +0100 Subject: [PATCH 8/8] Remove trailing whitespace --- base/rational.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/rational.jl b/base/rational.jl index e643972aa2b49..c53e311dbe068 100644 --- a/base/rational.jl +++ b/base/rational.jl @@ -252,7 +252,7 @@ function rationalize(::Type{T}, x::AbstractFloat, tol::Real)::Rational{T} where r, a = modf(abs(x)) if r > tol && r ≤ inv(maxintfloat(x)) - p = 1 - exponent(r) + p = 1 - exponent(r) if p > precision(Float64) return setprecision(() -> rationalize(T, BigFloat(x), tol), BigFloat, p) end