Skip to content

Commit

Permalink
Initial work to add a Time type to Base.Dates (#12274)
Browse files Browse the repository at this point in the history
* Add a Time type to the Base.Dates module
  • Loading branch information
quinnj authored Jan 24, 2017
1 parent 75302ae commit 06fa32c
Show file tree
Hide file tree
Showing 18 changed files with 467 additions and 94 deletions.
4 changes: 3 additions & 1 deletion base/dates/Dates.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ include("io.jl")

export Period, DatePeriod, TimePeriod,
Year, Month, Week, Day, Hour, Minute, Second, Millisecond,
TimeZone, UTC, TimeType, DateTime, Date,
Microsecond, Nanosecond,
TimeZone, UTC, TimeType, DateTime, Date, Time,
# periods.jl
canonicalize,
# accessors.jl
yearmonthday, yearmonth, monthday, year, month, week, day,
hour, minute, second, millisecond, dayofmonth,
microsecond, nanosecond,
# query.jl
dayofweek, isleapyear, daysinmonth, daysinyear, dayofyear, dayname, dayabbr,
dayofweekofmonth, daysofweekinmonth, monthname, monthabbr,
Expand Down
18 changes: 18 additions & 0 deletions base/dates/accessors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ end

# Accessor functions
value(dt::TimeType) = dt.instant.periods.value
value(t::Time) = t.instant.value
days(dt::Date) = value(dt)
days(dt::DateTime) = fld(value(dt),86400000)
year(dt::TimeType) = year(days(dt))
Expand All @@ -52,6 +53,12 @@ hour(dt::DateTime) = mod(fld(value(dt),3600000),24)
minute(dt::DateTime) = mod(fld(value(dt),60000),60)
second(dt::DateTime) = mod(fld(value(dt),1000),60)
millisecond(dt::DateTime) = mod(value(dt),1000)
hour(t::Time) = mod(fld(value(t),3600000000000),Int64(24))
minute(t::Time) = mod(fld(value(t),60000000000),Int64(60))
second(t::Time) = mod(fld(value(t),1000000000),Int64(60))
millisecond(t::Time) = mod(fld(value(t),Int64(1000000)),Int64(1000))
microsecond(t::Time) = mod(fld(value(t),Int64(1000)),Int64(1000))
nanosecond(t::Time) = mod(value(t),Int64(1000))

dayofmonth(dt::TimeType) = day(dt)

Expand Down Expand Up @@ -122,3 +129,14 @@ for parts in (["year", "month"], ["month", "day"], ["year", "month", "day"])
""" $func(dt::TimeType)
end
end

for func in (:hour, :minute, :second, :millisecond, :microsecond, :nanosecond)
name = string(func)
@eval begin
@doc """
$($name)(t::Time) -> Int64
The $($name) of a `Time` as an `Int64`.
""" $func(t::Time)
end
end
34 changes: 34 additions & 0 deletions base/dates/adjusters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ Base.trunc(dt::DateTime, p::Type{Minute}) = dt - Second(dt) - Millisecond(dt)
Base.trunc(dt::DateTime, p::Type{Second}) = dt - Millisecond(dt)
Base.trunc(dt::DateTime, p::Type{Millisecond}) = dt

Base.trunc(t::Time, p::Type{Hour}) = Time(Hour(t))
Base.trunc(t::Time, p::Type{Minute}) = Time(Hour(t), Minute(t))
Base.trunc(t::Time, p::Type{Second}) = Time(Hour(t), Minute(t), Second(t))
Base.trunc(t::Time, p::Type{Millisecond}) = t - Microsecond(t) - Nanosecond(t)
Base.trunc(t::Time, p::Type{Microsecond}) = t - Nanosecond(t)
Base.trunc(t::Time, p::Type{Nanosecond}) = t

"""
trunc(dt::TimeType, ::Type{Period}) -> TimeType
Expand Down Expand Up @@ -184,6 +191,33 @@ function DateTime(func::Function, y, m, d, h, mi, s; step::Period=Millisecond(1)
return adjust(DateFunction(func, negate, DateTime(y)), DateTime(y, m, d, h, mi, s), step, limit)
end

"""
Time(f::Function, h[, mi, s, ms, us]; step=Second(1), negate=false, limit=10000) -> Time
Create a `Time` through the adjuster API. The starting point will be constructed from the
provided `h, mi, s, ms, us` arguments, and will be adjusted until `f::Function` returns `true`. The step
size in adjusting can be provided manually through the `step` keyword. If `negate=true`,
then the adjusting will stop when `f::Function` returns `false` instead of `true`. `limit`
provides a limit to the max number of iterations the adjustment API will pursue before
throwing an error (in the case that `f::Function` is never satisfied). Note that the default step
will adjust to allow for greater precision for the given arguments; i.e. if hour, minute, and second
arguments are provided, the default step will be `Millisecond(1)` instead of `Second(1)`.
"""
Time(::Function, args...)

function Time(func::Function, h, mi=0; step::Period=Second(1), negate::Bool=false, limit::Int=10000)
return adjust(DateFunction(func, negate, Time(h, mi)), Time(h, mi), step, limit)
end
function Time(func::Function, h, mi, s; step::Period=Millisecond(1), negate::Bool=false, limit::Int=10000)
return adjust(DateFunction(func, negate, Time(h, mi, s)), Time(h, mi, s), step, limit)
end
function Time(func::Function, h, mi, s, ms; step::Period=Microsecond(1), negate::Bool=false, limit::Int=10000)
return adjust(DateFunction(func, negate,Time(h, mi, s, ms)),Time(h, mi, s, ms), step, limit)
end
function Time(func::Function, h, mi, s, ms, us; step::Period=Nanosecond(1), negate::Bool=false, limit::Int=10000)
return adjust(DateFunction(func, negate, Time(h, mi, s, ms, us)), Time(h, mi, s, ms, us), step, limit)
end

# Return the next TimeType that falls on dow
ISDAYOFWEEK = Dict(Mon => DateFunction(ismonday, false, Date(0)),
Tue => DateFunction(istuesday, false, Date(0)),
Expand Down
33 changes: 25 additions & 8 deletions base/dates/arithmetic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@
(+)(x::TimeType) = x
(-){T<:TimeType}(x::T,y::T) = x.instant - y.instant

# Date-Time arithmetic
"""
dt::Date + t::Time -> DateTime
The addition of a `Date` with a `Time` produces a `DateTime`. The hour, minute, second, and millisecond parts of
the `Time` are used along with the year, month, and day of the `Date` to create the new `DateTime`.
Non-zero microseconds or nanoseconds in the `Time` type will result in an `InexactError` being thrown.
"""
function (+)(dt::Date, t::Time)
(microsecond(t) > 0 || nanosecond(t) > 0) && throw(InexactError())
y, m, d = yearmonthday(dt)
return DateTime(y, m, d, hour(t), minute(t), second(t), millisecond(t))
end
(+)(t::Time, dt::Date) = dt + t

# TimeType-Year arithmetic
function (+)(dt::DateTime,y::Year)
oy,m,d = yearmonthday(dt); ny = oy+value(y); ld = daysinmonth(ny,m)
Expand Down Expand Up @@ -57,14 +72,16 @@ function (-)(dt::Date,z::Month)
mm = monthwrap(m,-value(z)); ld = daysinmonth(ny,mm)
return Date(ny,mm,d <= ld ? d : ld)
end
(+)(x::Date,y::Week) = return Date(UTD(value(x) + 7*value(y)))
(-)(x::Date,y::Week) = return Date(UTD(value(x) - 7*value(y)))
(+)(x::Date,y::Day) = return Date(UTD(value(x) + value(y)))
(-)(x::Date,y::Day) = return Date(UTD(value(x) - value(y)))
(+)(x::DateTime,y::Period) = return DateTime(UTM(value(x)+toms(y)))
(-)(x::DateTime,y::Period) = return DateTime(UTM(value(x)-toms(y)))
(+)(y::Period,x::TimeType) = x + y
(-)(y::Period,x::TimeType) = x - y
(+)(x::Date, y::Week) = return Date(UTD(value(x) + 7*value(y)))
(-)(x::Date, y::Week) = return Date(UTD(value(x) - 7*value(y)))
(+)(x::Date, y::Day) = return Date(UTD(value(x) + value(y)))
(-)(x::Date, y::Day) = return Date(UTD(value(x) - value(y)))
(+)(x::DateTime, y::Period) = return DateTime(UTM(value(x) + toms(y)))
(-)(x::DateTime, y::Period) = return DateTime(UTM(value(x) - toms(y)))
(+)(x::Time, y::TimePeriod) = return Time(Nanosecond(value(x) + tons(y)))
(-)(x::Time, y::TimePeriod) = return Time(Nanosecond(value(x) - tons(y)))
(+)(y::Period, x::TimeType) = x + y
(-)(y::Period, x::TimeType) = x - y

for op in (:+, :-)
@eval begin
Expand Down
14 changes: 12 additions & 2 deletions base/dates/conversions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@ the new `DateTime` are assumed to be zero.
"""
DateTime(dt::TimeType) = convert(DateTime,dt)

Base.convert(::Type{DateTime},dt::Date) = DateTime(UTM(value(dt)*86400000))
Base.convert(::Type{Date},dt::DateTime) = Date(UTD(days(dt)))
"""
Time(dt::DateTime) -> Time
Converts a `DateTime` to a `Time`. The hour, minute, second, and millisecond parts of
the `DateTime` are used to create the new `Time`. Microsecond and nanoseconds are zero by default.
"""
Time(dt::DateTime) = convert(Time, dt)

Base.convert(::Type{DateTime}, dt::Date) = DateTime(UTM(value(dt)*86400000))
Base.convert(::Type{Date}, dt::DateTime) = Date(UTD(days(dt)))
Base.convert(::Type{Time}, dt::DateTime) = Time(Nanosecond((value(dt) % 86400000) * 1000000))

"""
convert{T<:Real}(::Type{T}, dt::DateTime) -> T
Converts a DateTime value `dt` to a number of type `T`. The returned value corresponds to the number of Rata Die milliseconds since epoch.
Expand Down
47 changes: 31 additions & 16 deletions base/dates/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,41 @@

# TODO: optimize this
function Base.string(dt::DateTime)
y,m,d = yearmonthday(days(dt))
h,mi,s = hour(dt),minute(dt),second(dt)
yy = y < 0 ? @sprintf("%05i",y) : lpad(y,4,"0")
mm = lpad(m,2,"0")
dd = lpad(d,2,"0")
hh = lpad(h,2,"0")
mii = lpad(mi,2,"0")
ss = lpad(s,2,"0")
ms = millisecond(dt) == 0 ? "" : string(millisecond(dt)/1000.0)[2:end]
return "$yy-$mm-$(dd)T$hh:$mii:$ss$(ms)"
y, m, d = yearmonthday(days(dt))
h, mi, s = hour(dt), minute(dt), second(dt)
yy = y < 0 ? @sprintf("%05i", y) : lpad(y, 4, "0")
mm = lpad(m, 2, "0")
dd = lpad(d, 2, "0")
hh = lpad(h, 2, "0")
mii = lpad(mi, 2, "0")
ss = lpad(s, 2, "0")
ms = millisecond(dt) == 0 ? "" : string(millisecond(dt) / 1000.0)[2:end]
return "$yy-$mm-$(dd)T$hh:$mii:$ss$ms"
end
Base.show(io::IO,x::DateTime) = print(io,string(x))

Base.show(io::IO, x::DateTime) = print(io, string(x))

function Base.string(dt::Date)
y,m,d = yearmonthday(value(dt))
yy = y < 0 ? @sprintf("%05i",y) : lpad(y,4,"0")
mm = lpad(m,2,"0")
dd = lpad(d,2,"0")
y, m, d = yearmonthday(value(dt))
yy = y < 0 ? @sprintf("%05i", y) : lpad(y, 4, "0")
mm = lpad(m, 2, "0")
dd = lpad(d, 2, "0")
return "$yy-$mm-$dd"
end
Base.show(io::IO,x::Date) = print(io,string(x))

Base.show(io::IO, x::Date) = print(io, string(x))

function Base.string(t::Time)
h, mi, s = hour(t), minute(t), second(t)
hh = lpad(h, 2, "0")
mii = lpad(mi, 2, "0")
ss = lpad(s, 2, "0")
nss = tons(Millisecond(t)) + tons(Microsecond(t)) + tons(Nanosecond(t))
ns = nss == 0 ? "" : rstrip(@sprintf("%.9f", nss / 1e+9)[2:end], '0')
return "$hh:$mii:$ss$ns"
end

Base.show(io::IO, x::Time) = print(io, string(x))

### Parsing
const english = Dict{String,Int}("january"=>1,"february"=>2,"march"=>3,"april"=>4,
Expand Down
53 changes: 38 additions & 15 deletions base/dates/periods.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ value(x::Period) = x.value
# The default constructors for Periods work well in almost all cases
# P(x) = new((convert(Int64,x))
# The following definitions are for Period-specific safety
for period in (:Year, :Month, :Week, :Day, :Hour, :Minute, :Second, :Millisecond)
for period in (:Year, :Month, :Week, :Day, :Hour, :Minute, :Second, :Millisecond, :Microsecond, :Nanosecond)
period_str = string(period)
accessor_str = lowercase(period_str)
# Convenience method for show()
Expand All @@ -16,17 +16,19 @@ for period in (:Year, :Month, :Week, :Day, :Hour, :Minute, :Second, :Millisecond
# AbstractString parsing (mainly for IO code)
@eval $period(x::AbstractString) = $period(Base.parse(Int64,x))
# Period accessors
typ_str = period in (:Hour, :Minute, :Second, :Millisecond) ? "DateTime" : "TimeType"
description = typ_str == "TimeType" ? "`Date` or `DateTime`" : "`$typ_str`"
reference = period == :Week ? " For details see [`$accessor_str(::$typ_str)`](@ref)." : ""
typs = period in (:Microsecond, :Nanosecond) ? ["Time"] :
period in (:Hour, :Minute, :Second, :Millisecond) ? ["Time", "DateTime"] : ["Date","DateTime"]
reference = period == :Week ? " For details see [`$accessor_str(::Union{Date, DateTime})`](@ref)." : ""
for typ_str in typs
@eval begin
@doc """
$($period_str)(dt::$($typ_str)) -> $($period_str)
The $($accessor_str) part of a $($typ_str) as a `$($period_str)`.$($reference)
""" $period(dt::$(Symbol(typ_str))) = $period($(Symbol(accessor_str))(dt))
end
end
@eval begin
@doc """
$($period_str)(dt::$($typ_str)) -> $($period_str)
The $($accessor_str) part of a $($description) as a `$($period_str)`.$($reference)
""" ->
$period(dt::$(Symbol(typ_str))) = $period($(Symbol(accessor_str))(dt))

@doc """
$($period_str)(v)
Expand Down Expand Up @@ -117,15 +119,28 @@ periodisless(::Period,::Hour) = false
periodisless(::Minute,::Hour) = true
periodisless(::Second,::Hour) = true
periodisless(::Millisecond,::Hour) = true
periodisless(::Microsecond,::Hour) = true
periodisless(::Nanosecond,::Hour) = true
periodisless(::Period,::Minute) = false
periodisless(::Second,::Minute) = true
periodisless(::Millisecond,::Minute) = true
periodisless(::Microsecond,::Minute) = true
periodisless(::Nanosecond,::Minute) = true
periodisless(::Period,::Second) = false
periodisless(::Millisecond,::Second) = true
periodisless(::Microsecond,::Second) = true
periodisless(::Nanosecond,::Second) = true
periodisless(::Period,::Millisecond) = false
periodisless(::Microsecond,::Millisecond) = true
periodisless(::Nanosecond,::Millisecond) = true
periodisless(::Period,::Microsecond) = false
periodisless(::Nanosecond,::Microsecond) = true
periodisless(::Period,::Nanosecond) = false

# return (next coarser period, conversion factor):
coarserperiod{P<:Period}(::Type{P}) = (P,1)
coarserperiod(::Type{Nanosecond}) = (Microsecond,1000)
coarserperiod(::Type{Microsecond}) = (Millisecond,1000)
coarserperiod(::Type{Millisecond}) = (Second,1000)
coarserperiod(::Type{Second}) = (Minute,60)
coarserperiod(::Type{Minute}) = (Hour,60)
Expand Down Expand Up @@ -202,6 +217,9 @@ julia> Dates.CompoundPeriod(Dates.Minute(50000))
"""
CompoundPeriod{P<:Period}(p::Vector{P}) = CompoundPeriod(Array{Period}(p))

CompoundPeriod(t::Time) = CompoundPeriod(Period[Hour(t), Minute(t), Second(t), Millisecond(t),
Microsecond(t), Nanosecond(t)])

CompoundPeriod(p::Period...) = CompoundPeriod(Period[p...])


Expand Down Expand Up @@ -382,7 +400,7 @@ end

# Fixed-value Periods (periods corresponding to a well-defined time interval,
# as opposed to variable calendar intervals like Year).
typealias FixedPeriod Union{Week,Day,Hour,Minute,Second,Millisecond}
typealias FixedPeriod Union{Week,Day,Hour,Minute,Second,Millisecond,Microsecond,Nanosecond}

# like div but throw an error if remainder is nonzero
function divexact(x,y)
Expand All @@ -392,10 +410,10 @@ function divexact(x,y)
end

# FixedPeriod conversions and promotion rules
const fixedperiod_conversions = [(Week,7),(Day,24),(Hour,60),(Minute,60),(Second,1000),(Millisecond,1)]
const fixedperiod_conversions = [(Week,7),(Day,24),(Hour,60),(Minute,60),(Second,1000),(Millisecond,1000),(Microsecond,1000),(Nanosecond,1)]
for i = 1:length(fixedperiod_conversions)
(T,n) = fixedperiod_conversions[i]
N = 1
N = Int64(1)
for j = i-1:-1:1 # less-precise periods
(Tc,nc) = fixedperiod_conversions[j]
N *= nc
Expand Down Expand Up @@ -432,6 +450,8 @@ Base.promote_rule(::Type{Year}, ::Type{Month}) = Month
Base.isless{T<:OtherPeriod,S<:OtherPeriod}(x::T,y::S) = isless(promote(x,y)...)

# truncating conversions to milliseconds and days:
toms(c::Nanosecond) = div(value(c), 1000000)
toms(c::Microsecond) = div(value(c), 1000)
toms(c::Millisecond) = value(c)
toms(c::Second) = 1000*value(c)
toms(c::Minute) = 60000*value(c)
Expand All @@ -440,7 +460,10 @@ toms(c::Day) = 86400000*value(c)
toms(c::Week) = 604800000*value(c)
toms(c::Month) = 86400000.0*30.436875*value(c)
toms(c::Year) = 86400000.0*365.2425*value(c)
toms(c::CompoundPeriod) = isempty(c.periods)?0.0 : Float64(sum(toms,c.periods))
toms(c::CompoundPeriod) = isempty(c.periods)? 0.0 : Float64(sum(toms, c.periods))
tons(x) = toms(x) * 1000000
tons(x::Microsecond) = value(x) * 1000
tons(x::Nanosecond) = value(x)
days(c::Millisecond) = div(value(c),86400000)
days(c::Second) = div(value(c),86400)
days(c::Minute) = div(value(c),1440)
Expand Down
6 changes: 4 additions & 2 deletions base/dates/ranges.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

# Override default step; otherwise it would be Millisecond(1)
Base.colon{T<:DateTime}(start::T, stop::T) = StepRange(start, Day(1), stop)
Base.colon{T<:Time}(start::T, stop::T) = StepRange(start, Second(1), stop)

# Given a start and end date, how many steps/periods are in between
guess(a::DateTime,b::DateTime,c) = floor(Int64,(Int128(b) - Int128(a))/toms(c))
guess(a::Date,b::Date,c) = Int64(div(Int64(b - a),days(c)))
guess(a::DateTime,b::DateTime,c) = floor(Int64,(Int128(b) - Int128(a)) / toms(c))
guess(a::Date,b::Date,c) = Int64(div(Int64(b - a), days(c)))
len(a::Time,b::Time,c) = Int64(div(Int64(b - a), tons(c)))
function len(a,b,c)
lo, hi, st = min(a,b), max(a,b), abs(c)
i = guess(a,b,c)-1
Expand Down
Loading

2 comments on commit 06fa32c

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

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

Executing the daily benchmark build, I will reply here when finished:

@nanosoldier runbenchmarks(ALL, isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

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

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @jrevels

Please sign in to comment.