-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Enhance the date and time precision in Add methods #73198
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
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 curious why this is needed, vs
AddTicks((long) (value * TicksPerDay))Is there a perf benefit or something? Or some edge case with floating-point math?
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.
It is the later
some edge case with floating-point math. Can have precision loss with some values. @tannergooding can explain more details about that.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.
Interesting. If that's true, then I guess that still
DateTime.AddUnits(n) != DateTime.Add(TimeSpan.FromUnits(n))for some units and some value ofn?Uh oh!
There was an error while loading. Please reload this page.
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.
Yes, looks so. This can happen today too, I guess. We can track fixing in
TimeSpanlater. I wanted to get this fix now for .NET 7.0 and then we can fix any more issues as needed in the next releases.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.
Right.
doublecan only represent integrals exactly up to2^53anything higher than this will definitely have rounding error. Since there are approximately31,540,000,000,000,000nanoseconds per year, this is approximately315,400,000,000,000ticks per year.2^53 / TicksPerYeargives you approximately28.54 years(or10,425 days) before definite rounding error starts creeping in.This ends up with two very visible edge-case considerations:
double valuethat is already greater than2^53there is guaranteed loss of precisiondouble valuewhere(value * TicksPerUnit) > 2^53there is guaranteed loss of precisionThere is then a third, but less visible, consideration that these two rules applies mainly to the integral portion. The fractional portion is much tricker to understand since almost anything the user inputs has some loss of data due to
doubleonly being able to represent multiples of power of2.What this means is that at
2^52to2^53,doublecan represent no fractional data (that is it can only represent multiples of1). At2^51to2^52,doublecan only represent multiples of0.5, at2^50to2^51,doublecan only represent multiples of0.25, then0.125, then0.0625, and so on doubling in precision every smaller "power of two" down todouble.EpsilonatZero. This likewise halves the precision every larger "power of two" (2^53to2^54can only represent multiples of2, then4, then8, and so on).This boils down to: the closer
(value * TicksPerUnit)is to2^53, the more loss of data compared to the input ofvalueyou will observe. Splitting it into anintegralandfractionalportion helps reduce the overall rounding error by ensuring that the integral portion is handled and then the fractional portion by itself, which allows the most accuracy when it is scaled up.=====================
The current algorithm (which is
(long)((value * MillisPerUnit) + Adjustment) * MillisecondsPerTick) tries to minimize rounding error by computing adoublethat isMillisPerUnitrather than one that isTicksPerUnit. This attempts to guarantee millisecond accuracy (but not microsecond or nanosecond) and broadens the range significantly to some 28k years.Replacing the algorithm with purely
x * TicksPerDaywill end up broaching 2^53 much sooner and will cause the fractional part to no longer be considered (resulting in the result being off in various edge cases).A purely correct, tick accurate, approach would be more complex and likely cost too much for perf compared to the current algorithm.
However, splitting it into
integerandfractionalportions will likewise end up broaching2^53much sooner, but will in turn allow for the fractional part to be correctly considered. It likewise maintains millisecond accuracy. This is because, given the upper bound of integer accuracy:9,007,199,254,740,992millisecond accuracy involves ignoring the lowest 4 digits:9,007,199,254,740,000and we have a maximum error of only1024ticks at2^63. -- This is within102.4microseconds which means that we can get up to tick accuracy, but no worse than millisecond accuracy.=====================
As an aside, when considering #66815, we must first consider that
var seconds = 0.9999999;is actuallyseconds = 0.99999990000000005263558477963670156896114349365234375(as that is the nearest representabledoubleto0.9999999), it's not going to impact this scenario much but it is important to consider that values aren't always as "exact" as it might appear and so results may be "off" in other contexts regardless.