NMoneys,一个 .NET 中的货币值对象实现
NMoneys 是 Money Value Object 的一个实现,用于支持 .NET 平台中以 ISO 4217 标准定义的货币表示金额。
背景
尽管 .NET Framework 基类库 (BCL) 提供了大量的构件,但它没有提供一种很好的方式来表示不同货币的货币数量。相反,它提供了数字类型和根据不同格式格式化数字值的方法。问题在于这些格式化规则与文化、语言、日期、日历等概念混杂在一起,以至于变得难以,甚至在某些情况下不可能表示一个简单的概念,例如“一加拿大元”或“十点五赞比亚克瓦查”。
除了概念混淆不清之外,有时区域或格式信息是错误的:给定国家/地区的货币不正确、信息过时、缺少国家/地区等等。.NET Framework 作为一个庞大且广泛使用的框架,其发布修复的速度并不快(咳咳),如果一个应用程序依赖于 Framework 信息来显示缺失或不正确货币的货币数量,那么就相当不幸了。
此外,不仅实现不遵循 ISO 货币标准,而且格式信息可以由用户更改,使得跨版本、机器和更新的一致性成为一项非常具有挑战性的任务。
可能存在其他一些库,人们可能会使用手写的类,但在某些情况下,API 不像预期的那样好用,或者它们根本没有得到维护。
由于缺乏支持和一致性,加上有需求,以及一双能打字的手,我创建了 NMoneys 库来解决我的问题和一些 .NET 开发人员的问题。它是开源的,因为它既不是高深的技术,也不是公司工具箱中最有价值的资产。此外,令人惊讶的是,我并不了解货币和货币数量的一切。但通过社区的支持,这个问题可以解决,前提是对项目做出贡献。
在我、我的几位同事以及(希望)一些我还不认识的人使用该库之后,本文才发布,这意味着它至少对解决某些人的问题是有用的。
使用代码
我坚信富有表现力的单元测试是现存最好的文档形式。我也认为它们与其他任何展示 NMoneys 能力的方式一样好。这些单元测试是 NUnit 测试,可以通过您最喜欢的测试运行器执行。
所有代码片段(以及为了简洁起见未发布的更多内容)都将包含在演示项目(Visual Studio 2010,.NET 4.0)中。
演示项目的代码也可以从 项目网站 的原始来源中浏览。同样,NMoneys 库的最新代码版本可以轻松地从 这里 获取。
ISO 货币代码
来自 国际标准化组织 (ISO):“本国际标准规定了用于表示货币和基金的三字母字母代码和等效三位数字代码的结构”。该标准的维护由 ISO 4217 维护机构 负责。
所有这些官僚言辞意味着存在一个国际组织,它对货币及其字母和数字代码进行标准化。也就是说,它们反映了许多“实体”(他们这样称呼)或国家/机构(以便每个人都能理解)使用的法定货币的变化。
NMoneys 完全不关注实体(国家/地区)列表,而是关注当前时间点的有效货币列表。此列表确定了哪些货币有效,哪些不被承认。是的,我知道,这是一个标准,涉及很多政治和繁文缛节,但它是目前最可靠的货币列表。
归根结底:货币种类繁多,货币是动态的(新货币被使用,旧货币被弃用),而且 .NET Framework 不支持所有货币,或者在某些情况下干脆是错误的。
NMoneys 中的货币代码
在 NMoneys 中,给定的货币代码表示为枚举 CurrencyIsoCode
的值。
[Test]
public void currency_codes_are_modeled_as_enums_named_after_its_ISO_alphabetic_code()
{
CurrencyIsoCode usDollars = CurrencyIsoCode.USD;
CurrencyIsoCode euro = CurrencyIsoCode.EUR;
CurrencyIsoCode danishKrona = CurrencyIsoCode.DKK;
CurrencyIsoCode noCurrency = CurrencyIsoCode.XXX;
}
[Test]
public void currency_codes_have_their_ISO_numeric_value()
{
Assert.That((short)CurrencyIsoCode.USD, Is.EqualTo(840));
Assert.That((short)CurrencyIsoCode.EUR, Is.EqualTo(978));
Assert.That((short)CurrencyIsoCode.DKK, Is.EqualTo(208));
Assert.That((short)CurrencyIsoCode.XXX, Is.EqualTo(999));
}
[Test]
public void less_common_currencies_are_also_modeled_as_long_as_they_are_approved_by_ISO()
{
CurrencyIsoCode platinum = CurrencyIsoCode.XPT;
CurrencyIsoCode yemeniRial = CurrencyIsoCode.YER;
}
[Test]
public void recently_deprecated_currencies_are_also_present()
{
var estonianKroon = CurrencyIsoCode.EEK;
Assert.That(estonianKroon.AsAttributeProvider(), Has.Attribute<ObsoleteAttribute>());
}
从代码片段中可以看出,完整的 ISO 标准已经实现,包括字母和数字代码,包括不常见的货币和最近弃用的货币,但这些货币都用 ObsoleteAttribute
标记,以明确突出它们已被弃用。
货币
CurrencyIsoCode
是表示 ISO 标准中给定货币代码的方式。但是,枚举值可以提供的有趣行为数量非常有限。显然,我们需要另一个类型来为我们完成这项工作。接下来介绍 Currency
类型。
获取实例
只能创建有限数量的货币。由于代码是 CurrencyIsoCode
枚举,所以框架本身就限制了“实例”的数量,但对于 Currency
,则提供了类似享元模式的接口。
“流行”货币可以直接通过静态方式访问。
[Test]
public void popular_currency_instances_can_be_obtained_from_static_accessors()
{
Assert.That(Currency.Usd, Is.Not.Null.And.InstanceOf<Currency>());
Assert.That(Currency.Eur, Is.Not.Null.And.InstanceOf<Currency>());
Assert.That(Currency.Dkk, Is.Not.Null.And.InstanceOf<Currency>());
Assert.That(Currency.Xxx, Is.Not.Null.And.InstanceOf<Currency>());
}
并非所有货币都有静态访问方式。相反,工厂方法 Currency.Get()
接受各种输入以获取货币实例。
[Test]
public void currency_instances_can_be_obtained_from_its_code_enum()
{
Assert.That(Currency.Get(CurrencyIsoCode.ZAR), Is.InstanceOf<Currency>());
}
[Test]
public void currency_instances_can_be_obtained_from_its_code_string()
{
Assert.That(Currency.Get("eur"), Is.Not.Null);
Assert.That(Currency.Get("EUR"), Is.Not.Null);
}
[Test]
public void currency_instances_can_be_obtained_from_a_CultureInfo_instance()
{
CultureInfo swedish = CultureInfo.GetCultureInfo("sv-SE");
Assert.That(Currency.Get(swedish), Is.EqualTo(Currency.Sek));
}
[Test]
public void currency_instances_can_be_obtained_from_a_RegionInfo_instance()
{
RegionInfo spain = new RegionInfo("es");
Assert.That(Currency.Get(spain), Is.EqualTo(Currency.Eur));
}
如果输入以某种方式不安全,可以使用遵循众所周知的 *TryParse* 模式的另一个工厂方法 Currency.TryGet()
来避免异常。
[Test]
public void currency_instances_can_be_obtained_with_a_try_do_pattern()
{
Currency currency;
Assert.That(Currency.TryGet(CurrencyIsoCode.ZAR, out currency), Is.True);
Assert.That(currency, Is.Not.Null.And.InstanceOf<Currency>());
Assert.That(Currency.TryGet("zar", out currency), Is.True);
Assert.That(currency, Is.Not.Null.And.InstanceOf<Currency>());
Assert.That(Currency.TryGet(CultureInfo.GetCultureInfo("en-ZA"), out currency), Is.True);
Assert.That(currency, Is.Not.Null.And.InstanceOf<Currency>());
Assert.That(Currency.TryGet(new RegionInfo("ZA"), out currency), Is.True);
Assert.That(currency, Is.Not.Null.And.InstanceOf<Currency>());
}
[Test]
public void TryGet_does_not_throw_if_currency_cannot_be_found()
{
Currency currency;
CurrencyIsoCode notDefined = (CurrencyIsoCode)0;
Assert.That(Currency.TryGet(notDefined, out currency), Is.False);
Assert.That(currency, Is.Null);
Assert.That(Currency.TryGet("notAnIsoCode", out currency), Is.False);
Assert.That(currency, Is.Null);
CultureInfo neutralCulture = CultureInfo.GetCultureInfo("da");
Assert.That(Currency.TryGet(neutralCulture, out currency), Is.False);
Assert.That(currency, Is.Null);
}
弃用货币的处理
货币来了又去。新货币成为主流,一些货币被弃用。但这并不意味着我们不能再使用它们了。那些货币未被弃用时的数据可能存在,这种常见情况需要得到支持。
[Test]
public void instances_of_deprecated_currencies_can_still_be_obtained()
{
Currency deprecated = Currency.Get("EEK");
Assert.That(deprecated, Is.Not.Null.And.InstanceOf<Currency>());
Assert.That(deprecated.IsObsolete, Is.True);
}
然而,在某些问题领域,弃用货币的使用是不可接受的。对于这些场景,提供了以静态事件形式存在的通知系统。
[Test]
public void whenever_a_deprecated_currency_is_obtained_an_event_is_raised()
{
bool called = false;
CurrencyIsoCode obsolete = CurrencyIsoCode.XXX;
EventHandler<ObsoleteCurrencyEventArgs> callback = (sender, e) =>
{
called = true;
obsolete = e.Code;
};
try
{
Currency.ObsoleteCurrency += callback;
Currency.Get("EEK");
Assert.That(called, Is.True);
Assert.That(obsolete.ToString(), Is.EqualTo("EEK"));
Assert.That(obsolete.AsAttributeProvider(), Has.Attribute<ObsoleteAttribute>());
}
// DO unsubscribe from global events whenever listening isnot needed anymore
finally
{
Currency.ObsoleteCurrency -= callback;
}
}
请注意,代码片段对该问题非常清楚。**请注意**,静态事件处理程序是**内存泄漏**最常见的来源之一。当不再需要通知时,请务必取消订阅。您已被警告。
关于 LINQ 的诸多讨论
我们当然热爱 LINQ。它让我们的生活变得如此……更快乐?嗯,它对语言来说是一个伟大的补充。我们对这项事业的贡献在于提供一种以可枚举集合形式检索所有货币的方式。尽情地对世界上所有的货币进行归约、映射和聚合吧……
[Test]
public void all_currencies_can_be_obtained_and_linq_operators_applied()
{
Assert.That(Currency.FindAll(), Is.Not.Null.And.All.InstanceOf<Currency>());
var allCurrenciesWithoutMinorUnits =
Currency.FindAll().Where(c => c.SignificantDecimalDigits == 0);
Assert.That(allCurrenciesWithoutMinorUnits, Is.Not.Empty.And.Contains(Currency.Jpy));
}
货币中有什么
我们已经了解了如何获取 Currency
实例。但是,它里面有什么吸引人的地方呢?
[Test]
public void whats_in_a_currency_anyway()
{
Currency euro = Currency.Eur;
Assert.That(euro.IsObsolete, Is.False);
Assert.That(euro.IsoCode, Is.EqualTo(CurrencyIsoCode.EUR));
Assert.That(euro.IsoSymbol, Is.EqualTo("EUR"));
Assert.That(euro.NativeName, Is.EqualTo("Euro"),
"capitalized in the default instance");
Assert.That(euro.NumericCode, Is.EqualTo(978));
Assert.That(euro.PaddedNumericCode, Is.EqualTo("978"),
"a string of 3 characters containing the numeric code and zeros if needed");
Assert.That(euro.Symbol, Is.EqualTo("€"));
}
在一个各种“尖括号”语言都扮演着重要角色的世界里,有些特定的货币拥有特权,能够以一种特殊的方式在这些语言中表示。这种特殊的显示方式由 CharacterReference
属性提供。
[Test]
public void some_currencies_have_an_character_reference_for_angly_bracket_languages()
{
Currency qatariRial = Currency.Get(CurrencyIsoCode.QAR);
CharacterReference reference = qatariRial.Entity;
Assert.That(reference, Is.Not.Null.And.Property("IsEmpty").True,
"the Rial does not have an reference, but a 'null' object");
Currency euro = Currency.Euro;
reference = euro.Entity;
Assert.That(reference, Is.Not.Null.And.Property("IsEmpty").False,
"the euro, does");
Assert.That(reference.Character, Is.EqualTo("€"));
Assert.That(reference.CodePoint, Is.EqualTo(8364));
Assert.That(reference.EntityName, Is.EqualTo("€"));
Assert.That(reference.EntityNumber, Is.EqualTo("€"));
Assert.That(reference.SimpleName, Is.EqualTo("euro"));
}
如何使用货币?
到目前为止,Currency
最有趣和关键的行为是格式化数字数量。但是,通过展示它们的特殊情况:货币数量,来展示如何格式化数字数量,不是更有意义吗?
金钱
Currency
实现了给定货币的行为,主要是格式化。但是,在给定货币中唯一表示货币数量,并将其与另一种货币中的另一种数量区分开来的方式,尚未出现。这种表示就是 Money
结构。
获取实例
与 Currency
仅提供有限数量的实例创建不同,Money
可以有许多实例。它遵循数字类型的语义,因此已将其实现为结构而非引用类型。获取实例的最常见方式是使用其多个构造函数之一。
[Test]
public void a_Money_represents_a_monetary_quantity()
{
new Money(10m, Currency.Dollar); // Money --> tenDollars
new Money(2.5m, CurrencyIsoCode.EUR); // Money --> twoFiftyEuros
new Money(10m, "JPY"); // Money --> tenYen
new Money(); // Money --> zeroWithNoCurrency
}
与环境(例如当前文化)的交互是明确的。在下面的代码片段中,NUnit 属性 SetCultureAttribute
用于控制测试运行的文化(在本例中为丹麦语)。
[Test, SetCulture("da-DK")]
public void environment_dependencies_are_explicit()
{
Money fiveKrona = Money.ForCurrentCulture(5m);
Assert.That(fiveKrona.CurrencyCode, Is.EqualTo(CurrencyIsoCode.DKK));
Money currencyLessMoney = new Money(1);
Assert.That(currencyLessMoney.CurrencyCode, Is.EqualTo(CurrencyIsoCode.XXX));
Money zeroEuros = Money.ForCulture(0m, CultureInfo.GetCultureInfo("es-ES"));
Assert.That(zeroEuros.CurrencyCode, Is.EqualTo(CurrencyIsoCode.EUR));
}
还有另一种创建货币实例的方法:为了简洁起见,使用一组扩展方法。这种创建实例的主要场景是单元测试。在测试时,一种简短而富有表现力的方式来为您的场景创建对象非常有帮助。
[Test]
public void moneys_can_be_quickly_created_for_testing_scenarios_with_extension_methods()
{
// Money --> threeNoCurrencies
3m.Xxx();
3m.ToMoney();
// Money --> threeAndAHalfAustralianDollars
3.5m.Aud();
3.5m.ToMoney(Currency.Aud);
3.5m.ToMoney(CurrencyIsoCode.AUD);
CurrencyIsoCode.AUD.ToMoney(3.5m);
}
解析
创建 Money
实例的另一种方法是解析给定的字符串。此功能用处有限,因为输入所需的货币需要事先传递,因为许多货币使用相同的显示符号。
[Test]
public void moneys_can_be_parsed_to_a_known_currency()
{
Assert.That(Money.Parse("$1.5", Currency.Dollar),
isMoneyWith(1.5m, CurrencyIsoCode.USD), "one-and-the-half dollars");
Assert.That(Money.Parse("10 €", Currency.Euro),
isMoneyWith(10m, CurrencyIsoCode.EUR), "ten euros");
Assert.That(Money.Parse("kr -100", Currency.Dkk),
isMoneyWith(-100m, CurrencyIsoCode.DKK), "owe hundrede kroner");
Assert.That(Money.Parse("(¤1.2)", Currency.None),
isMoneyWith(-1.2m, CurrencyIsoCode.XXX), "owe one point two, no currency");
}
主要单位和次要单位
在货币中,有一个主要单位和一个次要单位是非常常见的。一个明显的例子是英镑。对于这种货币,主要单位是英镑本身,而次要单位是便士。
对于这种常见的场景,有一种方法可以根据主要单位的数量创建货币实例。
[Test]
public void what_is_with_this_Major_thing()
{
Assert.That(Money.ForMajor(234, Currency.Gbp), isMoneyWith(234, CurrencyIsoCode.GBP),
"instance created from the major units, in this case the Pound");
Assert.That(3m.Pounds().MajorAmount, Is.EqualTo(3m),
"for whole amounts is the quantity");
Assert.That(3.7m.Pounds().MajorAmount, Is.EqualTo(3m),
"for fractional amounts is the number of pounds");
Assert.That(0.7m.Pounds().MajorAmount, Is.EqualTo(0m),
"for fractional amounts is the number of pounds");
Assert.That(3m.Pounds().MajorIntegralAmount, Is.EqualTo(3L),
"for whole amounts is the non-fractional quantity");
Assert.That(3.7m.Pounds().MajorIntegralAmount, Is.EqualTo(3L),
"for fractional amounts is the number of pounds");
Assert.That(0.7m.Pounds().MajorIntegralAmount, Is.EqualTo(0L),
"for fractional amounts is the number of pounds");
}
并提供了一种根据次要单位数量创建实例的方法。
[Test]
public void what_is_with_this_Minor_thing()
{
Assert.That(Currency.Pound.SignificantDecimalDigits, Is.EqualTo(2),
"pounds have pence, which is a hundreth of the major unit");
Assert.That(Money.ForMinor(234, Currency.Gbp), isMoneyWith(2.34m, CurrencyIsoCode.GBP),
"234 pence is 2.34 pounds");
Assert.That(Money.ForMinor(50, Currency.Gbp), isMoneyWith(0.5m, CurrencyIsoCode.GBP),
"fifty pence is half a pound");
Assert.That(Money.ForMinor(-5, Currency.Gbp), isMoneyWith(-0.05m, CurrencyIsoCode.GBP),
"you owe me five pence, but keep them");
Assert.That(3m.Pounds().MinorAmount, Is.EqualTo(300m), "three pounds is 300 pence");
Assert.That(.07m.Pounds().MinorAmount, Is.EqualTo(7m),
"for fractional amounts, the minor unit prevails");
Assert.That(0.072m.Pounds().MinorAmount, Is.EqualTo(7m),
"tenths of pence are discarded");
Assert.That(3m.Pounds().MinorIntegralAmount, Is.EqualTo(300L),
"three pounds is 300 pence");
Assert.That(.07m.Pounds().MinorIntegralAmount, Is.EqualTo(7L),
"for fractional amounts, the minor unit prevails");
Assert.That(0.072m.Pounds().MinorIntegralAmount, Is.EqualTo(7L),
"tenths of pence are discarded");
}
有些货币采用次要单位是主要单位百分之一的模式。在其他货币中,次要单位是主要单位的千分之一。而有些货币根本没有次要单位。当然,所有这些情况都得到了支持。
金钱中有什么
我们已经看到了如何创建 Money
实例。为什么会有人实例化它呢?是为了它提供的数据吗?
[Test]
public void what_is_in_a_money()
{
Money threeCads = new Money(3m, "CAD");
Assert.That(threeCads.Amount, Is.EqualTo(3m));
Assert.That(threeCads.CurrencyCode, Is.EqualTo(CurrencyIsoCode.CAD));
Assert.That(threeCads.HasDecimals, Is.False);
Assert.That(threeCads.IsNegative(), Is.False);
Assert.That(threeCads.IsNegativeOrZero(), Is.False);
Assert.That(threeCads.IsPositive(), Is.True);
Assert.That(threeCads.IsPositiveOrZero(), Is.True);
Assert.That(threeCads.IsZero(), Is.False);
}
这些,以及关于主要和次要金额的信息,都很有趣。但是……我相信人们想用他们的钱做点什么。
如何使用金钱?
这是一个有趣的问题。但就本文而言,答案将侧重于 Money
对象的行为。
首先,作为某种数量,货币可以进行比较是很有意义的。否则,吹嘘我们的银行账户有多红将是不可能的……
[Test]
public void moneys_can_be_compared()
{
Assert.That(3m.Usd().Equals(CurrencyIsoCode.USD.ToMoney(3m)), Is.True);
Assert.That(3m.Usd() != CurrencyIsoCode.USD.ToMoney(3m), Is.False);
Assert.That(3m.Usd().CompareTo(CurrencyIsoCode.USD.ToMoney(5m)), Is.LessThan(0));
Assert.That(3m.Usd() < CurrencyIsoCode.USD.ToMoney(5m), Is.True);
}
不同货币的实例不能盲目地相互比较,就像“飞机”不能与“苹果”进行比较一样(至少对于大多数有用的目的而言)。由于 NMoneys 的目标中不包括提供汇率服务,因此具有不同货币的金额根本无法进行比较。
[Test]
public void comparisons_only_possible_if_they_have_the_same_currency()
{
Assert.That(3m.Usd().Equals(3m.Gbp()), Is.False);
Assert.That(3m.Usd() != CurrencyIsoCode.GBP.ToMoney(3m), Is.True);
Assert.That(() => 3m.Usd().CompareTo(CurrencyIsoCode.GBP.ToMoney(5m)),
Throws.InstanceOf<DifferentCurrencyException>());
Assert.That(() => 3m.Usd() < CurrencyIsoCode.GBP.ToMoney(5m),
Throws.InstanceOf<DifferentCurrencyException>());
}
货币数量可以多种方式格式化以供显示。
[Test]
public void moneys_are_to_be_displayed()
{
Assert.That(10.536m.Eur().ToString(), Is.EqualTo("10,54 €"),
"default currency formatting according to instance's currency");
Assert.That(3.2m.Usd().ToString("N"), Is.EqualTo("3.20"),
"alternative formatting according to instance's currency");
}
一个常见的场景是,同一种货币在不同的国家/地区使用,而这些国家/地区有不同的方式来表示货币数量。这种常见场景通过使用与实例货币提供的格式提供程序不同的格式提供程序来解决。
[Test]
public void using_different_styles_for_currencies_taht_span_multiple_countries()
{
Assert.That(3000.5m.Eur().ToString(), Is.EqualTo("3.000,50 €"), "default euro formatting");
// in French the group separator is neither the dot or the space
CultureInfo french = CultureInfo.GetCultureInfo("fr-FR");
string threeThousandAndTheHaldInFrench = string.Format("3{0}000,50 €",
french.NumberFormat.CurrencyGroupSeparator);
Assert.That(3000.5m.Eur().ToString(french),
Is.EqualTo(threeThousandAndTheHaldInFrench));
}
更丰富的格式化功能也可能实现。
[Test]
public void more_complex_formatting()
{
Assert.That(3m.Usd().Format("{0:00.00} {2}"), Is.EqualTo("03.00 USD"),
"formatting placeholders for code and amount");
Assert.That(2500m.Eur().Format("> {1} {0:#,#.00}"), Is.EqualTo("> € 2.500,00"),
"rich amount formatting");
}
显示货币很重要。但另一件非常重要的事情是,需要对货币数量进行一些算术运算。
[Test]
public void moneys_are_to_be_operated_with_arithmetic_operators()
{
Money fivePounds = 2m.Pounds().Plus(3m.Pounds());
Assert.That(fivePounds, isMoneyWith(5m, CurrencyIsoCode.GBP));
Money fiftyPence = 3m.Pounds() - 2.5m.Pounds();
Assert.That(fiftyPence, isMoneyWith(.5m, CurrencyIsoCode.GBP));
Money youOweMeThreeEuros = -3m.Eur();
Assert.That(youOweMeThreeEuros, isMoneyWith(-3m, CurrencyIsoCode.EUR));
Money nowIHaveThoseThreeEuros = youOweMeThreeEuros.Negate();
Assert.That(nowIHaveThoseThreeEuros, isMoneyWith(3m, CurrencyIsoCode.EUR));
Money youOweMeThreeEurosAgain = -nowIHaveThoseThreeEuros;
Assert.That(youOweMeThreeEurosAgain, isMoneyWith(-3m, CurrencyIsoCode.EUR));
}
由于难以预见对货币执行的完整操作集,因此有一些简单的扩展点。
[Test]
public void basic_arithmetic_operations_can_be_extended()
{
Money halfMyDebt = -60m.Eur().Perform(amt => amt / 2);
Assert.That(halfMyDebt, isMoneyWith(-30m, CurrencyIsoCode.EUR));
Money convolutedWayToCancelDebt = (-50m).Eur().Perform(-1m.Eur(),
(amt1, amt2) => decimal.Multiply(amt1, decimal.Negate(amt2)) - amt1);
Assert.That(convolutedWayToCancelDebt,
isMoneyWith(decimal.Zero, CurrencyIsoCode.EUR));
}
与比较一样,对相同货币的金额执行二元操作通常更有意义。
[Test]
public void binary_operations_only_possible_if_they_have_the_same_currency()
{
Assert.That(() => 2m.Gbp().Minus(3m.Eur()),
Throws.InstanceOf<DifferentCurrencyException>());
Assert.That(() => 2m.Cad() + 3m.Aud(), Throws.InstanceOf<DifferentCurrencyException>());
Assert.That(() => 3m.Usd().Perform(3m.Aud(), (x, y) => x + y),
Throws.InstanceOf<DifferentCurrencyException>());
}
大多数时候,两个人参与会更有趣,但一个金钱也可以独立完成不少事情。
[Test]
public void several_unary_operations_can_be_performed()
{
Assert.That(3m.Xxx().Negate(), isMoneyWith(-3m), "-1 * amount");
Assert.That((-3m).Xxx().Abs(), isMoneyWith(3m), "|amount|");
Money twoThirds = new Money(2m / 3);
Assert.That(twoThirds.Amount, Is.Not.EqualTo(0.66m),
"not exactly equals as it has more decimals");
Assert.That(twoThirds.TruncateToSignificantDecimalDigits().Amount, Is.EqualTo(0.66m),
"XXX has two significant decimals");
Money fractional = 123.456m.ToMoney();
Assert.That(fractional.Truncate(), isMoneyWith(123m), "whole amount");
Assert.That(.5m.ToMoney().RoundToNearestInt(), isMoneyWith(0m));
Assert.That(.599999m.ToMoney().RoundToNearestInt(), isMoneyWith(1m));
Assert.That(1.5m.ToMoney().RoundToNearestInt(), isMoneyWith(2m));
Assert.That(1.4999999m.ToMoney().RoundToNearestInt(), isMoneyWith(1m));
Assert.That(.5m.ToMoney().RoundToNearestInt(MidpointRounding.ToEven),
isMoneyWith(0m), "closest even number is 0");
Assert.That(.5m.ToMoney().RoundToNearestInt(MidpointRounding.AwayFromZero),
isMoneyWith(1m), "closest number away from zero is 1");
Assert.That(1.5m.ToMoney().RoundToNearestInt(MidpointRounding.ToEven),
isMoneyWith(2m), "closest even number is 2");
Assert.That(1.5m.ToMoney().RoundToNearestInt(MidpointRounding.AwayFromZero),
isMoneyWith(2m), "closest number away from zero is 2");
Assert.That(2.345m.Usd().Round(), isMoneyWith(2.34m), "round to two decimals");
Assert.That(2.345m.Jpy().Round(), isMoneyWith(2m), "round to no decimals");
Assert.That(2.355m.Usd().Round(), isMoneyWith(2.36m), "round to two decimals");
Assert.That(2.355m.Jpy().Round(), isMoneyWith(2m), "round to no decimals");
Assert.That(2.345m.Usd().Round(MidpointRounding.ToEven), isMoneyWith(2.34m));
Assert.That(2.345m.Usd().Round(MidpointRounding.AwayFromZero), isMoneyWith(2.35m));
Assert.That(2.345m.Jpy().Round(MidpointRounding.ToEven), isMoneyWith(2m));
Assert.That(2.345m.Jpy().Round(MidpointRounding.AwayFromZero), isMoneyWith(2m));
Assert.That(123.456m.ToMoney().Floor(), isMoneyWith(123m));
Assert.That((-123.456m).ToMoney().Floor(), isMoneyWith(-124m));
}
总结
我写这篇文章有几个目标。
主要的目的是提高 NMoneys 库的知名度,以便更多人可以受益于它的使用,更重要的是,更多人可以 贡献。
贡献可以有多种形式,但主要有两种方式:为库提供新功能和更准确的货币信息。两者都将非常受欢迎。
另一个目标是展示一些使该库具有吸引力的场景。帮助潜在用户成为真正的用户。
我希望这些目标(或其中任何一个)都能实现。如果没有,我个人也很高兴与你们分享这些。
历史
- 2011年5月8日 - 初始版本。
- 2011年5月10日 - 添加了源代码的外部链接。
- 2011年5月11日 - 在演示项目中包含了库代码。
- 2011年5月20日 - 修正了文章描述中的拼写错误。