117

I'm wondering if there is a concise and accurate way to pull out the number of decimal places in a decimal value (as an int) that will be safe to use across different culture info?

For example:
19.0 should return 1,
27.5999 should return 4,
19.12 should return 2,
etc.

I wrote a query that did a string split on a period to find decimal places:

int priceDecimalPlaces = price.ToString().Split('.').Count() > 1 
                  ? price.ToString().Split('.').ToList().ElementAt(1).Length 
                  : 0;

But it occurs to me that this will only work in regions that use the '.' as a decimal separator and is therefore very brittle across different systems.

Austin Salonen
  • 49,173
  • 15
  • 109
  • 139
Jesse Carter
  • 20,062
  • 7
  • 64
  • 101
  • A decimal as per the question title – Jesse Carter Nov 20 '12 at 16:34
  • How about some pattern matching prior to Split ?. Basically \d+(\D)\d+ where \D returns the separator (. , etc) – Anshul Nov 20 '12 at 16:35
  • 8
    This is not a closed-ended question as it may at first blush appear. Asking `19.0` to return `1` is an *implementation detail* regarding the internal storage of the value `19.0`. The fact is that it is perfectly legitimate for the program to store this as `190×10⁻¹` or `1900×10⁻²` or `19000×10⁻³`. All of those are equal. The fact that it uses the first representation when given a value of `19.0M` and this is exposed when using `ToString` without a format specifier is just a coincidence, and a happy-ish thing. Except it's not happy when people rely on the exponent in cases where they shouldn't. – ErikE Aug 21 '15 at 22:20
  • If you want a type that can carry "number of decimal places used" when it is created, so that you can reliably distinguish `19M` from `19.0M` from `19.00M`, you'll need to create a new class that bundles the underlying value as one property and the number of decimal places as another property. – ErikE Aug 21 '15 at 22:22
  • 1
    Even though the Decimal class can "distinguish" 19m, from 19.0m from 19.00m? Significant digits are like one of its major use cases. What is 19.0m * 1.0m? Seems to be saying 19.00m, maybe the C# devs are doing maths wrong though :P ? Again significant digits are a real thing. If you don't like significant digits, you should probably not be using the Decimal class. – Nicholi Aug 22 '15 at 01:16
  • What should `whatever(654.32100m)` return? – Solomon Ucko Feb 11 '20 at 12:29

20 Answers20

199

I used Joe's way to solve this issue :)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
Community
  • 1
  • 1
burning_LEGION
  • 13,246
  • 8
  • 40
  • 52
  • 2
    Recently I ran into an issue with this solution. The problem is that it also counts trailing zeros. E.g. for var argument = 123.4560m; the result would be 4. – raznagul Dec 18 '13 at 11:28
  • 11
    `decimal` keeps count digit after coma, that's why you find this "issue", you have to cast decimal to double and to decimal again for fix: BitConverter.GetBytes(decimal.GetBits((decimal)(double)argument)[3])[2]; – burning_LEGION Dec 18 '13 at 12:23
  • 3
    This didn't work for me. The value coming back from SQL is 21.17 it's saying 4 digits. The data-type is defined as DECIMAL(12,4) so perhaps that's it (using Entity Framework). – PeterX Jun 18 '14 at 07:31
  • 2
    @raznagul - I would suggest that for 123.4560 that 4 is the correct result because the number is accurate to 4 dp. The number 123.456 to 4dps could be 123.4558 for example. – Lee Oades Sep 02 '14 at 13:45
  • 1
    @m.edmondson Did you notice that (Decimal)0.01f actually creates a decimal with 3 digits though? Console.WriteLine((Decimal)0.01f); This is actually a very GOOD solution because GetBits is very well defined in what it returns. Microsoft cannot just change the documentation of GetBits on a whim, that would be breaking the interface they have defined and what it returns. That's not an implementation change...that is literally an interface change. Even IF there were a change to it in the future it will be likely part of a very WELL DEFINED breaking change for a MAJOR update. – Nicholi Aug 20 '15 at 23:32
  • 16
    @Nicholi - No, this is **exceptionally bad** because the method is relying on the placement of the **underlying bits of the decimal** - something which has _many ways_ to represent the _same number_. You wouldn't test a class based on the state of it's private fields would you? – m.edmondson Aug 21 '15 at 08:49
  • 1
    Agree, this is a horrible way to do it because 10×10⁻³ = 1×10⁻². Same number, two different representations. `GetBits` *answers the wrong question*, which only coincidentally is the right answer most of the time. – ErikE Aug 21 '15 at 22:12
  • 3
    If the question is "how many digits are in a Decimal object" GetBits provides that solution. And again IF the underlying representation of a Decimal were to change, the implementation of GetBits would have to change because it has a defined and documented return value. (Decimal)0.01f returns 3 digits BECAUSE IT IS A DECIMAL OBJECT WITH THREE DIGITS. If the question were "how many digits are in a double/float", then yeah casting to a Decimal and using GetBits may not result in the answer you want. Since the conversion/cast from double/float is going to be imprecise. – Nicholi Aug 22 '15 at 00:49
  • 1
    Also a very important use of the Decimal class is what is known as significant digits (I suggest you read up on them). Whereas you may feel 0.01 and 0.010 are exactly the same, not everyone else will. Which is a large purpose of the Decimal class. Would you say the literal value 0.010m has 2 or 3 digits? – Nicholi Aug 22 '15 at 00:50
  • 19
    Not sure what's supposed to be elegant or nice about this. This is about as obfuscated as it gets. Who knows whether it even works in all cases. Impossible to make sure. – usr Dec 27 '15 at 15:41
  • ***Is valid for float and double ?*** `var d10 = 54321.98M;` `var f10 = 54321.98f;` `var double10 = 54321.98;` – Kiquenet Feb 22 '17 at 13:19
  • @Kiquenet, no, is not, decimal only – burning_LEGION Feb 22 '17 at 13:25
  • 1
    Whilst this method is ingenious, it doesn't work as expected. It thinks a decimal initialised with value 1234.5600M has 4dp. See https://dotnetfiddle.net/FsP49s – Rob Jul 05 '19 at 14:30
44

Since none of the answers supplied were good enough for the magic number "-0.01f" converted to decimal.. i.e: GetDecimal((decimal)-0.01f);
I can only assume a colossal mind-fart virus attacked everyone 3 years ago :)
Here is what seems to be a working implementation to this evil and monstrous problem, the very complicated problem of counting the decimal places after the point - no strings, no cultures, no need to count the bits and no need to read math forums.. just simple 3rd grade math.

public static class MathDecimals
{
    public static int GetDecimalPlaces(decimal n)
    {
        n = Math.Abs(n); //make sure it is positive.
        n -= (int)n;     //remove the integer part of the number.
        var decimalPlaces = 0;
        while (n > 0)
        {
            decimalPlaces++;
            n *= 10;
            n -= (int)n;
        }
        return decimalPlaces;
    }
}

private static void Main(string[] args)
{
    Console.WriteLine(1/3m); //this is 0.3333333333333333333333333333
    Console.WriteLine(1/3f); //this is 0.3333333

    Console.WriteLine(MathDecimals.GetDecimalPlaces(0.0m));                  //0
    Console.WriteLine(MathDecimals.GetDecimalPlaces(1/3m));                  //28
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)(1 / 3f)));     //7
    Console.WriteLine(MathDecimals.GetDecimalPlaces(-1.123m));               //3
    Console.WriteLine(MathDecimals.GetDecimalPlaces(43.12345m));             //5
    Console.WriteLine(MathDecimals.GetDecimalPlaces(0));                     //0
    Console.WriteLine(MathDecimals.GetDecimalPlaces(0.01m));                 //2
    Console.WriteLine(MathDecimals.GetDecimalPlaces(-0.001m));               //3
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.00000001f)); //8
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.0001234f));   //7
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.01f));        //2
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.01f));       //2
}
G.Y
  • 6,042
  • 2
  • 37
  • 54
  • 6
    Your solution will fail for a number of cases which contain trailing zeros and the digits are SIGNIFICANT. 0.01m * 2.0m = 0.020m. Should be 3 digits, your method returns 2. You seem to be incorrectly understanding what happens when you cast 0.01f to Decimal. Floating points are inherently not precise, so the actual binary value stored for 0.01f is not exact. When you cast to Decimal (a very structured number notation) you might not get 0.01m (you actually get 0.010m). The GetBits solution is actually correct for getting the number of digits from a Decimal. How you convert to Decimal is key. – Nicholi Aug 21 '15 at 00:18
  • 4
    @Nicholi 0.020m is equal to 0.02m.. trailing zeros are not significant. OP is asking "regardless of culture" in the title and even more specific explains "..that will be safe to use across different culture info.." - therefore I think that my answer remains even more valid than others. – G.Y Sep 05 '15 at 14:18
  • 7
    OP said specifically: "19.0 should return 1". This code fails on that case. – daniloquio Jan 14 '16 at 22:27
  • 14
    maybe this is not what the OP wanted, but this answer better suits my needs than the top answer of this question – Arsen Zahray May 11 '17 at 11:17
  • 6
    The first two lines should be replaced with `n = n % 1; if (n < 0) n = -n;` because a value larger than `int.MaxValue` will cause an `OverflowException`, e.g. `2147483648.12345`. – Loathing Sep 21 '17 at 16:18
  • 1
    From what I am able to understand of the question, it sounds like instead of returning 0 for a whole number they want the minimum value to be 1. So I would swap out the return for this `return Math.Max(1,decimalPlaces)` If they wanted 0.0 to return 0 that could be an extra condition. – Pete May 24 '18 at 21:36
26

I'd probably use the solution in @fixagon's answer.

However, while the Decimal struct doesn't have a method to get the number of decimals, you could call Decimal.GetBits to extract the binary representation, then use the integer value and scale to compute the number of decimals.

This would probably be faster than formatting as a string, though you'd have to be processing an awful lot of decimals to notice the difference.

I'll leave the implementation as an exercise.

Community
  • 1
  • 1
Joe
  • 122,218
  • 32
  • 205
  • 338
20

One of the best solutions for finding the number of digits after the decimal point is shown in burning_LEGION's post.

Here I am using parts from a STSdb forum article: Number of digits after decimal point.

In MSDN we can read the following explanation:

"A decimal number is a floating-point value that consists of a sign, a numeric value where each digit in the value ranges from 0 to 9, and a scaling factor that indicates the position of a floating decimal point that separates the integral and fractional parts of the numeric value."

And also:

"The binary representation of a Decimal value consists of a 1-bit sign, a 96-bit integer number, and a scaling factor used to divide the 96-bit integer and specify what portion of it is a decimal fraction. The scaling factor is implicitly the number 10, raised to an exponent ranging from 0 to 28."

On internal level the decimal value is represented by four integer values.

Decimal internal representation

There is a publicly available GetBits function for getting the internal representation. The function returns an int[] array:

[__DynamicallyInvokable] 
public static int[] GetBits(decimal d)
{
    return new int[] { d.lo, d.mid, d.hi, d.flags };
}

The fourth element of the returned array contains a scale factor and a sign. And as the MSDN says the scaling factor is implicitly the number 10, raised to an exponent ranging from 0 to 28. This is exactly what we need.

Thus, based on all above investigations we can construct our method:

private const int SIGN_MASK = ~Int32.MinValue;

public static int GetDigits4(decimal value)
{
    return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16;
}

Here a SIGN_MASK is used to ignore the sign. After logical and we have also shifted the result with 16 bits to the right to receive the actual scale factor. This value, finally, indicates the number of digits after the decimal point.

Note that here MSDN also says the scaling factor also preserves any trailing zeros in a Decimal number. Trailing zeros do not affect the value of a Decimal number in arithmetic or comparison operations. However, trailing zeros might be revealed by the ToString method if an appropriate format string is applied.

This solutions looks like the best one, but wait, there is more. By accessing private methods in C# we can use expressions to build a direct access to the flags field and avoid constructing the int array:

public delegate int GetDigitsDelegate(ref Decimal value);

public class DecimalHelper
{
    public static readonly DecimalHelper Instance = new DecimalHelper();

    public readonly GetDigitsDelegate GetDigits;
    public readonly Expression<GetDigitsDelegate> GetDigitsLambda;

    public DecimalHelper()
    {
        GetDigitsLambda = CreateGetDigitsMethod();
        GetDigits = GetDigitsLambda.Compile();
    }

    private Expression<GetDigitsDelegate> CreateGetDigitsMethod()
    {
        var value = Expression.Parameter(typeof(Decimal).MakeByRefType(), "value");

        var digits = Expression.RightShift(
            Expression.And(Expression.Field(value, "flags"), Expression.Constant(~Int32.MinValue, typeof(int))), 
            Expression.Constant(16, typeof(int)));

        //return (value.flags & ~Int32.MinValue) >> 16

        return Expression.Lambda<GetDigitsDelegate>(digits, value);
    }
}

This compiled code is assigned to the GetDigits field. Note that the function receives the decimal value as ref, so no actual copying is performed - only a reference to the value. Using the GetDigits function from the DecimalHelper is easy:

decimal value = 3.14159m;
int digits = DecimalHelper.Instance.GetDigits(ref value);

This is the fastest possible method for getting number of digits after decimal point for decimal values.

Community
  • 1
  • 1
Kris Dimitrov
  • 460
  • 4
  • 9
  • 3
    decimal r = (decimal)-0.01f; and solution fails. (on all answers I seen in this page...) :) – G.Y May 13 '15 at 02:30
  • 6
    NOTE: About the whole (Decimal)0.01f thing, you are casting a floating point, inherently NOT PRECISE, to something very structured like a Decimal. Take a look at the output of Console.WriteLine((Decimal)0.01f). The Decimal being formed in the cast ACTUALLY has 3 digits, that's why all the solutions provided say 3 instead of 2. Everything is actually working as expected, the "problem" is you are expecting floating point values to be exact. They are not. – Nicholi Aug 20 '15 at 23:24
  • @Nicholi Your point fails when you realize that `0.01` and `0.010` are exactly equal **numbers**. Furthermore, the idea that a **numeric** data type has some kind of "number of digits used" semantic that can be relied on is completely mistaken (not to be confused with "number of digits allowed". Don't confuse presentation (the display of a number's value in a particular base, for example, the decimal expansion of the value indicated by the binary expansion 111) with the underlying value! To reiterate, **numbers are not digits, nor are they made up of digits**. – ErikE Aug 21 '15 at 22:01
  • The correct way to think about it is that **numbers can be represented by the digits assigned to represent a particular base**. (That digits are numeric is only happenstance, for example in hexadecimal the digits `A` through `F` are not numeric.) So I would have to absolutely and categorically say that these are not working as expected, because the `GetBits` techniques *ask the wrong question*, namely "what is the exponent of the number's current representation", which is only coincidentally the right answer in most cases. (continued) – ErikE Aug 21 '15 at 22:04
  • The right question is "what is the position of the last nonzero digit of the fractional portion of the number"? This question can't reliably be answered by `GetBits` since 10×10⁻³ = 1×10⁻². Same number, two different representations, getting the exponent of this *answers the wrong question*. – ErikE Aug 21 '15 at 22:11
  • 7
    They are equivalent in value, but not in significant digits. Which is a large use case of the Decimal class. If I asked how many digits are in the literal 0.010m, would you say only 2? Even though scores of math/science teachers around the globe would tell you the final 0 is significant? The problem we are referring to is manifested by casting from floating points to Decimal. Not the usage of GetBits itself, which is doing exactly as it is documented. If you don't care about significant digits, then yeah you have a problem and likely should not be using the Decimal class in the first place. – Nicholi Aug 22 '15 at 00:59
  • @kris why `private const int SIGN_MASK = ~Int32.MinValue` instead of simpler `private const int SIGN_MASK = Int32.MaxValue`? Is there any catch I don't see? – the berserker Nov 27 '19 at 11:55
  • 1
    @theberserker As far as I remember, there was no catch - it should work both ways. – Kris Dimitrov Nov 28 '19 at 13:49
20

Relying on the internal representation of decimals is not cool.

How about this:

    int CountDecimalDigits(decimal n)
    {
        return n.ToString(System.Globalization.CultureInfo.InvariantCulture)
                //.TrimEnd('0') uncomment if you don't want to count trailing zeroes
                .SkipWhile(c => c != '.')
                .Skip(1)
                .Count();
    }
Clement
  • 3,990
  • 4
  • 43
  • 44
11

you can use the InvariantCulture

string priceSameInAllCultures = price.ToString(System.Globalization.CultureInfo.InvariantCulture);

another possibility would be to do something like that:

private int GetDecimals(decimal d, int i = 0)
{
    decimal multiplied = (decimal)((double)d * Math.Pow(10, i));
    if (Math.Round(multiplied) == multiplied)
        return i;
    return GetDecimals(d, i+1);
}
fixagon
  • 5,506
  • 22
  • 26
  • How does this help me find the number of decimal places in the decimal? I have no problem converting the decimal to a string that is good in any culture. As per the question I am trying to find the number of decimal places that were on the decimal – Jesse Carter Nov 20 '12 at 16:35
  • @JesseCarter: It means you can always split on `.`. – Austin Salonen Nov 20 '12 at 16:36
  • @AustinSalonen Really? I wasn't aware that using InvariantCulture would enforce the use of a period as the decimal separator – Jesse Carter Nov 20 '12 at 16:37
  • as you did before, it will always cast the price to string with a . as decimal separator. but its not the most elegant way in my opinion... – fixagon Nov 20 '12 at 16:37
  • @JesseCarter: [NumberFormatInfo.NumberDecimalSeparator](http://msdn.microsoft.com/en-us/library/system.globalization.numberformatinfo.numberdecimalseparator.aspx) – Austin Salonen Nov 20 '12 at 16:39
  • Hmmmm thats good to know I might implement it this way. I was hoping that there might be some kind of method exposed by Decimal to just retrieve number of decimal places that didn't rely on strings at all – Jesse Carter Nov 20 '12 at 16:39
10

Most people here seem to be unaware that decimal considers trailing zeroes as significant for storage and printing.

So while 0.1m, 0.10m and 0.100m may compare as equal, they are stored differently (as value/scale 1/1, 10/2 and 100/3, respectively), and will be printed as 0.1, 0.10 and 0.100, respectively, by ToString().

As such, the solutions that report "too high a precision" are actually reporting the correct precision, on decimal's terms.

In addition, math-based solutions (like multiplying by powers of 10) will likely be very slow (decimal is ~40x slower than double for arithmetic, and you don't want to mix in floating-point either because that's likely to introduce imprecision). Similarly, casting to int or long as a means of truncating is error-prone (decimal has a much greater range than either of those - it's based around a 96-bit integer).

While not elegant as such, the following will likely be one of the fastest way to get the precision (when defined as "decimal places excluding trailing zeroes"):

public static byte PrecisionOf(decimal d) {
  var text = d.ToString(System.Globalization.CultureInfo.InvariantCulture).TrimEnd('0');
  var decpoint = text.IndexOf('.');
  if (decpoint < 0)
    return 0;
  return text.Length - decpoint - 1;
}

The invariant culture guarantees a '.' as decimal point, trailing zeroes are trimmed, and then it's just a matter of seeing of how many positions remain after the decimal point (if there even is one).

Zastai
  • 1,115
  • 14
  • 23
  • 1
    I agree that iterative and calculation based solutions are slow (besides not accounting for trailing zeros). However, allocating a string for this and operating on that instead is also not the most performant thing to do, especially in performance critical contexts and with a slow GC. Accessing the scale via pointer logic is a good deal faster and allocation free. – Martin Tilo Schmitz Feb 05 '19 at 14:44
  • Yes, getting the _scale_ can be done much more efficiently - but that would include the trailing zeroes. And removing them requires doing arithmetic on the integer part. – Zastai Feb 14 '20 at 09:25
6

And here's another way, use the type SqlDecimal which has a scale property with the count of the digits right of the decimal. Cast your decimal value to SqlDecimal and then access Scale.

((SqlDecimal)(decimal)yourValue).Scale
RitchieD
  • 1,831
  • 22
  • 21
  • 1
    Looking at the [Microsoft reference code](https://referencesource.microsoft.com/#System.Data/System/Data/SQLTypes/SQLDecimal.cs,adf3588a3686537c), casting to SqlDecimal internally uses the `GetBytes` so it allocates the Byte array instead of accessing the bytes in an unsafe context. There is even a note and commented out code in the reference code, stating that and how they could do that instead. Why they didn't is a mystery to me. I'd stay clear of this and access the scale bits directly instead of hiding the GC Alloc in this cast, as it is just not very obvious what it does under the hood. – Martin Tilo Schmitz Feb 05 '19 at 14:35
5

As a decimal extension method that takes into account:

  • Different cultures
  • Whole numbers
  • Negative numbers
  • Trailing set zeros on the decimal place (e.g. 1.2300M will return 2 not 4)
public static class DecimalExtensions
{
    public static int GetNumberDecimalPlaces(this decimal source)
    {
        var parts = source.ToString(CultureInfo.InvariantCulture).Split('.');

        if (parts.Length < 2)
            return 0;

        return parts[1].TrimEnd('0').Length;
    }
}
bytedev
  • 8,252
  • 4
  • 48
  • 56
4

So far, nearly all of the listed solutions are allocating GC Memory, which is very much the C# way to do things but far from ideal in performance critical environments. (The ones that do not allocate use loops and also don't take trailing zeros into consideration.)

So to avoid GC Allocs, you can just access the scale bits in an unsafe context. That might sound fragile but as per Microsoft's reference source, the struct layout of decimal is Sequential and even has a comment in there, not to change the order of the fields:

    // NOTE: Do not change the order in which these fields are declared. The
    // native methods in this class rely on this particular order.
    private int flags;
    private int hi;
    private int lo;
    private int mid;

As you can see, the first int here is the flags field. From the documentation and as mentioned in other comments here, we know that only the bits from 16-24 encode the scale and that we need to avoid the 31st bit which encodes the sign. Since int is the size of 4 bytes, we can safely do this:

internal static class DecimalExtensions
{
  public static byte GetScale(this decimal value)
  {
    unsafe
    {
      byte* v = (byte*)&value;
      return v[2];
    }
  }
}

This should be the most performant solution since there is no GC alloc of the bytes array or ToString conversions. I've tested it against .Net 4.x and .Net 3.5 in Unity 2019.1. If there are any versions where this does fail, please let me know.

Edit:

Thanks to @Zastai for reminding me about the possibility to use an explicit struct layout to practically achieve the same pointer logic outside of unsafe code:

[StructLayout(LayoutKind.Explicit)]
public struct DecimalHelper
{
    const byte k_SignBit = 1 << 7;

    [FieldOffset(0)]
    public decimal Value;

    [FieldOffset(0)]
    public readonly uint Flags;
    [FieldOffset(0)]
    public readonly ushort Reserved;
    [FieldOffset(2)]
    byte m_Scale;
    public byte Scale
    {
        get
        {
            return m_Scale;
        }
        set
        {
            if(value > 28)
                throw new System.ArgumentOutOfRangeException("value", "Scale can't be bigger than 28!")
            m_Scale = value;
        }
    }
    [FieldOffset(3)]
    byte m_SignByte;
    public int Sign
    {
        get
        {
            return m_SignByte > 0 ? -1 : 1;
        }
    }
    public bool Positive
    {
        get
        {
            return (m_SignByte & k_SignBit) > 0 ;
        }
        set
        {
            m_SignByte = value ? (byte)0 : k_SignBit;
        }
    }
    [FieldOffset(4)]
    public uint Hi;
    [FieldOffset(8)]
    public uint Lo;
    [FieldOffset(12)]
    public uint Mid;

    public DecimalHelper(decimal value) : this()
    {
        Value = value;
    }

    public static implicit operator DecimalHelper(decimal value)
    {
        return new DecimalHelper(value);
    }

    public static implicit operator decimal(DecimalHelper value)
    {
        return value.Value;
    }
}

To solve the original problem, you could strip away all fields besides Value and Scale but maybe it could be useful for someone to have them all.

  • 1
    You can also avoid unsafe code by coding your own struct with explict layout - put a decimal at position 0, then bytes/ints at the appropriate locations. Something like: `[StructLayout(LayoutKind.Explicit)] public struct DecimalHelper { [FieldOffset(0)] public decimal Value; [FieldOffset(0)] public uint Flags; [FieldOffset(0)] public ushort Reserved; [FieldOffset(2)] public byte Scale; [FieldOffset(3)] public DecimalSign Sign; [FieldOffset(4)] public uint ValuePart1; [FieldOffset(8)] public ulong ValuePart2; }` – Zastai Feb 05 '19 at 14:55
  • Thanks @Zastai, good point. I've incorporated that approach as well. :) – Martin Tilo Schmitz Feb 05 '19 at 17:42
  • 1
    One thing to note: setting the scale outside of the 0-28 range causes breakage. ToString() tends to work, but arithmetic fails. – Zastai Feb 06 '19 at 06:16
  • Thanks again @Zastai, I've added a check for that :) – Martin Tilo Schmitz Feb 07 '19 at 07:45
  • Another thing: several people here did not want to take trailing decimal zeroes into account. If you define a `const decimal Foo = 1.0000000000000000000000000000m;` then dividing a decimal by that will rescale it to the lowest scale possible (i.e. no longer including trailing decimal zeroes). I have not benchmarked this to see whether or not it’s faster than the string-based approach I suggested elsewhere though. – Zastai Feb 07 '19 at 08:35
  • 1
    Thanks this helped me make my answer https://stackoverflow.com/a/74663587/15719632 –  Dec 03 '22 at 03:07
4

I'm using something very similar to Clement's answer:

private int GetSignificantDecimalPlaces(decimal number, bool trimTrailingZeros = false)
{
    var stemp = Convert.ToString(number);
    var decSepIndex = stemp.IndexOf(System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator);

    if (decSepIndex == -1)
        return 0;

    if (trimTrailingZeros)
        stemp = stemp.TrimEnd('0');

    return stemp.Length - 1 - decSepIndex;
}

*Edit - fixed where no separator exists as per @nawfal

RooiWillie
  • 2,198
  • 1
  • 30
  • 36
2

I wrote a concise little method yesterday that also returns the number of decimal places without having to rely on any string splits or cultures which is ideal:

public int GetDecimalPlaces(decimal decimalNumber) { // 
try {
    // PRESERVE:BEGIN
        int decimalPlaces = 1;
        decimal powers = 10.0m;
        if (decimalNumber > 0.0m) {
            while ((decimalNumber * powers) % 1 != 0.0m) {
                powers *= 10.0m;
                ++decimalPlaces;
            }
        }
return decimalPlaces;
Jesse Carter
  • 20,062
  • 7
  • 64
  • 101
  • @fix-like-codings similar to your second answer although for something like this I favour the iterative approach rather than using recursion – Jesse Carter Nov 21 '12 at 14:16
  • The original post states that: `19.0 should return 1`. This solution will always assume a minimal amount of 1 decimal place and ignore trailing zeros. decimal can have those as it uses a scale factor. The scale factor can be accessed as in the bytes 16-24 of the the element with index 3 in the array gotten from `Decimal.GetBytes()` or by using pointer logic. – Martin Tilo Schmitz Feb 05 '19 at 14:54
2

I actually performance tested most of the solutions here. Some are fast but not reliable, some are reliable but not fast. With modification of @RooiWillie's answer, I get this which is fast enough and reliable:

public static int GetSignificantDecimalPlaces(decimal number)
{
    if (number % 1 == 0) return 0;
    var numstr = number.ToString(CultureInfo.InvariantCulture).TrimEnd('0');
    return numstr.Length - 1 - numstr.IndexOf('.');
}

Note: It does not count trailing zeros.

xUnit tests:

[Theory]
[InlineData(0, 0)]
[InlineData(1.0, 0)]
[InlineData(100, 0)]
[InlineData(100.10, 1)]
[InlineData(100.05, 2)]
[InlineData(100.0200, 2)]
[InlineData(0.0000000001, 10)]
[InlineData(-52.12340, 4)]
public void GetSignificantDecimalPlaces(decimal number, int expected)
{
    var actual = GetSignificantDecimalPlaces(number);
    Assert.Equal(expected, actual);
}
orad
  • 15,272
  • 23
  • 77
  • 113
2

Given that Decimal.Scale property was exposed (proposal) in .NET 7 and when one considers the trick by Thomas Materna we can write:

public static int GetDecimalPlaces(this decimal number)
{
    if (number.Scale == 0)
        return 0;

    number /= 1.000000000000000000000000000000000m;

    return number.Scale;
}

And (xUnit) tests still pass:

Assert.Equal(0, 0.0m.GetDecimalPlaces());
Assert.Equal(0, 1.0m.GetDecimalPlaces());
Assert.Equal(0, (-1.0m).GetDecimalPlaces());

Assert.Equal(2, 0.01m.GetDecimalPlaces());

Assert.Equal(3, 1.123m.GetDecimalPlaces());
Assert.Equal(3, (-1.123m).GetDecimalPlaces());
Assert.Equal(3, 0.001m.GetDecimalPlaces());

Assert.Equal(5, 43.12345m.GetDecimalPlaces());
Assert.Equal(5, 0.00005m.GetDecimalPlaces());
Assert.Equal(5, 0.00001m.GetDecimalPlaces());

Assert.Equal(7, 0.0000001m.GetDecimalPlaces());
Assert.Equal(8, 0.00000001m.GetDecimalPlaces());
Assert.Equal(9, 0.000000001m.GetDecimalPlaces());
Assert.Equal(10, 0.0000000001m.GetDecimalPlaces());
Assert.Equal(11, 0.00000000001m.GetDecimalPlaces());
Assert.Equal(12, 0.000000000001m.GetDecimalPlaces());
Assert.Equal(13, 0.0000000000001m.GetDecimalPlaces());
Assert.Equal(14, 0.00000000000001m.GetDecimalPlaces());
Assert.Equal(15, 0.000000000000001m.GetDecimalPlaces());
Assert.Equal(16, 0.0000000000000001m.GetDecimalPlaces());
Assert.Equal(17, 0.00000000000000001m.GetDecimalPlaces());
Assert.Equal(18, 0.000000000000000001m.GetDecimalPlaces());
Assert.Equal(19, 0.0000000000000000001m.GetDecimalPlaces());
Assert.Equal(20, 0.00000000000000000001m.GetDecimalPlaces());

Assert.Equal(19, 0.00000000000000000010m.GetDecimalPlaces());
Assert.Equal(18, 0.00000000000000000100m.GetDecimalPlaces());
Assert.Equal(17, 0.00000000000000001000m.GetDecimalPlaces());
Assert.Equal(16, 0.00000000000000010000m.GetDecimalPlaces());
Assert.Equal(15, 0.00000000000000100000m.GetDecimalPlaces());
Assert.Equal(14, 0.00000000000001000000m.GetDecimalPlaces());
Assert.Equal(13, 0.00000000000010000000m.GetDecimalPlaces());
Assert.Equal(12, 0.00000000000100000000m.GetDecimalPlaces());
Assert.Equal(11, 0.00000000001000000000m.GetDecimalPlaces());
Assert.Equal(10, 0.00000000010000000000m.GetDecimalPlaces());
Assert.Equal(9, 0.00000000100000000000m.GetDecimalPlaces());
Assert.Equal(8, 0.00000001000000000000m.GetDecimalPlaces());
Assert.Equal(7, 0.00000010000000000000m.GetDecimalPlaces());
Assert.Equal(6, 0.00000100000000000000m.GetDecimalPlaces());
Assert.Equal(5, 0.00001000000000000000m.GetDecimalPlaces());
Assert.Equal(4, 0.00010000000000000000m.GetDecimalPlaces());
Assert.Equal(3, 0.00100000000000000000m.GetDecimalPlaces());
Assert.Equal(2, 0.01000000000000000000m.GetDecimalPlaces());
Assert.Equal(1, 0.10000000000000000000m.GetDecimalPlaces());

Additionally, there is also a new proposal to add the requested feature. Too bad https://github.com/dotnet/runtime/issues/25715#issue-558361050 was closed as the suggestions were good.

How brave one has to be to use this snippet is yet to be determined :)

MartyIX
  • 27,828
  • 29
  • 136
  • 207
1

I use the following mechanism in my code

  public static int GetDecimalLength(string tempValue)
    {
        int decimalLength = 0;
        if (tempValue.Contains('.') || tempValue.Contains(','))
        {
            char[] separator = new char[] { '.', ',' };
            string[] tempstring = tempValue.Split(separator);

            decimalLength = tempstring[1].Length;
        }
        return decimalLength;
    }

decimal input=3.376; var instring=input.ToString();

call GetDecimalLength(instring)

Srikanth
  • 980
  • 3
  • 16
  • 30
  • 1
    This doesn't work for me as the ToString() representation of the decmial value adds "00" onto the end of my data - I'm using a Decimal(12,4) datatype from SQL Server. – PeterX Jun 18 '14 at 07:34
  • Can you cast your data to c# type decimal and try the solution. For me when i use Tostring() on c# decimal value I never see a "00". – Srikanth Jun 19 '14 at 10:51
  • This also won't work if `tempValue` has a comma or a period as a thousands separator (e.g. 1,234,567.89). – Nikola Novak Oct 08 '22 at 14:59
1

Using recursion you can do:

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}
Mars
  • 11
  • 3
  • The original post states that: `19.0 should return 1`. This solution will ignore trailing zeros. decimal can have those as it uses a scale factor. The scale factor can be accessed as in the bytes 16-24 of the the element with index 3 in `Decimal.GetBytes()` array or by using pointer logic. – Martin Tilo Schmitz Feb 05 '19 at 14:50
1
string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length;  // 6
Eva Chang
  • 23
  • 6
1

Since .Net 5, decimal.GetBits has an overload that takes a Span<int> as a destination. This avoids allocating a new array on the GC heap without needing to muck around with reflection to private members of System.Decimal.

static int GetDecimalPlaces(decimal value)
{
    Span<int> data = stackalloc int[4];
    decimal.GetBits(value, data);
    // extract bits 16-23 of the flags value
    const int mask = (1 << 8) - 1;
    return (data[3] >> 16) & mask;
}

Note that this answers the case given in the question, where 19.0 is specified to return 1. This matches how the .Net System.Decimal struct stores the decimal places which includes trailing zeroes (which may be regarded as significant for certain applications, e.g. representing measurements to a given precision).

A limitation here is that this is very specific to the .Net decimal format, and conversions from other floating point types may not give what you expect. For example, the case of converting the value 0.01f (which actually stores the number 0.00999999977648258209228515625) to decimal results in a value of 0.010m rather than 0.01m (this can be seen by passing the value to ToString()), and will thus give an output of 3 rather than 2. Getting the value of decimal places in a decimal value excluding trailing zeroes is another question.

0

I suggest using this method :

    public static int GetNumberOfDecimalPlaces(decimal value, int maxNumber)
    {
        if (maxNumber == 0)
            return 0;

        if (maxNumber > 28)
            maxNumber = 28;

        bool isEqual = false;
        int placeCount = maxNumber;
        while (placeCount > 0)
        {
            decimal vl = Math.Round(value, placeCount - 1);
            decimal vh = Math.Round(value, placeCount);
            isEqual = (vl == vh);

            if (isEqual == false)
                break;

            placeCount--;
        }
        return Math.Min(placeCount, maxNumber); 
    }
Veysel Ozdemir
  • 675
  • 7
  • 12
-1

You can try:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;
Snowbear
  • 16,924
  • 3
  • 43
  • 67
NicoRiff
  • 4,803
  • 3
  • 25
  • 54