I guess that the main reason for blurring distinction between [*, LocalDate.MAX
) and [*, +∞) was (1) inability to construct ranges containing LocalDate.MAX
(that's my impression after following #53 discussion). The other reason I see is (2) presence of LocalDateRange.ofClosed
which is the only possible way to (at least try to) create [*, LocalDate.MAX
].
Although fix for #53, more or less, resolved problem with (1), it introduced new problems instead. I'll try to show this later.
As a side note... One may think that using [a, b] instead of [a, b) could be a good solution for (1) and (2) - although not being so convenient to implement, but that's another story. [a, b] would indeed solve (1) and (2), but at the same time it would introduce almost the same problem with toString
. How would you stringify [LocalDate.MIN
, LocalDate.MAX
]? Would you consider "-999999999-01-01/+1000000000-01-01"
legal despite "+1000000000-01-01"
being out of domain? On the other hand, using "-999999999-01-01/+999999999-12-31"
would make [LocalDate.MIN
, LocalDate.MAX
] indistinguishable from [LocalDate.MIN
, LocalDate.MAX
). This seems like some kind of vicious circle. Finally, with [a, b] you couldn't represent empty ranges.
To summarize: with [a, b) and no LocalDateRange.ofClosed
, there would be just one problem: (1) inability to construct ranges containing LocalDate.MAX
.
I believe that making LocalDate.MIN
and LocalDate.MAX
special, caused more problems than it solved. Some of the resulting quirks are documented, some aren't.
I'll try to show several things at once using one example.
LocalDateRange range = LocalDateRange.ofClosed(
LocalDate.MAX.minusDays(1), LocalDate.MAX.minusDays(1)
);
range.isEmpty(); // -> false (of course)
range.lengthInDays(); // -> 1 (so far, so good)
range.contains(LocalDate.MAX.minusDays(1)); // -> true (obviously)
range.contains(LocalDate.MAX); // -> true (what???)
range.isUnboundedEnd(); // -> true (it's getting worse...)
Seems like non-empty range
of length 1 contains 2 days, all of that despite being unbounded at the end. On the surface, no LocalDate.MAX
involved. Weird.
As you can see, (3) isEmpty
, contains
, lengthInDays
and isUnboundedStart
/ isUnboundedEnd
are inconsistent with each other.
Needless to say, problem with non-expressibility hasn't disappeared. There is still no way to represent [LocalDate.MAX.minusDays(1)
, LocalDate.MAX.minusDays(1)
]. Internally, it is indistinguishable from [LocalDate.MAX.minusDays(1)
, +∞). Everything close to LocalDate.MAX
becomes suspicious. This is documented, but consequences are not so obvious and can be surprising.
There are similar problems with LocalDate.MIN
too. For instance, [LocalDate.MIN
, LocalDate.MIN
) is empty and unbounded at the same time.
I know it's rather unlikely to happen, but the least I can do is to leave my two cents here. Sticking to always bounded [a, b) would be perfect. It is possible to migrate in almost entirely backward compatible way.
- No more infinities. From now on,
isUnboundedStart
and isUnboundedEnd
are returning false
unconditionally.
- [*,
LocalDate.MAX
] silently becomes [*, LocalDate.MAX
), just as it is now. The only change is that resulting range doesn't contain LocalDate.MAX
.
I agree that everything what I've just described.. those are "just" edge-cases. They should't cause much trouble in real life. And maybe they won't. However, I would argue that if there have to be some quirks in the implementation - and we're stuck with (2) I believe - they should be as "local" and "constrained" as possible (for the lack of better words). It's easier to reason about code when there's only one method referring to edge case (ofClosed
), than where there are many (of
, getEnd
, getEndInclusive
, contains
).
Discussion Fixed