-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Math.Round and misunderstanding #38160
Comments
Tagging subscribers to this area: @tannergooding |
Even if unintuitive (which I agree it is, and the documentation does not help either), this behavior cannot be changed as it would be the worst kind of a breaking change (silent change of runtime behavior). The bar for these changes is incredibly high. |
You're right with the breaking change, but the documentation msdn says : So, the MidpointRounding mode is quiet a "nearest rounding" and not a "directed rounding". Is it a bad design and conception from start ? |
I would say it is a confusing design (the name of the enum is indeed However, what seems quite clear is that docs need some improvement. Maybe file a doc bug (or, if you want, create a pull request)? |
The APIs were originally added to cover some missing rounding modes from the IEEE 754 specification. I don't recall the exact reasoning why we decided to add them to At a minimum the docs should be updated to call out this difference. Fixing it would be a breaking change and would require further consideration. The algorithm for |
Actually, the more I think about these APIs, the more confusing they seem to me ( At this point, I would be in favor of obsoleting these enum members (and EB-nevering them, as there would presumably be a 1-1 replacement, but I know this has drawbacks and so people aren't enthusiastic about it) and actually adding a dedicated enum. This would only be source-breaking, so it might actually be actionable (and, presumably, not many people are actually using these APIs yet so the impact would be limited). @FrantzUml Do you feel strongly enough about this to edit the original issue into a dedicated obsoletion proposal or open another one and close this one? You would have my vote. Edit: the API review.
The point of it not really being a "midpoint rounding" was brought up, but nobody had strong opinions about the alternatives - it seems like the consensus was that it is fairly advanced API and you will have to read the docs anyway, so it was approved. |
I agree with no breaking change but adding a dedicated enum, explain the bug in MidPointRounding and make it obsolete. /// Directed round down (same as Floor). /// /// /// Assert.AreEqual(-2D, -1.8D); /// Assert.AreEqual(-2D, -1.5D); /// Assert.AreEqual(-2D, -1.2D); /// Assert.AreEqual(1D, +1.2D); /// Assert.AreEqual(1D, +1.5D); /// Assert.AreEqual(1D, +1.8D); /// Down,
} And the proposed implementation : Or with using the static ModF function (but i can't verify compliance as the function is internal in Core) : I will suggest implementation with "int digits" as argument, but I don't like the implmentation in Core because multiply value by 10^digits may overflow or create less accuracy.
|
@FrantzUml I think we need to support rounding to the specified number of significant digits ( I would open a new issue for the proposal and close this one (to avoid modifying your original post). |
I suggest this implementation. public static unsafe double Round(double value, int digits, RoundingMode rounding)
{
if ((digits < 0) || (digits > kMaxDoubleDigits))
{
throw new ArgumentException();
}
if (digits == 0)
{
return Round(value, rounding);
}
double fraction = ModF(value, &value);
double power10 = roundPower10Double[digits];
fraction *= power10;
return value + Round(fraction, rounding) / power10;
}
public static unsafe double Round(double value, RoundingMode rounding)
{
if (rounding < RoundingMode.HalfToEven || rounding > RoundingMode.HalfTowardsZero)
{
throw new ArgumentException();
}
if (System.Math.Abs(value) < kRoundDoubleLimit)
{
if (rounding == RoundingMode.Down)
{
return System.Math.Floor(value);
}
else if (rounding == RoundingMode.Up)
{
return System.Math.Ceiling(value);
}
else if (rounding == RoundingMode.TowardsZero)
{
return System.Math.Truncate(value);
}
else if (rounding == RoundingMode.AwayZero)
{
return System.Math.Sign(value) < 0 ? System.Math.Floor(value) : System.Math.Ceiling(value);
}
else if (rounding == RoundingMode.HalfToEven)
{
return System.Math.Round(value);
}
else
{
double fraction = ModF(value, &value);
double absoluteFrac = System.Math.Abs(fraction);
if (absoluteFrac < 0.5D)
{
return value;
}
int signFrac = System.Math.Sign(fraction);
switch (rounding)
{
case RoundingMode.HalfAwayZero:
return value + signFrac;
case RoundingMode.HalfDown:
return (signFrac < 0) ? value + signFrac : (absoluteFrac > 0.5D) ? value + signFrac : value;
case RoundingMode.HalfUp:
return (signFrac > 0) ? value + signFrac : (absoluteFrac > 0.5D) ? value + signFrac : value;
case RoundingMode.HalfTowardsZero:
return (absoluteFrac > 0.5D) ? value + signFrac : value;
default:
throw new ArgumentException();
}
}
}
return value;
}
`
public enum RoundingMode
{
/// <summary>`
`/// Directed round down (same as Floor).`
`/// </summary>`
`/// <remarks>`
`/// <code>Assert.AreEqual(-2D, -1.8D);</code>`
`/// <code>Assert.AreEqual(-2D, -1.5D);</code>`
`/// <code>Assert.AreEqual(-2D, -1.2D);</code>`
`/// <code>Assert.AreEqual(1D, +1.2D);</code>`
`/// <code>Assert.AreEqual(1D, +1.5D);</code>`
`/// <code>Assert.AreEqual(1D, +1.8D);</code>`
`/// </remarks>`
Down = 3,
/// <summary>
/// Directed round up (same as Ceiling).
/// </summary>
/// <remarks>
/// <code>Assert.AreEqual(-1D, -1.8D);</code>
/// <code>Assert.AreEqual(-1D, -1.5D);</code>
/// <code>Assert.AreEqual(-1D, -1.2D);</code>
/// <code>Assert.AreEqual(2D, +1.2D);</code>
/// <code>Assert.AreEqual(2D, +1.5D);</code>
/// <code>Assert.AreEqual(2D, +1.8D);</code>
/// </remarks>
Up = 4,
/// <summary>
/// Directed round towards zero (same as Truncate).
/// </summary>
/// <remarks>
/// <code>Assert.AreEqual(-1D, -1.8D);</code>
/// <code>Assert.AreEqual(-1D, -1.5D);</code>
/// <code>Assert.AreEqual(-1D, -1.2D);</code>
/// <code>Assert.AreEqual(1D, +1.2D);</code>
/// <code>Assert.AreEqual(1D, +1.5D);</code>
/// <code>Assert.AreEqual(1D, +1.8D);</code>
/// </remarks>
TowardsZero = 2,
/// <summary>
/// Directed round away from zero
/// </summary>
/// <remarks>
/// <code>Assert.AreEqual(-2D, -1.8D);</code>
/// <code>Assert.AreEqual(-2D, -1.5D);</code>
/// <code>Assert.AreEqual(-2D, -1.2D);</code>
/// <code>Assert.AreEqual(2D, +1.2D);</code>
/// <code>Assert.AreEqual(2D, +1.5D);</code>
/// <code>Assert.AreEqual(2D, +1.8D);</code>
/// </remarks>
AwayZero = 5,
/// <summary>
/// Round to nearest integer and down when the number is halfway between two others.
/// </summary>
/// <remarks>
/// <code>Assert.AreEqual(-2D, -1.8D);</code>
/// <code>Assert.AreEqual(-2D, -1.5D);</code>
/// <code>Assert.AreEqual(-1D, -1.2D);</code>
/// <code>Assert.AreEqual(1D, +1.2D);</code>
/// <code>Assert.AreEqual(1D, +1.5D);</code>
/// <code>Assert.AreEqual(2D, +1.8D);</code>
/// </remarks>
HalfDown = 6,
/// <summary>
/// Round to nearest integer and up when the number is halfway between two others.
/// </summary>
/// <remarks>
/// <code>Assert.AreEqual(-2D, -1.8D);</code>
/// <code>Assert.AreEqual(-1D, -1.5D);</code>
/// <code>Assert.AreEqual(-1D, -1.2D);</code>
/// <code>Assert.AreEqual(1D, +1.2D);</code>
/// <code>Assert.AreEqual(2D, +1.5D);</code>
/// <code>Assert.AreEqual(2D, +1.8D);</code>
/// </remarks>
HalfUp = 7,
/// <summary>
/// Round to nearest integer and towards zero when the number is halfway between two others.
/// </summary>
/// <remarks>
/// <code>Assert.AreEqual(-2D, -1.8D);</code>
/// <code>Assert.AreEqual(-1D, -1.5D);</code>
/// <code>Assert.AreEqual(-1D, -1.2D);</code>
/// <code>Assert.AreEqual(1D, +1.2D);</code>
/// <code>Assert.AreEqual(1D, +1.5D);</code>
/// <code>Assert.AreEqual(2D, +1.8D);</code>
/// </remarks>
HalfTowardsZero = 8,
/// <summary>
/// Round to nearest integer and away from zero when the number is halfway between two others.
/// </summary>
/// <remarks>
/// <code>Assert.AreEqual(-2D, -1.8D);</code>
/// <code>Assert.AreEqual(-2D, -1.5D);</code>
/// <code>Assert.AreEqual(-1D, -1.2D);</code>
/// <code>Assert.AreEqual(1D, +1.2D);</code>
/// <code>Assert.AreEqual(2D, +1.5D);</code>
/// <code>Assert.AreEqual(2D, +1.8D);</code>
/// </remarks>
HalfAwayZero = 1,
/// <summary>
/// Round to nearest integer and to nearest even integer when the number is halfway between two others.
/// </summary>
/// <remarks>
/// <code>Assert.AreEqual(-2D, -1.8D);</code>
/// <code>Assert.AreEqual(-2D, -1.5D);</code>
/// <code>Assert.AreEqual(-1D, -1.2D);</code>
/// <code>Assert.AreEqual(0D, -0.5D);</code>
/// <code>Assert.AreEqual(0D, +0.5D);</code>
/// <code>Assert.AreEqual(1D, +1.2D);</code>
/// <code>Assert.AreEqual(2D, +1.5D);</code>
/// <code>Assert.AreEqual(2D, +1.8D);</code>
/// </remarks>
HalfToEven = 0
} |
So, this is the process for API changes like this one in the runtime repository: 1. Someone creates an API proposal, that has:
#13933 is often quoted as a good template for a proposal. 2. Someone from the team thinks it would be worth implementing and marks the proposal 3. The proposal gets reviewed (you can watch reviews live on .NET Foundation's YouTube channel). When this happens depends on what status did the proposal get:
The team reviews backlog in a chronological order (so, older issues get reviewed first), except when a dedicated review session (or many of them) is needed for something big (recent examples: #34742, #1793). You can see issues that are about to get reviewed via http://aka.ms/ready-for-api-review) 4. The team can decide that the proposal:
5. After approval, the proposal must be implemented (if it hasn't been already). Easier issues can be marked as In view of all this:
I would suggest you create two new issues for each, formatted as proper proposals, with a link to this issue. I would also highly encourage you to create an issue in the docs repo (https://github.com/dotnet/docs) with a request to improve the documentation on the new rounding modes. You can improve the docs yourself by creating a PR there, if you feel that you can add the missing bits (I would do it myself, but unfortunately I do not have the time for it right now). |
@SingleAccretion that is a great summary I wonder whether it would be worth a PR to improve https://github.com/dotnet/runtime/blob/master/docs/project/api-review-process.md? Also note we now have a template: https://github.com/dotnet/runtime/issues/new?assignees=&labels=api-suggestion&template=02_api_proposal.md&title= @FrantzUml you can format code nicely by using three back ticks. So you start with
on its own line and end with 3 ticks on their own line. I updated your post above as an example. |
@danmosemsft I think the document is great, the only things I would add are these links:
Edit: and a link to the template, heh. I am not quite sure yet how to properly fit these into the structure (inline/separate section) and it is approaching midnight here so I'll leave it to my tomorrow's self to figure out. |
Also just ran into the problem that For what it's worth IMHO an API design like the following would handle all cases and would also be perfectly clear for the developers and consistent. public enum RoundingDirection
{
ToEven,
ToOdd,
ToZero,
AwayFromZero,
ToPositiveInfinity,
ToNegativeInfinity,
}
public enum RoundingStrategy
{
ToNearest,
Directed,
}
public static class Math
{
public static double Round
(
double value,
int digits,
RoundindDirection direction = RoundindDirection.ToEven,
RoundingStrategy strategy = RoundingStrategy.ToNearest
)
{
// implementation left as an exercise for the reader
throw new NotImplementedException();
}
} Of course there would be combinations that are probably very rare (like |
Today I ran into the issue of I like @SingleAccretion's idea of:
An issue I can see is that having a non-directed actually-midpoint It also adds two more overloads to a method that already has a few, not sure if that's a concern or not. I'm happy to make a proposal, should I bring up the above concerns in a new API proposal issue under the "Risks" section (as detailed here), or discuss them here before making a new issue? |
For a open source project, I maintain, I once implemented an extended version of rounding (only for decimals, but that should not matter). I extended the support methods of decimal rounding to 13 (see list below), and also allow a negative amount of decimals to round to, to be specified, meaning to round to a multiple of 10, 100, etc... Might that help fixing this API? /// <summary>Methods of rounding <see cref="decimal"/>s.</summary>
/// <remarks>
/// This is an extension on <see cref="MidpointRounding"/>.
/// </remarks>
public enum DecimalRounding
{
/// <summary>When a number is halfway between two others, it is rounded toward the nearest even.</summary>
ToEven = 0,
/// <summary>Bankers round, also known as <see cref="ToEven"/>.</summary>
BankersRound = ToEven,
/// <summary>When a number is halfway between two others, it is rounded toward the nearest number that is away from zero.</summary>
AwayFromZero = 1,
/// <summary>When a number is halfway between two others, it is rounded toward the nearest odd.</summary>
ToOdd = 2,
/// <summary>When a number is halfway between two others, it is rounded toward the nearest number that is closest to zero.</summary>
TowardsZero = 3,
/// <summary>When a number is halfway between two others, it is rounded toward the highest of the two.</summary>
Up = 4,
/// <summary>When a number is halfway between two others, it is rounded toward the lowest of the two.</summary>
Down = 5,
/// <summary>When a number is halfway between two others, it is randomly rounded up or down with equal probability.</summary>
RandomTieBreaking = 6,
/// <summary>When a number is between two others, the remainder is truncated/ignored.</summary>
Truncate = 7,
/// <summary>When a number is between two others, it is rounded toward the nearest number that is away from zero.</summary>
DirectAwayFromZero = 8,
/// <summary>When a number is between two others, it is rounded toward the nearest number that is closest to zero.</summary>
DirectTowardsZero = 9,
/// <summary>When a number is between two others, its rounded to the largest.</summary>
Ceiling = 10,
/// <summary>When a number is between two others, its rounded to the smallest.</summary>
Floor = 11,
/// <summary>When a number is between two others, it is randomly rounded up or down with stochastic probability.</summary>
StochasticRounding = 12,
} |
Math.Round with rounding type
The MidpointRounding enum is a "rounding to nearest integer" and not a "directed rounding".
So, for example, the MidpointRounding.ToZero description says : "when a number is halfway between two others......."
So, System.Math.Round(-1.8D, any type of MidpointRounding) should return -2 because it's the nearest integer value of -1.8.
But, with ToPositiveInfinity and ToZero, the result is badly -1
It's a misunderstanding of the rounding math theory.
The result should be different only when the value to round is halfway between two others.
Watching the code for Core 3.1, the developpers make select between the MidpointRounding mode even when the value to round is not halfway.
In definitive, the System.Math is buggy.
Cordially
The text was updated successfully, but these errors were encountered: