dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.94k stars 4.64k forks source link

Math.Round and misunderstanding #38160

Open FrantzUml opened 4 years ago

FrantzUml commented 4 years ago

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

ghost commented 4 years ago

Tagging subscribers to this area: @tannergooding Notify danmosemsft if you want to be subscribed.

SingleAccretion commented 4 years ago

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.

FrantzUml commented 4 years ago

You're right with the breaking change, but the documentation msdn says : the MidpointRounding enumeration does not affect the result of the rounding operation when the next digit is from 0 to 4 and from 6 to 9. However, if the next digit is 5, which is the midpoint between two possible results, and all remaining digits are zero or there are no remaining digits, the nearest number is ambiguous. In this case, the MidpointRounding enumeration enables you to specify whether the rounding operation returns the nearest number away from zero or the nearest even number.

So, the MidpointRounding mode is quiet a "nearest rounding" and not a "directed rounding". For example, the documentation of msdn about ToPositiveInfinity says "When a number is halfway between two others, it is rounded toward the result closest to and no less than the infinitely precise result." But he comments in the source code indicates "Directed rounding: Round up to the next value, toward positive infinity".

Is it a bad design and conception from start ?

SingleAccretion commented 4 years ago

Is it a bad design and conception from start?

I would say it is a confusing design (the name of the enum is indeed MidpointRounding). But given it is a very recent addition to the framework (see here), I suspect the reasoning behind it is discoverability, which was determined to be of higher value than consistency (see the original issue, I suppose). I might be able to dig up an API review for it from YouTube.

However, what seems quite clear is that docs need some improvement. Maybe file a doc bug (or, if you want, create a pull request)?

tannergooding commented 4 years ago

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 MidpointRounding rather than some new DirectedRounding type.

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 Math.Round is notably already buggy, even for the rounding modes that existed in .NET Framework and https://github.com/dotnet/runtime/issues/1643 tracks fixing it. This is also a breaking change.

SingleAccretion commented 4 years ago

Actually, the more I think about these APIs, the more confusing they seem to me (AwayFromZero and ToZero aren't actually related, heh?). I am not a smart person, but even accounting for this, it does take me an unusually high amount of mental effort to understand what "When a number is halfway between two others, it is rounded toward the result closest to and no greater in magnitude than the infinitely precise result" is supposed to mean (this, I guess, can be fixed with better documentation, but still).

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. Some quotes (slightly altered as to suit the professional setting of this issue):

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.

FrantzUml commented 4 years ago

I agree with no breaking change but adding a dedicated enum, explain the bug in MidPointRounding and make it obsolete. So, the enum should be : public enum RoundingMode { ///

/// 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,

/// <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,

/// <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,

/// <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,

/// <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,

/// <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,

/// <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,

/// <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,

/// <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

}

And the proposed implementation : public static double Round(double value, RoundingMode rounding) { 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 if (rounding == RoundingMode.HalfDown) { return System.Math.Ceiling(value - 0.5D); } else if (rounding == RoundingMode.HalfUp) { return System.Math.Floor(value + 0.5D); } else if (rounding == RoundingMode.HalfTowardsZero) { double absolute = System.Math.Ceiling(System.Math.Abs(value) - 0.5D); return (System.Math.Sign(value) > 0) ? absolute : -absolute; } else if (rounding == RoundingMode.HalfAwayZero) { double absolute = System.Math.Floor(System.Math.Abs(value) + 0.5D); return (System.Math.Sign(value) > 0) ? absolute : -absolute; } else { throw new ArgumentException(); } }

Or with using the static ModF function (but i can't verify compliance as the function is internal in Core) : public static unsafe double Round(double value, RoundingMode rounding) { 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(); } } }

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.

    }
SingleAccretion commented 4 years ago

@FrantzUml I think we need to support rounding to the specified number of significant digits (Math.Round(double value, int digits, MidpointRounding mode) overload), not just to digits.

I would open a new issue for the proposal and close this one (to avoid modifying your original post).

FrantzUml commented 4 years ago

I suggest this implementation. Do you think I have to pull a request and close this issue ? (sorry but I'm not a professional developer and not very used to github).

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
}
SingleAccretion commented 4 years ago

Do you think I have to pull a request and close this issue ? (sorry but I'm not a professional developer and not very used to github).

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 api-ready-for-review.

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 up-for-grabs, which means that the team accepts pull requests with implementations for those from the community.

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).

danmoseley commented 4 years ago

@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

```c#

on its own line and end with 3 ticks on their own line. I updated your post above as an example.

SingleAccretion commented 4 years ago

@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.

wischi-chr commented 2 years ago

Also just ran into the problem that ToZero isn't a mid-point rounding strategy.

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 ToEven + Directed) but at least it would be consistant. This would also solve https://github.com/dotnet/runtime/issues/40456

Grimeh commented 1 year ago

Today I ran into the issue of MidpointRounding being a confusing name when trying to determine if there was a way to do a directed AwayFromZero round in the Math API. The improved docs helped a lot, particularly the remarks section for MidpointRounding. I was disappointed to discover there was a directed ToZero but no matching directed AwayFromZero.

I like @SingleAccretion's idea of:

An issue I can see is that having a non-directed actually-midpoint MidpointRounding.ToZero is helpful if we wish to fill out the API for rounding modes, but changing the behaviour of the existing enum value would be undesirable for obvious reasons. Marking the existing ToZero value obsolete and adding a MidpointRounding.TowardsZero may fix that, but the ambiguity/similarity with MidpointRounding.ToZero might be confusing if the user who has used ToZero previously.

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? Please forgive my ignorance, first time contributor :)

Corniel commented 2 months ago

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,
}

Source