-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
union
, intersection
and overlaps?
for Range
s
#15106
base: master
Are you sure you want to change the base?
union
, intersection
and overlaps?
for Range
s
#15106
Conversation
This looks like a great start. A couple areas of focus:
|
src/range.cr
Outdated
# ``` | ||
def union(other : Range) | ||
if self.end < other.begin || other.end < self.begin | ||
return 0..0 |
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'm not sure we can get away with hardcoding a Range(Int32, Int32)
specifically. My own use case for this functionality is actually Range(Time, Time)
, so I'd get compile-time errors with this return value.
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.
Good catch, and thank you for the quick review!
I'll get on these changes asap! ✌️⭐
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.
Should we revert from 0..0
to the originally suggested nil
? I think it makes sense, but open to other options.
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.
Using any of the four endpoints here instead of 0
should probably work too
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.
To me nil seems like a more logical return value than using any of the existing endpoints which might be confusing.
Totally open to your reasoning though!
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.
Think the intersection of a list returning a nil vs an empty list: which feels more logical? Which is less of a burden?
Returning nil
means that the methods always return a nilable type (Range(B, E) | Nil
) and we must always check if the value is nil or not before we can do anything.
Returning an empty range means we always return a Range(B, E) that would act as a NOOP object, and if we need to know if the range is empty, we can call Range#empty?
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.
BTW: 0..0
wasn't an empty range, it's a range with a single value (starts at 0 and terminates at 0 inclusive):
(0..0).to_a # => [0]
(0...0).to_a # => []
union
, intersection
and overlaps?
for Range
sunion
, intersection
and overlaps?
for Range
s
Some specs would be a good idea as well 👍. |
Tests and specs? In this economy? |
Added some specs (edit: will go back and add tests for other types of Ranges) and went with the nil return as originally suggested in @jgaskins' issue. On error handling - should I be adding any type handling in the functions themselves or is an error message like below sufficient?
or
|
I don't think we can do much about improving these error message anyways. That would require knowing for which types the comparison operator is defined, and we cannot know this. We mustn't restrict it to identical types because there are many examples where different typs are still comparable. |
Added some additional tests for other datatypes (let me know if there are any more that should be spec'd), but running into an interesting issue - detailed over one of the tests.
Some of these intended adjacencies would depend on the user's use case, but I'm not sure there's a reliable way to allow the user that kind of customizability. Is there a standard we should follow? |
Adjacency can only work for types with (meaningfully) discrete values. This should be indicated by So for types that define |
spec/std/range_spec.cr
Outdated
# | ||
# How do we define adjacency? | ||
# | ||
# (1..5).union(6..10).should eq(1..10) # Adjacent ranges |
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.
As per the Range documentation you'd use #succ
and #pred
:
Ranges typically involve integers, but can be created using arbitrary objects as long as they define succ (or pred for #reverse_each), to get the next element in the range, and < and #==, to know when the range reached the end:
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.
As per the Range documentation you'd use
#succ
and#pred
:Ranges typically involve integers, but can be created using arbitrary objects as long as they define succ (or pred for #reverse_each), to get the next element in the range, and < and #==, to know when the range reached the end:
Making this change this weekend. ✨
What about some edge cases? For example empty ranges: (1...1).intersect(0..10) #=> empty |
I can add a check for one of the ranges being empty (i.e. non-inclusive ranges where the ends are equal), but is it valid to return 1...1, which is an accurate representation of the intersection. (...is it accurate?) |
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.
nil
is implicit with no return value.
Apologies for the untimely updates here - been a bit of a hectic few weeks, but will be jumping back into this asap! I have not forgotten! 😅 |
Well, |
I could imagine some edge effects, but only if the end user was comparing data to either the beginning or end of the empty range without considering context of the full object (i.e., both ends). I don't think it's unreasonable to expect users to handle that sort of return if their use case could result that way, but I'm open to other thoughts. I think I'm also a little unclear as to why nil isn't a valid return in such a case, but maybe I'm missing something. |
It's not that |
Coming back to this - what do people think is a reasonable empty range to return in the edge cases discussed above? I'm not sure if we've reached a solid conclusion. |
I implemented empty ranges with the logic @straight-shoota describes in #10148, which describes a
I think this is a good resolution as it logically makes sense and won't require the user to handle nil. I've also added #10148 to this PR, but I think there's a case to be made for giving |
Apologies for the earlier PR from my forked master branch.
Based on the request from @jgaskins. Behaviors below:
Fixes #14487
Fixes #14488
Fixes #10148
P.S., very excited to be making a small contribution to a lovely language.
Looking forward to review!