0

I have a C# extension method as follows:

public static double RoundOff(this double rawNumber, double roundToNearest)
{
    double rawMultiples     = rawNumber / roundToNearest;
    double roundedMultiples = Math.Round(rawMultiples);
    double roundedNumber    = roundedMultiples * roundToNearest;
    return roundedNumber;
}

I don't want to write it multiple times for all the different numeric types (int, decimal, etc.)

Is there a way to do it generically, like this?

public static double RoundOff<T>(this T rawNumber, T roundToNearest)
    where T : [SOME CONSTRAINT]
{
    T rawMultiples     = rawNumber / roundToNearest;
    T roundedMultiples = Math.Round(rawMultiples);
    T roundedNumber    = roundedMultiples * roundToNearest;
    return roundedNumber;
}

It would be so so useful to be able to do this more generally. Less code to maintain - and more power from just one extension method!

If it can't be done, is it because C# can't be extended to work in this way? Or might it one day be extended to allow an "all numeric types" generic constraint?

Any ideas welcomed.

UPDATE In response to a challenge about similarity to another question. Yes, it is similar in terms of the subject matter but is different because I am after a specific solution to a specific problem. I have added my own attempted answer below to clarify. Challenges welcome if anyone still thinks I have missed the point.

Bit Racketeer
  • 493
  • 1
  • 3
  • 13
  • 1
    There is no "numeric types" generic constraint. You will have to create overloads for every type you want to support. – Bradley Uffner Feb 07 '19 at 13:09
  • You can't even declare a type parameter constraint that says "this type has to support these maths operators". Some people have fudged around this by re-processing the compiled code. – Damien_The_Unbeliever Feb 07 '19 at 13:10
  • Possible duplicate of [Generic constraint to match numeric types](https://stackoverflow.com/questions/3329576/generic-constraint-to-match-numeric-types) – Sonal Borkar Feb 07 '19 at 13:12
  • You may wish to consider using a T4 template that can spit out this method with specific type names substituted and then use it for the types you need to support – Damien_The_Unbeliever Feb 07 '19 at 13:14
  • Be aware that `Math.Round` uses bankers rounding - just letting you know in case you weren't aware. – mjwills Feb 07 '19 at 13:16
  • 1
    `Math.Round` only has two versions (`Decimal` and `Double`) anyway - so there is no need to do more than that (the `int` and `long` ones should just call through to the `Decimal` one). And your code as is won't work for `int` anyway since `/` does integer division (in other words - you can't use your code in a generic way across numeric types - since it doesn't work for some of the types). – mjwills Feb 07 '19 at 13:19
  • I suppose the point being the extension method that the author want to call, thus all the overloads @mjwills – Ilya Chernomordik Feb 07 '19 at 13:24
  • The point I am making is that **even if** it was possible in the language (which it isn't), that code won't work (i.e. likely isn't doing what the OP wants) for some types (e.g. `int`) @IlyaChernomordik. – mjwills Feb 07 '19 at 13:25
  • @BitRacketeer Have you considered using `dynamic`? – mjwills Feb 07 '19 at 13:30
  • Possible duplicate of [Is there a constraint that restricts my generic method to numeric types?](https://stackoverflow.com/questions/32664/is-there-a-constraint-that-restricts-my-generic-method-to-numeric-types) – Aleks Andreev Feb 07 '19 at 14:15
  • It's not a duplicate as it is about a specific application & a specific solution. – Bit Racketeer Feb 14 '19 at 15:29
  • @mjwills No, how would that look? – Bit Racketeer Feb 14 '19 at 15:30

2 Answers2

1

It is apparently not possible to do exactly what you want with Generics as other commentators say, but since all numbers can be converted to double, you can at least reduce the logic duplication:

public static double RoundOff(this long rawNumber, double roundToNearest)
{
    return RoundOff((double) rawNumber, roundToNearest);
}

public static double RoundOff(this double rawNumber, double roundToNearest)
{
    T rawMultiples     = rawNumber / roundToNearest;
    T roundedMultiples = Math.Round(rawMultiples);
    T roundedNumber    = roundedMultiples * roundToNearest;
    return roundedNumber;
}

If you manage without the extension method, then you can just call this method directly on any numbers since C# will just auto convert all the types to double

int i = 5;
RoundOff(i, 0.5);
Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
1

I accept that this question bears some similarity to others - but my query is a very specific one about solving a very specific problem.

I want to share my own workaround solution here. On the plus side, it's fully generic. On the minus side, it is slow as it uses Decimals but this can be adjusted to faster doubles if required.

    public static T RoundOff<T>(this T rawNumber, T roundToNearest)
        where T : IComparable<T>
    {
        if (typeof(T).IsNumericType())
        {
            decimal decimalRoundToNearest   = Convert.ToDecimal(roundToNearest);
            decimal rawMultiples            = Convert.ToDecimal(rawNumber) / Convert.ToDecimal(roundToNearest);
            decimal decimalRoundedMultiples = Math.Round(rawMultiples);
            decimal decimalRoundedNumber    = decimalRoundedMultiples * decimalRoundToNearest;

            return (T)Convert.ChangeType(decimalRoundedNumber, typeof(T));
            // alternative
            // TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));
            // return (T)converter.ConvertFrom(decimalRoundedNumber);

        }
        else
        {
            throw new Exception("Type " + typeof(T) + " is not numeric");
        }
    }

And this extension method with logic borrowed from the cited answer

    public static bool IsNumericType(this object o)
    {
        // https://stackoverflow.com/questions/1749966/c-sharp-how-to-determine-whether-a-type-is-a-number
        switch (Type.GetTypeCode(o.GetType()))
        {
            case TypeCode.Byte:
            case TypeCode.SByte:
            case TypeCode.UInt16:
            case TypeCode.UInt32:
            case TypeCode.UInt64:
            case TypeCode.Int16:
            case TypeCode.Int32:
            case TypeCode.Int64:
            case TypeCode.Decimal:
            case TypeCode.Double:
            case TypeCode.Single:
                return true;

            default:
                return false;
        }
    }

It's not without its weaknesses, but it does do what I'm after.

One self-criticism: because we're rounding-off, the extra precision of decimals could be unnecessary - but intuitively it seems better to use a high-precision type. I could be wrong in this and maybe someone more knowledgeable can shed some insight.

So I hope this either helps someone or provokes an avalanche of angry responses as to how it can be done better!!

Bit Racketeer
  • 493
  • 1
  • 3
  • 13