Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Dates.FixedPeriod and Dates.CompoundPeriod #331

Merged
merged 13 commits into from
Feb 26, 2021
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = "1.2.1"

[deps]
ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

Expand Down
1 change: 1 addition & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Expand Down
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Documenter, Unitful
using Documenter, Unitful, Dates

DocMeta.setdocmeta!(Unitful, :DocTestSetup, :(using Unitful))

Expand All @@ -17,6 +17,7 @@ makedocs(
"How units are displayed" => "display.md"
"Logarithmic scales" => "logarithm.md"
"Temperature scales" => "temperature.md"
"Interoperability with `Dates`" => "dates.md"
"Extending Unitful" => "extending.md"
"Troubleshooting" => "trouble.md"
"License" => "LICENSE.md"
Expand Down
133 changes: 133 additions & 0 deletions docs/src/dates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
```@meta
DocTestSetup = quote
using Unitful
end
```
# Interoperability with the `Dates` standard library

[Julia's `Dates` standard library](https://docs.julialang.org/en/v1/stdlib/Dates/) provides data types for representing specific points in time `Date`/`DateTime` and differences between them, i.e., periods. Unitful provides methods for using period types from the `Dates` standard library together with `Quantity`s.

## Support for `Dates.FixedPeriod`s

The `Dates.FixedPeriod` union type includes all `Dates.Period`s that represent a fixed period of time, i.e., `Dates.Week`, `Dates.Day`, `Dates.Hour`, `Dates.Minute`, `Dates.Second`, `Dates.Millisecond`, `Dates.Microsecond`, and `Dates.Nanosecond`. These types can be converted to `Quantity`s or used in place of them.

!!! note
`Dates.Year` does not represent a fixed period and cannot be converted to a `Quantity`. While Unitful's `yr` unit is exactly equal to 365.25 days, a `Dates.Year` may contain 365 or 366 days.

Each `FixedPeriod` is considered equivalent to a `Quantity`. For example, `Dates.Millisecond(5)` corresponds to the quantity `Int64(5)*u"ms"`. A `FixedPeriod` can be converted to the equivalent `Quantity` with a constructor:

```@docs
Unitful.Quantity(::Dates.FixedPeriod)
```

In most respects, `FixedPeriod`s behave like their equivalent quantities. They can be converted to other units using `uconvert`, used in arithmetic operations with other quantities, and they have a `unit` and `dimension`:

```jldoctest
julia> using Dates: Hour

julia> p = Hour(3)
3 hours

julia> uconvert(u"s", p)
10800 s

julia> p == 180u"minute"
true

julia> p < 1u"d"
true

julia> 5u"s" + p
10805 s

julia> 210u"km" / p
70.0 km hr^-1

julia> unit(p) === u"hr"
true

julia> dimension(p)
𝐓
```

Conversely, a `FixedPeriod` can be created from a quantity using the appropriate constructor, `convert`, or `round` methods. This will fail (i.e., throw an `InexactError`) if the resulting value cannot be represented as an `Int64`:

```jldoctest
julia> using Dates: Day, Hour, Millisecond

julia> Millisecond(1.5u"s")
1500 milliseconds

julia> convert(Hour, 1u"yr")
8766 hours

julia> Day(1u"yr")
ERROR: InexactError: Int64(1461//4)
[...]

julia> round(Day, 1u"yr")
365 days
```

## Support for `Dates.CompoundPeriod`s

The `Dates` standard library provides the `Dates.CompoundPeriod` type to represent sums of periods of different types:

```@repl
using Dates: Day, Second
Day(5) + Second(1)
typeof(ans)
```

Unitful provides facilities to work with `CompoundPeriod`s as long as they consist only of `FixedPeriod`s. Such `CompoundPeriod`s can be converted to `Quantity`s using `convert`, `uconvert`, or `round`:

```@jldoctest
julia> using Dates: Day, Second

julia> p = Day(5) + Second(1)
5 days, 1 second

julia> uconvert(u"s", p)
432001//1 s

julia> convert(typeof(1.0u"yr"), p)
0.01368928562374832 yr

julia> round(u"d", p)
5//1 d

julia> q = Month(1) + Day(1) # Month is not a fixed period
1 month, 1 day

julia> uconvert(u"s", q)
ERROR: MethodError: no method matching Quantity{Rational{Int64},𝐓,Unitful.FreeUnits{(s,),𝐓,nothing}}(::Month)
[...]
```

However, not all operations that are defined for `FixedPeriod`s support `CompoundPeriod`s as well.
The reason for that is that a `CompoundPeriod` does not correspond to a specific unit:

```@jldoctest
julia> p = Day(365) + Hour(6)
365 days, 6 hours

julia> unit(p) # A CompoundPeriod does not have a corresponding unit ...
ERROR: MethodError: no method matching unit(::Dates.CompoundPeriod)
[...]

julia> dimension(p) # ... but it does have a dimension
𝐓

julia> Quantity(p) # As a result, there is no Quantity type associated with it ...
ERROR: MethodError: no method matching Quantity(::Int64)
[...]

julia> T = typeof(1.0u"hr"); T(p) # ... but it can be converted to a concrete time quantity
8766.0 hr
```

Consequently, any operation whose result would depend on the input unit is not supported by `CompoundPeriod`s. For example:

* `+(::Quantity, ::CompoundPeriod)` and `+(::CompoundPeriod, ::Quantity)` error, since the unit of the result depends on the units of both arguments.
* `div(::Quantity, ::CompoundPeriod)` and `div(::CompoundPeriod, ::Quantity)` work, since the result is a dimensionless number.
* `mod(::CompoundPeriod, ::Quantity)` works, but `mod(::Quantity, ::CompoundPeriod)` does not, since the second argument determines the unit of the returned quantity.
2 changes: 2 additions & 0 deletions src/Unitful.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Base: getindex, eltype, step, last, first, frexp
import Base: Integer, Rational, typemin, typemax
import Base: steprange_last, unsigned

import Dates
import LinearAlgebra: Diagonal, Bidiagonal, Tridiagonal, SymTridiagonal
import LinearAlgebra: istril, istriu, norm
import Random
Expand Down Expand Up @@ -64,5 +65,6 @@ include("fastmath.jl")
include("logarithm.jl")
include("complex.jl")
include("pkgdefaults.jl")
include("dates.jl")

end
188 changes: 188 additions & 0 deletions src/dates.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Conversion from and to types from the `Dates` stdlib

# Dates.FixedPeriod

for (period, unit) = ((Dates.Week, wk), (Dates.Day, d), (Dates.Hour, hr),
(Dates.Minute, minute), (Dates.Second, s), (Dates.Millisecond, ms),
(Dates.Microsecond, μs), (Dates.Nanosecond, ns))
@eval unit(::Type{$period}) = $unit
@eval (::Type{$period})(x::AbstractQuantity) = $period(ustrip(unit($period), x))
end

dimension(p::Dates.FixedPeriod) = dimension(typeof(p))
dimension(::Type{<:Dates.FixedPeriod}) = 𝐓

"""
unit(x::Dates.FixedPeriod)
unit(x::Type{<:Dates.FixedPeriod})

Return the units that correspond to a particular period.

# Examples

```julia
julia> unit(Second(15)) == u"s"
true

julia> unit(Hour) == u"hr"
true
```
"""
unit(p::Dates.FixedPeriod) = unit(typeof(p))

numtype(x::Dates.FixedPeriod) = numtype(typeof(x))
numtype(::Type{T}) where {T<:Dates.FixedPeriod} = Int64

quantitytype(::Type{T}) where {T<:Dates.FixedPeriod} =
Quantity{numtype(T),dimension(T),typeof(unit(T))}

ustrip(p::Dates.FixedPeriod) = Dates.value(p)

"""
Quantity(period::Dates.FixedPeriod)

Create a `Quantity` that corresponds to the given `period`. The numerical value of the
resulting `Quantity` is of type `Int64`.

# Example

```jldoctest
julia> using Dates: Second

julia> Quantity(Second(5))
5 s
```
"""
Quantity(period::Dates.FixedPeriod) = Quantity(ustrip(period), unit(period))

uconvert(u::Units, period::Dates.FixedPeriod) = uconvert(u, Quantity(period))

(T::Type{<:AbstractQuantity})(period::Dates.FixedPeriod) = T(Quantity(period))

convert(T::Type{<:AbstractQuantity}, period::Dates.FixedPeriod) = T(period)
convert(T::Type{<:Dates.FixedPeriod}, x::AbstractQuantity) = T(x)

round(T::Type{<:Dates.FixedPeriod}, x::AbstractQuantity, r::RoundingMode=RoundNearest) =
T(round(numtype(T), ustrip(unit(T), x), r))
round(u::Units, period::Dates.FixedPeriod, r::RoundingMode=RoundNearest; kwargs...) =
round(u, Quantity(period), r; kwargs...)
round(T::Type{<:Number}, u::Units, period::Dates.FixedPeriod, r::RoundingMode=RoundNearest;
kwargs...) = round(T, u, Quantity(period), r; kwargs...)
round(T::Type{<:AbstractQuantity}, period::Dates.FixedPeriod, r::RoundingMode=RoundNearest;
kwargs...) = round(T, Quantity(period), r; kwargs...)

for (f, r) in ((:floor,:RoundDown), (:ceil,:RoundUp), (:trunc,:RoundToZero))
@eval $f(T::Type{<:Dates.FixedPeriod}, x::AbstractQuantity) = round(T, x, $r)
@eval $f(u::Units, period::Dates.FixedPeriod; kwargs...) =
round(u, period, $r; kwargs...)
@eval $f(T::Type{<:Number}, u::Units, period::Dates.FixedPeriod; kwargs...) =
round(T, u, period, $r; kwargs...)
@eval $f(T::Type{<:AbstractQuantity}, period::Dates.FixedPeriod; kwargs...) =
round(T, period, $r; kwargs...)
end

for op = (:+, :-, :*, :/, ://, :fld, :cld, :mod, :rem, :atan,
:(==), :isequal, :<, :isless, :≤)
@eval $op(x::Dates.FixedPeriod, y::AbstractQuantity) = $op(Quantity(x), y)
@eval $op(x::AbstractQuantity, y::Dates.FixedPeriod) = $op(x, Quantity(y))
end
for op = (:*, :/, ://)
@eval $op(x::Dates.FixedPeriod, y::Units) = $op(Quantity(x), y)
@eval $op(x::Units, y::Dates.FixedPeriod) = $op(x, Quantity(y))
end
div(x::Dates.FixedPeriod, y::AbstractQuantity, r...) = div(Quantity(x), y, r...)
div(x::AbstractQuantity, y::Dates.FixedPeriod, r...) = div(x, Quantity(y), r...)

isapprox(x::Dates.FixedPeriod, y::AbstractQuantity; kwargs...) =
isapprox(Quantity(x), y; kwargs...)
isapprox(x::AbstractQuantity, y::Dates.FixedPeriod; kwargs...) =
isapprox(x, Quantity(y); kwargs...)

function isapprox(x::AbstractArray{<:AbstractQuantity}, y::AbstractArray{T};
kwargs...) where {T<:Dates.Period}
if isconcretetype(T)
y′ = reinterpret(quantitytype(T), y)
else
y′ = Quantity.(y)
end
isapprox(x, y′; kwargs...)
end
isapprox(x::AbstractArray{<:Dates.FixedPeriod}, y::AbstractArray{<:AbstractQuantity};
kwargs...) = isapprox(y, x; kwargs...)

Base.promote_rule(::Type{Quantity{T,𝐓,U}}, ::Type{S}) where {T,U,S<:Dates.FixedPeriod} =
promote_type(Quantity{T,𝐓,U}, quantitytype(S))

# Dates.CompoundPeriod

dimension(p::Dates.CompoundPeriod) = dimension(typeof(p))
dimension(::Type{<:Dates.CompoundPeriod}) = 𝐓

uconvert(u::Units, period::Dates.CompoundPeriod) =
Quantity{promote_type(Int64,typeof(convfact(u,ns))),dimension(u),typeof(u)}(period)

try_uconvert(u::Units, period::Dates.CompoundPeriod) = nothing
function try_uconvert(u::TimeUnits, period::Dates.CompoundPeriod)
T = Quantity{promote_type(Int64,typeof(convfact(u,ns))),dimension(u),typeof(u)}
val = zero(T)
for p in period.periods
p isa Dates.FixedPeriod || return nothing
val += T(p)
end
val
end

(T::Type{<:AbstractQuantity})(period::Dates.CompoundPeriod) =
mapreduce(T, +, period.periods, init=zero(T))

convert(T::Type{<:AbstractQuantity}, period::Dates.CompoundPeriod) = T(period)

round(u::Units, period::Dates.CompoundPeriod, r::RoundingMode=RoundNearest; kwargs...) =
round(u, uconvert(u, period), r; kwargs...)
round(T::Type{<:Number}, u::Units, period::Dates.CompoundPeriod,
r::RoundingMode=RoundNearest; kwargs...) =
round(T, u, uconvert(u, period), r; kwargs...)
round(T::Type{<:AbstractQuantity}, period::Dates.CompoundPeriod,
r::RoundingMode=RoundNearest; kwargs...) =
round(T, T(period), r; kwargs...)

for (f, r) in ((:floor,:RoundDown), (:ceil,:RoundUp), (:trunc,:RoundToZero))
@eval $f(u::Units, period::Dates.CompoundPeriod; kwargs...) =
round(u, period, $r; kwargs...)
@eval $f(T::Type{<:Number}, u::Units, period::Dates.CompoundPeriod; kwargs...) =
round(T, u, period, $r; kwargs...)
@eval $f(T::Type{<:AbstractQuantity}, period::Dates.CompoundPeriod; kwargs...) =
round(T, period, $r; kwargs...)
end

for op = (:fld, :cld, :atan, :<, :isless, :≤)
@eval $op(x::Dates.CompoundPeriod, y::AbstractQuantity) = $op(uconvert(unit(y),x), y)
@eval $op(x::AbstractQuantity, y::Dates.CompoundPeriod) = $op(x, uconvert(unit(x),y))
end
div(x::Dates.CompoundPeriod, y::AbstractQuantity, r...) = div(uconvert(unit(y),x), y, r...)
div(x::AbstractQuantity, y::Dates.CompoundPeriod, r...) = div(x, uconvert(unit(x),y), r...)
mod(x::Dates.CompoundPeriod, y::AbstractQuantity) = mod(uconvert(unit(y),x), y)
rem(x::Dates.CompoundPeriod, y::AbstractQuantity) = rem(uconvert(unit(y),x), y)
for op = (:(==), :isequal)
@eval $op(x::Dates.CompoundPeriod, y::AbstractQuantity{T,𝐓,U}) where {T,U} =
$op(try_uconvert(U(), x), y)
@eval $op(x::AbstractQuantity{T,𝐓,U}, y::Dates.CompoundPeriod) where {T,U} =
$op(x, try_uconvert(U(), y))
end

isapprox(x::Dates.CompoundPeriod, y::AbstractQuantity; kwargs...) =
dimension(y) === 𝐓 ? isapprox(uconvert(unit(y), x), y; kwargs...) : false
isapprox(x::AbstractQuantity, y::Dates.CompoundPeriod; kwargs...) =
dimension(x) === 𝐓 ? isapprox(x, uconvert(unit(x), y); kwargs...) : false

function isapprox(x::AbstractArray{<:AbstractQuantity},
y::AbstractArray{Dates.CompoundPeriod}; kwargs...)
if dimension(eltype(x)) === 𝐓
isapprox(x, uconvert.(unit(eltype(x)), y); kwargs...)
else
false
end
end

isapprox(x::AbstractArray{Dates.CompoundPeriod}, y::AbstractArray{<:AbstractQuantity};
kwargs...) = isapprox(y, x; kwargs...)
Loading