测量单位转换库






3.92/5 (8投票s)
该项目允许您存储带有单位信息的值,并在不同测量系统之间进行转换。还提供了一个层次结构,可以轻松创建新的转换器。
引言
我通常构建可本地化的网站,有时我需要存储来自不同源的值。有些是公制,有些不是。一种解决方案是将所有值存储为公制,然后进行转换以显示它们,这很容易,但大多数不同测量系统之间的转换并不精确,因此转换后的数字看起来相当难看。该项目允许您存储带有单位信息的值,并在不同测量系统之间进行转换。它还提供了一个层次结构,可以轻松创建新的转换器。
使用的所有转换常数都不保证是最好的,所以在查看结果时请小心。我将展示如何使用此库以及您自己的常数。
测量单位
我既需要转换值,又需要知道值代表哪个单位。然后我需要轻松地将这个值存储在数据库中,而无需使用序列化或 SQL 用户定义类型,这些类型需要程序集注册,而某些 Web 托管不支持。我能想到的最简单的解决方案是:使用 32 位整数表示单位描述,使用 32 位浮点数表示值,这对于所有现有的测量单位来说已经足够了,而且我认为对于大多数实际值来说也足够了。值将以 64 位整数的形式存储在数据库中。
单位由其名称、符号和复数形式表示。此外,它们在表中有一个唯一的标识符,稍后将进行描述。由于它们打算以整数形式存储,因此它们有一个属性可以将它们转换为 Int64
,并从 Int64
转换回来。单位与表绑定,这样它们只存储有关其代码和值的信息,没有字符串,没有转换,其他所有信息都来自它们的表。
单位表
它是一个包含一组测量单位及其转换的表。当前的实现包括 XML 初始化和资源本地化。


可以直接使用表实例来获取单位名称、符号、复数形式或转换器,转换器稍后将进行解释。
Hello World
假设我们有一个包含单位的表:米、厘米、英尺和英寸,以及它们的所有转换:米到厘米、米到英尺、英尺到英寸,依此类推。
UnitTable table = UnitTable.LengthTable;
Unit meters = new Unit(MetersCode, 100, table);
Unit inches = new Unit(InchesCode, 120, table);
Console.WriteLine(meters.Convert(CentimetersCode));
Console.WriteLine(meters.Convert(FeetCode));
Console.WriteLine(meters.Convert(InchesCode));
Console.WriteLine(inches.Convert(MetersCode));
Console.WriteLine(inches.Convert(CentimetersCode));
Console.WriteLine(inches.Convert(FeetCode));
在此代码片段中,我们使用一个名为 LengthTable
的预定义表,其中包含一些长度测量单位以及所有可能的交叉转换。然后我们创建 2 个单位,第一个表示 100 米 (m),第二个表示 120 英寸 (in)。然后我们打印 100 米转换为厘米、英尺和英寸,然后是 120 英寸转换为米、厘米和英尺。您实际上需要记住在首次创建表时分配给单位的代码,您可以轻松地使用 const
或 enum
来完成此任务。这取决于您。
运行上述样本的输出是
10000cm
328.0839ft
3937in
3.048006m
304.8006cm
10ft
XmlTables
这是定义单位和转换的简洁明了的方式。交叉转换可能是一场噩梦,我正在开发一个 Windows 应用程序来填充这些文件,但由于我在这类应用程序的构建方面真的很糟糕,所以您应该学会自己编写。
<UnitTable>
<Units>
<Unit code="1" name="Meter" symbol="m" plural="Meters"/>
<Unit code="2" name="Kilometer" symbol="km" plural="Kilometers"/>
<Unit code="3" name="Centimeter" symbol="cm" plural="Centimeters"/>
...
<Unit code="7" name="Foot" symbol="ft" plural="Feet"/>
<Unit code="8" name="Inch" symbol="in" plural="Inches"/>
</Units>
<Conversions>
<Linear srcCode="1" destCode="2" factor="1e-3"/>
<Linear srcCode="1" destCode="3" factor="1e2"/>
...
<Linear srcCode="1" destCode="7" factor="3.280839"/>
<Linear srcCode="1" destCode="8" factor="39.37"/>
...
<Linear srcCode="7" destCode="8" factor="12" />
</Conversions>
</UnitTable>
看起来很简单?确实如此。无论如何,让我来解释一下。Unit 元素定义单位:代码、名称、国际符号和复数形式。Linear 元素定义一个简单的 y = ax + b
转换,x
将是 srcCode
属性指定的源单位,y
将是 destCode
属性指定的目标单位,“a
” 将是“factor
”,“deltha
” 在这些转换中均未使用,将是“b
”。参数“deltha
” 不太常见,因为大多数测量单位都有一个非常明确的零,有些则没有,例如温度。如果需要的转换不是线性的,您可以使用 Custom 元素(稍后描述)代替。 “factor
” 和 “deltha
” 都是 double
,因此您可以使用非常精确的常数。
现在我们有了一个表文件,让我们使用它
UnitTable table = new XmlUnitTable("LengthUnits.xml");
Unit meters = new Unit(MetersCode, 100, table);
Unit inches = new Unit(InchesCode, 120, table);
如果我们使用此代码片段而不是之前的代码片段,我们将获得相同的结果。XmlUnitTable
构造函数重载还支持 XmlDocument
和 Stream
。
还实现了一个分贝转换器,但由于我不确定它能用于什么,所以我不敢冒险自嘲,也不会使用任何示例。想法是 y = 10 log (X / Reference)
,Inverse
已实现但内部化,因此您只能使用 Logarithmic 实现。
XML 表文件中的分贝使用
...
<Conversions>
<Decibel srcCode="1" destCode="2" reference="0.1"/>
...
本地化表
对我来说,最主要的任务是,如果网站无法本地化,那将毫无用处。这是一个 XmlUnitTable
的子类,它还接受一个 ResourceManager
,并使用名称和复数形式作为键从管理器中获取实际值。
public class LocalizedXmlUnitTable : XmlUnitTable
{
...
public override string GetUnitName(int unitCode)
{
return man_Resources.GetString(base.GetUnitName(unitCode));
}
}
现在让我们采用一个名为 Units
的资源文件,其中应该为表文件中的每个名称和复数形式都有一个行,并创建一个本地化表。
LocalizedXmlUnitTable table = new LocalizedXmlUnitTable("LengthUnits.xml",
Resources.Units.ResourceManager);
现在单位名称和复数形式是与文化相关的,我们可以用英语获取“meters”,用西班牙语获取“metros”,依此类推,我们只需要为每种语言编写一个资源文件。单位符号是不可本地化的,因为它们应该是国际化的。我不是完全确定,但我很肯定他们在中国、俄罗斯、以色列、希腊或任何不使用拉丁字母的国家,他们都用“m”来表示米。库中附带了一个默认资源管理器,以防您不想自己编写。
Converters
这些是这个库中真正辛苦工作的家伙。转换器是将一个单位转换为另一个单位的公式。您应该向表索取正确的公式;一旦您有了它,您就可以转换任意数量的值。您也可以让 Unit 自己转换,它会为您处理表查询。一些转换器可能是可逆的,也许全部都是,但当我编写这个程序时,我没有时间进行完整的数学论证来证明是否存在不可逆的公式来转换单位,因此转换器有一个属性来询问它们是否可以被逆转,以及一个属性来获取逆转。这样,如果您定义了一个从英里到米的转换器,您就不需要定义一个从米到英里的转换器。表将使用第一个的逆来完成工作。无论如何,您可以定义两者,显式转换器优先于隐式转换器。
当前的实现包括一个线性转换器,如果因子不为零,则可以对其进行逆转,尽管这没有意义,因为它代表一个恒定转换。这意味着因子等于零是被禁止的,不用担心,异常会确保您不会犯错误。
实现自定义转换器
这些类可以在 XmlTable
及其子类中使用。它们应该有一个 public
无参构造函数,并且能够从 XmlElement
初始化自身。
<UnitTable>
...
<Convertions>
<Custom srcCode="3" destCode="1" typeName="HypotheticalCustomConverter"
p1="v1" p2="v2" ... pn="vn"/>
...
</Convertions>
</UnitTable>
此表定义了一个类型为 HypotheticalCustomConverter
的自定义转换器。您应该在 XML 属性中指定其名称以及您喜欢的任意数量的信息。使用 XmlInitialize
方法,转换器应该能够从属性获取信息并自行初始化。

在下面的示例中,我们创建了一个自定义转换器,我使用了假设的情况,因为我现在想不出任何实际的自定义转换器。
public class HypotheticalCustomConverter : CustomConverter
{
object f1, f2, f3;
public override void XmlInitialize(System.Xml.XmlElement element)
{
f1 = element.Attributes["p1"].Value;
f2 = element.Attributes["p2"].Value;
f3 = element.Attributes["p3"].Value;
}
public override bool AllowInverse { get { return true; } }
public override IConversion Inverse
{
get { return new HypotheticalCustomConverter(); }
}
public override float Convert(float source)
{
return source * source;
}
}
此转换器可能没有意义,仅用于演示如何实现非线性转换器。
例如,我们考虑以下华氏度到摄氏度的转换器的实现,它不需要自定义初始化,而且是线性公式,但无论如何。
public class FahrenheitToCelsius : CustomConverter
{
...
public override float Convert(float source)
{
return (float)(1.8 * source + 32);
}
}
如果我们构建一个类似的摄氏度到华氏度的转换器,我们可以像这样在 FahrenheitToCelsius
中定义一个 Inverse
属性
public override IConverter Inverse
{
get { return new CelsiusToFahrenheit(); }
}
最后,一个用于使用这两个转换器的表文件如下所示
<UnitTable>
<Units>
<Unit code="1" name="Celsius" symbol="ºC" plural="PCelsius"/>
<Unit code="3" name="Fahrenheit" symbol="ºF" plural="PFahrenheit"/>
</Units>
<Conversions>
<Custom srcCode="3" destCode="1" typeName="Tests.FahrenheitToCelsius"/>
<Custom srcCode="1" destCode="3" typeName="Tests.CelsiusToFahrenheit"/>
</Conversions>
</UnitTable>
我使用了 Celsius
和 PCelsius
而不是 Degree Celsius
和 Degrees Fahrenheit
,以便能够将它们用作资源键,因为资源键不能包含空格,这意味着此文件 intended to be used by LocalizedXmlUnitTable
实例。
货币转换
根据定义,它们是线性转换,只是它们总是在变化。我构建了一些类,可以从互联网或本地 XML 文件下载汇率。您始终可以创建一个程序,使用线性转换创建更新的表文件。这只是另一种方式。使用此库处理货币存在一个问题。值存储为 float
,这会在舍入时丢失数字,因此,任何严肃的金融应用程序都不应依赖我的货币转换。货币转换应使用 decimals
完成。也许在未来的版本中,我会考虑一种方法来同时支持 float
和 decimal
,目前只有 float
。
CurrencyExchangeTable currTable = new CurrencyExchangeTable(
new WebExchangeRatesProvider());
Unit eur = currTable.CreateUnit(200, "EUR");
Unit usd = currTable.CreateUnit(150, "USD");
Unit yen = currTable.CreateUnit(1000, "JPY");
Console.WriteLine(yen.Convert(currTable.CurrencyCode("USD")));
Console.WriteLine(yen.Convert(currTable.CurrencyCode("CAD")));
Console.WriteLine(yen.Convert(currTable.CurrencyCode("EUR")));
Console.WriteLine(yen.Convert(currTable.CurrencyCode("GBP")));
对于货币,使用字符串常量更容易,并且由于代码是自动分配的,因此没有其他选择。货币表应支持创建单位并从字符串常量获取单位代码。此表是汇率提供程序的代理,该提供程序实际从 Web 或某些文件或其他源获取汇率。

汇率提供程序
基于接口 ICurrencyExchangeRatesProvider
提供了一种从任何地方获取汇率的方法。此库包含一个缓存版本和一个在线版本。

Web 汇率提供程序
我为在线汇率提供程序提供的默认实现。它连接到一个网站,并将 XSLT 转换应用于某些 XML 源数据,将其转换为 DataSet
兼容的 XML,然后读取 DataSet
。提供了适用于默认网站的 XSLT。
DataSet
的布局必须是
<Rates>
<Rate>
<Currency>United States Dollar</Currency>
<Exchange>1.4482</Exchange>
<Symbol>USD</Symbol>
</Rate>
<Rate>
<Currency>Euro</Currency>
<Exchange>1</Exchange>
<Symbol>EUR</Symbol>
</Rate>
</Rates>
要在默认提供程序以外的其他提供程序上使用此类,您应该创建一个 XSLT 文件并使用以下代码
CurrencyExchangeTable currTable = new CurrencyExchangeTable(
new WebExchangeRatesProvider("http://somesite.com/rate.xml", XSLStream));
您也可以通过实现接口 ICurrencyExchangeRatesProvider
来创建自己的提供程序。
待办事项
我计划收集尽可能多的测量转换,并将它们全部包含为默认表,获取一些其他货币汇率提供程序,并为所有这些提供 XSLT。
我需要一种方法来在数据库中存储 decimal 值和单位信息,而无需使用序列化或用户定义类型。一旦完成,就可以从 float
更改为 double
。