104

我想知道是否有一种简洁准确的方法来提取十进制值(作为 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;

但我突然想到,这只适用于使用“。”的地区。作为小数分隔符,因此在不同系统中非常脆弱。

4

18 回答 18

184

我用乔的方法来解决这个问题:)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
于 2012-11-21T12:55:54.320 回答
31

由于提供的答案都不足以将幻数“-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
}
于 2015-05-13T03:27:55.353 回答
26

我可能会使用@fixagon 的答案中的解决方案。

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

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

我将把实现留作练习。

于 2012-11-20T16:46:35.407 回答
19

Burning_LEGION 的帖子中显示了查找小数点后位数的最佳解决方案之一。

在这里,我使用了 STSdb 论坛文章中的部分内容:小数点后的位数

在 MSDN 中我们可以阅读以下解释:

“十进制数是一个浮点值,它由一个符号、一个数值(值中的每个数字的范围从 0 到 9)和一个比例因子组成,该比例因子表示将整数和小数分开的浮点小数点的位置数值的一部分。”

并且:

“Decimal 值的二进制表示由一个 1 位符号、一个 96 位整数和一个比例因子组成,该比例因子用于划分 96 位整数并指定它的哪一部分是小数部分。比例因子是隐含的数字 10,提高到从 0 到 28 的指数。”

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

十进制内部表示

有一个公开可用的 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 还说比例因子还保留了十进制数中的任何尾随零。尾随零不会影响算术或比较运算中的 Decimal 数的值。但是,如果应用了适当的格式字符串,ToString 方法可能会显示尾随零。

这个解决方案看起来是最好的,但是等等,还有更多。通过访问 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);

这是获取十进制值小数点后位数的最快方法。

于 2014-07-03T08:39:13.753 回答
17

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

这个怎么样:

    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();
    }
于 2015-09-29T07:08:16.183 回答
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);
}
于 2012-11-20T16:35:06.197 回答
9

这里的大多数人似乎没有意识到十进制认为尾随零对于存储和打印很重要。

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

因此,报告“精度太高”的解决方案实际上是在报告正确的精度decimal

此外,基于数学的解决方案(例如乘以 10 的幂)可能会非常慢(对于算术,十进制比 double 慢约 40 倍,而且您也不希望混合使用浮点,因为这可能会引入不精确性)。类似地,强制转换为截断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

于 2015-08-06T08:32:59.827 回答
6

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

((SqlDecimal)(decimal)yourValue).Scale
于 2017-03-07T20:20:54.067 回答
5

我正在使用与克莱门特的答案非常相似的东西:

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

于 2019-10-18T08:18:47.560 回答
4

到目前为止,几乎所有列出的解决方案都在分配 GC 内存,这在很大程度上是 C# 做事的方式,但在性能关键环境中远非理想。(那些不分配使用循环并且也不考虑尾随零的那些。)

因此,为了避免 GC Allocs,您可以在不安全的上下文中访问比例位。这听起来可能很脆弱,但根据Microsoft 的参考源,decimal 的结构布局是 Sequential 甚至在其中有注释,而不是更改字段的顺序:

    // 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但也许对某人拥有它们可能有用。

于 2019-02-04T13:55:09.597 回答
3

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

  • 不同的文化
  • 整数
  • 负数
  • 小数点后置零(例如 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;
    }
}
于 2020-07-09T07:47:11.837 回答
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;
于 2012-11-21T14:14:17.207 回答
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)

于 2014-02-10T06:06:09.813 回答
1

使用递归,您可以:

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}
于 2017-12-18T16:02:56.927 回答
1
string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length;  // 6
于 2019-03-05T03:24:26.993 回答
0

你可以试试:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;
于 2012-11-20T16:42:25.813 回答
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); 
    }
于 2017-02-13T11:10:55.903 回答
0

我实际上在这里测试了大多数解决方案。有些快速但不可靠,有些可靠但不快。通过修改@RooiWillie 的答案,我得到了足够快且可靠的答案:

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

注意:它不计算尾随零。

xUnit 测试:

[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);
}
于 2021-12-18T07:38:33.260 回答