From 75ca738e20b0b08693ce96e3db4be07bb75706b2 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:13:41 +0800 Subject: [PATCH 01/15] fix: Correct docstrings in interval_operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several docstrings in src/intervals/interval_operations/ contained wrong or leftover text; this commit fixes the text only, with zero behavior change. - cancellative.jl (cancelplus): the docstring claimed the result z satisfies y - z == x, but the implementation (cancelminus(x, -y), i.e. z = [inf(x)+sup(y), sup(x)+inf(y)]) satisfies z - y == x. For example, with x = [5, 7] and y = [1, 2], cancelplus(x, y) gives z = [7, 8], for which z - y == x holds while y - z == -x. Reworded to z - y == x. - constants.jl (emptyinterval): the docstring stated the interval invariant backwards ("lower bound is larger than the upper one"); the rule the empty interval is an exception to is lo <= hi. - boolean.jl (precedes, strictprecedes): the tests are universal over x (sup(x) <= inf(y) resp. sup(x) < inf(y)), so "any element of x" is replaced by "every element of x"; also fixed "every elements". - boolean.jl (isweakless): removed the clause "where < is replaced by <= for infinite values", a copy-paste remnant from isstrictless; the stated definition of isweakless contains no <. isstrictless is left unchanged. - set_operations.jl (interiordiff!): the signature line omitted the destination vector; it now reads interiordiff!(v, x, y; dec = :default). - extended_div.jl (extended_div): noted that extended_div(x, y) corresponds to mulRevToPair(y, x) of the standard (the numerator comes first). - Typo sweep in the same docstrings: "keywork" -> "keyword" and "ouptut" -> "output" in cancellative.jl and set_operations.jl. Tested by loading the package (JULIA_LOAD_PATH="@:@stdlib" julia --project=. -e 'using IntervalArithmetic') and by numerically checking the cancelplus identity above; no code paths were modified. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/boolean.jl | 7 +++---- src/intervals/interval_operations/cancellative.jl | 10 +++++----- src/intervals/interval_operations/constants.jl | 2 +- src/intervals/interval_operations/extended_div.jl | 2 ++ src/intervals/interval_operations/set_operations.jl | 6 +++--- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/intervals/interval_operations/boolean.jl b/src/intervals/interval_operations/boolean.jl index 16dd3b8b2..49c4d5d17 100644 --- a/src/intervals/interval_operations/boolean.jl +++ b/src/intervals/interval_operations/boolean.jl @@ -164,8 +164,7 @@ _isdisjoint_interval(x, y, z, w...) = _isdisjoint_interval(x, y) && _isdisjoint_ """ isweakless(x, y) -Test whether `inf(x) ≤ inf(y)` and `sup(x) ≤ sup(y)`, where `<` is replaced by -`≤` for infinite values. +Test whether `inf(x) ≤ inf(y)` and `sup(x) ≤ sup(y)`. Implement the `less` function of the IEEE Standard 1788-2015 (Table 10.3, and Sections 10.5.10 and 12.12.9). @@ -197,7 +196,7 @@ end """ precedes(x, y) -Test whether any element of `x` is lesser or equal to every elements of `y`. +Test whether every element of `x` is lesser or equal to every element of `y`. Implement the `precedes` function of the IEEE Standard 1788-2015 (Table 10.3, and Sections 10.5.10 and 12.12.9). @@ -212,7 +211,7 @@ end """ strictprecedes(x, y) -Test whether any element of `x` is strictly lesser than every elements of `y`. +Test whether every element of `x` is strictly lesser than every element of `y`. Implement the `strictPrecedes` function of the IEEE Standard 1788-2015 (Table 10.3, and Sections 10.5.10 and 12.12.9). diff --git a/src/intervals/interval_operations/cancellative.jl b/src/intervals/interval_operations/cancellative.jl index 1181b31f9..0643c4dd8 100644 --- a/src/intervals/interval_operations/cancellative.jl +++ b/src/intervals/interval_operations/cancellative.jl @@ -7,11 +7,11 @@ Compute the unique interval `z` such that `y + z == x`. -The keywork `dec` argument controls the decoration of the result. It can be +The keyword `dec` argument controls the decoration of the result. It can be either a specific decoration, or one of two following options: - `:default`: if at least one of the input intervals is `ill`, then the result is `ill`, otherwise it is `trv` (Section 11.7.1). - - `:auto`: the ouptut has the minimal decoration of the inputs. + - `:auto`: the output has the minimal decoration of the inputs. Implement the `cancelMinus` function of the IEEE Standard 1788-2015 (Section 9.2). """ @@ -50,14 +50,14 @@ end """ cancelplus(x, y; dec = :default) -Compute the unique interval `z` such that `y - z == x`; this is semantically +Compute the unique interval `z` such that `z - y == x`; this is semantically equivalent to `cancelminus(x, -y)`. -The keywork `dec` argument controls the decoration of the result. It can be +The keyword `dec` argument controls the decoration of the result. It can be either a specific decoration, or one of two following options: - `:default`: if at least one of the input intervals is `ill`, then the result is `ill`, otherwise it is `trv` (Section 11.7.1). - - `:auto`: the ouptut has the minimal decoration of the inputs. + - `:auto`: the output has the minimal decoration of the inputs. Implement the `cancelPlus` function of the IEEE Standard 1788-2015 (Section 9.2). """ diff --git a/src/intervals/interval_operations/constants.jl b/src/intervals/interval_operations/constants.jl index 51fa1f399..88429219e 100644 --- a/src/intervals/interval_operations/constants.jl +++ b/src/intervals/interval_operations/constants.jl @@ -5,7 +5,7 @@ emptyinterval(T=[default_numtype()]) Create an empty interval. This interval is an exception to the fact that the -lower bound is larger than the upper one. +lower bound is smaller than or equal to the upper one. Implement the `empty` function of the IEEE Standard 1788-2015 (Section 10.5.2). """ diff --git a/src/intervals/interval_operations/extended_div.jl b/src/intervals/interval_operations/extended_div.jl index ae3928d32..a122cdcd3 100644 --- a/src/intervals/interval_operations/extended_div.jl +++ b/src/intervals/interval_operations/extended_div.jl @@ -7,6 +7,8 @@ Two-output division. Implement the `mulRevToPair` function of the IEEE Standard 1788-2015 (Section 10.5.5). +`extended_div(x, y)` corresponds to `mulRevToPair(y, x)` of the standard (the +numerator comes first). """ function extended_div(x::BareInterval{T}, y::BareInterval{T}) where {T<:NumTypes} ylo, yhi = bounds(y) diff --git a/src/intervals/interval_operations/set_operations.jl b/src/intervals/interval_operations/set_operations.jl index 89dd57904..25afd0fb7 100644 --- a/src/intervals/interval_operations/set_operations.jl +++ b/src/intervals/interval_operations/set_operations.jl @@ -143,11 +143,11 @@ const union_interval = hull Remove the interior of `y` from `x`. If `x` and `y` are vectors, then they are treated as multi-dimensional intervals. -The keywork `dec` argument controls the decoration of the result. It can be +The keyword `dec` argument controls the decoration of the result. It can be either a specific decoration, or one of two following options: - `:default`: if at least one of the input intervals is `ill`, then the result is `ill`, otherwise it is `trv` (Section 11.7.1). -- `:auto`: the ouptut has the minimal decoration of the inputs. +- `:auto`: the output has the minimal decoration of the inputs. """ interiordiff(x::BareInterval, y::BareInterval) = interiordiff!(Vector{promote_type(typeof(x), typeof(y))}(undef, 0), x, y) @@ -162,7 +162,7 @@ interiordiff(x::AbstractVector{<:Interval}, y::AbstractVector{<:Interval}; dec = interiordiff!(Vector{promote_type(typeof(x), typeof(y))}(undef, 0), x, y; dec = dec) """ - interiordiff!(x, y; dec = :default) + interiordiff!(v, x, y; dec = :default) In-place version of [`interiordiff`](@ref). """ From 63fdb474970e8c6ea5ee02b6e7a60adf7694ff31 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:22 +0800 Subject: [PATCH 02/15] fix: Use any-component semantics for isnai on complex intervals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isnai(x::Complex{<:Interval}) was defined with &, so a complex interval was only reported as NaI when both the real and imaginary parts were NaI. A half-NaI value such as complex(nai(), interval(1)) therefore returned isnai == false, while Base.isnan on the same object returned true (Base's isnan(::Complex) combines the components with |, and this package defines Base.isnan(x::Interval) = isnai(x)). Any NaI component invalidates the whole complex interval, so the two predicates were inconsistent and isnai understated invalidity. The fix changes the & to |, matching the isnan semantics: a complex interval is NaI as soon as either component is NaI. A grep over src/ and test/ found no other code relying on the old & behavior. Tested with a new "isnai" testset in test/interval_tests/complex.jl covering NaI in the real part, the imaginary part, both, neither, and the isnan/isnai consistency; test/interval_tests/complex.jl (128 pass) and test/ITF1788_tests/libieeep1788_bool.jl (392 pass) both succeed. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/boolean.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/intervals/interval_operations/boolean.jl b/src/intervals/interval_operations/boolean.jl index 49c4d5d17..08b044419 100644 --- a/src/intervals/interval_operations/boolean.jl +++ b/src/intervals/interval_operations/boolean.jl @@ -295,7 +295,7 @@ Implement the `isNaI` function of the IEEE Standard 1788-2015 (Section 12.12.9). isnai(::BareInterval) = false isnai(x::Interval) = decoration(x) == ill -isnai(x::Complex{<:Interval}) = isnai(real(x)) & isnai(imag(x)) +isnai(x::Complex{<:Interval}) = isnai(real(x)) | isnai(imag(x)) """ isbounded(x) From 564a2955c8f5126e3e195eecfdef78f10866c92a Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:22 +0800 Subject: [PATCH 03/15] fix: Add promoting fallback for `extended_div` with mixed bound types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `extended_div` only had a bare method requiring identical bound types, and the decorated method forwards to it, so calls with mixed bound types threw a `MethodError`: extended_div(bareinterval(Float32, 1, 2), bareinterval(-4.0, 4.0)) extended_div(interval(Float32, 1, 2), interval(-4.0, 4.0)) Every sibling binary operation already has a promoting fallback (e.g. `cancelminus`, `intersect_interval`, `hull`), so this adds the same one-line pattern to `extended_div`: extended_div(x::BareInterval, y::BareInterval) = extended_div(promote(x, y)...) Tested by adding mixed-type bare and decorated cases to test/interval_tests/numeric.jl, checking they match the both-Float64 results; the file passes (278/278). No ITF1788 conformance file covers mulRevToPair. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/extended_div.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/intervals/interval_operations/extended_div.jl b/src/intervals/interval_operations/extended_div.jl index a122cdcd3..fdb6df5be 100644 --- a/src/intervals/interval_operations/extended_div.jl +++ b/src/intervals/interval_operations/extended_div.jl @@ -23,6 +23,8 @@ function extended_div(x::BareInterval{T}, y::BareInterval{T}) where {T<:NumTypes end end +extended_div(x::BareInterval, y::BareInterval) = extended_div(promote(x, y)...) + function extended_div(x::Interval, y::Interval) bx = bareinterval(x) by = bareinterval(y) From 0bc32637ebc43719d47b5ff55e9a27089a7eaec8 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:22 +0800 Subject: [PATCH 04/15] fix: Fix dist for intervals with shared infinite endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dist computed max(abs(inf(x) - inf(y)), abs(sup(x) - sup(y))), so any pair of intervals sharing an infinite endpoint produced Inf - Inf = NaN: dist(entireinterval(), entireinterval()) returned NaN instead of 0, and dist(interval(-Inf, 0), interval(-Inf, 1)) returned NaN instead of 1. For Rational bounds it was worse: (-1//0) - (-1//0) throws DivideError, so dist on same-side-unbounded rational intervals errored instead of returning 0//1. This adds an internal helper `_dist(a, b)` that returns zero when the two bounds are equal (equal infinite bounds contribute 0, matching the Hausdorff distance between identical unbounded sets), typemax for opposite infinities (which also throw DivideError for Rational on older Julia versions), and the upward-rounded difference otherwise. Rounding the subtraction upward (with the same `_fround` idiom as `diam`) makes `dist` a guaranteed upper bound of the Hausdorff distance, which the docstring now states: round-to-nearest could underestimate the distance by half an ulp, accepting a stopping criterion `dist(x, y) ≤ ε` slightly too early. Both bound pairs of the AbstractFloat and Rational `dist` methods go through the helper; the empty-interval guards are unchanged. Tested with test/interval_tests/numeric.jl and test/ITF1788_tests/libieeep1788_num.jl. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/numeric.jl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/intervals/interval_operations/numeric.jl b/src/intervals/interval_operations/numeric.jl index dd6439fd5..2383513b4 100644 --- a/src/intervals/interval_operations/numeric.jl +++ b/src/intervals/interval_operations/numeric.jl @@ -300,15 +300,17 @@ mig(x::Complex) = inf(abs(interval(x))) """ dist(x, y) -Distance between `x` and `y`. +Upper bound of the Hausdorff distance between `x` and `y`: the bound +differences are rounded upward, and equal bounds, including infinite ones, are +at distance zero. """ function dist(x::BareInterval{T}, y::BareInterval{T}) where {T<:AbstractFloat} isempty_interval(x) | isempty_interval(y) && return convert(T, NaN) - return max(abs(inf(x) - inf(y)), abs(sup(x) - sup(y))) + return max(_dist(inf(x), inf(y)), _dist(sup(x), sup(y))) end function dist(x::BareInterval{T}, y::BareInterval{T}) where {T<:Rational} isempty_interval(x) | isempty_interval(y) && return throw(ArgumentError("cannot compute the distance of empty intervals; cannot return a `Rational` NaN")) - return max(abs(inf(x) - inf(y)), abs(sup(x) - sup(y))) + return max(_dist(inf(x), inf(y)), _dist(sup(x), sup(y))) end dist(x::BareInterval, y::BareInterval) = dist(promote(x, y)...) @@ -321,3 +323,11 @@ function dist(x::Interval{T}, y::Interval{T}) where {T<:Rational} return dist(bareinterval(x), bareinterval(y)) end dist(x::Interval, y::Interval) = dist(promote(x, y)...) + +# used internally, upper bound of `abs(a - b)` with equal, possibly infinite, +# bounds at distance zero +function _dist(a::T, b::T) where {T<:NumTypes} + a == b && return zero(T) + isinf(a) & isinf(b) && return typemax(T) # opposite infinities + return _fround(-, max(a, b), min(a, b), RoundUp) +end From 55c2324da7b35ee3e120a2a93dd3f90e34185f48 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:22 +0800 Subject: [PATCH 05/15] fix: Make scalar mince! monotone and resize the output vector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scalar mince! methods (bare and decorated) had two bugs: 1. LinRange interpolation is not monotone in floating-point arithmetic, so a node could be smaller than its predecessor and produce an invalid interval. For example, mince(bareinterval(1.0, nextfloat(1.0)), 13) returned a piece with lo = 1.0000000000000002 > hi = 1.0, and the decorated version handed it out decorated com with diam == -2.2e-16. 2. The loops wrote v[i] for i in 1:n under @inbounds without checking length(v), so mince!(Vector{BareInterval{Float64}}(undef, 2), bareinterval(0, 1), 8) silently wrote out of bounds (memory corruption; large n segfaults). Both methods now resize!(v, n) upfront, matching the contract of the vector mince! method, and build each piece from the previous upper bound with the next node clamped to [lo, sup(x)] (the last piece ends exactly at sup(x)). This guarantees lo <= hi for every piece, that the first and last pieces hit the exact bounds of x, and that adjacent pieces share endpoints, while keeping the exact node values where they are already fine. Tested with new tests in test/interval_tests/bisect.jl covering the non-monotone examples, the resizing behavior (including the empty/NaI early returns), and the existing quartering behavior; the existing mince tests in test/interval_tests/numeric.jl and multidim.jl still pass. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/bisect.jl | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/intervals/interval_operations/bisect.jl b/src/intervals/interval_operations/bisect.jl index 7b73e7346..dd80d2a48 100644 --- a/src/intervals/interval_operations/bisect.jl +++ b/src/intervals/interval_operations/bisect.jl @@ -58,6 +58,7 @@ _check_minceable(x) = isbounded(x) || In-place version of [`mince`](@ref). """ function mince!(v::AbstractVector{<:BareInterval}, x::BareInterval{T}, n::Integer) where {T<:NumTypes} + resize!(v, n) if isempty_interval(x) # an empty interval has no nodes to interpolate for i ∈ 1:n v[i] = emptyinterval(x) @@ -66,13 +67,18 @@ function mince!(v::AbstractVector{<:BareInterval}, x::BareInterval{T}, n::Intege end _check_minceable(x) nodes = LinRange(inf(x), sup(x), n+1) + lo = inf(x) @inbounds for i ∈ 1:n - v[i] = _unsafe_bareinterval(T, nodes[i], nodes[i+1]) + # `LinRange` may interpolate non-monotonically, clamp the nodes to keep every piece valid + hi = i == n ? sup(x) : min(max(nodes[i+1], lo), sup(x)) + v[i] = _unsafe_bareinterval(T, lo, hi) + lo = hi end return v end function mince!(v::AbstractVector{<:Interval}, x::Interval{T}, n::Integer) where {T<:NumTypes} + resize!(v, n) if isnai(x) | isempty_interval(x) # neither has nodes to interpolate y = isnai(x) ? nai(x) : emptyinterval(x) for i ∈ 1:n @@ -84,10 +90,14 @@ function mince!(v::AbstractVector{<:Interval}, x::Interval{T}, n::Integer) where nodes = LinRange(inf(x), sup(x), n+1) d = decoration(x) t = isguaranteed(x) + lo = inf(x) @inbounds for i ∈ 1:n - rᵢ = _unsafe_bareinterval(T, nodes[i], nodes[i+1]) + # `LinRange` may interpolate non-monotonically, clamp the nodes to keep every piece valid + hi = i == n ? sup(x) : min(max(nodes[i+1], lo), sup(x)) + rᵢ = _unsafe_bareinterval(T, lo, hi) dᵢ = min(d, decoration(rᵢ)) v[i] = _unsafe_interval(rᵢ, dᵢ, t) + lo = hi end return v end From bc6e012d8c7ff7683069a35d3c6d536bac441744 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:23 +0800 Subject: [PATCH 06/15] fix: Clamp the decoration of set operations with `dec = :auto` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With `dec = :auto`, the set operations returned the minimum of the input decorations without reconciling it with what the result interval can carry. In particular, intersect_interval(interval(1, 2), interval(3, 4); dec = :auto) returned `∅_com`, an empty interval decorated `com`, which is invalid per the IEEE Standard 1788-2015 (an empty interval can only be `trv` or `ill`). The explicit-decoration path was unaffected since it goes through `setdecoration`, which clamps such combinations (Section 11.5.2): `dec = com` on the same call already yielded `∅_trv`. The fix routes the `:auto` branch of `_set_decoration` through the same clamping setter, `setdecoration(x, decoration(x))`, so the above call now returns `∅_trv` while valid combinations are unchanged: the hull of two `com` intervals stays `com`, and an unbounded hull stays `dac`. This covers all users of `_set_decoration`: `intersect_interval`, `hull`, `interiordiff` and the cancellative operations. The four `dec === :default && return _unsafe_interval(r, trv, t)` fast-paths in `intersect_interval` and `hull` are also removed: they were provably equivalent to falling through to `_set_decoration` (after the `ill` early return, `min(trv, d) == trv`, and `setdecoration` with `trv` hits none of its clamping branches, so both paths produce `_unsafe_interval(r, trv, t)` — checked exhaustively over empty, unbounded, thin and common results with both guarantee flags), and the other callsites (`cancelminus`, `interiordiff!`) already relied on `_set_decoration` alone. Every callsite now uses the same single idiom. Tested with test/interval_tests/set_operations.jl and test/ITF1788_tests/libieeep1788_set.jl and libieeep1788_cancel.jl. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/set_operations.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/intervals/interval_operations/set_operations.jl b/src/intervals/interval_operations/set_operations.jl index 25afd0fb7..713a8ab00 100644 --- a/src/intervals/interval_operations/set_operations.jl +++ b/src/intervals/interval_operations/set_operations.jl @@ -5,7 +5,7 @@ _set_decoration(x::Interval, d::Decoration) = setdecoration(x, d) function _set_decoration(x::Interval, d::Symbol) - d === :auto && return x + d === :auto && return setdecoration(x, decoration(x)) d === :default && return setdecoration(x, min(trv, decoration(x))) return throw(ArgumentError("decoration option must be `:default`, `:auto` or a specific decoration")) end @@ -39,7 +39,6 @@ function intersect_interval(x::Interval{T}, y::Interval{S}; dec = :default) wher d == ill && return nai(promote_type(T, S)) # one of the inputs is an NaI r = intersect_interval(_bareinterval(x), _bareinterval(y)) t = isguaranteed(x) & isguaranteed(y) - dec === :default && return _unsafe_interval(r, trv, t) return _set_decoration(_unsafe_interval(r, d, t), dec) end @@ -60,7 +59,6 @@ function intersect_interval(x::Interval, y::Interval, z::Interval, w::Vararg{Int d = mapreduce(decoration, min, xs) d == ill && return nai(numtype(r)) # one of the inputs is an NaI t = mapreduce(isguaranteed, &, xs) - dec === :default && return _unsafe_interval(r, trv, t) return _set_decoration(_unsafe_interval(r, d, t), dec) end @@ -98,7 +96,6 @@ function hull(x::Interval{T}, y::Interval{S}; dec = :default) where {T<:NumTypes d == ill && return nai(promote_type(T, S)) # one of the inputs is an NaI r = hull(_bareinterval(x), _bareinterval(y)) t = isguaranteed(x) & isguaranteed(y) - dec === :default && return _unsafe_interval(r, trv, t) return _set_decoration(_unsafe_interval(r, d, t), dec) end @@ -119,7 +116,6 @@ function hull(x::Interval, y::Interval, z::Interval, w::Vararg{Interval,N}; dec d = mapreduce(decoration, min, xs) d == ill && return nai(numtype(r)) # one of the inputs is an NaI t = mapreduce(isguaranteed, &, xs) - dec === :default && return _unsafe_interval(r, trv, t) return _set_decoration(_unsafe_interval(r, d, t), dec) end From 7c26e3d2ac8d064edbd135b399fd4083daaaf9b4 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:23 +0800 Subject: [PATCH 07/15] fix: Round the radius upward in `midradius` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generic `midradius(x::BareInterval)` computed the radius as `max(m - inf(x), sup(x) - m)` with round-to-nearest subtractions, so the enclosure promised by the `radius` docstring, `issubset_interval(x, mid(x) ± radius(x))`, could fail. For instance, `x = interval(-3.514641998989496e-149, 2.2210542297708545e247)` gave `mid(x) ± radius(x) == [0.0, 2.2210542297708545e247]`, losing the negative part of `x`; a random sweep over finite `Float64` bounds showed the containment failing for roughly half of wide intervals. IEEE 1788-2015 Section 12.12.8 requires the radius to be rounded upward. The fix computes both differences with `_fround(-, ..., RoundUp)`, the same helper `diam` already uses. `mid` is unchanged, the `Rational` method is exact arithmetic and stays as is, and the empty/entire cases still return `(NaN, NaN)` and `(0.0, Inf)` respectively. Three midpoint-format display tests pinned the buggy value: for `interval(-floatmin(Float64), 1.3)` the old radius `0.65` gave `[0.0, 1.3]`, which does not contain `-floatmin(Float64)`; the correct upward-rounded radius now displays as `0.650001` (`0.6504` in `Float16`), matching the documented display guarantee that `m_d ± r_d` encloses the interval. These tests were updated accordingly. Tested with test/interval_tests/numeric.jl (including a new seeded random sweep of 10^4 intervals asserting the containment property), the full test/ITF1788_tests suite (7654 passing), and the complex, consistency, display and rounding test files. `Random` was added to test/Project.toml for the seeded sweep. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/numeric.jl | 2 +- test/interval_tests/display.jl | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/intervals/interval_operations/numeric.jl b/src/intervals/interval_operations/numeric.jl index 2383513b4..b829eb116 100644 --- a/src/intervals/interval_operations/numeric.jl +++ b/src/intervals/interval_operations/numeric.jl @@ -215,7 +215,7 @@ See also: [`inf`](@ref), [`sup`](@ref), [`bounds`](@ref), [`mid`](@ref), """ function midradius(x::BareInterval) m = mid(x) - return m, max(m - inf(x), sup(x) - m) + return m, max(_fround(-, m, inf(x), RoundUp), _fround(-, sup(x), m, RoundUp)) # cf. Section 12.12.8 end function midradius(x::BareInterval{<:Rational}) # needed to avoid integer overflow error m = mid(x) diff --git a/test/interval_tests/display.jl b/test/interval_tests/display.jl index bb035c0f6..d9e7c095d 100644 --- a/test/interval_tests/display.jl +++ b/test/interval_tests/display.jl @@ -50,7 +50,7 @@ setprecision(BigFloat, 256) do @test sprint(show, MIME("text/plain"), emptyinterval(BareInterval{Float64})) == "∅" - @test sprint(show, MIME("text/plain"), a) == "0.65 ± 0.65" + @test sprint(show, MIME("text/plain"), a) == "0.65 ± 0.650001" @test sprint(show, MIME("text/plain"), large_expo) == "(5.0e+123456788 ± 5.0e+123456788)₂₅₆" end @@ -156,9 +156,9 @@ setprecision(BigFloat, 256) do @test sprint(show, MIME("text/plain"), a) == "(1.5 ± 0.5)_com" @test sprint(show, MIME("text/plain"), a_NG) == "(1.5 ± 0.5)_com_NG" - @test sprint(show, MIME("text/plain"), b) == "(0.65 ± 0.65)_com" - @test sprint(show, MIME("text/plain"), b32) == "(0.65f0 ± 0.65f0)_com" - @test sprint(show, MIME("text/plain"), b16) == "(Float16(0.65) ± Float16(0.65))_com" + @test sprint(show, MIME("text/plain"), b) == "(0.65 ± 0.650001)_com" + @test sprint(show, MIME("text/plain"), b32) == "(0.65f0 ± 0.650001f0)_com" + @test sprint(show, MIME("text/plain"), b16) == "(Float16(0.65) ± Float16(0.6504))_com" @test sprint(show, MIME("text/plain"), br) == "(1//10 ± 6//5)_com" @test sprint(show, MIME("text/plain"), c) == "(1.79769e+308 ± ∞)_dac" @test sprint(show, MIME("text/plain"), cr) == "(9223372036854775807//1 ± ∞)_dac" @@ -174,9 +174,9 @@ setprecision(BigFloat, 256) do @test sprint(show, MIME("text/plain"), a) == "1.5 ± 0.5" @test sprint(show, MIME("text/plain"), a_NG) == "(1.5 ± 0.5)_NG" - @test sprint(show, MIME("text/plain"), b) == "0.65 ± 0.65" - @test sprint(show, MIME("text/plain"), b32) == "0.65f0 ± 0.65f0" - @test sprint(show, MIME("text/plain"), b16) == "Float16(0.65) ± Float16(0.65)" + @test sprint(show, MIME("text/plain"), b) == "0.65 ± 0.650001" + @test sprint(show, MIME("text/plain"), b32) == "0.65f0 ± 0.650001f0" + @test sprint(show, MIME("text/plain"), b16) == "Float16(0.65) ± Float16(0.6504)" @test sprint(show, MIME("text/plain"), br) == "1//10 ± 6//5" @test sprint(show, MIME("text/plain"), c) == "1.79769e+308 ± ∞" @test sprint(show, MIME("text/plain"), cr) == "9223372036854775807//1 ± ∞" From 516208abba5d43d9120623f6633c09b7a14a3b07 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:23 +0800 Subject: [PATCH 08/15] fix: Fix isstrictsubset for a real interval in a complex interval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isstrictsubset(x::Interval, y::Complex{<:Interval}) required x to be a STRICT subset of real(y), missing the case where x == real(y) and imag(y) strictly contains [0, 0]: isstrictsubset(interval(0, 1), complex(interval(0, 1), interval(-1, 1))) returned false, while the mathematically identical complex(interval(0, 1), interval(0, 0)) as first argument returned true. By the docstring's own definition ("subset of, but not equal to"), and consistently with the mixed issubset_interval and isequal_interval methods, the correct result is true. All the Interval/Complex methods of isstrictsubset now use the same definitional idiom as the two real methods: `issubset_interval(x, y) & !isequal_interval(x, y)`. For the Complex/Complex and (Complex, Interval) methods this is a pure refactoring: the old componentwise formulas are Boolean-equivalent (exhaustively checked over all combinations of regular, thin, empty, entire and NaI components); only the (Interval, Complex) method changes behavior, fixing the bug above. Delegating to issubset_interval and isequal_interval keeps the three predicates mutually consistent by construction instead of re-deriving edge cases per method. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/boolean.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/intervals/interval_operations/boolean.jl b/src/intervals/interval_operations/boolean.jl index 08b044419..9e23cc092 100644 --- a/src/intervals/interval_operations/boolean.jl +++ b/src/intervals/interval_operations/boolean.jl @@ -88,10 +88,9 @@ See also: [`issubset_interval`](@ref) and [`isinterior`](@ref). isstrictsubset(x::BareInterval, y::BareInterval) = issubset_interval(x, y) & !isequal_interval(x, y) isstrictsubset(x::Interval, y::Interval) = issubset_interval(x, y) & !isequal_interval(x, y) -isstrictsubset(x::Complex{<:Interval}, y::Complex{<:Interval}) = - (isstrictsubset(real(x), real(y)) & issubset_interval(imag(x), imag(y))) | (issubset_interval(real(x), real(y)) & isstrictsubset(imag(x), imag(y))) -isstrictsubset(x::Complex{<:Interval}, y::Interval) = isstrictsubset(real(x), y) & isthinzero(imag(x)) -isstrictsubset(x::Interval, y::Complex{<:Interval}) = isstrictsubset(x, real(y)) & in_interval(0, imag(y)) +isstrictsubset(x::Complex{<:Interval}, y::Complex{<:Interval}) = issubset_interval(x, y) & !isequal_interval(x, y) +isstrictsubset(x::Complex{<:Interval}, y::Interval) = issubset_interval(x, y) & !isequal_interval(x, y) +isstrictsubset(x::Interval, y::Complex{<:Interval}) = issubset_interval(x, y) & !isequal_interval(x, y) isstrictsubset(x::AbstractVector, y::AbstractVector) = issubset_interval(x, y) & any(t -> isstrictsubset(t[1], t[2]), zip(x, y)) From 69a698ea9785e1662d566413a867b8bc4e2e9322 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:23 +0800 Subject: [PATCH 09/15] fix: Guard mixed Complex/Interval isdisjoint_interval against NaI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mixed methods isdisjoint_interval(x::Complex{<:Interval}, y::Interval) = isdisjoint_interval(real(x), y) | !in_interval(0, imag(x)) (and its mirror) inverted the NaI-safe `false` of `in_interval` into an unsafe assertion of disjointness: isdisjoint_interval(complex(interval(1, 2), nai()), interval(0, 3)) returned true, claiming certain disjointness from invalid data. The convention for decorated comparisons (Section 12.12.9 of the IEEE Standard 1788-2015, cf. the Interval/Interval method) is that any NaI input yields false. Both methods now start with the same `isnai(x) | isnai(y) && return false` guard as the Interval/Interval method; for the complex argument this relies on `isnai(::Complex)` flagging a single NaI component, fixed in an earlier commit. The Complex/Complex method is already NaI-safe (both components route through the NaI-guarded Interval method) and is unchanged. Non-NaI behavior is unchanged. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/boolean.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/intervals/interval_operations/boolean.jl b/src/intervals/interval_operations/boolean.jl index 9e23cc092..52585b393 100644 --- a/src/intervals/interval_operations/boolean.jl +++ b/src/intervals/interval_operations/boolean.jl @@ -144,8 +144,14 @@ function isdisjoint_interval(x::Interval, y::Interval) return isdisjoint_interval(bareinterval(x), bareinterval(y)) end isdisjoint_interval(x::Complex{<:Interval}, y::Complex{<:Interval}) = isdisjoint_interval(real(x), real(y)) | isdisjoint_interval(imag(x), imag(y)) -isdisjoint_interval(x::Complex{<:Interval}, y::Interval) = isdisjoint_interval(real(x), y) | !in_interval(0, imag(x)) -isdisjoint_interval(x::Interval, y::Complex{<:Interval}) = isdisjoint_interval(x, real(y)) | !in_interval(0, imag(y)) +function isdisjoint_interval(x::Complex{<:Interval}, y::Interval) + isnai(x) | isnai(y) && return false + return isdisjoint_interval(real(x), y) | !in_interval(0, imag(x)) +end +function isdisjoint_interval(x::Interval, y::Complex{<:Interval}) + isnai(x) | isnai(y) && return false + return isdisjoint_interval(x, real(y)) | !in_interval(0, imag(y)) +end function isdisjoint_interval(x::AbstractVector, y::AbstractVector) n = length(x) From 2c1097d086a1d65770ee286d70696f37aec703e0 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:23 +0800 Subject: [PATCH 10/15] fix: Return NaN for `mag` and `mig` of empty complex intervals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For a complex interval with an empty component, `abs(interval(x))` is the empty interval, so `mag(x::Complex)` returned `sup(∅) == -Inf` and `mig(x::Complex)` returned `inf(∅) == Inf` — a negative magnitude, with `mag(x) < mig(x)`. For instance: julia> mag(complex(emptyinterval(), interval(1, 2))) -Inf julia> mig(complex(emptyinterval(), interval(1, 2))) Inf The real-interval methods instead return `NaN` for empty intervals (or throw the documented `ArgumentError` for `Rational`). `mag(x::Complex)` and `mig(x::Complex)` now guard for emptiness and delegate to the real empty-interval methods via `emptyinterval(real(z))`, preserving the `AbstractFloat` NaN and `Rational` `ArgumentError` conventions. Non-empty inputs are unchanged. Tested with new cases in test/interval_tests/complex.jl covering empty and mixed-empty inputs for `Float64` and `Rational{Int}`, plus unchanged non-empty behavior; the complex.jl and numeric.jl test files pass. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/numeric.jl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/intervals/interval_operations/numeric.jl b/src/intervals/interval_operations/numeric.jl index b829eb116..347012120 100644 --- a/src/intervals/interval_operations/numeric.jl +++ b/src/intervals/interval_operations/numeric.jl @@ -263,7 +263,11 @@ function mag(x::Interval{<:Rational}) end mag(x::Real) = mag(interval(x)) -mag(x::Complex) = sup(abs(interval(x))) +function mag(x::Complex) + z = interval(x) + isempty_interval(z) && return mag(emptyinterval(real(z))) + return sup(abs(z)) +end """ mig(x) @@ -295,7 +299,11 @@ function mig(x::Interval{<:Rational}) end mig(x::Real) = mig(interval(x)) -mig(x::Complex) = inf(abs(interval(x))) +function mig(x::Complex) + z = interval(x) + isempty_interval(z) && return mig(emptyinterval(real(z))) + return inf(abs(z)) +end """ dist(x, y) From c5612d0d728497280e19a32c3623328dbd927cff Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:23 +0800 Subject: [PATCH 11/15] fix: Fix MethodError of cancelminus for Rational intervals and propagate NaI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in `cancelminus` (and hence `cancelplus`): 1. The corner case 1 guard in the bare method eagerly evaluated `prevfloat(inf(x))` and `nextfloat(sup(x))` (non-short-circuit `&` and `|`), which have no `Rational` methods. As a result, `cancelminus(interval(1//1, 2//1), interval(1//2, 1//1))` threw a `MethodError`, making the operation unusable for every non-degenerate `Rational` input. Since rational interval arithmetic is exact, corner case 1 (a finite-precision artifact) cannot occur, so the guard now only runs for `AbstractFloat` bounds. The Rational example above now returns the exact result `[1//2, 1//1]`. 2. The decorated method had no `ill` check, so an explicit `dec` keyword overwrote the `ill` decoration via `_set_decoration`: `cancelminus(nai(Float64), interval(1, 2); dec = com)` returned a non-NaI interval, and every NaI call also emitted the spurious "interval part of NaI" warning from `bareinterval`. The method now follows the `intersect_interval` pattern: it computes the minimal decoration of the inputs first, early-returns an NaI when it is `ill`, and uses the internal `_bareinterval` accessor. `cancelplus` flows through automatically. Tested with new cases in test/interval_tests/consistency.jl (exact Rational results for `cancelminus`/`cancelplus`, the Rational vs empty edge, and NaI propagation for both argument orders and `dec` options, checking with `@test_logs` that no warning is emitted). The test/ITF1788_tests/libieeep1788_cancel.jl conformance suite passes unchanged (242/242), so floating-point behavior is unaffected. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/cancellative.jl | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/intervals/interval_operations/cancellative.jl b/src/intervals/interval_operations/cancellative.jl index 0643c4dd8..ea1590c1e 100644 --- a/src/intervals/interval_operations/cancellative.jl +++ b/src/intervals/interval_operations/cancellative.jl @@ -32,17 +32,21 @@ function cancelminus(x::BareInterval{T}, y::BareInterval{T}) where {T<:NumTypes} z = _unsafe_bareinterval(T, z_lo, z_hi) isunbounded(z) && return z - # corner case 1 (page 62) involving finite precision for diam(x) and diam(y) - w_lo, w_hi = bounds(@round(T, inf(y) + z_lo, sup(y) + z_hi)) - (diam(x) == diam(y)) & ((prevfloat(inf(x)) > w_lo) | (nextfloat(sup(x)) < w_hi)) && return entireinterval(BareInterval{T}) + if T <: AbstractFloat # rational arithmetic is exact, so corner case 1 cannot occur + # corner case 1 (page 62) involving finite precision for diam(x) and diam(y) + w_lo, w_hi = bounds(@round(T, inf(y) + z_lo, sup(y) + z_hi)) + (diam(x) == diam(y)) & ((prevfloat(inf(x)) > w_lo) | (nextfloat(sup(x)) < w_hi)) && return entireinterval(BareInterval{T}) + end return z end cancelminus(x::BareInterval, y::BareInterval) = cancelminus(promote(x, y)...) function cancelminus(x::Interval, y::Interval; dec = :default) - r = cancelminus(bareinterval(x), bareinterval(y)) - d = min(decoration(x), decoration(y), decoration(r)) + d = min(decoration(x), decoration(y)) + d == ill && return nai(promote_type(numtype(x), numtype(y))) # one of the inputs is an NaI + r = cancelminus(_bareinterval(x), _bareinterval(y)) + d = min(d, decoration(r)) t = isguaranteed(x) & isguaranteed(y) return _set_decoration(_unsafe_interval(r, d, t), dec) end From b308633efeed271ff848b8da7e9aec30cdfe23a4 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:23 +0800 Subject: [PATCH 12/15] fix: Correct piece selection in `interiordiff` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scalar, vector and decorated code paths of `interiordiff` each picked the surviving pieces of `x \ int(y)` with ad hoc branching, and several of those branches selected the wrong piece or fabricated invalid intervals: - `interiordiff(interval(2, 3), interval(1, 3))` returned `{2}`, a point inside the interior of `y`, instead of `{3}`: the `isweakless` branch picked the left piece whenever the intersection equals `x`. - `interiordiff([interval(1, 2)], [interval(1, 3)])` returned `{2}` instead of `{1}`: in `_interiordiff` the `inf(x) == inf(inter)` branch fired before checking the supremum, so the scalar and vector paths disagreed on identical inputs. - `interiordiff([interval(1, 3)], [interval(1, 2)])` lost the boundary point `{1}`: the shared-endpoint branches of `_interiordiff` returned only one piece. - `interiordiff(interval(-Inf, 3), interval(-Inf, 2))` returned the invalid piece `[-Inf, -Inf]` (likewise `_interiordiff` for unbounded `x == y`): `±Inf` is never a member of an interval in the set-based flavor. All paths now share a single corrected `_interiordiff` core based on the set identity: for non-disjoint, non-interior inputs, the left piece `[inf(x), inf(y)]` survives iff `inf(x) <= inf(y)` and `inf(y)` is finite (an infinite bound of `y` means its interior is unbounded on that side), and symmetrically for the right piece `[sup(y), sup(x)]`. Further fixes included: - The multi-dimensional disjoint early return produced a box containing an empty component (e.g. `interiordiff([emptyinterval(Interval{Float64}), interval(1, 2)], [interval(1, 2), interval(1, 2)])` returned one box containing the empty interval); it now mirrors the filter of the general case and produces no boxes when a component of `x` is empty or NaI. - The decorated scalar disjoint branch stored `x` verbatim, ignoring the `dec` keyword and the guarantee flag of `y`. It now decorates the result like every other branch, so `dec = :default` yields `trv` (as the docstring already stated) and `isguaranteed` is the conjunction of the inputs. This deliberately changes the decoration of disjoint results from the input decoration to the documented one. - The decorated scalar path had no NaI guard: `interiordiff(nai(), interval(1, 2))` fabricated an interval with NaN bounds through `_unsafe_bareinterval`. An NaI input now yields the single element `nai(T)` (NaI in, NaI out), matching the convention of `intersect_interval` and `hull`. No existing test pinned the buggy behavior; all previous assertions in test/interval_tests/set_operations.jl and test/interval_tests/multidim.jl pass unchanged. New tests cover the regression cases above in scalar and 1-element vector forms, a scalar/vector agreement sweep over all interval pairs with endpoints in -1:0.5:3, a pointwise membership oracle for `x \ int(y)`, and the decorated `dec`/`isguaranteed`/NaI behavior. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- .../interval_operations/set_operations.jl | 79 +++++++------------ 1 file changed, 28 insertions(+), 51 deletions(-) diff --git a/src/intervals/interval_operations/set_operations.jl b/src/intervals/interval_operations/set_operations.jl index 713a8ab00..aee0c4533 100644 --- a/src/intervals/interval_operations/set_operations.jl +++ b/src/intervals/interval_operations/set_operations.jl @@ -163,25 +163,10 @@ interiordiff(x::AbstractVector{<:Interval}, y::AbstractVector{<:Interval}; dec = In-place version of [`interiordiff`](@ref). """ function interiordiff!(v::AbstractVector, x::BareInterval{T}, y::BareInterval{T}) where {T<:NumTypes} - if isinterior(x, y) - empty!(v) - elseif isdisjoint_interval(x, y) - resize!(v, 1) - @inbounds v[begin] = x - else - inter = intersect_interval(x, y) - if issubset_interval(y, x) - resize!(v, 2) - @inbounds v[begin] = _unsafe_bareinterval(T, inf(x), inf(inter)) - @inbounds v[end] = _unsafe_bareinterval(T, sup(inter), sup(x)) - elseif isweakless(x, inter) - resize!(v, 1) - @inbounds v[begin] = _unsafe_bareinterval(T, inf(x), inf(inter)) - else - resize!(v, 1) - @inbounds v[begin] = _unsafe_bareinterval(T, sup(inter), sup(x)) - end - end + h₁, h₂, _ = _interiordiff(x, y, nothing) + empty!(v) + isempty_interval(h₁) || push!(v, h₁) + isempty_interval(h₂) || push!(v, h₂) return v end interiordiff!(v::AbstractVector, x::BareInterval, y::BareInterval) = interiordiff!(v, promote(x, y)...) @@ -189,31 +174,17 @@ interiordiff!(v::AbstractVector{<:AbstractVector}, x::AbstractVector{<:BareInter _interiordiff!(v, x, y, nothing) function interiordiff!(v::AbstractVector, x::Interval{T}, y::Interval{T}; dec = :default) where {T<:NumTypes} - if isinterior(x, y) - empty!(v) - elseif isdisjoint_interval(x, y) + d = min(decoration(x), decoration(y)) + if d == ill # one of the inputs is an NaI resize!(v, 1) - @inbounds v[begin] = x - else - d = min(decoration(x), decoration(y)) - t = isguaranteed(x) & isguaranteed(y) - inter = intersect_interval(x, y) - if issubset_interval(y, x) - resize!(v, 2) - r1 = _unsafe_bareinterval(T, inf(x), inf(inter)) - @inbounds v[begin] = _set_decoration(_unsafe_interval(r1, d, t), dec) - r2 = _unsafe_bareinterval(T, sup(inter), sup(x)) - @inbounds v[end] = _set_decoration(_unsafe_interval(r2, d, t), dec) - elseif isweakless(x, inter) - resize!(v, 1) - r1 = _unsafe_bareinterval(T, inf(x), inf(inter)) - @inbounds v[begin] = _set_decoration(_unsafe_interval(r1, d, t), dec) - else - resize!(v, 1) - r2 = _unsafe_bareinterval(T, sup(inter), sup(x)) - @inbounds v[begin] = _set_decoration(_unsafe_interval(r2, d, t), dec) - end + @inbounds v[begin] = nai(T) + return v end + t = isguaranteed(x) & isguaranteed(y) + h₁, h₂, _ = _interiordiff(_bareinterval(x), _bareinterval(y), nothing) + empty!(v) + isempty_interval(h₁) || push!(v, _set_decoration(_unsafe_interval(h₁, d, t), dec)) + isempty_interval(h₂) || push!(v, _set_decoration(_unsafe_interval(h₂, d, t), dec)) return v end interiordiff!(v::AbstractVector, x::Interval, y::Interval; dec = :default) = interiordiff!(v, promote(x, y)...; dec = dec) @@ -228,8 +199,14 @@ function _interiordiff!(v::AbstractVector{<:AbstractVector}, x::AbstractVector, N == len || return throw(DimensionMismatch("x has length $N, y has length $len")) if any(t -> isdisjoint_interval(t[1], t[2]), zip(x, y)) - resize!(v, 1) - @inbounds v[begin] = copy(x) + # mirror the filter of the general case: an empty or NaI component + # means that `x` does not describe a box + if any(t -> isempty_interval(t) | isnai(t), x) + empty!(v) + else + resize!(v, 1) + @inbounds v[begin] = copy(x) + end else resize!(v, 2N) @@ -257,16 +234,16 @@ function _interiordiff!(v::AbstractVector{<:AbstractVector}, x::AbstractVector, end function _interiordiff(x::BareInterval{T}, y::BareInterval{T}, ::Nothing) where {T<:NumTypes} - isdisjoint_interval(x, y) && return (x, emptyinterval(BareInterval{T}), emptyinterval(BareInterval{T})) - inter = intersect_interval(x, y) + isdisjoint_interval(x, y) && return (x, emptyinterval(BareInterval{T}), inter) isinterior(x, y) && return (emptyinterval(BareInterval{T}), emptyinterval(BareInterval{T}), inter) - isequal_interval(x, y) && return (_unsafe_bareinterval(T, inf(x), inf(x)), _unsafe_bareinterval(T, sup(x), sup(x)), inter) - - inf(x) == inf(inter) && return (_unsafe_bareinterval(T, sup(inter), sup(x)), emptyinterval(BareInterval{T}), inter) - sup(x) == sup(inter) && return (_unsafe_bareinterval(T, inf(x), inf(inter)), emptyinterval(BareInterval{T}), inter) - return (_unsafe_bareinterval(T, inf(x), inf(y)), _unsafe_bareinterval(T, sup(y), sup(x)), inter) + # a piece of `x` survives on a given side only if the corresponding bound + # of `y` is finite, since an infinite bound means that the interior of `y` + # is unbounded on that side + h₁ = (inf(x) ≤ inf(y)) & (inf(y) != typemin(T)) ? _unsafe_bareinterval(T, inf(x), inf(y)) : emptyinterval(BareInterval{T}) + h₂ = (sup(y) ≤ sup(x)) & (sup(y) != typemax(T)) ? _unsafe_bareinterval(T, sup(y), sup(x)) : emptyinterval(BareInterval{T}) + return (h₁, h₂, inter) end _interiordiff(x::BareInterval, y::BareInterval, dec::Nothing) = _interiordiff(promote(x, y)..., dec) From fa950b25ee6b89fe4f837a033ff789e379b75804 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:38:23 +0800 Subject: [PATCH 13/15] fix: Prevent interval_diff from upgrading decoration and guarantee MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The internal interval_diff rebuilt its pieces with the checked interval(a, b) constructor, which raises the decoration and always marks the result as guaranteed. Consequently, interval_diff(interval(1, 10, def), interval(2, 5)) returned pieces decorated com (raised from def) with isguaranteed == true even when an input was not guaranteed, silently upgrading an unguaranteed enclosure to a guaranteed one. There was also no NaI guard: an NaI operand fell through to bound arithmetic on NaN, triggering the checked constructor's ill-formed interval warning. The fix mirrors the conventions already used in this file (e.g. intersect_interval, interiordiff!): compute the minimal decoration of the inputs and return [nai(T)] if it is ill, propagate isguaranteed(x) & isguaranteed(y), and build the pieces with _unsafe_interval(_unsafe_bareinterval(T, lo, hi), trv, t), so the result decoration is trv at best, matching the Section 11.7.1 default used across this file. The disjoint branch, which previously returned x verbatim, now follows the same construction convention: it returns x's bare interval decorated trv with the propagated guarantee, for consistency with the other branches. The branch structure and the computed bounds are unchanged. Tested by extending the interval_diff testset in test/interval_tests/set_operations.jl with decoration, guarantee, and NaI assertions; all 243 tests in the file pass, and the pre-existing value assertions are untouched. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- .../interval_operations/set_operations.jl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/intervals/interval_operations/set_operations.jl b/src/intervals/interval_operations/set_operations.jl index aee0c4533..ec3503ddd 100644 --- a/src/intervals/interval_operations/set_operations.jl +++ b/src/intervals/interval_operations/set_operations.jl @@ -257,15 +257,20 @@ end # function interval_diff(x::Interval{T}, y::Interval) where {T<:NumTypes} - isdisjoint_interval(x, y) && return [x] + d = min(decoration(x), decoration(y)) + d == ill && return [nai(T)] # one of the inputs is an NaI + t = isguaranteed(x) & isguaranteed(y) + # the pieces are decorated `trv` (Section 11.7.1), including in the + # disjoint branch where `x` is returned with its decoration downgraded + isdisjoint_interval(x, y) && return [_unsafe_interval(_bareinterval(x), trv, t)] issubset_interval(x, y) && return Interval{T}[] intersection = intersect_interval(x, y) - inf(x) == inf(intersection) && return [interval(sup(intersection), sup(x))] - sup(x) == sup(intersection) && return [interval(inf(x), inf(intersection))] + inf(x) == inf(intersection) && return [_unsafe_interval(_unsafe_bareinterval(T, sup(intersection), sup(x)), trv, t)] + sup(x) == sup(intersection) && return [_unsafe_interval(_unsafe_bareinterval(T, inf(x), inf(intersection)), trv, t)] return [ - interval(inf(x), inf(intersection)), - interval(sup(intersection), sup(x)) + _unsafe_interval(_unsafe_bareinterval(T, inf(x), inf(intersection)), trv, t), + _unsafe_interval(_unsafe_bareinterval(T, sup(intersection), sup(x)), trv, t) ] end From c17a5a93cb87fdf9a03da738e1701ebd0cf582f1 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:33:56 +0800 Subject: [PATCH 14/15] =?UTF-8?q?fix:=20Take=20`=CE=B1`=20into=20account?= =?UTF-8?q?=20for=20`mid`=20of=20unbounded=20intervals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For a half-unbounded interval, `mid(x, α)` returned the Section 12.12.8 clamp regardless of `α`: `mid(interval(-Inf, 0), 1)` gave `-floatmax` instead of the upper bound 0 promised by the docstring, and the finite bound was unreachable for every `α`. `α` now acts as a threshold, mirroring the existing entire-interval branch: the finite bound is returned when `α` selects its side (`α > 0.5` for a finite upper bound, `α < 0.5` for a finite lower bound), and the infinite bound is replaced by the largest finite value of the bound type otherwise, so `mid(x)` and `mid(x, 0.5)` keep the values required by Section 12.12.8 of the IEEE Standard 1788-2015 (as pinned by the ITF1788 test suite). The entire-interval branch and bounded intervals are unchanged, and the docstring states the new behavior. As a side effect, `mid` of an unbounded `BareInterval{Rational{BigInt}}` no longer throws a `MethodError` when `α` selects the finite bound (the other paths still hit the undefined `typemin(BigInt)`; that remains a known issue). [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/numeric.jl | 25 ++++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/intervals/interval_operations/numeric.jl b/src/intervals/interval_operations/numeric.jl index 347012120..60f29d6fc 100644 --- a/src/intervals/interval_operations/numeric.jl +++ b/src/intervals/interval_operations/numeric.jl @@ -83,7 +83,10 @@ bounds(x::Real) = bounds(interval(x)) Relative midpoint of `x`, for `α` between 0 and 1 such that `mid(x, 0)` is the lower bound of the interval, `mid(x, 1)` its upper bound, and `mid(x, 0.5)` its -midpoint. +midpoint. For an unbounded interval, the finite bound is returned whenever `α` +selects its side, and the infinite bound is replaced by the largest finite +value of the bound type otherwise (cf. Section 12.12.8 of the IEEE Standard +1788-2015). Implement the `mid` function of the IEEE Standard 1788-2015 (Table 9.2). @@ -99,8 +102,14 @@ function mid(x::BareInterval{T}, α = 0.5) where {T<:AbstractFloat} return nextfloat(typemin(T)) else lo, hi = bounds(x) - lo == typemin(T) && return nextfloat(lo) # cf. Section 12.12.8 - hi == typemax(T) && return prevfloat(hi) # cf. Section 12.12.8 + if lo == typemin(T) # cf. Section 12.12.8 + α > 0.5 && return _normalisezero(hi) + return nextfloat(lo) + end + if hi == typemax(T) # cf. Section 12.12.8 + α < 0.5 && return _normalisezero(lo) + return prevfloat(hi) + end β = convert(T, α) midpoint = β * (hi + lo * (1/β - 1)) # exactly 0.5 * (hi + lo) for β = 0.5 midpoint = ifelse(isfinite(midpoint), midpoint, (1 - β) * lo + β * hi) @@ -118,8 +127,14 @@ function mid(x::BareInterval{Rational{T}}, α = 1//2) where {T<:Integer} return convert(Rational{T}, typemin(T)) else lo, hi = bounds(x) - lo == typemin(Rational{T}) && return convert(Rational{T}, typemin(T)) # cf. Section 12.12.8 - hi == typemax(Rational{T}) && return convert(Rational{T}, typemax(T)) # cf. Section 12.12.8 + if lo == typemin(Rational{T}) # cf. Section 12.12.8 + α > 0.5 && return _normalisezero(hi) + return convert(Rational{T}, typemin(T)) + end + if hi == typemax(Rational{T}) # cf. Section 12.12.8 + α < 0.5 && return _normalisezero(lo) + return convert(Rational{T}, typemax(T)) + end β = convert(Rational{T}, α) return _normalisezero((1 - β) * lo + β * hi) end From 29b77b9c959db27445c778ca2663f47a11842560 Mon Sep 17 00:00:00 2001 From: OlivierHnt <38465572+OlivierHnt@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:42:05 +0800 Subject: [PATCH 15/15] fix: Throw an informative error for `mid` of unbounded `Rational{BigInt}` intervals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Rational` method of `mid` clamps infinite bounds with `convert(Rational{T}, typemin(T))` / `typemax(T)`, mirroring the float clamps `nextfloat(typemin(T))` / `prevfloat(typemax(T))` of Section 12.12.8. For `T = BigInt` these do not exist, so `mid` (and `radius` and `midradius`, which call it) threw a raw `MethodError: no method matching typemin(::Type{BigInt})` for unbounded `Rational{BigInt}` intervals — a supported bound type, with such intervals reachable from ordinary arithmetic. The principle is that `Rational` bounds mirror the behavior of float bounds, and whenever that is impossible an informative error is thrown (as the existing "cannot return a `Rational` NaN" refusals do). The clamps now go through `_finitemin` / `_finitemax` helpers: fixed-width integer types keep the current `±typemax(T)//1` values, while `Rational{BigInt}` throws a descriptive `ArgumentError` explaining that `BigInt` has no smallest or largest finite value. Paths that never need the clamp are unaffected: `α` selecting the finite bound, `mid` of the entire interval at `α = 1/2`, and `midradius` of the entire interval all keep working for `Rational{BigInt}`. [The regression tests written for this fix are contributed separately, as part of the test-suite rework.] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- src/intervals/interval_operations/numeric.jl | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/intervals/interval_operations/numeric.jl b/src/intervals/interval_operations/numeric.jl index 60f29d6fc..7dfe16f3d 100644 --- a/src/intervals/interval_operations/numeric.jl +++ b/src/intervals/interval_operations/numeric.jl @@ -78,6 +78,16 @@ end bounds(x::Real) = bounds(interval(x)) +# analogues of `nextfloat(typemin(T))` and `prevfloat(typemax(T))` for +# `Rational` bounds (cf. Section 12.12.8); unbounded integer types such as +# `BigInt` have no smallest or largest finite value +_finitemin(::Type{Rational{T}}) where {T<:Integer} = convert(Rational{T}, typemin(T)) +_finitemax(::Type{Rational{T}}) where {T<:Integer} = convert(Rational{T}, typemax(T)) +_finitemin(::Type{Rational{BigInt}}) = + throw(ArgumentError("cannot compute the midpoint of unbounded intervals with `Rational{BigInt}` bounds; there is no smallest finite `Rational{BigInt}`")) +_finitemax(::Type{Rational{BigInt}}) = + throw(ArgumentError("cannot compute the midpoint of unbounded intervals with `Rational{BigInt}` bounds; there is no largest finite `Rational{BigInt}`")) + """ mid(x, α = 0.5) @@ -123,17 +133,17 @@ function mid(x::BareInterval{Rational{T}}, α = 1//2) where {T<:Integer} isempty_interval(x) && return throw(ArgumentError("cannot compute the midpoint of empty intervals; cannot return a `Rational` NaN")) if isentire_interval(x) α == 0.5 && return zero(Rational{T}) - α > 0.5 && return convert(Rational{T}, typemax(T)) - return convert(Rational{T}, typemin(T)) + α > 0.5 && return _finitemax(Rational{T}) + return _finitemin(Rational{T}) else lo, hi = bounds(x) if lo == typemin(Rational{T}) # cf. Section 12.12.8 α > 0.5 && return _normalisezero(hi) - return convert(Rational{T}, typemin(T)) + return _finitemin(Rational{T}) end if hi == typemax(Rational{T}) # cf. Section 12.12.8 α < 0.5 && return _normalisezero(lo) - return convert(Rational{T}, typemax(T)) + return _finitemax(Rational{T}) end β = convert(Rational{T}, α) return _normalisezero((1 - β) * lo + β * hi)