-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -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) | ||
return T(x - mod(x, precision)) | ||
end | ||
|
||
""" | ||
floor(dt::TimeType, p::Period) -> TimeType | ||
|
||
|
@@ -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) | ||
|
||
|
@@ -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 | ||
|
||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extra space |
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unneeded? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kshyatt can you elaborate on why this is needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a case where I noticed that |
||
@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 |
There was a problem hiding this comment.
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