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 rounding support for Periods #24182

Merged
merged 4 commits into from
Nov 2, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 113 additions & 8 deletions base/dates/rounding.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const DATETIMEEPOCH = value(DateTime(0))
# According to ISO 8601, the first day of the first week of year 0000 is 0000-01-03
const WEEKEPOCH = value(Date(0, 1, 3))

const ConvertiblePeriod = Union{TimePeriod, Week, Day}
const TimeTypeOrPeriod = Union{TimeType, ConvertiblePeriod}

"""
epochdays2date(days) -> Date

Expand Down Expand Up @@ -76,6 +79,35 @@ function Base.floor(dt::DateTime, p::TimePeriod)
return epochms2datetime(milliseconds - mod(milliseconds, value(Millisecond(p))))
end

"""
floor(x::Period, precision::T) where T <: Union{TimePeriod, Week, Day} -> T

Rounds `x` down to the nearest multiple of `precision`. If `x` and `precision` are different
subtypes of `Period`, the return value will have the same type as `precision`.

For convenience, `precision` may be a type instead of a value: `floor(x, Dates.Hour)` is a
shortcut for `floor(x, Dates.Hour(1))`.

```jldoctest
julia> floor(Dates.Day(16), Dates.Week)
2 weeks

julia> floor(Dates.Minute(44), Dates.Minute(15))
30 minutes

julia> floor(Dates.Hour(36), Dates.Day)
1 day
```

Rounding to a `precision` of `Month`s or `Year`s is not supported, as these `Period`s are of
inconsistent length.
"""
function Base.floor(x::ConvertiblePeriod, precision::T) where T <: ConvertiblePeriod
value(precision) < 1 && throw(DomainError(precision))
x, precision = promote(x, precision)
Copy link
Member

Choose a reason for hiding this comment

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

Should use a different variable name for the assignment to ensure type stability

return T(x - mod(x, precision))
end

"""
floor(dt::TimeType, p::Period) -> TimeType

Expand Down Expand Up @@ -121,6 +153,34 @@ function Base.ceil(dt::TimeType, p::Period)
return (dt == f) ? f : f + p
end

"""
ceil(x::Period, precision::T) where T <: Union{TimePeriod, Week, Day} -> T

Rounds `x` up to the nearest multiple of `precision`. If `x` and `precision` are different
subtypes of `Period`, the return value will have the same type as `precision`.

For convenience, `precision` may be a type instead of a value: `ceil(x, Dates.Hour)` is a
shortcut for `ceil(x, Dates.Hour(1))`.

```jldoctest
julia> ceil(Dates.Day(16), Dates.Week)
3 weeks

julia> ceil(Dates.Minute(44), Dates.Minute(15))
45 minutes

julia> ceil(Dates.Hour(36), Dates.Day)
3 days
```

Rounding to a `precision` of `Month`s or `Year`s is not supported, as these `Period`s are of
inconsistent length.
"""
function Base.ceil(x::ConvertiblePeriod, precision::ConvertiblePeriod)
f = floor(x, precision)
return (x == f) ? f : f + precision
end

"""
floorceil(dt::TimeType, p::Period) -> (TimeType, TimeType)

Expand All @@ -132,6 +192,17 @@ function floorceil(dt::TimeType, p::Period)
return f, (dt == f) ? f : f + p
end

"""
floorceil(x::Period, precision::T) where T <: Union{TimePeriod, Week, Day} -> (T, T)

Simultaneously return the `floor` and `ceil` of `Period` at resolution `p`. More efficient
than calling both `floor` and `ceil` individually.
"""
function floorceil(x::ConvertiblePeriod, precision::ConvertiblePeriod)
f = floor(x, precision)
return f, (x == f) ? f : f + precision
end

"""
round(dt::TimeType, p::Period, [r::RoundingMode]) -> TimeType

Expand Down Expand Up @@ -160,21 +231,55 @@ function Base.round(dt::TimeType, p::Period, r::RoundingMode{:NearestTiesUp})
return (dt - f) < (c - dt) ? f : c
end

Base.round(dt::TimeType, p::Period, r::RoundingMode{:Down}) = Base.floor(dt, p)
Base.round(dt::TimeType, p::Period, r::RoundingMode{:Up}) = Base.ceil(dt, p)
"""
round(x::Period, precision::T, [r::RoundingMode]) where T <: Union{TimePeriod, Week, Day} -> T

Rounds `x` to the nearest multiple of `precision`. If `x` and `precision` are different
subtypes of `Period`, the return value will have the same type as `precision`. By default
(`RoundNearestTiesUp`), ties (e.g., rounding 90 minutes to the nearest hour) will be rounded
up.

For convenience, `precision` may be a type instead of a value: `round(x, Dates.Hour)` is a
shortcut for `round(x, Dates.Hour(1))`.

```jldoctest
julia> round(Dates.Day(16), Dates.Week)
2 weeks

julia> round(Dates.Minute(44), Dates.Minute(15))
45 minutes

julia> round(Dates.Hour(36), Dates.Day)
3 days
```

Valid rounding modes for `round(::Period, ::T, ::RoundingMode)` are `RoundNearestTiesUp`
(default), `RoundDown` (`floor`), and `RoundUp` (`ceil`).

Rounding to a `precision` of `Month`s or `Year`s is not supported, as these `Period`s are of
inconsistent length.
"""
function Base.round(x::ConvertiblePeriod, precision::ConvertiblePeriod, r::RoundingMode{:NearestTiesUp})
f, c = floorceil(x, precision)
common_x, common_f, common_c = promote(x, f, c)
return (common_x - common_f) < (common_c - common_x) ? f : c
Copy link
Member

Choose a reason for hiding this comment

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

I think _x, _f, _c = ... is fine if you want to use a more concise name

end

Base.round(x::TimeTypeOrPeriod, p::Period, r::RoundingMode{:Down}) = Base.floor(x, p)
Base.round(x::TimeTypeOrPeriod, p::Period, r::RoundingMode{:Up}) = Base.ceil(x, p)

# No implementation of other `RoundingMode`s: rounding to nearest "even" is skipped because
# "even" is not defined for Period; rounding toward/away from zero is skipped because ISO
# 8601's year 0000 is not really "zero".
Base.round(::TimeType, p::Period, ::RoundingMode) = throw(DomainError(p))
Base.round(::TimeTypeOrPeriod, p::Period, ::RoundingMode) = throw(DomainError(p))

# Default to RoundNearestTiesUp.
Base.round(dt::TimeType, p::Period) = Base.round(dt, p, RoundNearestTiesUp)
Base.round(x::TimeTypeOrPeriod, p::Period) = Base.round(x, p, RoundNearestTiesUp)

# Make rounding functions callable using Period types in addition to values.
Base.floor(dt::TimeType, p::Type{<:Period}) = Base.floor(dt, p(1))
Base.ceil(dt::TimeType, p::Type{<:Period}) = Base.ceil(dt, p(1))
Base.floor(x::TimeTypeOrPeriod, ::Type{P}) where P <: Period = Base.floor(x, oneunit(P))
Base.ceil(x::TimeTypeOrPeriod, ::Type{P}) where P <: Period = Base.ceil(x, oneunit(P))

function Base.round(dt::TimeType, p::Type{<:Period}, r::RoundingMode=RoundNearestTiesUp)
return Base.round(dt, p(1), r)
function Base.round(x::TimeTypeOrPeriod, ::Type{P}, r::RoundingMode=RoundNearestTiesUp) where P <: Period
return Base.round(x, oneunit(P), r)
Copy link
Member

Choose a reason for hiding this comment

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

Extra space

end
8 changes: 8 additions & 0 deletions doc/src/stdlib/dates.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ Base.ceil(::Base.Dates.TimeType, ::Base.Dates.Period)
Base.round(::Base.Dates.TimeType, ::Base.Dates.Period, ::RoundingMode{:NearestTiesUp})
```

Most `Period` values can also be rounded to a specified resolution:

```@docs
Base.floor(::Base.Dates.ConvertiblePeriod, ::T) where T <: Base.Dates.ConvertiblePeriod
Base.ceil(::Base.Dates.ConvertiblePeriod, ::Base.Dates.ConvertiblePeriod)
Base.round(::Base.Dates.ConvertiblePeriod, ::Base.Dates.ConvertiblePeriod, ::RoundingMode{:NearestTiesUp})
```

The following functions are not exported:

```@docs
Expand Down
76 changes: 75 additions & 1 deletion test/dates/rounding.jl
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ end
@test_throws DomainError round(dt, Dates.Day, RoundToZero)
@test round(dt, Dates.Day) == round(dt, Dates.Day, RoundNearestTiesUp)
end
@testset "Rounding to invalid resolutions" begin
@testset "Rounding datetimes to invalid resolutions" begin
dt = Dates.DateTime(2016, 2, 28, 12, 15)
for p in [Dates.Year, Dates.Month, Dates.Week, Dates.Day, Dates.Hour]
local p
Expand All @@ -144,3 +144,77 @@ end
end
end
end
@testset "Rounding for periods" begin
x = Dates.Second(172799)
@test floor(x, Dates.Week) == Dates.Week(0)
@test floor(x, Dates.Day) == Dates.Day(1)
@test floor(x, Dates.Hour) == Dates.Hour(47)
@test floor(x, Dates.Minute) == Dates.Minute(2879)
@test floor(x, Dates.Second) == Dates.Second(172799)
@test floor(x, Dates.Millisecond) == Dates.Millisecond(172799000)
@test ceil(x, Dates.Week) == Dates.Week(1)
@test ceil(x, Dates.Day) == Dates.Day(2)
@test ceil(x, Dates.Hour) == Dates.Hour(48)
@test ceil(x, Dates.Minute) == Dates.Minute(2880)
@test ceil(x, Dates.Second) == Dates.Second(172799)
@test ceil(x, Dates.Millisecond) == Dates.Millisecond(172799000)
@test round(x, Dates.Week) == Dates.Week(0)
@test round(x, Dates.Day) == Dates.Day(2)
@test round(x, Dates.Hour) == Dates.Hour(48)
@test round(x, Dates.Minute) == Dates.Minute(2880)
@test round(x, Dates.Second) == Dates.Second(172799)
@test round(x, Dates.Millisecond) == Dates.Millisecond(172799000)

x = Dates.Nanosecond(2000999999)
@test floor(x, Dates.Second) == Dates.Second(2)
@test floor(x, Dates.Millisecond) == Dates.Millisecond(2000)
@test floor(x, Dates.Microsecond) == Dates.Microsecond(2000999)
@test floor(x, Dates.Nanosecond) == x
@test ceil(x, Dates.Second) == Dates.Second(3)
@test ceil(x, Dates.Millisecond) == Dates.Millisecond(2001)
@test ceil(x, Dates.Microsecond) == Dates.Microsecond(2001000)
@test ceil(x, Dates.Nanosecond) == x
@test round(x, Dates.Second) == Dates.Second(2)
@test round(x, Dates.Millisecond) == Dates.Millisecond(2001)
@test round(x, Dates.Microsecond) == Dates.Microsecond(2001000)
@test round(x, Dates.Nanosecond) == x
end
@testset "Rounding for periods that should not need rounding" begin
for x in [Dates.Week(3), Dates.Day(14), Dates.Second(604800)]
local x
for p in [Dates.Week, Dates.Day, Dates.Hour, Dates.Second, Dates.Millisecond, Dates.Microsecond, Dates.Nanosecond]
local p
Copy link
Member

Choose a reason for hiding this comment

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

Unneeded?

Copy link
Member

Choose a reason for hiding this comment

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

@kshyatt can you elaborate on why this is needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a case where I noticed that local dt had been added to the tests in several places, so I replicated that syntax for these tests (in this case local dt should actually be local x above). But yeah, I don't actually know what those lines are meant to accomplish.

@test floor(x, p) == p(x)
@test ceil(x, p) == p(x)
end
end
end
@testset "Various available RoundingModes for periods" begin
x = Dates.Hour(36)
@test round(x, Dates.Day, RoundNearestTiesUp) == Dates.Day(2)
@test round(x, Dates.Day, RoundUp) == Dates.Day(2)
@test round(x, Dates.Day, RoundDown) == Dates.Day(1)
@test_throws DomainError round(x, Dates.Day, RoundNearest)
@test_throws DomainError round(x, Dates.Day, RoundNearestTiesAway)
@test_throws DomainError round(x, Dates.Day, RoundToZero)
@test round(x, Dates.Day) == round(x, Dates.Day, RoundNearestTiesUp)
end
@testset "Rounding periods to invalid resolutions" begin
x = Dates.Hour(86399)
for p in [Dates.Week, Dates.Day, Dates.Hour, Dates.Second, Dates.Millisecond, Dates.Microsecond, Dates.Nanosecond]
local p
for v in [-1, 0]
@test_throws DomainError floor(x, p(v))
@test_throws DomainError ceil(x, p(v))
@test_throws DomainError round(x, p(v))
end
end
for p in [Dates.Year, Dates.Month]
local p
for v in [-1, 0, 1]
@test_throws MethodError floor(x, p(v))
@test_throws MethodError ceil(x, p(v))
@test_throws DomainError round(x, p(v))
end
end
end