diff --git a/src/intervals/exact_literals.jl b/src/intervals/exact_literals.jl index 013f0eea..d240db3f 100644 --- a/src/intervals/exact_literals.jl +++ b/src/intervals/exact_literals.jl @@ -261,6 +261,57 @@ for f ∈ (:+, :-, :*, :/, :\, :^) @eval Base.$f(x::ExactReal, y::BareInterval) = $f(promote(x, y)...) end +# These specializations improve efficiency, but always agree precisely with +# the promoting fallbacks above. Several circumstances fall back to promotion: a +# non-finite point (whose thin interval is not even valid), an unbounded interval +# times a point (`0 * ∞` is flavor dependent), and division by zero (likewise). +# The type parameter is bounded so that these methods are more specific than the +# generic `Base.$f(::ExactReal, ::BareInterval)`, whose own parameters are bounded +# by `Real` and `NumTypes`. + +function Base.:+(x::BareInterval{T}, y::ExactReal{T}) where {T<:NumTypes} + v = y.value + isfinite(v) || return +(promote(x, y)...) + isempty_interval(x) && return x + return @round(T, inf(x) + v, sup(x) + v) +end +Base.:+(x::ExactReal{T}, y::BareInterval{T}) where {T<:NumTypes} = y + x + +function Base.:-(x::BareInterval{T}, y::ExactReal{T}) where {T<:NumTypes} + v = y.value + isfinite(v) || return -(promote(x, y)...) + isempty_interval(x) && return x + return @round(T, inf(x) - v, sup(x) - v) +end + +function Base.:-(x::ExactReal{T}, y::BareInterval{T}) where {T<:NumTypes} + v = x.value + isfinite(v) || return -(promote(x, y)...) + isempty_interval(y) && return y + return @round(T, v - sup(y), v - inf(y)) +end + +function Base.:*(x::BareInterval{T}, y::ExactReal{T}) where {T<:NumTypes} + v = y.value + isempty_interval(x) && return x # `isbounded` is true for the empty interval + (isfinite(v) & isbounded(x)) || return *(promote(x, y)...) + signbit(v) && return @round(T, v * sup(x), v * inf(x)) + return @round(T, v * inf(x), v * sup(x)) +end +Base.:*(x::ExactReal{T}, y::BareInterval{T}) where {T<:NumTypes} = y * x + +function Base.:/(x::BareInterval{T}, y::ExactReal{T}) where {T<:NumTypes} + v = y.value + isempty_interval(x) && return x + (isfinite(v) & !iszero(v)) || return /(promote(x, y)...) + signbit(v) && return @round(T, sup(x) / v, inf(x) / v) + return @round(T, inf(x) / v, sup(x) / v) +end + +# `x \ y` is `y / x`; the reverse case is a point divided by an interval, which +# has no shortcut — the zero-crossing tree of `/` is the whole computation. +Base.:\(x::ExactReal{T}, y::BareInterval{T}) where {T<:NumTypes} = y / x + """ has_exact_display(x::Real) diff --git a/src/intervals/intervals.jl b/src/intervals/intervals.jl index 280002ea..053952a1 100644 --- a/src/intervals/intervals.jl +++ b/src/intervals/intervals.jl @@ -5,12 +5,15 @@ include("construction.jl") include("parsing.jl") include("real_interface.jl") export numtype -include("exact_literals.jl") - export ExactReal, exact, @exact, has_exact_display # Rounding include("rounding.jl") +# after `rounding.jl`: the interval operations on `ExactReal` round their +# endpoints with `@round` +include("exact_literals.jl") + export ExactReal, exact, @exact, has_exact_display + # Flavor include("flavor.jl") diff --git a/test/interval_tests/exact_literals.jl b/test/interval_tests/exact_literals.jl index 5c76832b..b740aa68 100644 --- a/test/interval_tests/exact_literals.jl +++ b/test/interval_tests/exact_literals.jl @@ -98,3 +98,59 @@ @test val2 == 3.0 end end + +@testset "Exact literals with bare intervals" begin + # `+`, `-`, `*`, `/` and `\` on a matching number type bypass the promotion to + # a thin interval, so they must be more specific than the generic methods and + # must return exactly what those return. + specialized(f, S, R) = which(f, Tuple{S,R}).sig isa UnionAll + @test all(f -> specialized(f, BareInterval{Float64}, ExactReal{Float64}), (+, -, *, /)) + @test all(f -> specialized(f, ExactReal{Float64}, BareInterval{Float64}), (+, -, *, \)) + + # ... and the mixed number types must not, since the promotion is where the + # `ExactReal` gets rounded to the interval's number type. + @test !specialized(*, BareInterval{Float64}, ExactReal{Int}) + + viapromotion(f, x, y) = f(promote(x, y)...) + samebits(x::BareInterval, y::BareInterval) = (inf(x) === inf(y)) & (sup(x) === sup(y)) + + for T ∈ (Float64, Float32, Rational{Int}) + vals = T <: Rational ? + (zero(T), one(T), -one(T), T(1//2), T(-5//3)) : + (zero(T), -zero(T), one(T), -one(T), T(0.5), T(-2.5), T(0.1), T(-0.1), + floatmax(T), floatmin(T)) + ivs = (bareinterval(T, 1, 2), bareinterval(T, -2, -1), bareinterval(T, -1, 2), + bareinterval(T, 0, 1), bareinterval(T, -1, 0), bareinterval(T, 0, 0), + bareinterval(T(1//10), T(1//10)), bareinterval(T, -Inf, 2), + bareinterval(T, 3, Inf), entireinterval(BareInterval{T}), + emptyinterval(BareInterval{T})) + for v ∈ vals, x ∈ ivs + k = exact(v) + @test samebits(x + k, viapromotion(+, x, k)) + @test samebits(k + x, viapromotion(+, k, x)) + @test samebits(x - k, viapromotion(-, x, k)) + @test samebits(k - x, viapromotion(-, k, x)) + @test samebits(x * k, viapromotion(*, x, k)) + @test samebits(k * x, viapromotion(*, k, x)) + @test samebits(x / k, viapromotion(/, x, k)) + @test samebits(k \ x, viapromotion(\, k, x)) + end + end + + # A non-finite `ExactReal` has no thin interval; both routes warn and return + # the empty interval. + let x = bareinterval(1.0, 2.0) + for v ∈ (Inf, -Inf, NaN) + @test isempty_interval(@test_logs (:warn,) x + exact(v)) + @test isempty_interval(@test_logs (:warn,) x - exact(v)) + @test isempty_interval(@test_logs (:warn,) exact(v) - x) + @test isempty_interval(@test_logs (:warn,) x * exact(v)) + @test isempty_interval(@test_logs (:warn,) x / exact(v)) + end + end + + # Mixed number types still round through the promotion. + @test isequal_interval(bareinterval(1.0, 2.0) * exact(2), bareinterval(2.0, 4.0)) + @test isequal_interval(bareinterval(1.0, 2.0) + exact(1//3), + bareinterval(1.0, 2.0) + bareinterval(Float64, 1//3)) +end