NumberParser





5.00/5 (5投票s)
扩展 .NET 数值支持的库
引言
NumberParser 简化了 .NET 数值类型的用法,进一步最大化了 decimal
的高精度,并扩展了默认的数学支持。
此外,该库还允许处理超出 double
范围的值,并内部管理所有错误,而不会抛出异常。
NumberParser 是 FlexibleParser 的第二部分,FlexibleParser 是一组多用途的独立 .NET 解析库(在 codeproject.com 上的第一部分:UnitParser)。
本文档引用 NumberParser v. 1.0.8.5(稳定版)。
请注意,我还开发了该库的 Java 版本。要了解更多信息,请访问我主站上的相应页面。
背景
默认 .NET 数值类型管理中有三个主要方面与 NumberParser 相关
- 它在各种约束下支持不同的数值类型,这些约束并不立即完全兼容。
NumberParser 通过dynamic
变量或一个同时支持多种原生类型的类来消除数值类型之间的所有界限。此外,其定义的结构Value
(任意类型)* 10^BaseTenExponent
(int
)允许处理所需大小的数字。 System.Math
方法相当全面,但肯定有改进空间。
NumberParser 的Math2
类包含了 NumberParser 适应类的所有System.Math
方法以及扩展默认 .NET 功能的自定义方法。- .NET
decimal
类型的高精度并不总是得到充分利用。Math2.PowDecimal
和Math2.SqrtDecimal
方法依赖于一种自定义的指数化方法,该方法专门用于最大化decimal
的精度。
代码分析
超越个体数值类型:NumberX
NumberX 是 Number
、NumberD
、NumberO
和 NumberP
类的通用名称,它们提供了 NumberParser 实现预期数值类型同质化的基本条件。所有这些类都具有以下特性:
- 由
Value
*10^BaseTenExponent
定义,因此支持绰绰有余的范围。 - 所有错误都内部管理,不抛出异常。
- 直观的基本算术和比较运算符支持(运算符重载)。
- 它们之间以及与它们的定义原生类型之间可以隐式转换。
所有这些类都有其特定的特征,即:
Number
。它是最轻量级的,其Value
是decimal
。NumberD
。其Value
是dynamic
,因此它可以处理任何原生数值类型。NumberO
。其定义特征是Others
公有属性,这是一个NumberD
变量集合,包含用户指定的原生数值类型。NumberP
。它可以从字符串中提取数值信息,其Value
是dynamic
。
所有这些类都根据其自己的代码定义,包含在标记清晰的文件和文件夹中,例如 Constructors_Number.cs (位于 Constructors 文件夹内)或 Operations_Public_NumberD.cs (位于 Operations/Public 文件夹内)。所有处理通用部分的代码都依赖于尽可能轻量级的版本:Number
用于 decimal
计算,NumberD
用于任何其他场景。
在这些行下方,我包含了一个 NumberD
构造函数代码的摘录。它很好地展示了这段代码的大部分内容:一些公有属性通过 getter/setter 自动同步;以及大量的构造函数(其中上述属性以与该同步兼容的顺序填充),允许以多种方式实例化这些类,并且在单参数构造函数的情况下,还可以隐式转换为其他 NumberX 类/原生类型。
///<summary>
///<para>NumberD extends the limited decimal-only range of Number by supporting all the numeric
///types.</para>
///<para>It is implicitly convertible to Number, NumberO, NumberP and all the numeric types.</para>
///</summary>
public partial class NumberD
{
private dynamic _Value;
private Type _Type;
<summary><para>Numeric variable storing the primary value.</para></summary>
public dynamic Value
{
get { return _Value; }
set
{
Type type = ErrorInfoNumber.InputTypeIsValidNumeric(value);
if (type == null) _Value = null;
else
{
_Value = value;
if (_Value == 0) BaseTenExponent = 0;
}
if (Type != type) Type = type;
}
}
///<summary><para>Base-ten exponent complementing the primary value.</para></summary>
public int BaseTenExponent { get; set; }
///<summary><para>Numeric type of the Value property.</para></summary>
public Type Type
{
get { return _Type; }
set
{
if (Value != null && value != null)
{
if (Value.GetType() == value) _Type = value;
else
{
NumberD tempVar = new NumberD(Value, BaseTenExponent, value, false);
if (tempVar.Error == ErrorTypesNumber.None)
{
_Type = value;
BaseTenExponent = tempVar.BaseTenExponent;
Value = tempVar.Value;
}
//else -> The new type is wrong and can be safely ignored.
}
}
}
}
///<summary><para>Readonly member of the ErrorTypesNumber enum which best suits the current
///conditions.</para></summary>
public readonly ErrorTypesNumber Error;
///<summary><para>Initialises a new NumberD instance.</para></summary>
///<param name="type">Type to be assigned to the dynamic Value property. Only numeric types
///are valid.</param>
public NumberD(Type type)
{
Value = Basic.GetNumberSpecificType(0, type);
Type = type;
}
///<summary><para>Initialises a new NumberD instance.</para></summary>
///<param name="value">Main value to be used. Only numeric variables are valid.</param>
///<param name="baseTenExponent">Base-ten exponent to be used.</param>
public NumberD(dynamic value, int baseTenExponent)
{
Type type = ErrorInfoNumber.InputTypeIsValidNumeric(value);
if (type == null)
{
Error = ErrorTypesNumber.InvalidInput;
}
else
{
//To avoid problems with the automatic actions triggered by some setters, it is
//better to always assign values in this order (i.e., first BaseTenExponent, then
//Value and finally Type).
BaseTenExponent = baseTenExponent;
Value = value;
Type = type;
}
}
///<summary><para>Initialises a new NumberD instance.</para></summary>
///<param name="value">Main value to be used. Only numeric variables are valid.</param>
///<param name="type">Type to be assigned to the dynamic Value property. Only numeric types
///are valid.</param>
public NumberD(dynamic value, Type type)
{
NumberD numberD = ExtractValueAndTypeInfo(value, 0, type);
if (numberD.Error != ErrorTypesNumber.None)
{
Error = numberD.Error;
}
else
{
BaseTenExponent = numberD.BaseTenExponent;
Value = numberD.Value;
Type = type;
}
}
//etc.
}
要了解更多关于 NumberX 实现的信息,您可以访问 varocarbas.com 上的相应页面:https://varocarbas.com/flexible_parser/number_numberx/、https://varocarbas.com/flexible_parser/number_numbero/ 和 https://varocarbas.com/flexible_parser/number_numberp/。
NumberX 实例之间的基本操作和比较
在创建 NumberX 类之后,下一步是简化其在最常见场景下的使用,对于数值类型而言,这些场景是基本算术和比较操作。
我的方法是通过为每个类重载运算符来执行所有这些操作,例如,这允许执行类似 NumberD result = new NumberD(1.2345) + new NumberD(5555);
的操作。在所有 NumberX 类中,基本算术(+、-、* 和 /)和比较(==、!=、>、>=、<、<=)运算符都已重载。
上述操作预计将在同一 NumberX 类实例之间执行。换句话说,隐式 NumberX 转换不适用于重载运算符。例如,new NumberD(567) * new NumberP("12.3")
是错误的,而 new NumberD(567) * (NumberD)new NumberP("12.3")
是正确的。相同的规则不适用于原生类型之间的隐式转换,这就是为什么 new Number(987.6m) + 777m
是可以的。
上述限制是由 dynamic
类型特性引起的。要避免当前错误(即,NumberX 类的歧义确定)的唯一方法是显式重载 NumberX 类之间所有可能的组合。实现这种可能性从未是选项,因为它会导致代码大小和 NumberX 类相关资源的不可接受的增加,这将对其性能产生重大负面影响。仅仅为了完成避免在非常特定条件下进行强制转换的无关目标而做所有这些事情并没有太多意义。
作为 FlexibleParser 的一部分,NumberParser 依赖于与其他部分相同的默认假设,并且在不兼容的情况下(例如,不同的 NumberX 类或不同的 Value
类型),从左上角开始的第一个元素将始终被优先考虑。
处理这一切的最重要部分是以下内容:
- Operations/Public 文件夹。此处包含所有 NumberX 类的所有方法/运算符重载和隐式转换(即调用相应的单参数构造函数)。
- Operations/Private 文件夹。它包含上述公有资源使用的大部分内部资源。请注意,其中一个文件(Operations_Private_Managed.cs)包含 UnitParser 文章中讨论的托管操作的改编版本。
- Conversions 文件夹。由于所有 NumberX 类都必须能够无差别地处理不同的数值类型,因此转换也与基本操作密切相关。无论如何,请记住,这些只是仅在需要时才发生错误、将原生类型适配到 NumberX 格式的自定义转换,而不是原生类型之间的标准转换。例如,将整数 100000000 转换为
byte
时不会丢失任何信息,因为超出最大byte
范围的所有部分都存储在关联的BaseTenExponent
中。
以下代码可以很好地描述转换部分:
private static Number ModifyValueToFitType(Number number, Type target, decimal targetValue)
{
decimal sign = 1m;
if (number.Value < 0)
{
sign = -1m;
number.Value *= sign;
}
if (!Basic.AllDecimalTypes.Contains(target))
{
number.Value = Math.Round(number.Value, MidpointRounding.AwayFromZero);
}
targetValue = Math.Abs(targetValue);
bool increase = (number.Value < targetValue);
while (true)
{
if (number.Value == targetValue) break;
else
{
if (increase)
{
if
(
number.Value > Basic.AllNumberMinMaxPositives
[
typeof(decimal)
]
[1] / 10m
)
{ break; }
number.Value *= 10;
number.BaseTenExponent--;
if (number.Value > targetValue) break;
}
else
{
if
(
number.Value < Basic.AllNumberMinMaxPositives
[
typeof(decimal)
]
[0] * 10m
)
{ break; }
number.Value /= 10;
number.BaseTenExponent++;
if (number.Value < targetValue) break;
}
}
}
number.Value *= sign;
return number;
}
Math2 方法概述
在对一组类进行同质化数值类型管理及其所有基本比较/操作之后,扩展它们的数学支持似乎是下一步。在 .NET Framework 中,主要的内置数学方法存储在 System.Math
下,而 NumberParser 的等效方法是 Math2
。
有一组 Math2
方法,它们只是 System.Math
所有方法的 NumberX 适应版本。每个方法都产生与原始版本完全相同的结果。其全部目的是方便 NumberX 实例使用最常见的数学功能。即使是默认支持(例如,在大多数情况下是 double
范围),也会被尊重,并且(内部管理的)错误会被触发,尽管相应的 NumberX 类可以处理这些条件。
在下面的行中,您可以看到包含在这部分代码中的描述性摘录,位于 Math2_Private_Existing.cs
private delegate double Method1Arg(double value);
private delegate double Method2Arg(double value1, double value2);
private static Dictionary<ExistingOperations, Method1Arg> AllMathDouble1 =
new Dictionary<ExistingOperations, Method1Arg>()
{
{ ExistingOperations.Acos, Math.Acos }, { ExistingOperations.Asin, Math.Asin},
{ ExistingOperations.Atan, Math.Atan }, { ExistingOperations.Cos, Math.Cos },
{ ExistingOperations.Cosh, Math.Cosh }, { ExistingOperations.Exp, Math.Exp },
{ ExistingOperations.Log, Math.Log }, { ExistingOperations.Log10, Math.Log10 },
{ ExistingOperations.Sin, Math.Sin }, { ExistingOperations.Sinh, Math.Sinh },
{ ExistingOperations.Sqrt, Math.Sqrt }, { ExistingOperations.Tan, Math.Tan },
{ ExistingOperations.Tanh, Math.Tanh }
};
private static Dictionary<ExistingOperations, Method2Arg> AllMathDouble2 =
new Dictionary<ExistingOperations, Method2Arg>()
{
{ ExistingOperations.Atan2, Math.Atan2 },
{ ExistingOperations.IEEERemainder, Math.IEEERemainder },
{ ExistingOperations.Log, Math.Log }, { ExistingOperations.Pow, Math.Pow }
};
private static NumberD PerformOperationOneOperand(NumberD n, ExistingOperations operation)
{
NumberD n2 = AdaptInputsToMathMethod(n, GetTypesOperation(operation), operation);
if (n2.Error != ErrorTypesNumber.None) return new NumberD(n2.Error);
try
{
return ApplyMethod1(n2, operation);
}
catch
{
return new NumberD(ErrorTypesNumber.NativeMethodError);
}
}
private static NumberD PerformOperationTwoOperands(NumberD n1, NumberD n2, ExistingOperations operation)
{
NumberD[] ns = CheckTwoOperands
(
new NumberD[] { n1, n2 }, operation
);
if (ns[0].Error != ErrorTypesNumber.None) return ns[0];
try
{
return ApplyMethod2(ns[0], ns[1], operation);
}
catch
{
return new NumberD(ErrorTypesNumber.NativeMethodError);
}
}
private static NumberD[] CheckTwoOperands(NumberD[] ns, ExistingOperations operation)
{
ns = OrderTwoOperands(ns);
for (int i = 0; i < ns.Length; i++)
{
ns[i] = AdaptInputsToMathMethod
(
ns[i], (i == 0 ? GetTypesOperation(operation) : new Type[] { ns[0].Type }),
operation
);
if (ns[i].Error != ErrorTypesNumber.None)
{
return new NumberD[] { new NumberD(ns[i].Error) };
}
}
return ns;
}
Math2
类还包含以下一组自定义数学方法,这些方法是我从头开始开发的:
- GetPolynomialFit/ApplyPolynomialFit。它们计算一组 X/Y 值的 2 次多项式拟合,并将其应用于估计与 X2 输入相关的 Y2。
- Factorial。它计算小于 100000 的正整数的阶乘。
- RoundExact/TruncateExact。这些方法显著扩展了内置的 .NET 四舍五入/截断功能。它们允许将四舍五入/截断操作集中在整数/小数部分,并例如从输入 123.567 返回 123、124 或 123.6。
- PowDecimal/SqrtDecimal。下一节将对此进行讨论。
最有趣的代码是处理 RoundExact
/TruncateExact
的代码,这是其描述性样本:
private static decimal RoundInternalAfterZeroes(decimal d, int digits, RoundType type, decimal d2, int zeroCount)
{
if (digits < zeroCount)
{
//Cases like 0.001 with 1 digit or 0.0001 with 2 digits can reach this point.
//On the other hand, something like 0.001 with 2 digits requires further analysis.
return Math.Floor(d) +
(
type != RoundType.AlwaysAwayFromZero ? 0m :
1m / Power10Decimal[digits]
);
}
//d3 represent the decimal part after all the heading zeroes.
decimal d3 = d2 * Power10Decimal[zeroCount];
d3 = DecimalPartToInteger(d3 - Math.Floor(d3), 0, true);
int length3 = GetIntegerLength(d3);
decimal headingBit = 0;
digits -= zeroCount;
if (digits == 0)
{
//In a situation like 0.005 with 2 digits, the number to be analysed would be
//05 what cannot be (i.e., treated as 5, something different). That's why, in
//these cases, adding a heading number is required.
headingBit = 2; //2 avoids the ...ToEven types to be misinterpreted.
d3 = headingBit * Power10Decimal[length3] + d3;
digits = 0;
}
decimal output =
(
RoundExactInternal(d3, length3 - digits, type)
/ Power10Decimal[length3]
)
- headingBit;
return Math.Floor(d) +
(
output == 0m ? 0m :
output /= Power10Decimal[zeroCount]
);
}
要了解更多关于 Math2 方法的信息,您可以访问 varocarbas.com 上的相应页面:https://varocarbas.com/flexible_parser/number_native/ 和 https://varocarbas.com/flexible_parser/number_custom/。
Math2.PowDecimal 和 Math2.SqrtDecimal
内置的 .NET 指数化方法旨在最大化浮点数的特性(double
类型);这意味着大部分精力都集中在尽快提供合理准确的结果上。另一个相关问题是,特定的实现是私有的,并且无论如何都不太可能轻松地适应非浮点场景。
几乎所有编程语言都依赖于浮点方法来处理小数数值类型。 .NET 中的 decimal
类型是为数不多的例外之一,这正是我必须开发自定义方法来充分利用其定义的高精度原因。我将仅引用处理分数指数的实现,因为处理所有其他情况(例如,整数或负指数)非常简单。
我依赖于非常快速、可靠且全新的(开玩笑)牛顿-拉夫逊方法。这种方法的主要局限性在于其收敛速度高度依赖于提供足够好的初始猜测。在不太苛刻的条件下,不好的初始猜测影响不大,但在尝试最大化 decimal
精度时,它非常重要。请注意,此类型最多可以处理 28 位小数,这意味着执行操作和比较值的精度高达 10^-28。换句话说,除非初始猜测足够好,否则很容易陷入真正或实际上(即,需要无法接受的时间)的无限循环。
因此,Math2.PowDecimal
/Math2.SqrtDecimal
代码中最相关部分是我提出的确保牛顿-拉夫逊方法获得足够好的初始猜测的方法。考虑到它计算 n
次根,完美的猜测是实际根,并且 x
的 n
次根必须或多或少与 x
的 n-1
次根和 x
的 n+1
次根定义的趋势一致,我生成了大量 x
与 x
的 n
次根的配对,针对大量不同的 n
值。然后,我寻找潜在的趋势,总结了这些结论,并创建了在或多或少大的范围内复制这些行为的方程。请注意,这一部分只涉及处理正整数 n
和 10 可除的 x
。
尽管当前的方法已经相当快速可靠,但它仍然是第一版,我预计将来会进一步改进。因此,这部分代码没有包含太多注释:它仍在开发中。无论如何,这是其描述性样本:
private static decimal GetSmallValueBase10Guess(decimal value, decimal n)
{
decimal[] vals = new decimal[]
{
0.4605m, 0.5298m, 0.5704m, 0.5991m, 0.6215m
};
int index = (int)(value / 100m);
decimal ratio = (value - index * 100m) / 100m;
index--;
decimal outVal = vals[index];
if (ratio != 1m && index < 4)
{
outVal = vals[index] + ratio * (vals[index + 1] - vals[index]);
}
return 1m + outVal / Power10Decimal[GetIntegerLength(n) - 2];
}
private static decimal GetGenericBase10Guess(decimal value, decimal n)
{
bool small = false;
decimal value2 = GetInverseValue(value);
if (value2 != -1m)
{
small = true;
value = value2;
}
decimal outVal = 1m;
int exponent = GetIntegerLength(n) - 1;
if (value >= 500m)
{
decimal ratio = value / 500m;
if (ratio >= 100m)
{
exponent--;
if (ratio >= 1000m)
{
int length = GetIntegerLength(ratio);
//length -> addition
//4 -> 0.25
//5 -> 0.5
//6 -> 0.75
//7 -> 1
//8 -> 1.25
//...
decimal rem = length % 4;
outVal = length / 4 + 0.25m * (rem + 1m);
}
}
else if (ratio >= 10m) outVal *= 9m;
else if (ratio >= 1m) outVal *= 5m;
}
return
(
!small ? 1m + outVal / Power10Decimal[exponent] :
(1m - 1m / Power10Decimal[exponent]) + outVal / Power10Decimal[exponent + 1]
);
}
我在 https://varocarbas.com/fractional_exponentiation/(PDF)中对该实现进行了更详细的分析。
使用代码
NumberParser(在 FlexibleParser
命名空间内)提供了一个通用的框架来处理所有 .NET 数值类型。它依赖于以下四个类(NumberX):
Number
仅支持decimal
类型。NumberD
可以通过dynamic
支持任何数值类型。NumberO
可以同时支持不同的数值类型。NumberP
可以从字符串解析数字。
//1.23m (decimal). Number number = new Number(1.23m); //123 (int). NumberD numberD = new NumberD(123); //1.23 (decimal). Others: 1 (int) and ' ' (char). NumberO numberO = new NumberO(1.23m, new Type[] { typeof(int), typeof(char) }); //1 (long). NumberP numberP = new NumberP("1.23", new ParseConfig(typeof(long)));
通用功能
所有 NumberX 类都有许多共同的特征。
- 根据
Value
(decimal
或dynamic
)和BaseTenExponent
(int
)字段定义。它们都支持 [-1, 1] * 10^2147483647 之外的范围。 - 最常见的算术和比较运算符支持。
- 错误内部管理,不抛出异常。
- 大量的实例化选择。它们之间以及与相关类型之间可以隐式转换。
//12.3*10^456 (decimal). Number number = new Number(12.3m, 456); //123 (int). Number numberD = ( new NumberD(123) < (NumberD)new Number(456) ? //123 (int). new NumberD(123.456, typeof(int)) : //123.456 (double). new NumberD(123.456) ); //Error (ErrorTypesNumber.InvalidOperation) provoked when dividing by zero. NumberO numberO = new NumberO(123m, OtherTypes.IntegerTypes) / 0m; //1234*10^5678 (decimal). NumberP numberP = (NumberP)"1234e5678";
Math2 类
此类包含所有 NumberParser 的数学功能。
自定义功能
PowDecimal
/SqrtDecimal
,其基于decimal
的算法比System.Math
版本更精确。整个 varocarbas.com Project 10 解释了它们底层的计算方法。RoundExact
/TruncateExact
可以处理原生方法不支持的多种四舍五入/截断场景。GetPolynomialFit
/ApplyPolynomialFit
允许处理二次多项式拟合。Factorial
计算任何整数(最多 100000)的阶乘。
//158250272872244.91791560253776 (decimal). Number number = Math2.PowDecimal(123.45m, 6.789101112131415161718m); //123000 (decimal). Number number = Math2.RoundExact ( 123456.789m, 3, RoundType.AlwaysToZero, RoundSeparator.BeforeDecimalSeparator ); //30 (decimal). NumberD numberD = Math2.ApplyPolynomialFit ( Math2.GetPolynomialFit ( new NumberD[] { 1m, 2m, 4m }, new NumberD[] { 10m, 20m, 40m } ) , 3 ); //3628800 (int). NumberD numberD = Math2.Factorial(10);
原生方法
Math2
还包含 System.Math
所有方法的 NumberD
适应版本。
//158250289837968.16 (double). NumberD numberD = Math2.Pow(123.45, 6.789101112131415161718); //4.8158362157911885 (double). NumberD numberD = Math2.Log(123.45m);
更多代码示例
中的 测试应用程序 包含大量描述性代码示例。
关注点
用户友好的格式,允许轻松处理所有 .NET 数值类型,而无需担心转换或范围限制。
扩展了默认的数学支持,重点在于最大化与 decimal
相关的高精度。
它可以处理所需大小的数字,并内部管理所有错误。
作者
我,Alvaro Carballo Garcia,是本文档以及所有引用的 NumberParser/FlexibleParser 资源(如代码或文档)的唯一作者。