Skip to content

Commit

Permalink
Add support for Dates.FixedPeriod and Dates.CompoundPeriod (#331)
Browse files Browse the repository at this point in the history
* Add support for Dates.FixedPeriod

* Fix tests on Julia < 1.2

* Fix more tests

* Delete unused method

* Add some more tests

* Fix tests on 32-bit master

* Add three-argument div

* Add support for CompoundPeriod

* Fix tests on 32bit

* Add Quantity(::FixedPeriod) docstring

* Fix some tests

* Fix a test

* Add documentation page
  • Loading branch information
sostock committed Feb 26, 2021
1 parent f673d2d commit d4e15b5
Show file tree
Hide file tree
Showing 8 changed files with 1,121 additions and 1 deletion.
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.6.0"

[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 @@ -20,6 +20,7 @@ import Base: steprange_last, unsigned
import Base: isunordered
end

import Dates
import LinearAlgebra: Diagonal, Bidiagonal, Tridiagonal, SymTridiagonal
import LinearAlgebra: istril, istriu, norm
import Random
Expand Down Expand Up @@ -67,5 +68,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

0 comments on commit d4e15b5

Please sign in to comment.