From cc67bcfb630bfa97759baab0a5b3b3be993dce6f Mon Sep 17 00:00:00 2001 From: RemyDuijkeren Date: Sat, 25 Jan 2025 17:55:39 +0800 Subject: [PATCH] Refactor money formatting logic and improve performance. Consolidated formatting logic with cleaner code using switch expressions, reducing verbosity and improving maintainability. Updated performance benchmarks show significant enhancements in arithmetic and parsing operations. Simplified Money increment/decrement operations to eliminate unnecessary object allocations. --- src/NodaMoney/CurrencyInfo.cs | 129 +++++++++++--------------- src/NodaMoney/Money.UnaryOperators.cs | 4 +- tests/Benchmark/PerformanceReport.md | 30 +++--- tests/Benchmark/Program.cs | 4 +- 4 files changed, 75 insertions(+), 92 deletions(-) diff --git a/src/NodaMoney/CurrencyInfo.cs b/src/NodaMoney/CurrencyInfo.cs index a963a8b..2493381 100644 --- a/src/NodaMoney/CurrencyInfo.cs +++ b/src/NodaMoney/CurrencyInfo.cs @@ -319,11 +319,18 @@ public static CurrencyInfo GetInstance(IFormatProvider? formatProvider) /// public string Format(string? format, object? arg, IFormatProvider? formatProvider) // => Format(format.AsSpan(), arg, formatProvider); - // public string Format(ReadOnlySpan format, object? arg, IFormatProvider? formatProvider) + //private string Format(ReadOnlySpan format, object? arg, IFormatProvider? formatProvider) { - // TODO: Add Round-trip format specifier (R) https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#round-trip-format-specifier-r - // TODO: ICustomFormat : http://msdn.microsoft.com/query/dev12.query?appId=Dev12IDEF1&l=EN-US&k=k(System.IFormatProvider);k(TargetFrameworkMoniker-.NETPortable,Version%3Dv4.6);k(DevLang-csharp)&rd=true - // TODO: Hacked solution, solve with better implementation + // Supported formats: see https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings + // G: General format = C but with currency code => ISO code with number, like EUR 23.002,43 , EUR 23,002.43, 23,002.43 EUR + // C: Currency Symbol format, like € 23.002,43 , € 23,002.43, 23,002.43 € + // C => TODO: if symbol is GenericCurrencySign, then use code? What if NoCurrency? + // C => TODO: use C for long version (US$) and c for short version ($) in some locals + // R: Round-trip format with currency code + // N: Number format = decimal + // F: Fixed point format = decimal + // L: English name, like 23.002,43 dollar + // l: Native name, like 23.002,43 dólar if (arg is null) throw new ArgumentNullException(nameof(arg)); @@ -336,19 +343,7 @@ public string Format(string? format, object? arg, IFormatProvider? formatProvide : arg.ToString() ?? string.Empty; } - // Supported formats: see https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings - // G: General format = C but with currency code => ISO code with number, like EUR 23.002,43 , EUR 23,002.43, 23,002.43 EUR - // C: Currency Symbol format, like € 23.002,43 , € 23,002.43, 23,002.43 € - // C => TODO: if symbol is GenericCurrencySign, then use code? What if NoCurrency? - // C => TODO: use C for long version (US$) and c for short version ($) in some locals - // R: Round-trip format with currency code - // N: Number format = decimal - // F: Fixed point format = decimal - // L: English name, like 23.002,43 dollar - // l: Native name, like 23.002,43 dólar - // TODO: short= $13B, $12.8B or long= $14 billion - // TODO: CLDR-data: https://github.com/unicode-org/cldr-json/tree/main/cldr-json/cldr-numbers-full // For example USD in NL // "USD": { @@ -371,25 +366,29 @@ public string Format(string? format, object? arg, IFormatProvider? formatProvide return money.Amount.ToString(format, nfi); } - switch (fmt) + return fmt switch { - case 'C' when digits == -1: - return money.Amount.ToString("C", nfi); - case 'C' or 'c': // TODO: use C for long version (US$) and c for short version ($) in some locals? - return money.Amount.ToString(format, nfi); - case 'G' or 'g': - nfi = ToNumberFormatInfo(formatProvider, true); // replace currency symbol with code - return money.Amount.ToString($"C{format.AsSpan(1).ToString()}", nfi); - case 'L': // 1 US dollar, 0 US dollars, 1.000 US dollars, 1,000 US dollars - case 'l': // future: use lower-case for local native name + // Currency formats + 'C' when digits == -1 => money.Amount.ToString("C", nfi), + 'C' or 'c' => // TODO: use C for long version (US$) and c for short version ($) in some locals? + money.Amount.ToString($"C{digits}", nfi), + + // General format (uses currency code as symbol) + 'G' or 'g' when digits == -1 => money.Amount.ToString("C", ToNumberFormatInfo(formatProvider, true)), + 'G' or 'g' => money.Amount.ToString($"C{digits}", ToNumberFormatInfo(formatProvider, true)), + + // English Name currency (e.g., "1234.56 US dollars") // TODO: future use lower-case for local native name + 'L' or 'l' when digits == -1 => // N will use NumberDecimalDigits instead of CurrencyDecimalDigits. - nfi.NumberDecimalDigits = nfi.CurrencyDecimalDigits; - return $"{money.Amount.ToString($"N{format.AsSpan(1).ToString()}", nfi)} {EnglishName}"; - case 'R' or 'r': - return $"{Code} {money.Amount.ToString(format, nfi)}"; - default: - return money.Amount.ToString(format, nfi); - } + $"{money.Amount.ToString($"N{nfi.CurrencyDecimalDigits}", nfi)} {EnglishName}", + 'L' or 'l' => $"{money.Amount.ToString($"N{digits}", nfi)} {EnglishName}", + + // Round-trip format (e.g., "USD 1234.56") + 'R' or 'r' when digits == -1 => $"{Code} {money.Amount.ToString("R", nfi)}", + 'R' or 'r' => $"{Code} {money.Amount.ToString($"R{digits} ", nfi)}", + + _ => money.Amount.ToString(format, nfi) + }; } /// @@ -401,16 +400,12 @@ public string Format(string? format, object? arg, IFormatProvider? formatProvide /// A instance configured with currency formatting properties specific to the current currency. private NumberFormatInfo ToNumberFormatInfo(IFormatProvider? formatProvider, bool useCurrencyCode = false) { - NumberFormatInfo numberFormatInfo = (NumberFormatInfo)CultureInfo.CurrentCulture.NumberFormat.Clone(); - if (formatProvider != null) + NumberFormatInfo numberFormatInfo = formatProvider switch { - numberFormatInfo = formatProvider switch - { - CultureInfo ci => (NumberFormatInfo)ci.NumberFormat.Clone(), - NumberFormatInfo nfi => (NumberFormatInfo)nfi.Clone(), - _ => numberFormatInfo // use current culture - }; - } + CultureInfo ci => (NumberFormatInfo)ci.NumberFormat.Clone(), + NumberFormatInfo nfi => (NumberFormatInfo)nfi.Clone(), + _ => (NumberFormatInfo)CultureInfo.CurrentCulture.NumberFormat.Clone() + }; numberFormatInfo.CurrencyDecimalDigits = DecimalDigits; numberFormatInfo.CurrencySymbol = Symbol; @@ -421,39 +416,27 @@ private NumberFormatInfo ToNumberFormatInfo(IFormatProvider? formatProvider, boo // Replace currency symbol with the code numberFormatInfo.CurrencySymbol = Code; - // For PositivePattern and NegativePattern add space between code and value - if (numberFormatInfo.CurrencyPositivePattern == 0) // $n - numberFormatInfo.CurrencyPositivePattern = 2; // $ n - if (numberFormatInfo.CurrencyPositivePattern == 1) // n$ - numberFormatInfo.CurrencyPositivePattern = 3; // n $ + // For PositivePattern add space between code and value + numberFormatInfo.CurrencyPositivePattern = numberFormatInfo.CurrencyPositivePattern switch + { + 0 => 2, // $n -> $ n + 1 => 3, // n$ -> n $ + _ => numberFormatInfo.CurrencyPositivePattern // No change needed + }; - switch (numberFormatInfo.CurrencyNegativePattern) + // For NegativePattern add space between code and value + numberFormatInfo.CurrencyNegativePattern = numberFormatInfo.CurrencyNegativePattern switch { - case 0: // ($n) - numberFormatInfo.CurrencyNegativePattern = 14; // ($ n) - break; - case 1: // -$n - numberFormatInfo.CurrencyNegativePattern = 9; // -$ n - break; - case 2: // $-n - numberFormatInfo.CurrencyNegativePattern = 12; // $ -n - break; - case 3: // $n- - numberFormatInfo.CurrencyNegativePattern = 11; // $ n- - break; - case 4: // (n$) - numberFormatInfo.CurrencyNegativePattern = 15; // (n $) - break; - case 5: // -n$ - numberFormatInfo.CurrencyNegativePattern = 8; // -n $ - break; - case 6: // n-$ - numberFormatInfo.CurrencyNegativePattern = 13; // n- $ - break; - case 7: // n$- - numberFormatInfo.CurrencyNegativePattern = 10; // n $- - break; - } + 0 => 14, // ($n) -> ($ n) + 1 => 9, // -$n -> -$ n + 2 => 12, // $-n -> $ -n + 3 => 11, // $n- -> $ n- + 4 => 15, // (n$) -> (n $) + 5 => 8, // -n$ -> -n $ + 6 => 13, // n-$ -> n- $ + 7 => 10, // n$- -> n $- + _ => numberFormatInfo.CurrencyNegativePattern // No change needed + }; return numberFormatInfo; } diff --git a/src/NodaMoney/Money.UnaryOperators.cs b/src/NodaMoney/Money.UnaryOperators.cs index 0e2075d..99fb09f 100644 --- a/src/NodaMoney/Money.UnaryOperators.cs +++ b/src/NodaMoney/Money.UnaryOperators.cs @@ -41,10 +41,10 @@ public partial struct Money /// Increments the specified money. /// The money. /// The result. - public static Money Increment(in Money money) => Add(money, new Money(money.Currency.MinimalAmount, money.Currency)); + public static Money Increment(in Money money) => Add(money, money.Currency.MinimalAmount); /// Decrements the specified money. /// The money. /// The result. - public static Money Decrement(in Money money) => Subtract(money, new Money(money.Currency.MinimalAmount, money.Currency)); + public static Money Decrement(in Money money) => Subtract(money, money.Currency.MinimalAmount); } diff --git a/tests/Benchmark/PerformanceReport.md b/tests/Benchmark/PerformanceReport.md index 7508cc7..3da576b 100644 --- a/tests/Benchmark/PerformanceReport.md +++ b/tests/Benchmark/PerformanceReport.md @@ -51,15 +51,15 @@ AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores | Increment | 374.80 ns | 7.507 ns | 11.907 ns | - | | Decrement | 369.70 ns | 7.295 ns | 14.229 ns | - | #### after -| Method | Mean | Error | StdDev | Gen0 | Allocated | -|--------------------------|-----------:|----------:|----------:|-------:|----------:| -| Addition | 12.311 ns | 0.2696 ns | 0.3599 ns | - | - | -| Subtraction | 17.913 ns | 0.3833 ns | 0.5497 ns | - | - | -| CompareSameCurrency | 3.570 ns | 0.0905 ns | 0.0846 ns | - | - | -| CompareDifferentCurrency | 3.833 ns | 0.1055 ns | 0.1579 ns | - | - | -| CompareAmount | 4.470 ns | 0.0475 ns | 0.0421 ns | - | - | -| Increment | 105.702 ns | 1.3173 ns | 1.1000 ns | 0.0038 | 32 B | -| Decrement | 110.203 ns | 0.8956 ns | 0.7939 ns | 0.0038 | 32 B | +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|--------------------------|----------:|----------:|----------:|-------:|----------:| +| Addition | 16.804 ns | 0.1825 ns | 0.1618 ns | - | - | +| Subtraction | 15.575 ns | 0.1926 ns | 0.1707 ns | - | - | +| CompareSameCurrency | 3.639 ns | 0.0401 ns | 0.0356 ns | - | - | +| CompareDifferentCurrency | 3.667 ns | 0.0303 ns | 0.0253 ns | - | - | +| CompareAmount | 3.897 ns | 0.0780 ns | 0.0609 ns | - | - | +| Increment | 98.307 ns | 0.7919 ns | 0.7020 ns | 0.0038 | 32 B | +| Decrement | 99.775 ns | 1.5502 ns | 1.3743 ns | 0.0038 | 32 B | ## MoneyFormatting #### before (v1.x) @@ -70,12 +70,12 @@ AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores | Explicit | 271.5 ns | 5.50 ns | 9.33 ns | 0.0525 | 440 B | | ExplicitWithFormat | 270.3 ns | 5.42 ns | 12.56 ns | 0.0525 | 440 B | #### after -| Method | Mean | Error | StdDev | Median | Gen0 | Allocated | -|--------------------|---------:|--------:|---------:|---------:|-------:|----------:| -| Implicit | 107.3 ns | 2.19 ns | 3.95 ns | 107.7 ns | 0.0468 | 392 B | -| ImplicitWithFormat | 113.3 ns | 2.27 ns | 3.25 ns | 112.8 ns | 0.0468 | 392 B | -| Explicit | 143.9 ns | 2.89 ns | 5.84 ns | 144.3 ns | 0.0842 | 704 B | -| ExplicitWithFormat | 154.6 ns | 3.97 ns | 11.65 ns | 151.1 ns | 0.0842 | 704 B | +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|--------------------|---------:|--------:|--------:|-------:|----------:| +| Implicit | 109.6 ns | 2.24 ns | 4.14 ns | 0.0467 | 392 B | +| ImplicitWithFormat | 143.4 ns | 2.89 ns | 3.33 ns | 0.0505 | 424 B | +| Explicit | 111.5 ns | 2.25 ns | 4.44 ns | 0.0468 | 392 B | +| ExplicitWithFormat | 140.2 ns | 2.75 ns | 5.43 ns | 0.0505 | 424 B | ## MoneyParsing #### before (v1.x) diff --git a/tests/Benchmark/Program.cs b/tests/Benchmark/Program.cs index 511e2d1..d4320ac 100644 --- a/tests/Benchmark/Program.cs +++ b/tests/Benchmark/Program.cs @@ -3,8 +3,8 @@ // var initializingCurrencyReport = BenchmarkRunner.Run(); // var initializingMoneyReport = BenchmarkRunner.Run(); -// var moneyOperationsReport = BenchmarkRunner.Run(); -var moneyFormattingReport = BenchmarkRunner.Run(); +var moneyOperationsReport = BenchmarkRunner.Run(); +// var moneyFormattingReport = BenchmarkRunner.Run(); // var moneyParsingReport = BenchmarkRunner.Run(); // var addingCustomCurrencyReport = BenchmarkRunner.Run(); // var highLoadBenchReport = BenchmarkRunner.Run();