Skip to content

Commit c37cbb7

Browse files
Arvind Maanomus
andcommitted
Create minimum/maximum functions (#141)
* Add min/max functions with precision * Add tests for new min/max functions * Add default val for closed/unbounded intervals in min/max * Rename min/max functions to minimum/maximum * Rename percision kwarg in min/max to increment * Add integer specific min/max function * Add float specific min/max functions * Respect the increment variable in AbstractFloat min/max * Apply suggestions and add more tests * Add unbounded accessor tests * Add anchoredinterval support in min/max * Adjust accessor test structure * Handle various edge cases in min/max * Add docstrings for min/max * Update isfinite.jl to support TimeTypes * Apply suggested changes * Update docs to include bound-open Co-authored-by: Curtis Vogt <curtis.vogt@gmail.com>
1 parent a9d99c9 commit c37cbb7

5 files changed

Lines changed: 238 additions & 1 deletion

File tree

src/docstrings.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,31 @@ Note using `!isbounded` is commonly used to determine if any end of the interval
105105
unbounded.
106106
"""
107107
isbounded(::AbstractInterval)
108+
109+
"""
110+
minimum(interval::AbstractInterval{T}; [increment]) -> T
111+
112+
The minimum value contained within the `interval`.
113+
114+
If left-closed, returns `first(interval)`.
115+
If left-open, returns `first(interval) + eps(first(interval))`
116+
If left-unbounded, returns minimum value possible for type `T`.
117+
118+
A `BoundsError` is thrown for empty intervals or when the increment results in a minimum value
119+
not-contained by the interval.
120+
"""
121+
minimum(::AbstractInterval; increment)
122+
123+
"""
124+
maximum(interval::AbstractInterval{T}; [increment]) -> T
125+
126+
The maximum value contained within the `interval`.
127+
128+
If right-closed, returns `last(interval)`.
129+
If right-open, returns `first(interval) + eps(first(interval))`
130+
If right-unbounded, returns maximum value possible for type `T`.
131+
132+
A `BoundsError` is thrown for empty intervals or when the increment results in a maximum value
133+
not-contained by the interval.
134+
"""
135+
maximum(::AbstractInterval; increment)

src/interval.jl

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,65 @@ Base.isopen(interval::AbstractInterval{T,L,R}) where {T,L,R} = L === Open && R =
187187
isunbounded(interval::AbstractInterval{T,L,R}) where {T,L,R} = L === Unbounded && R === Unbounded
188188
isbounded(interval::AbstractInterval{T,L,R}) where {T,L,R} = L !== Unbounded && R !== Unbounded
189189

190+
function Base.minimum(interval::AbstractInterval{T,L,R}; increment=nothing) where {T,L,R}
191+
return L === Unbounded ? typemin(T) : first(interval)
192+
end
193+
194+
function Base.minimum(interval::AbstractInterval{T,Open,R}; increment=eps(T)) where {T,R}
195+
isempty(interval) && throw(BoundsError(interval, 0))
196+
min_val = first(interval) + increment
197+
# Since intervals can't have NaN, we can just use !isfinite to check if infinite
198+
!isfinite(min_val) && return typemin(T)
199+
min_val interval && return min_val
200+
throw(BoundsError(interval, min_val))
201+
end
202+
203+
function Base.minimum(interval::AbstractInterval{T,Open,R}) where {T<:Integer,R}
204+
return minimum(interval, increment=one(T))
205+
end
206+
207+
function Base.minimum(interval::AbstractInterval{T,Open,R}; increment=nothing) where {T<:AbstractFloat,R}
208+
isempty(interval) && throw(BoundsError(interval, 0))
209+
min_val = first(interval)
210+
# Since intervals can't have NaN, we can just use !isfinite to check if infinite
211+
next_val = if !isfinite(min_val) || increment === nothing
212+
nextfloat(min_val)
213+
else
214+
min_val + increment
215+
end
216+
next_val interval && return next_val
217+
throw(BoundsError(interval, next_val))
218+
end
219+
220+
function Base.maximum(interval::AbstractInterval{T,L,R}; increment=nothing) where {T,L,R}
221+
return R === Unbounded ? typemax(T) : last(interval)
222+
end
223+
224+
function Base.maximum(interval::AbstractInterval{T,L,Open}; increment=eps(T)) where {T,L}
225+
isempty(interval) && throw(BoundsError(interval, 0))
226+
max_val = last(interval) - increment
227+
# Since intervals can't have NaN, we can just use !isfinite to check if infinite
228+
!isfinite(max_val) && return typemax(T)
229+
max_val interval && return max_val
230+
throw(BoundsError(interval, max_val))
231+
end
232+
233+
function Base.maximum(interval::AbstractInterval{T,L,Open}) where {T<:Integer,L}
234+
return maximum(interval, increment=one(T))
235+
end
236+
237+
function Base.maximum(interval::AbstractInterval{T,L,Open}; increment=nothing) where {T<:AbstractFloat,L}
238+
isempty(interval) && throw(BoundsError(interval, 0))
239+
max_val = last(interval)
240+
next_val = if !isfinite(max_val) || increment === nothing
241+
prevfloat(max_val)
242+
else
243+
max_val - increment
244+
end
245+
next_val interval && return next_val
246+
throw(BoundsError(interval, next_val))
247+
end
248+
190249
##### CONVERSION #####
191250

192251
# Allows an interval to be converted to a scalar when the set contained by the interval only
@@ -201,6 +260,7 @@ end
201260

202261
##### DISPLAY #####
203262

263+
204264
function Base.show(io::IO, interval::Interval{T,L,R}) where {T,L,R}
205265
if get(io, :compact, false)
206266
print(io, interval)

src/isfinite.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
# `Char` and `Period` as well as other types.
33
isfinite(x) = iszero(x - x)
44
isfinite(x::Real) = Base.isfinite(x)
5+
isfinite(x::Union{Type{T}, T}) where T<:TimeType = Base.isfinite(x)

test/anchoredinterval.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
166166

167167
@test first(interval) == DateTime(2016, 8, 11, 1, 45)
168168
@test last(interval) == dt
169+
@test minimum(interval) == first(interval)
170+
@test maximum(interval) == last(interval)
169171
@test bounds_types(interval) == (Closed, Closed)
170172
@test span(interval) == -P
171173

@@ -175,6 +177,8 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
175177

176178
@test first(interval) == Date(2016, 8, 11)
177179
@test last(interval) == Date(2016, 8, 12)
180+
@test_throws BoundsError minimum(interval, increment=Day(1))
181+
@test_throws BoundsError maximum(interval, increment=Day(1))
178182
@test bounds_types(interval) == (Open, Open)
179183
@test span(interval) == P
180184

@@ -187,6 +191,8 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
187191
interval = AnchoredInterval{Day(1)}(startpoint)
188192
@test first(interval) == startpoint
189193
@test last(interval) == ZonedDateTime(2018, 3, 12, tz"America/Winnipeg")
194+
@test minimum(interval) == startpoint
195+
@test maximum(interval, increment=Hour(1)) == last(interval) - Hour(1)
190196
@test span(interval) == Day(1)
191197

192198
endpoint = ZonedDateTime(2018, 11, 4, 2, tz"America/Winnipeg")
@@ -197,24 +203,32 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
197203
interval = AnchoredInterval{Day(1)}(startpoint)
198204
@test first(interval) == startpoint
199205
@test last(interval) == ZonedDateTime(2018, 11, 5, tz"America/Winnipeg")
206+
@test minimum(interval) == startpoint
207+
@test maximum(interval, increment=Millisecond(1)) == last(interval) - Millisecond(1)
200208
@test span(interval) == Day(1)
201209

202210
endpoint = ZonedDateTime(2020, 3, 9, 2, tz"America/Winnipeg")
203211
interval = AnchoredInterval{Day(-1)}(endpoint)
204212
@test_throws NonExistentTimeError first(interval)
205213
@test last(interval) == endpoint
214+
@test_throws NonExistentTimeError minimum(interval, increment=Hour(1))
215+
@test maximum(interval) == endpoint
206216
@test span(interval) == Day(1)
207217

208218
# Non-period AnchoredIntervals
209219
interval = AnchoredInterval{-10}(10)
210220
@test first(interval) == 0
211221
@test last(interval) == 10
222+
@test minimum(interval) == 1
223+
@test maximum(interval) == 10
212224
@test bounds_types(interval) == (Open, Closed)
213225
@test span(interval) == 10
214226

215227
interval = AnchoredInterval{25}('a')
216228
@test first(interval) == 'a'
217229
@test last(interval) == 'z'
230+
@test minimum(interval) == 'a'
231+
@test maximum(interval, increment=1) == 'y'
218232
@test bounds_types(interval) == (Closed, Open)
219233
@test span(interval) == 25
220234
end

test/interval.jl

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@
123123
for (a, b, _) in test_values
124124
for (L, R) in BOUND_PERMUTATIONS
125125
interval = Interval{L, R}(a, b)
126-
127126
@test first(interval) == a
128127
@test last(interval) == b
129128
@test span(interval) == b - a
@@ -144,6 +143,141 @@
144143
@test span(interval) == Hour(3)
145144
end
146145

146+
@testset "maximum/minimum" begin
147+
# Helper functions that manage the value we should be expecting from min and max.
148+
function _min_val_helper(interval, a, unit)
149+
t = eltype(interval)
150+
# If the interal is empty, min is nothing
151+
isempty(interval) && return nothing
152+
153+
# If a is in the interval, it is closed/unbounded and min is the first value.
154+
# If a is nothing then it is unbounded and min is typemin(T)
155+
a === nothing && return typemin(t)
156+
a interval && return first(interval)
157+
158+
# From this point on, b ∉ interval so the bound is Open
159+
# Also, if a is infinite we return typemin
160+
# If it's an abstractfloat, we can't return just typemin since typemin IS Inf and
161+
# since the bound is open at this point, Inf ∉ interval So we return the one after INF
162+
!Intervals.isfinite(a) && t <: AbstractFloat && return nextfloat(a)
163+
!Intervals.isfinite(a) && return typemin(t)
164+
165+
f = first(interval)
166+
nv = if t <: AbstractFloat && unit === nothing
167+
nextfloat(f)
168+
else
169+
f + unit
170+
end
171+
172+
nv interval && return nv
173+
174+
# If we get to this point, the min/max functions throw a DomainError
175+
# Since we want our tests to be predictable, we will not throw an error in this helper.
176+
end
177+
178+
function _max_val_helper(interval, b, unit)
179+
t = eltype(interval)
180+
# If the interal is empty, min is nothing
181+
isempty(interval) && return nothing
182+
183+
# If a is in the interval, it is closed/unbounded and min is the first value.
184+
# If a is nothing then it is unbounded and min is typemin(T)
185+
b === nothing && return typemax(t)
186+
b interval && return last(interval)
187+
188+
# From this point on, b ∉ interval so the bound is Open
189+
# Also, if a is infinite we return typemin
190+
# If it's an abstractfloat, we can't return just typemin since typemin IS Inf and
191+
# since the bound is open at this point, Inf ∉ interval So we return the one after INF
192+
!isfinite(b) && t <: AbstractFloat && return prevfloat(b)
193+
!isfinite(b) && return typemax(t)
194+
195+
l = last(interval)
196+
nv = if t <: AbstractFloat && unit === nothing
197+
prevfloat(l)
198+
else
199+
l - unit
200+
end
201+
202+
nv interval && return nv
203+
204+
# If we get to this point, the min/max functions throw a DomainError
205+
# Since we want our tests to be predictable, we will not throw an error in this helper.
206+
end
207+
@testset "bounded intervals" begin
208+
bounded_test_vals = [
209+
#test nextfloat and prevfloat
210+
(-10.0, 10.0, nothing),
211+
(-Inf, Inf, nothing),
212+
213+
('c', 'x', 2),
214+
(Date(2004, 2, 13), Date(2020, 3, 13), Day(1)),
215+
]
216+
for (a, b, unit) in append!(bounded_test_vals, test_values)
217+
for (L, R) in BOUND_PERMUTATIONS
218+
interval = Interval{L, R}(a, b)
219+
220+
mi = _min_val_helper(interval, a, unit)
221+
ma = _max_val_helper(interval, b, unit)
222+
223+
@test minimum(interval; increment=unit) == mi
224+
@test maximum(interval; increment=unit) == ma
225+
end
226+
end
227+
end
228+
229+
@testset "unbounded intervals" begin
230+
unbounded_test_values = [
231+
# one side unbounded with different types
232+
(Interval{Open,Unbounded}(-10, nothing), 1),
233+
(Interval{Unbounded,Closed}(nothing, 1.0), 0.01),
234+
(Interval{Unbounded,Open}(nothing, 'z'), 1),
235+
(Interval{Closed,Unbounded}(Date(2013, 2, 13), nothing), Day(1)),
236+
(Interval{Open,Unbounded}(DateTime(2016, 8, 11, 0, 30), nothing), Millisecond(1)),
237+
# both sides unbounded different types
238+
(Interval{Int}(nothing, nothing), 1),
239+
(Interval{Float64}(nothing, nothing), 0.01),
240+
(Interval{Char}(nothing , nothing), 1),
241+
(Interval{Day}(nothing, nothing), Day(1)),
242+
(Interval{DateTime}(nothing, nothing), Millisecond(1)),
243+
# test adding eps() with unbounded
244+
(Interval{Open,Unbounded}(-10.0, nothing), nothing),
245+
(Interval{Unbounded,Open}(nothing, 10.0), nothing),
246+
# test infinity
247+
(Interval{Open,Unbounded}(-Inf, nothing), nothing),
248+
(Interval{Unbounded,Open}(nothing, Inf), nothing),
249+
]
250+
for (interval, unit) in unbounded_test_values
251+
a, b = first(interval), last(interval)
252+
253+
mi = _min_val_helper(interval, a, unit)
254+
ma = _max_val_helper(interval, b, unit)
255+
256+
@test minimum(interval; increment=unit) == mi
257+
@test maximum(interval; increment=unit) == ma
258+
@test_throws DomainError span(interval)
259+
260+
end
261+
end
262+
@testset "bounds errors in min/max" begin
263+
error_test_vals = [
264+
# empty intervals
265+
(Interval{Open,Open}(-10, -10), 1),
266+
(Interval{Open,Open}(0.0, 0.0), 60),
267+
(Interval{Open,Open}(Date(2013, 2, 13), Date(2013, 2, 13)), Day(1)),
268+
(Interval{Open,Open}(DateTime(2016, 8, 11, 0, 30), DateTime(2016, 8, 11, 0, 30)), Day(1)),
269+
# increment too large
270+
(Interval{Open,Open}(-10, 15), 60),
271+
(Interval{Open,Open}(0.0, 25), 60.0),
272+
(Interval{Open,Open}(Date(2013, 2, 13), Date(2013, 2, 14)), Day(5)),
273+
(Interval{Open,Open}(DateTime(2016, 8, 11, 0, 30), DateTime(2016, 8, 11, 5, 30)), Day(5)),
274+
]
275+
for (interval, unit) in error_test_vals
276+
@test_throws BoundsError minimum(interval; increment=unit)
277+
@test_throws BoundsError maximum(interval; increment=unit)
278+
end
279+
end
280+
end
147281
@testset "display" begin
148282
interval = Interval{Open, Open}(1, 2)
149283
@test string(interval) == "(1 .. 2)"

0 commit comments

Comments
 (0)