From 586d793ec7bc57716058cab7752631a838e01001 Mon Sep 17 00:00:00 2001 From: Gem Newman Date: Tue, 17 Oct 2017 10:41:38 -0500 Subject: [PATCH 1/4] Add rounding for Periods --- base/dates/rounding.jl | 120 +++++++++++++++++++++++++++++++++++++--- doc/src/stdlib/dates.md | 8 +++ test/dates/rounding.jl | 68 ++++++++++++++++++++++- 3 files changed, 187 insertions(+), 9 deletions(-) diff --git a/base/dates/rounding.jl b/base/dates/rounding.jl index 6a4a40d38ae13..16e901cd1909e 100644 --- a/base/dates/rounding.jl +++ b/base/dates/rounding.jl @@ -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 TimeTypePeriod = 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)) + nanoseconds = Nanosecond(x) + return T(nanoseconds - mod(nanoseconds, Nanosecond(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,54 @@ 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) + return (Nanosecond(x) - Nanosecond(f)) < (Nanosecond(c) - Nanosecond(x)) ? f : c +end + +Base.round(x::TimeTypePeriod, p::Period, r::RoundingMode{:Down}) = Base.floor(x, p) +Base.round(x::TimeTypePeriod, 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(::TimeTypePeriod, p::Period, ::RoundingMode) = throw(DomainError(p)) # Default to RoundNearestTiesUp. -Base.round(dt::TimeType, p::Period) = Base.round(dt, p, RoundNearestTiesUp) +Base.round(x::TimeTypePeriod, 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::TimeTypePeriod, p::Type{<:Period}) = Base.floor(x, p(1)) +Base.ceil(x::TimeTypePeriod, p::Type{<:Period}) = Base.ceil(x, p(1)) -function Base.round(dt::TimeType, p::Type{<:Period}, r::RoundingMode=RoundNearestTiesUp) - return Base.round(dt, p(1), r) +function Base.round(x::TimeTypePeriod,p::Type{<:Period},r::RoundingMode=RoundNearestTiesUp) + return Base.round(x, p(1), r) end diff --git a/doc/src/stdlib/dates.md b/doc/src/stdlib/dates.md index a7ebd9f3d64dd..b7ff1f950bda0 100644 --- a/doc/src/stdlib/dates.md +++ b/doc/src/stdlib/dates.md @@ -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 diff --git a/test/dates/rounding.jl b/test/dates/rounding.jl index b68c14ba80eb4..8237c0d71e6f3 100644 --- a/test/dates/rounding.jl +++ b/test/dates/rounding.jl @@ -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,69 @@ end end end end +@testset "Rounding for periods" begin + x = Dates.Nanosecond(172799999999999) + @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(172799999) + @test floor(x, Dates.Microsecond) == Dates.Microsecond(172799999999) + @test floor(x, Dates.Nanosecond) == x + @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(172800) + @test ceil(x, Dates.Millisecond) == Dates.Millisecond(172800000) + @test ceil(x, Dates.Microsecond) == Dates.Microsecond(172800000000) + @test ceil(x, Dates.Nanosecond) == x + @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(172800) + @test round(x, Dates.Millisecond) == Dates.Millisecond(172800000) + @test round(x, Dates.Microsecond) == Dates.Microsecond(172800000000) + @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.Microsecond(604800000000)] + local dt + for p in [Dates.Week, Dates.Day, Dates.Hour, Dates.Second, Dates.Millisecond, Dates.Microsecond, Dates.Nanosecond] + local p + @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.Nanosecond(86399999999999) + 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 From f1f019426ed9352a8d2fe0e7a76696456e8fade8 Mon Sep 17 00:00:00 2001 From: Gem Newman Date: Fri, 27 Oct 2017 11:44:28 -0500 Subject: [PATCH 2/4] Improve period rounding function type signatures --- base/dates/rounding.jl | 18 +++++++++--------- test/dates/rounding.jl | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/base/dates/rounding.jl b/base/dates/rounding.jl index 16e901cd1909e..c3fffad595373 100644 --- a/base/dates/rounding.jl +++ b/base/dates/rounding.jl @@ -8,7 +8,7 @@ const DATETIMEEPOCH = value(DateTime(0)) const WEEKEPOCH = value(Date(0, 1, 3)) const ConvertiblePeriod = Union{TimePeriod, Week, Day} -const TimeTypePeriod = Union{TimeType, ConvertiblePeriod} +const TimeTypeOrPeriod = Union{TimeType, ConvertiblePeriod} """ epochdays2date(days) -> Date @@ -264,21 +264,21 @@ function Base.round(x::ConvertiblePeriod, precision::ConvertiblePeriod, r::Round return (Nanosecond(x) - Nanosecond(f)) < (Nanosecond(c) - Nanosecond(x)) ? f : c end -Base.round(x::TimeTypePeriod, p::Period, r::RoundingMode{:Down}) = Base.floor(x, p) -Base.round(x::TimeTypePeriod, p::Period, r::RoundingMode{:Up}) = Base.ceil(x, p) +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(::TimeTypePeriod, p::Period, ::RoundingMode) = throw(DomainError(p)) +Base.round(::TimeTypeOrPeriod, p::Period, ::RoundingMode) = throw(DomainError(p)) # Default to RoundNearestTiesUp. -Base.round(x::TimeTypePeriod, p::Period) = Base.round(x, 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(x::TimeTypePeriod, p::Type{<:Period}) = Base.floor(x, p(1)) -Base.ceil(x::TimeTypePeriod, p::Type{<:Period}) = Base.ceil(x, 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(x::TimeTypePeriod,p::Type{<:Period},r::RoundingMode=RoundNearestTiesUp) - return Base.round(x, p(1), r) +function Base.round(x::TimeTypeOrPeriod, ::Type{P}, r::RoundingMode=RoundNearestTiesUp) where P <: Period + return Base.round(x, oneunit(P), r) end diff --git a/test/dates/rounding.jl b/test/dates/rounding.jl index 8237c0d71e6f3..bd0de4ac65340 100644 --- a/test/dates/rounding.jl +++ b/test/dates/rounding.jl @@ -173,7 +173,7 @@ end end @testset "Rounding for periods that should not need rounding" begin for x in [Dates.Week(3), Dates.Day(14), Dates.Microsecond(604800000000)] - local dt + local x for p in [Dates.Week, Dates.Day, Dates.Hour, Dates.Second, Dates.Millisecond, Dates.Microsecond, Dates.Nanosecond] local p @test floor(x, p) == p(x) From 6cd95a0a78bb129912bef049ddc28747d968ea4c Mon Sep 17 00:00:00 2001 From: Gem Newman Date: Fri, 27 Oct 2017 15:46:54 -0500 Subject: [PATCH 3/4] Use promote instead of Nanosecond to round periods Period rounding functions no longer unnecessarily convert all values to nanoseconds prior to rounding. --- base/dates/rounding.jl | 7 ++++--- test/dates/rounding.jl | 34 +++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/base/dates/rounding.jl b/base/dates/rounding.jl index c3fffad595373..0c4d828854211 100644 --- a/base/dates/rounding.jl +++ b/base/dates/rounding.jl @@ -104,8 +104,8 @@ inconsistent length. """ function Base.floor(x::ConvertiblePeriod, precision::T) where T <: ConvertiblePeriod value(precision) < 1 && throw(DomainError(precision)) - nanoseconds = Nanosecond(x) - return T(nanoseconds - mod(nanoseconds, Nanosecond(precision))) + x, precision = promote(x, precision) + return T(x - mod(x, precision)) end """ @@ -261,7 +261,8 @@ inconsistent length. """ function Base.round(x::ConvertiblePeriod, precision::ConvertiblePeriod, r::RoundingMode{:NearestTiesUp}) f, c = floorceil(x, precision) - return (Nanosecond(x) - Nanosecond(f)) < (Nanosecond(c) - Nanosecond(x)) ? f : c + common_x, common_f, common_c = promote(x, f, c) + return (common_x - common_f) < (common_c - common_x) ? f : c end Base.round(x::TimeTypeOrPeriod, p::Period, r::RoundingMode{:Down}) = Base.floor(x, p) diff --git a/test/dates/rounding.jl b/test/dates/rounding.jl index bd0de4ac65340..b12d1a757a992 100644 --- a/test/dates/rounding.jl +++ b/test/dates/rounding.jl @@ -145,34 +145,42 @@ end end end @testset "Rounding for periods" begin - x = Dates.Nanosecond(172799999999999) + 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(172799999) - @test floor(x, Dates.Microsecond) == Dates.Microsecond(172799999999) - @test floor(x, Dates.Nanosecond) == x + @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(172800) - @test ceil(x, Dates.Millisecond) == Dates.Millisecond(172800000) - @test ceil(x, Dates.Microsecond) == Dates.Microsecond(172800000000) - @test ceil(x, Dates.Nanosecond) == x + @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(172800) - @test round(x, Dates.Millisecond) == Dates.Millisecond(172800000) - @test round(x, Dates.Microsecond) == Dates.Microsecond(172800000000) + @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.Microsecond(604800000000)] + 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 @@ -192,7 +200,7 @@ end @test round(x, Dates.Day) == round(x, Dates.Day, RoundNearestTiesUp) end @testset "Rounding periods to invalid resolutions" begin - x = Dates.Nanosecond(86399999999999) + 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] From ff158ec6ba1144d8f6cb74458903314ac3b5f075 Mon Sep 17 00:00:00 2001 From: Gem Newman Date: Mon, 30 Oct 2017 09:30:34 -0500 Subject: [PATCH 4/4] Minor refactoring for period rounding --- base/dates/rounding.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/base/dates/rounding.jl b/base/dates/rounding.jl index 0c4d828854211..d8efe47f1a94c 100644 --- a/base/dates/rounding.jl +++ b/base/dates/rounding.jl @@ -104,8 +104,8 @@ 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)) + _x, _precision = promote(x, precision) + return T(_x - mod(_x, _precision)) end """ @@ -261,8 +261,8 @@ 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 + _x, _f, _c = promote(x, f, c) + return (_x - _f) < (_c - _x) ? f : c end Base.round(x::TimeTypeOrPeriod, p::Period, r::RoundingMode{:Down}) = Base.floor(x, p) @@ -281,5 +281,5 @@ Base.floor(x::TimeTypeOrPeriod, ::Type{P}) where P <: Period = Base.floor(x, one Base.ceil(x::TimeTypeOrPeriod, ::Type{P}) where P <: Period = Base.ceil(x, oneunit(P)) function Base.round(x::TimeTypeOrPeriod, ::Type{P}, r::RoundingMode=RoundNearestTiesUp) where P <: Period - return Base.round(x, oneunit(P), r) + return Base.round(x, oneunit(P), r) end