From 4d2183594786f860adac5b2a22f05fc8be55b443 Mon Sep 17 00:00:00 2001 From: Gem Newman Date: Tue, 14 Jun 2016 15:40:24 -0500 Subject: [PATCH 1/5] Added floor, ceil, round for Date and DateTime. --- base/dates/adjusters.jl | 174 ++++++++++++++++++++++++++++++++++++++ base/dates/conversions.jl | 36 ++++++++ test/dates/adjusters.jl | 135 +++++++++++++++++++++++++++++ test/dates/conversions.jl | 17 ++++ 4 files changed, 362 insertions(+) diff --git a/base/dates/adjusters.jl b/base/dates/adjusters.jl index 4836bc4dafa40..9cede6221f6e7 100644 --- a/base/dates/adjusters.jl +++ b/base/dates/adjusters.jl @@ -21,6 +21,180 @@ Truncates the value of `dt` according to the provided `Period` type. E.g. if `dt """ Dates.trunc(::Dates.TimeType, ::Type{Dates.Period}) + +# Rounding + +# TODO: Docstrings + +function Base.floor(dt::Date, p::Year) + years = year(dt) + return Date(years - mod(years, value(p)), 1, 1) +end + +function Base.floor(dt::Date, p::Month) + months_since_epoch = year(dt) * 12 + month(dt) - 1 + month_offset = months_since_epoch - mod(months_since_epoch, value(p)) + target_month = mod(month_offset, 12) + 1 + target_year = div(month_offset, 12) - (month_offset < 0 && target_month != 1) + return Date(target_year, target_month, 1) +end + +# According to ISO 8601, the first day of the first week of year 0000 is 0000-01-03. +const ISO8601WEEKEPOCH = value(Date(0, 1, 3)) +function Base.floor(dt::Date, p::Week) + days = value(dt) - ISO8601WEEKEPOCH + days = days - mod(days, value(p) * 7) + return Date(UTD(ISO8601WEEKEPOCH + Int64(days))) +end + +# For days, the math is straightforward. +function Base.floor(dt::Date, p::Day) + days = date2iso8601(dt) + return iso86012date(days - mod(days, value(p))) +end + +Base.floor(dt::DateTime, p::DatePeriod) = DateTime(Base.floor(Date(dt), p)) + +# All TimePeriods are convertible to milliseconds, so the math is straightforward. +function Base.floor(dt::DateTime, p::TimePeriod) + milliseconds = datetime2iso8601(dt) + return iso86012datetime(milliseconds - mod(milliseconds, value(Millisecond(p)))) +end + +function Base.ceil(dt::TimeType, p::Period) + f = floor(dt, p) + return (dt == f) ? f : f + p +end + +function Base.round(dt::TimeType, p::Period, r::RoundingMode{:NearestTiesUp}) + f = floor(dt, p) + c = ceil(dt, p) + return (dt - f < c - dt) ? f : c +end + +Base.round(dt::TimeType, p::Period, r::RoundingMode{:Up}) = Base.ceil(dt, p) +Base.round(dt::TimeType, p::Period, r::RoundingMode{:Down}) = Base.floor(dt, p) + +# No implementation of rounding to nearest "even", because "even" is not defined for Period. +Base.round(::TimeType, ::Period, ::RoundingMode{:Nearest}) = throw(DomainError()) + +# No implementation of rounding toward/away from ISO 8601's arbitrary zero-point. +Base.round(::TimeType, ::Period, ::RoundingMode{:NearestTiesAway}) = throw(DomainError()) +Base.round(::TimeType, ::Period, ::RoundingMode{:ToZero}) = throw(DomainError()) + +# Default to RoundNearestTiesUp. +Base.round(dt::TimeType, p::Period) = Base.round(dt, p, RoundNearestTiesUp) + +# Callable using Period types in addition to values. +Base.floor{T <: Period}(dt::TimeType, p::Type{T}) = Base.floor(dt, p(1)) +Base.ceil{T <: Period}(dt::TimeType, p::Type{T}) = Base.ceil(dt, p(1)) + +function Base.round{T <: Period}(dt::TimeType, p::Type{T}, r=RoundNearestTiesUp) + return Base.round(dt, p(1), r) +end + +#= +TODO: Update documentation for dates and for RoundingMode to indicate that the default is +RoundNearestTiesUp + +TODO: Add example calls for rounding to the documentation and to the PR itself! + +TODO: Add some of this to the documentation. + +Who has opinions on date rounding? Specifically, I need to define a zero-point for rounding +a TimeType to a multiple of a Period. + +Candidate "zero dates" include: +1. `0000-01-01T00:00:00.000`, the first day of the first year specified by ISO 8601, which + Julia uses for external representations (display, etc.) of Date and DateTime +2. `0001-01-01T00:00:00.000`, the Rata Die base date, which is how Julia represents dates + internally +3. `YYYY-01-01T00:00:00.000`, where `YYYY` is the year of the date to be rounded +4. `YYYY-01-01T00:00:00.000` for rounding to `DatePeriod`s and `YYYY-MM-DDT00:00:00.000` for + rounding `TimePeriod`s. +5. "Closest current state" (better name to be determined), where the "zero date" is + dependent on the Period type to be used in rounding. Rounding a date to the nearest X + milliseconds would use the date's current second as its base; rounding a date to the + nearest X minutes would use the date's current hour as its base, etc. (For rounding to X + weeks we'd have to make a judgment about whether we use current month or current year; + year probably makes the most sense, as programming things that are defined as "biweekly" + might work better than if we used months.) + +For most use cases we will encounter, it essentially doesn't matter, because when we round +to a multiple of a Period, it's usually something that divides easily and nicely and hides +the problem. This example works the same regardless of what we choose: + +```julia> dt = DateTime(2016, 1, 2, 3, 21) +2016-01-02T03:21:00 + +julia> floor(dt, Dates.Minute(15)) +2016-01-02T03:15:00 +``` + +However, this example... + +```julia> dt = DateTime(2016, 1, 2, 3, 21) +2016-01-02T03:21:00 + +julia> floor(dt, Dates.Minute(11)) +``` + +...will return `2016-01-02T03:11:00` if we go with #5 (starting with current hour), +`2016-01-02T03:18:00` if we go with #4 (starting with current day), `2016-01-02T03:19:00` if +we go with #3 (starting with the current year) or #1, and `2016-01-02T03:17:00` if we go +with #2. + +#1, with a "zero date" of `0001-01-01` is cleanest (and probably fastest) for rounding days, +hours, minutes, seconds, and milliseconds. #2 would be similarly efficient, but would +present a challenge, because for dates between 0000-01-01 and 0001-01-01 `floor` would +actually round "up" (toward year 0001) instead of "down" (toward year 0000) because all +dates before 0000-12-31 are represented in Rata Die as negative numbers (even though Julia +displays the year 0000 as non-negative). + +#1 also has the advantage of being a (fairly) intuitive choice, given the way Julia choses +to represent its dates. For this reason, we elected to implement rounding based on #1. + +TODO: Add additional rationale. (It's common to want to round a datetime to the nearest +fifteen minutes or nearest hour or execute a task every two weeks...) + +TODO: Add this rationale for rounding weeks to the docs. + +Since `0000-01-01` is a Saturday, it can't be our "base date" for rounding to a certain +number of weeks (unless we want `floor(now(), Week(1))` to return last Saturday, which we +obviously don't). + +Instead, rounding to a number of weeks should round to the appropriate Monday (because, much +as it vexes me, Monday is the first day of the week in ISO 8601). + +The most intuitive approach to date rounding might be to use the first day of the week that +contains `0000-01-01` (which is `-0001-12-27`) as the "base date" for weeks. If we do this, +`floor(now(), Week(1))` returning this past Monday and `floor(Date0, 1, 1), Week(1))` +returning `-0001-12-27`. However, I contend that this would be wrong. + +Instead, I propose that we use the first day of the first week in 0000 as the base date. +Because the ISO defines the "first week" of a year as the week with the first Thursday in it +(for those unaware of this, surprise! :ghost:), this would make the base date for weeks +`0000-01-03` (the first Monday in 0000, which I guess is also intuitive in a way). +`floor(now(), Week(1))` and `floor(Date0, 1, 1), Week(1))` would have the same result as the +other system, but the results for rounding to every second week, every third week, etc. +would be different. + +TODO: Note that calling `ceil` with a number of years will round up to the start of a +year (January 1st). This is expected behaviour. + +TODO: Note that when flooring/ceiling dates to an even number of months (e.g., every +two months), this will inevitable result in an odd month number. This is because months +are one-indexed (i.e., the first month, January, is assigned "01"). +So `floor(Date(2016, 2, 13), Month(2))` will return `2016-01-01`. +This is also true for days (as they are also one-indexed), but it is obscured by the fact +that (most) years have an odd number of days, so which "day" is relevant when we're +concerned with "every second day" becomes less obvious the further we get from the epoch. + +TODO: CompoundPeriods left out because every solution I came up with was horrendously +inefficient. +=# + + # Adjusters """ firstdayofweek(dt::TimeType) -> TimeType diff --git a/base/dates/conversions.jl b/base/dates/conversions.jl index a3f82c74e2604..88639975683d7 100644 --- a/base/dates/conversions.jl +++ b/base/dates/conversions.jl @@ -133,6 +133,42 @@ epoch `-4713-11-24T12:00:00` as a `Float64`. """ datetime2julian(dt::DateTime) = (value(dt) - JULIANEPOCH)/86400000.0 +# ISO 8601 conversions +const ISO8601DATEEPOCH = value(Date(0, 1, 1)) +const ISO8601DATETIMEEPOCH = value(DateTime(0, 1, 1)) + +""" + iso86012date(days) -> DateTime + +Takes the number of days since epoch `0000-01-01T00:00:00` and returns the corresponding +`Date`. +""" +iso86012date(i) = Date(UTD(ISO8601DATEEPOCH + Int64(i))) + +""" + iso86012datetime(milliseconds) -> DateTime + +Takes the number of milliseconds since epoch `0000-01-01T00:00:00` and returns the +corresponding `DateTime`. +""" +iso86012datetime(i) = DateTime(UTM(ISO8601DATETIMEEPOCH + Int64(i))) + +""" + date2iso8601(dt::DateTime) -> Int64 + +Takes the given `Date` and returns the number of days since `0000-01-01T00:00:00` as an +`Int64`. +""" +date2iso8601(dt::Date) = value(dt) - ISO8601DATEEPOCH + +""" + datetime2iso8601(dt::DateTime) -> Int64 + +Takes the given `DateTime` and returns the number of milliseconds since +`0000-01-01T00:00:00` as an `Int64`. +""" +datetime2iso8601(dt::DateTime) = value(dt) - ISO8601DATETIMEEPOCH + @vectorize_1arg Real unix2datetime @vectorize_1arg DateTime datetime2unix @vectorize_1arg Real rata2datetime diff --git a/test/dates/adjusters.jl b/test/dates/adjusters.jl index c19c16fd4fe42..d67b06da4813b 100644 --- a/test/dates/adjusters.jl +++ b/test/dates/adjusters.jl @@ -14,6 +14,141 @@ dt = Dates.DateTime(2012,12,21,16,30,20,200) @test trunc(dt,Dates.Second) == Dates.DateTime(2012,12,21,16,30,20) @test trunc(dt,Dates.Millisecond) == Dates.DateTime(2012,12,21,16,30,20,200) +# Basic rounding tests +dt = Dates.Date(2016, 2, 28) # Sunday +@test floor(dt, Dates.Year) == Dates.Date(2016) +@test floor(dt, Dates.Year(5)) == Dates.Date(2015) +@test floor(dt, Dates.Year(10)) == Dates.Date(2010) +@test floor(dt, Dates.Month) == Dates.Date(2016, 2) +@test floor(dt, Dates.Month(6)) == Dates.Date(2016, 1) +@test floor(dt, Dates.Week) == toprev(dt, Dates.Monday) +@test ceil(dt, Dates.Year) == Dates.Date(2017) +@test ceil(dt, Dates.Year(5)) == Dates.Date(2020) +@test ceil(dt, Dates.Month) == Dates.Date(2016, 3) +@test ceil(dt, Dates.Month(6)) == Dates.Date(2016, 7) +@test ceil(dt, Dates.Week) == tonext(dt, Dates.Monday) +@test round(dt, Dates.Year) == Dates.Date(2016) +@test round(dt, Dates.Month) == Dates.Date(2016, 3) +@test round(dt, Dates.Week) == Dates.Date(2016, 2, 29) + +dt = Dates.DateTime(2016, 2, 28, 15, 10, 50, 500) +@test floor(dt, Dates.Day) == Dates.DateTime(2016, 2, 28) +@test floor(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 15) +@test floor(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 14) +@test floor(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 28, 12) +@test floor(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 10) +@test floor(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 0) +@test floor(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 50) +@test floor(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 10, 30) +@test ceil(dt, Dates.Day) == Dates.DateTime(2016, 2, 29) +@test ceil(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 16) +@test ceil(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 16) +@test ceil(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 29, 0) +@test ceil(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 11) +@test ceil(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 15) +@test ceil(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 51) +@test ceil(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 11, 0) +@test round(dt, Dates.Day) == Dates.DateTime(2016, 2, 29) +@test round(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 15) +@test round(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 16) +@test round(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 28, 12) +@test round(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 11) +@test round(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 15) +@test round(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 51) +@test round(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 11, 0) + +# Rounding for dates at the rounding epoch (year 0000) +dt = Dates.DateTime(0) +@test floor(dt, Dates.Year) == dt +@test floor(dt, Dates.Month) == dt +@test floor(dt, Dates.Week) == Dates.Date(-1, 12, 27) # Monday prior to 0000-01-01 +@test floor(Dates.Date(-1, 12, 27), Dates.Week) == Dates.Date(-1, 12, 27) +@test floor(dt, Dates.Day) == dt +@test floor(dt, Dates.Hour) == dt +@test floor(dt, Dates.Minute) == dt +@test floor(dt, Dates.Second) == dt +@test ceil(dt, Dates.Year) == dt +@test ceil(dt, Dates.Month) == dt +@test ceil(dt, Dates.Week) == Dates.Date(0, 1, 3) # Monday following 0000-01-01 +@test ceil(Dates.Date(0, 1, 3), Dates.Week) == Dates.Date(0, 1, 3) +@test ceil(dt, Dates.Day) == dt +@test ceil(dt, Dates.Hour) == dt +@test ceil(dt, Dates.Minute) == dt +@test ceil(dt, Dates.Second) == dt + +# Test rounding for multiples of a period (easiest to test close to rounding epoch) +dt = Dates.DateTime(0, 1, 19, 19, 19, 19, 19) +@test floor(dt, Dates.Year(2)) == DateTime(0) +@test floor(dt, Dates.Month(2)) == DateTime(0, 1) # Odd number; months are 1-indexed +@test floor(dt, Dates.Week(2)) == DateTime(0, 1, 17) # Third Monday of 0000 +@test floor(dt, Dates.Day(2)) == DateTime(0, 1, 19) # Odd number; days are 1-indexed +@test floor(dt, Dates.Hour(2)) == DateTime(0, 1, 19, 18) +@test floor(dt, Dates.Minute(2)) == DateTime(0, 1, 19, 19, 18) +@test floor(dt, Dates.Second(2)) == DateTime(0, 1, 19, 19, 19, 18) +@test ceil(dt, Dates.Year(2)) == DateTime(2) +@test ceil(dt, Dates.Month(2)) == DateTime(0, 3) # Odd number; months are 1-indexed +@test ceil(dt, Dates.Week(2)) == DateTime(0, 1, 31) # Fifth Monday of 0000 +@test ceil(dt, Dates.Day(2)) == DateTime(0, 1, 21) # Odd number; days are 1-indexed +@test ceil(dt, Dates.Hour(2)) == DateTime(0, 1, 19, 20) +@test ceil(dt, Dates.Minute(2)) == DateTime(0, 1, 19, 19, 20) +@test ceil(dt, Dates.Second(2)) == DateTime(0, 1, 19, 19, 19, 20) + +# Test rounding for dates with negative years +dt = Dates.DateTime(-1, 12, 29, 19, 19, 19, 19) +@test floor(dt, Dates.Year(2)) == DateTime(-2) +@test floor(dt, Dates.Month(2)) == DateTime(-1, 11) # Odd number; months are 1-indexed +@test floor(dt, Dates.Week(2)) == DateTime(-1, 12, 20) # 2 weeks prior to 0000-01-03 +@test floor(dt, Dates.Day(2)) == DateTime(-1, 12, 28) # Even; 4 days prior to 0000-01-01 +@test floor(dt, Dates.Hour(2)) == DateTime(-1, 12, 29, 18) +@test floor(dt, Dates.Minute(2)) == DateTime(-1, 12, 29, 19, 18) +@test floor(dt, Dates.Second(2)) == DateTime(-1, 12, 29, 19, 19, 18) +@test ceil(dt, Dates.Year(2)) == DateTime(0) +@test ceil(dt, Dates.Month(2)) == DateTime(0, 1) # Odd number; months are 1-indexed +@test ceil(dt, Dates.Week(2)) == DateTime(0, 1, 3) # First Monday of 0000 +@test ceil(dt, Dates.Day(2)) == DateTime(-1, 12, 30) # Even; 2 days prior to 0000-01-01 +@test ceil(dt, Dates.Hour(2)) == DateTime(-1, 12, 29, 20) +@test ceil(dt, Dates.Minute(2)) == DateTime(-1, 12, 29, 19, 20) +@test ceil(dt, Dates.Second(2)) == DateTime(-1, 12, 29, 19, 19, 20) + +# Test rounding for dates that should not need rounding +dt = Dates.DateTime(2016, 1, 1) +@test floor(dt, Dates.Year) == dt +@test floor(dt, Dates.Month) == dt +@test floor(dt, Dates.Day) == dt +@test floor(dt, Dates.Hour) == dt +@test floor(dt, Dates.Minute) == dt +@test floor(dt, Dates.Second) == dt +@test ceil(dt, Dates.Year) == dt +@test ceil(dt, Dates.Month) == dt +@test ceil(dt, Dates.Day) == dt +@test ceil(dt, Dates.Hour) == dt +@test ceil(dt, Dates.Minute) == dt +@test ceil(dt, Dates.Second) == dt + +dt = Dates.DateTime(-2016, 1, 1) +@test floor(dt, Dates.Year) == dt +@test floor(dt, Dates.Month) == dt +@test floor(dt, Dates.Day) == dt +@test floor(dt, Dates.Hour) == dt +@test floor(dt, Dates.Minute) == dt +@test floor(dt, Dates.Second) == dt +@test ceil(dt, Dates.Year) == dt +@test ceil(dt, Dates.Month) == dt +@test ceil(dt, Dates.Day) == dt +@test ceil(dt, Dates.Hour) == dt +@test ceil(dt, Dates.Minute) == dt +@test ceil(dt, Dates.Second) == dt + +# Test available RoundingModes +dt = Dates.DateTime(2016, 2, 28, 12) +@test round(dt, Dates.Day, RoundNearestTiesUp) == Dates.DateTime(2016, 2, 29) +@test round(dt, Dates.Day, RoundUp) == Dates.DateTime(2016, 2, 29) +@test round(dt, Dates.Day, RoundDown) == Dates.DateTime(2016, 2, 28) +@test_throws DomainError round(dt, Dates.Day, RoundNearest) +@test_throws DomainError round(dt, Dates.Day, RoundNearestTiesAway) +@test_throws DomainError round(dt, Dates.Day, RoundToZero) +@test round(dt, Dates.Day) == round(dt, Dates.Day, RoundNearestTiesUp) + # Date functions jan = Dates.DateTime(2013,1,1) #Tuesday feb = Dates.DateTime(2013,2,2) #Saturday diff --git a/test/dates/conversions.jl b/test/dates/conversions.jl index 3706c9ef96aa7..615ea431578cb 100644 --- a/test/dates/conversions.jl +++ b/test/dates/conversions.jl @@ -28,6 +28,7 @@ @test string(Dates.unix2datetime(915148801.00)) == string("1999-01-01T00:00:01") @test string(Dates.unix2datetime(915148801.25)) == string("1999-01-01T00:00:01.25") +# Test conversion to and from Rata Die @test Date(Dates.rata2datetime(734869)) == Dates.Date(2013,1,1) @test Dates.datetime2rata(Dates.rata2datetime(734869)) == 734869 @@ -43,6 +44,22 @@ @test Dates.julian2datetime(2452695.625) == Dates.DateTime(2003,2,25,3) @test Dates.datetime2julian(Dates.DateTime(2013,12,3,21)) == 2456630.375 +# Test conversion to and from the ISO 8601 year 0000 epoch (used for rounding) +@test Dates.iso86012date(-1) == Dates.Date(-1, 12, 31) +@test Dates.iso86012date(0) == Dates.Date(0, 1, 1) +@test Dates.iso86012date(1) == Dates.Date(0, 1, 2) +@test Dates.iso86012date(736329) == Dates.Date(2016, 1, 1) +@test Dates.iso86012datetime(-86400000) == Dates.DateTime(-1, 12, 31) +@test Dates.iso86012datetime(0) == Dates.DateTime(0, 1, 1) +@test Dates.iso86012datetime(86400000) == Dates.DateTime(0, 1, 2) +@test Dates.iso86012datetime(736329 * 86400000) == Dates.DateTime(2016, 1, 1) +@test Dates.date2iso8601(Dates.Date(-1, 12, 31)) == -1 +@test Dates.date2iso8601(Dates.Date(0, 1, 1)) == 0 +@test Dates.date2iso8601(Dates.Date(2016, 1, 1)) == 736329 +@test Dates.datetime2iso8601(Dates.DateTime(-1, 12, 31)) == -86400000 +@test Dates.datetime2iso8601(Dates.DateTime(0, 1, 1)) == 0 +@test Dates.datetime2iso8601(Dates.DateTime(2016, 1, 1)) == 736329 * 86400000 + @test typeof(Dates.now()) <: Dates.DateTime @test typeof(Dates.today()) <: Dates.Date @test typeof(Dates.now(Dates.UTC)) <: Dates.DateTime From c4959b2e7a35ce0248657d6c2d3949d08562f080 Mon Sep 17 00:00:00 2001 From: Gem Newman Date: Fri, 17 Jun 2016 14:31:31 -0500 Subject: [PATCH 2/5] Refactoring and documentation for date rounding. --- NEWS.md | 3 + base/dates/Dates.jl | 1 + base/dates/adjusters.jl | 174 -------------------------------------- base/dates/conversions.jl | 36 -------- base/dates/rounding.jl | 173 +++++++++++++++++++++++++++++++++++++ doc/manual/dates.rst | 91 ++++++++++++++++++++ doc/stdlib/dates.rst | 97 +++++++++++++++++++++ test/dates.jl | 1 + test/dates/adjusters.jl | 135 ----------------------------- test/dates/conversions.jl | 16 ---- test/dates/rounding.jl | 150 ++++++++++++++++++++++++++++++++ 11 files changed, 516 insertions(+), 361 deletions(-) create mode 100644 base/dates/rounding.jl create mode 100644 test/dates/rounding.jl diff --git a/NEWS.md b/NEWS.md index d332f885826bc..2384f00cd5f4f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -206,6 +206,9 @@ Library improvements * Prime number related functions have been moved from `Base` to the [Primes.jl package](https://github.com/JuliaMath/Primes.jl) ([#16481]). + * `Date` and `DateTime` values can now be rounded to a specified resolution (e.g., 1 month or + 15 minutes) with `floor`, `ceil`, and `round` ([#PR]). + Deprecated or removed --------------------- diff --git a/base/dates/Dates.jl b/base/dates/Dates.jl index f2efebd4a00aa..ff2165de586d0 100644 --- a/base/dates/Dates.jl +++ b/base/dates/Dates.jl @@ -12,6 +12,7 @@ include("arithmetic.jl") include("conversions.jl") include("ranges.jl") include("adjusters.jl") +include("rounding.jl") include("io.jl") export Period, DatePeriod, TimePeriod, diff --git a/base/dates/adjusters.jl b/base/dates/adjusters.jl index 9cede6221f6e7..4836bc4dafa40 100644 --- a/base/dates/adjusters.jl +++ b/base/dates/adjusters.jl @@ -21,180 +21,6 @@ Truncates the value of `dt` according to the provided `Period` type. E.g. if `dt """ Dates.trunc(::Dates.TimeType, ::Type{Dates.Period}) - -# Rounding - -# TODO: Docstrings - -function Base.floor(dt::Date, p::Year) - years = year(dt) - return Date(years - mod(years, value(p)), 1, 1) -end - -function Base.floor(dt::Date, p::Month) - months_since_epoch = year(dt) * 12 + month(dt) - 1 - month_offset = months_since_epoch - mod(months_since_epoch, value(p)) - target_month = mod(month_offset, 12) + 1 - target_year = div(month_offset, 12) - (month_offset < 0 && target_month != 1) - return Date(target_year, target_month, 1) -end - -# According to ISO 8601, the first day of the first week of year 0000 is 0000-01-03. -const ISO8601WEEKEPOCH = value(Date(0, 1, 3)) -function Base.floor(dt::Date, p::Week) - days = value(dt) - ISO8601WEEKEPOCH - days = days - mod(days, value(p) * 7) - return Date(UTD(ISO8601WEEKEPOCH + Int64(days))) -end - -# For days, the math is straightforward. -function Base.floor(dt::Date, p::Day) - days = date2iso8601(dt) - return iso86012date(days - mod(days, value(p))) -end - -Base.floor(dt::DateTime, p::DatePeriod) = DateTime(Base.floor(Date(dt), p)) - -# All TimePeriods are convertible to milliseconds, so the math is straightforward. -function Base.floor(dt::DateTime, p::TimePeriod) - milliseconds = datetime2iso8601(dt) - return iso86012datetime(milliseconds - mod(milliseconds, value(Millisecond(p)))) -end - -function Base.ceil(dt::TimeType, p::Period) - f = floor(dt, p) - return (dt == f) ? f : f + p -end - -function Base.round(dt::TimeType, p::Period, r::RoundingMode{:NearestTiesUp}) - f = floor(dt, p) - c = ceil(dt, p) - return (dt - f < c - dt) ? f : c -end - -Base.round(dt::TimeType, p::Period, r::RoundingMode{:Up}) = Base.ceil(dt, p) -Base.round(dt::TimeType, p::Period, r::RoundingMode{:Down}) = Base.floor(dt, p) - -# No implementation of rounding to nearest "even", because "even" is not defined for Period. -Base.round(::TimeType, ::Period, ::RoundingMode{:Nearest}) = throw(DomainError()) - -# No implementation of rounding toward/away from ISO 8601's arbitrary zero-point. -Base.round(::TimeType, ::Period, ::RoundingMode{:NearestTiesAway}) = throw(DomainError()) -Base.round(::TimeType, ::Period, ::RoundingMode{:ToZero}) = throw(DomainError()) - -# Default to RoundNearestTiesUp. -Base.round(dt::TimeType, p::Period) = Base.round(dt, p, RoundNearestTiesUp) - -# Callable using Period types in addition to values. -Base.floor{T <: Period}(dt::TimeType, p::Type{T}) = Base.floor(dt, p(1)) -Base.ceil{T <: Period}(dt::TimeType, p::Type{T}) = Base.ceil(dt, p(1)) - -function Base.round{T <: Period}(dt::TimeType, p::Type{T}, r=RoundNearestTiesUp) - return Base.round(dt, p(1), r) -end - -#= -TODO: Update documentation for dates and for RoundingMode to indicate that the default is -RoundNearestTiesUp - -TODO: Add example calls for rounding to the documentation and to the PR itself! - -TODO: Add some of this to the documentation. - -Who has opinions on date rounding? Specifically, I need to define a zero-point for rounding -a TimeType to a multiple of a Period. - -Candidate "zero dates" include: -1. `0000-01-01T00:00:00.000`, the first day of the first year specified by ISO 8601, which - Julia uses for external representations (display, etc.) of Date and DateTime -2. `0001-01-01T00:00:00.000`, the Rata Die base date, which is how Julia represents dates - internally -3. `YYYY-01-01T00:00:00.000`, where `YYYY` is the year of the date to be rounded -4. `YYYY-01-01T00:00:00.000` for rounding to `DatePeriod`s and `YYYY-MM-DDT00:00:00.000` for - rounding `TimePeriod`s. -5. "Closest current state" (better name to be determined), where the "zero date" is - dependent on the Period type to be used in rounding. Rounding a date to the nearest X - milliseconds would use the date's current second as its base; rounding a date to the - nearest X minutes would use the date's current hour as its base, etc. (For rounding to X - weeks we'd have to make a judgment about whether we use current month or current year; - year probably makes the most sense, as programming things that are defined as "biweekly" - might work better than if we used months.) - -For most use cases we will encounter, it essentially doesn't matter, because when we round -to a multiple of a Period, it's usually something that divides easily and nicely and hides -the problem. This example works the same regardless of what we choose: - -```julia> dt = DateTime(2016, 1, 2, 3, 21) -2016-01-02T03:21:00 - -julia> floor(dt, Dates.Minute(15)) -2016-01-02T03:15:00 -``` - -However, this example... - -```julia> dt = DateTime(2016, 1, 2, 3, 21) -2016-01-02T03:21:00 - -julia> floor(dt, Dates.Minute(11)) -``` - -...will return `2016-01-02T03:11:00` if we go with #5 (starting with current hour), -`2016-01-02T03:18:00` if we go with #4 (starting with current day), `2016-01-02T03:19:00` if -we go with #3 (starting with the current year) or #1, and `2016-01-02T03:17:00` if we go -with #2. - -#1, with a "zero date" of `0001-01-01` is cleanest (and probably fastest) for rounding days, -hours, minutes, seconds, and milliseconds. #2 would be similarly efficient, but would -present a challenge, because for dates between 0000-01-01 and 0001-01-01 `floor` would -actually round "up" (toward year 0001) instead of "down" (toward year 0000) because all -dates before 0000-12-31 are represented in Rata Die as negative numbers (even though Julia -displays the year 0000 as non-negative). - -#1 also has the advantage of being a (fairly) intuitive choice, given the way Julia choses -to represent its dates. For this reason, we elected to implement rounding based on #1. - -TODO: Add additional rationale. (It's common to want to round a datetime to the nearest -fifteen minutes or nearest hour or execute a task every two weeks...) - -TODO: Add this rationale for rounding weeks to the docs. - -Since `0000-01-01` is a Saturday, it can't be our "base date" for rounding to a certain -number of weeks (unless we want `floor(now(), Week(1))` to return last Saturday, which we -obviously don't). - -Instead, rounding to a number of weeks should round to the appropriate Monday (because, much -as it vexes me, Monday is the first day of the week in ISO 8601). - -The most intuitive approach to date rounding might be to use the first day of the week that -contains `0000-01-01` (which is `-0001-12-27`) as the "base date" for weeks. If we do this, -`floor(now(), Week(1))` returning this past Monday and `floor(Date0, 1, 1), Week(1))` -returning `-0001-12-27`. However, I contend that this would be wrong. - -Instead, I propose that we use the first day of the first week in 0000 as the base date. -Because the ISO defines the "first week" of a year as the week with the first Thursday in it -(for those unaware of this, surprise! :ghost:), this would make the base date for weeks -`0000-01-03` (the first Monday in 0000, which I guess is also intuitive in a way). -`floor(now(), Week(1))` and `floor(Date0, 1, 1), Week(1))` would have the same result as the -other system, but the results for rounding to every second week, every third week, etc. -would be different. - -TODO: Note that calling `ceil` with a number of years will round up to the start of a -year (January 1st). This is expected behaviour. - -TODO: Note that when flooring/ceiling dates to an even number of months (e.g., every -two months), this will inevitable result in an odd month number. This is because months -are one-indexed (i.e., the first month, January, is assigned "01"). -So `floor(Date(2016, 2, 13), Month(2))` will return `2016-01-01`. -This is also true for days (as they are also one-indexed), but it is obscured by the fact -that (most) years have an odd number of days, so which "day" is relevant when we're -concerned with "every second day" becomes less obvious the further we get from the epoch. - -TODO: CompoundPeriods left out because every solution I came up with was horrendously -inefficient. -=# - - # Adjusters """ firstdayofweek(dt::TimeType) -> TimeType diff --git a/base/dates/conversions.jl b/base/dates/conversions.jl index 88639975683d7..a3f82c74e2604 100644 --- a/base/dates/conversions.jl +++ b/base/dates/conversions.jl @@ -133,42 +133,6 @@ epoch `-4713-11-24T12:00:00` as a `Float64`. """ datetime2julian(dt::DateTime) = (value(dt) - JULIANEPOCH)/86400000.0 -# ISO 8601 conversions -const ISO8601DATEEPOCH = value(Date(0, 1, 1)) -const ISO8601DATETIMEEPOCH = value(DateTime(0, 1, 1)) - -""" - iso86012date(days) -> DateTime - -Takes the number of days since epoch `0000-01-01T00:00:00` and returns the corresponding -`Date`. -""" -iso86012date(i) = Date(UTD(ISO8601DATEEPOCH + Int64(i))) - -""" - iso86012datetime(milliseconds) -> DateTime - -Takes the number of milliseconds since epoch `0000-01-01T00:00:00` and returns the -corresponding `DateTime`. -""" -iso86012datetime(i) = DateTime(UTM(ISO8601DATETIMEEPOCH + Int64(i))) - -""" - date2iso8601(dt::DateTime) -> Int64 - -Takes the given `Date` and returns the number of days since `0000-01-01T00:00:00` as an -`Int64`. -""" -date2iso8601(dt::Date) = value(dt) - ISO8601DATEEPOCH - -""" - datetime2iso8601(dt::DateTime) -> Int64 - -Takes the given `DateTime` and returns the number of milliseconds since -`0000-01-01T00:00:00` as an `Int64`. -""" -datetime2iso8601(dt::DateTime) = value(dt) - ISO8601DATETIMEEPOCH - @vectorize_1arg Real unix2datetime @vectorize_1arg DateTime datetime2unix @vectorize_1arg Real rata2datetime diff --git a/base/dates/rounding.jl b/base/dates/rounding.jl new file mode 100644 index 0000000000000..6a5ce62ca3d53 --- /dev/null +++ b/base/dates/rounding.jl @@ -0,0 +1,173 @@ +# The epochs used for date rounding are based ISO 8601's "year zero" notation +const DATEEPOCH = value(Date(0)) +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)) + +""" + epochdays2date(days) -> DateTime + +Takes the number of days since the rounding epoch (`0000-01-01T00:00:00`) and returns the +corresponding `Date`. +""" +epochdays2date(i) = Date(UTD(DATEEPOCH + Int64(i))) + +""" + epochms2datetime(milliseconds) -> DateTime + +Takes the number of milliseconds since the rounding epoch (`0000-01-01T00:00:00`) and +returns the corresponding `DateTime`. +""" +epochms2datetime(i) = DateTime(UTM(DATETIMEEPOCH + Int64(i))) + +""" + date2epochdays(dt::DateTime) -> Int64 + +Takes the given `Date` and returns the number of days since the rounding epoch +(`0000-01-01T00:00:00`) as an `Int64`. +""" +date2epochdays(dt::Date) = value(dt) - DATEEPOCH + +""" + datetime2epochms(dt::DateTime) -> Int64 + +Takes the given `DateTime` and returns the number of milliseconds since the rounding epoch +(`0000-01-01T00:00:00`) as an `Int64`. +""" +datetime2epochms(dt::DateTime) = value(dt) - DATETIMEEPOCH + +function Base.floor(dt::Date, p::Year) + years = year(dt) + return Date(years - mod(years, value(p))) +end + +function Base.floor(dt::Date, p::Month) + y, m = yearmonth(dt) + months_since_epoch = y * 12 + m - 1 + month_offset = months_since_epoch - mod(months_since_epoch, value(p)) + target_month = mod(month_offset, 12) + 1 + target_year = div(month_offset, 12) - (month_offset < 0 && target_month != 1) + return Date(target_year, target_month) +end + +function Base.floor(dt::Date, p::Week) + days = value(dt) - WEEKEPOCH + days = days - mod(days, value(Day(p))) + return Date(UTD(WEEKEPOCH + Int64(days))) +end + +function Base.floor(dt::Date, p::Day) + days = date2epochdays(dt) + return epochdays2date(days - mod(days, value(p))) +end + +Base.floor(dt::DateTime, p::DatePeriod) = DateTime(Base.floor(Date(dt), p)) + +function Base.floor(dt::DateTime, p::TimePeriod) + milliseconds = datetime2epochms(dt) + return epochms2datetime(milliseconds - mod(milliseconds, value(Millisecond(p)))) +end + +""" + floor(dt::TimeType, p::Period) -> TimeType + +Returns the nearest `Date` or `DateTime` less than or equal to `dt` at resolution `p`. + +For convenience, `p` may be a type instead of a value: `floor(dt, Dates.Hour)` is a shortcut +for `floor(dt, Dates.Hour(1))`. + +```jldoctest +julia> floor(Date(1985, 8, 16), Dates.Month) +1985-08-01 + +julia> floor(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15)) +2013-02-13T00:30:00 + +julia> floor(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day) +2016-08-06T00:00:00 +``` +""" +Base.floor(::Dates.TimeType, ::Dates.Period) + +""" + ceil(dt::TimeType, p::Period) -> TimeType + +Returns the nearest `Date` or `DateTime` greater than or equal to `dt` at resolution `p`. + +For convenience, `p` may be a type instead of a value: `ceil(dt, Dates.Hour)` is a shortcut +for `ceil(dt, Dates.Hour(1))`. + +```jldoctest +julia> ceil(Date(1985, 8, 16), Dates.Month) +1985-09-01 + +julia> ceil(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15)) +2013-02-13T00:45:00 + +julia> ceil(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day) +2016-08-07T00:00:00 +``` +""" +function Base.ceil(dt::TimeType, p::Period) + f = floor(dt, p) + return (dt == f) ? f : f + p +end + +""" + floorceil(dt::TimeType, p::Period) -> (TimeType, TimeType) + +Simultaneously return the `floor` and `ceil` of a `Date` or `DateTime` at resolution `p`. +More efficient than calling both `floor` and `ceil` individually. +""" +function floorceil(dt::TimeType, p::Period) + f = floor(dt, p) + return f, (dt == f) ? f : f + p +end + +""" + round(dt::TimeType, p::Period, [r::RoundingMode]) -> TimeType + +Returns the `Date` or `DateTime` nearest to `dt` at resolution `p`. By default +(`RoundNearestTiesUp`), ties (e.g., rounding 9:30 to the nearest hour) will be rounded up. + +For convenience, `p` may be a type instead of a value: `round(dt, Dates.Hour)` is a shortcut +for `round(dt, Dates.Hour(1))`. + +```jldoctest +julia> round(Date(1985, 8, 16), Dates.Month) +1985-08-01 + +julia> round(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15)) +2013-02-13T00:30:00 + +julia> round(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day) +2016-08-07T00:00:00 +``` + +Valid rounding modes for `round(::TimeType, ::Period, ::RoundingMode)` are +`RoundNearestTiesUp` (default), `RoundDown` (`floor`), and `RoundUp` (`ceil`). +""" +function Base.round(dt::TimeType, p::Period, r::RoundingMode{:NearestTiesUp}) + f, c = floorceil(dt, p) + 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) + +# 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, ::Period, ::RoundingMode) = throw(DomainError()) + +# Default to RoundNearestTiesUp. +Base.round(dt::TimeType, p::Period) = Base.round(dt, p, RoundNearestTiesUp) + +# Make rounding functions callable using Period types in addition to values. +Base.floor{T <: Period}(dt::TimeType, p::Type{T}) = Base.floor(dt, p(1)) +Base.ceil{T <: Period}(dt::TimeType, p::Type{T}) = Base.ceil(dt, p(1)) + +function Base.round{T<:Period}(dt::TimeType, p::Type{T}, r::RoundingMode=RoundNearestTiesUp) + return Base.round(dt, p(1), r) +end diff --git a/doc/manual/dates.rst b/doc/manual/dates.rst index bf589a46fad9f..253371b4d95a8 100644 --- a/doc/manual/dates.rst +++ b/doc/manual/dates.rst @@ -367,4 +367,95 @@ Periods are a human view of discrete, sometimes irregular durations of time. Con 3 years +Rounding +-------- + +:class:`Date` and :class:`DateTime` values can be rounded to a specified resolution (e.g., +1 month or 15 minutes) with :func:`floor`, :func:`ceil`, or :func:`round`: + +.. doctest:: + + julia> floor(Date(1985, 8, 16), Dates.Month) + 1985-08-01 + + julia> ceil(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15)) + 2013-02-13T00:45:00 + + julia> round(DateTime(2016, 8, 6, 20, 15), Dates.Day) + 2016-08-07T00:00:00 + +Unlike the numeric :func:`round` method, which breaks ties toward the even number by +default, the :class:`TimeType` :func:`round` method uses the ``RoundNearestTiesUp`` +rounding mode. (It's difficult to guess what breaking ties to nearest "even" +:class:`TimeType` would entail.) Further details on the available ``RoundingMode`` s can +be found in the +`API reference `_. + +Rounding should generally behave as expected, but there are a few cases in which the +expected behaviour is not obvious. + +Rounding Epoch +~~~~~~~~~~~~~~ + +In many cases, the resolution specified for rounding (e.g., ``Dates.Second(30)``) divides +evenly into the next largest period (in this case, ``Dates.Minute(1)``). But rounding +behaviour in cases in which this is not true may lead to confusion. What is the expected +result of rounding a :class:`DateTime` to the nearest 10 hours? + +.. doctest:: + + julia> round(DateTime(2016, 7, 17, 11, 55), Dates.Hour(10)) + 2016-07-17T12:00:00 + +That may seem confusing, given that the hour (12) is not divisible by 10. The reason that +``2016-07-17T12:00:00`` was chosen is that it is 17,676,660 hours after +``0000-01-01T00:00:00``, and 17,676,660 is divisible by 10. + +As Julia :class:`Date` and :class:`DateTime` values are represented according to the ISO +8601 standard, ``0000-01-01T00:00:00`` was chosen as base (or "rounding epoch") from +which to begin the count of days (and milliseconds) used in rounding calculations. (Note +that this differs slightly from Julia's internal representation of :class:`Date` s using +Rata Die notation; but since the ISO 8601 standard is most visible to the end user, +``0000-01-01T00:00:00`` was chosen as the rounding epoch instead of the +``0000-12-31T00:00:00`` used internally to minimize confusion.) + +The only exception to the use of ``0000-01-01T00:00:00`` as the rounding epoch is when +rounding to weeks. Rounding to the nearest week will always return a Monday (the first day +of the week as specified by ISO 8601). For this reason, we use ``0000-01-03T00:00:00`` +(the first day of the first week of year 0000, as defined by ISO 8601) as the base when +rounding to a number of weeks. + +Here is a related case in which the expected behaviour is not necessarily obvious: What +happens when we round to the nearest ``P(2)``, where ``P`` is a :class:`Period` type? In +some cases (specifically, when ``P <: Dates.TimePeriod``) the answer is clear: + +.. doctest:: + + julia> round(DateTime(2016, 7, 17, 8, 55, 30), Dates.Hour(2)) + 2016-07-17T08:00:00 + + julia> round(DateTime(2016, 7, 17, 8, 55, 30), Dates.Minute(2)) + 2016-07-17T08:56:00 + +This seems obvious, because two of each of these periods still divides evenly into the +next larger order period. But in the case of two months (which still divides evenly into +one year), the answer may be surprising: + +.. doctest:: + + julia> round(DateTime(2016, 7, 17, 8, 55, 30), Dates.Month(2)) + 2016-07-01T00:00:00 + +Why round to the first day in July, even though it is month 7 (an odd number)? The key is +that months are 1-indexed (the first month is assigned 1), unlike hours, minutes, seconds, +and milliseconds (the first of which are assigned 0). + +This means that rounding seconds, minutes, hours, or years (because the ISO 8601 +specification includes a year zero) to an even multiple will result in that field having +an even value, while rounding to an even multiple of months will result in that field +having an odd value. Because both months and years may contain an irregular number of +days, whether rounding to an even number of days will result in an even value in the days +field is uncertain. + + See the `API reference `_ for additional information on methods exported from the :mod:`Dates` module. diff --git a/doc/stdlib/dates.rst b/doc/stdlib/dates.rst index dd9ef62218a81..cd0698b66b917 100644 --- a/doc/stdlib/dates.rst +++ b/doc/stdlib/dates.rst @@ -575,6 +575,103 @@ Periods Returns a sensible "default" value for the input Period by returning ``one(p)`` for Year, Month, and Day, and ``zero(p)`` for Hour, Minute, Second, and Millisecond. +Rounding Functions +~~~~~~~~~~~~~~~~~~ + +``Date`` and ``DateTime`` values can be rounded to a specified resolution (e.g., 1 month +or 15 minutes) with ``floor``, ``ceil``, or ``round``. + +.. function:: floor(dt::TimeType, p::Period) -> TimeType + + .. Docstring generated from Julia source + + Returns the nearest ``Date`` or ``DateTime`` less than or equal to ``dt`` at resolution ``p``\ . + + For convenience, ``p`` may be a type instead of a value: ``floor(dt, Dates.Hour)`` is a shortcut for ``floor(dt, Dates.Hour(1))``\ . + + .. doctest:: + + julia> floor(Date(1985, 8, 16), Dates.Month) + 1985-08-01 + + julia> floor(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15)) + 2013-02-13T00:30:00 + + julia> floor(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day) + 2016-08-06T00:00:00 + +.. function:: ceil(dt::TimeType, p::Period) -> TimeType + + .. Docstring generated from Julia source + + Returns the nearest ``Date`` or ``DateTime`` greater than or equal to ``dt`` at resolution ``p``\ . + + For convenience, ``p`` may be a type instead of a value: ``ceil(dt, Dates.Hour)`` is a shortcut for ``ceil(dt, Dates.Hour(1))``\ . + + .. doctest:: + + julia> ceil(Date(1985, 8, 16), Dates.Month) + 1985-09-01 + + julia> ceil(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15)) + 2013-02-13T00:45:00 + + julia> ceil(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day) + 2016-08-07T00:00:00 + +.. function:: round(dt::TimeType, p::Period, [r::RoundingMode]) -> TimeType + + .. Docstring generated from Julia source + + Returns the ``Date`` or ``DateTime`` nearest to ``dt`` at resolution ``p``\ . By default (``RoundNearestTiesUp``\ ), ties (e.g., rounding 9:30 to the nearest hour) will be rounded up. + + For convenience, ``p`` may be a type instead of a value: ``round(dt, Dates.Hour)`` is a shortcut for ``round(dt, Dates.Hour(1))``\ . + + .. doctest:: + + julia> round(Date(1985, 8, 16), Dates.Month) + 1985-08-01 + + julia> round(DateTime(2013, 2, 13, 0, 31, 20), Dates.Minute(15)) + 2013-02-13T00:30:00 + + julia> round(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day) + 2016-08-07T00:00:00 + + Valid RoundingModes for ``round(::TimeType, ::Period, ::RoundingMode)`` are ``RoundNearestTiesUp`` (default), ``RoundDown`` (``floor``\ ), and ``RoundUp`` (``ceil``\ ). + +The following functions are not exported: + +.. function:: floorceil(dt::TimeType, p::Period) -> (TimeType, TimeType) + + .. Docstring generated from Julia source + + Simultaneously return the ``floor`` and ``ceil`` of a ``Date`` or ``DateTime`` at resolution ``p``\ . More efficient than calling both ``floor`` and ``ceil`` individually. + +.. function:: epochdays2date(days) -> DateTime + + .. Docstring generated from Julia source + + Takes the number of days since the rounding epoch (``0000-01-01T00:00:00``\ ) and returns the corresponding ``Date``\ . + +.. function:: epochms2datetime(milliseconds) -> DateTime + + .. Docstring generated from Julia source + + Takes the number of milliseconds since the rounding epoch (``0000-01-01T00:00:00``\ ) and returns the corresponding ``DateTime``\ . + +.. function:: date2epochdays(dt::DateTime) -> Int64 + + .. Docstring generated from Julia source + + Takes the given ``Date`` and returns the number of days since the rounding epoch (``0000-01-01T00:00:00``\ ) as an ``Int64``\ . + +.. function:: datetime2epochms(dt::DateTime) -> Int64 + + .. Docstring generated from Julia source + + Takes the given ``DateTime`` and returns the number of milliseconds since the rounding epoch (``0000-01-01T00:00:00``\ ) as an ``Int64``\ . + Conversion Functions ~~~~~~~~~~~~~~~~~~~~ diff --git a/test/dates.jl b/test/dates.jl index 6c8467a9f1acc..ea654b7c2fe47 100644 --- a/test/dates.jl +++ b/test/dates.jl @@ -13,6 +13,7 @@ include("dates/arithmetic.jl") include("dates/conversions.jl") include("dates/ranges.jl") include("dates/adjusters.jl") +include("dates/rounding.jl") include("dates/io.jl") end diff --git a/test/dates/adjusters.jl b/test/dates/adjusters.jl index d67b06da4813b..c19c16fd4fe42 100644 --- a/test/dates/adjusters.jl +++ b/test/dates/adjusters.jl @@ -14,141 +14,6 @@ dt = Dates.DateTime(2012,12,21,16,30,20,200) @test trunc(dt,Dates.Second) == Dates.DateTime(2012,12,21,16,30,20) @test trunc(dt,Dates.Millisecond) == Dates.DateTime(2012,12,21,16,30,20,200) -# Basic rounding tests -dt = Dates.Date(2016, 2, 28) # Sunday -@test floor(dt, Dates.Year) == Dates.Date(2016) -@test floor(dt, Dates.Year(5)) == Dates.Date(2015) -@test floor(dt, Dates.Year(10)) == Dates.Date(2010) -@test floor(dt, Dates.Month) == Dates.Date(2016, 2) -@test floor(dt, Dates.Month(6)) == Dates.Date(2016, 1) -@test floor(dt, Dates.Week) == toprev(dt, Dates.Monday) -@test ceil(dt, Dates.Year) == Dates.Date(2017) -@test ceil(dt, Dates.Year(5)) == Dates.Date(2020) -@test ceil(dt, Dates.Month) == Dates.Date(2016, 3) -@test ceil(dt, Dates.Month(6)) == Dates.Date(2016, 7) -@test ceil(dt, Dates.Week) == tonext(dt, Dates.Monday) -@test round(dt, Dates.Year) == Dates.Date(2016) -@test round(dt, Dates.Month) == Dates.Date(2016, 3) -@test round(dt, Dates.Week) == Dates.Date(2016, 2, 29) - -dt = Dates.DateTime(2016, 2, 28, 15, 10, 50, 500) -@test floor(dt, Dates.Day) == Dates.DateTime(2016, 2, 28) -@test floor(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 15) -@test floor(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 14) -@test floor(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 28, 12) -@test floor(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 10) -@test floor(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 0) -@test floor(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 50) -@test floor(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 10, 30) -@test ceil(dt, Dates.Day) == Dates.DateTime(2016, 2, 29) -@test ceil(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 16) -@test ceil(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 16) -@test ceil(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 29, 0) -@test ceil(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 11) -@test ceil(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 15) -@test ceil(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 51) -@test ceil(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 11, 0) -@test round(dt, Dates.Day) == Dates.DateTime(2016, 2, 29) -@test round(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 15) -@test round(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 16) -@test round(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 28, 12) -@test round(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 11) -@test round(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 15) -@test round(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 51) -@test round(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 11, 0) - -# Rounding for dates at the rounding epoch (year 0000) -dt = Dates.DateTime(0) -@test floor(dt, Dates.Year) == dt -@test floor(dt, Dates.Month) == dt -@test floor(dt, Dates.Week) == Dates.Date(-1, 12, 27) # Monday prior to 0000-01-01 -@test floor(Dates.Date(-1, 12, 27), Dates.Week) == Dates.Date(-1, 12, 27) -@test floor(dt, Dates.Day) == dt -@test floor(dt, Dates.Hour) == dt -@test floor(dt, Dates.Minute) == dt -@test floor(dt, Dates.Second) == dt -@test ceil(dt, Dates.Year) == dt -@test ceil(dt, Dates.Month) == dt -@test ceil(dt, Dates.Week) == Dates.Date(0, 1, 3) # Monday following 0000-01-01 -@test ceil(Dates.Date(0, 1, 3), Dates.Week) == Dates.Date(0, 1, 3) -@test ceil(dt, Dates.Day) == dt -@test ceil(dt, Dates.Hour) == dt -@test ceil(dt, Dates.Minute) == dt -@test ceil(dt, Dates.Second) == dt - -# Test rounding for multiples of a period (easiest to test close to rounding epoch) -dt = Dates.DateTime(0, 1, 19, 19, 19, 19, 19) -@test floor(dt, Dates.Year(2)) == DateTime(0) -@test floor(dt, Dates.Month(2)) == DateTime(0, 1) # Odd number; months are 1-indexed -@test floor(dt, Dates.Week(2)) == DateTime(0, 1, 17) # Third Monday of 0000 -@test floor(dt, Dates.Day(2)) == DateTime(0, 1, 19) # Odd number; days are 1-indexed -@test floor(dt, Dates.Hour(2)) == DateTime(0, 1, 19, 18) -@test floor(dt, Dates.Minute(2)) == DateTime(0, 1, 19, 19, 18) -@test floor(dt, Dates.Second(2)) == DateTime(0, 1, 19, 19, 19, 18) -@test ceil(dt, Dates.Year(2)) == DateTime(2) -@test ceil(dt, Dates.Month(2)) == DateTime(0, 3) # Odd number; months are 1-indexed -@test ceil(dt, Dates.Week(2)) == DateTime(0, 1, 31) # Fifth Monday of 0000 -@test ceil(dt, Dates.Day(2)) == DateTime(0, 1, 21) # Odd number; days are 1-indexed -@test ceil(dt, Dates.Hour(2)) == DateTime(0, 1, 19, 20) -@test ceil(dt, Dates.Minute(2)) == DateTime(0, 1, 19, 19, 20) -@test ceil(dt, Dates.Second(2)) == DateTime(0, 1, 19, 19, 19, 20) - -# Test rounding for dates with negative years -dt = Dates.DateTime(-1, 12, 29, 19, 19, 19, 19) -@test floor(dt, Dates.Year(2)) == DateTime(-2) -@test floor(dt, Dates.Month(2)) == DateTime(-1, 11) # Odd number; months are 1-indexed -@test floor(dt, Dates.Week(2)) == DateTime(-1, 12, 20) # 2 weeks prior to 0000-01-03 -@test floor(dt, Dates.Day(2)) == DateTime(-1, 12, 28) # Even; 4 days prior to 0000-01-01 -@test floor(dt, Dates.Hour(2)) == DateTime(-1, 12, 29, 18) -@test floor(dt, Dates.Minute(2)) == DateTime(-1, 12, 29, 19, 18) -@test floor(dt, Dates.Second(2)) == DateTime(-1, 12, 29, 19, 19, 18) -@test ceil(dt, Dates.Year(2)) == DateTime(0) -@test ceil(dt, Dates.Month(2)) == DateTime(0, 1) # Odd number; months are 1-indexed -@test ceil(dt, Dates.Week(2)) == DateTime(0, 1, 3) # First Monday of 0000 -@test ceil(dt, Dates.Day(2)) == DateTime(-1, 12, 30) # Even; 2 days prior to 0000-01-01 -@test ceil(dt, Dates.Hour(2)) == DateTime(-1, 12, 29, 20) -@test ceil(dt, Dates.Minute(2)) == DateTime(-1, 12, 29, 19, 20) -@test ceil(dt, Dates.Second(2)) == DateTime(-1, 12, 29, 19, 19, 20) - -# Test rounding for dates that should not need rounding -dt = Dates.DateTime(2016, 1, 1) -@test floor(dt, Dates.Year) == dt -@test floor(dt, Dates.Month) == dt -@test floor(dt, Dates.Day) == dt -@test floor(dt, Dates.Hour) == dt -@test floor(dt, Dates.Minute) == dt -@test floor(dt, Dates.Second) == dt -@test ceil(dt, Dates.Year) == dt -@test ceil(dt, Dates.Month) == dt -@test ceil(dt, Dates.Day) == dt -@test ceil(dt, Dates.Hour) == dt -@test ceil(dt, Dates.Minute) == dt -@test ceil(dt, Dates.Second) == dt - -dt = Dates.DateTime(-2016, 1, 1) -@test floor(dt, Dates.Year) == dt -@test floor(dt, Dates.Month) == dt -@test floor(dt, Dates.Day) == dt -@test floor(dt, Dates.Hour) == dt -@test floor(dt, Dates.Minute) == dt -@test floor(dt, Dates.Second) == dt -@test ceil(dt, Dates.Year) == dt -@test ceil(dt, Dates.Month) == dt -@test ceil(dt, Dates.Day) == dt -@test ceil(dt, Dates.Hour) == dt -@test ceil(dt, Dates.Minute) == dt -@test ceil(dt, Dates.Second) == dt - -# Test available RoundingModes -dt = Dates.DateTime(2016, 2, 28, 12) -@test round(dt, Dates.Day, RoundNearestTiesUp) == Dates.DateTime(2016, 2, 29) -@test round(dt, Dates.Day, RoundUp) == Dates.DateTime(2016, 2, 29) -@test round(dt, Dates.Day, RoundDown) == Dates.DateTime(2016, 2, 28) -@test_throws DomainError round(dt, Dates.Day, RoundNearest) -@test_throws DomainError round(dt, Dates.Day, RoundNearestTiesAway) -@test_throws DomainError round(dt, Dates.Day, RoundToZero) -@test round(dt, Dates.Day) == round(dt, Dates.Day, RoundNearestTiesUp) - # Date functions jan = Dates.DateTime(2013,1,1) #Tuesday feb = Dates.DateTime(2013,2,2) #Saturday diff --git a/test/dates/conversions.jl b/test/dates/conversions.jl index 615ea431578cb..db3fd97948ee0 100644 --- a/test/dates/conversions.jl +++ b/test/dates/conversions.jl @@ -44,22 +44,6 @@ @test Dates.julian2datetime(2452695.625) == Dates.DateTime(2003,2,25,3) @test Dates.datetime2julian(Dates.DateTime(2013,12,3,21)) == 2456630.375 -# Test conversion to and from the ISO 8601 year 0000 epoch (used for rounding) -@test Dates.iso86012date(-1) == Dates.Date(-1, 12, 31) -@test Dates.iso86012date(0) == Dates.Date(0, 1, 1) -@test Dates.iso86012date(1) == Dates.Date(0, 1, 2) -@test Dates.iso86012date(736329) == Dates.Date(2016, 1, 1) -@test Dates.iso86012datetime(-86400000) == Dates.DateTime(-1, 12, 31) -@test Dates.iso86012datetime(0) == Dates.DateTime(0, 1, 1) -@test Dates.iso86012datetime(86400000) == Dates.DateTime(0, 1, 2) -@test Dates.iso86012datetime(736329 * 86400000) == Dates.DateTime(2016, 1, 1) -@test Dates.date2iso8601(Dates.Date(-1, 12, 31)) == -1 -@test Dates.date2iso8601(Dates.Date(0, 1, 1)) == 0 -@test Dates.date2iso8601(Dates.Date(2016, 1, 1)) == 736329 -@test Dates.datetime2iso8601(Dates.DateTime(-1, 12, 31)) == -86400000 -@test Dates.datetime2iso8601(Dates.DateTime(0, 1, 1)) == 0 -@test Dates.datetime2iso8601(Dates.DateTime(2016, 1, 1)) == 736329 * 86400000 - @test typeof(Dates.now()) <: Dates.DateTime @test typeof(Dates.today()) <: Dates.Date @test typeof(Dates.now(Dates.UTC)) <: Dates.DateTime diff --git a/test/dates/rounding.jl b/test/dates/rounding.jl new file mode 100644 index 0000000000000..8a1e208457c50 --- /dev/null +++ b/test/dates/rounding.jl @@ -0,0 +1,150 @@ +# Test conversion to and from the rounding epoch (ISO 8601 year 0000) +@test Dates.epochdays2date(-1) == Dates.Date(-1, 12, 31) +@test Dates.epochdays2date(0) == Dates.Date(0, 1, 1) +@test Dates.epochdays2date(1) == Dates.Date(0, 1, 2) +@test Dates.epochdays2date(736329) == Dates.Date(2016, 1, 1) +@test Dates.epochms2datetime(-86400000) == Dates.DateTime(-1, 12, 31) +@test Dates.epochms2datetime(0) == Dates.DateTime(0, 1, 1) +@test Dates.epochms2datetime(86400000) == Dates.DateTime(0, 1, 2) +@test Dates.epochms2datetime(736329 * 86400000) == Dates.DateTime(2016, 1, 1) +@test Dates.date2epochdays(Dates.Date(-1, 12, 31)) == -1 +@test Dates.date2epochdays(Dates.Date(0, 1, 1)) == 0 +@test Dates.date2epochdays(Dates.Date(2016, 1, 1)) == 736329 +@test Dates.datetime2epochms(Dates.DateTime(-1, 12, 31)) == -86400000 +@test Dates.datetime2epochms(Dates.DateTime(0, 1, 1)) == 0 +@test Dates.datetime2epochms(Dates.DateTime(2016, 1, 1)) == 736329 * 86400000 + +# Basic rounding tests +dt = Dates.Date(2016, 2, 28) # Sunday +@test floor(dt, Dates.Year) == Dates.Date(2016) +@test floor(dt, Dates.Year(5)) == Dates.Date(2015) +@test floor(dt, Dates.Year(10)) == Dates.Date(2010) +@test floor(dt, Dates.Month) == Dates.Date(2016, 2) +@test floor(dt, Dates.Month(6)) == Dates.Date(2016, 1) +@test floor(dt, Dates.Week) == toprev(dt, Dates.Monday) +@test ceil(dt, Dates.Year) == Dates.Date(2017) +@test ceil(dt, Dates.Year(5)) == Dates.Date(2020) +@test ceil(dt, Dates.Month) == Dates.Date(2016, 3) +@test ceil(dt, Dates.Month(6)) == Dates.Date(2016, 7) +@test ceil(dt, Dates.Week) == tonext(dt, Dates.Monday) +@test round(dt, Dates.Year) == Dates.Date(2016) +@test round(dt, Dates.Month) == Dates.Date(2016, 3) +@test round(dt, Dates.Week) == Dates.Date(2016, 2, 29) + +dt = Dates.DateTime(2016, 2, 28, 15, 10, 50, 500) +@test floor(dt, Dates.Day) == Dates.DateTime(2016, 2, 28) +@test floor(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 15) +@test floor(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 14) +@test floor(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 28, 12) +@test floor(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 10) +@test floor(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 0) +@test floor(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 50) +@test floor(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 10, 30) +@test ceil(dt, Dates.Day) == Dates.DateTime(2016, 2, 29) +@test ceil(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 16) +@test ceil(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 16) +@test ceil(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 29, 0) +@test ceil(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 11) +@test ceil(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 15) +@test ceil(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 51) +@test ceil(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 11, 0) +@test round(dt, Dates.Day) == Dates.DateTime(2016, 2, 29) +@test round(dt, Dates.Hour) == Dates.DateTime(2016, 2, 28, 15) +@test round(dt, Dates.Hour(2)) == Dates.DateTime(2016, 2, 28, 16) +@test round(dt, Dates.Hour(12)) == Dates.DateTime(2016, 2, 28, 12) +@test round(dt, Dates.Minute) == Dates.DateTime(2016, 2, 28, 15, 11) +@test round(dt, Dates.Minute(15)) == Dates.DateTime(2016, 2, 28, 15, 15) +@test round(dt, Dates.Second) == Dates.DateTime(2016, 2, 28, 15, 10, 51) +@test round(dt, Dates.Second(30)) == Dates.DateTime(2016, 2, 28, 15, 11, 0) + +# Rounding for dates at the rounding epoch (year 0000) +dt = Dates.DateTime(0) +@test floor(dt, Dates.Year) == dt +@test floor(dt, Dates.Month) == dt +@test floor(dt, Dates.Week) == Dates.Date(-1, 12, 27) # Monday prior to 0000-01-01 +@test floor(Dates.Date(-1, 12, 27), Dates.Week) == Dates.Date(-1, 12, 27) +@test floor(dt, Dates.Day) == dt +@test floor(dt, Dates.Hour) == dt +@test floor(dt, Dates.Minute) == dt +@test floor(dt, Dates.Second) == dt +@test ceil(dt, Dates.Year) == dt +@test ceil(dt, Dates.Month) == dt +@test ceil(dt, Dates.Week) == Dates.Date(0, 1, 3) # Monday following 0000-01-01 +@test ceil(Dates.Date(0, 1, 3), Dates.Week) == Dates.Date(0, 1, 3) +@test ceil(dt, Dates.Day) == dt +@test ceil(dt, Dates.Hour) == dt +@test ceil(dt, Dates.Minute) == dt +@test ceil(dt, Dates.Second) == dt + +# Test rounding for multiples of a period (easiest to test close to rounding epoch) +dt = Dates.DateTime(0, 1, 19, 19, 19, 19, 19) +@test floor(dt, Dates.Year(2)) == DateTime(0) +@test floor(dt, Dates.Month(2)) == DateTime(0, 1) # Odd number; months are 1-indexed +@test floor(dt, Dates.Week(2)) == DateTime(0, 1, 17) # Third Monday of 0000 +@test floor(dt, Dates.Day(2)) == DateTime(0, 1, 19) # Odd number; days are 1-indexed +@test floor(dt, Dates.Hour(2)) == DateTime(0, 1, 19, 18) +@test floor(dt, Dates.Minute(2)) == DateTime(0, 1, 19, 19, 18) +@test floor(dt, Dates.Second(2)) == DateTime(0, 1, 19, 19, 19, 18) +@test ceil(dt, Dates.Year(2)) == DateTime(2) +@test ceil(dt, Dates.Month(2)) == DateTime(0, 3) # Odd number; months are 1-indexed +@test ceil(dt, Dates.Week(2)) == DateTime(0, 1, 31) # Fifth Monday of 0000 +@test ceil(dt, Dates.Day(2)) == DateTime(0, 1, 21) # Odd number; days are 1-indexed +@test ceil(dt, Dates.Hour(2)) == DateTime(0, 1, 19, 20) +@test ceil(dt, Dates.Minute(2)) == DateTime(0, 1, 19, 19, 20) +@test ceil(dt, Dates.Second(2)) == DateTime(0, 1, 19, 19, 19, 20) + +# Test rounding for dates with negative years +dt = Dates.DateTime(-1, 12, 29, 19, 19, 19, 19) +@test floor(dt, Dates.Year(2)) == DateTime(-2) +@test floor(dt, Dates.Month(2)) == DateTime(-1, 11) # Odd number; months are 1-indexed +@test floor(dt, Dates.Week(2)) == DateTime(-1, 12, 20) # 2 weeks prior to 0000-01-03 +@test floor(dt, Dates.Day(2)) == DateTime(-1, 12, 28) # Even; 4 days prior to 0000-01-01 +@test floor(dt, Dates.Hour(2)) == DateTime(-1, 12, 29, 18) +@test floor(dt, Dates.Minute(2)) == DateTime(-1, 12, 29, 19, 18) +@test floor(dt, Dates.Second(2)) == DateTime(-1, 12, 29, 19, 19, 18) +@test ceil(dt, Dates.Year(2)) == DateTime(0) +@test ceil(dt, Dates.Month(2)) == DateTime(0, 1) # Odd number; months are 1-indexed +@test ceil(dt, Dates.Week(2)) == DateTime(0, 1, 3) # First Monday of 0000 +@test ceil(dt, Dates.Day(2)) == DateTime(-1, 12, 30) # Even; 2 days prior to 0000-01-01 +@test ceil(dt, Dates.Hour(2)) == DateTime(-1, 12, 29, 20) +@test ceil(dt, Dates.Minute(2)) == DateTime(-1, 12, 29, 19, 20) +@test ceil(dt, Dates.Second(2)) == DateTime(-1, 12, 29, 19, 19, 20) + +# Test rounding for dates that should not need rounding +dt = Dates.DateTime(2016, 1, 1) +@test floor(dt, Dates.Year) == dt +@test floor(dt, Dates.Month) == dt +@test floor(dt, Dates.Day) == dt +@test floor(dt, Dates.Hour) == dt +@test floor(dt, Dates.Minute) == dt +@test floor(dt, Dates.Second) == dt +@test ceil(dt, Dates.Year) == dt +@test ceil(dt, Dates.Month) == dt +@test ceil(dt, Dates.Day) == dt +@test ceil(dt, Dates.Hour) == dt +@test ceil(dt, Dates.Minute) == dt +@test ceil(dt, Dates.Second) == dt + +dt = Dates.DateTime(-2016, 1, 1) +@test floor(dt, Dates.Year) == dt +@test floor(dt, Dates.Month) == dt +@test floor(dt, Dates.Day) == dt +@test floor(dt, Dates.Hour) == dt +@test floor(dt, Dates.Minute) == dt +@test floor(dt, Dates.Second) == dt +@test ceil(dt, Dates.Year) == dt +@test ceil(dt, Dates.Month) == dt +@test ceil(dt, Dates.Day) == dt +@test ceil(dt, Dates.Hour) == dt +@test ceil(dt, Dates.Minute) == dt +@test ceil(dt, Dates.Second) == dt + +# Test available RoundingModes +dt = Dates.DateTime(2016, 2, 28, 12) +@test round(dt, Dates.Day, RoundNearestTiesUp) == Dates.DateTime(2016, 2, 29) +@test round(dt, Dates.Day, RoundUp) == Dates.DateTime(2016, 2, 29) +@test round(dt, Dates.Day, RoundDown) == Dates.DateTime(2016, 2, 28) +@test_throws DomainError round(dt, Dates.Day, RoundNearest) +@test_throws DomainError round(dt, Dates.Day, RoundNearestTiesAway) +@test_throws DomainError round(dt, Dates.Day, RoundToZero) +@test round(dt, Dates.Day) == round(dt, Dates.Day, RoundNearestTiesUp) From 3890dca3cc76e818add76e3ad308ac1c5c0f8a6b Mon Sep 17 00:00:00 2001 From: Gem Newman Date: Mon, 20 Jun 2016 16:23:11 -0500 Subject: [PATCH 3/5] Added PR reference in NEWS.md. --- NEWS.md | 3 ++- doc/stdlib/strings.rst | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2384f00cd5f4f..d4143b0a6b066 100644 --- a/NEWS.md +++ b/NEWS.md @@ -207,7 +207,7 @@ Library improvements [Primes.jl package](https://github.com/JuliaMath/Primes.jl) ([#16481]). * `Date` and `DateTime` values can now be rounded to a specified resolution (e.g., 1 month or - 15 minutes) with `floor`, `ceil`, and `round` ([#PR]). + 15 minutes) with `floor`, `ceil`, and `round` ([#17037]). Deprecated or removed --------------------- @@ -300,3 +300,4 @@ Deprecated or removed [#16731]: https://github.com/JuliaLang/julia/issues/16731 [#16972]: https://github.com/JuliaLang/julia/issues/16972 [#17266]: https://github.com/JuliaLang/julia/issues/17266 +[#17037]: https://github.com/JuliaLang/julia/issues/17037 diff --git a/doc/stdlib/strings.rst b/doc/stdlib/strings.rst index 0817c94f0fa56..3b98d8a73b0b2 100644 --- a/doc/stdlib/strings.rst +++ b/doc/stdlib/strings.rst @@ -500,4 +500,3 @@ .. Docstring generated from Julia source Create a string from the address of a NUL-terminated UTF-32 string. A copy is made; the pointer can be safely freed. If ``length`` is specified, the string does not have to be NUL-terminated. - From ca7a277365551d891a6ea52b4427fcc3505e41cf Mon Sep 17 00:00:00 2001 From: Gem Newman Date: Tue, 21 Jun 2016 09:36:05 -0500 Subject: [PATCH 4/5] Bugfix for date rounding test in 32-bit. Doc clarifications. --- base/dates/rounding.jl | 4 ++-- doc/manual/dates.rst | 12 ++++++------ doc/stdlib/dates.rst | 6 +++--- doc/stdlib/strings.rst | 1 + test/dates/rounding.jl | 4 ++-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/base/dates/rounding.jl b/base/dates/rounding.jl index 6a5ce62ca3d53..1ffbe586b7a38 100644 --- a/base/dates/rounding.jl +++ b/base/dates/rounding.jl @@ -6,7 +6,7 @@ const DATETIMEEPOCH = value(DateTime(0)) const WEEKEPOCH = value(Date(0, 1, 3)) """ - epochdays2date(days) -> DateTime + epochdays2date(days) -> Date Takes the number of days since the rounding epoch (`0000-01-01T00:00:00`) and returns the corresponding `Date`. @@ -22,7 +22,7 @@ returns the corresponding `DateTime`. epochms2datetime(i) = DateTime(UTM(DATETIMEEPOCH + Int64(i))) """ - date2epochdays(dt::DateTime) -> Int64 + date2epochdays(dt::Date) -> Int64 Takes the given `Date` and returns the number of days since the rounding epoch (`0000-01-01T00:00:00`) as an `Int64`. diff --git a/doc/manual/dates.rst b/doc/manual/dates.rst index 253371b4d95a8..08b5ba148b228 100644 --- a/doc/manual/dates.rst +++ b/doc/manual/dates.rst @@ -450,12 +450,12 @@ Why round to the first day in July, even though it is month 7 (an odd number)? T that months are 1-indexed (the first month is assigned 1), unlike hours, minutes, seconds, and milliseconds (the first of which are assigned 0). -This means that rounding seconds, minutes, hours, or years (because the ISO 8601 -specification includes a year zero) to an even multiple will result in that field having -an even value, while rounding to an even multiple of months will result in that field -having an odd value. Because both months and years may contain an irregular number of -days, whether rounding to an even number of days will result in an even value in the days -field is uncertain. +This means that rounding a :class:`DateTime` to an even multiple of seconds, minutes, +hours, or years (because the ISO 8601 specification includes a year zero) will result in +a :class:`DateTime` with an even value in that field, while rounding a :class:`DateTime` +to an even multiple of months will result in the months field having an odd value. Because +both months and years may contain an irregular number of days, whether rounding to an even +number of days will result in an even value in the days field is uncertain. See the `API reference `_ for additional information on methods exported from the :mod:`Dates` module. diff --git a/doc/stdlib/dates.rst b/doc/stdlib/dates.rst index cd0698b66b917..1cc554b42b613 100644 --- a/doc/stdlib/dates.rst +++ b/doc/stdlib/dates.rst @@ -638,7 +638,7 @@ or 15 minutes) with ``floor``, ``ceil``, or ``round``. julia> round(DateTime(2016, 8, 6, 12, 0, 0), Dates.Day) 2016-08-07T00:00:00 - Valid RoundingModes for ``round(::TimeType, ::Period, ::RoundingMode)`` are ``RoundNearestTiesUp`` (default), ``RoundDown`` (``floor``\ ), and ``RoundUp`` (``ceil``\ ). + Valid rounding modes for ``round(::TimeType, ::Period, ::RoundingMode)`` are ``RoundNearestTiesUp`` (default), ``RoundDown`` (``floor``\ ), and ``RoundUp`` (``ceil``\ ). The following functions are not exported: @@ -648,7 +648,7 @@ The following functions are not exported: Simultaneously return the ``floor`` and ``ceil`` of a ``Date`` or ``DateTime`` at resolution ``p``\ . More efficient than calling both ``floor`` and ``ceil`` individually. -.. function:: epochdays2date(days) -> DateTime +.. function:: epochdays2date(days) -> Date .. Docstring generated from Julia source @@ -660,7 +660,7 @@ The following functions are not exported: Takes the number of milliseconds since the rounding epoch (``0000-01-01T00:00:00``\ ) and returns the corresponding ``DateTime``\ . -.. function:: date2epochdays(dt::DateTime) -> Int64 +.. function:: date2epochdays(dt::Date) -> Int64 .. Docstring generated from Julia source diff --git a/doc/stdlib/strings.rst b/doc/stdlib/strings.rst index 3b98d8a73b0b2..0817c94f0fa56 100644 --- a/doc/stdlib/strings.rst +++ b/doc/stdlib/strings.rst @@ -500,3 +500,4 @@ .. Docstring generated from Julia source Create a string from the address of a NUL-terminated UTF-32 string. A copy is made; the pointer can be safely freed. If ``length`` is specified, the string does not have to be NUL-terminated. + diff --git a/test/dates/rounding.jl b/test/dates/rounding.jl index 8a1e208457c50..4cf852deb314d 100644 --- a/test/dates/rounding.jl +++ b/test/dates/rounding.jl @@ -6,13 +6,13 @@ @test Dates.epochms2datetime(-86400000) == Dates.DateTime(-1, 12, 31) @test Dates.epochms2datetime(0) == Dates.DateTime(0, 1, 1) @test Dates.epochms2datetime(86400000) == Dates.DateTime(0, 1, 2) -@test Dates.epochms2datetime(736329 * 86400000) == Dates.DateTime(2016, 1, 1) +@test Dates.epochms2datetime(Int64(736329) * 86400000) == Dates.DateTime(2016, 1, 1) @test Dates.date2epochdays(Dates.Date(-1, 12, 31)) == -1 @test Dates.date2epochdays(Dates.Date(0, 1, 1)) == 0 @test Dates.date2epochdays(Dates.Date(2016, 1, 1)) == 736329 @test Dates.datetime2epochms(Dates.DateTime(-1, 12, 31)) == -86400000 @test Dates.datetime2epochms(Dates.DateTime(0, 1, 1)) == 0 -@test Dates.datetime2epochms(Dates.DateTime(2016, 1, 1)) == 736329 * 86400000 +@test Dates.datetime2epochms(Dates.DateTime(2016, 1, 1)) == Int64(736329) * 86400000 # Basic rounding tests dt = Dates.Date(2016, 2, 28) # Sunday From e67d9a196e79809edad04a792c10e411e16cf294 Mon Sep 17 00:00:00 2001 From: Gem Newman Date: Tue, 28 Jun 2016 09:36:42 -0500 Subject: [PATCH 5/5] Round to nonpositive resolution throws DomainError Throw DomainError on rounding to an invalid (non-positive) resolution Clean up test cases for rounding dates that don't need rounding Add test cases for rounding to invalid (non-positive) resolutions --- base/dates/rounding.jl | 5 +++++ test/dates/rounding.jl | 43 ++++++++++++++++-------------------------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/base/dates/rounding.jl b/base/dates/rounding.jl index 1ffbe586b7a38..e9a08c6fc13c2 100644 --- a/base/dates/rounding.jl +++ b/base/dates/rounding.jl @@ -38,11 +38,13 @@ Takes the given `DateTime` and returns the number of milliseconds since the roun datetime2epochms(dt::DateTime) = value(dt) - DATETIMEEPOCH function Base.floor(dt::Date, p::Year) + value(p) < 1 && throw(DomainError()) years = year(dt) return Date(years - mod(years, value(p))) end function Base.floor(dt::Date, p::Month) + value(p) < 1 && throw(DomainError()) y, m = yearmonth(dt) months_since_epoch = y * 12 + m - 1 month_offset = months_since_epoch - mod(months_since_epoch, value(p)) @@ -52,12 +54,14 @@ function Base.floor(dt::Date, p::Month) end function Base.floor(dt::Date, p::Week) + value(p) < 1 && throw(DomainError()) days = value(dt) - WEEKEPOCH days = days - mod(days, value(Day(p))) return Date(UTD(WEEKEPOCH + Int64(days))) end function Base.floor(dt::Date, p::Day) + value(p) < 1 && throw(DomainError()) days = date2epochdays(dt) return epochdays2date(days - mod(days, value(p))) end @@ -65,6 +69,7 @@ end Base.floor(dt::DateTime, p::DatePeriod) = DateTime(Base.floor(Date(dt), p)) function Base.floor(dt::DateTime, p::TimePeriod) + value(p) < 1 && throw(DomainError()) milliseconds = datetime2epochms(dt) return epochms2datetime(milliseconds - mod(milliseconds, value(Millisecond(p)))) end diff --git a/test/dates/rounding.jl b/test/dates/rounding.jl index 4cf852deb314d..80218fcf0db47 100644 --- a/test/dates/rounding.jl +++ b/test/dates/rounding.jl @@ -111,33 +111,12 @@ dt = Dates.DateTime(-1, 12, 29, 19, 19, 19, 19) @test ceil(dt, Dates.Second(2)) == DateTime(-1, 12, 29, 19, 19, 20) # Test rounding for dates that should not need rounding -dt = Dates.DateTime(2016, 1, 1) -@test floor(dt, Dates.Year) == dt -@test floor(dt, Dates.Month) == dt -@test floor(dt, Dates.Day) == dt -@test floor(dt, Dates.Hour) == dt -@test floor(dt, Dates.Minute) == dt -@test floor(dt, Dates.Second) == dt -@test ceil(dt, Dates.Year) == dt -@test ceil(dt, Dates.Month) == dt -@test ceil(dt, Dates.Day) == dt -@test ceil(dt, Dates.Hour) == dt -@test ceil(dt, Dates.Minute) == dt -@test ceil(dt, Dates.Second) == dt - -dt = Dates.DateTime(-2016, 1, 1) -@test floor(dt, Dates.Year) == dt -@test floor(dt, Dates.Month) == dt -@test floor(dt, Dates.Day) == dt -@test floor(dt, Dates.Hour) == dt -@test floor(dt, Dates.Minute) == dt -@test floor(dt, Dates.Second) == dt -@test ceil(dt, Dates.Year) == dt -@test ceil(dt, Dates.Month) == dt -@test ceil(dt, Dates.Day) == dt -@test ceil(dt, Dates.Hour) == dt -@test ceil(dt, Dates.Minute) == dt -@test ceil(dt, Dates.Second) == dt +for dt in [Dates.DateTime(2016, 1, 1), Dates.DateTime(-2016, 1, 1)] + for p in [Dates.Year, Dates.Month, Dates.Day, Dates.Hour, Dates.Minute, Dates.Second] + @test floor(dt, p) == dt + @test ceil(dt, p) == dt + end +end # Test available RoundingModes dt = Dates.DateTime(2016, 2, 28, 12) @@ -148,3 +127,13 @@ dt = Dates.DateTime(2016, 2, 28, 12) @test_throws DomainError round(dt, Dates.Day, RoundNearestTiesAway) @test_throws DomainError round(dt, Dates.Day, RoundToZero) @test round(dt, Dates.Day) == round(dt, Dates.Day, RoundNearestTiesUp) + +# Test rounding to invalid resolutions +dt = Dates.DateTime(2016, 2, 28, 12, 15) +for p in [Dates.Year, Dates.Month, Dates.Week, Dates.Day, Dates.Hour] + for v in [-1, 0] + @test_throws DomainError floor(dt, p(v)) + @test_throws DomainError ceil(dt, p(v)) + @test_throws DomainError round(dt, p(v)) + end +end