From 97ed369861d088286ff9f2f8e27a2571290f76a5 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Tue, 22 Aug 2023 17:49:31 +0200 Subject: [PATCH 1/6] Update to .NET Framework 7.0 * Newest SDK 7.0.400 * add package source mapping * use C# 11 * use new central package managment --- Directory.Build.props | 3 ++- Directory.Packages.props | 22 +++++++++++++++++++ .../Funcky.Money.SourceGenerator.csproj | 2 +- Funcky.Money.Test/Funcky.Money.Test.csproj | 4 ++-- Funcky.Money.sln | 3 ++- Funcky.Money/Funcky.Money.csproj | 4 ++-- Packages.props | 22 ------------------- global.json | 5 +---- nuget.config | 14 ++++++++++++ 9 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 Directory.Packages.props delete mode 100644 Packages.props create mode 100644 nuget.config diff --git a/Directory.Build.props b/Directory.Build.props index c85c822..bc260e1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,11 +1,12 @@ - 10.0 + 11.0 enable true enable false + true Polyadic diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..8e40454 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj b/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj index 3c97126..e046a4d 100644 --- a/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj +++ b/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/Funcky.Money.Test/Funcky.Money.Test.csproj b/Funcky.Money.Test/Funcky.Money.Test.csproj index 1fe1f5d..eb75f69 100644 --- a/Funcky.Money.Test/Funcky.Money.Test.csproj +++ b/Funcky.Money.Test/Funcky.Money.Test.csproj @@ -1,7 +1,7 @@ - + - net6.0; + net7.0; Funcky.Test diff --git a/Funcky.Money.sln b/Funcky.Money.sln index 2dbac46..5883aa0 100644 --- a/Funcky.Money.sln +++ b/Funcky.Money.sln @@ -21,8 +21,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build Config", "Build Confi ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props FrameworkFeatureConstants.props = FrameworkFeatureConstants.props + Directory.Packages.props = Directory.Packages.props global.json = global.json - Packages.props = Packages.props + nuget.config = nuget.config EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Funcky.Money.SourceGenerator", "Funcky.Money.SourceGenerator\Funcky.Money.SourceGenerator.csproj", "{A5B8FAE1-86B6-4868-87DF-F7B20C1C04F0}" diff --git a/Funcky.Money/Funcky.Money.csproj b/Funcky.Money/Funcky.Money.csproj index c71a8a9..6e771d2 100644 --- a/Funcky.Money/Funcky.Money.csproj +++ b/Funcky.Money/Funcky.Money.csproj @@ -1,6 +1,6 @@ - + - netstandard2.0;net6.0 + netstandard2.0;net7.0 Funcky Funcky.Money Funcky.Money is based on Kent Beck's TDD exercise but with more features. diff --git a/Packages.props b/Packages.props deleted file mode 100644 index 37a7e3a..0000000 --- a/Packages.props +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/global.json b/global.json index ebd4051..f4ce9dd 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,6 @@ { "sdk": { - "version": "6.0.400", + "version": "7.0.400", "rollForward": "feature" - }, - "msbuild-sdks": { - "Microsoft.Build.CentralPackageVersions" : "2.1.3" } } diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..3d94608 --- /dev/null +++ b/nuget.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + From d80dd1e63dd7dfda29efb48c0f4e35041ec69372 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 23 Aug 2023 14:44:37 +0200 Subject: [PATCH 2/6] Update packages if possible --- Directory.Packages.props | 38 ++++++++-------- .../Funcky.Money.SourceGenerator.csproj | 1 + .../Iso4217RecordGenerator.cs | 45 +++++++++---------- .../XmlNodeExtensions.cs | 2 - Funcky.Money.Test/Funcky.Money.Test.csproj | 7 ++- Funcky.Money.Test/MoneyTest.cs | 8 ++-- global.json | 2 +- 7 files changed, 52 insertions(+), 51 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8e40454..302f63c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,22 +1,22 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj b/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj index e046a4d..0214978 100644 --- a/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj +++ b/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj @@ -1,6 +1,7 @@ netstandard2.0 + true diff --git a/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs b/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs index d0f0041..1f23777 100644 --- a/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs +++ b/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs @@ -5,7 +5,6 @@ using Funcky.Monads; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; -using static System.Environment; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Funcky.Money.SourceGenerator; @@ -34,18 +33,18 @@ private static void GenerateSource(SourceProductionContext context, ImmutableArr } private static string GenerateCurrencyClass(IReadOnlyList records) - => $"using System.Collections.Generic;{NewLine}" + - $"using System.Collections.Immutable;{NewLine}" + - $"using Funcky.Monads;{NewLine}" + - $"namespace {RootNamespace}{NewLine}" + - $"{{{NewLine}" + - $"{Indent}public partial record Currency{NewLine}" + - $"{Indent}{{{NewLine}" + + => $"using System.Collections.Generic;\n" + + $"using System.Collections.Immutable;\n" + + $"using Funcky.Monads;\n" + + $"namespace {RootNamespace}\n" + + $"{{\n" + + $"{Indent}public partial record Currency\n" + + $"{Indent}{{\n" + $"{GenerateCurrencyProperties(records)}" + $"{GenerateAllCurrenciesProperty(records)}" + $"{GenerateParseMethod(records)}" + - $"{Indent}}}{NewLine}" + - $"}}{NewLine}"; + $"{Indent}}}\n" + + $"}}\n"; private static string GenerateParseMethod(IEnumerable records) { @@ -58,11 +57,11 @@ private static string GenerateParseMethod(IEnumerable records) switchCases.AppendLine($"{Indent}{Indent}{Indent} _ => Option.None,"); - return $"{Indent}{Indent}public static partial Option ParseOrNone(string input){NewLine}" + - $"{Indent}{Indent} => input switch{NewLine}" + - $"{Indent}{Indent} {{{NewLine}" + + return $"{Indent}{Indent}public static partial Option ParseOrNone(string input)\n" + + $"{Indent}{Indent} => input switch\n" + + $"{Indent}{Indent} {{\n" + switchCases + - $"{Indent}{Indent} }};{NewLine}"; + $"{Indent}{Indent} }};\n"; } private static string GenerateCurrencyProperties(IEnumerable records) @@ -101,14 +100,14 @@ private static string GenerateAllCurrenciesProperty(IReadOnlyCollection records) - => $"using Funcky.Monads;{NewLine}" + - $"namespace {RootNamespace}{NewLine}" + - $"{{{NewLine}" + - $"{Indent}public partial record Money{NewLine}" + - $"{Indent}{{{NewLine}" + + => $"using Funcky.Monads;\n" + + $"namespace {RootNamespace}\n" + + $"{{\n" + + $"{Indent}public partial record Money\n" + + $"{Indent}{{\n" + $"{GenerateMoneyFactoryMethods(records)}" + - $"{Indent}}}{NewLine}" + - $"}}{NewLine}"; + $"{Indent}}}\n" + + $"}}\n"; private static string GenerateMoneyFactoryMethods(IEnumerable records) => records.Aggregate(new StringBuilder(), AppendCurrencyFactory).ToString(); @@ -120,8 +119,8 @@ private static string CreateCurrencyFactory(Iso4217Record record) { var identifier = Identifier(record.AlphabeticCurrencyCode); - return $"{Indent}{Indent}/// Creates a new instance using the currency.{NewLine}" + - $"{Indent}{Indent}public static Money {identifier}(decimal amount){NewLine}" + + return $"{Indent}{Indent}/// Creates a new instance using the currency.\n" + + $"{Indent}{Indent}public static Money {identifier}(decimal amount)\n" + $"{Indent}{Indent} => new(amount, MoneyEvaluationContext.Builder.Default.WithTargetCurrency(Currency.{identifier}).Build());"; } diff --git a/Funcky.Money.SourceGenerator/XmlNodeExtensions.cs b/Funcky.Money.SourceGenerator/XmlNodeExtensions.cs index 142b977..abb2a8b 100644 --- a/Funcky.Money.SourceGenerator/XmlNodeExtensions.cs +++ b/Funcky.Money.SourceGenerator/XmlNodeExtensions.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Xml; namespace Funcky.Money.SourceGenerator; diff --git a/Funcky.Money.Test/Funcky.Money.Test.csproj b/Funcky.Money.Test/Funcky.Money.Test.csproj index eb75f69..aab1bac 100644 --- a/Funcky.Money.Test/Funcky.Money.Test.csproj +++ b/Funcky.Money.Test/Funcky.Money.Test.csproj @@ -1,4 +1,4 @@ - + net7.0; @@ -10,7 +10,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Funcky.Money.Test/MoneyTest.cs b/Funcky.Money.Test/MoneyTest.cs index ad17061..68e69a4 100644 --- a/Funcky.Money.Test/MoneyTest.cs +++ b/Funcky.Money.Test/MoneyTest.cs @@ -200,13 +200,13 @@ public void MoneyFormatsCorrectlyAccordingToTheCurrency() [Fact] public void MoneyParsesCorrectlyFromString() { - var r1 = FunctionalAssert.IsSome(Money.ParseOrNone("CHF-1’000.00", Currency.CHF)); + var r1 = FunctionalAssert.Some(Money.ParseOrNone("CHF-1’000.00", Currency.CHF)); Assert.Equal(new Money(-1000, Currency.CHF), r1); - var r2 = FunctionalAssert.IsSome(Money.ParseOrNone("-$1,000.00", Currency.USD)); + var r2 = FunctionalAssert.Some(Money.ParseOrNone("-$1,000.00", Currency.USD)); Assert.Equal(new Money(-1000, Currency.USD), r2); - var r3 = FunctionalAssert.IsSome(Money.ParseOrNone("1000", Currency.CHF)); + var r3 = FunctionalAssert.Some(Money.ParseOrNone("1000", Currency.CHF)); Assert.Equal(new Money(1000, Currency.CHF), r3); } @@ -220,7 +220,7 @@ public void CurrenciesWithoutFormatProviders() var currencyWithoutFormatProvider = Money.XAU(9585); Assert.Equal("9’585 XAU", currencyWithoutFormatProvider.ToString()); - var money = FunctionalAssert.IsSome(Money.ParseOrNone("9’585.00 XAU", Currency.XAU)); + var money = FunctionalAssert.Some(Money.ParseOrNone("9’585.00 XAU", Currency.XAU)); Assert.Equal(new Money(9585, Currency.XAU), money); } diff --git a/global.json b/global.json index f4ce9dd..0c4b7f5 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "version": "7.0.400", - "rollForward": "feature" + "rollForward": "latestFeature" } } From 3731bad8cc9b1f85432e6abdc36e04672c3c9233 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 23 Aug 2023 15:38:24 +0200 Subject: [PATCH 3/6] Generic math is only possible in .NET 7.0 and above --- Funcky.Money.Test/Funcky.Money.Test.csproj | 2 +- Funcky.Money/Funcky.Money.csproj | 4 +-- Funcky.Money/Money.cs | 35 +++++++++++----------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Funcky.Money.Test/Funcky.Money.Test.csproj b/Funcky.Money.Test/Funcky.Money.Test.csproj index aab1bac..d5e76ca 100644 --- a/Funcky.Money.Test/Funcky.Money.Test.csproj +++ b/Funcky.Money.Test/Funcky.Money.Test.csproj @@ -1,7 +1,7 @@  - net7.0; + net7.0 Funcky.Test diff --git a/Funcky.Money/Funcky.Money.csproj b/Funcky.Money/Funcky.Money.csproj index 6e771d2..dd4666b 100644 --- a/Funcky.Money/Funcky.Money.csproj +++ b/Funcky.Money/Funcky.Money.csproj @@ -1,12 +1,12 @@ - netstandard2.0;net7.0 + net7.0 Funcky Funcky.Money Funcky.Money is based on Kent Beck's TDD exercise but with more features. Functional Money true - 1.2.0 + 2.0.0 diff --git a/Funcky.Money/Money.cs b/Funcky.Money/Money.cs index ede72ff..34d65f8 100644 --- a/Funcky.Money/Money.cs +++ b/Funcky.Money/Money.cs @@ -1,14 +1,16 @@ using System.Diagnostics; using System.Globalization; +using System.Numerics; using Funcky.Extensions; using Funcky.Monads; namespace Funcky; [DebuggerDisplay("{Amount} {Currency.AlphabeticCurrencyCode,nq}")] -public sealed partial record Money : IMoneyExpression +public sealed record Money : IMoneyExpression + where TUnderlyingType : INumberBase { - public static readonly Money Zero = new(0m); + public static readonly Money Zero = new(0m); public Money(decimal amount, Option currency = default) { @@ -39,52 +41,49 @@ public bool IsZero => Amount == 0m; // These operators supports the operators on IMoneyExpression, because Money + Money or Money * factor does not work otherwise without a cast. - public static IMoneyExpression operator +(Money augend, IMoneyExpression addend) + public static IMoneyExpression operator +(Money augend, IMoneyExpression addend) => augend.Add(addend); - public static IMoneyExpression operator +(Money money) + public static IMoneyExpression operator +(Money money) => money; - public static IMoneyExpression operator -(Money minuend, IMoneyExpression subtrahend) + public static IMoneyExpression operator -(Money minuend, IMoneyExpression subtrahend) => minuend.Subtract(subtrahend); - public static Money operator -(Money money) + public static Money operator -(Money money) => money with { Amount = -money.Amount }; - public static IMoneyExpression operator *(Money multiplicand, decimal multiplier) + public static IMoneyExpression operator *(Money multiplicand, decimal multiplier) => multiplicand.Multiply(multiplier); - public static IMoneyExpression operator *(decimal multiplier, Money multiplicand) + public static IMoneyExpression operator *(decimal multiplier, Money multiplicand) => multiplicand.Multiply(multiplier); - public static IMoneyExpression operator /(Money dividend, decimal divisor) + public static IMoneyExpression operator /(Money dividend, decimal divisor) => dividend.Divide(divisor); - public static decimal operator /(Money dividend, IMoneyExpression divisor) + public static decimal operator /(Money dividend, IMoneyExpression divisor) => dividend.Divide(divisor); private static Currency SelectCurrency(Option currency) => currency.GetOrElse(CurrencyCulture.CurrentCurrency); - public static Option ParseOrNone(string money, Option currency = default) + public static Option> ParseOrNone(string money, Option currency = default) => CurrencyCulture .FormatProviderFromCurrency(SelectCurrency(currency)) .Match( none: ParseManually(money), some: ParseWithFormatProvider(money)) - .AndThen(amount => new Money(amount, SelectCurrency(currency))); + .AndThen(amount => new Money(amount, SelectCurrency(currency))); private static Func> ParseManually(string money) => () => RemoveIsoCurrency(money).ParseDecimalOrNone(); private static string RemoveIsoCurrency(string money) - { - var parts = money.Split(' '); - return parts.Length == 2 && parts[1].Length == 3 - ? parts[0] + => money.Split(' ') is [var first, { Length: 3 }] + ? first : money; - } private static Func> ParseWithFormatProvider(string money) => formatProvider @@ -95,6 +94,6 @@ public override string ToString() none: () => string.Format($"{{0:N{Currency.MinorUnitDigits}}} {{1}}", Amount, Currency.AlphabeticCurrencyCode), some: formatProvider => string.Format(formatProvider, $"{{0:C{Currency.MinorUnitDigits}}}", Amount)); - TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) + TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) => visitor.Visit(this); } From c53dca7c2f2014e7b13e9a52a2d13a3f7f6b3ff6 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Fri, 25 Aug 2023 10:25:26 +0200 Subject: [PATCH 4/6] use generic math for rounding stratgey --- Funcky.Money.Test/MoneyArbitraries.cs | 2 +- Funcky.Money.Test/MoneyTest.cs | 4 +-- .../DefaultDistributionStrategy.cs | 4 +-- .../Extensions/MoneyEvaluationExtension.cs | 2 +- Funcky.Money/MetaInformation/Power.cs | 17 +++++++---- Funcky.Money/Money.cs | 30 +++++++++---------- Funcky.Money/MoneyEvaluationContext.cs | 14 ++++----- Funcky.Money/Rounding/BankersRounding.cs | 20 +++++++------ Funcky.Money/Rounding/IRoundingStrategy.cs | 4 +-- Funcky.Money/Rounding/NoRounding.cs | 8 ++--- .../Rounding/RoundWithAwayFromZero.cs | 20 +++++++------ Funcky.Money/Rounding/RoundingStrategy.cs | 24 +++++++-------- .../Rounding/RoundingStrategyExtension.cs | 2 +- Funcky.Money/Visitors/MoneyBag.cs | 4 +-- 14 files changed, 82 insertions(+), 73 deletions(-) diff --git a/Funcky.Money.Test/MoneyArbitraries.cs b/Funcky.Money.Test/MoneyArbitraries.cs index 0fc8cd2..dc1ef5d 100644 --- a/Funcky.Money.Test/MoneyArbitraries.cs +++ b/Funcky.Money.Test/MoneyArbitraries.cs @@ -16,7 +16,7 @@ public static Arbitrary ArbitrarySwissMoney() private static Gen GenerateMoney() => from currency in Arb.Generate() from amount in Arb.Generate() - select new Money(Power.OfATenth(currency.MinorUnitDigits) * amount, currency); + select new Money(Power.OfATenth(currency.MinorUnitDigits) * amount, currency); private static Gen GenerateSwissFranc() => from amount in Arb.Generate() diff --git a/Funcky.Money.Test/MoneyTest.cs b/Funcky.Money.Test/MoneyTest.cs index 68e69a4..44e0b26 100644 --- a/Funcky.Money.Test/MoneyTest.cs +++ b/Funcky.Money.Test/MoneyTest.cs @@ -286,8 +286,8 @@ public void DefaultRoundingStrategyIsBankersRounding() var francs = new Money(1m, SwissRounding); var evaluationContext = MoneyEvaluationContext.Builder.Default.WithTargetCurrency(Currency.CHF); - Assert.Equal(new BankersRounding(0.05m), francs.RoundingStrategy); - Assert.Equal(new BankersRounding(0.01m), francs.Evaluate(evaluationContext.Build()).RoundingStrategy); + Assert.Equal(new BankersRounding(0.05m), francs.RoundingStrategy); + Assert.Equal(new BankersRounding(0.01m), francs.Evaluate(evaluationContext.Build()).RoundingStrategy); } [Fact] diff --git a/Funcky.Money/Distribution/DefaultDistributionStrategy.cs b/Funcky.Money/Distribution/DefaultDistributionStrategy.cs index 7e1054b..c962dcf 100644 --- a/Funcky.Money/Distribution/DefaultDistributionStrategy.cs +++ b/Funcky.Money/Distribution/DefaultDistributionStrategy.cs @@ -41,7 +41,7 @@ private bool BetweenZeroToOneDistributionUnitLeft(MoneyDistributionPart part, Mo private decimal AlreadyDistributed(MoneyDistributionPart part, Money money) => SignedPrecision(part.Distribution, money) * part.Index; - private IRoundingStrategy RoundingStrategy(Money money) + private IRoundingStrategy RoundingStrategy(Money money) => _context.Match( some: c => c.RoundingStrategy, none: money.RoundingStrategy); @@ -70,7 +70,7 @@ private decimal Precision(MoneyDistribution distribution, Money money) => distribution .Precision .OrElse(_context.AndThen(c => c.DistributionUnit)) - .GetOrElse(Power.OfATenth(MinorUnitDigits(money))); + .GetOrElse(Power.OfATenth(MinorUnitDigits(money))); private int MinorUnitDigits(Money money) => _context.Match( diff --git a/Funcky.Money/Extensions/MoneyEvaluationExtension.cs b/Funcky.Money/Extensions/MoneyEvaluationExtension.cs index 12f34e8..56401ea 100644 --- a/Funcky.Money/Extensions/MoneyEvaluationExtension.cs +++ b/Funcky.Money/Extensions/MoneyEvaluationExtension.cs @@ -26,7 +26,7 @@ private static EvaluationVisitor CreateVisitor(Option co private static IDistributionStrategy CreateDistributionStrategy(Option context) => new DefaultDistributionStrategy(context); - private static IRoundingStrategy FindRoundingStrategy(Money money, Option context) + private static IRoundingStrategy FindRoundingStrategy(Money money, Option context) => context .AndThen(c => c.RoundingStrategy) .GetOrElse(money.RoundingStrategy); diff --git a/Funcky.Money/MetaInformation/Power.cs b/Funcky.Money/MetaInformation/Power.cs index 29bb9f5..234d334 100644 --- a/Funcky.Money/MetaInformation/Power.cs +++ b/Funcky.Money/MetaInformation/Power.cs @@ -1,10 +1,17 @@ +using System.Numerics; + namespace Funcky; -internal static class Power +internal static class Power + where TUnderlyingType : INumberBase { - public static decimal OfTen(int exponent) - => Enumerable.Repeat(10m, exponent).Aggregate(1m, (p, b) => b * p); + public static TUnderlyingType OfATenth(int exponent) + => Exp(TUnderlyingType.CreateChecked(0.1m), exponent); + + private static TUnderlyingType Exp(TUnderlyingType @base, int exponent) + => Enumerable.Repeat(@base, exponent) + .Aggregate(TUnderlyingType.One, Multiply); - public static decimal OfATenth(int exponent) - => Enumerable.Repeat(0.1m, exponent).Aggregate(1m, (p, b) => b * p); + private static TUnderlyingType Multiply(TUnderlyingType multiplicand, TUnderlyingType multiplier) + => multiplicand * multiplier; } diff --git a/Funcky.Money/Money.cs b/Funcky.Money/Money.cs index 34d65f8..71e537d 100644 --- a/Funcky.Money/Money.cs +++ b/Funcky.Money/Money.cs @@ -1,16 +1,14 @@ using System.Diagnostics; using System.Globalization; -using System.Numerics; using Funcky.Extensions; using Funcky.Monads; namespace Funcky; [DebuggerDisplay("{Amount} {Currency.AlphabeticCurrencyCode,nq}")] -public sealed record Money : IMoneyExpression - where TUnderlyingType : INumberBase +public sealed partial record Money : IMoneyExpression { - public static readonly Money Zero = new(0m); + public static readonly Money Zero = new(0m); public Money(decimal amount, Option currency = default) { @@ -35,46 +33,46 @@ public Money(int amount, Option currency = default) public Currency Currency { get; init; } - public IRoundingStrategy RoundingStrategy { get; } + public IRoundingStrategy RoundingStrategy { get; } public bool IsZero => Amount == 0m; // These operators supports the operators on IMoneyExpression, because Money + Money or Money * factor does not work otherwise without a cast. - public static IMoneyExpression operator +(Money augend, IMoneyExpression addend) + public static IMoneyExpression operator +(Money augend, IMoneyExpression addend) => augend.Add(addend); - public static IMoneyExpression operator +(Money money) + public static IMoneyExpression operator +(Money money) => money; - public static IMoneyExpression operator -(Money minuend, IMoneyExpression subtrahend) + public static IMoneyExpression operator -(Money minuend, IMoneyExpression subtrahend) => minuend.Subtract(subtrahend); - public static Money operator -(Money money) + public static Money operator -(Money money) => money with { Amount = -money.Amount }; - public static IMoneyExpression operator *(Money multiplicand, decimal multiplier) + public static IMoneyExpression operator *(Money multiplicand, decimal multiplier) => multiplicand.Multiply(multiplier); - public static IMoneyExpression operator *(decimal multiplier, Money multiplicand) + public static IMoneyExpression operator *(decimal multiplier, Money multiplicand) => multiplicand.Multiply(multiplier); - public static IMoneyExpression operator /(Money dividend, decimal divisor) + public static IMoneyExpression operator /(Money dividend, decimal divisor) => dividend.Divide(divisor); - public static decimal operator /(Money dividend, IMoneyExpression divisor) + public static decimal operator /(Money dividend, IMoneyExpression divisor) => dividend.Divide(divisor); private static Currency SelectCurrency(Option currency) => currency.GetOrElse(CurrencyCulture.CurrentCurrency); - public static Option> ParseOrNone(string money, Option currency = default) + public static Option ParseOrNone(string money, Option currency = default) => CurrencyCulture .FormatProviderFromCurrency(SelectCurrency(currency)) .Match( none: ParseManually(money), some: ParseWithFormatProvider(money)) - .AndThen(amount => new Money(amount, SelectCurrency(currency))); + .AndThen(amount => new Money(amount, SelectCurrency(currency))); private static Func> ParseManually(string money) => () @@ -94,6 +92,6 @@ public override string ToString() none: () => string.Format($"{{0:N{Currency.MinorUnitDigits}}} {{1}}", Amount, Currency.AlphabeticCurrencyCode), some: formatProvider => string.Format(formatProvider, $"{{0:C{Currency.MinorUnitDigits}}}", Amount)); - TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) + TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) => visitor.Visit(this); } diff --git a/Funcky.Money/MoneyEvaluationContext.cs b/Funcky.Money/MoneyEvaluationContext.cs index 1707c17..10a3528 100644 --- a/Funcky.Money/MoneyEvaluationContext.cs +++ b/Funcky.Money/MoneyEvaluationContext.cs @@ -5,11 +5,11 @@ namespace Funcky; public sealed class MoneyEvaluationContext { - private MoneyEvaluationContext(Currency targetCurrency, Option distributionUnit, Option roundingStrategy, IBank bank) + private MoneyEvaluationContext(Currency targetCurrency, Option distributionUnit, Option> roundingStrategy, IBank bank) { TargetCurrency = targetCurrency; DistributionUnit = distributionUnit; - RoundingStrategy = roundingStrategy.GetOrElse(Funcky.RoundingStrategy.Default(distributionUnit.GetOrElse(Power.OfATenth(TargetCurrency.MinorUnitDigits)))); + RoundingStrategy = roundingStrategy.GetOrElse(Funcky.RoundingStrategy.Default(distributionUnit.GetOrElse(Power.OfATenth(TargetCurrency.MinorUnitDigits)))); Bank = bank; } @@ -27,7 +27,7 @@ private MoneyEvaluationContext(Currency targetCurrency, Option distribu /// /// Defines how we round amounts in the evaluation. /// - public IRoundingStrategy RoundingStrategy { get; } + public IRoundingStrategy RoundingStrategy { get; } /// /// Source for exchange rates. @@ -40,7 +40,7 @@ public sealed class Builder private readonly Option _targetCurrency; private readonly Option _distributionUnit; - private readonly Option _roundingStrategy; + private readonly Option> _roundingStrategy; private readonly IBank _bank; private Builder() @@ -51,7 +51,7 @@ private Builder() _bank = DefaultBank.Empty; } - private Builder(Option currency, Option distributionUnit, Option roundingStrategy, IBank bank) + private Builder(Option currency, Option distributionUnit, Option> roundingStrategy, IBank bank) { _targetCurrency = currency; _distributionUnit = distributionUnit; @@ -67,7 +67,7 @@ public MoneyEvaluationContext Build() public Builder WithTargetCurrency(Currency currency) => With(targetCurrency: currency); - public Builder WithRounding(IRoundingStrategy roundingStrategy) + public Builder WithRounding(IRoundingStrategy roundingStrategy) => With(roundingStrategy: Option.Some(roundingStrategy)); public Builder WithExchangeRate(Currency currency, decimal sellRate) @@ -81,7 +81,7 @@ public Builder WithBank(IBank bank) public Builder WithSmallestDistributionUnit(decimal distributionUnit) => With(distributionUnit: distributionUnit); - private Builder With(Option targetCurrency = default, Option distributionUnit = default, Option roundingStrategy = default, Option bank = default) + private Builder With(Option targetCurrency = default, Option distributionUnit = default, Option> roundingStrategy = default, Option bank = default) => new( targetCurrency.OrElse(_targetCurrency), distributionUnit.OrElse(_distributionUnit), diff --git a/Funcky.Money/Rounding/BankersRounding.cs b/Funcky.Money/Rounding/BankersRounding.cs index 04e1cec..17e4791 100644 --- a/Funcky.Money/Rounding/BankersRounding.cs +++ b/Funcky.Money/Rounding/BankersRounding.cs @@ -1,15 +1,17 @@ using System.Diagnostics; +using System.Numerics; namespace Funcky; [DebuggerDisplay("{ToString()}")] -internal sealed record BankersRounding : IRoundingStrategy +internal sealed record BankersRounding : IRoundingStrategy + where TUnderlyingType : INumberBase, IFloatingPoint { - private readonly decimal _precision; + private readonly TUnderlyingType _precision; - public BankersRounding(decimal precision) + public BankersRounding(TUnderlyingType precision) { - if (precision <= 0m) + if (precision <= TUnderlyingType.Zero) { throw new InvalidPrecisionException(); } @@ -19,15 +21,15 @@ public BankersRounding(decimal precision) public BankersRounding(Currency currency) { - _precision = Power.OfATenth(currency.MinorUnitDigits); + _precision = Power.OfATenth(currency.MinorUnitDigits); } - public bool Equals(IRoundingStrategy? roundingStrategy) - => roundingStrategy is BankersRounding bankersRounding + public bool Equals(IRoundingStrategy? roundingStrategy) + => roundingStrategy is BankersRounding bankersRounding && Equals(bankersRounding); - public decimal Round(decimal value) - => Math.Round(value / _precision, MidpointRounding.ToEven) * _precision; + public TUnderlyingType Round(TUnderlyingType value) + => TUnderlyingType.Round(value / _precision, MidpointRounding.ToEven) * _precision; public override string ToString() => $"BankersRounding {{ Precision: {_precision} }}"; diff --git a/Funcky.Money/Rounding/IRoundingStrategy.cs b/Funcky.Money/Rounding/IRoundingStrategy.cs index 7573f85..aefacb1 100644 --- a/Funcky.Money/Rounding/IRoundingStrategy.cs +++ b/Funcky.Money/Rounding/IRoundingStrategy.cs @@ -1,6 +1,6 @@ namespace Funcky; -public interface IRoundingStrategy : IEquatable +public interface IRoundingStrategy : IEquatable> { - decimal Round(decimal value); + TUnderlyingType Round(TUnderlyingType value); } diff --git a/Funcky.Money/Rounding/NoRounding.cs b/Funcky.Money/Rounding/NoRounding.cs index 8c9fda2..3474c96 100644 --- a/Funcky.Money/Rounding/NoRounding.cs +++ b/Funcky.Money/Rounding/NoRounding.cs @@ -3,12 +3,12 @@ namespace Funcky; [DebuggerDisplay("NoRounding")] -internal sealed record NoRounding : IRoundingStrategy +internal sealed record NoRounding : IRoundingStrategy { - public decimal Round(decimal value) + public TUnderlyingType Round(TUnderlyingType value) => value; - public bool Equals(IRoundingStrategy? roundingStrategy) - => roundingStrategy is NoRounding noRounding + public bool Equals(IRoundingStrategy? roundingStrategy) + => roundingStrategy is NoRounding noRounding && Equals(noRounding); } diff --git a/Funcky.Money/Rounding/RoundWithAwayFromZero.cs b/Funcky.Money/Rounding/RoundWithAwayFromZero.cs index 59ea718..3ad5e41 100644 --- a/Funcky.Money/Rounding/RoundWithAwayFromZero.cs +++ b/Funcky.Money/Rounding/RoundWithAwayFromZero.cs @@ -1,13 +1,15 @@ using System.Diagnostics; +using System.Numerics; namespace Funcky; [DebuggerDisplay("{ToString()}")] -internal sealed record RoundWithAwayFromZero : IRoundingStrategy +internal sealed record RoundWithAwayFromZero : IRoundingStrategy + where TUnderlyingType : INumberBase, IFloatingPoint { - public RoundWithAwayFromZero(decimal precision) + public RoundWithAwayFromZero(TUnderlyingType precision) { - if (precision <= 0m) + if (precision <= TUnderlyingType.Zero) { throw new InvalidPrecisionException(); } @@ -17,18 +19,18 @@ public RoundWithAwayFromZero(decimal precision) public RoundWithAwayFromZero(Currency currency) { - Precision = Power.OfATenth(currency.MinorUnitDigits); + Precision = Power.OfATenth(currency.MinorUnitDigits); } - public decimal Precision { get; } + public TUnderlyingType Precision { get; } - public decimal Round(decimal value) - => Math.Round(value / Precision, MidpointRounding.AwayFromZero) * Precision; + public TUnderlyingType Round(TUnderlyingType value) + => TUnderlyingType.Round(value / Precision, MidpointRounding.AwayFromZero) * Precision; public override string ToString() => $"Round {{ MidpointRounding: AwayFromZero, Precision: {Precision} }}"; - public bool Equals(IRoundingStrategy? roundingStrategy) - => roundingStrategy is RoundWithAwayFromZero roundWithAwayFromZero + public bool Equals(IRoundingStrategy? roundingStrategy) + => roundingStrategy is RoundWithAwayFromZero roundWithAwayFromZero && Equals(roundWithAwayFromZero); } diff --git a/Funcky.Money/Rounding/RoundingStrategy.cs b/Funcky.Money/Rounding/RoundingStrategy.cs index fccdd0c..2663ae5 100644 --- a/Funcky.Money/Rounding/RoundingStrategy.cs +++ b/Funcky.Money/Rounding/RoundingStrategy.cs @@ -2,24 +2,24 @@ namespace Funcky; public static class RoundingStrategy { - public static IRoundingStrategy NoRounding() - => new NoRounding(); + public static IRoundingStrategy NoRounding() + => new NoRounding(); - public static IRoundingStrategy BankersRounding(decimal precision) - => new BankersRounding(precision); + public static IRoundingStrategy BankersRounding(decimal precision) + => new BankersRounding(precision); - public static IRoundingStrategy BankersRounding(Currency currency) - => new BankersRounding(currency); + public static IRoundingStrategy BankersRounding(Currency currency) + => new BankersRounding(currency); - public static IRoundingStrategy RoundWithAwayFromZero(decimal precision) - => new RoundWithAwayFromZero(precision); + public static IRoundingStrategy RoundWithAwayFromZero(decimal precision) + => new RoundWithAwayFromZero(precision); - public static IRoundingStrategy RoundWithAwayFromZero(Currency currency) - => new RoundWithAwayFromZero(currency); + public static IRoundingStrategy RoundWithAwayFromZero(Currency currency) + => new RoundWithAwayFromZero(currency); - internal static IRoundingStrategy Default(decimal precision) + internal static IRoundingStrategy Default(decimal precision) => BankersRounding(precision); - internal static IRoundingStrategy Default(Currency currency) + internal static IRoundingStrategy Default(Currency currency) => BankersRounding(currency); } diff --git a/Funcky.Money/Rounding/RoundingStrategyExtension.cs b/Funcky.Money/Rounding/RoundingStrategyExtension.cs index ebbb2cf..b66f520 100644 --- a/Funcky.Money/Rounding/RoundingStrategyExtension.cs +++ b/Funcky.Money/Rounding/RoundingStrategyExtension.cs @@ -2,6 +2,6 @@ namespace Funcky; internal static class RoundingStrategyExtension { - public static bool IsSameAfterRounding(this IRoundingStrategy roundingStrategy, decimal amount) + public static bool IsSameAfterRounding(this IRoundingStrategy roundingStrategy, decimal amount) => roundingStrategy.Round(amount) == amount; } diff --git a/Funcky.Money/Visitors/MoneyBag.cs b/Funcky.Money/Visitors/MoneyBag.cs index 1b96948..b035f50 100644 --- a/Funcky.Money/Visitors/MoneyBag.cs +++ b/Funcky.Money/Visitors/MoneyBag.cs @@ -6,7 +6,7 @@ namespace Funcky; internal sealed class MoneyBag { private readonly Dictionary> _currencies = new(); - private Option _roundingStrategy; + private Option> _roundingStrategy; private Option _emptyCurrency; public MoneyBag(Money money) @@ -87,7 +87,7 @@ private void CheckEvaluationRules(Money money) none: () => _roundingStrategy = Option.Some(money.RoundingStrategy), some: r => CheckRoundingStrategy(money, r)); - private static void CheckRoundingStrategy(Money money, IRoundingStrategy roundingStrategy) + private static void CheckRoundingStrategy(Money money, IRoundingStrategy roundingStrategy) { if (!money.RoundingStrategy.Equals(roundingStrategy)) { From 5f4c5df6a004b4e906f5008e33668bcf5e1f033b Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Fri, 25 Aug 2023 10:31:06 +0200 Subject: [PATCH 5/6] use generic math for IBank --- Funcky.Money.Test/MoneyTest.cs | 2 +- Funcky.Money.Test/OneToOneBank.cs | 11 +++++++---- Funcky.Money/Bank/DefaultBank.cs | 16 +++++++++------- Funcky.Money/Bank/IBank.cs | 4 ++-- Funcky.Money/MoneyEvaluationContext.cs | 16 ++++++++-------- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/Funcky.Money.Test/MoneyTest.cs b/Funcky.Money.Test/MoneyTest.cs index 44e0b26..74dd558 100644 --- a/Funcky.Money.Test/MoneyTest.cs +++ b/Funcky.Money.Test/MoneyTest.cs @@ -517,6 +517,6 @@ private static MoneyEvaluationContext OneToOneContext(Currency targetCurrency) .Builder .Default .WithTargetCurrency(targetCurrency) - .WithBank(OneToOneBank.Instance) + .WithBank(OneToOneBank.Instance) .Build(); } diff --git a/Funcky.Money.Test/OneToOneBank.cs b/Funcky.Money.Test/OneToOneBank.cs index d4e8711..09d41aa 100644 --- a/Funcky.Money.Test/OneToOneBank.cs +++ b/Funcky.Money.Test/OneToOneBank.cs @@ -1,10 +1,13 @@ +using System.Numerics; + namespace Funcky.Test; // Bank which returns a 1:1 exchange rate for every pair of currencies. -internal sealed class OneToOneBank : IBank +internal sealed class OneToOneBank : IBank + where TUnderlyingType : INumberBase { - public static readonly IBank Instance = new OneToOneBank(); + public static readonly IBank Instance = new OneToOneBank(); - public decimal ExchangeRate(Currency source, Currency target) - => 1m; + public TUnderlyingType ExchangeRate(Currency source, Currency target) + => TUnderlyingType.One; } diff --git a/Funcky.Money/Bank/DefaultBank.cs b/Funcky.Money/Bank/DefaultBank.cs index d9f3a17..72f91f7 100644 --- a/Funcky.Money/Bank/DefaultBank.cs +++ b/Funcky.Money/Bank/DefaultBank.cs @@ -1,29 +1,31 @@ using System.Collections.Immutable; +using System.Numerics; using Funcky.Extensions; namespace Funcky; -internal sealed class DefaultBank : IBank +internal sealed class DefaultBank : IBank + where TUnderlyingType : INumberBase { - internal static readonly DefaultBank Empty = new(); + internal static readonly DefaultBank Empty = new(); - public DefaultBank(ImmutableDictionary<(Currency Source, Currency Target), decimal> exchangeRates) + public DefaultBank(ImmutableDictionary<(Currency Source, Currency Target), TUnderlyingType> exchangeRates) { ExchangeRates = exchangeRates; } private DefaultBank() { - ExchangeRates = ImmutableDictionary<(Currency Source, Currency Target), decimal>.Empty; + ExchangeRates = ImmutableDictionary<(Currency Source, Currency Target), TUnderlyingType>.Empty; } - public ImmutableDictionary<(Currency Source, Currency Target), decimal> ExchangeRates { get; } + public ImmutableDictionary<(Currency Source, Currency Target), TUnderlyingType> ExchangeRates { get; } - public decimal ExchangeRate(Currency source, Currency target) + public TUnderlyingType ExchangeRate(Currency source, Currency target) => ExchangeRates .GetValueOrNone(key: (source, target)) .GetOrElse(() => throw new MissingExchangeRateException($"No exchange rate for {source.AlphabeticCurrencyCode} => {target.AlphabeticCurrencyCode}.")); - internal DefaultBank AddExchangeRate(Currency source, Currency target, decimal sellRate) + internal DefaultBank AddExchangeRate(Currency source, Currency target, TUnderlyingType sellRate) => new(ExchangeRates.Add((source, target), sellRate)); } diff --git a/Funcky.Money/Bank/IBank.cs b/Funcky.Money/Bank/IBank.cs index f1a34f9..cac8770 100644 --- a/Funcky.Money/Bank/IBank.cs +++ b/Funcky.Money/Bank/IBank.cs @@ -1,6 +1,6 @@ namespace Funcky; -public interface IBank +public interface IBank { - decimal ExchangeRate(Currency source, Currency target); + TUnderlyingType ExchangeRate(Currency source, Currency target); } diff --git a/Funcky.Money/MoneyEvaluationContext.cs b/Funcky.Money/MoneyEvaluationContext.cs index 10a3528..3665ce5 100644 --- a/Funcky.Money/MoneyEvaluationContext.cs +++ b/Funcky.Money/MoneyEvaluationContext.cs @@ -5,7 +5,7 @@ namespace Funcky; public sealed class MoneyEvaluationContext { - private MoneyEvaluationContext(Currency targetCurrency, Option distributionUnit, Option> roundingStrategy, IBank bank) + private MoneyEvaluationContext(Currency targetCurrency, Option distributionUnit, Option> roundingStrategy, IBank bank) { TargetCurrency = targetCurrency; DistributionUnit = distributionUnit; @@ -32,7 +32,7 @@ private MoneyEvaluationContext(Currency targetCurrency, Option distribu /// /// Source for exchange rates. /// - public IBank Bank { get; } + public IBank Bank { get; } public sealed class Builder { @@ -41,17 +41,17 @@ public sealed class Builder private readonly Option _targetCurrency; private readonly Option _distributionUnit; private readonly Option> _roundingStrategy; - private readonly IBank _bank; + private readonly IBank _bank; private Builder() { _targetCurrency = default; _roundingStrategy = default; _distributionUnit = default; - _bank = DefaultBank.Empty; + _bank = DefaultBank.Empty; } - private Builder(Option currency, Option distributionUnit, Option> roundingStrategy, IBank bank) + private Builder(Option currency, Option distributionUnit, Option> roundingStrategy, IBank bank) { _targetCurrency = currency; _distributionUnit = distributionUnit; @@ -71,17 +71,17 @@ public Builder WithRounding(IRoundingStrategy roundingStrategy) => With(roundingStrategy: Option.Some(roundingStrategy)); public Builder WithExchangeRate(Currency currency, decimal sellRate) - => _bank is DefaultBank bank + => _bank is DefaultBank bank ? With(bank: bank.AddExchangeRate(currency, GetTargetCurrencyOrException(), sellRate)) : throw new InvalidMoneyEvaluationContextBuilderException("You can either use WithExchangeRate or WithBank, but not both."); - public Builder WithBank(IBank bank) + public Builder WithBank(IBank bank) => With(bank: Option.Some(bank)); public Builder WithSmallestDistributionUnit(decimal distributionUnit) => With(distributionUnit: distributionUnit); - private Builder With(Option targetCurrency = default, Option distributionUnit = default, Option> roundingStrategy = default, Option bank = default) + private Builder With(Option targetCurrency = default, Option distributionUnit = default, Option> roundingStrategy = default, Option> bank = default) => new( targetCurrency.OrElse(_targetCurrency), distributionUnit.OrElse(_distributionUnit), From c758d0a3392d7f3bda10d6a05b0ad8fe3925b292 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Fri, 25 Aug 2023 12:19:08 +0200 Subject: [PATCH 6/6] Finish genericity and keep the tests at decimal so far. --- .../Iso4217RecordGenerator.cs | 8 +- Funcky.Money.Test/MoneyArbitraries.cs | 8 +- Funcky.Money.Test/MoneyTest.cs | 190 +++++++++--------- Funcky.Money.Test/SwissMoney.cs | 2 +- .../DefaultDistributionStrategy.cs | 60 +++--- .../Distribution/IDistributionStrategy.cs | 7 +- .../ExpressionNodes/IMoneyExpression.cs | 27 +-- .../ExpressionNodes/MoneyDistribution.cs | 14 +- .../ExpressionNodes/MoneyDistributionPart.cs | 11 +- Funcky.Money/ExpressionNodes/MoneyProduct.cs | 13 +- Funcky.Money/ExpressionNodes/MoneySum.cs | 13 +- .../Extensions/MoneyDistributionExtension.cs | 9 +- .../Extensions/MoneyDivisionExtension.cs | 12 +- .../Extensions/MoneyEvaluationExtension.cs | 27 ++- .../MoneyMultiplicationExtension.cs | 7 +- .../Extensions/MoneySubtractionExtension.cs | 7 +- .../Extensions/MoneySummationExtension.cs | 7 +- Funcky.Money/Extensions/SignExtension.cs | 11 - .../Extensions/ToHumanReadableExtension.cs | 7 +- Funcky.Money/Money.cs | 55 ++--- Funcky.Money/MoneyEvaluationContext.cs | 40 ++-- Funcky.Money/Rounding/BankersRounding.cs | 2 +- .../Rounding/RoundWithAwayFromZero.cs | 2 +- Funcky.Money/Rounding/RoundingStrategy.cs | 29 +-- .../Rounding/RoundingStrategyExtension.cs | 5 +- Funcky.Money/Visitors/EvaluationVisitor.cs | 22 +- .../Visitors/IMoneyExpressionVisitor.cs | 13 +- Funcky.Money/Visitors/MoneyBag.cs | 44 ++-- .../Visitors/ToHumanReadableVisitor.cs | 18 +- 29 files changed, 362 insertions(+), 308 deletions(-) delete mode 100644 Funcky.Money/Extensions/SignExtension.cs diff --git a/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs b/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs index 1f23777..5c5e837 100644 --- a/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs +++ b/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs @@ -103,7 +103,7 @@ private static string GenerateMoneyClass(IEnumerable records) => $"using Funcky.Monads;\n" + $"namespace {RootNamespace}\n" + $"{{\n" + - $"{Indent}public partial record Money\n" + + $"{Indent}public partial record Money\n" + $"{Indent}{{\n" + $"{GenerateMoneyFactoryMethods(records)}" + $"{Indent}}}\n" + @@ -119,9 +119,9 @@ private static string CreateCurrencyFactory(Iso4217Record record) { var identifier = Identifier(record.AlphabeticCurrencyCode); - return $"{Indent}{Indent}/// Creates a new instance using the currency.\n" + - $"{Indent}{Indent}public static Money {identifier}(decimal amount)\n" + - $"{Indent}{Indent} => new(amount, MoneyEvaluationContext.Builder.Default.WithTargetCurrency(Currency.{identifier}).Build());"; + return $"{Indent}{Indent}/// Creates a new instance using the currency.\n" + + $"{Indent}{Indent}public static Money {identifier}(TUnderlyingType amount)\n" + + $"{Indent}{Indent} => new(amount, MoneyEvaluationContext.Builder.Default.WithTargetCurrency(Currency.{identifier}).Build());"; } private static IEnumerable ReadIso4217RecordsFromAdditionalFiles( diff --git a/Funcky.Money.Test/MoneyArbitraries.cs b/Funcky.Money.Test/MoneyArbitraries.cs index dc1ef5d..0953167 100644 --- a/Funcky.Money.Test/MoneyArbitraries.cs +++ b/Funcky.Money.Test/MoneyArbitraries.cs @@ -7,18 +7,18 @@ internal class MoneyArbitraries public static Arbitrary ArbitraryCurrency() => Arb.From(Gen.Elements(Currency.AllCurrencies)); - public static Arbitrary ArbitraryMoney() + public static Arbitrary> ArbitraryMoney() => GenerateMoney().ToArbitrary(); public static Arbitrary ArbitrarySwissMoney() => GenerateSwissFranc().ToArbitrary(); - private static Gen GenerateMoney() + private static Gen> GenerateMoney() => from currency in Arb.Generate() from amount in Arb.Generate() - select new Money(Power.OfATenth(currency.MinorUnitDigits) * amount, currency); + select new Money(Power.OfATenth(currency.MinorUnitDigits) * amount, currency); private static Gen GenerateSwissFranc() => from amount in Arb.Generate() - select new SwissMoney(Money.CHF(SwissMoney.SmallestCoin * amount)); + select new SwissMoney(Money.CHF(SwissMoney.SmallestCoin * amount)); } diff --git a/Funcky.Money.Test/MoneyTest.cs b/Funcky.Money.Test/MoneyTest.cs index 74dd558..ce93724 100644 --- a/Funcky.Money.Test/MoneyTest.cs +++ b/Funcky.Money.Test/MoneyTest.cs @@ -11,8 +11,8 @@ public MoneyTest() => Arb .Register(); - private static MoneyEvaluationContext SwissRounding - => MoneyEvaluationContext + private static MoneyEvaluationContext SwissRounding + => MoneyEvaluationContext .Builder .Default .WithTargetCurrency(Currency.CHF) @@ -20,15 +20,15 @@ private static MoneyEvaluationContext SwissRounding .Build(); [Property] - public Property EvaluatingAMoneyInTheSameCurrencyDoesReturnTheSameAmount(Money money) + public Property EvaluatingAMoneyInTheSameCurrencyDoesReturnTheSameAmount(Money money) { return (money.Amount == money.Evaluate().Amount).ToProperty(); } [Property] - public Property TheSumOfTwoMoneysIsCommutative(Money money1, Money money2) + public Property TheSumOfTwoMoneysIsCommutative(Money money1, Money money2) { - money2 = new Money(money2.Amount, money1.Currency); + money2 = new Money(money2.Amount, money1.Currency); return (money1.Add(money2).Evaluate().Amount == money2.Add(money1).Evaluate().Amount).ToProperty(); } @@ -36,8 +36,8 @@ public Property TheSumOfTwoMoneysIsCommutative(Money money1, Money money2) [Fact] public void WeCanBuildTheSumOfTwoMoneysWithDifferentCurrenciesButOnEvaluationYouNeedAnEvaluationContextWithDefinedExchangeRates() { - var fiveFrancs = new Money(5, Currency.CHF); - var tenDollars = new Money(10, Currency.USD); + var fiveFrancs = new Money(5, Currency.CHF); + var tenDollars = new Money(10, Currency.USD); var sum = fiveFrancs.Add(tenDollars); @@ -48,14 +48,14 @@ public void WeCanBuildTheSumOfTwoMoneysWithDifferentCurrenciesButOnEvaluationYou [Property] public Property DollarsAreNotFrancs(decimal amount) { - var francs = Money.CHF(amount); - var dollars = Money.USD(amount); + var francs = Money.CHF(amount); + var dollars = Money.USD(amount); return (francs != dollars).ToProperty(); } [Property] - public Property MoneyCanBeMultipliedByConstantsFactors(Money someMoney, decimal multiplier) + public Property MoneyCanBeMultipliedByConstantsFactors(Money someMoney, decimal multiplier) { var result = decimal.Round(someMoney.Amount * multiplier, someMoney.Currency.MinorUnitDigits) == someMoney.Multiply(multiplier).Evaluate().Amount; @@ -82,7 +82,7 @@ public Property DistributeMoneyHasMinimalDifference(SwissMoney someMoney, Positi [MemberData(nameof(ProportionalDistributionData))] public void DistributeMoneyProportionally(int first, int second, decimal expected1, decimal expected2) { - var fiftyCents = Money.EUR(0.5m); + var fiftyCents = Money.EUR(0.5m); var sum = fiftyCents.Add(fiftyCents); var distribution = sum.Distribute(new[] { first, second }); @@ -99,7 +99,7 @@ public void DistributeMoneyProportionally(int first, int second, decimal expecte [MemberData(nameof(ProportionalDistributionData))] public void DistributeNegativeMoneyProportionally(int first, int second, decimal expected1, decimal expected2) { - var fiftyCents = Money.EUR(-0.5m); + var fiftyCents = Money.EUR(-0.5m); var sum = fiftyCents.Add(fiftyCents); var distribution = sum.Distribute(new[] { first, second }); @@ -128,10 +128,10 @@ public static TheoryData ProportionalDistributionDat [Fact] public void InputValuesGetRoundedDuringEvaluation() { - var fiveDollarsSeventy = Money.USD(5.7m); - var midpoint1 = Money.USD(5.715m); - var midpoint2 = Money.USD(5.725m); - var pi = Money.USD((decimal)Math.PI); + var fiveDollarsSeventy = Money.USD(5.7m); + var midpoint1 = Money.USD(5.715m); + var midpoint2 = Money.USD(5.725m); + var pi = Money.USD((decimal)Math.PI); Assert.Equal(5.70m, fiveDollarsSeventy.Evaluate().Amount); Assert.Equal(5.72m, midpoint1.Evaluate().Amount); @@ -142,13 +142,13 @@ public void InputValuesGetRoundedDuringEvaluation() [Fact] public void WeCanEvaluateASumOfDifferentCurrenciesWithAContextWhichDefinesExchangeRates() { - var fiveFrancs = new Money(5, Currency.CHF); - var tenDollars = new Money(10, Currency.USD); - var fiveEuros = new Money(5, Currency.EUR); + var fiveFrancs = new Money(5, Currency.CHF); + var tenDollars = new Money(10, Currency.USD); + var fiveEuros = new Money(5, Currency.EUR); var sum = fiveFrancs.Add(tenDollars).Add(fiveEuros).Multiply(2); - var context = MoneyEvaluationContext + var context = MoneyEvaluationContext .Builder .Default .WithTargetCurrency(Currency.CHF) @@ -162,27 +162,27 @@ public void WeCanEvaluateASumOfDifferentCurrenciesWithAContextWhichDefinesExchan [Fact] public void WeCanDefineMoneyExpressionsWithOperators() { - var fiveDollars = Money.USD(5); - var tenDollars = Money.USD(10); + var fiveDollars = Money.USD(5); + var tenDollars = Money.USD(10); var sum = fiveDollars + tenDollars + fiveDollars; var product = 3.00m * tenDollars; - Assert.Equal(Money.USD(20), sum.Evaluate()); - Assert.Equal(Money.USD(30), product.Evaluate()); + Assert.Equal(Money.USD(20), sum.Evaluate()); + Assert.Equal(Money.USD(30), product.Evaluate()); } [Property] - public Property TheMoneyNeutralElementWorksWithAnyCurrency(Money money) + public Property TheMoneyNeutralElementWorksWithAnyCurrency(Money money) { - return (money == (money + Money.Zero).Evaluate() - && (money == (Money.Zero + money).Evaluate())).ToProperty().When(!money.IsZero); + return (money == (money + Money.Zero).Evaluate() + && (money == (Money.Zero + money).Evaluate())).ToProperty().When(!money.IsZero); } [Property] public Property InASumOfMultipleZerosWithDifferentCurrenciesTheEvaluationHasTheSameCurrencyAsTheFirstMoneyInTheExpression(Currency c1, Currency c2, Currency c3) { - var sum = new Money(0m, c1) + new Money(0m, c2) + new Money(0m, c3) + new Money(0m, c2); + var sum = new Money(0m, c1) + new Money(0m, c2) + new Money(0m, c3) + new Money(0m, c2); return (sum.Evaluate().Currency == c1).ToProperty(); } @@ -190,8 +190,8 @@ public Property InASumOfMultipleZerosWithDifferentCurrenciesTheEvaluationHasTheS [Fact] public void MoneyFormatsCorrectlyAccordingToTheCurrency() { - var thousandFrancs = Money.CHF(-1000); - var thousandDollars = Money.USD(-1000); + var thousandFrancs = Money.CHF(-1000); + var thousandDollars = Money.USD(-1000); Assert.Equal("CHF-1’000.00", thousandFrancs.ToString()); Assert.Equal("-$1,000.00", thousandDollars.ToString()); @@ -200,14 +200,14 @@ public void MoneyFormatsCorrectlyAccordingToTheCurrency() [Fact] public void MoneyParsesCorrectlyFromString() { - var r1 = FunctionalAssert.Some(Money.ParseOrNone("CHF-1’000.00", Currency.CHF)); - Assert.Equal(new Money(-1000, Currency.CHF), r1); + var r1 = FunctionalAssert.Some(Money.ParseOrNone("CHF-1’000.00", Currency.CHF)); + Assert.Equal(new Money(-1000, Currency.CHF), r1); - var r2 = FunctionalAssert.Some(Money.ParseOrNone("-$1,000.00", Currency.USD)); - Assert.Equal(new Money(-1000, Currency.USD), r2); + var r2 = FunctionalAssert.Some(Money.ParseOrNone("-$1,000.00", Currency.USD)); + Assert.Equal(new Money(-1000, Currency.USD), r2); - var r3 = FunctionalAssert.Some(Money.ParseOrNone("1000", Currency.CHF)); - Assert.Equal(new Money(1000, Currency.CHF), r3); + var r3 = FunctionalAssert.Some(Money.ParseOrNone("1000", Currency.CHF)); + Assert.Equal(new Money(1000, Currency.CHF), r3); } [Fact] @@ -217,24 +217,24 @@ public void CurrenciesWithoutFormatProviders() // Funcky therefore chooses the current culture to format such currencies, that is why we set a specific one here. using var cultureSwitch = new TemporaryCultureSwitch("de-CH"); - var currencyWithoutFormatProvider = Money.XAU(9585); + var currencyWithoutFormatProvider = Money.XAU(9585); Assert.Equal("9’585 XAU", currencyWithoutFormatProvider.ToString()); - var money = FunctionalAssert.Some(Money.ParseOrNone("9’585.00 XAU", Currency.XAU)); - Assert.Equal(new Money(9585, Currency.XAU), money); + var money = FunctionalAssert.Some(Money.ParseOrNone("9’585.00 XAU", Currency.XAU)); + Assert.Equal(new Money(9585, Currency.XAU), money); } [Property] - public Property WeCanParseTheStringsWeGenerate(Money money) + public Property WeCanParseTheStringsWeGenerate(Money money) { - return Money.ParseOrNone(money.ToString(), money.Currency).Match(false, m => m == money).ToProperty(); + return Money.ParseOrNone(money.ToString(), money.Currency).Match(false, m => m == money).ToProperty(); } [Fact] public void ThePrecisionCanBeSetToSomethingOtherThanAPowerOfTen() { - var precision05 = new Money(1m, SwissRounding); - var precision002 = new Money(1m, MoneyEvaluationContext.Builder.Default.WithTargetCurrency(Currency.CHF).WithRounding(RoundingStrategy.BankersRounding(0.002m)).Build()); + var precision05 = new Money(1m, SwissRounding); + var precision002 = new Money(1m, MoneyEvaluationContext.Builder.Default.WithTargetCurrency(Currency.CHF).WithRounding(RoundingStrategy.BankersRounding(0.002m)).Build()); Assert.Collection( precision05.Distribute(3, 0.05m).Select(e => e.Evaluate().Amount), @@ -248,7 +248,7 @@ public void ThePrecisionCanBeSetToSomethingOtherThanAPowerOfTen() item => Assert.Equal(0.334m, item), item => Assert.Equal(0.332m, item)); - var francs = new Money(0.10m, SwissRounding); + var francs = new Money(0.10m, SwissRounding); Assert.Collection( francs.Distribute(3, 0.05m).Select(e => e.Evaluate().Amount), item => Assert.Equal(0.05m, item), @@ -259,13 +259,13 @@ public void ThePrecisionCanBeSetToSomethingOtherThanAPowerOfTen() [Fact] public void TheRoundingStrategyIsCorrectlyPassedThrough() { - var commonContext = MoneyEvaluationContext + var commonContext = MoneyEvaluationContext .Builder .Default .WithTargetCurrency(Currency.CHF); - var precision05 = new Money(1m, commonContext.WithRounding(RoundingStrategy.BankersRounding(SwissMoney.SmallestCoin)).Build()); - var precision002 = new Money(1m, commonContext.WithRounding(RoundingStrategy.BankersRounding(0.002m)).Build()); + var precision05 = new Money(1m, commonContext.WithRounding(RoundingStrategy.BankersRounding(SwissMoney.SmallestCoin)).Build()); + var precision002 = new Money(1m, commonContext.WithRounding(RoundingStrategy.BankersRounding(0.002m)).Build()); Assert.Equal(precision05.RoundingStrategy, precision05.Distribute(3, SwissMoney.SmallestCoin).First().Evaluate().RoundingStrategy); Assert.Equal(precision002.RoundingStrategy, precision002.Distribute(3, 0.002m).First().Evaluate().RoundingStrategy); @@ -274,7 +274,7 @@ public void TheRoundingStrategyIsCorrectlyPassedThrough() [Fact] public void DistributionMustDistributeExactlyTheGivenAmount() { - var francs = new Money(0.08m, SwissRounding); + var francs = new Money(0.08m, SwissRounding); Assert.Throws(() => francs.Distribute(3, 0.05m).Select(e => e.Evaluate()).First()); @@ -283,8 +283,8 @@ public void DistributionMustDistributeExactlyTheGivenAmount() [Fact] public void DefaultRoundingStrategyIsBankersRounding() { - var francs = new Money(1m, SwissRounding); - var evaluationContext = MoneyEvaluationContext.Builder.Default.WithTargetCurrency(Currency.CHF); + var francs = new Money(1m, SwissRounding); + var evaluationContext = MoneyEvaluationContext.Builder.Default.WithTargetCurrency(Currency.CHF); Assert.Equal(new BankersRounding(0.05m), francs.RoundingStrategy); Assert.Equal(new BankersRounding(0.01m), francs.Evaluate(evaluationContext.Build()).RoundingStrategy); @@ -293,9 +293,9 @@ public void DefaultRoundingStrategyIsBankersRounding() [Fact] public void WeCanDelegateTheExchangeRatesToABank() { - var fiveFrancs = new Money(5, Currency.CHF); - var tenDollars = new Money(10, Currency.USD); - var fiveEuros = new Money(5, Currency.EUR); + var fiveFrancs = new Money(5, Currency.CHF); + var tenDollars = new Money(10, Currency.USD); + var fiveEuros = new Money(5, Currency.EUR); var sum = (fiveFrancs + tenDollars + fiveEuros) * 1.5m; @@ -305,29 +305,29 @@ public void WeCanDelegateTheExchangeRatesToABank() [Fact] public void EvaluationOnZeroMoneysWorks() { - var sum = (Money.Zero + Money.Zero) * 1.5m; + var sum = (Money.Zero + Money.Zero) * 1.5m; var context = OneToOneContext(Currency.JPY); - Assert.True(Money.Zero.Evaluate(context).IsZero); + Assert.True(Money.Zero.Evaluate(context).IsZero); Assert.True(sum.Evaluate(context).IsZero); - Assert.True(Money.Zero.Evaluate().IsZero); + Assert.True(Money.Zero.Evaluate().IsZero); Assert.True(sum.Evaluate().IsZero); } [Fact] public void DifferentPrecisionsCannotBeEvaluatedWithoutAnEvaluationContext() { - var francs = MoneyEvaluationContext + var francs = MoneyEvaluationContext .Builder .Default .WithTargetCurrency(Currency.CHF); - var normalFrancs = francs.WithRounding(RoundingStrategy.BankersRounding(0.05m)); - var preciseFrancs = francs.WithRounding(RoundingStrategy.BankersRounding(0.001m)); + var normalFrancs = francs.WithRounding(RoundingStrategy.BankersRounding(0.05m)); + var preciseFrancs = francs.WithRounding(RoundingStrategy.BankersRounding(0.001m)); - var two = new Money(2, normalFrancs.Build()); - var oneHalf = new Money(0.5m, preciseFrancs.Build()); + var two = new Money(2, normalFrancs.Build()); + var oneHalf = new Money(0.5m, preciseFrancs.Build()); var sum = (two + oneHalf) * 0.01m; Assert.Throws(() => sum.Evaluate()); @@ -337,16 +337,16 @@ public void DifferentPrecisionsCannotBeEvaluatedWithoutAnEvaluationContext() [Fact] public void DifferentRoundingStrategiesCannotBeEvaluatedWithoutAnEvaluationContext() { - var francs = MoneyEvaluationContext + var francs = MoneyEvaluationContext .Builder .Default .WithTargetCurrency(Currency.CHF); - var normalFrancs = francs.WithRounding(RoundingStrategy.RoundWithAwayFromZero(0.05m)); - var preciseFrancs = francs.WithRounding(RoundingStrategy.BankersRounding(0.001m)); + var normalFrancs = francs.WithRounding(RoundingStrategy.RoundWithAwayFromZero(0.05m)); + var preciseFrancs = francs.WithRounding(RoundingStrategy.BankersRounding(0.001m)); - var two = new Money(2, normalFrancs.Build()); - var oneHalf = new Money(0.5m, preciseFrancs.Build()); + var two = new Money(2, normalFrancs.Build()); + var oneHalf = new Money(0.5m, preciseFrancs.Build()); var sum = (two + oneHalf) * 0.05m; Assert.Throws(() => sum.Evaluate()); @@ -356,12 +356,12 @@ public void DifferentRoundingStrategiesCannotBeEvaluatedWithoutAnEvaluationConte [Fact] public void RoundingIsOnlyDoneAtTheEndOfTheEvaluation() { - var francs = new Money(0.01m, SwissRounding); - var noRounding = MoneyEvaluationContext + var francs = new Money(0.01m, SwissRounding); + var noRounding = MoneyEvaluationContext .Builder .Default .WithTargetCurrency(Currency.CHF) - .WithRounding(RoundingStrategy.NoRounding()) + .WithRounding(RoundingStrategy.NoRounding()) .Build(); Assert.Equal(0.01m, francs.Amount); @@ -372,7 +372,7 @@ public void RoundingIsOnlyDoneAtTheEndOfTheEvaluation() [Fact] public void AllNecessaryOperatorsAreDefined() { - var francs = new Money(0.50m, SwissRounding); + var francs = new Money(0.50m, SwissRounding); var allOperators = -((((francs * 2) + francs) / 2) - +(francs * 2)); @@ -390,12 +390,12 @@ public void WeCanEvaluateComplexExpressions() [Fact] public void BuildContextFailsWhenCreatingItWithARoundingStrategyIncompatibleWithSmallestDistributionUnit() { - var incompatibleRoundingContext = MoneyEvaluationContext + var incompatibleRoundingContext = MoneyEvaluationContext .Builder .Default .WithTargetCurrency(Currency.CHF) .WithSmallestDistributionUnit(0.01m) - .WithRounding(RoundingStrategy.Default(0.05m)); + .WithRounding(RoundingStrategy.Default(0.05m)); Assert.Throws(() => incompatibleRoundingContext.Build()); } @@ -406,7 +406,7 @@ public void IfCurrencyIsOmittedOnConstructionOfAMoneyItGetsDeducedByTheCurrentCu { using var cultureSwitch = new TemporaryCultureSwitch(culture); - var money = new Money(5); + var money = new Money(5); Assert.Equal(currency, money.Currency); } @@ -426,18 +426,18 @@ public void YouCannotConstructAMoneyWithIllegalCulturesWithoutACorrectCurrencyIs { using var cultureSwitch = new TemporaryCultureSwitch("en-UK"); - Assert.Throws(() => new Money(5)); + Assert.Throws(() => new Money(5)); } [Fact] public void ToHumanReadableExtensionTransformsExpressionsCorrectly() { - var distribution = ((Money.CHF(1.5m) + Money.EUR(2.5m)) * 3).Distribute(new[] { 3, 1, 3, 2 }); - var expression = (distribution.Skip(2).First() + (Money.USD(2.99m) * 2)) / 2; - var sum = Money.CHF(30) + Money.JPY(500); - var product = Money.CHF(100) * 2.5m; - var difference = Money.CHF(200) - Money.JPY(500); - var quotient = Money.CHF(500) / 2; + var distribution = ((Money.CHF(1.5m) + Money.EUR(2.5m)) * 3).Distribute(new[] { 3, 1, 3, 2 }); + var expression = (distribution.Skip(2).First() + (Money.USD(2.99m) * 2)) / 2; + var sum = Money.CHF(30) + Money.JPY(500); + var product = Money.CHF(100) * 2.5m; + var difference = Money.CHF(200) - Money.JPY(500); + var quotient = Money.CHF(500) / 2; // this also shows a few quirks of the Expression-Tree (subtraction and division are only convenience) Assert.Equal("(((2.50CHF + ((1.5 * 7.00CHF) + 0.50CHF)) + ((2 * 7.00CHF) + 0.50CHF)) + ((2.50CHF + (((0.5 * 7.00CHF) + 0.50CHF) + (-1 * 7.00CHF))) + (7.00CHF + 0.50CHF)))", ComplexExpression().ToHumanReadable()); @@ -452,35 +452,35 @@ public void ToHumanReadableExtensionTransformsExpressionsCorrectly() [Fact] public void RoundingStrategiesMustBeInitializedWithAValidPrecision() { - Assert.Throws(() => _ = RoundingStrategy.Default(0.0m)); - Assert.Throws(() => _ = RoundingStrategy.BankersRounding(0.0m)); - Assert.Throws(() => _ = RoundingStrategy.RoundWithAwayFromZero(0.0m)); + Assert.Throws(() => _ = RoundingStrategy.Default(0.0m)); + Assert.Throws(() => _ = RoundingStrategy.BankersRounding(0.0m)); + Assert.Throws(() => _ = RoundingStrategy.RoundWithAwayFromZero(0.0m)); } [Fact] public void WeCanCalculateADimensionlessFactorByDividingAMoneyByAnother() { - Assert.Equal(2.5m, Money.CHF(5) / Money.CHF(2)); - Assert.Equal(0.75m, Money.USD(3).Divide(Money.USD(4))); + Assert.Equal(2.5m, Money.CHF(5) / Money.CHF(2)); + Assert.Equal(0.75m, Money.USD(3).Divide(Money.USD(4))); } [Fact] public void DividingTwoMoneysOnlyWorksIfTheyAreOfTheSameCurrency() { - Assert.ThrowsAny(() => Money.CHF(5) / Money.USD(2)); + Assert.ThrowsAny(() => Money.CHF(5) / Money.USD(2)); } [Fact] public void DividingTwoMoneysWithDifferentCurrenciesNeedAnEvaluationContext() { - Assert.Equal(0.8m, Money.CHF(4).Divide(Money.USD(5), OneToOneContext(Currency.USD))); + Assert.Equal(0.8m, Money.CHF(4).Divide(Money.USD(5), OneToOneContext(Currency.USD))); } [Fact] public void DividingTwoMoneysOnlyWorksIfTheDivisorIsNonZero() { - Assert.Throws(() => Money.CHF(5) / Money.Zero); - Assert.Throws(() => Money.USD(3).Divide(Money.USD(0))); + Assert.Throws(() => Money.CHF(5) / Money.Zero); + Assert.Throws(() => Money.USD(3).Divide(Money.USD(0))); } private static List Distributed(SwissMoney someMoney, int numberOfParts) @@ -502,18 +502,18 @@ private static bool TheNumberOfPartsIsCorrect(int numberOfParts, ICollection dis private static bool TheSumOfThePartsIsEqualToTheTotal(decimal validAmount, IEnumerable distributed) => distributed.Sum() == validAmount; - private static IMoneyExpression ComplexExpression() + private static IMoneyExpression ComplexExpression() { - var v1 = new Money(0.50m, SwissRounding); - var v2 = new Money(7m, SwissRounding); - var v3 = new Money(2.50m, SwissRounding); + var v1 = new Money(0.50m, SwissRounding); + var v2 = new Money(7m, SwissRounding); + var v3 = new Money(2.50m, SwissRounding); return v3.Add(v2.Multiply(1.5m).Add(v1)).Add(v2.Multiply(2).Add(v1)) .Add(v3.Add(v2.Divide(2).Add(v1).Subtract(v2)).Add(v2.Add(v1))); } - private static MoneyEvaluationContext OneToOneContext(Currency targetCurrency) - => MoneyEvaluationContext + private static MoneyEvaluationContext OneToOneContext(Currency targetCurrency) + => MoneyEvaluationContext .Builder .Default .WithTargetCurrency(targetCurrency) diff --git a/Funcky.Money.Test/SwissMoney.cs b/Funcky.Money.Test/SwissMoney.cs index 26ab1f5..c1abb96 100644 --- a/Funcky.Money.Test/SwissMoney.cs +++ b/Funcky.Money.Test/SwissMoney.cs @@ -1,6 +1,6 @@ namespace Funcky.Test; -public record SwissMoney(Money Get) +public record SwissMoney(Money Get) { public const decimal SmallestCoin = 0.05m; } diff --git a/Funcky.Money/Distribution/DefaultDistributionStrategy.cs b/Funcky.Money/Distribution/DefaultDistributionStrategy.cs index c962dcf..da820d4 100644 --- a/Funcky.Money/Distribution/DefaultDistributionStrategy.cs +++ b/Funcky.Money/Distribution/DefaultDistributionStrategy.cs @@ -1,85 +1,87 @@ +using System.Numerics; using Funcky.Extensions; using Funcky.Monads; namespace Funcky; -internal class DefaultDistributionStrategy : IDistributionStrategy +internal class DefaultDistributionStrategy : IDistributionStrategy + where TUnderlyingType : IFloatingPoint { - private readonly Option _context; + private readonly Option> _context; - public DefaultDistributionStrategy(Option context) + public DefaultDistributionStrategy(Option> context) { _context = context; } - public Money Distribute(MoneyDistributionPart part, Money total) + public Money Distribute(MoneyDistributionPart part, Money total) => IsDistributable(part, total) ? total with { Amount = SliceAmount(part, total), Currency = total.Currency } : throw new ImpossibleDistributionException($"It is impossible to distribute {ToDistribute(part, total)} in sizes of {Precision(part.Distribution, total)} with the current Rounding strategy: {RoundingStrategy(total)}."); - private bool IsDistributable(MoneyDistributionPart part, Money money) + private bool IsDistributable(MoneyDistributionPart part, Money money) => RoundingStrategy(money).IsSameAfterRounding(Precision(part.Distribution, money)) && RoundingStrategy(money).IsSameAfterRounding(ToDistribute(part, money)); - private decimal SliceAmount(MoneyDistributionPart part, Money money) + private TUnderlyingType SliceAmount(MoneyDistributionPart part, Money money) => Slice(part.Distribution, part.Index, money) + DistributeRest(part, money); - private decimal DistributeRest(MoneyDistributionPart part, Money money) + private TUnderlyingType DistributeRest(MoneyDistributionPart part, Money money) => part.Index switch { _ when AtLeastOneDistributionUnitLeft(part, money) => SignedPrecision(part.Distribution, money), _ when BetweenZeroToOneDistributionUnitLeft(part, money) => ToDistribute(part, money) - AlreadyDistributed(part, money), - _ => 0.0m, + _ => TUnderlyingType.Zero, }; - private bool AtLeastOneDistributionUnitLeft(MoneyDistributionPart part, Money money) - => Precision(part.Distribution, money) * (part.Index + 1) < Math.Abs(ToDistribute(part, money)); + private bool AtLeastOneDistributionUnitLeft(MoneyDistributionPart part, Money money) + => Precision(part.Distribution, money) * TUnderlyingType.CreateChecked(part.Index + 1) < TUnderlyingType.Abs(ToDistribute(part, money)); - private bool BetweenZeroToOneDistributionUnitLeft(MoneyDistributionPart part, Money money) - => Precision(part.Distribution, money) * part.Index < Math.Abs(ToDistribute(part, money)); + private bool BetweenZeroToOneDistributionUnitLeft(MoneyDistributionPart part, Money money) + => Precision(part.Distribution, money) * TUnderlyingType.CreateChecked(part.Index) < TUnderlyingType.Abs(ToDistribute(part, money)); - private decimal AlreadyDistributed(MoneyDistributionPart part, Money money) - => SignedPrecision(part.Distribution, money) * part.Index; + private TUnderlyingType AlreadyDistributed(MoneyDistributionPart part, Money money) + => SignedPrecision(part.Distribution, money) * TUnderlyingType.CreateChecked(part.Index); - private IRoundingStrategy RoundingStrategy(Money money) + private IRoundingStrategy RoundingStrategy(Money money) => _context.Match( some: c => c.RoundingStrategy, none: money.RoundingStrategy); - private decimal ToDistribute(MoneyDistributionPart part, Money money) + private TUnderlyingType ToDistribute(MoneyDistributionPart part, Money money) => money.Amount - DistributedTotal(part, money); - private decimal DistributedTotal(MoneyDistributionPart part, Money money) + private TUnderlyingType DistributedTotal(MoneyDistributionPart part, Money money) => part .Distribution .Factors .WithIndex() - .Sum(f => Slice(part.Distribution, f.Index, money)); + .Aggregate(TUnderlyingType.Zero, (sum, value) => sum + Slice(part.Distribution, value.Index, money)); - private decimal Slice(MoneyDistribution distribution, int index, Money money) + private TUnderlyingType Slice(MoneyDistribution distribution, int index, Money money) => Truncate(ExactSlice(distribution, index, money), Precision(distribution, money)); - private static decimal ExactSlice(MoneyDistribution distribution, int index, Money money) - => money.Amount / DistributionTotal(distribution) * distribution.Factors[index]; + private static TUnderlyingType ExactSlice(MoneyDistribution distribution, int index, Money money) + => money.Amount / TUnderlyingType.CreateChecked(DistributionTotal(distribution)) * TUnderlyingType.CreateChecked(distribution.Factors[index]); - private decimal SignedPrecision(MoneyDistribution distribution, Money money) - => Precision(distribution, money).CopySign(money.Amount); + private TUnderlyingType SignedPrecision(MoneyDistribution distribution, Money money) + => TUnderlyingType.CopySign(Precision(distribution, money), money.Amount); // Order of evaluation: Distribution > Context Distribution > Context Currency > Money Currency - private decimal Precision(MoneyDistribution distribution, Money money) + private TUnderlyingType Precision(MoneyDistribution distribution, Money money) => distribution .Precision .OrElse(_context.AndThen(c => c.DistributionUnit)) - .GetOrElse(Power.OfATenth(MinorUnitDigits(money))); + .GetOrElse(Power.OfATenth(MinorUnitDigits(money))); - private int MinorUnitDigits(Money money) + private int MinorUnitDigits(Money money) => _context.Match( none: money.Currency.MinorUnitDigits, some: c => c.TargetCurrency.MinorUnitDigits); - private static decimal Truncate(decimal amount, decimal precision) - => decimal.Truncate(amount / precision) * precision; + private static TUnderlyingType Truncate(TUnderlyingType amount, TUnderlyingType precision) + => TUnderlyingType.Truncate(amount / precision) * precision; - private static int DistributionTotal(MoneyDistribution distribution) + private static int DistributionTotal(MoneyDistribution distribution) => distribution.Factors.Sum(); } diff --git a/Funcky.Money/Distribution/IDistributionStrategy.cs b/Funcky.Money/Distribution/IDistributionStrategy.cs index ff6f925..846e66b 100644 --- a/Funcky.Money/Distribution/IDistributionStrategy.cs +++ b/Funcky.Money/Distribution/IDistributionStrategy.cs @@ -1,6 +1,9 @@ +using System.Numerics; + namespace Funcky; -internal interface IDistributionStrategy +internal interface IDistributionStrategy + where TUnderlyingType : IFloatingPoint { - Money Distribute(MoneyDistributionPart part, Money total); + Money Distribute(MoneyDistributionPart part, Money total); } diff --git a/Funcky.Money/ExpressionNodes/IMoneyExpression.cs b/Funcky.Money/ExpressionNodes/IMoneyExpression.cs index e64a692..fca7f99 100644 --- a/Funcky.Money/ExpressionNodes/IMoneyExpression.cs +++ b/Funcky.Money/ExpressionNodes/IMoneyExpression.cs @@ -1,33 +1,34 @@ +using System.Numerics; + namespace Funcky; -public interface IMoneyExpression +public interface IMoneyExpression + where TUnderlyingType : IFloatingPoint { -#if DEFAULT_INTERFACE_IMPLEMENTATION_SUPPORTED - public static IMoneyExpression operator *(IMoneyExpression multiplicand, decimal multiplier) + public static IMoneyExpression operator *(IMoneyExpression multiplicand, TUnderlyingType multiplier) => multiplicand.Multiply(multiplier); - public static IMoneyExpression operator *(decimal multiplier, IMoneyExpression multiplicand) + public static IMoneyExpression operator *(TUnderlyingType multiplier, IMoneyExpression multiplicand) => multiplicand.Multiply(multiplier); - public static IMoneyExpression operator /(IMoneyExpression dividend, decimal divisor) + public static IMoneyExpression operator /(IMoneyExpression dividend, TUnderlyingType divisor) => dividend.Divide(divisor); - public static decimal operator /(IMoneyExpression dividend, IMoneyExpression divisor) + public static TUnderlyingType operator /(IMoneyExpression dividend, IMoneyExpression divisor) => dividend.Divide(divisor); - public static IMoneyExpression operator +(IMoneyExpression augend, IMoneyExpression addend) + public static IMoneyExpression operator +(IMoneyExpression augend, IMoneyExpression addend) => augend.Add(addend); - public static IMoneyExpression operator +(IMoneyExpression moneyExpression) + public static IMoneyExpression operator +(IMoneyExpression moneyExpression) => moneyExpression; - public static IMoneyExpression operator -(IMoneyExpression minuend, IMoneyExpression subtrahend) + public static IMoneyExpression operator -(IMoneyExpression minuend, IMoneyExpression subtrahend) => minuend.Subtract(subtrahend); - public static IMoneyExpression operator -(IMoneyExpression moneyExpression) - => moneyExpression.Multiply(-1); -#endif + public static IMoneyExpression operator -(IMoneyExpression moneyExpression) + => moneyExpression.Multiply(TUnderlyingType.NegativeOne); - internal TState Accept(IMoneyExpressionVisitor visitor) + internal TState Accept(IMoneyExpressionVisitor visitor) where TState : notnull; } diff --git a/Funcky.Money/ExpressionNodes/MoneyDistribution.cs b/Funcky.Money/ExpressionNodes/MoneyDistribution.cs index 50acc9f..6b686c8 100644 --- a/Funcky.Money/ExpressionNodes/MoneyDistribution.cs +++ b/Funcky.Money/ExpressionNodes/MoneyDistribution.cs @@ -1,12 +1,14 @@ using System.Collections; +using System.Numerics; using Funcky.Extensions; using Funcky.Monads; namespace Funcky; -internal sealed class MoneyDistribution : IEnumerable +internal sealed class MoneyDistribution : IEnumerable> + where TUnderlyingType : IFloatingPoint { - public MoneyDistribution(IMoneyExpression moneyExpression, IEnumerable factors, Option precision) + public MoneyDistribution(IMoneyExpression moneyExpression, IEnumerable factors, Option precision) { Expression = moneyExpression; Factors = factors.ToList(); @@ -18,16 +20,16 @@ public MoneyDistribution(IMoneyExpression moneyExpression, IEnumerable fact } } - public IMoneyExpression Expression { get; } + public IMoneyExpression Expression { get; } public List Factors { get; } - public Option Precision { get; } + public Option Precision { get; } - public IEnumerator GetEnumerator() + public IEnumerator> GetEnumerator() => Factors .WithIndex() - .Select(f => (IMoneyExpression)new MoneyDistributionPart(this, f.Index)) + .Select(f => (IMoneyExpression)new MoneyDistributionPart(this, f.Index)) .GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() diff --git a/Funcky.Money/ExpressionNodes/MoneyDistributionPart.cs b/Funcky.Money/ExpressionNodes/MoneyDistributionPart.cs index c8396b7..fda318a 100644 --- a/Funcky.Money/ExpressionNodes/MoneyDistributionPart.cs +++ b/Funcky.Money/ExpressionNodes/MoneyDistributionPart.cs @@ -1,17 +1,20 @@ +using System.Numerics; + namespace Funcky; -internal sealed class MoneyDistributionPart : IMoneyExpression +internal sealed class MoneyDistributionPart : IMoneyExpression + where TUnderlyingType : IFloatingPoint { - public MoneyDistributionPart(MoneyDistribution distribution, int index) + public MoneyDistributionPart(MoneyDistribution distribution, int index) { Distribution = distribution; Index = index; } - public MoneyDistribution Distribution { get; } + public MoneyDistribution Distribution { get; } public int Index { get; } - TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) + TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) => visitor.Visit(this); } diff --git a/Funcky.Money/ExpressionNodes/MoneyProduct.cs b/Funcky.Money/ExpressionNodes/MoneyProduct.cs index f4bccd8..65c714a 100644 --- a/Funcky.Money/ExpressionNodes/MoneyProduct.cs +++ b/Funcky.Money/ExpressionNodes/MoneyProduct.cs @@ -1,17 +1,20 @@ +using System.Numerics; + namespace Funcky; -internal sealed record MoneyProduct : IMoneyExpression +internal sealed record MoneyProduct : IMoneyExpression + where TUnderlyingType : IFloatingPoint { - public MoneyProduct(IMoneyExpression moneyExpression, decimal factor) + public MoneyProduct(IMoneyExpression moneyExpression, TUnderlyingType factor) { Expression = moneyExpression; Factor = factor; } - public IMoneyExpression Expression { get; } + public IMoneyExpression Expression { get; } - public decimal Factor { get; } + public TUnderlyingType Factor { get; } - TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) + TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) => visitor.Visit(this); } diff --git a/Funcky.Money/ExpressionNodes/MoneySum.cs b/Funcky.Money/ExpressionNodes/MoneySum.cs index 87ee422..b845b77 100644 --- a/Funcky.Money/ExpressionNodes/MoneySum.cs +++ b/Funcky.Money/ExpressionNodes/MoneySum.cs @@ -1,17 +1,20 @@ +using System.Numerics; + namespace Funcky; -internal sealed record MoneySum : IMoneyExpression +internal sealed record MoneySum : IMoneyExpression + where TUnderlyingType : IFloatingPoint { - public MoneySum(IMoneyExpression leftMoneyExpression, IMoneyExpression rightMoneyExpression) + public MoneySum(IMoneyExpression leftMoneyExpression, IMoneyExpression rightMoneyExpression) { Left = leftMoneyExpression; Right = rightMoneyExpression; } - public IMoneyExpression Left { get; } + public IMoneyExpression Left { get; } - public IMoneyExpression Right { get; } + public IMoneyExpression Right { get; } - TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) + TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) => visitor.Visit(this); } diff --git a/Funcky.Money/Extensions/MoneyDistributionExtension.cs b/Funcky.Money/Extensions/MoneyDistributionExtension.cs index 44c88d2..d5cdfd4 100644 --- a/Funcky.Money/Extensions/MoneyDistributionExtension.cs +++ b/Funcky.Money/Extensions/MoneyDistributionExtension.cs @@ -1,12 +1,15 @@ +using System.Numerics; using Funcky.Monads; namespace Funcky; public static class MoneyDistributionExtension { - public static IEnumerable Distribute(this IMoneyExpression moneyExpression, int numberOfSlices, Option precision = default) + public static IEnumerable> Distribute(this IMoneyExpression moneyExpression, int numberOfSlices, Option precision = default) + where TUnderlyingType : IFloatingPoint => moneyExpression.Distribute(Enumerable.Repeat(element: 1, count: numberOfSlices), precision); - public static IEnumerable Distribute(this IMoneyExpression moneyExpression, IEnumerable factors, Option precision = default) - => new MoneyDistribution(moneyExpression, factors, precision); + public static IEnumerable> Distribute(this IMoneyExpression moneyExpression, IEnumerable factors, Option precision = default) + where TUnderlyingType : IFloatingPoint + => new MoneyDistribution(moneyExpression, factors, precision); } diff --git a/Funcky.Money/Extensions/MoneyDivisionExtension.cs b/Funcky.Money/Extensions/MoneyDivisionExtension.cs index add16a7..fdd2d2e 100644 --- a/Funcky.Money/Extensions/MoneyDivisionExtension.cs +++ b/Funcky.Money/Extensions/MoneyDivisionExtension.cs @@ -1,18 +1,22 @@ +using System.Numerics; using Funcky.Monads; namespace Funcky; public static class MoneyDivisionExtension { - public static IMoneyExpression Divide(this IMoneyExpression dividend, decimal divisor) - => new MoneyProduct(dividend, 1.0m / divisor); + public static IMoneyExpression Divide(this IMoneyExpression dividend, TUnderlyingType divisor) + where TUnderlyingType : IFloatingPoint + => new MoneyProduct(dividend, TUnderlyingType.One / divisor); - public static decimal Divide(this IMoneyExpression dividend, IMoneyExpression divisor, Option context = default) + public static TUnderlyingType Divide(this IMoneyExpression dividend, IMoneyExpression divisor, Option> context = default) + where TUnderlyingType : IFloatingPoint => Divide( dividend.Evaluate(context), divisor.Evaluate(context)); - private static decimal Divide(Money dividend, Money divisor) + private static TUnderlyingType Divide(Money dividend, Money divisor) + where TUnderlyingType : IFloatingPoint => dividend.Currency == divisor.Currency ? dividend.Amount / divisor.Amount : throw new MissingExchangeRateException(); diff --git a/Funcky.Money/Extensions/MoneyEvaluationExtension.cs b/Funcky.Money/Extensions/MoneyEvaluationExtension.cs index 56401ea..a99a3a7 100644 --- a/Funcky.Money/Extensions/MoneyEvaluationExtension.cs +++ b/Funcky.Money/Extensions/MoneyEvaluationExtension.cs @@ -1,32 +1,39 @@ +using System.Numerics; using Funcky.Monads; namespace Funcky; public static class MoneyEvaluationExtension { - public static Money Evaluate(this IMoneyExpression moneyExpression, Option context = default) + public static Money Evaluate(this IMoneyExpression moneyExpression, Option> context = default) + where TUnderlyingType : IFloatingPoint => Evaluate(moneyExpression, Round(context), CalculateTotal(context)); - private static Money Evaluate(IMoneyExpression moneyExpression, Func round, Func total) + private static Money Evaluate(IMoneyExpression moneyExpression, Func, Money> round, Func, Money> total) + where TUnderlyingType : IFloatingPoint => round(total(moneyExpression)); - private static Func CalculateTotal(Option context) + private static Func, Money> CalculateTotal(Option> context) + where TUnderlyingType : IFloatingPoint => moneyExpression => moneyExpression .Accept(CreateVisitor(context)) .CalculateTotal(context); - private static Func Round(Option context) - => money - => money with { Amount = FindRoundingStrategy(money, context).Round(money.Amount) }; + private static Func, Money> Round(Option> context) + where TUnderlyingType : IFloatingPoint => money + => money with { Amount = FindRoundingStrategy(money, context).Round(money.Amount) }; - private static EvaluationVisitor CreateVisitor(Option context) + private static EvaluationVisitor CreateVisitor(Option> context) + where TUnderlyingType : IFloatingPoint => new(CreateDistributionStrategy(context), context); - private static IDistributionStrategy CreateDistributionStrategy(Option context) - => new DefaultDistributionStrategy(context); + private static IDistributionStrategy CreateDistributionStrategy(Option> context) + where TUnderlyingType : IFloatingPoint + => new DefaultDistributionStrategy(context); - private static IRoundingStrategy FindRoundingStrategy(Money money, Option context) + private static IRoundingStrategy FindRoundingStrategy(Money money, Option> context) + where TUnderlyingType : IFloatingPoint => context .AndThen(c => c.RoundingStrategy) .GetOrElse(money.RoundingStrategy); diff --git a/Funcky.Money/Extensions/MoneyMultiplicationExtension.cs b/Funcky.Money/Extensions/MoneyMultiplicationExtension.cs index 05d1a53..b8f9f06 100644 --- a/Funcky.Money/Extensions/MoneyMultiplicationExtension.cs +++ b/Funcky.Money/Extensions/MoneyMultiplicationExtension.cs @@ -1,7 +1,10 @@ +using System.Numerics; + namespace Funcky; public static class MoneyMultiplicationExtension { - public static IMoneyExpression Multiply(this IMoneyExpression multiplicand, decimal multiplier) - => new MoneyProduct(multiplicand, multiplier); + public static IMoneyExpression Multiply(this IMoneyExpression multiplicand, TUnderlyingType multiplier) + where TUnderlyingType : IFloatingPoint + => new MoneyProduct(multiplicand, multiplier); } diff --git a/Funcky.Money/Extensions/MoneySubtractionExtension.cs b/Funcky.Money/Extensions/MoneySubtractionExtension.cs index e242cda..428b3b5 100644 --- a/Funcky.Money/Extensions/MoneySubtractionExtension.cs +++ b/Funcky.Money/Extensions/MoneySubtractionExtension.cs @@ -1,7 +1,10 @@ +using System.Numerics; + namespace Funcky; public static class MoneySubtractionExtension { - public static IMoneyExpression Subtract(this IMoneyExpression minuend, IMoneyExpression subtrahend) - => new MoneySum(minuend, new MoneyProduct(subtrahend, -1)); + public static IMoneyExpression Subtract(this IMoneyExpression minuend, IMoneyExpression subtrahend) + where TUnderlyingType : IFloatingPoint + => new MoneySum(minuend, new MoneyProduct(subtrahend, TUnderlyingType.NegativeOne)); } diff --git a/Funcky.Money/Extensions/MoneySummationExtension.cs b/Funcky.Money/Extensions/MoneySummationExtension.cs index b08aaa7..5aff37c 100644 --- a/Funcky.Money/Extensions/MoneySummationExtension.cs +++ b/Funcky.Money/Extensions/MoneySummationExtension.cs @@ -1,7 +1,10 @@ +using System.Numerics; + namespace Funcky; public static class MoneySummationExtension { - public static IMoneyExpression Add(this IMoneyExpression augend, IMoneyExpression addend) - => new MoneySum(augend, addend); + public static IMoneyExpression Add(this IMoneyExpression augend, IMoneyExpression addend) + where TUnderlyingType : IFloatingPoint + => new MoneySum(augend, addend); } diff --git a/Funcky.Money/Extensions/SignExtension.cs b/Funcky.Money/Extensions/SignExtension.cs deleted file mode 100644 index d486cf6..0000000 --- a/Funcky.Money/Extensions/SignExtension.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Funcky.Extensions; - -internal static class SignExtension -{ - public static decimal CopySign(this decimal positiveNumber, decimal signSource) - => signSource switch - { - < 0 => -positiveNumber, - >= 0 => positiveNumber, - }; -} diff --git a/Funcky.Money/Extensions/ToHumanReadableExtension.cs b/Funcky.Money/Extensions/ToHumanReadableExtension.cs index 091422d..060d360 100644 --- a/Funcky.Money/Extensions/ToHumanReadableExtension.cs +++ b/Funcky.Money/Extensions/ToHumanReadableExtension.cs @@ -1,7 +1,10 @@ +using System.Numerics; + namespace Funcky; public static class ToHumanReadableExtension { - public static string ToHumanReadable(this IMoneyExpression moneyExpression) - => moneyExpression.Accept(ToHumanReadableVisitor.Instance); + public static string ToHumanReadable(this IMoneyExpression moneyExpression) + where TUnderlyingType : IFloatingPoint + => moneyExpression.Accept(ToHumanReadableVisitor.Instance); } diff --git a/Funcky.Money/Money.cs b/Funcky.Money/Money.cs index 71e537d..d97c711 100644 --- a/Funcky.Money/Money.cs +++ b/Funcky.Money/Money.cs @@ -1,23 +1,24 @@ using System.Diagnostics; using System.Globalization; -using Funcky.Extensions; +using System.Numerics; using Funcky.Monads; namespace Funcky; [DebuggerDisplay("{Amount} {Currency.AlphabeticCurrencyCode,nq}")] -public sealed partial record Money : IMoneyExpression +public sealed partial record Money : IMoneyExpression +where TUnderlyingType : IFloatingPoint { - public static readonly Money Zero = new(0m); + public static readonly Money Zero = new(TUnderlyingType.Zero); - public Money(decimal amount, Option currency = default) + public Money(TUnderlyingType amount, Option currency = default) { Amount = amount; Currency = SelectCurrency(currency); - RoundingStrategy = Funcky.RoundingStrategy.Default(Currency); + RoundingStrategy = RoundingStrategy.Default(Currency); } - public Money(decimal amount, MoneyEvaluationContext context) + public Money(TUnderlyingType amount, MoneyEvaluationContext context) { Amount = amount; Currency = context.TargetCurrency; @@ -25,73 +26,77 @@ public Money(decimal amount, MoneyEvaluationContext context) } public Money(int amount, Option currency = default) - : this((decimal)amount, currency) + : this(TUnderlyingType.CreateChecked(amount), currency) { } - public decimal Amount { get; init; } + public TUnderlyingType Amount { get; init; } public Currency Currency { get; init; } - public IRoundingStrategy RoundingStrategy { get; } + public IRoundingStrategy RoundingStrategy { get; } public bool IsZero - => Amount == 0m; + => TUnderlyingType.IsZero(Amount); // These operators supports the operators on IMoneyExpression, because Money + Money or Money * factor does not work otherwise without a cast. - public static IMoneyExpression operator +(Money augend, IMoneyExpression addend) + public static IMoneyExpression operator +(Money augend, IMoneyExpression addend) => augend.Add(addend); - public static IMoneyExpression operator +(Money money) + public static IMoneyExpression operator +(Money money) => money; - public static IMoneyExpression operator -(Money minuend, IMoneyExpression subtrahend) + public static IMoneyExpression operator -(Money minuend, IMoneyExpression subtrahend) => minuend.Subtract(subtrahend); - public static Money operator -(Money money) + public static Money operator -(Money money) => money with { Amount = -money.Amount }; - public static IMoneyExpression operator *(Money multiplicand, decimal multiplier) + public static IMoneyExpression operator *(Money multiplicand, TUnderlyingType multiplier) => multiplicand.Multiply(multiplier); - public static IMoneyExpression operator *(decimal multiplier, Money multiplicand) + public static IMoneyExpression operator *(TUnderlyingType multiplier, Money multiplicand) => multiplicand.Multiply(multiplier); - public static IMoneyExpression operator /(Money dividend, decimal divisor) + public static IMoneyExpression operator /(Money dividend, TUnderlyingType divisor) => dividend.Divide(divisor); - public static decimal operator /(Money dividend, IMoneyExpression divisor) + public static TUnderlyingType operator /(Money dividend, IMoneyExpression divisor) => dividend.Divide(divisor); private static Currency SelectCurrency(Option currency) => currency.GetOrElse(CurrencyCulture.CurrentCurrency); - public static Option ParseOrNone(string money, Option currency = default) + public static Option> ParseOrNone(string money, Option currency = default) => CurrencyCulture .FormatProviderFromCurrency(SelectCurrency(currency)) .Match( none: ParseManually(money), some: ParseWithFormatProvider(money)) - .AndThen(amount => new Money(amount, SelectCurrency(currency))); + .AndThen(amount => new Money(amount, SelectCurrency(currency))); - private static Func> ParseManually(string money) + private static Func> ParseManually(string money) => () - => RemoveIsoCurrency(money).ParseDecimalOrNone(); + => TUnderlyingType.TryParse(RemoveIsoCurrency(money), CultureInfo.CurrentCulture, out var result) + ? result + : Option.None; private static string RemoveIsoCurrency(string money) => money.Split(' ') is [var first, { Length: 3 }] ? first : money; - private static Func> ParseWithFormatProvider(string money) + private static Func> ParseWithFormatProvider(string money) => formatProvider - => money.ParseDecimalOrNone(NumberStyles.Currency, formatProvider); + => TUnderlyingType.TryParse(RemoveIsoCurrency(money), NumberStyles.Currency, formatProvider, out var result) + ? result + : Option.None; public override string ToString() => CurrencyCulture.FormatProviderFromCurrency(Currency).Match( none: () => string.Format($"{{0:N{Currency.MinorUnitDigits}}} {{1}}", Amount, Currency.AlphabeticCurrencyCode), some: formatProvider => string.Format(formatProvider, $"{{0:C{Currency.MinorUnitDigits}}}", Amount)); - TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) + TState IMoneyExpression.Accept(IMoneyExpressionVisitor visitor) => visitor.Visit(this); } diff --git a/Funcky.Money/MoneyEvaluationContext.cs b/Funcky.Money/MoneyEvaluationContext.cs index 3665ce5..0cb126c 100644 --- a/Funcky.Money/MoneyEvaluationContext.cs +++ b/Funcky.Money/MoneyEvaluationContext.cs @@ -1,15 +1,17 @@ +using System.Numerics; using Funcky.Monads; using static Funcky.Functional; namespace Funcky; -public sealed class MoneyEvaluationContext +public sealed class MoneyEvaluationContext + where TUnderlyingType : IFloatingPoint { - private MoneyEvaluationContext(Currency targetCurrency, Option distributionUnit, Option> roundingStrategy, IBank bank) + private MoneyEvaluationContext(Currency targetCurrency, Option distributionUnit, Option> roundingStrategy, IBank bank) { TargetCurrency = targetCurrency; DistributionUnit = distributionUnit; - RoundingStrategy = roundingStrategy.GetOrElse(Funcky.RoundingStrategy.Default(distributionUnit.GetOrElse(Power.OfATenth(TargetCurrency.MinorUnitDigits)))); + RoundingStrategy = roundingStrategy.GetOrElse(RoundingStrategy.Default(distributionUnit.GetOrElse(Power.OfATenth(TargetCurrency.MinorUnitDigits)))); Bank = bank; } @@ -22,36 +24,36 @@ private MoneyEvaluationContext(Currency targetCurrency, Option distribu /// This is the smallest money unit you want to have distributed. /// Usually you want this to be the smallest coin of the given currency. /// - public Option DistributionUnit { get; } + public Option DistributionUnit { get; } /// /// Defines how we round amounts in the evaluation. /// - public IRoundingStrategy RoundingStrategy { get; } + public IRoundingStrategy RoundingStrategy { get; } /// /// Source for exchange rates. /// - public IBank Bank { get; } + public IBank Bank { get; } public sealed class Builder { public static readonly Builder Default = new(); private readonly Option _targetCurrency; - private readonly Option _distributionUnit; - private readonly Option> _roundingStrategy; - private readonly IBank _bank; + private readonly Option _distributionUnit; + private readonly Option> _roundingStrategy; + private readonly IBank _bank; private Builder() { _targetCurrency = default; _roundingStrategy = default; _distributionUnit = default; - _bank = DefaultBank.Empty; + _bank = DefaultBank.Empty; } - private Builder(Option currency, Option distributionUnit, Option> roundingStrategy, IBank bank) + private Builder(Option currency, Option distributionUnit, Option> roundingStrategy, IBank bank) { _targetCurrency = currency; _distributionUnit = distributionUnit; @@ -59,7 +61,7 @@ private Builder(Option currency, Option distributionUnit, Opt _bank = bank; } - public MoneyEvaluationContext Build() + public MoneyEvaluationContext Build() => CompatibleRounding().Match(none: false, some: Not(Identity)) ? throw new IncompatibleRoundingException($"The rounding strategy {_roundingStrategy} is incompatible with the smallest possible distribution unit {_distributionUnit}.") : CreateContext(); @@ -67,21 +69,21 @@ public MoneyEvaluationContext Build() public Builder WithTargetCurrency(Currency currency) => With(targetCurrency: currency); - public Builder WithRounding(IRoundingStrategy roundingStrategy) + public Builder WithRounding(IRoundingStrategy roundingStrategy) => With(roundingStrategy: Option.Some(roundingStrategy)); - public Builder WithExchangeRate(Currency currency, decimal sellRate) - => _bank is DefaultBank bank + public Builder WithExchangeRate(Currency currency, TUnderlyingType sellRate) + => _bank is DefaultBank bank ? With(bank: bank.AddExchangeRate(currency, GetTargetCurrencyOrException(), sellRate)) : throw new InvalidMoneyEvaluationContextBuilderException("You can either use WithExchangeRate or WithBank, but not both."); - public Builder WithBank(IBank bank) + public Builder WithBank(IBank bank) => With(bank: Option.Some(bank)); - public Builder WithSmallestDistributionUnit(decimal distributionUnit) + public Builder WithSmallestDistributionUnit(TUnderlyingType distributionUnit) => With(distributionUnit: distributionUnit); - private Builder With(Option targetCurrency = default, Option distributionUnit = default, Option> roundingStrategy = default, Option> bank = default) + private Builder With(Option targetCurrency = default, Option distributionUnit = default, Option> roundingStrategy = default, Option> bank = default) => new( targetCurrency.OrElse(_targetCurrency), distributionUnit.OrElse(_distributionUnit), @@ -97,7 +99,7 @@ private Option CompatibleRounding() from unit in _distributionUnit select rounding.IsSameAfterRounding(unit); - private MoneyEvaluationContext CreateContext() + private MoneyEvaluationContext CreateContext() => new( _targetCurrency.GetOrElse(() => throw new InvalidMoneyEvaluationContextBuilderException("Money evaluation context has no target currency set.")), _distributionUnit, diff --git a/Funcky.Money/Rounding/BankersRounding.cs b/Funcky.Money/Rounding/BankersRounding.cs index 17e4791..3e2869c 100644 --- a/Funcky.Money/Rounding/BankersRounding.cs +++ b/Funcky.Money/Rounding/BankersRounding.cs @@ -5,7 +5,7 @@ namespace Funcky; [DebuggerDisplay("{ToString()}")] internal sealed record BankersRounding : IRoundingStrategy - where TUnderlyingType : INumberBase, IFloatingPoint + where TUnderlyingType : IFloatingPoint { private readonly TUnderlyingType _precision; diff --git a/Funcky.Money/Rounding/RoundWithAwayFromZero.cs b/Funcky.Money/Rounding/RoundWithAwayFromZero.cs index 3ad5e41..b747f20 100644 --- a/Funcky.Money/Rounding/RoundWithAwayFromZero.cs +++ b/Funcky.Money/Rounding/RoundWithAwayFromZero.cs @@ -5,7 +5,7 @@ namespace Funcky; [DebuggerDisplay("{ToString()}")] internal sealed record RoundWithAwayFromZero : IRoundingStrategy - where TUnderlyingType : INumberBase, IFloatingPoint + where TUnderlyingType : IFloatingPoint { public RoundWithAwayFromZero(TUnderlyingType precision) { diff --git a/Funcky.Money/Rounding/RoundingStrategy.cs b/Funcky.Money/Rounding/RoundingStrategy.cs index 2663ae5..a946d4d 100644 --- a/Funcky.Money/Rounding/RoundingStrategy.cs +++ b/Funcky.Money/Rounding/RoundingStrategy.cs @@ -1,25 +1,28 @@ +using System.Numerics; + namespace Funcky; -public static class RoundingStrategy +public static class RoundingStrategy + where TUnderlyingType : IFloatingPoint { - public static IRoundingStrategy NoRounding() - => new NoRounding(); + public static IRoundingStrategy NoRounding() + => new NoRounding(); - public static IRoundingStrategy BankersRounding(decimal precision) - => new BankersRounding(precision); + public static IRoundingStrategy BankersRounding(TUnderlyingType precision) + => new BankersRounding(precision); - public static IRoundingStrategy BankersRounding(Currency currency) - => new BankersRounding(currency); + public static IRoundingStrategy BankersRounding(Currency currency) + => new BankersRounding(currency); - public static IRoundingStrategy RoundWithAwayFromZero(decimal precision) - => new RoundWithAwayFromZero(precision); + public static IRoundingStrategy RoundWithAwayFromZero(TUnderlyingType precision) + => new RoundWithAwayFromZero(precision); - public static IRoundingStrategy RoundWithAwayFromZero(Currency currency) - => new RoundWithAwayFromZero(currency); + public static IRoundingStrategy RoundWithAwayFromZero(Currency currency) + => new RoundWithAwayFromZero(currency); - internal static IRoundingStrategy Default(decimal precision) + internal static IRoundingStrategy Default(TUnderlyingType precision) => BankersRounding(precision); - internal static IRoundingStrategy Default(Currency currency) + internal static IRoundingStrategy Default(Currency currency) => BankersRounding(currency); } diff --git a/Funcky.Money/Rounding/RoundingStrategyExtension.cs b/Funcky.Money/Rounding/RoundingStrategyExtension.cs index b66f520..73b554d 100644 --- a/Funcky.Money/Rounding/RoundingStrategyExtension.cs +++ b/Funcky.Money/Rounding/RoundingStrategyExtension.cs @@ -1,7 +1,10 @@ +using System.Numerics; + namespace Funcky; internal static class RoundingStrategyExtension { - public static bool IsSameAfterRounding(this IRoundingStrategy roundingStrategy, decimal amount) + public static bool IsSameAfterRounding(this IRoundingStrategy roundingStrategy, TUnderlyingType amount) + where TUnderlyingType : INumberBase => roundingStrategy.Round(amount) == amount; } diff --git a/Funcky.Money/Visitors/EvaluationVisitor.cs b/Funcky.Money/Visitors/EvaluationVisitor.cs index 4abe83f..3235f22 100644 --- a/Funcky.Money/Visitors/EvaluationVisitor.cs +++ b/Funcky.Money/Visitors/EvaluationVisitor.cs @@ -1,37 +1,39 @@ +using System.Numerics; using Funcky.Monads; namespace Funcky; -internal sealed class EvaluationVisitor : IMoneyExpressionVisitor +internal sealed class EvaluationVisitor : IMoneyExpressionVisitor> + where TUnderlyingType : IFloatingPoint { - private readonly IDistributionStrategy _distributionStrategy; - private readonly Option _context; + private readonly IDistributionStrategy _distributionStrategy; + private readonly Option> _context; - public EvaluationVisitor(IDistributionStrategy distributionStrategy, Option context) + public EvaluationVisitor(IDistributionStrategy distributionStrategy, Option> context) { _distributionStrategy = distributionStrategy; _context = context; } - public MoneyBag Visit(Money money) + public MoneyBag Visit(Money money) => new(money); - public MoneyBag Visit(MoneySum sum) + public MoneyBag Visit(MoneySum sum) => Accept(sum.Left) .Merge(Accept(sum.Right)); - public MoneyBag Visit(MoneyProduct product) + public MoneyBag Visit(MoneyProduct product) => Accept(product.Expression) .Multiply(product.Factor); - public MoneyBag Visit(MoneyDistributionPart part) + public MoneyBag Visit(MoneyDistributionPart part) => new(_distributionStrategy.Distribute(part, CalculateTotal(part))); - private MoneyBag Accept(IMoneyExpression expression) + private MoneyBag Accept(IMoneyExpression expression) => expression .Accept(this); - private Money CalculateTotal(MoneyDistributionPart part) + private Money CalculateTotal(MoneyDistributionPart part) => Accept(part.Distribution.Expression) .CalculateTotal(_context); } diff --git a/Funcky.Money/Visitors/IMoneyExpressionVisitor.cs b/Funcky.Money/Visitors/IMoneyExpressionVisitor.cs index 57b5de2..3321733 100644 --- a/Funcky.Money/Visitors/IMoneyExpressionVisitor.cs +++ b/Funcky.Money/Visitors/IMoneyExpressionVisitor.cs @@ -1,13 +1,16 @@ +using System.Numerics; + namespace Funcky; -internal interface IMoneyExpressionVisitor +internal interface IMoneyExpressionVisitor where TState : notnull + where TUnderlyingType : IFloatingPoint { - TState Visit(Money money); + TState Visit(Money money); - TState Visit(MoneySum sum); + TState Visit(MoneySum sum); - TState Visit(MoneyProduct product); + TState Visit(MoneyProduct product); - TState Visit(MoneyDistributionPart part); + TState Visit(MoneyDistributionPart part); } diff --git a/Funcky.Money/Visitors/MoneyBag.cs b/Funcky.Money/Visitors/MoneyBag.cs index b035f50..0ea3049 100644 --- a/Funcky.Money/Visitors/MoneyBag.cs +++ b/Funcky.Money/Visitors/MoneyBag.cs @@ -1,20 +1,22 @@ +using System.Numerics; using Funcky.Extensions; using Funcky.Monads; namespace Funcky; -internal sealed class MoneyBag +internal sealed class MoneyBag + where TUnderlyingType : IFloatingPoint { - private readonly Dictionary> _currencies = new(); - private Option> _roundingStrategy; + private readonly Dictionary>> _currencies = new(); + private Option> _roundingStrategy; private Option _emptyCurrency; - public MoneyBag(Money money) + public MoneyBag(Money money) { Add(money); } - public MoneyBag Merge(MoneyBag moneyBag) + public MoneyBag Merge(MoneyBag moneyBag) { moneyBag ._currencies @@ -24,7 +26,7 @@ public MoneyBag Merge(MoneyBag moneyBag) return this; } - public MoneyBag Multiply(decimal factor) + public MoneyBag Multiply(TUnderlyingType factor) { _currencies .ForEach(kv => _currencies[kv.Key] = MultiplyBag(factor, kv.Value)); @@ -32,12 +34,12 @@ public MoneyBag Multiply(decimal factor) return this; } - public Money CalculateTotal(Option context) + public Money CalculateTotal(Option> context) => context.Match( none: AggregateWithoutEvaluationContext, some: AggregateWithEvaluationContext); - private void Add(Money money) + private void Add(Money money) { if (money.IsZero) { @@ -53,41 +55,41 @@ private void Add(Money money) } } - private static List MultiplyBag(decimal factor, IEnumerable bag) + private static List> MultiplyBag(TUnderlyingType factor, IEnumerable> bag) => bag .Select(m => m with { Amount = m.Amount * factor }) .ToList(); - private Money AggregateWithEvaluationContext(MoneyEvaluationContext context) + private Money AggregateWithEvaluationContext(MoneyEvaluationContext context) => _currencies .Values .Select(c => c.Aggregate(MoneySum(context))) .Select(ExchangeToTargetCurrency(context)) - .Aggregate(new Money(0m, context), MoneySum); + .Aggregate(new Money(TUnderlyingType.Zero, context), MoneySum); - private Money AggregateWithoutEvaluationContext() + private Money AggregateWithoutEvaluationContext() => ExceptionTransformer.Transform( AggregateSingleMoneyBag, exception => throw new MissingEvaluationContextException("Different currencies cannot be evaluated without an evaluation context.", exception)); - private Money AggregateSingleMoneyBag() + private Money AggregateSingleMoneyBag() => _currencies .SingleOrNone() // Single or None throws an InvalidOperationException if we have more than one currency in the Bag .Match( - none: () => _emptyCurrency.Match(Money.Zero, c => Money.Zero with { Currency = c }), + none: () => _emptyCurrency.Match(Money.Zero, c => Money.Zero with { Currency = c }), some: m => CheckAndAggregateBag(m.Value)); - private Money CheckAndAggregateBag(IEnumerable bag) + private Money CheckAndAggregateBag(IEnumerable> bag) => bag .Inspect(CheckEvaluationRules) .Aggregate(MoneySum); - private void CheckEvaluationRules(Money money) + private void CheckEvaluationRules(Money money) => _roundingStrategy.Switch( none: () => _roundingStrategy = Option.Some(money.RoundingStrategy), some: r => CheckRoundingStrategy(money, r)); - private static void CheckRoundingStrategy(Money money, IRoundingStrategy roundingStrategy) + private static void CheckRoundingStrategy(Money money, IRoundingStrategy roundingStrategy) { if (!money.RoundingStrategy.Equals(roundingStrategy)) { @@ -103,14 +105,14 @@ private void CreateMoneyBagByCurrency(Currency currency) } } - private static Money MoneySum(Money currentSum, Money money) + private static Money MoneySum(Money currentSum, Money money) => currentSum with { Amount = currentSum.Amount + money.Amount }; - private static Func MoneySum(MoneyEvaluationContext context) + private static Func, Money, Money> MoneySum(MoneyEvaluationContext context) => (currentSum, money) - => new Money(currentSum.Amount + money.Amount, context); + => new Money(currentSum.Amount + money.Amount, context); - private static Func ExchangeToTargetCurrency(MoneyEvaluationContext context) + private static Func, Money> ExchangeToTargetCurrency(MoneyEvaluationContext context) => money => money.Currency == context.TargetCurrency ? money : money with { Amount = money.Amount * context.Bank.ExchangeRate(money.Currency, context.TargetCurrency), Currency = context.TargetCurrency }; diff --git a/Funcky.Money/Visitors/ToHumanReadableVisitor.cs b/Funcky.Money/Visitors/ToHumanReadableVisitor.cs index b3e1eff..a8a5e33 100644 --- a/Funcky.Money/Visitors/ToHumanReadableVisitor.cs +++ b/Funcky.Money/Visitors/ToHumanReadableVisitor.cs @@ -1,27 +1,29 @@ +using System.Numerics; using Funcky.Extensions; namespace Funcky; -internal class ToHumanReadableVisitor : IMoneyExpressionVisitor +internal class ToHumanReadableVisitor : IMoneyExpressionVisitor + where TUnderlyingType : IFloatingPoint { private const string DistributionSeparator = ", "; - private static readonly Lazy LazyInstance = new(() => new()); + private static readonly Lazy> LazyInstance = new(() => new()); - public static ToHumanReadableVisitor Instance + public static ToHumanReadableVisitor Instance => LazyInstance.Value; - public string Visit(Money money) + public string Visit(Money money) => string.Format($"{{0:N{money.Currency.MinorUnitDigits}}}{{1}}", money.Amount, money.Currency.AlphabeticCurrencyCode); - public string Visit(MoneySum sum) + public string Visit(MoneySum sum) => $"({Accept(sum.Left)} + {Accept(sum.Right)})"; - public string Visit(MoneyProduct product) + public string Visit(MoneyProduct product) => $"({product.Factor} * {Accept(product.Expression)})"; - public string Visit(MoneyDistributionPart part) + public string Visit(MoneyDistributionPart part) => $"{Accept(part.Distribution.Expression)}.Distribute({part.Distribution.Factors.JoinToString(DistributionSeparator)})[{part.Index}]"; - private string Accept(IMoneyExpression expression) + private string Accept(IMoneyExpression expression) => expression.Accept(this); }