无论文化如何,查找十进制值的小数位数
Posted
技术标签:
【中文标题】无论文化如何,查找十进制值的小数位数【英文标题】:Find number of decimal places in decimal value regardless of culture 【发布时间】:2012-11-08 18:50:22 【问题描述】:我想知道是否有一种简洁准确的方法来提取十进制值(作为 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;
但我突然想到,这仅适用于使用“。”的地区。作为小数分隔符,因此在不同系统中非常脆弱。
【问题讨论】:
根据问题标题的小数 Split 之前的一些模式匹配怎么样?基本上 \d+(\D)\d+ 其中 \D 返回分隔符(. 等) 这不是一个封闭式的问题,因为它乍一看可能会出现。要求19.0
返回1
是关于值19.0
的内部存储的实现细节。事实上,程序将其存储为190×10⁻¹
或1900×10⁻²
或19000×10⁻³
是完全合法的。所有这些都是平等的。事实上,它在给定 19.0M
的值时使用第一个表示,而在没有格式说明符的情况下使用 ToString
时会暴露这一点,这只是一个巧合,也是一件快乐的事情。除非人们在不应该依赖指数的情况下依赖指数,这并不令人高兴。
如果你想要一个在创建时可以携带“使用的小数位数”的类型,以便可靠地区分 19M
和 19.0M
和 19.00M
,你需要创建一个新类,将基础值捆绑为一个属性,将小数位数捆绑为另一个属性。
即使 Decimal 类可以“区分” 19m、19.0m 和 19.00m?有效数字就像它的主要用例之一。 19.0m * 1.0m 是多少?似乎是说 19.00m,也许 C# 开发人员做错了数学:P?同样,有效数字是真实的。如果您不喜欢有效数字,您可能不应该使用 Decimal 类。
【参考方案1】:
我使用Joe's way 解决了这个问题:)
decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
【讨论】:
decimal
在昏迷后保持计数位数,这就是为什么你会发现这个“问题”,你必须将十进制转换为双精度并再次转换为十进制以进行修复:BitConverter.GetBytes(decimal.GetBits((decimal )(double)argument)[3])[2];
这对我不起作用。从 SQL 返回的值是 21.17 表示 4 位数。数据类型定义为 DECIMAL(12,4) 所以也许就是这样(使用实体框架)。
@Nicholi - 不,这是非常糟糕,因为该方法依赖于 小数的基础位的位置 - 这有许多方式来表示相同的数字。您不会根据其私有字段的状态来测试一个类吗?
如果问题是“十进制对象中有多少位数”,GetBits 提供了该解决方案。同样,如果 Decimal 的底层表示发生变化,则 GetBits 的实现将不得不更改,因为它具有已定义并记录在案的返回值。 (十进制)0.01f 返回 3 位数字,因为它是三位数的十进制对象。如果问题是“双精度/浮点数中有多少位”,那么是的,转换为小数并使用 GetBits 可能不会得到您想要的答案。由于 double/float 的转换/转换将是不精确的。
不知道什么应该是优雅或好的。这几乎是令人困惑的。谁知道它是否在所有情况下都有效。无法确定。【参考方案2】:
由于提供的答案都不足以将幻数“-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
【讨论】:
您的解决方案将在许多包含尾随零且数字很重要的情况下失败。 0.01m * 2.0m = 0.020m。应该是 3 位数,您的方法返回 2。您似乎错误地理解了将 0.01f 转换为 Decimal 时会发生什么。浮点本质上是不精确的,因此为 0.01f 存储的实际二进制值并不精确。当您转换为 Decimal(一种非常结构化的数字符号)时,您可能不会得到 0.01m(实际上是 0.010m)。 GetBits 解决方案对于从 Decimal 获取位数实际上是正确的。如何转换为十进制是关键。 @Nicholi 0.020m 等于 0.02m .. 尾随零不重要。 OP 在标题中询问“无论文化如何”,甚至更具体地解释了“..在不同的文化信息中使用是安全的..” - 因此我认为我的回答仍然比其他人更有效。 OP 明确表示:“19.0 应该返回 1”。此代码在这种情况下失败。 也许这不是 OP 想要的,但这个答案比这个问题的最佳答案更适合我的需要 前两行应替换为n = n % 1; if (n < 0) n = -n;
,因为大于int.MaxValue
的值将导致OverflowException
,例如2147483648.12345
.【参考方案3】:
我可能会使用@fixagon's answer 中的解决方案。
但是,虽然 Decimal 结构没有获取小数位数的方法,但您可以调用 Decimal.GetBits 来提取二进制表示,然后使用整数值和小数位数来计算小数位数。
这可能比格式化为字符串更快,尽管您必须处理大量小数才能注意到差异。
我将把实现留作练习。
【讨论】:
【参考方案4】:burning_LEGION's post 中显示了查找小数点后位数的最佳解决方案之一。
这里我使用的是 STSdb 论坛文章中的部分内容:Number of digits after decimal point。
在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 方法可能会显示尾随零。
这个解决方案看起来是最好的,但是等等,还有更多。通过accessing private methods in 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);
此编译代码分配给 GetDigits 字段。请注意,该函数接收十进制值作为 ref,因此不执行实际复制 - 仅对该值的引用。使用 DecimalHelper 中的 GetDigits 函数很简单:
decimal value = 3.14159m;
int digits = DecimalHelper.Instance.GetDigits(ref value);
这是获取十进制值小数点后位数的最快方法。
【讨论】:
十进制 r = (十进制)-0.01f;解决方案失败。 (关于我在此页面中看到的所有答案...):) 注意:关于整个(十进制)0.01f 的东西,你正在将一个本质上不精确的浮点转换为像十进制这样非常结构化的东西。看看 Console.WriteLine((Decimal)0.01f) 的输出。在演员阵容中形成的小数实际上有 3 位数字,这就是为什么提供的所有解决方案都说 3 而不是 2。一切实际上都按预期工作,“问题”是您期望浮点值是准确的。他们不是。 @Nicholi 当您意识到0.01
和0.010
完全相同数字时,您的观点就失败了。此外,numeric 数据类型具有某种可以依赖的“使用的位数”语义的想法是完全错误的(不要与“允许的位数”混淆。不要t 混淆表示(以特定基数显示数字的值,例如,由二进制扩展 111 指示的值的十进制扩展)与基础值!重申一下,数字不是数字,它们也不是由数字组成.
它们在值上是等价的,但不是有效数字。这是 Decimal 类的一个大型用例。如果我问文字 0.010m 中有多少个数字,你会说只有 2 个吗?即使全球数学/科学教师的分数会告诉你最后的 0 很重要?我们所指的问题表现为从浮点数转换为十进制数。不是 GetBits 本身的使用,它完全按照记录的方式进行。如果您不关心有效数字,那么是的,您有问题并且可能不应该首先使用 Decimal 类。
@theberserker 据我记得,没有任何问题 - 它应该双向工作。【参考方案5】:
依赖小数的内部表示并不酷。
这个怎么样:
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】:你可以使用 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);
【讨论】:
这如何帮助我找到小数点中的小数位数?我可以将小数转换为在任何文化中都适用的字符串。根据问题,我试图找出小数点后的小数位数 @JesseCarter:这意味着你可以随时在.
上进行拆分。
@AustinSalonen 真的吗?我不知道使用 InvariantCulture 会强制使用句点作为小数分隔符
就像你之前做的那样,它总是将价格转换为带有 .作为小数分隔符。但在我看来,这不是最优雅的方式......
@JesseCarter: NumberFormatInfo.NumberDecimalSeparator【参考方案7】:
这里的大多数人似乎没有意识到小数点认为尾随零对于存储和打印很重要。
因此 0.1m、0.10m 和 0.100m 可以比较相等,它们的存储方式不同(分别为值/比例 1/1、10/2 和 100/3),并将打印为 0.1、0.10 和ToString()
分别为 0.100。
因此,根据decimal
的条款,报告“精度太高”的解决方案实际上报告了正确 精度。
此外,基于数学的解决方案(例如乘以 10 的幂)可能会非常慢(对于算术,十进制比 double 慢约 40 倍,而且您也不想混合使用浮点,因为这很可能引入不精确性)。同样,强制转换为 int
或 long
作为截断方式也容易出错(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
【讨论】:
@mvmorten 不知道为什么你觉得有必要将返回类型更改为 int; byte 更准确地表示返回值:无符号和小范围(实际上是 0-29)。 我同意基于迭代和计算的解决方案很慢(除了不考虑尾随零)。但是,为此分配一个字符串并对其进行操作也不是最高效的事情,尤其是在性能关键的上下文和缓慢的 GC 中。通过指针逻辑访问比例会更快且无需分配。 是的,获得 scale 可以更有效地完成 - 但这将包括尾随零。删除它们需要对整数部分进行算术运算。【参考方案8】:还有另一种方法,使用类型 SqlDecimal,它具有小数点右侧的数字计数的比例属性。将您的十进制值转换为 SqlDecimal,然后访问 Scale。
((SqlDecimal)(decimal)yourValue).Scale
【讨论】:
查看Microsoft reference code,转换为SqlDecimal 在内部使用GetBytes
,因此它分配字节数组而不是在不安全的上下文中访问字节。参考代码中甚至还有一个注释和注释掉的代码,说明了这一点以及他们如何做到这一点。为什么他们不这样做对我来说是个谜。我会避开这个并直接访问比例位,而不是在这个演员表中隐藏 GC Alloc,因为它在引擎盖下的作用并不是很明显。【参考方案9】:
我正在使用与克莱门特的回答非常相似的东西:
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
【讨论】:
【参考方案10】:到目前为止,几乎所有列出的解决方案都在分配 GC 内存,这在很大程度上是 C# 的处理方式,但在性能关键型环境中远非理想。 (那些不分配使用循环并且也不考虑尾随零的那些。)
因此,为了避免 GC Alloc,您可以在不安全的上下文中访问比例位。这听起来可能很脆弱,但根据Microsoft's reference source,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 字段。从文档和这里其他 cmets 中提到的,我们知道只有 16-24 的位对比例进行编码,我们需要避免对符号进行编码的第 31 位。由于 int 是 4 个字节的大小,我们可以安全地这样做:
internal static class DecimalExtensions
public static byte GetScale(this decimal value)
unsafe
byte* v = (byte*)&value;
return v[2];
这应该是最高效的解决方案,因为没有 GC 分配字节数组或 ToString 转换。我已经在 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;
要解决最初的问题,您可以删除除 Value
和 Scale
之外的所有字段,但也许对某人拥有它们可能会有用。
【讨论】:
您还可以通过使用显式布局对自己的结构进行编码来避免不安全的代码 - 在位置 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,好点子。我也采用了这种方法。 :)
需要注意的一点:将比例设置在 0-28 范围之外会导致破损。 ToString() 倾向于工作,但算术失败。
再次感谢@Zastai,我已经为此添加了支票:)
另一件事:这里有几个人不想考虑尾随十进制零。如果您定义const decimal Foo = 1.0000000000000000000000000000m;
,那么将小数除以它会将其重新缩放到可能的最低比例(即不再包括尾随小数零)。我还没有对此进行基准测试,看看它是否比我在其他地方建议的基于字符串的方法更快。【参考方案11】:
作为考虑到的小数扩展方法:
不同的文化 整数 负数 在小数点后置零(例如 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;
【讨论】:
【参考方案12】:我昨天写了一个简洁的小方法,它还可以返回小数位数,而不必依赖任何理想的字符串拆分或文化:
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 类似于您的第二个答案,尽管对于这样的事情我更喜欢迭代方法而不是使用递归 原帖指出:19.0 should return 1
。此解决方案将始终假定最少 1 位小数并忽略尾随零。十进制可以有这些,因为它使用比例因子。可以通过从Decimal.GetBytes()
获取的数组中索引为 3 的元素的字节 16-24 或使用指针逻辑来访问比例因子。【参考方案13】:
我在我的代码中使用以下机制
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)
【讨论】:
这对我不起作用,因为十进制值的 ToString() 表示将“00”添加到我的数据末尾 - 我正在使用 SQL 中的 Decimal(12,4) 数据类型服务器。 您能否将数据转换为 c# 类型的小数并尝试解决方案。对我来说,当我在 c# 十进制值上使用 Tostring() 时,我从来没有看到“00”。【参考方案14】:使用递归你可以做到:
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 中访问,也可以使用指针逻辑访问。【参考方案15】:
string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length; // 6
【讨论】:
【参考方案16】:你可以试试:
int priceDecimalPlaces =
price.ToString(System.Globalization.CultureInfo.InvariantCulture)
.Split('.')[1].Length;
【讨论】:
如果小数是整数,这不会失败吗?[1]
【参考方案17】:
我建议使用这种方法:
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);
【讨论】:
【参考方案18】:我实际上对这里的大多数解决方案进行了性能测试。有些快速但不可靠,有些可靠但不快速。通过修改@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);
【讨论】:
以上是关于无论文化如何,查找十进制值的小数位数的主要内容,如果未能解决你的问题,请参考以下文章
Javascript如何将十进制数转换为具有特定小数位数的字符串