Skip to content

Commit

Permalink
Merge pull request #147 from invenia/ne/rounding
Browse files Browse the repository at this point in the history
Implement interval rounding
  • Loading branch information
nicoleepp authored Oct 16, 2020
2 parents c37cbb7 + cfc2c13 commit 168565e
Show file tree
Hide file tree
Showing 6 changed files with 391 additions and 5 deletions.
38 changes: 38 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,44 @@ julia> 0..10 ≪ 11..20
true
```

### Rounding

Interval rounding maintains the original span of the interval, shifting it according to
whichever endpoint is specified as the one to use for rounding. The operations `floor`,
`ceil`, and `round` are supported, as long as the `on` keyword is supplied to specify which
endpoint should be used for rounding. Valid options are `:left`, `:right`, or
`:anchor` if dealing with anchored intervals.

```jldoctest
julia> floor(Interval(0.0, 1.0), on=:left)
Interval{Float64,Closed,Closed}(0.0, 1.0)
julia> floor(Interval(0.5, 1.0), on=:left)
Interval{Float64,Closed,Closed}(0.0, 0.5)
julia> floor(Interval(0.5, 1.5), on=:right)
Interval{Float64,Closed,Closed}(0.0, 1.0)
```

Anchored intervals default to rounding using the anchor point.

```jldoctest
julia> round(AnchoredInterval{-0.5}(1.0))
AnchoredInterval{-0.5,Float64,Open,Closed}(1.0)
julia> round(AnchoredInterval{+0.5}(0.5))
AnchoredInterval{0.5,Float64,Closed,Open}(0.0)
julia> round(AnchoredInterval{+0.5}(0.5), on=:anchor)
AnchoredInterval{0.5,Float64,Closed,Open}(0.0)
julia> round(AnchoredInterval{+0.5}(0.5), on=:left)
AnchoredInterval{0.5,Float64,Closed,Open}(0.0)
julia> round(AnchoredInterval{+0.5}(0.5), on=:right)
AnchoredInterval{0.5,Float64,Closed,Open}(0.5)
```

## API

```@docs
Expand Down
46 changes: 43 additions & 3 deletions src/anchoredinterval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,15 @@ HourBeginning(anchor::T) where T = HourBeginning{T}(anchor)
`HE` is a pseudoconstructor for [`HourEnding`](@ref) that rounds the anchor provided up to the
nearest hour.
"""
HE(anchor) = HourEnding(ceil(anchor, Hour))
HE(anchor) = ceil(HourEnding(anchor), Hour)

"""
HB(anchor) -> HourBeginning
`HB` is a pseudoconstructor for [`HourBeginning`](@ref) that rounds the anchor provided down to the
nearest hour.
"""
HB(anchor) = HourBeginning(floor(anchor, Hour))
HB(anchor) = floor(HourBeginning(anchor), Hour)

function Base.copy(x::AnchoredInterval{P,T,L,R}) where {P,T,L,R}
return AnchoredInterval{P,T,L,R}(anchor(x))
Expand Down Expand Up @@ -329,7 +329,47 @@ function Base.intersect(a::AnchoredInterval{P,T}, b::AnchoredInterval{Q,T}) wher
end

L, R = bounds_types(interval)
return AnchoredInterval{new_P, T, L, R}(anchor)
return AnchoredInterval{new_P,T,L,R}(anchor)
end

##### ROUNDING #####

for f in (:floor, :ceil, :round)
@eval begin
"""
$($f)(interval::AnchoredInterval, args...; on::Symbol=:anchor)
Round the anchored interval by applying `$($f)` to a single endpoint, then shifting
the interval so that the span remains the same. The `on` keyword determines which
endpoint the rounding will be applied to. Valid options are `:anchor`, `:left`, or
`:right`. Rounding defaults to using the anchor point.
"""
function Base.$f(
interval::AnchoredInterval{P,T,L,R},
args...;
on::Symbol=:anchor,
) where {P,T,L,R}
anc = if on === :anchor
$f(anchor(interval), args...)
elseif on === :left
if P zero(P)
$f(first(interval), args...) - P
else
$f(first(interval), args...)
end
elseif on === :right
if P zero(P)
$f(last(interval), args...)
else
$f(last(interval), args...) - P
end
else
throw(ArgumentError("Unhandled `on` type: $on"))
end

return AnchoredInterval{P,T,L,R}(anc)
end
end
end

##### UTILITIES #####
Expand Down
5 changes: 3 additions & 2 deletions src/endpoint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ struct Endpoint{T, D, B <: Bound}
end
end


Endpoint{T,D,B}(ep) where {T, D, B <: Bounded} = Endpoint{T,D,B}(convert(T, ep))

const LeftEndpoint{T,B} = Endpoint{T, Left, B} where {T,B}
const RightEndpoint{T,B} = Endpoint{T, Right, B} where {T,B}
const LeftEndpoint{T,B} = Endpoint{T, Left, B} where {T,B <: Bound}
const RightEndpoint{T,B} = Endpoint{T, Right, B} where {T,B <: Bound}

LeftEndpoint{B}(ep::T) where {T,B} = LeftEndpoint{T,B}(ep)
RightEndpoint{B}(ep::T) where {T,B} = RightEndpoint{T,B}(ep)
Expand Down
63 changes: 63 additions & 0 deletions src/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,69 @@ function Base.merge(a::AbstractInterval, b::AbstractInterval)
return Interval(left, right)
end

##### ROUNDING #####
const RoundingFunctionTypes = Union{typeof(floor), typeof(ceil), typeof(round)}

for f in (:floor, :ceil, :round)
@eval begin
"""
$($f)(interval::Interval, args...; on::Symbol)
Round the interval by applying `$($f)` to a single endpoint, then shifting the
interval so that the span remains the same. The `on` keyword determines which
endpoint the rounding will be applied to. Valid options are `:left` or `:right`.
"""
function Base.$f(interval::Interval, args...; on::Symbol)
return _round($f, interval, Val(on), args...)
end
end
end

function _round(f::RoundingFunctionTypes, interval::Interval, on::Val{:anchor}, args...)
throw(ArgumentError(":anchor is only usable with an AnchoredInterval."))
end

function _round(
f::RoundingFunctionTypes, interval::Interval{T,L,R}, on::Val{:left}, args...
) where {T, L <: Bounded, R <: Bounded}
left_val = f(first(interval), args...)
return Interval{T,L,R}(left_val, left_val + span(interval))
end

function _round(
f::RoundingFunctionTypes, interval::Interval{T,L,R}, on::Val{:left}, args...
) where {T, L <: Bounded, R <: Unbounded}
left_val = f(first(interval), args...)
return Interval{T,L,R}(left_val, nothing)
end

function _round(
f::RoundingFunctionTypes, interval::Interval{T,L,R}, on::Val{:left}, args...
) where {T, L <: Unbounded, R <: Bound}
return interval
end

function _round(
f::RoundingFunctionTypes, interval::Interval{T,L,R}, on::Val{:right}, args...
) where {T, L <: Bounded, R <: Bounded}
right_val = f(last(interval), args...)
return Interval{T,L,R}(right_val - span(interval), right_val)
end

function _round(
f::RoundingFunctionTypes, interval::Interval{T,L,R}, on::Val{:right}, args...
) where {T, L <: Unbounded, R <: Bounded}
right_val = f(last(interval), args...)
return Interval{T,L,R}(nothing, right_val)
end

function _round(
f::RoundingFunctionTypes, interval::Interval{T,L,R}, on::Val{:right}, args...
) where {T, L <: Bound, R <: Unbounded}
return interval
end


##### TIME ZONES #####

function TimeZones.astimezone(i::Interval{ZonedDateTime, L, R}, tz::TimeZone) where {L,R}
Expand Down
108 changes: 108 additions & 0 deletions test/anchoredinterval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -841,4 +841,112 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
@test interval isa AnchoredInterval
@test interval == AnchoredInterval{-1,Int,Closed,Open}(2)
end

@testset "floor" begin
# only :anchor, :left, and :right are supported
@test_throws ArgumentError floor(AnchoredInterval{-0.5}(1.0); on=:nothing)

@test floor(AnchoredInterval{-0.5}(1.0)) == AnchoredInterval{-0.5}(1.0)
@test floor(AnchoredInterval{+0.5}(1.0)) == AnchoredInterval{+0.5}(1.0)
@test floor(AnchoredInterval{-0.5}(0.5)) == AnchoredInterval{-0.5}(0.0)
@test floor(AnchoredInterval{+0.5}(0.5)) == AnchoredInterval{+0.5}(0.0)

@test floor(AnchoredInterval{-0.5}(1.0); on=:left) == AnchoredInterval{-0.5}(0.5)
@test floor(AnchoredInterval{+0.5}(1.0); on=:left) == AnchoredInterval{+0.5}(1.0)
@test floor(AnchoredInterval{-0.5}(0.5); on=:left) == AnchoredInterval{-0.5}(0.5)
@test floor(AnchoredInterval{+0.5}(0.5); on=:left) == AnchoredInterval{+0.5}(0.0)

@test floor(AnchoredInterval{-0.5}(1.0); on=:right) == AnchoredInterval{-0.5}(1.0)
@test floor(AnchoredInterval{+0.5}(1.0); on=:right) == AnchoredInterval{+0.5}(0.5)
@test floor(AnchoredInterval{-0.5}(0.5); on=:right) == AnchoredInterval{-0.5}(0.0)
@test floor(AnchoredInterval{+0.5}(0.5); on=:right) == AnchoredInterval{+0.5}(0.5)

@test floor(AnchoredInterval{-0.5}(1.0); on=:anchor) == AnchoredInterval{-0.5}(1.0)
@test floor(AnchoredInterval{+0.5}(1.0); on=:anchor) == AnchoredInterval{+0.5}(1.0)
@test floor(AnchoredInterval{-0.5}(0.5); on=:anchor) == AnchoredInterval{-0.5}(0.0)
@test floor(AnchoredInterval{+0.5}(0.5); on=:anchor) == AnchoredInterval{+0.5}(0.0)

# Test supplying a period to floor to
interval_ending = AnchoredInterval{Day(-1)}(DateTime(2011, 2, 1, 12))
expected = AnchoredInterval{Day(-1)}(DateTime(2011, 2, 1))
@test floor(interval_ending, Day) == expected
@test floor(interval_ending, Day(1)) == expected

interval_beginning = AnchoredInterval{Day(1)}(DateTime(2011, 2, 1, 12))
expected = AnchoredInterval{Day(1)}(DateTime(2011, 2, 1))
@test floor(interval_beginning, Day) == expected
@test floor(interval_beginning, Day(1)) == expected
end

@testset "ceil" begin
# only :anchor, :left, and :right are supported
@test_throws ArgumentError ceil(AnchoredInterval{-0.5}(1.0); on=:nothing)

@test ceil(AnchoredInterval{-0.5}(1.0)) == AnchoredInterval{-0.5}(1.0)
@test ceil(AnchoredInterval{+0.5}(1.0)) == AnchoredInterval{+0.5}(1.0)
@test ceil(AnchoredInterval{-0.5}(0.5)) == AnchoredInterval{-0.5}(1.0)
@test ceil(AnchoredInterval{+0.5}(0.5)) == AnchoredInterval{+0.5}(1.0)

@test ceil(AnchoredInterval{-0.5}(1.0); on=:left) == AnchoredInterval{-0.5}(1.5)
@test ceil(AnchoredInterval{+0.5}(1.0); on=:left) == AnchoredInterval{+0.5}(1.0)
@test ceil(AnchoredInterval{-0.5}(0.5); on=:left) == AnchoredInterval{-0.5}(0.5)
@test ceil(AnchoredInterval{+0.5}(0.5); on=:left) == AnchoredInterval{+0.5}(1.0)

@test ceil(AnchoredInterval{-0.5}(1.0); on=:right) == AnchoredInterval{-0.5}(1.0)
@test ceil(AnchoredInterval{+0.5}(1.0); on=:right) == AnchoredInterval{+0.5}(1.5)
@test ceil(AnchoredInterval{-0.5}(0.5); on=:right) == AnchoredInterval{-0.5}(1.0)
@test ceil(AnchoredInterval{+0.5}(0.5); on=:right) == AnchoredInterval{+0.5}(0.5)

@test ceil(AnchoredInterval{-0.5}(1.0); on=:anchor) == AnchoredInterval{-0.5}(1.0)
@test ceil(AnchoredInterval{+0.5}(1.0); on=:anchor) == AnchoredInterval{+0.5}(1.0)
@test ceil(AnchoredInterval{-0.5}(0.5); on=:anchor) == AnchoredInterval{-0.5}(1.0)
@test ceil(AnchoredInterval{+0.5}(0.5); on=:anchor) == AnchoredInterval{+0.5}(1.0)

# Test supplying a period to ceil to
interval_ending = AnchoredInterval{Day(-1)}(DateTime(2011, 2, 1, 12))
expected = AnchoredInterval{Day(-1)}(DateTime(2011, 2, 2))
@test ceil(interval_ending, Day) == expected
@test ceil(interval_ending, Day(1)) == expected

interval_beginning = AnchoredInterval{Day(1)}(DateTime(2011, 2, 1, 12))
expected = AnchoredInterval{Day(1)}(DateTime(2011, 2, 2))
@test ceil(interval_beginning, Day) == expected
@test ceil(interval_beginning, Day(1)) == expected
end

@testset "round" begin
# only :anchor, :left, and :right are supported
@test_throws ArgumentError round(AnchoredInterval{-0.5}(1.0); on=:nothing)

@test round(AnchoredInterval{-0.5}(1.0)) == AnchoredInterval{-0.5}(1.0)
@test round(AnchoredInterval{+0.5}(1.0)) == AnchoredInterval{+0.5}(1.0)
@test round(AnchoredInterval{-0.5}(0.5)) == AnchoredInterval{-0.5}(0.0)
@test round(AnchoredInterval{+0.5}(0.5)) == AnchoredInterval{+0.5}(0.0)

@test round(AnchoredInterval{-0.5}(1.0); on=:left) == AnchoredInterval{-0.5}(0.5)
@test round(AnchoredInterval{+0.5}(1.0); on=:left) == AnchoredInterval{+0.5}(1.0)
@test round(AnchoredInterval{-0.5}(0.5); on=:left) == AnchoredInterval{-0.5}(0.5)
@test round(AnchoredInterval{+0.5}(0.5); on=:left) == AnchoredInterval{+0.5}(0.0)

@test round(AnchoredInterval{-0.5}(1.0); on=:right) == AnchoredInterval{-0.5}(1.0)
@test round(AnchoredInterval{+0.5}(1.0); on=:right) == AnchoredInterval{+0.5}(1.5)
@test round(AnchoredInterval{-0.5}(0.5); on=:right) == AnchoredInterval{-0.5}(0.0)
@test round(AnchoredInterval{+0.5}(0.5); on=:right) == AnchoredInterval{+0.5}(0.5)

@test round(AnchoredInterval{-0.5}(1.0); on=:anchor) == AnchoredInterval{-0.5}(1.0)
@test round(AnchoredInterval{+0.5}(1.0); on=:anchor) == AnchoredInterval{+0.5}(1.0)
@test round(AnchoredInterval{-0.5}(0.5); on=:anchor) == AnchoredInterval{-0.5}(0.0)
@test round(AnchoredInterval{+0.5}(0.5); on=:anchor) == AnchoredInterval{+0.5}(0.0)

# Test supplying a period to round to
interval_ending = AnchoredInterval{Day(-1)}(DateTime(2011, 2, 1, 12))
expected = AnchoredInterval{Day(-1)}(DateTime(2011, 2, 2))
@test round(interval_ending, Day) == expected
@test round(interval_ending, Day(1)) == expected

interval_beginning = AnchoredInterval{Day(1)}(DateTime(2011, 2, 1, 12))
expected = AnchoredInterval{Day(1)}(DateTime(2011, 2, 2))
@test round(interval_beginning, Day) == expected
@test round(interval_beginning, Day(1)) == expected
end
end
Loading

0 comments on commit 168565e

Please sign in to comment.