Skip to content

Commit

Permalink
Clarify documentation, fix rare ceil error case
Browse files Browse the repository at this point in the history
Documentation changes were made based on PR feedback.
`ceil(ZonedDateTime(1996, 10, 26, 0, 35, TimeZone("Asia/Colombo")), Dates.Day)`
no longer results in an AmbiguousTimeError.
  • Loading branch information
spurll committed Jul 5, 2016
1 parent 58eb8fb commit 971b457
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 49 deletions.
43 changes: 33 additions & 10 deletions docs/rounding.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Rounding ZonedDateTimes
## Rounding a ZonedDateTime

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)
Expand All @@ -7,9 +7,12 @@ however, unexpected behaviour may be encountered.

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. (Consider how
rounding to the nearest day would be resolved for non-UTC time zones, or even rounding to
the nearest hour for time zones like `America/St_Johns` or `Australia/Eucla`.)
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

Expand All @@ -25,7 +28,7 @@ When the target resolution is a `DatePeriod` rounding is done in the local time
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.
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.)

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

```julia
julia> floor(ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")), Dates.Day)
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(ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")), Dates.Day)
julia> ceil(zdt, Dates.Day)
2016-03-14T00:00:00-05:00

julia> round(ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")), Dates.Day)
julia> round(zdt, Dates.Day)
2016-03-13T00:00:00-06:00

julia> ceil(ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")), Dates.Hour)
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(ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")), Dates.Hour)
julia> round(zdt, Dates.Hour)
2016-03-13T03:00:00-05:00
```

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
```
74 changes: 56 additions & 18 deletions src/timezones/rounding.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ function Base.floor(zdt::ZonedDateTime, p::Dates.TimePeriod)
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
Returns the nearest `ZonedDateTime` less than or equal to `zdt` at resolution `p`. The
result will be in the same time zone as `zdt`.
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))`.
Expand All @@ -25,9 +33,15 @@ shortcut for `floor(zdt, Dates.Hour(1))`.
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> floor(ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")), Dates.Day)
```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, ::Dates.Period)
Expand All @@ -36,7 +50,7 @@ Base.floor(::TimeZones.ZonedDateTime, ::Dates.Period)
ceil(zdt::ZonedDateTime, p::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`.
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))`.
Expand All @@ -48,21 +62,25 @@ shortcut for `ceil(zdt, Dates.Hour(1))`.
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> ceil(ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")), Dates.Hour)
2016-03-13T03:00:00-05:00
```julia
julia> zdt = ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg"))
2016-03-13T01:45:00-06:00
julia> ceil(ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")), Dates.Day)
julia> ceil(zdt, Dates.Day)
2016-03-14T00:00:00-05:00
julia> ceilj(zdt, Dates.Hour)
2016-03-13T03:00:00-05:00
```
"""
Base.ceil(::TimeZones.ZonedDateTime, ::Dates.Period)
Base.ceil(::TimeZones.ZonedDateTime, ::Dates.Period) # Defined in base/dates/rounding.jl

"""
round(zdt::ZonedDateTime, p::Period, [r::RoundingMode]) -> ZonedDateTime
Returns the `ZonedDateTime` nearest to `zdt` at resolution `p`. By default
(`RoundNearestTiesUp`), ties (e.g., rounding 9:30 to the nearest hour) will be rounded up.
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))`.
Expand All @@ -74,9 +92,12 @@ Valid rounding modes for `round(::TimeType, ::Period, ::RoundingMode)` are
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. (Consider how
rounding to the nearest day would be resolved for non-UTC time zones, or even rounding to
the nearest hour for time zones like `America/St_Johns` or `Australia/Eucla`.)
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
Expand All @@ -97,12 +118,29 @@ Regular daylight saving time transitions should be safe.
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> round(ZonedDateTime(2016, 3, 13, 1, 45, TimeZone("America/Winnipeg")), Dates.Hour)
```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(ZonedDateTime(2016, 3, 13, 6, TimeZone("America/Winnipeg")), Dates.Day)
julia> round(zdt, Dates.Day)
2016-03-13T00:00:00-06:00
```
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
```
"""
Base.round(::TimeZones.ZonedDateTime, ::Dates.Period)
Base.round(::TimeZones.ZonedDateTime, ::Dates.Period) # Defined in base/dates/rounding.jl
74 changes: 53 additions & 21 deletions test/timezones/rounding.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,35 @@ colombo = TimeZone("Asia/Colombo") # See note below
# NO TRANSITIONS #
##################

# Test rounding where no rounding is necessary.

dt = DateTime(2016)

for tz in [utc, fixed, winnipeg, st_johns, eucla, colombo]
zdt = ZonedDateTime(dt, tz)

@test floor(zdt, Dates.Year) == zdt
@test floor(zdt, Dates.Month) == zdt
@test floor(zdt, Dates.Day) == zdt
@test floor(zdt, Dates.Hour) == zdt
@test floor(zdt, Dates.Minute) == zdt
@test floor(zdt, Dates.Second) == zdt

@test ceil(zdt, Dates.Year) == zdt
@test ceil(zdt, Dates.Month) == zdt
@test ceil(zdt, Dates.Day) == zdt
@test ceil(zdt, Dates.Hour) == zdt
@test ceil(zdt, Dates.Minute) == zdt
@test ceil(zdt, Dates.Second) == zdt

@test round(zdt, Dates.Year) == zdt
@test round(zdt, Dates.Month) == zdt
@test round(zdt, Dates.Day) == zdt
@test round(zdt, Dates.Hour) == zdt
@test round(zdt, Dates.Minute) == zdt
@test round(zdt, Dates.Second) == zdt
end

# Test rounding non-controversial ZonedDateTimes (no transitions).

dt = DateTime(2016, 2, 5, 13, 10, 20, 500)
Expand Down Expand Up @@ -111,11 +140,14 @@ dt = DateTime(2015, 11, 1, 2, 15) # 15 minutes after ambiguous hour
zdt = ZonedDateTime(dt, fixed)
@test floor(zdt, Dates.Day) == ZonedDateTime(2015, 11, 1, fixed)
@test floor(zdt, Dates.Hour(3)) == ZonedDateTime(2015, 11, 1, fixed)
# Rounding to Hour(3) will give 00:00, 03:00, 06:00, 09:00, etc.

for tz in [winnipeg, st_johns]
zdt = ZonedDateTime(dt, tz)
@test floor(zdt, Dates.Day) == ZonedDateTime(2015, 11, 1, tz)
@test floor(zdt, Dates.Hour(3)) == ZonedDateTime(2015, 11, 1, 1, tz, 1)
# Rounding is performed in the current fixed zone, then relocalized if a transition has
# occurred. This means that instead of 00:00, 03:00, etc., we expect 01:00, 04:00, etc.
end

# Test rounding forward, toward the ambiguous hour.
Expand Down Expand Up @@ -155,9 +187,9 @@ zdt = ZonedDateTime(dt, fixed)

for tz in [winnipeg, st_johns]
zdt = ZonedDateTime(dt, tz, 1) # First 1:25, before "falling back"
prev_hour = ZonedDateTime(DateTime(2015, 11, 1, 1), tz, 1)
between_hours = ZonedDateTime(DateTime(2015, 11, 1, 1, 30), tz, 1)
next_hour = ZonedDateTime(DateTime(2015, 11, 1, 1), tz, 2)
prev_hour = ZonedDateTime(2015, 11, 1, 1, tz, 1)
between_hours = ZonedDateTime(2015, 11, 1, 1, 30, tz, 1)
next_hour = ZonedDateTime(2015, 11, 1, 1, tz, 2)
@test floor(zdt, Dates.Day) == ZonedDateTime(2015, 11, 1, tz)
@test ceil(zdt, Dates.Day) == ZonedDateTime(2015, 11, 2, tz)
@test round(zdt, Dates.Day) == ZonedDateTime(2015, 11, 1, tz)
Expand All @@ -183,37 +215,37 @@ zdt = ZonedDateTime(1996, 10, 25, 23, 55, colombo) # 5 minutes before ambiguous

zdt = ZonedDateTime(1996, 10, 26, 0, 35, colombo) # 5 minutes after ambiguous half-hour
@test_throws AmbiguousTimeError floor(zdt, Dates.Day)
@test_throws AmbiguousTimeError ceil(zdt, Dates.Day) # floor is called during ceil
@test ceil(zdt, Dates.Day) == ZonedDateTime(1996, 10, 27, colombo)
@test_throws AmbiguousTimeError round(zdt, Dates.Day)

# Rounding to the ambiguous midnight works fine using a TimePeriod resolution, however.

zdt = ZonedDateTime(1996, 10, 25, 23, 55, colombo) # 5 minutes before ambiguous half-hour
@test ceil(zdt, Dates.Hour) == ZonedDateTime(DateTime(1996, 10, 26), colombo, 1)
@test round(zdt, Dates.Hour) == ZonedDateTime(DateTime(1996, 10, 26), colombo, 1)
@test ceil(zdt, Dates.Hour) == ZonedDateTime(1996, 10, 26, colombo, 1)
@test round(zdt, Dates.Hour) == ZonedDateTime(1996, 10, 26, colombo, 1)

zdt = ZonedDateTime(1996, 10, 26, 0, 35, colombo) # 5 minutes after ambiguous half-hour
@test floor(zdt, Dates.Hour) == ZonedDateTime(DateTime(1996, 10, 26), colombo, 2)
@test floor(zdt, Dates.Hour) == ZonedDateTime(1996, 10, 26, colombo, 2)

# Rounding during the first half-hour between 00:00 and 00:30.

zdt = ZonedDateTime(DateTime(1996, 10, 26, 0, 15), colombo, 1)
@test floor(zdt, Dates.Hour) == ZonedDateTime(DateTime(1996, 10, 26), colombo, 1)
@test ceil(zdt, Dates.Hour) == ZonedDateTime(DateTime(1996, 10, 26, 0, 30), colombo)
@test round(zdt, Dates.Hour) == ZonedDateTime(DateTime(1996, 10, 26), colombo, 1)
@test floor(zdt, Dates.Minute(30)) == ZonedDateTime(DateTime(1996, 10, 26), colombo, 1)
@test ceil(zdt, Dates.Minute(30)) == ZonedDateTime(DateTime(1996, 10, 26, 0), colombo, 2)
@test round(zdt, Dates.Minute(30)) == ZonedDateTime(DateTime(1996, 10, 26, 0), colombo, 2)
zdt = ZonedDateTime(1996, 10, 26, 0, 15, colombo, 1)
@test floor(zdt, Dates.Hour) == ZonedDateTime(1996, 10, 26, colombo, 1)
@test ceil(zdt, Dates.Hour) == ZonedDateTime(1996, 10, 26, 0, 30, colombo)
@test round(zdt, Dates.Hour) == ZonedDateTime(1996, 10, 26, colombo, 1)
@test floor(zdt, Dates.Minute(30)) == ZonedDateTime(1996, 10, 26, colombo, 1)
@test ceil(zdt, Dates.Minute(30)) == ZonedDateTime(1996, 10, 26, colombo, 2)
@test round(zdt, Dates.Minute(30)) == ZonedDateTime(1996, 10, 26, colombo, 2)

# Rounding during the second half-hour between 00:00 and 00:30.

zdt = ZonedDateTime(DateTime(1996, 10, 26, 0, 15), colombo, 2)
@test floor(zdt, Dates.Hour) == ZonedDateTime(DateTime(1996, 10, 26), colombo, 2)
@test ceil(zdt, Dates.Hour) == ZonedDateTime(DateTime(1996, 10, 26, 1), colombo)
@test round(zdt, Dates.Hour) == ZonedDateTime(DateTime(1996, 10, 26), colombo, 2)
@test floor(zdt, Dates.Minute(30)) == ZonedDateTime(DateTime(1996, 10, 26), colombo, 2)
@test ceil(zdt, Dates.Minute(30)) == ZonedDateTime(DateTime(1996, 10, 26, 0, 30), colombo)
@test round(zdt, Dates.Minute(30)) == ZonedDateTime(DateTime(1996, 10, 26, 0, 30), colombo)
zdt = ZonedDateTime(1996, 10, 26, 0, 15, colombo, 2)
@test floor(zdt, Dates.Hour) == ZonedDateTime(1996, 10, 26, colombo, 2)
@test ceil(zdt, Dates.Hour) == ZonedDateTime(1996, 10, 26, 1, colombo)
@test round(zdt, Dates.Hour) == ZonedDateTime(1996, 10, 26, colombo, 2)
@test floor(zdt, Dates.Minute(30)) == ZonedDateTime(1996, 10, 26, colombo, 2)
@test ceil(zdt, Dates.Minute(30)) == ZonedDateTime(1996, 10, 26, 0, 30, colombo)
@test round(zdt, Dates.Minute(30)) == ZonedDateTime(1996, 10, 26, 0, 30, colombo)

###############
# ERROR CASES #
Expand Down

0 comments on commit 971b457

Please sign in to comment.