在不考虑区域性的情况下,在十进制值中查找小数位数


91

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

但是我想到这只会在使用“。”的区域中起作用。作为小数点分隔符,因此在不同系统之间非常脆弱。


根据问题标题的十进制
Jesse Carter

拆分之前的某些模式匹配怎么样?基本上是\ d +(\ D)\ d +,其中\ D返回分隔符(。等)
Anshul 2012年

7
这不是一个封闭式的问题,因为乍看起来可能会脸红。要求19.0返回1是有关值的内部存储的实现细节19.0。事实是程序将其存储为as190×10⁻¹1900×10⁻²or是完全合法的19000×10⁻³。所有这些都是平等的。它在给定值时使用第一个表示形式,19.0M而在ToString不使用格式说明符的情况下使用该表示形式这一事实,只是一个巧合,是一件令人高兴的事。除非人们不愿依靠指数而感到不高兴。
ErikE

如果你想有一个类型,它可以携带“使用的小数号”创建时,这样就可以可靠地区分19M19.0M19.00M,你需要创建一个捆绑潜在价值为一体的特性和数量的新类小数位作为另一个属性。
ErikE

1
即使Decimal类可以“区分” 19m,还是从19.0m从19.00m区分出来?有效数字就像其主要用例之一。19.0m * 1.0m是什么?似乎在说19.00m,也许C#开发人员在数学上做错了,尽管:P?同样重要的数字是真实的东西。如果您不喜欢有效数字,则可能不应该使用Decimal类。
Nicholi

Answers:


167

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

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

6
在进一步研究并看到它的作用后,我将其标记为答案,因为在我看来,这是返回我在这里看到的小数位的最简洁,最优雅的方法。如果可以的话,我会再次+1:D
杰西·卡特

9
decimal在昏迷后保持计数位数,这就是为什么您找到此“问题”的原因,必须将十进制转换为双精度,然后再次转换为十进制进行修复:BitConverter.GetBytes(decimal.GetBits((decimal)(double)argument)[3])[ 2];
Burn_LEGION 2013年

3
这对我不起作用。从SQL返回的值是21.17,表示4位数字。数据类型定义为DECIMAL(12,4),所以就这样(使用实体框架)。
PeterX

11
@Nicholi-不,这是非常糟糕的,因为该方法依赖于小数点基础位的放置-有些东西可以用多种方式表示相同的数字。您不会基于私有字段的状态来测试类吗?
m.edmondson

14
不知道这应该是优雅还是不错的。这几乎是一成不变的。谁知道它是否在所有情况下都有效。不可能确定。
usr

24

由于提供的答案均不足以将魔术数字“ -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
}

6
您的解决方案在许多情况下都会失败,这些情况包含尾随零并且数字是有效的。0.01m * 2.0m = 0.020m。应该为3位数字,您的方法返回2。您似乎错误地理解了将0.01f强制转换为Decimal时会发生什么。浮点本质上并不精确,因此为0.01f存储的实际二进制值并不精确。当您转换为Decimal(非常结构化的数字符号)时,您可能不会得到0.01m(实际上是0.010m)。实际上,GetBits解决方案对于从十进制获取位数是正确的。如何转换为十进制是关键。
Nicholi

2
@Nicholi 0.020m等于0.02m ..后面的零并不重要。OP在标题中询问“无论文化如何”,甚至更具体地解释“在不同文化信息中可以安全使用的..”-因此,我认为我的回答仍然比其他答案更有效。
GY 2015年

6
OP专门说:“ 19.0应该返回1”。在这种情况下,此代码将失败。
daniloquio

9
也许这不是OP想要的,但是这个答案比这个问题的最高答案更适合我的需求
Arsen Zahray

2
前两行应替换为,n = n % 1; if (n < 0) n = -n;因为大于的值int.MaxValue将引起OverflowException,例如2147483648.12345
讨厌

23

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

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

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

我将把实施留为练习。


1
谢谢@Joe,这是一种非常巧妙的解决方法。根据上司对使用其他解决方案的感觉,我将考虑实施您的想法。肯定会是一个有趣的练习:)
杰西·卡特

17

burning_LEGION的post中显示了找到小数点后数字位数的最佳解决方案之一。

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

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

“十进制数是一个浮点值,它由一个符号,一个数值(其中值中的每个数字都介于0到9之间)以及一个比例因子(表示将整数和小数分开的浮点小数的位置)组成部分数值。”

并且:

“十进制值的二进制表示形式包括一个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还说比例因子还保留了十进制数中的所有尾随零。尾随零不会影响算术或比较操作中小数的值。但是,如果应用了适当的格式字符串,则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);

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


3
十进制r =(十进制)-0.01f; 解决方案失败。(关于我在此页面上看到的所有答案...):)
GY

5
注意:关于整个(十进制)0.01f,您正在将一个浮点(本质上不是精确的)强制转换为非常类似于十进制的结构。看一下Console.WriteLine((Decimal)0.01f)的输出。在强制转换中形成的小数实际上有3位,这就是为什么提供的所有解决方案都用3而不是2表示的原因。所有事情实际上都按预期工作,“问题”是您期望浮点值是准确的。他们不是。
Nicholi

@Nicholi当您意识到这一点0.01并且0.010正好等于数字时,您的观点就会失败。此外,数字数据类型具有某种可以依赖的“使用的数字位数”语义的想法是完全错误的(不要与“允许的数字位数”相混淆。不要混淆表示(显示的内容)。在一个特定的碱的数的值,例如,该值的小数膨胀指示由二进制展开111)与下面的值再次重申,!数字不是数字的,也不是由数字组成
ErikE

6
它们的价值是相等的,但不是有效数字。这是Decimal类的一个大用例。如果我问字面量为0.010m的位数是多少,您只说2吗?即使全球数十名数学/科学老师会告诉您最后的0还是很重要的?我们所指的问题通过将浮点转换为十进制来体现。不是完全按照记录中的说明使用GetBits本身。如果您不关心有效位数,那么是的,您有问题,很可能一开始就不应使用Decimal类。
Nicholi 2015年

1
@theberserker据我所知,没有任何问题-它应该双向运行。
克里斯蒂安·迪米特洛夫

13

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

这个怎么样:

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

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

这如何帮助我找到小数位数?我毫不费力地将小数转换为任何文化中都很好的字符串。根据这个问题,我试图找到小数点后的小数位数
Jesse Carter

@JesseCarter:这意味着您可以随时拆分.
奥斯汀·萨洛宁

@AustinSalonen真的吗?我不知道使用InvariantCulture会强制使用句点作为小数点分隔符
Jesse Carter

如您之前所做的那样,它将始终将价格强制转换为带有的字符串。作为小数点分隔符。但是在我看来,这不是最优雅的方式...
fixagon


8

这里的大多数人似乎都不知道十进制认为尾随零对于存储和打印很重要。

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

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

此外,基于数学的解决方案(例如乘以10的幂)可能会非常慢(小数比算术的两倍慢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


1
@mvmorten不知道为什么您觉得有必要将返回类型更改为int;字节更准确地表示返回值:无符号且范围较小(实际上为0-29)。
Zastai

1
我同意基于迭代和计算的解决方案速度较慢(除了不考虑尾随零)。但是,为此分配一个字符串并对其进行操作也不是最有效的操作,尤其是在性能关键的情况下以及GC较慢的情况下。通过指针逻辑访问标度要快得多且无需分配。
Martin Tilo Schmitz

是的,可以更有效地获得规模-但这将包括尾随零。删除它们需要对整数部分进行算术运算。
Zastai

6

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

((SqlDecimal)(decimal)yourValue).Scale

1
查看Microsoft参考代码,强制转换为SqlDecimal在内部使用,GetBytes因此它分配Byte数组,而不是在不安全的上下文中访问字节。甚至在参考代码中也有注释和注释掉的代码,说明了该代码以及他们如何执行此操作。为什么他们没有成为我的谜。我会避免这种情况,直接访问比例位,而不是在此转换中隐藏GC Alloc,因为它在幕后的工作不是很明显。
Martin Tilo Schmitz

4

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

因此,为避免使用GC Allocs,您可以仅在不安全的上下文中访问比例位。这听起来可能很脆弱,但是按照Microsoft的参考资料,小数的结构布局是顺序的,甚至在其中都有注释,而不更改字段的顺序:

    // 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但可能对某些人有用。


1
您还可以通过使用显式布局编码自己的结构来避免不安全的代码-在位置0放置小数,然后在适当的位置放置字节/整数。像这样的东西:[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

谢谢@Zastai,很好。我也采用了这种方法。:)
Martin Tilo Schmitz

1
需要注意的一件事:将刻度设置在0-28范围之外会导致破裂。ToString()倾向于工作,但是算术失败。
Zastai

再次感谢@Zastai,我为此添加了一张支票:)
Martin Tilo Schmitz,

另一件事:这里的几个人不想考虑尾随的十进制零。如果定义a,const decimal Foo = 1.0000000000000000000000000000m;则将小数除以该位数将重新缩放到可能的最小刻度(即不再包括尾随的十进制零)。我尚未对此进行基准测试,以查看它是否比我在其他地方建议的基于字符串的方法快。
Zastai

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;

@ fix-like-codings与您的第二个答案相似,尽管对于这样的事情,我更喜欢迭代方法,而不是使用递归
Jesse Carter

原始帖子指出:19.0 should return 1。此解决方案将始终假设最小数量为小数点后1位,并忽略尾随零。因为使用比例因子,所以小数可以包含那些。比例因子可以像从数组中使用索引Decimal.GetBytes()逻辑或通过使用指针逻辑获取的索引为3的元素的字节16-24中那样进行访问。
Martin Tilo Schmitz

2

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

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


1

你可以试试:

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

7
如果小数是整数,这不会失败吗?[1]
Silvermind 2012年

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)


1
这对我不起作用,因为decmial值的ToString()表示将“ 00”添加到我的数据末尾-我正在使用SQL Server中的Decimal(12,4)数据类型。
PeterX

您能否将数据转换为c#类型的十进制并尝试解决方案。对我来说,当我在C#十进制值上使用Tostring()时,我再也看不到“ 00”。
Srikanth 2014年

1

使用递归可以做到:

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

原始帖子指出:19.0 should return 1。该解决方案将忽略尾随零。因为使用比例因子,所以小数可以包含那些。比例因子可以在Decimal.GetBytes()数组中索引为3的元素的字节16-24中访问,也可以使用指针逻辑进行访问。
Martin Tilo Schmitz

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

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

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;
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.