65.9K
CodeProject 正在变化。 阅读更多。
Home

UnitParser

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2017年10月20日

CPOL

11分钟阅读

viewsIcon

50651

downloadIcon

354

全面的单元解析库

引言

UnitParser 提供了一种可靠的方式,可以轻松地与各种测量单位进行交互。其可适应的格式允许在几乎任何情况下直观地处理与单位相关的信息。

它还包括其他相关功能,例如可配置的异常触发器或优雅地处理任意大小的数值。

UnitParser 是 `FlexibleParser` 的第一部分,`FlexibleParser` 是一个多用途的独立 .NET 解析库组(第二个部分在 codeproject.com 上:NumberParser)。

本文档引用 UnitParser v. 1.0.9.0(稳定版)。

此外,请注意,此库还有一个Web API 和一个Java 版本

背景

测量单位代表着一种复杂的现实,即使在今天,也尚未完全系统化。

传统上,大多数软件方法主要关注问题的最简单方面:单个单位/转换因子。大多数软件包通常忽略诸如不同的单位系统(例如,SI 或英制)、复合单位(例如,kg*m/s^2 等于 N)或简化(例如,kg*m/kg 等于 m)之类的问题。也很难找到一个软件可以将基于 string 的输入转换为安全的编程结构,从而可以轻松地管理这种复杂的现实。

UnitParser 旨在通过应用以下思想来克服上述通常的局限性:

  • 全面的分类,涵盖所有可能的情况。每个单位都根据以下内容定义:系统(SI、英制、USCS 或 CGS)、类型(所有类型)、特定名称(所有名称)、符号/缩写(所有符号),如果适用、前缀(所有前缀),如果适用,以及组成部分(例如,N 由 kg、m 和 s^-2 组成)。
  • 程序员友好的结构,允许以尽可能直观的方式处理任何情况。有一个主类(UnitP)处理所有可能的情况并内部管理潜在的错误/不兼容性。例如:new UnitP("m/s") 是可以的(m/s 是有效的 SI 速度单位),但 new UnitP("m*s") 则不行。
  • 最少量的用户输入和系统化、一致地应用的规则。例如,在不兼容的情况下,从左上角开始的第一个元素定义的条件将始终优先。
  • 主要关注较新/常用单位。
  • 尽可能使用形式上正确的替代方案。
  • 仅在严格需要时创建自定义格式。它们必须始终保持简单和一致。
  • 所有分类均应用“如有疑问,则默认/无”的规则。

代码分析

UnitParser 代码非常庞大且复杂。它没有一个可以轻松概括的明确定义的结构。由于其难度或实现上的特殊性,它不包含值得特别强调的特定部分。我在从头开始开发合理复杂的软件、.NET Framework/C# 和测量单位方面拥有丰富的经验;当前的代码只是这一现实的产物:一位经验丰富的开发人员,针对他感到非常舒适的复杂问题提出了一个全面的解决方案。因此,我认为要对这段代码有一个好的认识,最好的方法是仔细分析它;例如,通过调试其描述性的测试示例代码在 GitHub 上)。

几个月前,我提交了一篇关于该库早期版本的文章。当时我没有得到很好的反馈,主要是因为没有太详细地解释代码。尽管我不喜欢那样的反馈,并最终删除了那次提交,但我还是应用了那些想法,并为代码中最相关的部分编写了描述性文本。当前的分析有所不同,但也引用了这些其他资源中最相关的部分。

希望下面这些内容能帮助人们对 UnitParser 产生兴趣,并继续进行我(在我看来)理想的、对代码进行适当分析。

一个类统领一切:UnitP

UnitParser 的一个定义性特征是,与其它软件解决方案不同,它仅依赖于一个类来处理所有单位。从一开始,我的意图就是开发一种非常全面的方法,其中海量的情况表明不应采用更符合逻辑的多类替代方案。

UnitP 拥有大量不同的构造函数,支持多种场景,并且由以下代码片段中列出的 public 变量定义:

///<summary><para>Basic UnitParser class containing all the information
///about units and values.</para></summary>
public partial class UnitP
{
    ///<summary><para>Member of the Units enum which best suits the 
    ///current conditions.</para></summary>
    public readonly Units Unit;

    ///<summary><para>Member of the UnitTypes enum which best suits the 
    ///current conditions.</para></summary>
    public readonly UnitTypes UnitType;

    ///<summary><para>Member of the UnitSystems enum which best suits the 
    ///current conditions.</para></summary>
    public readonly UnitSystems UnitSystem;

    ///<summary><para>Prefix information affecting all the unit parts.</para></summary>
    public readonly Prefix UnitPrefix = new Prefix();

    ///<summary><para>List containing the basic unit parts which define the
    ///current unit.</para></summary>
    public ReadOnlyCollection<UnitPart> UnitParts;

    ///<summary><para>String variable including the unit information which was 
    ///input at variable instantiation.</para></summary>
    public readonly string OriginalUnitString;

    ///<summary><para>String variable containing the symbol(s) best describing 
    ///the current unit.</para></summary>
    public readonly string UnitString;

    ///<summary><para>String variable including both numeric and unit 
    ///information associated with the current conditions.</para></summary>
    public readonly string ValueAndUnitString;

    ///<summary><para>Base-ten exponent used when dealing with too small/big 
    ///numeric values.</para></summary>
    public readonly int BaseTenExponent;

    ///<summary><para>ErrorInfo variable containing all the error- and 
    ///exception-related information.</para></summary>
    public readonly ErrorInfo Error;

    ///<summary><para>Decimal variable storing the primary numeric information 
    ///under the current conditions.</para></summary>
    public decimal Value { get; set; }

    //etc.
}

UnitP 主要由 readonly 字段定义,这是其内部处理一切的性质所带来的正常结果。请注意,其他方法,例如自动更新的 getter/setter,由于输入条件略有变化也需要大量内部检查,因此会增加相关的复杂性。得益于这种 readonly 设置,整个过程可以简化为三个主要部分:

  1. 通过其中一个公共构造函数提供的定义明确的输入
  2. 根据所考虑的输入类型进行专门分析
  3. 填充 readonly 公共变量

总之,所有 UnitP 实例变量都可以假定为有效,它们要么包含支持的单位信息,要么包含错误(默认情况下,在内部处理,不抛出任何异常)。

UnitP newton = new UnitP(Units.Newton);
UnitP newton2 = new UnitP("kg*m/s^2");
UnitP newton3 = new UnitP("kg*m*s-2");
UnitP newton4 = new UnitP("1000 g*m/s2");

UnitP wrong1 = new UnitP("asfasf");
UnitP wrong2 = new UnitP("kG*m/s2");
UnitP wrong3 = new UnitP("kg*m*s-3");
UnitP wrong4 = new UnitP("10_0 g*m/s2");

前四个变量是有效的实例,并且它们都彼此相同(1 N,SI,力)。下面的四个变量都是错误的,这个问题通过 Error 字段指示,但不会抛出异常(这只能通过依赖某些构造函数来完成)。

UnitP 实例之间的运算

依赖于一个类来处理如此多的不同场景的另一个重要方面是确保该类实例之间的所有运算都遵循预期规则。当处理测量单位和数值时,这至少包括算术和比较运算。

所有 UnitP 的公共重载/隐式运算都存储在 Operations/Operations_Public.cs 文件中;这是该代码的一小部分示例:

public partial class UnitP : IComparable<UnitP>
{
    ///<summary><para>Compares the current instance against another UnitP one.</para></summary>
    ///<param name="other">The other UnitP instance.</param>
    public int CompareTo(UnitP other)
    {
        return
        ( 
            this.BaseTenExponent == other.BaseTenExponent ?
            (this.Value * this.UnitPrefix.Factor).CompareTo
            (other.Value * other.UnitPrefix.Factor) :
            (this.BaseTenExponent.CompareTo(other.BaseTenExponent)
       );
    }

    ///<summary><para>Creates a new UnitP instance by relying on the most 
    ///adequate constructor.</para></summary>
    ///<param name="input">String input.</param>
    public static implicit operator UnitP(string input)
    {
        return new UnitP(input);
    }

    ///<summary><para>Creates a new UnitP instance by relying on the most 
    ///adequate constructor.</para></summary>
    ///<param name="input">Decimal input.</param>
    public static implicit operator UnitP(decimal input)
    {
        return new UnitP(input);
    }

    ///<summary><para>Creates a new UnitP instance by relying on the most 
    ///adequate constructor.</para></summary>
    ///<param name="input">Units input.</param>
    public static implicit operator UnitP(Units input)
    {
        return new UnitP(input);
    }

    ///<summary>
    ///<para>Adds two UnitP variables by giving preference to the configuration 
    ///of the first operand.</para>
    ///<para>Different unit types will trigger an error.</para>
    ///</summary>
    ///<param name="first">Augend. In case of incompatibilities, its configuration 
    ///would prevail.</param>
    ///<param name="second">Addend.</param>
    public static UnitP operator +(UnitP first, UnitP second)
    {
        return PerformUnitOperation
        (
            first, second, Operations.Addition,
            GetOperationString(first, second, Operations.Addition)
        );
    }

    //etc.
}

在执行任何运算之前,必须对两个 UnitP 实例进行分析,并且可能最终进行修改。在这些预检查期间,将考虑以下字段:

  • UnitType。不同类型之间的运算只有在特定条件下才能进行,如下文所述。
  • UnitParts。此集合包含单位的最精确定义。具有不同 UnitPartsUnitP 实例之间的运算是可能的,但很可能会发生自动转换(阅读下文)。
  • Error。一个或两个实例错误可能会影响运算结果。
  • 数值字段(例如,ValueBaseTenExponentUnitPrefix)。在确认两个实例兼容并执行了所有必需的操作(例如,转换)后,通过引入数值字段来执行相应的运算。

继续上面的代码示例之一,考虑以下运算:

UnitP allNewtons = newton + newton2 + newton3;
UnitP wrongOperation = new UnitP("m") + newton;

allNewtons 是一个有效实例(3 N,SI,力),但 wrongOperation 不是,因为米和牛顿不能相加。

在处理测量单位时,加法/减法(仅数值受影响)与乘法/除法(作为运算结果,会创建新单位)的处理方式不同。UnitParser 在每个级别都遵循这些特性;例如,new UnitP("m")/new UniP("s") 的输出与 new UnitP("m/s") 相同,即米/秒(SI,速度)。

在任何情况下,都建议对相对复杂的运算依赖基于 string 的方法,因为每个运算符重载都会单独进行分析,这可能会导致某些情况被误判。例如,new UnitP("m*s/s") 是可以的(1 米);但 new UnitP("m") * new UnitP("s") / new UnitP("s") 是错误的(在分析 new UnitP("m") * new UnitP("s") 时会触发错误)。

Unit 解析的特殊性

UnitParser 的主要目标之一是尽可能直观。支持输入 string 是实现这一目标的方式之一,但它也带来了大量可能的情况:各种有效、无效甚至奇怪但技术上正确的输入。

负责所有单位-string-解析部分的代码相当复杂,可以在 Parse 文件夹内的各个文件中找到。在下面,我包含了启动所有复合单位(即由多个组成元素组成的单位)解析操作的方法。

private static ParseInfo StartCompoundAnalysis(ParseInfo parseInfo)
{

    if (parseInfo.UnitInfo.Error.Type != ErrorTypes.None)
    { 
        return parseInfo;
    }

    if (parseInfo.ValidCompound == null)
    {
        parseInfo.ValidCompound = new StringBuilder();
    }

    parseInfo.UnitInfo = RemoveAllUnitInformation(parseInfo.UnitInfo);

    //Knowing the initial positions of all the unit parts is important because of the defining
    //"first element rules" idea which underlies this whole approach. Such a determination 
    //isn't always straightforward due to the numerous unit part modifications.
    parseInfo.UnitInfo = UpdateInitialPositions(parseInfo.UnitInfo);

    //This is the best place to determine the system before finding the unit, because the
    //subsequent unit part corrections might provoke some misunderstandings on this front
    //(e.g., CGS named compound divided into SI basic units).
    parseInfo.UnitInfo.System = GetSystemFromUnitInfo(parseInfo.UnitInfo);

    //This is also an excellent place to correct eventual system mismatches. For example:
    //N/pint where pint has to be converted into m3, the SI (first operand system) basic
    //unit for volume.
    parseInfo.UnitInfo = CorrectDifferentSystemIssues(parseInfo.UnitInfo);
    parseInfo.UnitInfo = ImproveUnitParts(parseInfo.UnitInfo);

    if (parseInfo.UnitInfo.Type == UnitTypes.None)
    {
        parseInfo.UnitInfo = GetUnitFromParts(parseInfo.UnitInfo);
    }

    parseInfo.UnitInfo = UpdateMainUnitVariables(parseInfo.UnitInfo);
    if (parseInfo.UnitInfo.Unit == Units.None)
    {
        parseInfo.UnitInfo.Error = new ErrorInfo(ErrorTypes.InvalidUnit);
    }
    else parseInfo = AnalyseValidCompoundInfo(parseInfo);

    return parseInfo;
}

UnitParser 可以处理以下 string 输入场景:

  • 有效符号、常见缩写和名称:new UnitP("s")new UnitP("sec")new UnitP("seConD") 都是指 1 秒的有效方式。
  • 复合单位的组成部分:new UnitP("kg*m/s2") 被理解为 1 N。
  • 可相互转换的单位组成的复合单位:new UnitP("kg*ft/s2") 被理解为 0.3048 N(第一个单位 kg 表明应考虑 SI;ft 不属于 SI,但可以通过 0.3048 m 直接转换)。

在解析由多个部分组成的 string 时,必须始终遵守一些规则:

  • 从左上角开始,第一个具有受支持系统(请注意,有大量单位假定不属于任何系统)的单位定义了整个复合体的系统。这有助于确定组成部分最终转换的目标(即,给定系统和类型的默认单位)。上面的 0.3048 N 示例对此特定场景提供了相当描述性的想法。
  • 只允许一个除法符号,它分隔分子和分母。

高质量信息

任何处理测量单位及其相关所有内容(表示、分类、转换等)的工具都必须依赖大量硬编码信息。在开发 UnitParser 时,我付出了相当大的努力来收集各种类型的高质量信息。

UnitParser 最相关的硬编码信息存储在 Keywords 文件夹下的文件中。这不仅包括更简单的格式(例如,符号或转换因子),还包括更复杂的格式,例如复合单位的定义,如下面的代码片段所示:

//Contains the definitions of all the supported compounds, understood as units formed by
//other units and/or variations (e.g., exponents different than 1) of them.
//In order to be as efficient as possible, AllCompounds ignores the difference between 
//dividable and non-dividable units. For example: N is formed by kg*m/s2, exactly what 
//this collection expects; on the other hand, lbf isn't formed by the expected lb*ft/s2. 
//In any case, note that this "faulty" format is only used internally, never shown to 
//the user.
//NOTE: the order of the compounds within each type does matter. The first position is 
//reserved for the main fully-expanded version (e.g., mass*length/time2 for force). In 
//the second position, the compound basic units (e.g., force) are expected to have their 
//1-part version (e.g., 1 force part for force).
private static Dictionary<UnitTypes, Compound[]> AllCompounds = new Dictionary<UnitTypes, Compound[]>()
{
    {
        UnitTypes.Area, new Compound[]
        {
            new Compound
            (
                new List<CompoundPart>() { new CompoundPart(UnitTypes.Length, 2) }
            ),
            new Compound
            (
                new List<CompoundPart>() { new CompoundPart(UnitTypes.Area) }
            )
        }
    },
    {
        UnitTypes.Volume, new Compound[]
        {
            new Compound
            (
                new List<CompoundPart>() { new CompoundPart(UnitTypes.Length, 3) }
            ),
            new Compound
            (
                new List<CompoundPart>() { new CompoundPart(UnitTypes.Volume) }
            )
        }
    },
    {
        UnitTypes.Velocity, new Compound[]
        {
            new Compound
            (
                new List<CompoundPart>()
                {
                    new CompoundPart(UnitTypes.Length),
                    new CompoundPart(UnitTypes.Time, -1)
                }
            )
        }
    },
    {
        UnitTypes.Acceleration, new Compound[]
        {
            new Compound
            (
                new List<CompoundPart>()
                {
                    new CompoundPart(UnitTypes.Length),
                    new CompoundPart(UnitTypes.Time, -2)
                }
            )
        }
    },
    {
        UnitTypes.Force, new Compound[]
        {
            new Compound
            (
                new List<CompoundPart>()
                {
                    new CompoundPart(UnitTypes.Mass),
                    new CompoundPart(UnitTypes.Length),
                    new CompoundPart(UnitTypes.Time, -2)
                }
            ),
            new Compound
            (
                new List<CompoundPart>() { new CompoundPart(UnitTypes.Force) }
            )
        }
    },
    {
        UnitTypes.Energy, new Compound[]
        {
            new Compound
            (
                new List<CompoundPart>()
                {
                    new CompoundPart(UnitTypes.Mass),
                    new CompoundPart(UnitTypes.Length, 2),
                    new CompoundPart(UnitTypes.Time, -2)
                }
            ),
            new Compound
            (
                new List<CompoundPart>() { new CompoundPart(UnitTypes.Energy) }
            )
        }
    }

    //etc.
}

托管操作

最初考虑 UnitParser 时,我关心的问题之一是如何处理相关数值运算的固有困难。一方面,您拥有由值、前缀(例如,1 kg 等于 1000 g)和有时涉及不同指数的转换组成的复杂数值现实。另一方面,一个类处理所有错误/正确情况并内部管理异常的想法。所有这些现实似乎都超出了内置数值类型的能力,或者至少需要大量额外工作才能达到一个不完全受控的阶段。因此,实现这些托管操作从一开始就在我的待办事项列表中。

托管操作指的是所有涉及 UnitP 和数值变量的运算代码。从数值上看,UniP 实例由一个 decimal 值、一个 int 的十的幂指数以及最终一个前缀组成(例如,new UnitP("1 kg") 被理解为值为 1,前缀为 1000,十的幂指数为零;或值为 1000,前缀为 1,十的幂指数为 0;或值为 1,前缀为 1,十的幂指数为 3)。这种设置需要特殊的自定义计算和进一步的问题,例如内部处理所有错误(或管理错误,这正是“托管操作”的由来)。有趣的是,我已将此概念应用于 NumberParser,它是 FlexibleParser 的第二部分,它也可以处理任意大小的数字并在内部管理错误。

处理托管操作的主要代码存储在 Operations_Private_Managed.cs 文件中,在下面您可以找到相当具有描述性的示例。

private static UnitInfo ConvertBaseTenToValue(UnitInfo unitInfo)
{
    if (unitInfo.BaseTenExponent == 0) return unitInfo;

    UnitInfo outInfo = new UnitInfo(unitInfo);
    bool decrease = unitInfo.BaseTenExponent > 0;
    int sign = Math.Sign(outInfo.Value);
    decimal absValue = Math.Abs(outInfo.Value);

    while (outInfo.BaseTenExponent != 0m)
    {
        if (decrease)
        {
            if (absValue >= MaxValueDec / 10m) break;
            absValue *= 10m;
            outInfo.BaseTenExponent -= 1;
        }
        else
        {
            if (absValue <= MinValueDec * 10m) break;
            absValue /= 10m;
            outInfo.BaseTenExponent += 1;
        }
    }

    outInfo.Value = sign * absValue;

    return outInfo;
}

Using the Code

第一步是在您的代码中添加对 UnitParser.dll 的引用(命名空间 FlexibleParser)。请注意,UnitParser 也作为NuGet 包提供。

主类名为 UnitP,可以通过多种方式进行实例化。

//1 N. UnitP 
unitP = new UnitP("1 N"); 

//1 N. 
unitP = new UnitP(1m, UnitSymbols.Newton); 

//1 N. 
unitP = new UnitP(1m, "nEwTon"); 

//1 N. 
unitP = new UnitP(1m, Units.Newton);

UnitP 可以被看作是一个包含许多特定类型的抽象概念。同类型变量可以进行加/减运算。不同类型变量可以进行乘/除运算,但前提是能够生成有效的类型输出。

//2 N.
unitP = new UnitP("1 N") + new UnitP(1m, Units.Newton);

//1 J.
unitP = new UnitP("1 N") * new UnitP("1 m");

//Error not triggering an exception. 
//The output unit N*m^2 doesn't match any supported type.
unitP = new UnitP("1 N") * new UnitP("1 m") * new UnitP("1 m");

主要变量信息

UnitP 变量根据在实例化时填充的各种 readonly 字段进行定义。

  • Unit - 对应的 Units 成员
  • UnitType - 对应的 UnitTypes 成员
  • UnitSystem - 对应的 UnitSystems 成员
  • UnitParts - 定义给定单位的组成部分
  • UnitPrefix - 影响所有单位组成部分的支持前缀
  • BaseTenExponent - 处理过小/过大值时使用的十的幂指数
  • Error - 存储所有错误和异常相关信息的变量

一般规则

所有功能均基于以下思想:

  • 在不兼容的情况下,始终优先考虑第一个元素。
  • 默认情况下,优先选择形式上正确的替代方案。可能会执行一些必需的修改。
  • 默认情况下,所有错误都在内部处理。
//1.3048 m.
unitP = new UnitP("1 m") + new UnitP("1 ft"); 

//Error not triggering an exception. 
//The parser expects "km" or a full-name-based version like "KiLom".
unitP = new UnitP("1 Km"); 

//999999.999999900000 * 10^19 YSt.
unitP = 999999999999999999999999999999999999.9 * new UnitP("9999999999999 St");

Unit 字符串解析格式

单元 string 解析部分非常灵活,但有一些基本规则。

  • 字符串多部分单位预计仅由单位、乘除号和整数指数组成。
  • 只允许一个除法符号。解析器会将其前面的所有内容理解为分子,后面的所有内容理解为分母。
//Error not triggering an exception. 
//The parser expects "1 m" or any other version including a separating blank space.
unitP = new UnitP("1m"); 

//1 W.
unitP = new UnitP("1 J*J/s*J2*J-1*s*s-1");

//Error not triggering an exception. 
//The parser understands "J*J/(s*J2*s*J*s)", what doesn't represent a supported type.
unitP = new UnitP("1 J*J/(s*J2*s)*J*s");

数值支持

形式上,支持两种数值类型:decimal,几乎在所有地方;double,仅在与 UnitP 变量进行乘除运算时使用。实际上,UnitP 变量实现了一个混合系统,提供 decimal 精度和超越 double 范围的支持。

//7.81011 ft.
unitP = new UnitP("1 ft") * 7.891011m;

//1213141516 s.
unitP = new UnitP("1 s") * 1213141516.0;

//0.0003094346047382564187537561*10^-752 ym.
unitP = 0.0000000000000000000000000000000000000000000000001 * 
new UnitP(0.000000000000000000001m, "ym2") / 
new UnitP("999999999999999999999 Ym") / double.MaxValue / double.MaxValue; 

关注点

大量与测量单位相关的高质量硬编码信息。其中一部分可以通过 API 直接查看(例如,enumpublic 注释),另一部分可以通过使用不同的功能来享受(例如,分数简化和复合单位管理)。

相当强大的解析能力,使该库能够直接或经过少量修改,处理涉及测量单位的大量原始数据场景。

它可以处理任意大的数字,并在内部管理所有错误。

作者

我,Alvaro Carballo Garcia,是本文以及所有引用的 UnitParser/FlexibleParser 资源(如代码或文档)的唯一作者。

© . All rights reserved.