无论区域性如何,都可以在小数值中查找小数位数

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

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

我编写了一个查询,对一个句点进行字符串分割,以查找小数位:

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

但我突然想到,这将只在使用’的地区工作作为一种小数点,因此在不同的系统中是非常脆弱的。

132240 次浏览

你可以使用 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);
}

你可以试试:

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

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

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

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

我将把实现作为一个练习。

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

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[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;

我在代码中使用以下机制

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

调用 GetDecimalLlength (字符串)

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

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

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

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

还有:

”Decimal 值的二进制表示形式由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 还说缩放因子还保留了 Decimal 数字的任何尾随零。在算术或比较操作中,尾随零不影响 Decimal 数的值。但是,如果应用了适当的格式字符串,ToString 方法可能会显示尾随零。

这个解决方案看起来是最好的一个,但是等等,还有更多。通过 访问 C # 中的私有方法,我们可以使用表达式构建对标志字段的直接访问,并避免构造 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);
}
}

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

这是获得小数点后小数位数的最快可能的方法。

由于所提供的答案都不足以将“-0.01 f”这个神奇的数字转换为十进制,即: 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
}

这里的大多数人似乎没有意识到,小数认为后面的零对于存储和打印非常重要。

So 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().

因此,报告“太高的精度”的解决方案实际上是报告 正确的精度,按照 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

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

这样吧:

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

我建议使用这种方法:

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

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

使用递归你可以做到:

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

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

因此,为了避免 GC 分配,您只需在不安全的上下文中访问比例位。这听起来可能很脆弱,但是根据 微软的参考资料来源,decal 的 struct 布局是 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;

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

这应该是性能最好的解决方案,因为没有字节数组或 ToString 转换的 GC 分配。我拿它做过测试。净值4.x 及。Unity 2019.1年净收入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之外的所有字段,但是对于某些人来说,拥有它们可能是有用的。

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

我的回答和克莱门特非常相似:

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

* 按照@nawfal 编辑-修正不存在分隔符的地方

作为一种十进制扩展方法,它考虑到:

  • 不同的文化
  • Whole numbers
  • 负数
  • 小数点后的零(例如1.2300 M 将返回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;
}
}

实际上,我对这里的大多数解决方案进行了性能测试。有些快但不可靠,有些可靠但不快。通过修改@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);
}

自从。Net5,decimal.GetBits有一个以 Span<int>作为目的地的过载。这避免了在 GC 堆上分配一个新数组,而无需对 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;
}

注意,这回答了问题中给出的情况,其中19.0被指定为返回1。这符合。Net System.Decimal struct 存储小数位,其中包括尾随零(在某些应用中可能被认为是重要的,例如按给定的精度表示测量值)。

这里的一个限制是,它非常特定于。净十进制格式,以及从其他浮点类型的转换可能不会给您所期望的。例如,将值 0.01f(实际上存储数字0.00999997648258209228515625)转换为 decimal的情况导致值为 0.010m而不是 0.01m(这可以通过将值传递给 ToString()来看到) ,因此输出为3而不是2。在不包括尾随零的 decimal值中获取小数位的值是另一个问题。

假设 Decimal.Scale属性是 暴露了(建议书)。NET 7,当我们考虑托马斯 · 马特纳的 恶作剧时,我们可以这样写:

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


number /= 1.000000000000000000000000000000000m;


return number.Scale;
}

而且(xUnit)测试仍然通过:

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

此外,还有一个新的建议,添加 requested feature。可惜的是,由于建议是好的,https://github.com/dotnet/runtime/issues/25715#issue-558361050被关闭了。

使用这个代码片段需要多大的勇气还有待确定:)