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 rounding operations for ZonedDateTimes #29

Merged
merged 4 commits into from
Jul 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/rounding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
## Rounding a ZonedDateTime

(Only supported in Julia version 0.5 and later.)

Rounding operations (`floor`, `ceil`, and `round`) on `ZonedDateTime`s are performed in a
[similar manner to `DateTime`](http://julia.readthedocs.org/en/latest/manual/dates/#rounding)
and should generally behave as expected. When `VariableTimeZone` transitions are involved,
however, unexpected behaviour may be encountered.

Instead of performing rounding operations on a UTC representation of the `ZonedDateTime`,
which would in some cases be computationally less expensive, rounding is done in the local
time zone. This ensures that rounding behaves as expected and is maximally meaningful.

If rounding were done in UTC, consider how rounding to the nearest day would be resolved for
non-UTC time zones: the result would be 00:00 UTC, which wouldn't be midnight local time.
Similarly, when rounding to the nearest hour in `Australia/Eucla (UTC+08:45)`, the result
wouldn't be on the hour in the local time zone.

### Rounding to a TimePeriod

When the target resolution is a `TimePeriod` the likelihood of encountering an ambiguous or
non-existent time (due to daylight saving time transitions) is increased. To resolve this
issue, rounding a `ZonedDateTime` with a `VariableTimeZone` to a `TimePeriod` uses the
`DateTime` value in the appropriate `FixedTimeZone`, then reconverts it to a `ZonedDateTime`
in the appropriate `VariableTimeZone` afterward. (See [Examples](#examples) below.)

### Rounding to a DatePeriod

When the target resolution is a `DatePeriod` rounding is done in the local time zone in a
straightforward fashion.

Rounding is not an entirely "safe" operation for `ZonedDateTime`s, as in some cases
historical transitions for some time zones (`Asia/Colombo`, for example) occur at midnight.
In such cases rounding to a `DatePeriod` may still result in an `AmbiguousTimeError` or a
`NonExistentTimeError`s. (But such occurrences should be relatively rare.)

Regular daylight saving time transitions should be safe.

### Examples

The `America/Winnipeg` time zone transitioned from Central Standard Time (UTC-6:00) to
Central Daylight Time (UTC-5:00) on 2016-03-13, moving directly from 01:59:59 to 03:00:00.

```julia
julia> zdt = ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg"))
2016-03-13T01:45:00-06:00

julia> floor(zdt, Dates.Day)
2016-03-13T00:00:00-06:00

julia> ceil(zdt, Dates.Day)
2016-03-14T00:00:00-05:00

julia> round(zdt, Dates.Day)
2016-03-13T00:00:00-06:00

julia> floor(zdt, Dates.Hour)
2016-03-13T01:00:00-06:00

julia> ceil(zdt, Dates.Hour)
2016-03-13T03:00:00-05:00

julia> round(zdt, Dates.Hour)
2016-03-13T03:00:00-05:00
Copy link
Member

Choose a reason for hiding this comment

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

Maybe start the example with zdt = ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")) and use zdt in all the examples

```

The `Asia/Colombo` time zone revised the definition of Lanka Time from UTC+6:30 to UTC+6:00
on 1996-10-26, moving from 00:29:59 back to 00:00:00.

```julia
julia> zdt = ZonedDateTime(1996, 10, 25, 23, 45, TimeZone("Asia/Colombo"))
1996-10-25T23:45:00+06:30

julia> round(zdt, Dates.Hour)
1996-10-26T00:00:00+06:30

julia> round(zdt, Dates.Day)
ERROR: Local DateTime 1996-10-26T00:00:00 is ambiguious
```
3 changes: 2 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ pages:
- Types: types.md
- Converting: conversions.md
- Arithmetic: arithmetic.md
- Rounding: rounding.md
- Current Time: current.md
- Frequently Asked Questions: faq.md
- Frequently Asked Questions: faq.md
1 change: 1 addition & 0 deletions src/TimeZones.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ include(joinpath("timezones", "conversions.jl"))
include(joinpath("timezones", "local.jl"))
include(joinpath("timezones", "ranges.jl"))
include(joinpath("timezones", "discovery.jl"))
VERSION >= v"0.5.0-dev+5244" && include(joinpath("timezones", "rounding.jl"))

"""
TimeZone(name::AbstractString) -> TimeZone
Expand Down
149 changes: 149 additions & 0 deletions src/timezones/rounding.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
function Base.floor(zdt::ZonedDateTime, p::Dates.DatePeriod)
return ZonedDateTime(floor(localtime(zdt), p), zdt.timezone)
end

function Base.floor(zdt::ZonedDateTime, p::Dates.TimePeriod)
# Rounding is done using the current fixed offset to avoid transitional ambiguities.
dt = floor(localtime(zdt), p)
utc_dt = dt - zdt.zone.offset
return ZonedDateTime(utc_dt, zdt.timezone; from_utc=true)
end

function Base.ceil(zdt::ZonedDateTime, p::Dates.DatePeriod)
return ZonedDateTime(ceil(localtime(zdt), p), zdt.timezone)
end

#function Base.Dates.floorceil(zdt::ZonedDateTime, p::Dates.DatePeriod)
#return floor(zdt, p), ceil(zdt, p)
#end

"""
floor(zdt::ZonedDateTime, p::Period) -> ZonedDateTime
Copy link
Member

Choose a reason for hiding this comment

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

Add type signature as well:

    floor(zdt::ZonedDateTime, p::Period) -> ZonedDateTime
    floor(zdt::ZonedDateTime, p::Type{Period}) -> ZonedDateTime

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. I wasn't aware multiple signatures were supported.

Copy link
Member

Choose a reason for hiding this comment

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

Multiple signatures are supported. When working with Base you just need to make sure that the signatures you supply in the docstring match that of the manual.

floor(zdt::ZonedDateTime, p::Type{Period}) -> ZonedDateTime

Returns the nearest `ZonedDateTime` less than or equal to `zdt` at resolution `p`. The
result will be in the same time zone as `zdt`.

For convenience, `p` may be a type instead of a value: `floor(zdt, Dates.Hour)` is a
shortcut for `floor(zdt, Dates.Hour(1))`.

`VariableTimeZone` transitions are handled as for `round`.

### Examples

The `America/Winnipeg` time zone transitioned from Central Standard Time (UTC-6:00) to
Central Daylight Time (UTC-5:00) on 2016-03-13, moving directly from 01:59:59 to 03:00:00.

```julia
julia> zdt = ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg"))
2016-03-13T01:45:00-06:00

julia> floor(zdt, Dates.Day)
2016-03-13T00:00:00-06:00

julia> floor(zdt, Dates.Hour)
2016-03-13T01:00:00-06:00
```
"""
Base.floor(::TimeZones.ZonedDateTime, ::Union{Dates.Period, Type{Dates.Period}})

"""
ceil(zdt::ZonedDateTime, p::Period) -> ZonedDateTime
ceil(zdt::ZonedDateTime, p::Type{Period}) -> ZonedDateTime

Returns the nearest `ZonedDateTime` greater than or equal to `zdt` at resolution `p`.
The result will be in the same time zone as `zdt`.

For convenience, `p` may be a type instead of a value: `ceil(zdt, Dates.Hour)` is a
shortcut for `ceil(zdt, Dates.Hour(1))`.

`VariableTimeZone` transitions are handled as for `round`.

### Examples

The `America/Winnipeg` time zone transitioned from Central Standard Time (UTC-6:00) to
Central Daylight Time (UTC-5:00) on 2016-03-13, moving directly from 01:59:59 to 03:00:00.

```julia
julia> zdt = ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg"))
2016-03-13T01:45:00-06:00

julia> ceil(zdt, Dates.Day)
2016-03-14T00:00:00-05:00

julia> ceil(zdt, Dates.Hour)
2016-03-13T03:00:00-05:00
```
Copy link
Member

Choose a reason for hiding this comment

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

Should be marked as a "julia" code-block

"""
Base.ceil(::TimeZones.ZonedDateTime, ::Union{Dates.Period, Type{Dates.Period}})

"""
round(zdt::ZonedDateTime, p::Period, [r::RoundingMode]) -> ZonedDateTime
round(zdt::ZonedDateTime, p::Type{Period}, [r::RoundingMode]) -> ZonedDateTime

Returns the `ZonedDateTime` nearest to `zdt` at resolution `p`. The result will be in the
same time zone as `zdt`. 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(zdt, Dates.Hour)` is a
shortcut for `round(zdt, Dates.Hour(1))`.

Valid rounding modes for `round(::TimeType, ::Period, ::RoundingMode)` are
`RoundNearestTiesUp` (default), `RoundDown` (`floor`), and `RoundUp` (`ceil`).

### `VariableTimeZone` Transitions

Instead of performing rounding operations on the `ZonedDateTime`'s internal UTC `DateTime`,
which would be computationally less expensive, rounding is done in the local time zone.
This ensures that rounding behaves as expected and is maximally meaningful.

If rounding were done in UTC, consider how rounding to the nearest day would be resolved for
non-UTC time zones: the result would be 00:00 UTC, which wouldn't be midnight local time.
Similarly, when rounding to the nearest hour in `Australia/Eucla (UTC+08:45)`, the result
wouldn't be on the hour in the local time zone.

When `p` is a `DatePeriod` rounding is done in the local time zone in a straightforward
fashion. When `p` is a `TimePeriod` the likelihood of encountering an ambiguous or
non-existent time (due to daylight saving time transitions) is increased. To resolve this
issue, rounding a `ZonedDateTime` with a `VariableTimeZone` to a `TimePeriod` uses the
`DateTime` value in the appropriate `FixedTimeZone`, then reconverts it to a `ZonedDateTime`
in the appropriate `VariableTimeZone` afterward.

Rounding is not an entirely "safe" operation for `ZonedDateTime`s, as in some cases
historical transitions for some time zones (such as `Asia/Colombo`) occur at midnight. In
such cases rounding to a `DatePeriod` may still result in an `AmbiguousTimeError` or a
`NonExistentTimeError`. (But these events should be relatively rare.)

Regular daylight saving time transitions should be safe.

### Examples

The `America/Winnipeg` time zone transitioned from Central Standard Time (UTC-6:00) to
Central Daylight Time (UTC-5:00) on 2016-03-13, moving directly from 01:59:59 to 03:00:00.

```julia
julia> zdt = ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg"))
2016-03-13T01:45:00-06:00

julia> round(zdt, Dates.Hour)
2016-03-13T03:00:00-05:00

julia> round(zdt, Dates.Day)
2016-03-13T00:00:00-06:00
```
Copy link
Member

@omus omus Jul 5, 2016

Choose a reason for hiding this comment

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

I think examples are more meaningful if you show the input value before and after rounding.

julia> zdt = ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg"))
2016-03-13T01:45:00-06:00

julia> round(zdt, Dates.Hour)
2016-03-13T03:00:00-05:00

julia> zdt = ZonedDateTime(2016, 3, 13, 6, TimeZone("America/Winnipeg"))
2016-03-13T06:00:00-05:00

julia> round(zdt, Dates.Day)
2016-03-13T00:00:00-06:00

Alternatively:

julia> zdt = ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")); display(zdt); display(round(zdt, Dates.Hour))
2016-03-13T01:45:00-06:00
2016-03-13T03:00:00-05:00

julia> zdt = ZonedDateTime(2016, 3, 13, 6, TimeZone("America/Winnipeg")); display(zdt); display(round(zdt, Dates.Day))
2016-03-13T06:00:00-05:00
2016-03-13T00:00:00-06:00

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. The first example is much cleaner, in my opinion.

Copy link
Member

Choose a reason for hiding this comment

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

I also prefer the first example


The `Asia/Colombo` time zone revised the definition of Lanka Time from UTC+6:30 to UTC+6:00
on 1996-10-26, moving from 00:29:59 back to 00:00:00.

```julia
julia> zdt = ZonedDateTime(1996, 10, 25, 23, 45, TimeZone("Asia/Colombo"))
1996-10-25T23:45:00+06:30

julia> round(zdt, Dates.Hour)
1996-10-26T00:00:00+06:30

julia> round(zdt, Dates.Day)
ERROR: Local DateTime 1996-10-26T00:00:00 is ambiguious
```
""" # Defined in base/dates/rounding.jl
Base.round(::TimeZones.ZonedDateTime, ::Union{Dates.Period, Type{Dates.Period}})
3 changes: 2 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const TZFILE_DIR = joinpath(PKG_DIR, "test", "tzfile")
# Note: resolving only the time zones we want is much faster than running compile which
# recompiles all the time zones.
tzdata = Dict{AbstractString,Tuple{ZoneDict,RuleDict}}()
for name in ("australasia", "europe", "northamerica")
for name in ("asia", "australasia", "europe", "northamerica")
tzdata[name] = tzparse(joinpath(TZDATA_DIR, name))
end

Expand All @@ -34,4 +34,5 @@ include(joinpath("timezones", "ranges.jl"))
include(joinpath("timezones", "local.jl"))
include(joinpath("timezones", "local_mocking.jl"))
include(joinpath("timezones", "discovery.jl"))
VERSION >= v"0.5.0-dev+5244" && include(joinpath("timezones", "rounding.jl"))
include("TimeZones.jl")
Loading