查找十进制值的小数位数,无论文化如何

时间:2012-11-20 16:31:41

标签: c# decimal cultureinfo

我想知道是否有一种简洁而准确的方法来提取十进制值中的小数位数(作为一个int),可以安全地在不同的文化信息中使用?

例如:
19.0应该返回1,
27.5999应该返回4,
19.12应该返回2,

我写了一个查询,在一段时间内执行字符串拆分以查找小数位:

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

但我发现这只适用于使用'。'的地区。作为小数分隔符,因此在不同系统中非常脆弱。

17 个答案:

答案 0 :(得分:134)

我使用Joe's way来解决此问题:)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];

答案 1 :(得分:19)

由于所提供的答案都不足以使幻数“-0.01f”转换为十进制..即:GetDecimal((decimal)-0.01f);
我只能假设一个巨大的心灵屁病毒3年前袭击了所有人:) 这是一个似乎是这个邪恶和怪异问题的工作实现,在点之后计算小数位的非常复杂的问题 - 没有字符串,没有文化,不需要计算位数,也不需要阅读数学论坛..只是简单的三年级数学。

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
}

答案 2 :(得分:17)

我可能会使用@fixagon's answer中的解决方案。

但是,虽然Decimal结构没有获取小数位数的方法,但可以调用Decimal.GetBits来提取二进制表示,然后使用整数值和scale来计算小数位数。

这可能比格式化字符串更快,尽管你必须处理大量的小数以注意差异。

我将把实施作为练习。

答案 3 :(得分:15)

查找小数点后位数的最佳解决方案之一显示在burning_LEGION's post中。

这里我使用的是STSdb论坛文章中的部分:Number of digits after decimal point

在MSDN中,我们可以阅读以下说明:

"十进制数是一个浮点值,由一个符号组成,一个数值,其中每个数字的值在0到9之间, 和一个比例因子,表示浮动小数点的位置,用于分隔数值的整数部分和小数部分。"

还有:

"十进制值的二进制表示由1位符号,96位整数和用于除以96位整数的比例因子组成 并指定它的哪个部分是小数部分。缩放因子隐式为数字10,从0到28的指数上升。"

在内部级别,十进制值由四个整数值表示。

Decimal internal representation

有一个公开的GetBits函数可用于获取内部表示。该函数返回一个int []数组:

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

返回数组的第四个元素包含比例因子和符号。并且正如MSDN所说,缩放因子隐含地为数字10,提升到0到28之间的指数。这正是我们所需要的。

因此,基于上述所有调查,我们可以构建我们的方法:

private const int SIGN_MASK = ~Int32.MinValue;

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

这里SIGN_MASK用于忽略符号。在逻辑之后,我们还将结果向右移16位以接收实际比例因子。最后,该值表示小数点后的位数。

请注意,此处MSDN还说缩放因子也会保留十进制数中的任何尾随零。尾随零不会影响算术或比较运算中的十进制数的值。但是,如果应用了适当的格式字符串,ToString方法可能会显示尾随零。

这个解决方案看起来是最好的,但等等,还有更多。通过accessing private methods in C#,我们可以使用表达式构建对flags字段的直接访问,并避免构造int数组:

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);
    }
}

此编译代码已分配给GetDigits字段。请注意,该函数接收十进制值作为ref,因此不执行实际复制 - 仅对值的引用。使用DecimalHelper中的GetDigits函数很简单:

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

这是获取小数点后小数点后位数的最快方法。

答案 4 :(得分:11)

您可以使用InvariantCulture

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

另一种可能性是做类似的事情:

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);
}

答案 5 :(得分:9)

依赖于小数的内部表示并不酷。

这个怎么样:

    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();
    }

答案 6 :(得分:5)

这里的大多数人似乎都不知道小数会将尾随零视为存储和打印的重要因素。

因此0.1m,0.10m和0.100m可以比较相同,它们的存储方式不同(分别为值/比例1 / 1,10 / 2和100/3),并将打印为0.1,0.10和分别为0.100,ToString()

因此,报告“精度太高”的解决方案实际上是根据decimal条款报告正确的精度。

此外,基于数学的解决方案(如乘以10的幂)可能会非常慢(十进制比算术的两倍慢〜并且你不想混合使用浮点数,因为这很可能引入不精确)。同样,转换为intlong作为截断方式容易出错(decimal的范围比其中任何一个都大得多 - 它基于96位整数)。

虽然不如此优雅,但以下可能是获得精度的最快方法之一(定义为“排除尾随零的小数位数”):

public static int 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;
}

不变的文化保证'。'作为小数点,尾随零被修剪,然后它只是看到小数点后剩余多少个位置(如果有一个)。

编辑:将返回类型更改为int

答案 7 :(得分:5)

这是另一种方法,使用类型SqlDecimal,它具有scale属性,小数点右边的位数。将您的十进制值转换为SqlDecimal,然后访问Scale。

((SqlDecimal)(decimal)yourValue).Scale

答案 8 :(得分:2)

昨天我写了一个简洁的小方法,它也返回小数位数,而不必依赖任何字符串分割或理想的文化:

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;

答案 9 :(得分:2)

到目前为止,几乎所有列出的解决方案都在分配GC内存,这是C#的处理方式,但是在性能关键的环境中却并非理想。 (不分配的循环使用循环,也没有考虑尾随零。)

因此,为了避免使用GC Allocs,您可以仅在不安全的上下文中访问刻度位。听起来可能很脆弱,但是根据Microsoft's reference source,十进制的结构布局是顺序的,甚至在其中也有注释,而不更改字段的顺序:

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

如您所见,这里的第一个int是flags字段。从文档以及此处的其他注释中可以看出,我们仅知道16-24的位对比例进行编码,并且需要避免编码符号的第31位。由于int的大小为4个字节,因此我们可以放心地这样做:

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

这应该是性能最高的解决方案,因为没有字节数组或ToString转换的GC分配。我已经在Unity 2019.1中针对.Net 4.x和.Net 3.5对其进行了测试。如果有任何失败的版本,请告诉我。

编辑:

感谢@Zastai提醒我可以使用显式结构布局在不安全代码之外实际上实现相同的指针逻辑的可能性:

[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;
    }
}

要解决最初的问题,您可以删除ValueScale以外的所有字段,但也许对某些人来说都是有用的。

答案 10 :(得分:1)

您可以尝试:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;

答案 11 :(得分:1)

string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length;  // 6

答案 12 :(得分:1)

我在我的代码中使用以下机制

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

十进制输入= 3.376; var instring = input.ToString();

调用GetDecimalLength(instring)

答案 13 :(得分:0)

使用递归可以:

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}

答案 14 :(得分:0)

我建议使用这种方法:

    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); 
    }

答案 15 :(得分:0)

我使用的内容与克莱门特的答案非常相似:

private int GetSignificantDecimalPlaces(decimal number, bool trimTrailingZeros = true)
{
  string stemp = Convert.ToString(number);

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

  return stemp.Length - 1 - stemp.IndexOf(
         Application.CurrentCulture.NumberFormat.NumberDecimalSeparator);
}

记住要使用System.Windows.Forms来访问Application.CurrentCulture

答案 16 :(得分:0)

作为考虑到十进制扩展名的方法:

  • 不同文化
  • 整数
  • 负数
  • 尾随设置为零的小数位(例如1.2300M将返回2而不是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;
    }
}