Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Infinity.jl #95

Closed
wants to merge 12 commits into from
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ name = "Intervals"
uuid = "d8418881-c3e1-53bb-8760-2df7ec849ed5"
license = "MIT"
authors = ["Invenia Technical Computing"]
version = "1.1.0"
version = "1.2.0"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Infinity = "a303e19e-6eb4-11e9-3b09-cd9505f79100"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"

[compat]
Infinity = "0.1"
RecipesBase = "0.8, 1"
TimeZones = "0.7, 0.8, 0.9, 0.10, 0.11, 1"
julia = "1"
Expand Down
2 changes: 2 additions & 0 deletions src/Intervals.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module Intervals

using Dates
using Infinity
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this needed as you explicitly import methods below?

Copy link
Member Author

Choose a reason for hiding this comment

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

If you don’t then you can’t use the infinity symbol correctly. I suppose I could just import it with the other items.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In the context of the package I don't think we're referencing the infinity symbol

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm good point I guess it's just for the tests.

using Infinity: isposinf, isneginf
using Printf
using RecipesBase
using TimeZones
Expand Down
34 changes: 31 additions & 3 deletions src/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,27 @@ struct Interval{T} <: AbstractInterval{T}
end
end

Interval{T}(f, l, inc::Inclusivity) where T = Interval{T}(convert(T, f), convert(T, l), inc)
isbounded(a) = !isposinf(a) && !isneginf(a)
isunbounded(a) = !isbounded(a)
omus marked this conversation as resolved.
Show resolved Hide resolved

function Interval{T}(f, l, inc::Inclusivity) where T
if (isbounded(f) && isbounded(l)) || (isunbounded(f) && isunbounded(l))
return Interval{T}(convert(T, f), convert(T, l), inc)
else
# If either endpoint is unbounded, we want to convert the bounded variable, and then
# try and promote them both to a compatable type.
# If T is a subset of the Infinite type, then don't try to convert at all, as trying
# to convert any type to Infinite will result in an error
if !(T <: Infinite)
if isbounded(f)
f = convert(T, f)
else
l = convert(T, l)
end
end
return Interval(f, l, inc)
end
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think if a user has explicitly set T we shouldn't be messing with it.

Copy link
Member Author

@fchorney fchorney May 28, 2020

Choose a reason for hiding this comment

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

hm yeah I suppose this change is actually just a byproduct of trying to throw in unbounded values into the tests.

This test is specifically the issue (https://github.com/invenia/Intervals.jl/blob/fc/using-infinity/test/interval.jl#L46-L47):

@test Interval(b, a, Inclusivity(true, false)) == Interval{typeof(a)}(a, b, Inclusivity(false, true))

When you create the interval on the left side of == it will promote(b, a) so if one value is and the other is a value, let's say 10, then it converts both values to InfExtended. Now on the right side of ==, if a is of type Infinite (due to being a or -∞) then it will try to convert(Infinite, 10) for b, which is an error. On the other side of the spectrum, if a is 10 which would make it Int64 type, then trying to do convert(Int64, ∞) for b will also fail. I guess we can just let it be an error if somebody puts in the wrong type in the Interval constructor.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the test should be updated to be using:

julia> promote_type(Infinite, Int)
InfExtended{Int64}

Then in the tests you can use Interval{promote_type(typeof(a), typeof(b))}(...)

Interval{T}(f, l, x::Bool, y::Bool) where T = Interval{T}(f, l, Inclusivity(x, y))
Interval{T}(f, l) where T = Interval{T}(f, l, true, true)

Expand Down Expand Up @@ -161,7 +181,8 @@ end

##### ARITHMETIC #####

Base.:+(a::T, b) where {T <: Interval} = T(first(a) + b, last(a) + b, inclusivity(a))
Base.:+(a::Interval, b) = Interval(first(a) + b, last(a) + b, inclusivity(a))
Copy link
Collaborator

Choose a reason for hiding this comment

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

The previous version always kept the Interval unit type the same. This could cause the Interval type to change.

Copy link
Member Author

Choose a reason for hiding this comment

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

This becomes the same issue as most of the other issues that cropped up by adding Infinity. If a is -∞ .. ∞ and b is of type Int then ∞ - b where b is 1 becomes an InfExtended{Int64} type which will error out since T would have been Interval{Infinite}(...). So we end up trying to run convert(Infinite, InfExtended{Int64}) which is undefined. That being said, maybe that shouldn't be undefined. Converting an InfExtended{Int64}) to Infinite could potentially work if we check the value to make sure it's not an actual extended value

Copy link
Collaborator

Choose a reason for hiding this comment

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

That conversion makes sense if the InfExtended{Int64} is infinite

Copy link
Collaborator

Choose a reason for hiding this comment

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

Fixed by adding the conversion: cjdoris/Infinity.jl#7



Base.:+(a, b::Interval) = b + a
Base.:-(a::Interval, b) = a + -b
Expand Down Expand Up @@ -338,7 +359,14 @@ function Base.merge(a::AbstractInterval, b::AbstractInterval)

left = min(LeftEndpoint(a), LeftEndpoint(b))
right = max(RightEndpoint(a), RightEndpoint(b))
return Interval(left, right)

# This promotion fixes the situation where one endpoint has a type of
# `InfExtended{T}` yet the ∞ value is of type `Infinite`.
# This will cause an error when trying to `promote(left, right)`
return Interval(
promote(left.endpoint, right.endpoint)...,
left.included, right.included
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems preferable to change the endpoint constructor (Interval{T}(left::LeftEndpoint{T}, right::RightEndpoint{T}))

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem here is that you'll end up with situations such that

typeof(left) = Intervals.Endpoint{InfExtended{Int64},Intervals.Direction{:Left}()}
typeof(right) = Intervals.Endpoint{Int64,Intervals.Direction{:Right}()}

Thus you don't actually make it to that constructor you mention since the types are different. It ends up trying to call Interval(f, l, inc...) = Interval(promote(f, l)..., inc...) which fails because there are no rules on how to promote two endpoints.

Copy link
Member Author

Choose a reason for hiding this comment

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

Would it make sense to also define a constructor for two endpoints where the types are different? I seems like we typically don't want to change what the Interval Type is, but with Infinity it seems to be required in certain situations

function Interval(left::LeftEndpoint{S}, right::RightEndpoint{D}) where {S, D}
    T = promote_type(S, D)
    return Interval{T}(left.endpoint, right.endpoint, left.included, right.included)
end

Copy link
Collaborator

Choose a reason for hiding this comment

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

That is one option. We may alternatively want to define our own promote_rule

end

##### TIME ZONES #####
Expand Down
60 changes: 35 additions & 25 deletions test/comparisons.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,41 @@ const INTERVAL_TYPES = [Interval, AnchoredInterval{Ending}, AnchoredInterval{Beg
# [12]
# [45]
@testset "non-overlapping" begin
earlier = convert(A, Interval(1, 2, true, true))
later = convert(B, Interval(4, 5, true, true))

@test earlier != later
@test !isequal(earlier, later)
@test hash(earlier) != hash(later)

@test isless(earlier, later)
@test !isless(later, earlier)

@test earlier < later
@test !(later < earlier)

@test earlier ≪ later
@test !(later ≪ earlier)

@test !issubset(earlier, later)
@test !issubset(later, earlier)

@test isempty(intersect(earlier, later))
@test_throws ArgumentError merge(earlier, later)
@test union([earlier, later]) == [earlier, later]
@test !overlaps(earlier, later)
@test !contiguous(earlier, later)
@test superset([earlier, later]) == Interval(1, 5, true, true)
tests = [
((1, 2, true, true), (4, 5, true, true)),
((-∞, 2, true, true), (4, ∞, true, true)),
]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Testsets have a for-loop syntax which would be better as you can output details in the name of the testset (https://docs.julialang.org/en/v1/stdlib/Test/#Test.@testset). I'd probably prefer a distinct testset named "non-overlapping unbounded"

for (a, b) in tests
@show a
@show b
@show A
@show B
earlier = convert(A, Interval(a[1], a[2], a[3], a[4]))
later = convert(B, Interval(b[1], b[2], b[3], b[4]))
Copy link
Member Author

Choose a reason for hiding this comment

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

These converts seem to fail for AnchoredIntervals, and I currently can't seem to figure out how to fix it.

Seems to fail here https://github.com/invenia/Intervals.jl/blob/master/src/anchoredinterval.jl#L165 with the error

a = (-∞, 2, true, true)
b = (4, ∞, true, true)
non-overlapping: Error During Test at /Users/fchorney/Documents/repos/julia/Intervals.jl/test/comparisons.jl:20
  Got exception outside of a @test
  TypeError: in Type, in parameter, expected Int64, got InfExtended{Int64}
  Stacktrace:
   [1] convert(::Type{AnchoredInterval{Intervals.Direction{:Right}(),T} where T}, ::Interval{InfExtended{Int64}}) at /Users/fchorney/Documents/repos/julia/Intervals.jl/src/anchoredinterval.jl:167
   [2] top-level scope at /Users/fchorney/Documents/repos/julia/Intervals.jl/test/comparisons.jl:29
   [3] top-level scope at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.3/Test/src/Test.jl:1107
   [4] top-level scope at /Users/fchorney/Documents/repos/julia/Intervals.jl/test/comparisons.jl:21
   [5] top-level scope at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.3/Test/src/Test.jl:1180
   [6] include at ./boot.jl:328 [inlined]
   [7] include_relative(::Module, ::String) at ./loading.jl:1105
   [8] include(::Module, ::String) at ./Base.jl:31
   [9] include(::String) at ./client.jl:424
   [10] top-level scope at /Users/fchorney/Documents/repos/julia/Intervals.jl/test/runtests.jl:15
   [11] top-level scope at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.3/Test/src/Test.jl:1107
   [12] top-level scope at /Users/fchorney/Documents/repos/julia/Intervals.jl/test/runtests.jl:15
   [13] include at ./boot.jl:328 [inlined]
   [14] include_relative(::Module, ::String) at ./loading.jl:1105
   [15] include(::Module, ::String) at ./Base.jl:31
   [16] include(::String) at ./client.jl:424
   [17] top-level scope at none:6
   [18] eval(::Module, ::Any) at ./boot.jl:330
   [19] exec_options(::Base.JLOptions) at ./client.jl:263
   [20] _start() at ./client.jl:460

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this call is occuring convert(AnchoredInterval{Ending}, Interval(4, ∞, true, true)) which doesn't make sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

I’m unsure why that doesn’t make any sense? Seems like it would work how it used to but now has infinity values in it

Copy link
Collaborator

Choose a reason for hiding this comment

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

The reason it's non-sense is the point at which you're setting the one endpoint used in the AnchoredInterval is . Then we're calculating the other endpoint by subtracting 4 from which is also . There doesn't seem to be a way to do this conversion without losing information which is why I'm thinking this conversion should error. We should detect this and report a nice error.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I wrote up another example here: #89 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

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

ah ok I see. Thanks for the explanation!


@test earlier != later
@test !isequal(earlier, later)
@test hash(earlier) != hash(later)

@test isless(earlier, later)
@test !isless(later, earlier)

@test earlier < later
@test !(later < earlier)

@test earlier ≪ later
@test !(later ≪ earlier)

@test !issubset(earlier, later)
@test !issubset(later, earlier)

@test isempty(intersect(earlier, later))
@test_throws ArgumentError merge(earlier, later)
@test union([earlier, later]) == [earlier, later]
@test !overlaps(earlier, later)
@test !contiguous(earlier, later)
@test superset([earlier, later]) == Interval(a[1], b[2], a[3], b[4])
end
end

# Compare two intervals which "touch" but both intervals do not include that point:
Expand Down
Loading