-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Dates.Time compatibility with StepRangeLen by adding converting constructors #58846
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
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems quite sensible. Looking at the three concrete subtypes of DateTime
:
For Date
, the conversion is strange; semantically it should always be the zero time, I suppose, but the convert method throws which I like. This PR adds a layer of indirection to that error.
For DateTime
, this PR doesn't change behavior.
For Time
, Time(t::Time)
should be a no-op and this PR changes it from a method error to a no-op.
From a documentation perspective, the Time(dt::DateTime)
method is semantically nontrivial and has a good docstring. I like it how it is, but if this PR were to change the type signature, the docstring would have to change, too.
Perhaps it would be cleaner and semantically equivalent on existing concrete types to implement this as Time(t::Time) = t
with no docstring on that new method?
thank-you for the review and sending me back to look harder. My initial solution was based on The docstring and the method signature should agree regarding parameter type, though this hasn't been done for the other conversion constructors - method I accept your suggestion that it's better to simply define Footnotes
|
sorry - I seem to have gotten some other changes intermingled with my PR... not sure what to do about that. |
Just to highlight the periodicity this achieves:
|
You could try |
3892a2d
to
69d50cf
Compare
resolved by force pushing onto my fork. |
I see there are build errors re documentation along the lines discussed.... i'll investigate.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR has grown quite a bit since its original form. I'm going to try to find someone more familiar with the Dates
library to take over reviewing.
much appreciated thank-you. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't able to find another reviewer, so I'll continue to review this.
This PR contains a few distinct and separable changes.
- Mentioning
Time
more prominently in the prose documentation. This is a good change that I would be happy to merge. - Refactoring some conversions, which I have yet to give a detailed technical review
- Enabling cyclic
Time
ranges which is an expansion of the public API and needs broader review. It also has interesting consequences with respect to the assumptions we can make about ranges.
I'm tagging triage to discuss 3. We meet every other week on Thursdays or Fridays depending on time zone, with our next meeting on the 17th, 1:15 EDT or the 18th, at 2:15AM YAKT on google meet.
@@ -457,6 +463,18 @@ julia> collect(dr) | |||
2014-05-29 | |||
2014-06-29 | |||
2014-07-29 | |||
|
|||
julia> r = range(Time(0), step = Hour(9), length = 5) | |||
Time(0):Hour(9):Time(12) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems... odd.
It violates extrema(range) == minmax(first(range), last(range))
But the definition of extrema
assumes this identity, and therefore gets the wrong answer.
julia> r = range(Time(0), step = Hour(9), length = 5)
Time(0):Hour(9):Time(12)
julia> extrema(r)
(Time(0), Time(12))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok that's nasty odd, but it looks to me like faulty short-cutting minimum
and maximum
methods for StepRangeLen
. We could fix those to actually iterate the sequence. For comparison, this gives the expected result:
julia> extrema(identity, r)
(Time(0), Time(18))
I suppose another unhappy oddity that initially drew your attention is that the returned range violates the implied expectation (see ?show
- The representation used by show ... should be parseable Julia code when possible) that its repr
could be used to construct an equivalent range, but in this case it stops early (length 2) rather than wrapping around to the full length of 5:
julia> length(eval(Meta.parse(repr(r)))), length(r)
(2, 5)
We could fix this too - there is already special handing in the show
method to show ranges such StepRangeLen(0, 0, 0)
with constructor syntax.
Are ranges expected to be monotonic? Both the above oddities are due to that unsatisfied expectation. But nothing normative in the docs for range
, AbstractRange
, or StrepRangeLen
(or the types OrdinalRange
, StepRange
) firmly specifies monotonicity. Extracting key references:
range
- ... a specialized array with evenly spaced elements and optimized storageAbstractRange
- Supertype for linear ranges ...StepRangeLen
- a range r where r[i] produces values of type T ...OrdinalRange
- ... with spacings of type S. The steps should be always-exact multiples of oneunitStepRange
- the step between each element is constant. Neither T nor S should be floating point types.
If so, ideally that expectation should be codified somehow, or at least documented. Here is an example from another package that already behaves similarly to the proposed behavior for Time
:
julia> using Mods
julia> r2 = range(Mod{24}(0), step=9, length=5)
Mod{24}(0):9:Mod{24}(12)
julia> collect(r2)
5-element Vector{Mod{24}}:
Mod{24}(0)
Mod{24}(9)
Mod{24}(18)
Mod{24}(3)
Mod{24}(12)
julia> maximum(r2)
ERROR: MethodError: no method matching isless(::Mod{24}, ::Mod{24})
It already works because, unlike Dates.Time
, Mods.Mod
already has a copy constructor:
julia> Mod{24}(Mod{24}(0))
Mod{24}(0)
Now, that expectation of a copy constructor for T in StepRangeLen
(ie that T(t::T) = T
) is the original problem that I solved by adding copy constructor for Time
. But, in retrospect, I now think a better solution could be to relieve StepRangeLen
of this undocumented expectation, by changing to use the available parametric self-conversion (and then document the expectation of a conversion from R to T where they are not identical):
--- a/base/range.jl
+++ b/base/range.jl
@@ -974,7 +974,7 @@ unsafe_getindex(v::OneTo{T}, i::Integer) where T = convert(T, i)
unsafe_getindex(v::AbstractRange{T}, i::Integer) where T = convert(T, first(v) + (i - oneunit(i))*step_hp(v))
function unsafe_getindex(r::StepRangeLen{T}, i::Integer) where T
u = oftype(r.offset, i) - r.offset
- T(r.ref + u*r.step)
+ convert(T, (r.ref + u*r.step))
end
Sorry for the long reply, but I think it's worth digging into the details to help decide what is the best way forward.
I am happy to separate out a documentation change, but I'd like to first get a direction on the other changes.
Please hold off on that till after triage. Really the only reason I was adding conversions was to overcome the issue using
Indeed, although those concerns with ranges exist anyway and appear more fundamental. There are other ways of getting cyclic time sequences - eg:
I'd like to attend, and I'll try, but there's a good chance I'll be sleeping at 03:15 UTC+10:00, so I apologize in advance if I don't attend. My goal is to improve the usability of julia - I appreciate the feeback you've given, which has highlighted that there are subtleties I wasn't initially aware of. Please let me know if there's a better way forward than PR, such as raising a github issue, or a new discussion on julia discourse. (I'm going to rebase this PR, update the doco changes, and mark this is draft to reflect the true status.) |
Thank you for these comments! |
It's very wild that adding these converting constructors — which I think probably should exist in some fashion — has this impact on ranges. This wrapping of ranges also happens with integers, but it's largely avoided in two ways:
So it's only really seen in the very explicit (and unwieldy): julia> r = StepRangeLen(typemax(Int)-2, 1, 5)
9223372036854775805:1:-9223372036854775807
julia> collect(r)
5-element Vector{Int64}:
9223372036854775805
9223372036854775806
9223372036854775807
-9223372036854775808
-9223372036854775807
julia> extrema(r)
(-9223372036854775807, 9223372036854775805) Dates' |
This is to satisfy the expectation that `Time(t::Time) == t`, similar to other subtypes of TimeType and Period. This also resolves issues ranges of Time, such as given by `range(Time(0), step=Hour(1), length=24)`.
…vor of explicity copy constructors. Additional test to show the periodicity of StepRangeLen{Time}
Co-authored-by: Lilith Orion Hafner <lilithhafner@gmail.com>
0de2836
to
1f4b093
Compare
This is improve the usability of
Time
by satisfying the expectation thatTime(t::Time) == t
, similar to other subtypes ofTimeType
andPeriod
, and most other types.For example, it resolves this unexpected behavior: