-
Notifications
You must be signed in to change notification settings - Fork 49
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
DateTime::Duration::in_units('days') should not return completely wrong answers for durations over 1 month #133
Comments
Note: Even if the decision is to |
i sympathize with the objection,
But i'm unclear if any change is safe (even a Carp instead of a Croak), as this arguably buggy behavior may by now be relied upon ? To really fix this API would probably be majorly backwards-incompatible , so should probably be a major version bump and big warnings in CHANGES and README.md etc ... I'm fingerpainting that it could be phased
(This could still seriously break an app that migrated from $n to $n+3 after many years of non-maintenance! Here's hoping they have regression tests ) |
I can certainly imagine that existing code might be relying on this to return some numeric value, and croaking or returning
This approach is predicated on the presumption that relying on To minimize backwards-compatibility issues, my suggestion would be to alter the definition of this API call to return a "best effort" approximation in circumstances where a fixed conversion rate does not exist, and replace the current caveat with clear documentation of the exact approximation algorithm. For example, the conversion from years to days could be defined as 365.2425 days/year, taking leap days into account, and the conversion from days to seconds could defined as 86,400 seconds/day, explicitly ignoring leap seconds. All the other conversions are obvious: 12 months/year, 7 days/week, etc. Fractional values of any unit (e.g. partial days) could be converted down into smaller units all the way down to nanoseconds and added to any duration values stored in those smaller units, and then the Ideally, I would also suggest extending the internals of |
On a separate note, the fact that |
Thanks for this bug report and the good discussion. I don't think doing any sort of guesstimation of the return values (like a month is 30.5 days) is on the table. This seems like the most likely thing to break existing code, since it wouldn't even cause a warning, much less an exception. If we want to make it possible to turn months into days, the right way to do this is by using a my $dur = $dt1->delta_md($dt2)->with_base($dt1); We could even consider adding a set of new methods to my $dur = $dt->delta_md_with_base($dt) This As far as other changes like making Adding new methods and classes is much safer. It fixes the problem for people who care without any risk of breaking existing code. |
I was thinking about this a bit more and there is one big question with adding a new Duration class. How do the I think this should just work as long as the new Duration class implements the |
That's what one would need to do if one needs to offset other DateTime's by the same amount, so not unreasonable to support. The new alternative Duration class could either be duck-type compatible (supporting |
I think for max compatibility we'd want |
You're talking about the risk of breaking existing already-broken code. How would this break the code any worse than it already is? Any code which would be affected by this change is already broken and this would be likely to make it less broken than it already is. I understand that adding a warning after the fact is potentially detrimental. But why on earth would you want to continue returning 20 as a completely wrong answer just because you can't be certain whether 50 or 51 is the correct answer? Returning either 50 or 51, on any arbitrary basis, is clearly better than returning 20. Such an approximation may not be returning a guaranteed correct answer, but the existing implementation is already returning wrong answers! If the result is subject to potential error anyhow, why not at least minimize that potential error to a day or two, rather than an unlimited error range that could easily be off by months or years?
This is approximately what I had in mind, some sort of duration with knowledge of the actual date(s) involved so that it can return correct answers. Is there a reason it needs to be a different class though? Why not just have different behavior based on whether the reference date(s) have been set on the object?
This is part of the reason I suggested the "best effort" approximation, because any existing code that would be affected is already broken but likely not being properly maintained, and the maintainers are probably not even aware of the issue. Replacing a completely wrong answer with a close approximation is likely to make such code work better, not worse. It's not a perfect solution, but it's a reasonable compromise.
What is the value of maintaining bug-for-bug compatibility for code which is already undeniably invalid code according to the existing documentation? |
sigh. While i get your point that broken code is broken, it's possible someone is using the current broken API with understanding of what it actually does. Changing the behavior of broken code in production that may have been worked-around is still frowned upon in IT shops. This isn't like CPR where worrying about breaking ribs isn't a concern since broken ribs is better than dead. (And if you don't break ribs, you might not be doing it right.) Some script in a production job may well be still breathing despite this borked API. So at a minimum it would need very loud INCOMPATIBLE CHANGE verbiage. (Since DateTime::Duration is not a dual-life Core module, at least there isn't the dreaded possibility that an OS upgrade slips a new version of the module in with an updated Perl5, for those shops that foolishly use OS /usr/bin/perl for production jobs Noooooo.) |
FWIW, at $dayjob the biggest Perl project I support uses DateTime 714 times, codebase written from 2009-present. Whatever it's been doing since 2009 is Correct(tm) from our perspective since we "worked around" any non-intuitive behavior years ago. Please don't stop us from continually updating |
I'm pretty sure I already wrote this in some other issue somewhere, but I'll say it again here ... Sadly, I think that most of the Perl code that will ever be written already exists. So because of that, I think backwards compatibility has a much higher priority than fixing poorly designed APIs (note that fixing bugs is something else entirely). The situation was different 10-20 years ago, when the benefit of a backwards incompatible change could be enjoyed by more people than were upset at the breakage. |
I understand where you're coming from, and this viewpoint makes sense in general. In this particular instance, it seems incredibly unrealistic to me. Do you truly believe there's anyone out there who (1) is aware of the subtle nuance that On the other hand, I opened this ticket because I encountered |
714 times? That number is very precise! I'm guessing you actually scanned the codebase to get that count, am I right? Just for the sake of curiosity, if you wouldn't mind scanning the codebase again, it would be very interesting to hear (1) how many times your codebase actually calls the |
Yup!
14
2
In one case it's doing this:
Is that a case where my horrible code has always been broken and I never realized it? In the other case someone else wrote this in 2012:
I don't know, off-hand, whether or not changing the behavior of |
That's a rather depressing thought! Sadly, you're probably right. As for fixing bugs, this is what the documentation for
The method call in question is Yes, there is a caveat about which conversions are possible, which warns the user not to rely on unsupported conversions. That doesn't make it any less of a bug that the method call is returning a remainder in violation of the documented return value! If you don't want to return a best-effort approximation of the total number of days, then there's one more option we haven't considered. You could change the implementation to always return zero for each of the requested units when an "impossible" conversion is attempted. The last example in the documentation could even be viewed as implying that result already! While |
Thanks, Jay! This is an excellent real-world code example, and it proves that there is code in the wild which is relying on the current API behavior. This is valuable input to the discussion. Sorry, guys! I was wrong, underestimating the likelihood of anyone relying on this bug. This isn't the unrealistic situation that I described, but it's a real-world example that does change my opinion on how to proceed. I was thinking of the API call being made in isolation, but the usage of sequential calls like this didn't occur to me. My bad! Unfortunately, it seems that any change in the behavior of this API call will indeed impact some of the existing code out there, and precautions like an INCOMPATIBLE CHANGE warning or a major version number change seem warranted. Perhaps it's not worth the hassle and potential disruption, especially given Dave's point about the sad state of Perl programming these days... This is hardly ideal, but in lieu of fixing the current behavior of this API call to match the documentation, perhaps the best alternative would be to retcon the situation by changing the documentation to match the implementation instead. It would make the API a bit strange, but there would be no backwards compatibility risk at all. (Adding a new API call that works better might be worth considering too.)
Sorry, I'm afraid your code above has always been broken. While it does work fine for many values, it fails badly for many more. You're asking the API for years, days, minutes and nanoseconds. The correct way to use this API call would be to ask for all of those units at once, so it knows the larger units to properly calculate the remainders for the smaller units. For example: my ($years, $days, $minutes, $nanoseconds) = $duration->in_units(qw[years days minutes nanoseconds]); However, because each of these units happens to be in a separate group, you're actually getting the same results back despite using the API call incorrectly, because of how limited the conversions are. Look at the list of possible conversions listed in the documentation:
This is actually four independent groups of units that will convert values within a group, but not between groups. At first glance, you might think that "days <=> hours" and "minutes <=> seconds" are also fixed conversion rates, but technically they're not due to Daylight Saving Time and leap seconds. Why is your code broken? You never asked the API for months. While there is a conversion rate between months and years, the API won't return fractional values, so you're losing the months entirely! (If you had asked for months instead of years, the years would've been added to the months and you'd be fine.) I tested your code, and here's some examples. Take particular note of the 1-month example and the final example from subtracting two different DateTime objects (not strings):
This final example returns the number of seconds in a day, completely omitting the other 2 months.
This code is actually fine, because
None of the proposed changes would affect your |
Oh well good to know my code has always been broken AND it stopped others from fixing other broken code. :) |
If you’re wanting to fix that function, you could try something like this if you want: sub flatten_to_seconds {
my ($duration) = @_;
my ($months, $days, $minutes, $nanoseconds) = $duration->in_units(qw[months days minutes nanoseconds]);
# Ha. DateTime is technically correct, that this CAN'T be calculated.
# It depends on leap seconds and all kinds of craziness.
# But we really don't care about those errors for this report.
# So squash a duration, close enough.
my $seconds = ($months / 12 * 365.2425 + $days) * 86400 + $minutes * 60 + $nanoseconds / 1000000000;
… I believe this should work correctly, and it uses the API the way it’s meant to be used. |
Currently,
DateTime::Duration::in_units('days')
will only return the days portion of theDateTime::Duration
object, completely ignoring the number of months that may also be stored. Although this behavior is documented, the caveat is very easy to miss. This method should not return completely wrong answers like this.Here is an example script which demonstrates the problem, and also shows that
DateTime::delta_days()
will return the correct number of days (albeit losing the negative sign), unlikeDateTime::subtract_datetime()
:Here is the output generated by the above script with DateTime version 1.58:
Returning -22 for
$duration->in_units("days")
when the correct duration is -53 days is clearly wrong. Several better options come to mind:die
with an error because this duration is over a month, "months" was not in the list of units passed toin_units()
, and there's no way to know the exact number of days which would be correct. This is arguably the right thing to do if the correct value cannot be calculated, but it might cause backwards-compatibility issues.undef
for the same reasons, preferably with a warning message generated. This could also cause backwards-compatibility issues, but likely to a lesser degree.The text was updated successfully, but these errors were encountered: