24

I have a situation that I cannot change: one database table (table A) accepts 6 decimal places, while a related column in a different table (table B) only has 3 decimal places.

I need to copy from A to B, but if A has more than 3 decimal places the extra data will be lost. I cant change the table definition but I can add a workaround. So I'm trying to find out how to check if a decimal has more than 3 decimal places or not?

eg

Table A
Id, Qty,  Unit(=6dp)
1,  1,     0.00025
2,  4000,  0.00025

Table B
Id, TotalQty(=3dp)

I want to be able to find out if Qty * Unit from Table A has more than 3 decimals (row 1 would fail, row 2 would pass):

if (CountDecimalPlaces(tableA.Qty * tableA.Unit) > 3)
{
    return false;
}
tableB.TotalQty = tableA.Qty * tableA.Unit;

How would I implement the CountDecimalPlaces(decimal value) {} function?

JK.
  • 21,477
  • 35
  • 135
  • 214

13 Answers13

74

You could compare the value of the number rounded to 3 decimal places with the original value.

if (Decimal.Round(valueDecimal, 3) != valueDecimal)
{
   //Too many decimals
}
RodH257
  • 3,552
  • 5
  • 36
  • 46
19

This works for 3 decimal places, and it can be adapted for a generic solution:

static bool LessThan3DecimalPlaces(decimal dec)
{
    decimal value = dec * 1000;
    return value == Math.Floor(value);
}
static void Test()
{
    Console.WriteLine(LessThan3DecimalPlaces(1m * 0.00025m));
    Console.WriteLine(LessThan3DecimalPlaces(4000m * 0.00025m));
}

For a real generic solution, you'll need to "deconstruct" the decimal value in its parts - take a look at Decimal.GetBits for more information.

Update: this is a simple implementation of a generic solution which works for all decimals whose integer part is less than long.MaxValue (you'd need something like a "big integer" for a trully generic function).

static decimal CountDecimalPlaces(decimal dec)
{
    Console.Write("{0}: ", dec);
    int[] bits = Decimal.GetBits(dec);
    ulong lowInt = (uint)bits[0];
    ulong midInt = (uint)bits[1];
    int exponent = (bits[3] & 0x00FF0000) >> 16;
    int result = exponent;
    ulong lowDecimal = lowInt | (midInt << 32);
    while (result > 0 && (lowDecimal % 10) == 0)
    {
        result--;
        lowDecimal /= 10;
    }

    return result;
}

static void Foo()
{
    Console.WriteLine(CountDecimalPlaces(1.6m));
    Console.WriteLine(CountDecimalPlaces(1.600m));
    Console.WriteLine(CountDecimalPlaces(decimal.MaxValue));
    Console.WriteLine(CountDecimalPlaces(1m * 0.00025m));
    Console.WriteLine(CountDecimalPlaces(4000m * 0.00025m));
}
carlosfigueira
  • 85,035
  • 14
  • 131
  • 171
11

This is a very simple one line code to get count of decimals in a Decimal:

decimal myDecimal = 1.000000021300010000001m;
byte decimals = (byte)((Decimal.GetBits(myDecimal)[3] >> 16) & 0x7F);
Jaska
  • 1,412
  • 1
  • 18
  • 39
5

Multiplying a number with 3 decimal places by 10 to the power of 3 will give you a number with no decimal places. It's a whole number when the modulus % 1 == 0. So I came up with this...

bool hasMoreThanNDecimals(decimal d, int n)
{
    return !(d * (decimal)Math.Pow(10, n) % 1 == 0);
}

Returns true when n is less than (not equal) to the number of decimal places.

Phil M
  • 975
  • 1
  • 9
  • 13
3

The basics is to know how to test if there are decimal places, this is done by comparing the value to its rounding

double number;
bool hasDecimals = number == (int) number;

Then, to count 3 decimal places, you just need to do the same for your number multiplied by 1000:

bool hasMoreThan3decimals = number*1000 != (int) (number * 1000)
J.N.
  • 8,203
  • 3
  • 29
  • 39
3

All of the solutions proposed so far are not extensible ... fine if you are never going to check a value other than 3, but I prefer this because if the requirement changes the code to handle it is already written. Also this solution wont overflow.

int GetDecimalCount(decimal val)
{
    if(val == val*10)
    {
        return int.MaxValue; // no decimal.Epsilon I don't use this type enough to know why... this will work
    }

    int decimalCount = 0;
    while(val != Math.Floor(val))
    {
        val = (val - Math.Floor(val)) * 10;
        decimalCount++;
    }
    return decimalCount;
}       
Yaur
  • 7,333
  • 1
  • 25
  • 36
2

carlosfigueira solution will need to check for 0 otherwise "while ((lowDecimal % 10) == 0)" will produce an infinity loop when called with dec = 0

static decimal CountDecimalPlaces(decimal dec)
    {
        if (dec == 0)
            return 0;
        int[] bits = Decimal.GetBits(dec);
        int exponent = bits[3] >> 16;
        int result = exponent;
        long lowDecimal = bits[0] | (bits[1] >> 8);
        while ((lowDecimal % 10) == 0)
        {
            result--;
            lowDecimal /= 10;
        }
        return result;
    }

    Assert.AreEqual(0, DecimalHelper.CountDecimalPlaces(0m));      
    Assert.AreEqual(1, DecimalHelper.CountDecimalPlaces(0.5m));
    Assert.AreEqual(2, DecimalHelper.CountDecimalPlaces(10.51m));
    Assert.AreEqual(13, DecimalHelper.CountDecimalPlaces(10.5123456978563m));
Henrik Stenbæk
  • 3,982
  • 5
  • 31
  • 33
2

One more option based on @RodH257's solution, but reworked as an extension method:

public static bool HasThisManyDecimalPlacesOrLess(this decimal value, int noDecimalPlaces)
{
    return (Decimal.Round(value, noDecimalPlaces) == value);
}

You can then call that as:

If !(tableA.Qty * tableA.Unit).HasThisManyDecimalPlacesOrLess(3)) return;
tomRedox
  • 28,092
  • 24
  • 117
  • 154
  • I was going to answer the same, an extension method is certainly the most re-useful solution. I named the method HasMaxDecimalPlaces() just to simplify "this many or less" to "max" – Richard Jun 07 '20 at 10:39
1
    bool CountDecimalPlaces(decimal input)
    {
        return input*1000.0 == (int) (input*1000);
    }
pickypg
  • 22,034
  • 5
  • 72
  • 84
Bala R
  • 107,317
  • 23
  • 199
  • 210
1

There is probably a more elegant way to do this, but off the top of my head I would try

  1. a = multiply by 1000
  2. b = truncate a
  3. if (b != a) then there is additional precision that has been lost
Jason
  • 86,222
  • 15
  • 131
  • 146
1

Here is my version:

public static int CountDecimalPlaces(decimal dec)
{
    var a = Math.Abs(dec);
    var x = a;
    var count = 1;
    while (x % 1 != 0)
    {
        x = a * new decimal(Math.Pow(10, count++));
    }

    var result = count - 1;

    return result;
}

I tried first @carlosfigueira/@Henrik Stenbæk, but their version does not work with 324000.00m

TEST:

Console.WriteLine(CountDecimalPlaces(0m)); //0
Console.WriteLine(CountDecimalPlaces(0.5m)); //1
Console.WriteLine(CountDecimalPlaces(10.51m)); //2
Console.WriteLine(CountDecimalPlaces(10.5123456978563m)); //13
Console.WriteLine(CountDecimalPlaces(324000.0001m)); //4
Console.WriteLine(CountDecimalPlaces(324000.0000m)); //0
Luis Lavieri
  • 4,064
  • 6
  • 39
  • 69
0
Public Function getDecimalCount(decWork As Decimal) As Integer

    Dim intDecimalCount As Int32 = 0
    Dim strDecAbs As String = decWork.ToString.Trim("0")

    intDecimalCount = strDecAbs.Substring(strDecAbs.IndexOf(".")).Length -1

    Return intDecimalCount

End Function
Steve Czetty
  • 6,147
  • 9
  • 39
  • 48
Ricardo
  • 105
  • 1
  • 11
  • Could you add some explanation of the solution to the code you have there? – jonsca Sep 24 '12 at 12:35
  • I didnt care about extra zeros, so, the decWork.ToString.Trim("0") removes them and .50 becomes .5 strDecAbs.SubstringstrDecAbs.Substring(strDecAbs.IndexOf(".")).Length -1 gives the length of the string from the decimal point and since the point is included I subtract 1. – Ricardo Sep 24 '12 at 15:38
  • 1
    Won't work for all languages unless culture info is specified so be careful – Mikael Holmberg Sep 12 '19 at 14:28
0

could you convert it to a string and just do a len function or would that not cover your situation?

follow up question: would 300.4 be ok?

Mark Hosang
  • 501
  • 5
  • 21