From 5fd192ff2ffe3d2e07aac497159b1f04ec2e560d Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 6 Jan 2021 15:20:03 +0100 Subject: [PATCH 01/13] Simplify Generator --- Funcky.Money.Test/CurrencyGenerator.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Funcky.Money.Test/CurrencyGenerator.cs b/Funcky.Money.Test/CurrencyGenerator.cs index 99e79af..3b70279 100644 --- a/Funcky.Money.Test/CurrencyGenerator.cs +++ b/Funcky.Money.Test/CurrencyGenerator.cs @@ -4,11 +4,7 @@ namespace Funcky.Test { internal class CurrencyGenerator { - public static Arbitrary Generate() - { - return Arb.From( - from int id in Gen.Choose(0, Currency.AllCurrencies.Count - 1) - select Currency.AllCurrencies[id]); - } + public static Arbitrary GenerateCurrency() + => Arb.From(Gen.Elements(Currency.AllCurrencies)); } } From 98565a4300178ae0e9b811d87fc2279f72141e17 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 6 Jan 2021 16:29:41 +0100 Subject: [PATCH 02/13] Generate Money for property tests --- Funcky.Money.Test/CurrencyGenerator.cs | 10 ------- Funcky.Money.Test/MoneyArbitraries.cs | 25 ++++++++++++++++ Funcky.Money.Test/MoneyTest.cs | 41 +++++++++----------------- Funcky.Money.Test/SwissMoney.cs | 4 +++ 4 files changed, 43 insertions(+), 37 deletions(-) delete mode 100644 Funcky.Money.Test/CurrencyGenerator.cs create mode 100644 Funcky.Money.Test/MoneyArbitraries.cs create mode 100644 Funcky.Money.Test/SwissMoney.cs diff --git a/Funcky.Money.Test/CurrencyGenerator.cs b/Funcky.Money.Test/CurrencyGenerator.cs deleted file mode 100644 index 3b70279..0000000 --- a/Funcky.Money.Test/CurrencyGenerator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FsCheck; - -namespace Funcky.Test -{ - internal class CurrencyGenerator - { - public static Arbitrary GenerateCurrency() - => Arb.From(Gen.Elements(Currency.AllCurrencies)); - } -} diff --git a/Funcky.Money.Test/MoneyArbitraries.cs b/Funcky.Money.Test/MoneyArbitraries.cs new file mode 100644 index 0000000..1cb8bc0 --- /dev/null +++ b/Funcky.Money.Test/MoneyArbitraries.cs @@ -0,0 +1,25 @@ +using FsCheck; + +namespace Funcky.Test +{ + internal class MoneyArbitraries + { + public static Arbitrary ArbitraryCurrency() + => Arb.From(Gen.Elements(Currency.AllCurrencies)); + + public static Arbitrary ArbitraryMoney() + => GenerateMoney().ToArbitrary(); + + public static Arbitrary ArbitrarySwissFrancs() + => GenerateSwissFranc().ToArbitrary(); + + private static Gen GenerateMoney() + => from currency in Arb.Generate() + from amount in Arb.Generate() + select new Money(Power.OfATenth(currency.MinorUnitDigits) * amount.Get, currency); + + private static Gen GenerateSwissFranc() + => from amount in Arb.Generate() + select new SwissMoney(Money.CHF(0.05m * amount.Get)); + } +} diff --git a/Funcky.Money.Test/MoneyTest.cs b/Funcky.Money.Test/MoneyTest.cs index e7163cc..ddc0cf2 100644 --- a/Funcky.Money.Test/MoneyTest.cs +++ b/Funcky.Money.Test/MoneyTest.cs @@ -15,7 +15,7 @@ public sealed class MoneyTest public MoneyTest() => Arb - .Register(); + .Register(); private static MoneyEvaluationContext SwissRounding => MoneyEvaluationContext @@ -26,18 +26,15 @@ private static MoneyEvaluationContext SwissRounding .Build(); [Property] - public Property EvaluatingAMoneyInTheSameCurrencyDoesReturnTheSameAmount(decimal amount, Currency currency) + public Property EvaluatingAMoneyInTheSameCurrencyDoesReturnTheSameAmount(Money money) { - var money = new Money(ValidAmount(amount, currency), currency); - return (money.Amount == money.Evaluate().Amount).ToProperty(); } [Property] - public Property TheSumOfTwoMoneysIsCommutative(decimal amount1, decimal amount2, Currency currency) + public Property TheSumOfTwoMoneysIsCommutative(Money money1, Money money2) { - var money1 = new Money(ValidAmount(amount1, currency)); - var money2 = new Money(ValidAmount(amount2, currency)); + money2 = new Money(money2.Amount, money1.Currency); return (money1.Add(money2).Evaluate().Amount == money2.Add(money1).Evaluate().Amount).ToProperty(); } @@ -56,31 +53,28 @@ public void WeCanBuildTheSumOfTwoMoneysWithDifferentCurrenciesButOnEvaluationYou [Property] public Property DollarsAreNotFrancs(decimal amount) { - var francs = Money.CHF(ValidAmount(amount, Currency.CHF)); - var dollars = Money.USD(ValidAmount(amount, Currency.USD)); + var francs = Money.CHF(amount); + var dollars = Money.USD(amount); return (francs != dollars).ToProperty(); } [Property] - public Property MoneyCanBeMultipliedByConstantsFactors(decimal amount, decimal multiplier) + public Property MoneyCanBeMultipliedByConstantsFactors(Money someMoney, decimal multiplier) { - var someMoney = Money.CHF(amount); - var result = decimal.Round(amount * multiplier, someMoney.Currency.MinorUnitDigits) + var result = decimal.Round(someMoney.Amount * multiplier, someMoney.Currency.MinorUnitDigits) == someMoney.Multiply(multiplier).Evaluate().Amount; return result.ToProperty(); } [Property] - public Property DistributeMoneyEqually(decimal amount, PositiveInt numberOfParts) + public Property DistributeMoneyEqually(SwissMoney someMoney, PositiveInt numberOfParts) { - var validAmount = SwissRounding.RoundingStrategy.Round(amount); - var someMoney = new Money(validAmount, SwissRounding); - var distributed = someMoney.Distribute(numberOfParts.Get).Select(e => e.Evaluate(SwissRounding).Amount).ToList(); + var distributed = someMoney.Get.Distribute(numberOfParts.Get).Select(e => e.Evaluate(SwissRounding).Amount).ToList(); var first = distributed.First(); - return (TheSumOfThePartsIsEqualToTheTotal(distributed, validAmount) + return (TheSumOfThePartsIsEqualToTheTotal(distributed, someMoney.Get.Amount) && TheNumberOfPartsIsCorrect(numberOfParts, distributed) && TheIndividualPartsAreAtMostOneUnitApart(distributed, first)).ToProperty(); } @@ -163,10 +157,8 @@ public void WeCanDefineMoneyExpressionsWithOperators() } [Property] - public Property TheMoneyNeutralElementWorksWithAnyCurrency(decimal amount, Currency currency) + public Property TheMoneyNeutralElementWorksWithAnyCurrency(Money money) { - var money = new Money(ValidAmount(amount, currency), currency); - return (money == (money + Money.Zero).Evaluate() && (money == (Money.Zero + money).Evaluate())).ToProperty().When(!money.IsZero); } @@ -217,11 +209,9 @@ public void CurrenciesWithoutFormatProviders() } [Property] - public Property WeCanParseTheStringsWeGenerate(decimal amount, Currency currency) + public Property WeCanParseTheStringsWeGenerate(Money money) { - var money = new Money(decimal.Round(amount, currency.MinorUnitDigits), currency); - - return Money.ParseOrNone(money.ToString(), currency).Match(false, m => m == money).ToProperty(); + return Money.ParseOrNone(money.ToString(), money.Currency).Match(false, m => m == money).ToProperty(); } [Fact] @@ -463,9 +453,6 @@ public void RoundingStrategiesMustBeInitializedWithAValidPrecision() Assert.Throws(() => _ = RoundingStrategy.RoundWithAwayFromZero(0.0m)); } - private static decimal ValidAmount(decimal amount, Currency currency) - => decimal.Round(amount, currency.MinorUnitDigits); - private static bool TheIndividualPartsAreAtMostOneUnitApart(IEnumerable distributed, decimal first) => distributed.All(AtMostOneUnitLess(first, SmallestCoin)); diff --git a/Funcky.Money.Test/SwissMoney.cs b/Funcky.Money.Test/SwissMoney.cs new file mode 100644 index 0000000..bfed4d5 --- /dev/null +++ b/Funcky.Money.Test/SwissMoney.cs @@ -0,0 +1,4 @@ +namespace Funcky.Test +{ + public record SwissMoney(Money Get); +} From 1359c6e359626229d4e5589467c61f4d159b859a Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 6 Jan 2021 16:42:50 +0100 Subject: [PATCH 03/13] DRY SmallestCoin moved to SwissMoney --- Funcky.Money.Test/MoneyArbitraries.cs | 4 ++-- Funcky.Money.Test/MoneyTest.cs | 10 ++++------ Funcky.Money.Test/SwissMoney.cs | 5 ++++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Funcky.Money.Test/MoneyArbitraries.cs b/Funcky.Money.Test/MoneyArbitraries.cs index 1cb8bc0..4bc7605 100644 --- a/Funcky.Money.Test/MoneyArbitraries.cs +++ b/Funcky.Money.Test/MoneyArbitraries.cs @@ -5,7 +5,7 @@ namespace Funcky.Test internal class MoneyArbitraries { public static Arbitrary ArbitraryCurrency() - => Arb.From(Gen.Elements(Currency.AllCurrencies)); + => Arb.From(Gen.Elements(Currency.AllCurrencies)); public static Arbitrary ArbitraryMoney() => GenerateMoney().ToArbitrary(); @@ -20,6 +20,6 @@ from amount in Arb.Generate() private static Gen GenerateSwissFranc() => from amount in Arb.Generate() - select new SwissMoney(Money.CHF(0.05m * amount.Get)); + select new SwissMoney(Money.CHF(SwissMoney.SmallestCoin * amount.Get)); } } diff --git a/Funcky.Money.Test/MoneyTest.cs b/Funcky.Money.Test/MoneyTest.cs index ddc0cf2..0500124 100644 --- a/Funcky.Money.Test/MoneyTest.cs +++ b/Funcky.Money.Test/MoneyTest.cs @@ -11,8 +11,6 @@ namespace Funcky.Test { public sealed class MoneyTest { - private const decimal SmallestCoin = 0.05m; - public MoneyTest() => Arb .Register(); @@ -22,7 +20,7 @@ private static MoneyEvaluationContext SwissRounding .Builder .Default .WithTargetCurrency(Currency.CHF) - .WithSmallestDistributionUnit(SmallestCoin) + .WithSmallestDistributionUnit(SwissMoney.SmallestCoin) .Build(); [Property] @@ -248,10 +246,10 @@ public void TheRoundingStrategyIsCorrectlyPassedThrough() .Default .WithTargetCurrency(Currency.CHF); - var precision05 = new Money(1m, commonContext.WithRounding(RoundingStrategy.BankersRounding(SmallestCoin)).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, SmallestCoin).First().Evaluate().RoundingStrategy); + Assert.Equal(precision05.RoundingStrategy, precision05.Distribute(3, SwissMoney.SmallestCoin).First().Evaluate().RoundingStrategy); Assert.Equal(precision002.RoundingStrategy, precision002.Distribute(3, 0.002m).First().Evaluate().RoundingStrategy); } @@ -454,7 +452,7 @@ public void RoundingStrategiesMustBeInitializedWithAValidPrecision() } private static bool TheIndividualPartsAreAtMostOneUnitApart(IEnumerable distributed, decimal first) - => distributed.All(AtMostOneUnitLess(first, SmallestCoin)); + => distributed.All(AtMostOneUnitLess(first, SwissMoney.SmallestCoin)); private static Func AtMostOneUnitLess(decimal reference, decimal unit) => amount => amount == reference || amount == reference - unit; diff --git a/Funcky.Money.Test/SwissMoney.cs b/Funcky.Money.Test/SwissMoney.cs index bfed4d5..03bc340 100644 --- a/Funcky.Money.Test/SwissMoney.cs +++ b/Funcky.Money.Test/SwissMoney.cs @@ -1,4 +1,7 @@ namespace Funcky.Test { - public record SwissMoney(Money Get); + public record SwissMoney(Money Get) + { + public const decimal SmallestCoin = 0.05m; + } } From 5c8aa20e3576dd6477ecdca4d70e0f679d110bcd Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 6 Jan 2021 16:44:14 +0100 Subject: [PATCH 04/13] Improved consisteny in naming --- Funcky.Money.Test/MoneyArbitraries.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Funcky.Money.Test/MoneyArbitraries.cs b/Funcky.Money.Test/MoneyArbitraries.cs index 4bc7605..e26483a 100644 --- a/Funcky.Money.Test/MoneyArbitraries.cs +++ b/Funcky.Money.Test/MoneyArbitraries.cs @@ -10,7 +10,7 @@ public static Arbitrary ArbitraryCurrency() public static Arbitrary ArbitraryMoney() => GenerateMoney().ToArbitrary(); - public static Arbitrary ArbitrarySwissFrancs() + public static Arbitrary ArbitrarySwissMoney() => GenerateSwissFranc().ToArbitrary(); private static Gen GenerateMoney() From d93bbe5985d5f78b00382e9cfabaf4d781b78afc Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 6 Jan 2021 16:54:15 +0100 Subject: [PATCH 05/13] Allowe the generation of negative Money objects... * New Bug found: DistributeMoneyEqually does not work with negative amounts --- Funcky.Money.Test/MoneyArbitraries.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Funcky.Money.Test/MoneyArbitraries.cs b/Funcky.Money.Test/MoneyArbitraries.cs index e26483a..67a4c05 100644 --- a/Funcky.Money.Test/MoneyArbitraries.cs +++ b/Funcky.Money.Test/MoneyArbitraries.cs @@ -15,11 +15,11 @@ 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.Get, currency); + from amount in Arb.Generate() + 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.Get)); + => from amount in Arb.Generate() + select new SwissMoney(Money.CHF(SwissMoney.SmallestCoin * amount)); } } From 4f6310a80a43b33b15c2db56ae29fe69a4fb1c54 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 6 Jan 2021 17:02:44 +0100 Subject: [PATCH 06/13] Added test for negative proportional distributions --- Funcky.Money.Test/MoneyTest.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Funcky.Money.Test/MoneyTest.cs b/Funcky.Money.Test/MoneyTest.cs index 0500124..b8a7e60 100644 --- a/Funcky.Money.Test/MoneyTest.cs +++ b/Funcky.Money.Test/MoneyTest.cs @@ -94,6 +94,23 @@ public void DistributeMoneyProportionally(int first, int second, decimal expecte item => Assert.Equal(expected2, item)); } + [Theory] + [MemberData(nameof(ProportionalDistributionData))] + public void DistributeNegativeMoneyProportionally(int first, int second, decimal expected1, decimal expected2) + { + var fiftyCents = Money.EUR(-0.5m); + var sum = fiftyCents.Add(fiftyCents); + var distribution = sum.Distribute(new[] { first, second }); + + var distributed = distribution.Select(e => e.Evaluate().Amount).ToList(); + Assert.Equal(sum.Evaluate().Amount, distributed.Sum()); + + Assert.Collection( + distributed, + item => Assert.Equal(expected1 * -1, item), + item => Assert.Equal(expected2 * -1, item)); + } + public static TheoryData ProportionalDistributionData() => new() { From 5ec08eb464cfb48dd33ffce136ac75a25574c5e2 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 6 Jan 2021 17:46:52 +0100 Subject: [PATCH 07/13] Refactored the distribution laws --- Funcky.Money.Test/MoneyTest.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Funcky.Money.Test/MoneyTest.cs b/Funcky.Money.Test/MoneyTest.cs index b8a7e60..5525bbf 100644 --- a/Funcky.Money.Test/MoneyTest.cs +++ b/Funcky.Money.Test/MoneyTest.cs @@ -68,14 +68,8 @@ public Property MoneyCanBeMultipliedByConstantsFactors(Money someMoney, decimal [Property] public Property DistributeMoneyEqually(SwissMoney someMoney, PositiveInt numberOfParts) - { - var distributed = someMoney.Get.Distribute(numberOfParts.Get).Select(e => e.Evaluate(SwissRounding).Amount).ToList(); - var first = distributed.First(); - - return (TheSumOfThePartsIsEqualToTheTotal(distributed, someMoney.Get.Amount) - && TheNumberOfPartsIsCorrect(numberOfParts, distributed) - && TheIndividualPartsAreAtMostOneUnitApart(distributed, first)).ToProperty(); - } + => DistributionLaws(someMoney, numberOfParts, Distributed(someMoney, numberOfParts)) + .ToProperty(); [Theory] [MemberData(nameof(ProportionalDistributionData))] @@ -468,6 +462,18 @@ public void RoundingStrategiesMustBeInitializedWithAValidPrecision() Assert.Throws(() => _ = RoundingStrategy.RoundWithAwayFromZero(0.0m)); } + private static List Distributed(SwissMoney someMoney, PositiveInt numberOfParts) + => someMoney + .Get + .Distribute(numberOfParts.Get) + .Select(e => e.Evaluate(SwissRounding).Amount) + .ToList(); + + private static bool DistributionLaws(SwissMoney someMoney, PositiveInt numberOfParts, List distributed) + => TheSumOfThePartsIsEqualToTheTotal(distributed, someMoney.Get.Amount) + && TheNumberOfPartsIsCorrect(numberOfParts, distributed) + && TheIndividualPartsAreAtMostOneUnitApart(distributed, distributed.First()); + private static bool TheIndividualPartsAreAtMostOneUnitApart(IEnumerable distributed, decimal first) => distributed.All(AtMostOneUnitLess(first, SwissMoney.SmallestCoin)); From 98ceabfcacc2bf485634f70ce292c0589fe0e5af Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Thu, 7 Jan 2021 10:22:28 +0100 Subject: [PATCH 08/13] Fix distribution stratgey for negative amounts of money * split the three money distribution law tests --- Funcky.Money.Test/MoneyTest.cs | 37 +++++++++++-------- .../DefaultDistributionStrategy.cs | 21 +++++++++-- Funcky.Money/Extensions/SignExtension.cs | 12 ++++++ 3 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 Funcky.Money/Extensions/SignExtension.cs diff --git a/Funcky.Money.Test/MoneyTest.cs b/Funcky.Money.Test/MoneyTest.cs index 5525bbf..1cab043 100644 --- a/Funcky.Money.Test/MoneyTest.cs +++ b/Funcky.Money.Test/MoneyTest.cs @@ -67,8 +67,18 @@ public Property MoneyCanBeMultipliedByConstantsFactors(Money someMoney, decimal } [Property] - public Property DistributeMoneyEqually(SwissMoney someMoney, PositiveInt numberOfParts) - => DistributionLaws(someMoney, numberOfParts, Distributed(someMoney, numberOfParts)) + public Property DistributeMoneyConservesTheTotal(SwissMoney someMoney, PositiveInt numberOfParts) + => TheSumOfThePartsIsEqualToTheTotal(someMoney.Get.Amount, Distributed(someMoney, numberOfParts.Get)) + .ToProperty(); + + [Property] + public Property DistributeMoneyHasNumberOfParts(SwissMoney someMoney, PositiveInt numberOfParts) + => TheNumberOfPartsIsCorrect(numberOfParts.Get, Distributed(someMoney, numberOfParts.Get)) + .ToProperty(); + + [Property] + public Property DistributeMoneyHasMinimalDifference(SwissMoney someMoney, PositiveInt numberOfParts) + => TheIndividualPartsAreAtMostOneUnitApart(Distributed(someMoney, numberOfParts.Get)) .ToProperty(); [Theory] @@ -462,28 +472,23 @@ public void RoundingStrategiesMustBeInitializedWithAValidPrecision() Assert.Throws(() => _ = RoundingStrategy.RoundWithAwayFromZero(0.0m)); } - private static List Distributed(SwissMoney someMoney, PositiveInt numberOfParts) + private static List Distributed(SwissMoney someMoney, int numberOfParts) => someMoney .Get - .Distribute(numberOfParts.Get) + .Distribute(numberOfParts) .Select(e => e.Evaluate(SwissRounding).Amount) .ToList(); - private static bool DistributionLaws(SwissMoney someMoney, PositiveInt numberOfParts, List distributed) - => TheSumOfThePartsIsEqualToTheTotal(distributed, someMoney.Get.Amount) - && TheNumberOfPartsIsCorrect(numberOfParts, distributed) - && TheIndividualPartsAreAtMostOneUnitApart(distributed, distributed.First()); - - private static bool TheIndividualPartsAreAtMostOneUnitApart(IEnumerable distributed, decimal first) - => distributed.All(AtMostOneUnitLess(first, SwissMoney.SmallestCoin)); + private static bool TheIndividualPartsAreAtMostOneUnitApart(IEnumerable distributed) + => distributed.All(AtMostOneDistributionUnitLess(distributed.First())); - private static Func AtMostOneUnitLess(decimal reference, decimal unit) - => amount => amount == reference || amount == reference - unit; + private static Func AtMostOneDistributionUnitLess(decimal reference) + => amount => Math.Abs(amount - reference) <= SwissMoney.SmallestCoin; - private static bool TheNumberOfPartsIsCorrect(PositiveInt numberOfParts, ICollection distributed) - => distributed.Count == numberOfParts.Get; + private static bool TheNumberOfPartsIsCorrect(int numberOfParts, ICollection distributed) + => distributed.Count == numberOfParts; - private static bool TheSumOfThePartsIsEqualToTheTotal(IEnumerable distributed, decimal validAmount) + private static bool TheSumOfThePartsIsEqualToTheTotal(decimal validAmount, IEnumerable distributed) => distributed.Sum() == validAmount; private static IMoneyExpression ComplexExpression() diff --git a/Funcky.Money/Distribution/DefaultDistributionStrategy.cs b/Funcky.Money/Distribution/DefaultDistributionStrategy.cs index 1a119da..b42f34f 100644 --- a/Funcky.Money/Distribution/DefaultDistributionStrategy.cs +++ b/Funcky.Money/Distribution/DefaultDistributionStrategy.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Funcky.Extensions; using Funcky.Monads; @@ -23,15 +24,24 @@ private bool IsDistributable(MoneyDistributionPart part, Money money) && RoundingStrategy(money).IsSameAfterRounding(ToDistribute(part, money)); private decimal SliceAmount(MoneyDistributionPart part, Money money) + => Slice(part.Distribution, part.Index, money) + DistributeRest(part, money); + + private decimal DistributeRest(MoneyDistributionPart part, Money money) => part.Index switch { - _ when Precision(part.Distribution, money) * (part.Index + 1) < ToDistribute(part, money) => Slice(part.Distribution, part.Index, money) + Precision(part.Distribution, money), - _ when Precision(part.Distribution, money) * part.Index < ToDistribute(part, money) => Slice(part.Distribution, part.Index, money) + ToDistribute(part, money) - AlreadyDistributed(part, money), - _ => Slice(part.Distribution, part.Index, money), + _ when AtLeastOneDistributionUnitLeft(part, money) => SignedPrecision(part.Distribution, money), + _ when BetweenZeroToOneDistributionUnitLeft(part, money) => ToDistribute(part, money) - AlreadyDistributed(part, money), + _ => 0.0m, }; + private bool AtLeastOneDistributionUnitLeft(MoneyDistributionPart part, Money money) + => Precision(part.Distribution, money) * (part.Index + 1) < Math.Abs(ToDistribute(part, money)); + + private bool BetweenZeroToOneDistributionUnitLeft(MoneyDistributionPart part, Money money) + => Precision(part.Distribution, money) * part.Index < Math.Abs(ToDistribute(part, money)); + private decimal AlreadyDistributed(MoneyDistributionPart part, Money money) - => Precision(part.Distribution, money) * part.Index; + => SignedPrecision(part.Distribution, money) * part.Index; private IRoundingStrategy RoundingStrategy(Money money) => _context.Match( @@ -54,6 +64,9 @@ private decimal Slice(MoneyDistribution distribution, int index, Money money) private static decimal ExactSlice(MoneyDistribution distribution, int index, Money money) => money.Amount / DistributionTotal(distribution) * distribution.Factors[index]; + private decimal SignedPrecision(MoneyDistribution distribution, Money money) + => Precision(distribution, money).WithSignFrom(money.Amount); + // Order of evaluation: Distribution > Context Distribution > Context Currency > Money Currency private decimal Precision(MoneyDistribution distribution, Money money) => distribution diff --git a/Funcky.Money/Extensions/SignExtension.cs b/Funcky.Money/Extensions/SignExtension.cs new file mode 100644 index 0000000..45bda0b --- /dev/null +++ b/Funcky.Money/Extensions/SignExtension.cs @@ -0,0 +1,12 @@ +namespace Funcky.Extensions +{ + internal static class SignExtension + { + public static decimal WithSignFrom(this decimal positiveNumber, decimal signSource) + => signSource switch + { + < 0 => -positiveNumber, + >= 0 => positiveNumber, + }; + } +} From 97c14a3ea471bf04d4d821be4fd3c7a30ec16ce0 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Thu, 7 Jan 2021 10:25:51 +0100 Subject: [PATCH 09/13] Bugfix release 1.0.1 --- Funcky.Money/Funcky.Money.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Funcky.Money/Funcky.Money.csproj b/Funcky.Money/Funcky.Money.csproj index a1bac0c..bd58bb5 100644 --- a/Funcky.Money/Funcky.Money.csproj +++ b/Funcky.Money/Funcky.Money.csproj @@ -9,7 +9,7 @@ Funcky.Money Funcky.Money is the Money-Class you want to have. Functional Money - 1.0.0 + 1.0.1 true snupkg From f3f53d83551fc67fa0d085db501f01526af3ff04 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Thu, 7 Jan 2021 14:35:14 +0100 Subject: [PATCH 10/13] Fix Nullability with Funcky, and solve dependency issue --- .../Funcky.Money.SourceGenerator.csproj | 15 ++++++- .../Iso4217RecordGenerator.cs | 40 ++++++++++++++----- Funcky.Money.Test/Funcky.Money.Test.csproj | 3 -- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj b/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj index 81f5794..2e03424 100644 --- a/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj +++ b/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj @@ -1,10 +1,23 @@ - netstandard2.0;net5.0 + netstandard2.0 9.0 + enable + true + + false + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + diff --git a/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs b/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs index c8f2bbe..8e7e038 100644 --- a/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs +++ b/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Text; using System.Xml; +using Funcky.Extensions; +using Funcky.Monads; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using static System.Environment; @@ -131,7 +133,7 @@ private static IEnumerable ReadIso4217RecordsFromAdditionalFiles( .Where(f => f is not null) .SelectMany(text => CreateXmlDocumentFromString(text!.ToString()) .SelectNodesAsEnumerable("//CcyNtry/Ccy/..") - .Select(ReadIso4217RecordFromNode)) + .WhereSelect(ReadIso4217RecordFromNode)) .ToImmutableDictionary(r => r.AlphabeticCurrencyCode) .Select(r => r.Value); @@ -142,13 +144,33 @@ private static XmlDocument CreateXmlDocumentFromString(string xml) return document; } - private static Iso4217Record ReadIso4217RecordFromNode(XmlNode node) - { - var currencyName = node.SelectSingleNode(CurrencyNameNode)?.InnerText; - var alphabeticCurrencyName = node.SelectSingleNode(AlphabeticCurrencyCodeNode)?.InnerText; - var numericCurrencyCode = int.Parse(node.SelectSingleNode(NumericCurrencyCodeNode)?.InnerText); - var minorUnit = int.TryParse(node.SelectSingleNode(MinorUnitNode)?.InnerText, out var minorUnitTemp) ? (int?)minorUnitTemp : null; - return new Iso4217Record(currencyName, alphabeticCurrencyName, numericCurrencyCode, minorUnit); - } + private static Option ReadIso4217RecordFromNode(XmlNode node) + => from currencyName in CurrencyName(node) + from alphabeticCurrencyName in AlphabeticCurrencyName(node) + from numericCurrencyCode in NumericCurrencyCode(node) + select new Iso4217Record(currencyName, alphabeticCurrencyName, numericCurrencyCode, MinorUnit(node)); + + private static Option CurrencyName(XmlNode node) + => Option + .FromNullable(node.SelectSingleNode(CurrencyNameNode)) + .AndThen(n => n.InnerText); + + private static Option AlphabeticCurrencyName(XmlNode node) + => Option + .FromNullable(node.SelectSingleNode(AlphabeticCurrencyCodeNode)) + .AndThen(n => n.InnerText); + + private static Option NumericCurrencyCode(XmlNode node) + => Option + .FromNullable(node.SelectSingleNode(NumericCurrencyCodeNode)) + .AndThen(n => n.InnerText) + .AndThen(s => s.TryParseInt()); + + private static int MinorUnit(XmlNode node) + => Option + .FromNullable(node.SelectSingleNode(MinorUnitNode)) + .AndThen(n => n.InnerText) + .AndThen(s => s.TryParseInt()) + .GetOrElse(0); } } diff --git a/Funcky.Money.Test/Funcky.Money.Test.csproj b/Funcky.Money.Test/Funcky.Money.Test.csproj index cda71a3..0de63bf 100644 --- a/Funcky.Money.Test/Funcky.Money.Test.csproj +++ b/Funcky.Money.Test/Funcky.Money.Test.csproj @@ -6,11 +6,8 @@ enable true - false - Funcky.Test - false From ad6cabedef9f3e74b59797c367dc9ba01576fcc5 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Thu, 7 Jan 2021 14:45:49 +0100 Subject: [PATCH 11/13] Remove redundant WarningsAsErrors --- Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj | 1 - Funcky.Money.Test/Funcky.Money.Test.csproj | 1 - Funcky.Money/Funcky.Money.csproj | 1 - 3 files changed, 3 deletions(-) diff --git a/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj b/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj index 2e03424..79275ab 100644 --- a/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj +++ b/Funcky.Money.SourceGenerator/Funcky.Money.SourceGenerator.csproj @@ -4,7 +4,6 @@ 9.0 enable true - false diff --git a/Funcky.Money.Test/Funcky.Money.Test.csproj b/Funcky.Money.Test/Funcky.Money.Test.csproj index 0de63bf..bf9ed5f 100644 --- a/Funcky.Money.Test/Funcky.Money.Test.csproj +++ b/Funcky.Money.Test/Funcky.Money.Test.csproj @@ -5,7 +5,6 @@ 9.0 enable true - false Funcky.Test diff --git a/Funcky.Money/Funcky.Money.csproj b/Funcky.Money/Funcky.Money.csproj index bd58bb5..72e7eeb 100644 --- a/Funcky.Money/Funcky.Money.csproj +++ b/Funcky.Money/Funcky.Money.csproj @@ -4,7 +4,6 @@ 9.0 enable true - Funcky Funcky.Money Funcky.Money is the Money-Class you want to have. From 6dcd2dd980e07fb2bb0531b8d41d91b732b833b8 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Thu, 7 Jan 2021 14:50:43 +0100 Subject: [PATCH 12/13] Extract GetInnerText --- .../Iso4217RecordGenerator.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs b/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs index 8e7e038..f6e9afb 100644 --- a/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs +++ b/Funcky.Money.SourceGenerator/Iso4217RecordGenerator.cs @@ -151,26 +151,23 @@ from numericCurrencyCode in NumericCurrencyCode(node) select new Iso4217Record(currencyName, alphabeticCurrencyName, numericCurrencyCode, MinorUnit(node)); private static Option CurrencyName(XmlNode node) - => Option - .FromNullable(node.SelectSingleNode(CurrencyNameNode)) - .AndThen(n => n.InnerText); + => GetInnerText(node, CurrencyNameNode); private static Option AlphabeticCurrencyName(XmlNode node) - => Option - .FromNullable(node.SelectSingleNode(AlphabeticCurrencyCodeNode)) - .AndThen(n => n.InnerText); + => GetInnerText(node, AlphabeticCurrencyCodeNode); private static Option NumericCurrencyCode(XmlNode node) - => Option - .FromNullable(node.SelectSingleNode(NumericCurrencyCodeNode)) - .AndThen(n => n.InnerText) + => GetInnerText(node, NumericCurrencyCodeNode) .AndThen(s => s.TryParseInt()); private static int MinorUnit(XmlNode node) - => Option - .FromNullable(node.SelectSingleNode(MinorUnitNode)) - .AndThen(n => n.InnerText) + => GetInnerText(node, MinorUnitNode) .AndThen(s => s.TryParseInt()) .GetOrElse(0); + + private static Option GetInnerText(XmlNode node, string nodeName) + => Option + .FromNullable(node.SelectSingleNode(nodeName)) + .AndThen(n => n.InnerText); } } From 38cc72ffe7580904dd8b73afa6dd12fe9a08db8c Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Thu, 7 Jan 2021 14:56:26 +0100 Subject: [PATCH 13/13] Use canonical name CopySign --- Funcky.Money/Distribution/DefaultDistributionStrategy.cs | 2 +- Funcky.Money/Extensions/SignExtension.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Funcky.Money/Distribution/DefaultDistributionStrategy.cs b/Funcky.Money/Distribution/DefaultDistributionStrategy.cs index b42f34f..516c9ac 100644 --- a/Funcky.Money/Distribution/DefaultDistributionStrategy.cs +++ b/Funcky.Money/Distribution/DefaultDistributionStrategy.cs @@ -65,7 +65,7 @@ private static decimal ExactSlice(MoneyDistribution distribution, int index, Mon => money.Amount / DistributionTotal(distribution) * distribution.Factors[index]; private decimal SignedPrecision(MoneyDistribution distribution, Money money) - => Precision(distribution, money).WithSignFrom(money.Amount); + => Precision(distribution, money).CopySign(money.Amount); // Order of evaluation: Distribution > Context Distribution > Context Currency > Money Currency private decimal Precision(MoneyDistribution distribution, Money money) diff --git a/Funcky.Money/Extensions/SignExtension.cs b/Funcky.Money/Extensions/SignExtension.cs index 45bda0b..6bb614c 100644 --- a/Funcky.Money/Extensions/SignExtension.cs +++ b/Funcky.Money/Extensions/SignExtension.cs @@ -2,7 +2,7 @@ namespace Funcky.Extensions { internal static class SignExtension { - public static decimal WithSignFrom(this decimal positiveNumber, decimal signSource) + public static decimal CopySign(this decimal positiveNumber, decimal signSource) => signSource switch { < 0 => -positiveNumber,