解析十进制数而不丢失有效数字
Parse decimal number without loosing significant digits
我需要将用户输入解析为数字并将其存储在 decimal 变量中。
不接受任何无法用 decimal 值正确表示的用户输入对我来说很重要。
这适用于非常大(或非常小)的数字,因为在这些情况下 Parse method throws an OverflowException。
然而,当一个数字有太多有效数字时,Parse 方法将默默地 return 截断(或四舍五入?)值。
例如,解析 1.23456789123456789123456789123
(30 位有效数字)的结果等于 1.2345678912345678912345678912
(29 位有效数字)。
根据 规范,decimal 值的精度为 28-29 位有效数字。
但是,我需要能够检测(并拒绝)在解析时将被截断的数字,因为在我的情况下丢失有效数字是不可接受的。
解决此问题的最佳方法是什么?
请注意,通过字符串比较进行预解析或 post 验证并不是一种简单的方法,因为我需要支持各种特定于文化的输入和各种 number styles(空格、千位分隔符、括号、指数语法等)。
因此,我正在寻找一种解决方案,而无需复制 .NET 提供的解析代码。
我目前正在使用此解决方法来检测具有 28 位或更多有效数字的输入。虽然这有效,但它有效地将所有输入限制为最多 27 个有效数字(而不是 28-29):
/// <summary>
/// Determines whether the specified value has 28 or more significant digits,
/// in which case it must be rejected since it may have been truncated when
/// we parsed it.
/// </summary>
static bool MayHaveBeenTruncated(decimal value)
{
const string format = "#.###########################e0";
string str = value.ToString(format, CultureInfo.InvariantCulture);
return (str.LastIndexOf('e') - str.IndexOf('.')) > 27;
}
假设输入是一个字符串并且已经验证为数字,您可以使用String.Split:
text = text.Trim().Replace(",", "");
bool neg = text.Contains("-");
if (neg) text = text.Replace("-", "");
while (text.Substring(0, 1) == 0 && text.Substring(0, 2) != "0." && text != "0")
text = text.Substring(1);
if (text.Contains("."))
{
while (text.Substring(text.Length - 1) == "0")
text = text.Substring(0, text.Length - 1);
}
if (text.Split(".")[0].Length + text.Split(".")[1].Length + (neg ? 1 : 0) <= 29)
valid = true;
您可以覆盖或替换 Parse 并包括此检查。
问题是当您进行对话时会处理四舍五入,即如果小数位数超过 28 位,Decimal myNumber = Decimal.Parse(myInput)
将始终 return 为四舍五入的数字。
您也不想创建大型解析器,所以我要做的是将输入字符串值与新的十进制值作为字符串进行比较:
//This is the string input from the user
string myInput = "1.23456789123456789123456789123";
//This is the decimal conversation in your application
Decimal myDecimal = Decimal.Parse(myInput);
//This is the check to see if the input string value from the user is the same
//after we parsed it to a decimal value. Now we need to parse it back to a string to verify
//the two different string values:
if(myInput.CompareTo(myDecimal.ToString()) == 0)
Console.WriteLine("EQUAL: Have NOT been rounded!");
else
Console.WriteLine("NOT EQUAL: Have been rounded!");
这样 C# 将处理所有数字内容,您只需进行快速检查。
您应该看看 BigRational 实现。它不是(还?).Net 框架的一部分,但它相当于 BigInteger class 并提供 TryParse 方法。这样你应该能够比较你解析的 BigRational 是否等于解析的小数。
首先声明 没有 "official" 解决方案。通常我不会依赖内部实施,所以我向您提供以下内容只是因为您说解决该问题对您来说非常重要。
如果你看一下参考源,你会发现所有的解析方法都是在一个(不幸的是内部的)System.Number class. Further investigating, the decimal
related methods are TryParseDecimal and ParseDecimal 中实现的,并且它们都使用类似这样的东西
byte* buffer = stackalloc byte[NumberBuffer.NumberBufferBytes];
var number = new NumberBuffer(buffer);
if (TryStringToNumber(s, styles, ref number, numfmt, true))
{
// other stuff
}
其中 NumberBuffer
是另一个内部 struct
。关键是整个解析发生在 TryStringToNumber
方法内部,结果用于生成结果。我们感兴趣的是一个名为 precision
的 NumberBuffer 字段,它由上述方法填充。
考虑到所有这些,我们可以生成一个类似的方法,只是在调用基本十进制方法之后提取精度,以确保在我们进行 post 处理之前正常 validation/exceptions。所以方法应该是这样的
static unsafe bool GetPrecision(string s, NumberStyles style, NumberFormatInfo numfmt)
{
byte* buffer = stackalloc byte[Number.NumberBuffer.NumberBufferBytes];
var number = new NumberBuffer(buffer);
TryStringToNumber(s, styles, ref number, numfmt, true);
return number.precision;
}
但请记住,这些类型及其方法都是内部类型,因此很难应用正常的反射、委托或基于 Expression
的技术。幸运的是,使用 System.Reflection.Emit
编写这样的方法并不难。完整实现如下
public static class DecimalUtils
{
public static decimal ParseExact(string s, NumberStyles style = NumberStyles.Number, IFormatProvider provider = null)
{
// NOTE: Always call base method first
var value = decimal.Parse(s, style, provider);
if (!IsValidPrecision(s, style, provider))
throw new InvalidCastException(); // TODO: throw appropriate exception
return value;
}
public static bool TryParseExact(string s, out decimal result, NumberStyles style = NumberStyles.Number, IFormatProvider provider = null)
{
// NOTE: Always call base method first
return decimal.TryParse(s, style, provider, out result) && !IsValidPrecision(s, style, provider);
}
static bool IsValidPrecision(string s, NumberStyles style, IFormatProvider provider)
{
var precision = GetPrecision(s, style, NumberFormatInfo.GetInstance(provider));
return precision <= 29;
}
static readonly Func<string, NumberStyles, NumberFormatInfo, int> GetPrecision = BuildGetPrecisionFunc();
static Func<string, NumberStyles, NumberFormatInfo, int> BuildGetPrecisionFunc()
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic;
const BindingFlags InstanceFlags = Flags | BindingFlags.Instance;
const BindingFlags StaticFlags = Flags | BindingFlags.Static;
var numberType = typeof(decimal).Assembly.GetType("System.Number");
var numberBufferType = numberType.GetNestedType("NumberBuffer", Flags);
var method = new DynamicMethod("GetPrecision", typeof(int),
new[] { typeof(string), typeof(NumberStyles), typeof(NumberFormatInfo) },
typeof(DecimalUtils), true);
var body = method.GetILGenerator();
// byte* buffer = stackalloc byte[Number.NumberBuffer.NumberBufferBytes];
var buffer = body.DeclareLocal(typeof(byte*));
body.Emit(OpCodes.Ldsfld, numberBufferType.GetField("NumberBufferBytes", StaticFlags));
body.Emit(OpCodes.Localloc);
body.Emit(OpCodes.Stloc, buffer.LocalIndex);
// var number = new Number.NumberBuffer(buffer);
var number = body.DeclareLocal(numberBufferType);
body.Emit(OpCodes.Ldloca_S, number.LocalIndex);
body.Emit(OpCodes.Ldloc, buffer.LocalIndex);
body.Emit(OpCodes.Call, numberBufferType.GetConstructor(InstanceFlags, null,
new[] { typeof(byte*) }, null));
// Number.TryStringToNumber(value, options, ref number, numfmt, true);
body.Emit(OpCodes.Ldarg_0);
body.Emit(OpCodes.Ldarg_1);
body.Emit(OpCodes.Ldloca_S, number.LocalIndex);
body.Emit(OpCodes.Ldarg_2);
body.Emit(OpCodes.Ldc_I4_1);
body.Emit(OpCodes.Call, numberType.GetMethod("TryStringToNumber", StaticFlags, null,
new[] { typeof(string), typeof(NumberStyles), numberBufferType.MakeByRefType(), typeof(NumberFormatInfo), typeof(bool) }, null));
body.Emit(OpCodes.Pop);
// return number.precision;
body.Emit(OpCodes.Ldloca_S, number.LocalIndex);
body.Emit(OpCodes.Ldfld, numberBufferType.GetField("precision", InstanceFlags));
body.Emit(OpCodes.Ret);
return (Func<string, NumberStyles, NumberFormatInfo, int>)method.CreateDelegate(typeof(Func<string, NumberStyles, NumberFormatInfo, int>));
}
}
使用风险自负:)
我需要将用户输入解析为数字并将其存储在 decimal 变量中。
不接受任何无法用 decimal 值正确表示的用户输入对我来说很重要。
这适用于非常大(或非常小)的数字,因为在这些情况下 Parse method throws an OverflowException。
然而,当一个数字有太多有效数字时,Parse 方法将默默地 return 截断(或四舍五入?)值。
例如,解析 1.23456789123456789123456789123
(30 位有效数字)的结果等于 1.2345678912345678912345678912
(29 位有效数字)。
根据 规范,decimal 值的精度为 28-29 位有效数字。
但是,我需要能够检测(并拒绝)在解析时将被截断的数字,因为在我的情况下丢失有效数字是不可接受的。
解决此问题的最佳方法是什么?
请注意,通过字符串比较进行预解析或 post 验证并不是一种简单的方法,因为我需要支持各种特定于文化的输入和各种 number styles(空格、千位分隔符、括号、指数语法等)。
因此,我正在寻找一种解决方案,而无需复制 .NET 提供的解析代码。
我目前正在使用此解决方法来检测具有 28 位或更多有效数字的输入。虽然这有效,但它有效地将所有输入限制为最多 27 个有效数字(而不是 28-29):
/// <summary>
/// Determines whether the specified value has 28 or more significant digits,
/// in which case it must be rejected since it may have been truncated when
/// we parsed it.
/// </summary>
static bool MayHaveBeenTruncated(decimal value)
{
const string format = "#.###########################e0";
string str = value.ToString(format, CultureInfo.InvariantCulture);
return (str.LastIndexOf('e') - str.IndexOf('.')) > 27;
}
假设输入是一个字符串并且已经验证为数字,您可以使用String.Split:
text = text.Trim().Replace(",", "");
bool neg = text.Contains("-");
if (neg) text = text.Replace("-", "");
while (text.Substring(0, 1) == 0 && text.Substring(0, 2) != "0." && text != "0")
text = text.Substring(1);
if (text.Contains("."))
{
while (text.Substring(text.Length - 1) == "0")
text = text.Substring(0, text.Length - 1);
}
if (text.Split(".")[0].Length + text.Split(".")[1].Length + (neg ? 1 : 0) <= 29)
valid = true;
您可以覆盖或替换 Parse 并包括此检查。
问题是当您进行对话时会处理四舍五入,即如果小数位数超过 28 位,Decimal myNumber = Decimal.Parse(myInput)
将始终 return 为四舍五入的数字。
您也不想创建大型解析器,所以我要做的是将输入字符串值与新的十进制值作为字符串进行比较:
//This is the string input from the user
string myInput = "1.23456789123456789123456789123";
//This is the decimal conversation in your application
Decimal myDecimal = Decimal.Parse(myInput);
//This is the check to see if the input string value from the user is the same
//after we parsed it to a decimal value. Now we need to parse it back to a string to verify
//the two different string values:
if(myInput.CompareTo(myDecimal.ToString()) == 0)
Console.WriteLine("EQUAL: Have NOT been rounded!");
else
Console.WriteLine("NOT EQUAL: Have been rounded!");
这样 C# 将处理所有数字内容,您只需进行快速检查。
您应该看看 BigRational 实现。它不是(还?).Net 框架的一部分,但它相当于 BigInteger class 并提供 TryParse 方法。这样你应该能够比较你解析的 BigRational 是否等于解析的小数。
首先声明 没有 "official" 解决方案。通常我不会依赖内部实施,所以我向您提供以下内容只是因为您说解决该问题对您来说非常重要。
如果你看一下参考源,你会发现所有的解析方法都是在一个(不幸的是内部的)System.Number class. Further investigating, the decimal
related methods are TryParseDecimal and ParseDecimal 中实现的,并且它们都使用类似这样的东西
byte* buffer = stackalloc byte[NumberBuffer.NumberBufferBytes];
var number = new NumberBuffer(buffer);
if (TryStringToNumber(s, styles, ref number, numfmt, true))
{
// other stuff
}
其中 NumberBuffer
是另一个内部 struct
。关键是整个解析发生在 TryStringToNumber
方法内部,结果用于生成结果。我们感兴趣的是一个名为 precision
的 NumberBuffer 字段,它由上述方法填充。
考虑到所有这些,我们可以生成一个类似的方法,只是在调用基本十进制方法之后提取精度,以确保在我们进行 post 处理之前正常 validation/exceptions。所以方法应该是这样的
static unsafe bool GetPrecision(string s, NumberStyles style, NumberFormatInfo numfmt)
{
byte* buffer = stackalloc byte[Number.NumberBuffer.NumberBufferBytes];
var number = new NumberBuffer(buffer);
TryStringToNumber(s, styles, ref number, numfmt, true);
return number.precision;
}
但请记住,这些类型及其方法都是内部类型,因此很难应用正常的反射、委托或基于 Expression
的技术。幸运的是,使用 System.Reflection.Emit
编写这样的方法并不难。完整实现如下
public static class DecimalUtils
{
public static decimal ParseExact(string s, NumberStyles style = NumberStyles.Number, IFormatProvider provider = null)
{
// NOTE: Always call base method first
var value = decimal.Parse(s, style, provider);
if (!IsValidPrecision(s, style, provider))
throw new InvalidCastException(); // TODO: throw appropriate exception
return value;
}
public static bool TryParseExact(string s, out decimal result, NumberStyles style = NumberStyles.Number, IFormatProvider provider = null)
{
// NOTE: Always call base method first
return decimal.TryParse(s, style, provider, out result) && !IsValidPrecision(s, style, provider);
}
static bool IsValidPrecision(string s, NumberStyles style, IFormatProvider provider)
{
var precision = GetPrecision(s, style, NumberFormatInfo.GetInstance(provider));
return precision <= 29;
}
static readonly Func<string, NumberStyles, NumberFormatInfo, int> GetPrecision = BuildGetPrecisionFunc();
static Func<string, NumberStyles, NumberFormatInfo, int> BuildGetPrecisionFunc()
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic;
const BindingFlags InstanceFlags = Flags | BindingFlags.Instance;
const BindingFlags StaticFlags = Flags | BindingFlags.Static;
var numberType = typeof(decimal).Assembly.GetType("System.Number");
var numberBufferType = numberType.GetNestedType("NumberBuffer", Flags);
var method = new DynamicMethod("GetPrecision", typeof(int),
new[] { typeof(string), typeof(NumberStyles), typeof(NumberFormatInfo) },
typeof(DecimalUtils), true);
var body = method.GetILGenerator();
// byte* buffer = stackalloc byte[Number.NumberBuffer.NumberBufferBytes];
var buffer = body.DeclareLocal(typeof(byte*));
body.Emit(OpCodes.Ldsfld, numberBufferType.GetField("NumberBufferBytes", StaticFlags));
body.Emit(OpCodes.Localloc);
body.Emit(OpCodes.Stloc, buffer.LocalIndex);
// var number = new Number.NumberBuffer(buffer);
var number = body.DeclareLocal(numberBufferType);
body.Emit(OpCodes.Ldloca_S, number.LocalIndex);
body.Emit(OpCodes.Ldloc, buffer.LocalIndex);
body.Emit(OpCodes.Call, numberBufferType.GetConstructor(InstanceFlags, null,
new[] { typeof(byte*) }, null));
// Number.TryStringToNumber(value, options, ref number, numfmt, true);
body.Emit(OpCodes.Ldarg_0);
body.Emit(OpCodes.Ldarg_1);
body.Emit(OpCodes.Ldloca_S, number.LocalIndex);
body.Emit(OpCodes.Ldarg_2);
body.Emit(OpCodes.Ldc_I4_1);
body.Emit(OpCodes.Call, numberType.GetMethod("TryStringToNumber", StaticFlags, null,
new[] { typeof(string), typeof(NumberStyles), numberBufferType.MakeByRefType(), typeof(NumberFormatInfo), typeof(bool) }, null));
body.Emit(OpCodes.Pop);
// return number.precision;
body.Emit(OpCodes.Ldloca_S, number.LocalIndex);
body.Emit(OpCodes.Ldfld, numberBufferType.GetField("precision", InstanceFlags));
body.Emit(OpCodes.Ret);
return (Func<string, NumberStyles, NumberFormatInfo, int>)method.CreateDelegate(typeof(Func<string, NumberStyles, NumberFormatInfo, int>));
}
}
使用风险自负:)