diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5fcad27 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: nuget + target-branch: main + directory: "/Source" + schedule: + interval: weekly +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly + target-branch: main \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0aac6dc..9c6a9dc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -12,9 +12,9 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x @@ -35,9 +35,9 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 6.0.x diff --git a/Docs/index.md b/Docs/index.md index a79cbae..00709c1 100644 --- a/Docs/index.md +++ b/Docs/index.md @@ -4,22 +4,24 @@ ## Overview -**BigDecimal** is a fully-featured decimal type that supports arbitrarily large precision values. It has all the mathematical operators, string conversions and parsing functionality (with full `NumberStyles` support) that you would expect from a complete implementation and performance has been highly optimized for values of all sizes. All operations are exact except for divisions which result in repeating decimals, and the behavior of all the methods is well documented so there are no surprises. +**BigDecimal** is a fully-featured decimal type that supports arbitrarily large precision values with an API that matches standard .NET numeric types so usage should feel natural and familiar. It has all the mathematical operators, string conversions and parsing functionality (including full `NumberStyles` support) that you would expect from a complete implementation and performance has been highly optimized for values of all sizes. All operations are exact except for divisions that result in repeating decimals, and the behavior of all the methods is well documented so there are no surprises. -**Singulink.Numerics.BigDecimal** is part of the **Singulink Libraries** collection. Visit https://github.com/Singulink/ to see the full list of libraries available. +### About Singulink + +We are a small team of engineers and designers dedicated to building beautiful, functional and well-engineered software solutions. We offer very competitive rates as well as fixed-price contracts and welcome inquiries to discuss any custom development / project support needs you may have. + +This package is part of our **Singulink Libraries** collection. Visit https://github.com/Singulink to see our full list of publicly available libraries and other open-source projects. ## Installation The package is available on NuGet - simply install the `Singulink.Numerics.BigDecimal` package. -**Supported Runtimes**: Anywhere .NET Standard 2.0+ is supported, including: -- .NET Core 2.0+ -- .NET Framework 4.6.1+ -- Mono 5.4+ -- Xamarin.iOS 10.14+ -- Xamarin.Android 8.0+ +**Supported Runtimes**: Everywhere .NET Standard 2.0 is supported, including: +- .NET +- .NET Framework +- Mono / Xamarin -The package multitargets .NET Standard 2.1 for extra performance optimizations and .NET 7 for generic math support. +End-of-life runtime versions that are no longer officially supported are not tested or supported by this library. ## Information and Links diff --git a/Source/.editorconfig b/Source/.editorconfig index 89c9586..0a1bbce 100644 --- a/Source/.editorconfig +++ b/Source/.editorconfig @@ -1,4 +1,4 @@ -# Remove the line below if you want to inherit .editorconfig settings from higher directories +# Root editor config for all projects in the repo root = true @@ -9,6 +9,25 @@ root = true guidelines = 160 +########### Project XML files ########### +[*.csproj] + +indent_size = 2 +indent_style = space +tab_width = 2 + +[*.props] + +indent_size = 2 +indent_style = space +tab_width = 2 + +[*.targets] + +indent_size = 2 +indent_style = space +tab_width = 2 + ########### C# files ########### [*.cs] @@ -71,10 +90,13 @@ dotnet_code_quality_unused_parameters = all:suggestion #### C# Coding Conventions #### +# Namespace preferences +csharp_style_namespace_declarations = file_scoped:warning + # var preferences csharp_style_var_elsewhere = false:silent -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = true:warning # Expression-bodied members csharp_style_expression_bodied_accessors = true:suggestion @@ -174,6 +196,14 @@ dotnet_naming_rule.types_should_be_pascalcase.severity = warning dotnet_naming_rule.types_should_be_pascalcase.symbols = types dotnet_naming_rule.types_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.private_static_field_should_be_s_prefixed_camelcase.severity = none +dotnet_naming_rule.private_static_field_should_be_s_prefixed_camelcase.symbols = private_static_field +dotnet_naming_rule.private_static_field_should_be_s_prefixed_camelcase.style = prefixed_camelcase + +dotnet_naming_rule.private_field_should_be_prefixed_camelcase.severity = warning +dotnet_naming_rule.private_field_should_be_prefixed_camelcase.symbols = private_field +dotnet_naming_rule.private_field_should_be_prefixed_camelcase.style = prefixed_camelcase + dotnet_naming_rule.constant_field_should_be_pascalcase.severity = warning dotnet_naming_rule.constant_field_should_be_pascalcase.symbols = constant_field dotnet_naming_rule.constant_field_should_be_pascalcase.style = pascalcase @@ -199,12 +229,25 @@ dotnet_naming_symbols.constant_field.applicable_kinds = field dotnet_naming_symbols.constant_field.applicable_accessibilities = public, internal, private, protected, protected_internal dotnet_naming_symbols.constant_field.required_modifiers = const +dotnet_naming_symbols.private_static_field.applicable_kinds = field +dotnet_naming_symbols.private_static_field.applicable_accessibilities = private +dotnet_naming_symbols.private_static_field.required_modifiers = static + +dotnet_naming_symbols.private_field.applicable_kinds = field +dotnet_naming_symbols.private_field.applicable_accessibilities = private +dotnet_naming_symbols.private_field.required_modifiers = + # Naming styles dotnet_naming_style.pascalcase.required_prefix = dotnet_naming_style.pascalcase.required_suffix = dotnet_naming_style.pascalcase.word_separator = dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_style.prefixed_camelcase.required_prefix = _ +dotnet_naming_style.prefixed_camelcase.required_suffix = +dotnet_naming_style.prefixed_camelcase.word_separator = +dotnet_naming_style.prefixed_camelcase.capitalization = camel_case + dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = @@ -213,6 +256,10 @@ dotnet_naming_style.begins_with_i.capitalization = pascal_case #### Analyzer diagnostics #### +stylecop.documentation.documentExposedElements = true +stylecop.documentation.documentInternalElements = false +stylecop.documentation.documentInterfaces = false + # SA1503: Braces should not be omitted dotnet_diagnostic.SA1503.severity = none @@ -332,3 +379,21 @@ dotnet_diagnostic.SA1519.severity = silent # SA1214: Readonly fields should appear before non-readonly fields dotnet_diagnostic.SA1214.severity = suggestion + +# SA1518: Use line endings correctly at end of file +dotnet_diagnostic.SA1518.severity = none + +# SA1402: File may only contain a single type +dotnet_diagnostic.SA1402.severity = none + +# IDE0028: Simplify collection initialization +dotnet_diagnostic.IDE0028.severity = warning + +# IDE0057: Use range operator +dotnet_diagnostic.IDE0057.severity = warning + +# IDE0059: Unnecessary assignment of a value +dotnet_diagnostic.IDE0059.severity = warning + +# RS0030: Do not use banned APIs +dotnet_diagnostic.RS0030.severity = error diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index fd423aa..0a32999 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -3,6 +3,12 @@ 12.0 enable true + + + + + + @@ -15,7 +21,7 @@ - - + + \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal.Tests/AssociativeTests.cs b/Source/Singulink.Numerics.BigDecimal.Tests/AssociativeTests.cs index 312fa51..c287c02 100644 --- a/Source/Singulink.Numerics.BigDecimal.Tests/AssociativeTests.cs +++ b/Source/Singulink.Numerics.BigDecimal.Tests/AssociativeTests.cs @@ -1,9 +1,8 @@ using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Singulink.Numerics.Tests; -[TestClass] +[PrefixTestClass] public class AssociativeTests { [TestMethod] diff --git a/Source/Singulink.Numerics.BigDecimal.Tests/ConvertToBigDecimalTests.cs b/Source/Singulink.Numerics.BigDecimal.Tests/ConvertToBigDecimalTests.cs index 7068270..090037f 100644 --- a/Source/Singulink.Numerics.BigDecimal.Tests/ConvertToBigDecimalTests.cs +++ b/Source/Singulink.Numerics.BigDecimal.Tests/ConvertToBigDecimalTests.cs @@ -1,13 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace Singulink.Numerics.Tests; -namespace Singulink.Numerics.Tests; - -[TestClass] +[PrefixTestClass] public class ConvertToBigDecimalTests { [TestMethod] diff --git a/Source/Singulink.Numerics.BigDecimal.Tests/MathOperationTests.cs b/Source/Singulink.Numerics.BigDecimal.Tests/MathOperationTests.cs index 48f864f..8a2cbe5 100644 --- a/Source/Singulink.Numerics.BigDecimal.Tests/MathOperationTests.cs +++ b/Source/Singulink.Numerics.BigDecimal.Tests/MathOperationTests.cs @@ -1,9 +1,6 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace Singulink.Numerics.Tests; -namespace Singulink.Numerics.Tests; - -[TestClass] +[PrefixTestClass] public class MathOperationTests { [TestMethod] diff --git a/Source/Singulink.Numerics.BigDecimal.Tests/ParseTests.cs b/Source/Singulink.Numerics.BigDecimal.Tests/ParseTests.cs index 1e668f5..5062e57 100644 --- a/Source/Singulink.Numerics.BigDecimal.Tests/ParseTests.cs +++ b/Source/Singulink.Numerics.BigDecimal.Tests/ParseTests.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Globalization; namespace Singulink.Numerics.Tests; -[TestClass] +[PrefixTestClass] public class ParseTests { [TestMethod] diff --git a/Source/Singulink.Numerics.BigDecimal.Tests/RoundingTests.cs b/Source/Singulink.Numerics.BigDecimal.Tests/RoundingTests.cs index 161993e..8e729bd 100644 --- a/Source/Singulink.Numerics.BigDecimal.Tests/RoundingTests.cs +++ b/Source/Singulink.Numerics.BigDecimal.Tests/RoundingTests.cs @@ -1,8 +1,6 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace Singulink.Numerics.Tests; -namespace Singulink.Numerics.Tests; - -[TestClass] +[PrefixTestClass] public class RoundingTests { [TestMethod] diff --git a/Source/Singulink.Numerics.BigDecimal.Tests/Singulink.Numerics.BigDecimal.Tests.csproj b/Source/Singulink.Numerics.BigDecimal.Tests/Singulink.Numerics.BigDecimal.Tests.csproj index 2e2571f..4d77b38 100644 --- a/Source/Singulink.Numerics.BigDecimal.Tests/Singulink.Numerics.BigDecimal.Tests.csproj +++ b/Source/Singulink.Numerics.BigDecimal.Tests/Singulink.Numerics.BigDecimal.Tests.csproj @@ -18,13 +18,11 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + diff --git a/Source/Singulink.Numerics.BigDecimal.Tests/StringFormattingTests.cs b/Source/Singulink.Numerics.BigDecimal.Tests/StringFormattingTests.cs index ebfe26a..1c710bc 100644 --- a/Source/Singulink.Numerics.BigDecimal.Tests/StringFormattingTests.cs +++ b/Source/Singulink.Numerics.BigDecimal.Tests/StringFormattingTests.cs @@ -1,10 +1,8 @@ -using System; -using System.Globalization; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Globalization; namespace Singulink.Numerics.Tests; -[TestClass] +[PrefixTestClass] public class StringFormattingTests { [TestMethod] diff --git a/Source/Singulink.Numerics.BigDecimal.Tests/Usings.cs b/Source/Singulink.Numerics.BigDecimal.Tests/Usings.cs new file mode 100644 index 0000000..243a34a --- /dev/null +++ b/Source/Singulink.Numerics.BigDecimal.Tests/Usings.cs @@ -0,0 +1,2 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using PrefixClassName.MsTest; \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal.sln b/Source/Singulink.Numerics.BigDecimal.sln index 14b1706..544bcb5 100644 --- a/Source/Singulink.Numerics.BigDecimal.sln +++ b/Source/Singulink.Numerics.BigDecimal.sln @@ -17,6 +17,28 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BFF58257 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.Numerics.BigDecimal.Tests", "Singulink.Numerics.BigDecimal.Tests\Singulink.Numerics.BigDecimal.Tests.csproj", "{45D09D15-3C40-44CF-BCDD-52AC75A22FE6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{FB50DCE8-0FE6-41E0-8B5A-78F75877E6A3}" + ProjectSection(SolutionItems) = preProject + ..\.github\dependabot.yml = ..\.github\dependabot.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{497F71F4-0EBE-4147-90EA-705CD858B3F4}" + ProjectSection(SolutionItems) = preProject + ..\Docs\docfx.json = ..\Docs\docfx.json + ..\Docs\index.md = ..\Docs\index.md + ..\Docs\toc.yml = ..\Docs\toc.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{BE9E0348-5C28-48D3-8152-4F61032069F6}" + ProjectSection(SolutionItems) = preProject + ..\.github\workflows\build-and-test.yml = ..\.github\workflows\build-and-test.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "api", "api", "{D1A1D6AB-1FAB-4655-8545-B1A7B659AA86}" + ProjectSection(SolutionItems) = preProject + ..\Docs\api\index.md = ..\Docs\api\index.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +79,8 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {45D09D15-3C40-44CF-BCDD-52AC75A22FE6} = {BFF58257-7C21-4F5C-84D1-80E07B8AB8CD} + {BE9E0348-5C28-48D3-8152-4F61032069F6} = {FB50DCE8-0FE6-41E0-8B5A-78F75877E6A3} + {D1A1D6AB-1FAB-4655-8545-B1A7B659AA86} = {497F71F4-0EBE-4147-90EA-705CD858B3F4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8D945EE2-B1AC-4666-BC91-156394840F36} diff --git a/Source/Singulink.Numerics.BigDecimal/BigDecimal.Conversions.cs b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Conversions.cs new file mode 100644 index 0000000..172dd72 --- /dev/null +++ b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Conversions.cs @@ -0,0 +1,222 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Numerics; +using System.Runtime.CompilerServices; +using Singulink.Numerics.Utilities; + +namespace Singulink.Numerics; + +/// +/// Contains conversion operators and methods for . +/// +partial struct BigDecimal +{ + private const string ToDecimalOrFloatFormat = "R"; + private const NumberStyles ToDecimalOrFloatStyle = NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign; + + private const string FromFloatFormat = "G"; + private const NumberStyles FromFloatStyle = NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + + #region Conversions to BigDecimal + + public static implicit operator BigDecimal(BigInteger value) => new BigDecimal(value, 0); + + public static implicit operator BigDecimal(byte value) => new BigDecimal(value, 0); + + public static implicit operator BigDecimal(sbyte value) => new BigDecimal(value, 0); + + public static implicit operator BigDecimal(short value) => new BigDecimal(value, 0); + + public static implicit operator BigDecimal(ushort value) => new BigDecimal(value, 0); + + public static implicit operator BigDecimal(int value) => new BigDecimal(value, 0); + + public static implicit operator BigDecimal(uint value) => new BigDecimal(value, 0); + + public static implicit operator BigDecimal(long value) => new BigDecimal(value, 0); + + public static implicit operator BigDecimal(ulong value) => new BigDecimal(value, 0); + + public static explicit operator BigDecimal(float value) => FromSingle(value); + + public static explicit operator BigDecimal(double value) => FromDouble(value); + + public static implicit operator BigDecimal(decimal value) + { + ref var decimalData = ref Unsafe.As(ref value); + + var mantissa = (new BigInteger(decimalData.Hi) << 64) | decimalData.Lo; + + if (!decimalData.IsPositive) + mantissa = -mantissa; + + return new BigDecimal(mantissa, -decimalData.Scale); + } + + #endregion + + #region Conversions from BigDecimal + + public static explicit operator BigInteger(BigDecimal value) + { + return value._exponent < 0 ? value._mantissa / BigIntegerPow10.Get(-value._exponent) : value._mantissa * BigIntegerPow10.Get(value._exponent); + } + + public static explicit operator byte(BigDecimal value) => (byte)(BigInteger)value; + + public static explicit operator sbyte(BigDecimal value) => (sbyte)(BigInteger)value; + + public static explicit operator short(BigDecimal value) => (short)(BigInteger)value; + + public static explicit operator ushort(BigDecimal value) => (ushort)(BigInteger)value; + + public static explicit operator int(BigDecimal value) => (int)(BigInteger)value; + + public static explicit operator uint(BigDecimal value) => (uint)(BigInteger)value; + + public static explicit operator long(BigDecimal value) => (long)(BigInteger)value; + + public static explicit operator ulong(BigDecimal value) => (ulong)(BigInteger)value; + + public static explicit operator float(BigDecimal value) + { + return float.Parse(value.ToString(ToDecimalOrFloatFormat), ToDecimalOrFloatStyle, CultureInfo.InvariantCulture); + } + + public static explicit operator double(BigDecimal value) + { + return double.Parse(value.ToString(ToDecimalOrFloatFormat), ToDecimalOrFloatStyle, CultureInfo.InvariantCulture); + } + + public static explicit operator decimal(BigDecimal value) + { + return decimal.Parse(value.ToString(ToDecimalOrFloatFormat), ToDecimalOrFloatStyle, CultureInfo.InvariantCulture); + } + + #endregion + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + #region Conversion Methods + + /// + /// Gets a representation of a value. + /// + public static BigDecimal FromSingle(float value, FloatConversion conversionMode = FloatConversion.Truncate) + { + return conversionMode switch { + FloatConversion.Roundtrip => FromFloat(value, 9), + FloatConversion.Truncate => FromFloat(value, 7), + FloatConversion.Exact => FromFloat(value, 0), + FloatConversion.ParseString => Parse(value.ToString(FromFloatFormat, CultureInfo.InvariantCulture).AsSpan(), FromFloatStyle, CultureInfo.InvariantCulture), + _ => Throw.ArgumentOutOfRangeEx(nameof(conversionMode)), + }; + } + + /// + /// Gets a representation of a value. + /// + public static BigDecimal FromDouble(double value, FloatConversion conversionMode = FloatConversion.Truncate) + { + return conversionMode switch { + FloatConversion.Roundtrip => FromFloat(value, 17), + FloatConversion.Truncate => FromFloat(value, 15), + FloatConversion.Exact => FromFloat(value, 0), + FloatConversion.ParseString => Parse(value.ToString(FromFloatFormat, CultureInfo.InvariantCulture).AsSpan(), FromFloatStyle, CultureInfo.InvariantCulture), + _ => Throw.ArgumentOutOfRangeEx(nameof(conversionMode)), + }; + } + + private static BigDecimal FromFloat(double value, int precision) + { + if (double.IsNaN(value)) + Throw.ArgumentOutOfRangeEx(nameof(value), "Floating point NaN values cannot be converted to BigDecimal."); + + if (double.IsNegativeInfinity(value) || double.IsPositiveInfinity(value)) + Throw.ArgumentOutOfRangeEx(nameof(value), "Floating point infinity values cannot be converted to BigDecimal."); + + Debug.Assert(precision is 0 or 7 or 9 or 15 or 17, "unexpected precision value"); + + unchecked + { + // Based loosely on Jon Skeet's DoubleConverter: + + long bits = BitConverter.DoubleToInt64Bits(value); + bool negative = bits < 0; + int exponent = (int)((bits >> 52) & 0x7ffL); + long mantissa = bits & 0xfffffffffffffL; + + // Subnormal numbers: exponent is effectively one higher, but there's no extra normalization bit in the mantissa Normal numbers. + // Leave exponent as it is but add extra bit to the front of the mantissa + if (exponent is 0) + exponent++; + else + mantissa |= 1L << 52; + + if (mantissa is 0) + return Zero; + + // Bias the exponent. It's actually biased by 1023, but we're treating the mantissa as m.0 rather than 0.m, so we need to subtract another 52 + // from it. + exponent -= 1075; + + // Normalize + + while ((mantissa & 1) is 0) + { + // mantissa is even + mantissa >>= 1; + exponent++; + } + + if (negative) + mantissa = -mantissa; + + var resultMantissa = (BigInteger)mantissa; + int resultExponent; + bool trimTrailingZeros; + + if (exponent is 0) + { + resultExponent = 0; + trimTrailingZeros = false; + } + else if (exponent < 0) + { + resultMantissa *= BigIntegerPow5.Get(-exponent); + resultExponent = exponent; + trimTrailingZeros = false; + } + else + { + // exponent > 0 + resultMantissa <<= exponent; // *= BigInteger.Pow(BigInt2, exponent); + resultExponent = 0; + trimTrailingZeros = true; + } + + if (precision > 0) + { + int digits = resultMantissa.CountDigits(); + int extraDigits = digits - precision; + + if (extraDigits <= 0) + return new BigDecimal(resultMantissa, resultExponent, digits); + + resultMantissa = resultMantissa.Divide(BigIntegerPow10.Get(extraDigits)); + resultExponent += extraDigits; + trimTrailingZeros = true; + } + + if (trimTrailingZeros) + return new BigDecimal(resultMantissa, resultExponent); + + return new BigDecimal(resultMantissa, resultExponent, resultMantissa.CountDigits()); + } + } + + #endregion +} \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal/BigDecimal.Formatting.cs b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Formatting.cs new file mode 100644 index 0000000..175a35a --- /dev/null +++ b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Formatting.cs @@ -0,0 +1,304 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Numerics; +using System.Text; +using Singulink.Numerics.Utilities; + +namespace Singulink.Numerics; + +/// +/// Contains string formatting functionality for . +/// +partial struct BigDecimal : IFormattable +{ + /// + /// Returns a full-precision decimal form string representation of this value using the current culture. + /// + public override string ToString() => ToString(null); + + /// + /// Returns a string representation of this value. + /// + /// The string format to use. The "G" format is used if none is provided. + /// The format provider that will be used to obtain number format information. The current culture is used if none is + /// provided. + /// + /// String format is composed of a format specifier followed by an optional precision specifier. + /// Format specifiers: + /// + /// + /// Specifier + /// Name + /// Description + /// + /// + /// "G" + /// General + /// Default format specifier if none is provided. Precision specifier determines the number of significant digits. If the precision + /// specifier is omitted then the value is written out in full precision decimal form. If a precision specifier is provided then the + /// more compact of either decimal form or scientific notation is used. + /// + /// + /// "F" + /// Fixed-point + /// Precision specifier determines the number of decimal digits. Default value is . + /// + /// + /// "N" + /// Number + /// Like fixed-point, but also outputs group separators. Precision specifier determines the number of decimal digits. Default value is . + /// + /// + /// "E" + /// Exponential + /// Exponential (scientific) notation. Precision specifier determines the number of decimal digits. + /// + /// + /// "C" + /// Currency + /// Precision specifier determines the number of decimal digits. Default value is . + /// + /// + /// "P" + /// Percentage + /// Precision specifier determines the number of decimal digits. Default value is . + /// + /// + /// "R" + /// Round-trip + /// Outputs the mantissa followed by E and then the exponent, always using the . + /// + /// + /// + public string ToString(string? format, IFormatProvider? formatProvider = null) + { + format = format?.Trim(); + var formatInfo = NumberFormatInfo.GetInstance(formatProvider); + + char formatSpecifier; + int? precisionSpecifier = null; + + if (string.IsNullOrEmpty(format)) + { + formatSpecifier = 'G'; + } + else + { + formatSpecifier = char.ToUpperInvariant(format![0]); + + if (format.Length > 1) + { +#if NETSTANDARD2_0 + if (int.TryParse(format[1..], NumberStyles.None, CultureInfo.InvariantCulture, out int ps)) +#else + if (int.TryParse(format.AsSpan()[1..], NumberStyles.None, CultureInfo.InvariantCulture, out int ps)) +#endif + precisionSpecifier = ps; + else + Throw.FormatEx($"Invalid precision specifier: '{format[1..]}'"); + } + } + + if (formatSpecifier is 'G') + { + BigDecimal value; + + if (precisionSpecifier is null || precisionSpecifier.GetValueOrDefault() is 0) + { + value = this; + } + else + { + int precision = precisionSpecifier.GetValueOrDefault(); + value = RoundToPrecision(this, precision, RoundingMode.MidpointAwayFromZero); + + if (GetEstimatedFullDecimalLength(value) > GetEstimatedExponentialLength(value)) + { + int exponentDecimals = Math.Min(value.Precision, precision) - 1; + return GetExponentialString(value, exponentDecimals); + } + } + + if (value._exponent >= 0) + return GetIntegerString(value, "G"); + + return GetDecimalString(value, "G", null); + } + + if (formatSpecifier is 'F' or 'N') + { + string wholePartFormat = formatSpecifier is 'F' ? "F0" : "N0"; + + int decimals = precisionSpecifier.HasValue ? precisionSpecifier.GetValueOrDefault() : formatInfo.NumberDecimalDigits; + var value = Round(this, decimals, RoundingMode.MidpointAwayFromZero); + + if (decimals is 0) + return GetIntegerString(value, wholePartFormat); + + return GetDecimalString(value, wholePartFormat, decimals); + } + + if (formatSpecifier is 'E') + return GetExponentialString(this, precisionSpecifier); + + if (formatSpecifier is 'C' or 'P') + { + BigDecimal value = this; + + if (formatSpecifier is 'P') + { + // Convert percentage format info params to currency params and write it out as a currency value: + + formatInfo = new NumberFormatInfo() { + CurrencySymbol = formatInfo.PercentSymbol, + CurrencyDecimalDigits = formatInfo.PercentDecimalDigits, + CurrencyDecimalSeparator = formatInfo.PercentDecimalSeparator, + CurrencyGroupSeparator = formatInfo.PercentGroupSeparator, + CurrencyGroupSizes = formatInfo.PercentGroupSizes, + CurrencyPositivePattern = PositivePercentagePatternToCurrencyPattern(formatInfo.PercentPositivePattern), + CurrencyNegativePattern = NegativePercentagePatternToCurrencyPattern(formatInfo.PercentNegativePattern), + }; + + value *= 100; + } + + int decimals = precisionSpecifier.HasValue ? precisionSpecifier.GetValueOrDefault() : formatInfo.CurrencyDecimalDigits; + value = Round(value, decimals, RoundingMode.MidpointAwayFromZero); + + if (decimals is 0) + return GetIntegerString(value, "C0"); + + return GetDecimalString(value, "C0", decimals); + } + + if (formatSpecifier is not 'R') + Throw.FormatEx($"Format specifier was invalid: '{formatSpecifier}'."); + + if (_exponent is 0) + return _mantissa.ToString(CultureInfo.InvariantCulture); + + return ((FormattableString)$"{_mantissa}E{_exponent}").ToString(CultureInfo.InvariantCulture); + + static int GetEstimatedFullDecimalLength(BigDecimal value) + { + if (value._exponent >= 0) + return value.Precision + value._exponent; + + return value.Precision + Math.Max(0, -value._exponent - value.Precision) + 1; // digits + additional leading zeros + decimal separator + } + + static int GetEstimatedExponentialLength(BigDecimal value) => value.Precision + 5; // .E+99 + + string GetExponentialString(BigDecimal value, int? precisionSpecifier) + { + string result = value._mantissa.ToString("E" + precisionSpecifier, formatInfo); + + if (value._exponent is 0) + return result; + + int eIndex = result.LastIndexOf("E", StringComparison.Ordinal); + +#if NETSTANDARD2_0 + string exponentString = result[(eIndex + 1)..]; +#else + var exponentString = result.AsSpan()[(eIndex + 1)..]; +#endif + int exponent = int.Parse(exponentString, NumberStyles.AllowLeadingSign, formatInfo) + value._exponent; + var mantissa = result.AsSpan()[..(eIndex + 1)]; + string absExponentString = Math.Abs(exponent).ToString(formatInfo); + + if (exponent > 0) + return StringHelper.Concat(mantissa, formatInfo.PositiveSign.AsSpan(), absExponentString.AsSpan()); + + return StringHelper.Concat(mantissa, formatInfo.NegativeSign.AsSpan(), absExponentString.AsSpan()); + } + + string GetDecimalString(BigDecimal value, string wholePartFormat, int? fixedDecimalPlaces) + { + var wholePart = Truncate(value); + string wholeString; + + if (wholePart.IsZero && value.Sign < 0) + wholeString = (-1).ToString(wholePartFormat, formatInfo).Replace('1', '0'); + else + wholeString = GetIntegerString(wholePart, wholePartFormat); + + var decimalPart = Abs(value - wholePart); + int decimalPartShift = -decimalPart._exponent; + int decimalLeadingZeros = decimalPart.IsZero ? 0 : decimalPartShift - decimalPart.Precision; + int decimalTrailingZeros = 0; + + if (fixedDecimalPlaces.HasValue) + decimalTrailingZeros = Math.Max(0, fixedDecimalPlaces.GetValueOrDefault() - decimalPart.Precision - decimalLeadingZeros); + + decimalPart = decimalPart._mantissa; + Debug.Assert(decimalPart._exponent is 0, "unexpected transformed decimal part exponent"); + + string decimalString = GetIntegerString(decimalPart, "G"); + + int insertPoint; + + for (insertPoint = wholeString.Length; insertPoint > 0; insertPoint--) + { + if (char.IsDigit(wholeString[insertPoint - 1])) + break; + } + + string decimalSeparator = wholePartFormat[0] == 'C' ? formatInfo.CurrencyDecimalSeparator : formatInfo.NumberDecimalSeparator; + + var sb = new StringBuilder(wholeString.Length + decimalSeparator.Length + decimalLeadingZeros + decimalString.Length); +#if NETSTANDARD2_0 + sb.Append(wholeString[..insertPoint]); +#else + sb.Append(wholeString.AsSpan()[..insertPoint]); +#endif + sb.Append(decimalSeparator); + sb.Append('0', decimalLeadingZeros); + sb.Append(decimalString); + sb.Append('0', decimalTrailingZeros); +#if NETSTANDARD2_0 + sb.Append(wholeString[insertPoint..]); +#else + sb.Append(wholeString.AsSpan()[insertPoint..]); +#endif + + return sb.ToString(); + } + + string GetIntegerString(BigDecimal value, string format) + { + Debug.Assert(value._exponent >= 0, "value contains decimal digits"); + BigInteger intValue = value._mantissa; + + if (value._exponent > 0) + intValue *= BigIntegerPow10.Get(value._exponent); + + return intValue.ToString(format, formatInfo); + } + + static int PositivePercentagePatternToCurrencyPattern(int positivePercentagePattern) => positivePercentagePattern switch { + 0 => 3, + 1 => 1, + 2 => 0, + 3 => 2, + _ => Throw.NotSupportedEx("Unsupported positive percentage pattern."), + }; + + static int NegativePercentagePatternToCurrencyPattern(int negativePercentagePattern) => negativePercentagePattern switch { + 0 => 8, + 1 => 5, + 2 => 1, + 3 => 2, + 4 => 3, + 5 => 6, + 6 => 7, + 7 => 9, + 8 => 10, + 9 => 11, + 10 => 12, + 11 => 13, + _ => Throw.NotSupportedEx("Unsupported negative percentage pattern."), + }; + } +} \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal/BigDecimal.GenericMath.cs b/Source/Singulink.Numerics.BigDecimal/BigDecimal.GenericMath.cs new file mode 100644 index 0000000..33558a7 --- /dev/null +++ b/Source/Singulink.Numerics.BigDecimal/BigDecimal.GenericMath.cs @@ -0,0 +1,376 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using Singulink.Numerics.Utilities; + +namespace Singulink.Numerics; + +#if NET7_0_OR_GREATER + +/// +/// Contains .NET7+ generic math support for . +/// +partial struct BigDecimal : IFloatingPoint +{ + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BigDecimal CreateChecked(TOther value) + where TOther : INumberBase + { + BigDecimal result; + + if (typeof(TOther) == typeof(BigDecimal)) + result = (BigDecimal)(object)value; + else if (!TryConvertFromChecked(value, out result) && !TOther.TryConvertToChecked(value, out result)) + Throw.NotSupportedEx(); + + return result; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BigDecimal CreateSaturating(TOther value) + where TOther : INumberBase + { + BigDecimal result; + + if (typeof(TOther) == typeof(BigDecimal)) + result = (BigDecimal)(object)value; + else if (!TryConvertFrom(value, out result) && !TOther.TryConvertToSaturating(value, out result)) + Throw.NotSupportedEx(); + + return result; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BigDecimal CreateTruncating(TOther value) + where TOther : INumberBase + { + BigDecimal result; + + if (typeof(TOther) == typeof(BigDecimal)) + result = (BigDecimal)(object)value; + else if (!TryConvertFrom(value, out result) && !TOther.TryConvertToTruncating(value, out result)) + Throw.NotSupportedEx(); + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryConvertFromChecked(TOther value, out BigDecimal result) where TOther : INumberBase + { + if (typeof(TOther) == typeof(float)) + { + result = (BigDecimal)(float)(object)value; + return true; + } + else if (typeof(TOther) == typeof(double)) + { + result = (BigDecimal)(double)(object)value; + return true; + } + else if (typeof(TOther) == typeof(decimal)) + { + result = (BigDecimal)(decimal)(object)value; + return true; + } + else if (typeof(TOther).IsAssignableTo(typeof(IBinaryInteger<>))) + { + if (TOther.TryConvertToChecked(value, out var intValue)) + { + result = (BigDecimal)intValue; + return true; + } + } + + result = default; + return false; + } + + private static bool TryConvertFrom(TOther value, out BigDecimal result) where TOther : INumberBase + { + if (typeof(TOther) == typeof(float)) + { + float actualValue = (float)(object)value; + result = float.IsNaN(actualValue) ? Zero : (BigDecimal)actualValue; + return true; + } + else if (typeof(TOther) == typeof(double)) + { + double actualValue = (double)(object)value; + result = double.IsNaN(actualValue) ? Zero : (BigDecimal)actualValue; + return true; + } + else if (typeof(TOther) == typeof(decimal)) + { + result = (BigDecimal)(decimal)(object)value; + return true; + } + else if (TOther.IsInteger(value)) + { + if (TOther.TryConvertToChecked(value, out var integer)) + { + result = (BigDecimal)integer; + return true; + } + } + + result = default; + return false; + } + + #region Explicit Generic Math Implementations + + private static BigDecimal _e; + private static BigDecimal _pi; + private static BigDecimal _tau; + + /// + static BigDecimal IFloatingPointConstants.E + { + get { + if (_e.IsZero) + _e = Parse("2.7182818284590452353602874713526624977572470936999"); + + return _e; + } + } + + /// + static BigDecimal IFloatingPointConstants.Pi + { + get { + if (_pi.IsZero) + _pi = Parse("3.1415926535897932384626433832795028841971693993751"); + + return _pi; + } + } + + /// + static BigDecimal IFloatingPointConstants.Tau + { + get { + if (_tau.IsZero) + _tau = Parse("6.2831853071795864769252867665590057683943387987502"); + + return _tau; + } + } + + /// + static BigDecimal ISignedNumber.NegativeOne => MinusOne; + + /// + static int INumberBase.Radix => 10; + + /// + static BigDecimal IAdditiveIdentity.AdditiveIdentity => Zero; + + /// + static BigDecimal IMultiplicativeIdentity.MultiplicativeIdentity => One; + + /// + int IFloatingPoint.GetExponentByteCount() => sizeof(int); + + /// + int IFloatingPoint.GetExponentShortestBitLength() => ((IBinaryInteger)_exponent).GetShortestBitLength(); + + /// + int IFloatingPoint.GetSignificandBitLength() => _mantissa.GetByteCount(false) * 8; + + /// + int IFloatingPoint.GetSignificandByteCount() => _mantissa.GetByteCount(); + + /// + static BigDecimal IFloatingPoint.Round(BigDecimal x, int digits, MidpointRounding mode) => Round(x, digits, (RoundingMode)mode); + + /// + bool IFloatingPoint.TryWriteExponentBigEndian(Span destination, out int bytesWritten) + { + return ((IBinaryInteger)_exponent).TryWriteBigEndian(destination, out bytesWritten); + } + + /// + bool IFloatingPoint.TryWriteExponentLittleEndian(Span destination, out int bytesWritten) + { + return ((IBinaryInteger)_exponent).TryWriteLittleEndian(destination, out bytesWritten); + } + + /// + bool IFloatingPoint.TryWriteSignificandBigEndian(Span destination, out int bytesWritten) + { + return ((IBinaryInteger)_mantissa).TryWriteBigEndian(destination, out bytesWritten); + } + + /// + bool IFloatingPoint.TryWriteSignificandLittleEndian(Span destination, out int bytesWritten) + { + return ((IBinaryInteger)_mantissa).TryWriteLittleEndian(destination, out bytesWritten); + } + + /// + static bool INumberBase.IsCanonical(BigDecimal value) => true; + + /// + static bool INumberBase.IsComplexNumber(BigDecimal value) => false; + + /// + static bool INumberBase.IsFinite(BigDecimal value) => true; + + /// + static bool INumberBase.IsImaginaryNumber(BigDecimal value) => false; + + /// + static bool INumberBase.IsInfinity(BigDecimal value) => false; + + /// + static bool INumberBase.IsNaN(BigDecimal value) => false; + + /// + static bool INumberBase.IsNegativeInfinity(BigDecimal value) => false; + + /// + static bool INumberBase.IsNormal(BigDecimal value) => !value.IsZero; + + /// + static bool INumberBase.IsPositiveInfinity(BigDecimal value) => false; + + /// + static bool INumberBase.IsRealNumber(BigDecimal value) => true; + + /// + static bool INumberBase.IsSubnormal(BigDecimal value) => false; + + /// + static bool INumberBase.IsZero(BigDecimal value) => value.IsZero; + + /// + static BigDecimal INumberBase.MaxMagnitudeNumber(BigDecimal x, BigDecimal y) => MaxMagnitude(x, y); + + /// + static BigDecimal INumberBase.MinMagnitudeNumber(BigDecimal x, BigDecimal y) => MinMagnitude(x, y); + + /// + static bool INumberBase.TryConvertFromChecked(TOther value, out BigDecimal result) => TryConvertFromChecked(value, out result); + + /// + static bool INumberBase.TryConvertFromSaturating(TOther value, out BigDecimal result) => TryConvertFrom(value, out result); + + /// + static bool INumberBase.TryConvertFromTruncating(TOther value, out BigDecimal result) => TryConvertFrom(value, out result); + + /// + static bool INumberBase.TryConvertToChecked(BigDecimal value, [MaybeNullWhen(false)] out TOther result) + { + if (typeof(TOther) == typeof(float)) + { + result = (TOther)(object)(float)value; + return true; + } + else if (typeof(TOther) == typeof(double)) + { + result = (TOther)(object)(double)value; + return true; + } + else if (typeof(TOther) == typeof(decimal)) + { + result = (TOther)(object)(decimal)value; + return true; + } + else if (typeof(TOther).IsAssignableTo(typeof(IBinaryInteger<>))) + { + var intValue = (BigInteger)value; + return TOther.TryConvertFromChecked(intValue, out result); + } + + result = default; + return false; + } + + /// + static bool INumberBase.TryConvertToSaturating(BigDecimal value, [MaybeNullWhen(false)] out TOther result) + { + if (typeof(TOther) == typeof(float)) + { + result = (TOther)(object)(float)value; + return true; + } + else if (typeof(TOther) == typeof(double)) + { + result = (TOther)(object)(double)value; + return true; + } + else if (typeof(TOther) == typeof(decimal)) + { + result = value < decimal.MinValue ? (TOther)(object)decimal.MinValue : + value > decimal.MaxValue ? (TOther)(object)decimal.MaxValue : + (TOther)(object)(decimal)value; + + return true; + } + else if (typeof(TOther).IsAssignableTo(typeof(IBinaryInteger<>))) + { + var intValue = (BigInteger)value; + return TOther.TryConvertFromSaturating(intValue, out result); + } + + result = default; + return false; + } + + /// + static bool INumberBase.TryConvertToTruncating(BigDecimal value, [MaybeNullWhen(false)] out TOther result) + { + if (typeof(TOther) == typeof(float)) + { + result = (TOther)(object)(float)value; + return true; + } + else if (typeof(TOther) == typeof(double)) + { + result = (TOther)(object)(double)value; + return true; + } + else if (typeof(TOther) == typeof(decimal)) + { + result = value < decimal.MinValue ? (TOther)(object)decimal.MinValue : + value > decimal.MaxValue ? (TOther)(object)decimal.MaxValue : + (TOther)(object)(decimal)value; + + return true; + } + else if (typeof(TOther).IsAssignableTo(typeof(IBinaryInteger<>))) + { + var intValue = (BigInteger)value; + return TOther.TryConvertFromTruncating(intValue, out result); + } + + result = default; + return false; + } + + /// + bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + // TODO: Implement better performing option + + string s = ToString(format.ToString(), provider); + + if (destination.Length < s.Length) + { + charsWritten = 0; + return false; + } + + s.CopyTo(destination); + charsWritten = s.Length; + return true; + } + + #endregion +} + +#endif \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal/BigDecimal.Operators.cs b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Operators.cs new file mode 100644 index 0000000..c9bb266 --- /dev/null +++ b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Operators.cs @@ -0,0 +1,82 @@ +using System.Numerics; + +namespace Singulink.Numerics; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +/// +/// Contains operator implementations for . +/// +partial struct BigDecimal +{ + public static BigDecimal operator +(BigDecimal value) => value; + + public static BigDecimal operator -(BigDecimal value) => new(BigInteger.Negate(value._mantissa), value._exponent, value._precision); + + public static BigDecimal operator ++(BigDecimal value) => value + One; + + public static BigDecimal operator --(BigDecimal value) => value - One; + + public static BigDecimal operator +(BigDecimal left, BigDecimal right) + { + if (left.IsZero) + return right; + + if (right.IsZero) + return left; + + return left._exponent > right._exponent + ? new BigDecimal(AlignMantissa(left, right) + right._mantissa, right._exponent) + : new BigDecimal(AlignMantissa(right, left) + left._mantissa, left._exponent); + } + + public static BigDecimal operator -(BigDecimal left, BigDecimal right) => left + (-right); + + public static BigDecimal operator *(BigDecimal left, BigDecimal right) + { + if (left.IsZero || right.IsZero) + return Zero; + + if (left.IsOne) + return right; + + if (right.IsOne) + return left; + + return new BigDecimal(left._mantissa * right._mantissa, left._exponent + right._exponent); + } + + public static BigDecimal operator /(BigDecimal dividend, BigDecimal divisor) + { + if (TryDivideExact(dividend, divisor, out var result)) + return result; + + return Divide(dividend, divisor, MaxExtendedDivisionPrecision); + } + + public static BigDecimal operator %(BigDecimal left, BigDecimal right) => left - (right * Floor(left / right)); + + public static bool operator ==(BigDecimal left, BigDecimal right) => left._exponent == right._exponent && left._mantissa == right._mantissa; + + public static bool operator !=(BigDecimal left, BigDecimal right) => left._exponent != right._exponent || left._mantissa != right._mantissa; + + public static bool operator <(BigDecimal left, BigDecimal right) + { + return left._exponent > right._exponent ? AlignMantissa(left, right) < right._mantissa : left._mantissa < AlignMantissa(right, left); + } + + public static bool operator >(BigDecimal left, BigDecimal right) + { + return left._exponent > right._exponent ? AlignMantissa(left, right) > right._mantissa : left._mantissa > AlignMantissa(right, left); + } + + public static bool operator <=(BigDecimal left, BigDecimal right) + { + return left._exponent > right._exponent ? AlignMantissa(left, right) <= right._mantissa : left._mantissa <= AlignMantissa(right, left); + } + + public static bool operator >=(BigDecimal left, BigDecimal right) + { + return left._exponent > right._exponent ? AlignMantissa(left, right) >= right._mantissa : left._mantissa >= AlignMantissa(right, left); + } +} \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal/BigDecimal.Parsing.cs b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Parsing.cs new file mode 100644 index 0000000..cbbb2d3 --- /dev/null +++ b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Parsing.cs @@ -0,0 +1,372 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using Singulink.Numerics.Utilities; + +namespace Singulink.Numerics; + +/// +/// Contains parsing functionality for . +/// +partial struct BigDecimal +{ + /// + public static BigDecimal Parse(string s, IFormatProvider? provider) => Parse(s.AsSpan(), provider); + + /// + public static BigDecimal Parse(string s, NumberStyles style = NumberStyles.Number, IFormatProvider? provider = null) => Parse(s.AsSpan(), style, provider); + + /// + public static BigDecimal Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s, NumberStyles.Number, provider); + + /// + /// Converts the string representation of a number to its decimal equivalent. + /// + /// The string representation of the number to convert. + /// A combination of values that indicate the styles that can be parsed. + /// A format provider that supplies culture-specific parsing information. + public static BigDecimal Parse(ReadOnlySpan s, NumberStyles style = NumberStyles.Number, IFormatProvider? provider = null) + { + if (!TryParse(s, style, provider, out var result)) + Throw.FormatEx("Input string was not in a correct format."); + + return result; + } + + /// + public static bool TryParse([NotNullWhen(true)] string? s, out BigDecimal result) => TryParse(s.AsSpan(), out result); + + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out BigDecimal result) => TryParse(s.AsSpan(), provider, out result); + + /// + public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out BigDecimal result) => TryParse(s.AsSpan(), style, provider, out result); + + /// + public static bool TryParse(ReadOnlySpan s, out BigDecimal result) => TryParse(s, NumberStyles.Number, null, out result); + + /// + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out BigDecimal result) => TryParse(s, NumberStyles.Number, provider, out result); + + /// + /// Converts the string representation of a number to its decimal equivalent. + /// + /// The string representation of the number to convert. + /// A combination of values that indicate the styles that can be parsed. + /// A format provider that supplies culture-specific parsing information. + /// The parsed decimal value if parsing was successful, otherwise zero. + /// if parsing was successful, otherwise . + public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider, out BigDecimal result) + { + if (style.HasFlag(NumberStyles.AllowHexSpecifier)) + Throw.ArgumentEx("Hex number styles are not supported.", nameof(style)); + + var formatInfo = NumberFormatInfo.GetInstance(provider); + const StringComparison cmp = StringComparison.Ordinal; + + bool allowCurrencySymbol = style.HasFlag(NumberStyles.AllowCurrencySymbol); + bool allowLeadingWhite = style.HasFlag(NumberStyles.AllowLeadingWhite); + bool allowLeadingSign = style.HasFlag(NumberStyles.AllowLeadingSign); + bool allowTrailingWhite = style.HasFlag(NumberStyles.AllowTrailingWhite); + bool allowTrailingSign = style.HasFlag(NumberStyles.AllowTrailingSign); + bool allowParenthesis = style.HasFlag(NumberStyles.AllowParentheses); + bool allowExponent = style.HasFlag(NumberStyles.AllowExponent); + bool allowDecimalPoint = style.HasFlag(NumberStyles.AllowDecimalPoint); + bool allowThousands = style.HasFlag(NumberStyles.AllowThousands); + + bool currency = false; + int sign = 0; + + Trim(ref s); + + if (TryParseParenthesis(ref s) && + TryParseStart(ref s) && + TryParseEnd(ref s) && + TryParseExponent(ref s, out int exponent) && + TryParseFractional(ref s, out var fractional) && + TryParseWhole(s, out var whole) && + (fractional.HasValue || whole.HasValue)) + { + result = fractional.GetValueOrDefault() + whole.GetValueOrDefault(); + + if (sign < 0) + result = -result; + + if (exponent is not 0) + result *= Pow10(exponent); + + return true; + } + + result = Zero; + return false; + + bool TryParseParenthesis(ref ReadOnlySpan s) + { + if (allowParenthesis && s.Length >= 3 && s[0] == '(') + { + if (s[^1] != ')') + return false; + + sign = -1; + s = s[1..^1]; + Trim(ref s); + } + + return true; + } + + bool TryParseStart(ref ReadOnlySpan s) + { + while (s.Length > 0 && !char.IsDigit(s[0]) && !s.StartsWith(formatInfo.NumberDecimalSeparator.AsSpan(), cmp)) + { + if (allowCurrencySymbol && s.StartsWith(formatInfo.CurrencySymbol.AsSpan(), cmp)) + { + if (currency) + return false; + + currency = true; + s = s[formatInfo.CurrencySymbol.Length..]; + } + else if (allowLeadingSign && StartsWithSign(s, out int parsedSign, out int signLength)) + { + if (sign is not 0) + return false; + + sign = parsedSign; + s = s[signLength..]; + } + else + { + return false; + } + + TrimStart(ref s); + } + + return true; + + bool StartsWithSign(ReadOnlySpan s, out int sign, out int signLength) + { + if (s.StartsWith(formatInfo.PositiveSign.AsSpan(), cmp)) + { + sign = 1; + signLength = formatInfo.PositiveSign.Length; + return true; + } + else if (s.StartsWith(formatInfo.NegativeSign.AsSpan(), cmp)) + { + sign = -1; + signLength = formatInfo.NegativeSign.Length; + return true; + } + + sign = 0; + signLength = 0; + return false; + } + } + + bool TryParseEnd(ref ReadOnlySpan s) + { + while (s.Length > 0 && !char.IsDigit(s[^1]) && !s.EndsWith(formatInfo.NumberDecimalSeparator.AsSpan(), cmp)) + { + if (allowCurrencySymbol && s.EndsWith(formatInfo.CurrencySymbol.AsSpan(), cmp)) + { + if (currency) + return false; + + currency = true; + s = s[..^formatInfo.CurrencySymbol.Length]; + } + else if (allowTrailingSign && EndsWithSign(s, out int parsedSign, out int signLength)) + { + if (sign is not 0) + return false; + + sign = parsedSign; + s = s[..^signLength]; + } + else + { + return false; + } + + TrimEnd(ref s); + } + + return true; + + bool EndsWithSign(ReadOnlySpan s, out int sign, out int signLength) + { + if (s.EndsWith(formatInfo.PositiveSign.AsSpan(), cmp)) + { + sign = 1; + signLength = formatInfo.PositiveSign.Length; + return true; + } + else if (s.EndsWith(formatInfo.NegativeSign.AsSpan(), cmp)) + { + sign = -1; + signLength = formatInfo.NegativeSign.Length; + return true; + } + + sign = 0; + signLength = 0; + return false; + } + } + + bool TryParseExponent(ref ReadOnlySpan s, out int result) + { + if (allowExponent) + { + int index = s.LastIndexOfAny('E', 'e'); + + if (index >= 0) + { + var e = s[(index + 1)..]; + s = s[..index]; +#if NETSTANDARD2_0 + return int.TryParse(e.ToString(), NumberStyles.AllowLeadingSign, provider, out result); +#else + return int.TryParse(e, NumberStyles.AllowLeadingSign, provider, out result); +#endif + } + } + + result = 0; + return true; + } + + bool TryParseFractional(ref ReadOnlySpan s, out BigDecimal? result) + { + if (!allowDecimalPoint || !SplitFractional(ref s, out var f)) + { + result = null; + return true; + } + + f = f.TrimEnd('0'); + + if (f.Length is 0) + { + result = Zero; + return true; + } + + int exponent = -f.Length; + f = f.TrimStart('0'); + +#if NETSTANDARD2_0 + if (!BigInteger.TryParse(f.ToString(), NumberStyles.None, provider, out var mantissa)) +#else + if (!BigInteger.TryParse(f, NumberStyles.None, provider, out var mantissa)) +#endif + { + result = null; + return false; + } + + result = new BigDecimal(mantissa, exponent, f.Length); + return true; + + bool SplitFractional(ref ReadOnlySpan s, out ReadOnlySpan f) + { + string decimalSeparator = currency ? formatInfo.CurrencyDecimalSeparator : formatInfo.NumberDecimalSeparator; + int decimalIndex = s.IndexOf(decimalSeparator.AsSpan(), cmp); + + if (decimalIndex >= 0) + { + f = s[(decimalIndex + decimalSeparator.Length)..]; + s = s[..decimalIndex]; + + return f.Length > 0; + } + + f = default; + return false; + } + } + + bool TryParseWhole(ReadOnlySpan s, out BigDecimal? result) + { + if (s.Length is 0) + { + result = null; + return true; + } + + s = s.TrimStart('0'); + + if (s.Length is 0) + { + result = Zero; + return true; + } + + int preTrimLength = s.Length; + s = s.TrimEnd('0'); + int exponent = preTrimLength - s.Length; + + var (wholeStyle, wholeFormatInfo) = GetWholeStyleAndInfo(); + +#if NETSTANDARD2_0 + if (!BigInteger.TryParse(s.ToString(), wholeStyle, wholeFormatInfo, out var mantissa)) +#else + if (!BigInteger.TryParse(s, wholeStyle, wholeFormatInfo, out var mantissa)) +#endif + { + result = null; + return false; + } + + if (allowThousands) + result = new BigDecimal(mantissa, exponent); + else + result = new BigDecimal(mantissa, exponent, s.Length); + + return true; + + (NumberStyles Style, NumberFormatInfo FormatInfo) GetWholeStyleAndInfo() + { + if (allowThousands) + { + if (currency && formatInfo.CurrencyGroupSeparator != formatInfo.NumberGroupSeparator) + { + var copy = (NumberFormatInfo)formatInfo.Clone(); + copy.NumberGroupSeparator = formatInfo.CurrencyGroupSeparator; + + return (NumberStyles.AllowThousands, copy); + } + else + { + return (NumberStyles.AllowThousands, formatInfo); + } + } + + return (NumberStyles.None, formatInfo); + } + } + + void Trim(ref ReadOnlySpan s) + { + TrimStart(ref s); + TrimEnd(ref s); + } + + void TrimStart(ref ReadOnlySpan s) + { + if (allowLeadingWhite) + s = s.TrimStart(); + } + + void TrimEnd(ref ReadOnlySpan s) + { + if (allowTrailingWhite) + s = s.TrimEnd(); + } + } +} \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal/BigDecimal.Rounding.cs b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Rounding.cs new file mode 100644 index 0000000..7be167e --- /dev/null +++ b/Source/Singulink.Numerics.BigDecimal/BigDecimal.Rounding.cs @@ -0,0 +1,99 @@ +using Singulink.Numerics.Utilities; + +namespace Singulink.Numerics; + +/// +/// Contains the rounding methods for the struct. +/// +partial struct BigDecimal +{ + /// + /// Discards any fractional digits, effectively rounding towards zero. + /// + public static BigDecimal Truncate(BigDecimal value) + { + if (value._exponent >= 0) + return value; + + return new BigDecimal(value._mantissa / BigIntegerPow10.Get(-value._exponent), 0); + } + + /// + /// Truncates the number to the given precision by removing any extra least significant digits. + /// + public static BigDecimal TruncateToPrecision(BigDecimal value, int precision) + { + if (precision < 1) + Throw.ArgumentOutOfRangeEx(nameof(precision)); + + int extraDigits = value.Precision - precision; + + if (extraDigits <= 0) + return value; + + return new BigDecimal(value._mantissa / BigIntegerPow10.Get(extraDigits), value._exponent + extraDigits); + } + + /// + /// Rounds down to the nearest integral value. + /// + public static BigDecimal Floor(BigDecimal value) + { + var result = Truncate(value); + + if (value._mantissa.Sign < 0 && value != result) + result -= 1; + + return result; + } + + /// + /// Rounds up to the nearest integral value. + /// + public static BigDecimal Ceiling(BigDecimal value) + { + var result = Truncate(value); + + if (value._mantissa.Sign > 0 && value != result) + result += 1; + + return result; + } + + /// + /// Rounds the value to the nearest integer using the given rounding mode. + /// + public static BigDecimal Round(BigDecimal value, RoundingMode mode = RoundingMode.MidpointToEven) => Round(value, 0, mode); + + /// + /// Rounds the value to the specified number of decimal places using the given rounding mode. + /// + /// + /// A negative number of decimal places indicates rounding to a whole number digit, i.e. -1 for the nearest 10, -2 for the nearest 100, etc. + /// + public static BigDecimal Round(BigDecimal value, int decimals, RoundingMode mode = RoundingMode.MidpointToEven) + { + int extraDigits = -value._exponent - decimals; + + if (extraDigits <= 0) + return value; + + return new BigDecimal(value._mantissa.Divide(BigIntegerPow10.Get(extraDigits), mode), value._exponent + extraDigits); + } + + /// + /// Rounds the value to the specified precision using the given rounding mode. + /// + public static BigDecimal RoundToPrecision(BigDecimal value, int precision, RoundingMode mode = RoundingMode.MidpointToEven) + { + if (precision < 1) + Throw.ArgumentOutOfRangeEx(nameof(precision)); + + int extraDigits = value.Precision - precision; + + if (extraDigits <= 0) + return value; + + return new BigDecimal(value._mantissa.Divide(BigIntegerPow10.Get(extraDigits), mode), value._exponent + extraDigits); + } +} \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal/BigDecimal.cs b/Source/Singulink.Numerics.BigDecimal/BigDecimal.cs index 47b30b0..4fe2a17 100644 --- a/Source/Singulink.Numerics.BigDecimal/BigDecimal.cs +++ b/Source/Singulink.Numerics.BigDecimal/BigDecimal.cs @@ -1,10 +1,7 @@ using System; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; -using System.Runtime.CompilerServices; -using System.Text; using Singulink.Numerics.Utilities; namespace Singulink.Numerics; @@ -14,38 +11,32 @@ namespace Singulink.Numerics; /// /// /// -/// All operations on values are exact except division in the case of a repeating decimal result. If the result of the division -/// cannot be exactly represented in decimal form then the largest of the dividend precision, divisor precision and the specified maximum extended precision -/// is used to represent the result. You can specify the maximum extended precision to use for each division operation by calling the method or use the / methods for division operations that are expected to return exact results. The standard -/// division operator (/) first attempts to do an exact division and falls back to extended precision division using as the maximum extended precision parameter. +/// Note that this implementation of big decimal always stores values with the minimum precision possible to accurately represent the value in order to conserve +/// memory and maintain optimal performance of operations on values. /// -/// Addition and subtraction are fully commutitive and associative for all converted data types. This makes a great data type to -/// store aggregate totals that can freely add and subtract values without accruing inaccuracies over time. +/// All operations on values are exact except division in the case of a repeating decimal result. If the result of the division cannot +/// be exactly represented in decimal form then the largest of the dividend precision, divisor precision and the specified maximum extended precision is used to +/// represent the result. You can specify the maximum extended precision to use for each division operation by calling the method. The and methods can be used for division operations that are expected to return exact results. /// -/// Conversions from floating-point types (/) default to mode in order to -/// match the behavior of floating point to conversions, but there are several conversion modes available that are each suitable in -/// different situations. You can use the or methods to -/// specify a different conversion mode. +/// The standard division operator (/) first attempts to do an exact division and falls back to extended precision division using as the maximum extended precision parameter. It is recommended that you always specify the maximum extended precision +/// instead of depending on the default of the operator. +/// +/// Addition and subtraction are fully commutative and associative for all converted data types. This makes a great data type to store +/// aggregate totals that can freely add and subtract values without accruing inaccuracies over time. +/// +/// Conversions from floating-point types (/) default to mode in order to match +/// the behavior of floating point to conversions, but there are several conversion modes available that are each suitable in different +/// situations. You can use the or methods to specify a +/// different conversion mode. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public readonly struct BigDecimal : IComparable, IEquatable, IComparable, IFormattable -#if NET7_0_OR_GREATER -#pragma warning disable SA1001 // Commas should be spaced correctly - , IFloatingPoint -#pragma warning restore SA1001 -#endif +public readonly partial struct BigDecimal : IComparable, IEquatable, IComparable { #region Static Contants/Fields/Properties - private const string ToDecimalOrFloatFormat = "R"; - private const NumberStyles ToDecimalOrFloatStyle = NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign; - - private const string FromFloatFormat = "G"; - private const NumberStyles FromFloatStyle = NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent | NumberStyles.AllowLeadingSign; - // A max size of 1024 conveniently fits all the base2 double exponent ranges needed for the base5 cache to do fast double => BigDecimal conversions // and also happens to be a good limit for the base10 cache. @@ -94,7 +85,7 @@ namespace Singulink.Numerics; /// /// Gets a value indicating whether the current value is 1. /// - public bool IsOne => _mantissa.IsOne && _exponent == 0; + public bool IsOne => _mantissa.IsOne && _exponent is 0; /// /// Gets a number indicating the sign (negative, positive, or zero) of the current value. @@ -104,7 +95,7 @@ namespace Singulink.Numerics; /// /// Gets the precision of this value, i.e. the total number of digits it contains (excluding any leading/trailing zeros). Zero values have a precision of 1. /// - public int Precision => _precision == 0 ? 1 : _precision; + public int Precision => _precision is 0 ? 1 : _precision; /// /// Gets the number of digits that appear after the decimal point. @@ -142,8 +133,9 @@ public BigDecimal(BigInteger mantissa, int exponent) } } - // Trusted private constructor - + /// + /// Initializes a new instance of the struct. Trusted private constructor. + /// private BigDecimal(BigInteger mantissa, int exponent, int precision) { _mantissa = mantissa; @@ -151,397 +143,6 @@ private BigDecimal(BigInteger mantissa, int exponent, int precision) _precision = precision; } -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - - #region Conversions to BigDecimal - - public static implicit operator BigDecimal(BigInteger value) => new BigDecimal(value, 0); - - public static implicit operator BigDecimal(byte value) => new BigDecimal(value, 0); - - public static implicit operator BigDecimal(sbyte value) => new BigDecimal(value, 0); - - public static implicit operator BigDecimal(short value) => new BigDecimal(value, 0); - - public static implicit operator BigDecimal(ushort value) => new BigDecimal(value, 0); - - public static implicit operator BigDecimal(int value) => new BigDecimal(value, 0); - - public static implicit operator BigDecimal(uint value) => new BigDecimal(value, 0); - - public static implicit operator BigDecimal(long value) => new BigDecimal(value, 0); - - public static implicit operator BigDecimal(ulong value) => new BigDecimal(value, 0); - - public static explicit operator BigDecimal(float value) => FromSingle(value); - - public static explicit operator BigDecimal(double value) => FromDouble(value); - - public static implicit operator BigDecimal(decimal value) - { - ref var decimalData = ref Unsafe.As(ref value); - - var mantissa = (new BigInteger(decimalData.Hi) << 64) | decimalData.Lo; - - if (!decimalData.IsPositive) - mantissa = -mantissa; - - return new BigDecimal(mantissa, -decimalData.Scale); - } - - #endregion - - #region Conversions from BigDecimal - - public static explicit operator BigInteger(BigDecimal value) - { - return value._exponent < 0 ? value._mantissa / BigIntegerPow10.Get(-value._exponent) : value._mantissa * BigIntegerPow10.Get(value._exponent); - } - - public static explicit operator byte(BigDecimal value) => (byte)(BigInteger)value; - - public static explicit operator sbyte(BigDecimal value) => (sbyte)(BigInteger)value; - - public static explicit operator short(BigDecimal value) => (short)(BigInteger)value; - - public static explicit operator ushort(BigDecimal value) => (ushort)(BigInteger)value; - - public static explicit operator int(BigDecimal value) => (int)(BigInteger)value; - - public static explicit operator uint(BigDecimal value) => (uint)(BigInteger)value; - - public static explicit operator long(BigDecimal value) => (long)(BigInteger)value; - - public static explicit operator ulong(BigDecimal value) => (ulong)(BigInteger)value; - - public static explicit operator float(BigDecimal value) - { - return float.Parse(value.ToString(ToDecimalOrFloatFormat), ToDecimalOrFloatStyle, CultureInfo.InvariantCulture); - } - - public static explicit operator double(BigDecimal value) - { - return double.Parse(value.ToString(ToDecimalOrFloatFormat), ToDecimalOrFloatStyle, CultureInfo.InvariantCulture); - } - - public static explicit operator decimal(BigDecimal value) - { - return decimal.Parse(value.ToString(ToDecimalOrFloatFormat), ToDecimalOrFloatStyle, CultureInfo.InvariantCulture); - } - - #endregion - - #region Mathematical Operators - - public static BigDecimal operator +(BigDecimal value) => value; - - public static BigDecimal operator -(BigDecimal value) => new(BigInteger.Negate(value._mantissa), value._exponent, value._precision); - - public static BigDecimal operator ++(BigDecimal value) => value + One; - - public static BigDecimal operator --(BigDecimal value) => value - One; - - public static BigDecimal operator +(BigDecimal left, BigDecimal right) - { - if (left.IsZero) - return right; - - if (right.IsZero) - return left; - - return left._exponent > right._exponent - ? new BigDecimal(AlignMantissa(left, right) + right._mantissa, right._exponent) - : new BigDecimal(AlignMantissa(right, left) + left._mantissa, left._exponent); - } - - public static BigDecimal operator -(BigDecimal left, BigDecimal right) => left + (-right); - - public static BigDecimal operator *(BigDecimal left, BigDecimal right) - { - if (left.IsZero || right.IsZero) - return Zero; - - if (left.IsOne) - return right; - - if (right.IsOne) - return left; - - return new BigDecimal(left._mantissa * right._mantissa, left._exponent + right._exponent); - } - - public static BigDecimal operator /(BigDecimal dividend, BigDecimal divisor) - { - if (TryDivideExact(dividend, divisor, out var result)) - return result; - - return Divide(dividend, divisor, MaxExtendedDivisionPrecision); - } - - public static BigDecimal operator %(BigDecimal left, BigDecimal right) => left - (right * Floor(left / right)); - - public static bool operator ==(BigDecimal left, BigDecimal right) => left._exponent == right._exponent && left._mantissa == right._mantissa; - - public static bool operator !=(BigDecimal left, BigDecimal right) => left._exponent != right._exponent || left._mantissa != right._mantissa; - - public static bool operator <(BigDecimal left, BigDecimal right) - { - return left._exponent > right._exponent ? AlignMantissa(left, right) < right._mantissa : left._mantissa < AlignMantissa(right, left); - } - - public static bool operator >(BigDecimal left, BigDecimal right) - { - return left._exponent > right._exponent ? AlignMantissa(left, right) > right._mantissa : left._mantissa > AlignMantissa(right, left); - } - - public static bool operator <=(BigDecimal left, BigDecimal right) - { - return left._exponent > right._exponent ? AlignMantissa(left, right) <= right._mantissa : left._mantissa <= AlignMantissa(right, left); - } - - public static bool operator >=(BigDecimal left, BigDecimal right) - { - return left._exponent > right._exponent ? AlignMantissa(left, right) >= right._mantissa : left._mantissa >= AlignMantissa(right, left); - } - - #endregion - -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - - #region Conversion Methods - -#if NET7_0_OR_GREATER - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BigDecimal CreateChecked(TOther value) - where TOther : INumberBase - { - BigDecimal result; - - if (typeof(TOther) == typeof(BigDecimal)) - result = (BigDecimal)(object)value; - else if (!TryConvertFromChecked(value, out result) && !TOther.TryConvertToChecked(value, out result)) - Throw.NotSupportedEx(); - - return result; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BigDecimal CreateSaturating(TOther value) - where TOther : INumberBase - { - BigDecimal result; - - if (typeof(TOther) == typeof(BigDecimal)) - result = (BigDecimal)(object)value; - else if (!TryConvertFrom(value, out result) && !TOther.TryConvertToSaturating(value, out result)) - Throw.NotSupportedEx(); - - return result; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BigDecimal CreateTruncating(TOther value) - where TOther : INumberBase - { - BigDecimal result; - - if (typeof(TOther) == typeof(BigDecimal)) - result = (BigDecimal)(object)value; - else if (!TryConvertFrom(value, out result) && !TOther.TryConvertToTruncating(value, out result)) - Throw.NotSupportedEx(); - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryConvertFromChecked(TOther value, out BigDecimal result) where TOther : INumberBase - { - if (typeof(TOther) == typeof(float)) - { - result = (BigDecimal)(float)(object)value; - return true; - } - else if (typeof(TOther) == typeof(double)) - { - result = (BigDecimal)(double)(object)value; - return true; - } - else if (typeof(TOther) == typeof(decimal)) - { - result = (BigDecimal)(decimal)(object)value; - return true; - } - else if (typeof(TOther).IsAssignableTo(typeof(IBinaryInteger<>))) - { - if (TOther.TryConvertToChecked(value, out var intValue)) - { - result = (BigDecimal)intValue; - return true; - } - } - - result = default; - return false; - } - - private static bool TryConvertFrom(TOther value, out BigDecimal result) where TOther : INumberBase - { - if (typeof(TOther) == typeof(float)) - { - float actualValue = (float)(object)value; - result = float.IsNaN(actualValue) ? Zero : (BigDecimal)actualValue; - return true; - } - else if (typeof(TOther) == typeof(double)) - { - double actualValue = (double)(object)value; - result = double.IsNaN(actualValue) ? Zero : (BigDecimal)actualValue; - return true; - } - else if (typeof(TOther) == typeof(decimal)) - { - result = (BigDecimal)(decimal)(object)value; - return true; - } - else if (TOther.IsInteger(value)) - { - if (TOther.TryConvertToChecked(value, out var integer)) - { - result = (BigDecimal)integer; - return true; - } - } - - result = default; - return false; - } - -#endif - - /// - /// Gets a representation of a value. - /// - public static BigDecimal FromSingle(float value, FloatConversion conversionMode = FloatConversion.Truncate) - { - return conversionMode switch - { - FloatConversion.Roundtrip => FromFloat(value, 9), - FloatConversion.Truncate => FromFloat(value, 7), - FloatConversion.Exact => FromFloat(value, 0), - FloatConversion.ParseString => Parse(value.ToString(FromFloatFormat, CultureInfo.InvariantCulture).AsSpan(), FromFloatStyle, CultureInfo.InvariantCulture), - _ => Throw.ArgumentOutOfRangeEx(nameof(conversionMode)), - }; - } - - /// - /// Gets a representation of a value. - /// - public static BigDecimal FromDouble(double value, FloatConversion conversionMode = FloatConversion.Truncate) - { - return conversionMode switch - { - FloatConversion.Roundtrip => FromFloat(value, 17), - FloatConversion.Truncate => FromFloat(value, 15), - FloatConversion.Exact => FromFloat(value, 0), - FloatConversion.ParseString => Parse(value.ToString(FromFloatFormat, CultureInfo.InvariantCulture).AsSpan(), FromFloatStyle, CultureInfo.InvariantCulture), - _ => Throw.ArgumentOutOfRangeEx(nameof(conversionMode)), - }; - } - - private static BigDecimal FromFloat(double value, int precision) - { - if (double.IsNaN(value)) - Throw.ArgumentOutOfRangeEx(nameof(value), "Floating point NaN values cannot be converted to BigDecimal."); - - if (double.IsNegativeInfinity(value) || double.IsPositiveInfinity(value)) - Throw.ArgumentOutOfRangeEx(nameof(value), "Floating point infinity values cannot be converted to BigDecimal."); - - Debug.Assert(precision is 0 or 7 or 9 or 15 or 17, "unexpected precision value"); - - unchecked - { - // Based loosely on Jon Skeet's DoubleConverter: - - long bits = BitConverter.DoubleToInt64Bits(value); - bool negative = bits < 0; - int exponent = (int)((bits >> 52) & 0x7ffL); - long mantissa = bits & 0xfffffffffffffL; - - // Subnormal numbers: exponent is effectively one higher, but there's no extra normalisation bit in the mantissa Normal numbers. - // Leave exponent as it is but add extra bit to the front of the mantissa - if (exponent == 0) - exponent++; - else - mantissa |= 1L << 52; - - if (mantissa == 0) - return Zero; - - // Bias the exponent. It's actually biased by 1023, but we're treating the mantissa as m.0 rather than 0.m, so we need to subtract another 52 - // from it. - exponent -= 1075; - - // Normalize - - while ((mantissa & 1) == 0) - { - // mantissa is even - mantissa >>= 1; - exponent++; - } - - if (negative) - mantissa = -mantissa; - - var resultMantissa = (BigInteger)mantissa; - int resultExponent; - bool trimTrailingZeros; - - if (exponent == 0) - { - resultExponent = 0; - trimTrailingZeros = false; - } - else if (exponent < 0) - { - resultMantissa *= BigIntegerPow5.Get(-exponent); - resultExponent = exponent; - trimTrailingZeros = false; - } - else - { - // exponent > 0 - resultMantissa <<= exponent; // *= BigInteger.Pow(BigInt2, exponent); - resultExponent = 0; - trimTrailingZeros = true; - } - - if (precision > 0) - { - int digits = resultMantissa.CountDigits(); - int extraDigits = digits - precision; - - if (extraDigits <= 0) - return new BigDecimal(resultMantissa, resultExponent, digits); - - resultMantissa = resultMantissa.Divide(BigIntegerPow10.Get(extraDigits)); - resultExponent += extraDigits; - trimTrailingZeros = true; - } - - if (trimTrailingZeros) - return new BigDecimal(resultMantissa, resultExponent); - - return new BigDecimal(resultMantissa, resultExponent, resultMantissa.CountDigits()); - } - } - - #endregion - #region Mathematical Functions /// @@ -655,7 +256,7 @@ public static bool TryDivideExact(BigDecimal dividend, BigDecimal divisor, out B /// /// Returns ten (10) raised to the specified exponent. /// - public static BigDecimal Pow10(int exponent) => exponent == 0 ? One : new BigDecimal(BigInteger.One, exponent, 1); + public static BigDecimal Pow10(int exponent) => exponent is 0 ? One : new BigDecimal(BigInteger.One, exponent, 1); /// /// Determines whether a value represents an integral value. @@ -665,12 +266,12 @@ public static bool TryDivideExact(BigDecimal dividend, BigDecimal divisor, out B /// /// Determines whether a value represents an odd integral value. /// - public static bool IsOddInteger(BigDecimal value) => value._exponent == 0 && !value._mantissa.IsEven; + public static bool IsOddInteger(BigDecimal value) => value._exponent is 0 && !value._mantissa.IsEven; /// /// Determines whether a value represents an even integral value. /// - public static bool IsEvenInteger(BigDecimal value) => value._exponent > 0 || (value._exponent == 0 && value._mantissa.IsEven); + public static bool IsEvenInteger(BigDecimal value) => value._exponent > 0 || (value._exponent is 0 && value._mantissa.IsEven); /// /// Determines if a value is negative. @@ -726,1101 +327,52 @@ public static BigDecimal MinMagnitude(BigDecimal x, BigDecimal y) #endregion - #region Rounding Functions - - /// - /// Discards any fractional digits, effectively rounding towards zero. - /// - public static BigDecimal Truncate(BigDecimal value) - { - if (value._exponent >= 0) - return value; - - return new BigDecimal(value._mantissa / BigIntegerPow10.Get(-value._exponent), 0); - } - - /// - /// Truncates the number to the given precision by removing any extra least significant digits. - /// - public static BigDecimal TruncateToPrecision(BigDecimal value, int precision) - { - if (precision < 1) - Throw.ArgumentOutOfRangeEx(nameof(precision)); - - int extraDigits = value.Precision - precision; - - if (extraDigits <= 0) - return value; - - return new BigDecimal(value._mantissa / BigIntegerPow10.Get(extraDigits), value._exponent + extraDigits); - } + #region Equality and Comparison Methods /// - /// Rounds down to the nearest integral value. + /// Compares this to another . /// - public static BigDecimal Floor(BigDecimal value) + public int CompareTo(BigDecimal other) { - var result = Truncate(value); - - if (value._mantissa.Sign < 0 && value != result) - result -= 1; - - return result; + return _exponent > other._exponent ? AlignMantissa(this, other).CompareTo(other._mantissa) : _mantissa.CompareTo(AlignMantissa(other, this)); } - /// - /// Rounds up to the nearest integral value. - /// - public static BigDecimal Ceiling(BigDecimal value) + /// + int IComparable.CompareTo(object? obj) { - var result = Truncate(value); - - if (value._mantissa.Sign > 0 && value != result) - result += 1; + if (obj is null) + return 1; - return result; + return CompareTo((BigDecimal)obj); } /// - /// Rounds the value to the nearest integer using the given rounding mode. + /// Indicates whether this value and the specified other value are equal. /// - public static BigDecimal Round(BigDecimal value, RoundingMode mode = RoundingMode.MidpointToEven) => Round(value, 0, mode); + public bool Equals(BigDecimal other) => other._mantissa.Equals(_mantissa) && other._exponent == _exponent; /// - /// Rounds the value to the specified number of decimal places using the given rounding mode. + /// Indicates whether this value and the specified object are equal. /// - /// - /// A negative number of decimal places indicates rounding to a whole number digit, i.e. -1 for the nearest 10, -2 for the nearest 100, etc. - /// - public static BigDecimal Round(BigDecimal value, int decimals, RoundingMode mode = RoundingMode.MidpointToEven) - { - int extraDigits = -value._exponent - decimals; - - if (extraDigits <= 0) - return value; - - return new BigDecimal(value._mantissa.Divide(BigIntegerPow10.Get(extraDigits), mode), value._exponent + extraDigits); - } + public override bool Equals(object? obj) => obj is BigDecimal bigDecimal && Equals(bigDecimal); /// - /// Rounds the value to the specified precision using the given rounding mode. + /// Returns the hash code for this value. /// - public static BigDecimal RoundToPrecision(BigDecimal value, int precision, RoundingMode mode = RoundingMode.MidpointToEven) - { - if (precision < 1) - Throw.ArgumentOutOfRangeEx(nameof(precision)); - - int extraDigits = value.Precision - precision; - - if (extraDigits <= 0) - return value; - - return new BigDecimal(value._mantissa.Divide(BigIntegerPow10.Get(extraDigits), mode), value._exponent + extraDigits); - } + public override int GetHashCode() => HashCode.Combine(_mantissa, _exponent); #endregion - #region String Conversion Methods - - /// - /// Converts the string representation of a number to its decimal equivalent. - /// - /// The string representation of the number to convert. - /// A format provider that supplies culture-specific parsing information. - public static BigDecimal Parse(string s, IFormatProvider? provider) => Parse(s.AsSpan(), provider); - - /// - /// Converts the string representation of a number to its decimal equivalent. - /// - /// The string representation of the number to convert. - /// A combination of values that indicate the styles that can be parsed. - /// A format provider that supplies culture-specific parsing information. - public static BigDecimal Parse(string s, NumberStyles style = NumberStyles.Number, IFormatProvider? provider = null) => Parse(s.AsSpan(), style, provider); - - /// - /// Converts the string representation of a number to its decimal equivalent. - /// - /// The string representation of the number to convert. - /// A format provider that supplies culture-specific parsing information. - /// The parsed decimal value if parsing was successful, otherwise zero. - /// if parsing was successful, otherwise . - public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out BigDecimal result) => TryParse(s.AsSpan(), provider, out result); - - /// - /// Converts the string representation of a number to its decimal equivalent. - /// - /// The string representation of the number to convert. - /// A combination of values that indicate the styles that can be parsed. - /// A format provider that supplies culture-specific parsing information. - /// The parsed decimal value if parsing was successful, otherwise zero. - /// if parsing was successful, otherwise . - public static bool TryParse(string? s, NumberStyles style, IFormatProvider? provider, out BigDecimal result) => TryParse(s.AsSpan(), style, provider, out result); - - /// - /// Converts the string representation of a number to its decimal equivalent. - /// - /// The string representation of the number to convert. - /// A format provider that supplies culture-specific parsing information. - public static BigDecimal Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s, NumberStyles.Number, provider); + #region Helper Methods /// - /// Converts the string representation of a number to its decimal equivalent. + /// Returns the mantissa of value, aligned to the reference exponent. Assumes the value exponent is larger than the reference exponent. /// - /// The string representation of the number to convert. - /// A combination of values that indicate the styles that can be parsed. - /// A format provider that supplies culture-specific parsing information. - public static BigDecimal Parse(ReadOnlySpan s, NumberStyles style = NumberStyles.Number, IFormatProvider? provider = null) + private static BigInteger AlignMantissa(BigDecimal value, BigDecimal reference) { - if (!TryParse(s, style, provider, out var result)) - Throw.FormatEx("Input string was not in a correct format."); - - return result; - } - - /// - /// Converts the string representation of a number to its decimal equivalent. - /// - /// The string representation of the number to convert. - /// The parsed decimal value if parsing was successful, otherwise zero. - /// if parsing was successful, otherwise . - public static bool TryParse(ReadOnlySpan s, out BigDecimal result) => TryParse(s, NumberStyles.Number, null, out result); - - /// - /// Converts the string representation of a number to its decimal equivalent. - /// - /// The string representation of the number to convert. - /// A format provider that supplies culture-specific parsing information. - /// The parsed decimal value if parsing was successful, otherwise zero. - /// if parsing was successful, otherwise . - public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out BigDecimal result) => TryParse(s, NumberStyles.Number, provider, out result); - - /// - /// Converts the string representation of a number to its decimal equivalent. - /// - /// The string representation of the number to convert. - /// A combination of values that indicate the styles that can be parsed. - /// A format provider that supplies culture-specific parsing information. - /// The parsed decimal value if parsing was successful, otherwise zero. - /// if parsing was successful, otherwise . - public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider, out BigDecimal result) - { - if (style.HasFlag(NumberStyles.AllowHexSpecifier)) - Throw.ArgumentEx("Hex number styles are not supported.", nameof(style)); - - var formatInfo = NumberFormatInfo.GetInstance(provider); - const StringComparison cmp = StringComparison.Ordinal; - - bool allowCurrencySymbol = style.HasFlag(NumberStyles.AllowCurrencySymbol); - bool allowLeadingWhite = style.HasFlag(NumberStyles.AllowLeadingWhite); - bool allowLeadingSign = style.HasFlag(NumberStyles.AllowLeadingSign); - bool allowTrailingWhite = style.HasFlag(NumberStyles.AllowTrailingWhite); - bool allowTrailingSign = style.HasFlag(NumberStyles.AllowTrailingSign); - bool allowParenthesis = style.HasFlag(NumberStyles.AllowParentheses); - bool allowExponent = style.HasFlag(NumberStyles.AllowExponent); - bool allowDecimalPoint = style.HasFlag(NumberStyles.AllowDecimalPoint); - bool allowThousands = style.HasFlag(NumberStyles.AllowThousands); - - bool currency = false; - int sign = 0; - - Trim(ref s); - - if (TryParseParenthesis(ref s) && - TryParseStart(ref s) && - TryParseEnd(ref s) && - TryParseExponent(ref s, out int exponent) && - TryParseFractional(ref s, out var fractional) && - TryParseWhole(s, out var whole) && - (fractional.HasValue || whole.HasValue)) - { - result = fractional.GetValueOrDefault() + whole.GetValueOrDefault(); - - if (sign < 0) - result = -result; - - if (exponent != 0) - result *= Pow10(exponent); - - return true; - } - - result = Zero; - return false; - - bool TryParseParenthesis(ref ReadOnlySpan s) - { - if (allowParenthesis && s.Length >= 3 && s[0] == '(') - { - if (s[^1] != ')') - return false; - - sign = -1; - s = s[1..^1]; - Trim(ref s); - } - - return true; - } - - bool TryParseStart(ref ReadOnlySpan s) - { - while (s.Length > 0 && !char.IsDigit(s[0]) && !s.StartsWith(formatInfo.NumberDecimalSeparator.AsSpan(), cmp)) - { - if (allowCurrencySymbol && s.StartsWith(formatInfo.CurrencySymbol.AsSpan(), cmp)) - { - if (currency) - return false; - - currency = true; - s = s[formatInfo.CurrencySymbol.Length..]; - } - else if (allowLeadingSign && StartsWithSign(s, out int parsedSign, out int signLength)) - { - if (sign != 0) - return false; - - sign = parsedSign; - s = s[signLength..]; - } - else - { - return false; - } - - TrimStart(ref s); - } - - return true; - - bool StartsWithSign(ReadOnlySpan s, out int sign, out int signLength) - { - if (s.StartsWith(formatInfo.PositiveSign.AsSpan(), cmp)) - { - sign = 1; - signLength = formatInfo.PositiveSign.Length; - return true; - } - else if (s.StartsWith(formatInfo.NegativeSign.AsSpan(), cmp)) - { - sign = -1; - signLength = formatInfo.NegativeSign.Length; - return true; - } - - sign = 0; - signLength = 0; - return false; - } - } - - bool TryParseEnd(ref ReadOnlySpan s) - { - while (s.Length > 0 && !char.IsDigit(s[^1]) && !s.EndsWith(formatInfo.NumberDecimalSeparator.AsSpan(), cmp)) - { - if (allowCurrencySymbol && s.EndsWith(formatInfo.CurrencySymbol.AsSpan(), cmp)) - { - if (currency) - return false; - - currency = true; - s = s[..^formatInfo.CurrencySymbol.Length]; - } - else if (allowTrailingSign && EndsWithSign(s, out int parsedSign, out int signLength)) - { - if (sign != 0) - return false; - - sign = parsedSign; - s = s[..^signLength]; - } - else - { - return false; - } - - TrimEnd(ref s); - } - - return true; - - bool EndsWithSign(ReadOnlySpan s, out int sign, out int signLength) - { - if (s.EndsWith(formatInfo.PositiveSign.AsSpan(), cmp)) - { - sign = 1; - signLength = formatInfo.PositiveSign.Length; - return true; - } - else if (s.EndsWith(formatInfo.NegativeSign.AsSpan(), cmp)) - { - sign = -1; - signLength = formatInfo.NegativeSign.Length; - return true; - } - - sign = 0; - signLength = 0; - return false; - } - } - - bool TryParseExponent(ref ReadOnlySpan s, out int result) - { - if (allowExponent) - { - int index = s.LastIndexOfAny('E', 'e'); - - if (index >= 0) - { - var e = s[(index + 1)..]; - s = s[..index]; -#if NETSTANDARD2_0 - return int.TryParse(e.ToString(), NumberStyles.AllowLeadingSign, provider, out result); -#else - return int.TryParse(e, NumberStyles.AllowLeadingSign, provider, out result); -#endif - } - } - - result = 0; - return true; - } - - bool TryParseFractional(ref ReadOnlySpan s, out BigDecimal? result) - { - if (!allowDecimalPoint || !SplitFractional(ref s, out var f)) - { - result = null; - return true; - } - - f = f.TrimEnd('0'); - - if (f.Length == 0) - { - result = Zero; - return true; - } - - int exponent = -f.Length; - f = f.TrimStart('0'); - -#if NETSTANDARD2_0 - if (!BigInteger.TryParse(f.ToString(), NumberStyles.None, provider, out var mantissa)) -#else - if (!BigInteger.TryParse(f, NumberStyles.None, provider, out var mantissa)) -#endif - { - result = null; - return false; - } - - result = new BigDecimal(mantissa, exponent, f.Length); - return true; - - bool SplitFractional(ref ReadOnlySpan s, out ReadOnlySpan f) - { - string decimalSeparator = currency ? formatInfo.CurrencyDecimalSeparator : formatInfo.NumberDecimalSeparator; - int decimalIndex = s.IndexOf(decimalSeparator.AsSpan(), cmp); - - if (decimalIndex >= 0) - { - f = s[(decimalIndex + decimalSeparator.Length)..]; - s = s[..decimalIndex]; - - return f.Length > 0; - } - - f = default; - return false; - } - } - - bool TryParseWhole(ReadOnlySpan s, out BigDecimal? result) - { - if (s.Length == 0) - { - result = null; - return true; - } - - s = s.TrimStart('0'); - - if (s.Length == 0) - { - result = Zero; - return true; - } - - int preTrimLength = s.Length; - s = s.TrimEnd('0'); - int exponent = preTrimLength - s.Length; - - var (wholeStyle, wholeFormatInfo) = GetWholeStyleAndInfo(); - -#if NETSTANDARD2_0 - if (!BigInteger.TryParse(s.ToString(), wholeStyle, wholeFormatInfo, out var mantissa)) -#else - if (!BigInteger.TryParse(s, wholeStyle, wholeFormatInfo, out var mantissa)) -#endif - { - result = null; - return false; - } - - if (allowThousands) - result = new BigDecimal(mantissa, exponent); - else - result = new BigDecimal(mantissa, exponent, s.Length); - - return true; - - (NumberStyles Style, NumberFormatInfo FormatInfo) GetWholeStyleAndInfo() - { - if (allowThousands) - { - if (currency && formatInfo.CurrencyGroupSeparator != formatInfo.NumberGroupSeparator) - { - var copy = (NumberFormatInfo)formatInfo.Clone(); - copy.NumberGroupSeparator = formatInfo.CurrencyGroupSeparator; - - return (NumberStyles.AllowThousands, copy); - } - else - { - return (NumberStyles.AllowThousands, formatInfo); - } - } - - return (NumberStyles.None, formatInfo); - } - } - - void Trim(ref ReadOnlySpan s) - { - TrimStart(ref s); - TrimEnd(ref s); - } - - void TrimStart(ref ReadOnlySpan s) - { - if (allowLeadingWhite) - s = s.TrimStart(); - } - - void TrimEnd(ref ReadOnlySpan s) - { - if (allowTrailingWhite) - s = s.TrimEnd(); - } - } - - /// - /// Returns a full-precision decimal form string representation of this value using the current culture. - /// - public override string ToString() => ToString(null); - - /// - /// Returns a full-precision decimal form string representation of this value. - /// - /// The format provider that will be used to obtain number format information. The current culture is used if none is - /// provided. - public string ToString(IFormatProvider? formatProvider) => ToString(null, formatProvider); - - /// - /// Returns a string representation of this value. - /// - /// The string format to use. The "G" format is used if none is provided. - /// The format provider that will be used to obtain number format information. The current culture is used if none is - /// provided. - /// - /// String format is composed of a format specifier followed by an optional precision specifier. - /// Format specifiers: - /// - /// - /// Specifier - /// Name - /// Description - /// - /// - /// "G" - /// General - /// Default format specifier if none is provided. Precision specifier determines the number of significant digits. If the precision - /// specifier is omitted then the value is written out in full precision decimal form. If a precision specifier is provided then the - /// more compact of either decimal form or scientific notation is used. - /// - /// - /// "F" - /// Fixed-point - /// Precision specifier determines the number of decimal digits. Default value is . - /// - /// - /// "N" - /// Number - /// Like fixed-point, but also outputs group separators. Precision specifier determines the number of decimal digits. Default value is . - /// - /// - /// "E" - /// Exponential - /// Exponential (scientific) notation. Precision specifier determines the number of decimal digits. - /// - /// - /// "C" - /// Currency - /// Precision specifier determines the number of decimal digits. Default value is . - /// - /// - /// "P" - /// Percentage - /// Precision specifier determines the number of decimal digits. Default value is . - /// - /// - /// "R" - /// Round-trip - /// Outputs the mantissa followed by E and then the exponent, always using the . - /// - /// - /// - public string ToString(string? format, IFormatProvider? formatProvider = null) - { - format = format?.Trim(); - var formatInfo = NumberFormatInfo.GetInstance(formatProvider); - - char formatSpecifier; - int? precisionSpecifier = null; - - if (string.IsNullOrEmpty(format)) - { - formatSpecifier = 'G'; - } - else - { - formatSpecifier = char.ToUpperInvariant(format![0]); - - if (format.Length > 1) - { -#if NETSTANDARD2_0 - if (int.TryParse(format[1..], NumberStyles.None, CultureInfo.InvariantCulture, out int ps)) -#else - if (int.TryParse(format.AsSpan()[1..], NumberStyles.None, CultureInfo.InvariantCulture, out int ps)) -#endif - precisionSpecifier = ps; - else - Throw.FormatEx($"Invalid precision specifier: '{format[1..]}'"); - } - } - - if (formatSpecifier == 'G') - { - BigDecimal value; - - if (precisionSpecifier == null || precisionSpecifier.GetValueOrDefault() == 0) - { - value = this; - } - else - { - int precision = precisionSpecifier.GetValueOrDefault(); - value = RoundToPrecision(this, precision, RoundingMode.MidpointAwayFromZero); - - if (GetEstimatedFullDecimalLength(value) > GetEstimatedExponentialLength(value)) - { - int exponentDecimals = Math.Min(value.Precision, precision) - 1; - return GetExponentialString(value, exponentDecimals); - } - } - - if (value._exponent >= 0) - return GetIntegerString(value, "G"); - - return GetDecimalString(value, "G", null); - } - - if (formatSpecifier == 'F' || formatSpecifier == 'N') - { - string wholePartFormat = formatSpecifier == 'F' ? "F0" : "N0"; - - int decimals = precisionSpecifier.HasValue ? precisionSpecifier.GetValueOrDefault() : formatInfo.NumberDecimalDigits; - var value = Round(this, decimals, RoundingMode.MidpointAwayFromZero); - - if (decimals == 0) - return GetIntegerString(value, wholePartFormat); - - return GetDecimalString(value, wholePartFormat, decimals); - } - - if (formatSpecifier == 'E') - return GetExponentialString(this, precisionSpecifier); - - if (formatSpecifier == 'C' || formatSpecifier == 'P') - { - BigDecimal value = this; - - if (formatSpecifier == 'P') - { - // Convert percentage format info params to currency params and write it out as a currency value: - - formatInfo = new NumberFormatInfo() - { - CurrencySymbol = formatInfo.PercentSymbol, - CurrencyDecimalDigits = formatInfo.PercentDecimalDigits, - CurrencyDecimalSeparator = formatInfo.PercentDecimalSeparator, - CurrencyGroupSeparator = formatInfo.PercentGroupSeparator, - CurrencyGroupSizes = formatInfo.PercentGroupSizes, - CurrencyPositivePattern = PositivePercentagePatternToCurrencyPattern(formatInfo.PercentPositivePattern), - CurrencyNegativePattern = NegativePercentagePatternToCurrencyPattern(formatInfo.PercentNegativePattern), - }; - - value *= 100; - } - - int decimals = precisionSpecifier.HasValue ? precisionSpecifier.GetValueOrDefault() : formatInfo.CurrencyDecimalDigits; - value = Round(value, decimals, RoundingMode.MidpointAwayFromZero); - - if (decimals == 0) - return GetIntegerString(value, "C0"); - - return GetDecimalString(value, "C0", decimals); - } - - if (formatSpecifier == 'R') - { - if (_exponent == 0) - return _mantissa.ToString(CultureInfo.InvariantCulture); - - return ((FormattableString)$"{_mantissa}E{_exponent}").ToString(CultureInfo.InvariantCulture); - } - - Throw.FormatEx($"Format specifier was invalid: '{formatSpecifier}'."); - return default; - - static int GetEstimatedFullDecimalLength(BigDecimal value) - { - if (value._exponent >= 0) - return value.Precision + value._exponent; - - return value.Precision + Math.Max(0, -value._exponent - value.Precision) + 1; // digits + additional leading zeros + decimal separator - } - - static int GetEstimatedExponentialLength(BigDecimal value) => value.Precision + 5; // .E+99 - - string GetExponentialString(BigDecimal value, int? precisionSpecifier) - { - string result = value._mantissa.ToString("E" + precisionSpecifier, formatInfo); - - if (value._exponent == 0) - return result; - - int eIndex = result.LastIndexOf("E", StringComparison.Ordinal); - -#if NETSTANDARD2_0 - string exponentString = result[(eIndex + 1)..]; -#else - var exponentString = result.AsSpan()[(eIndex + 1)..]; -#endif - int exponent = int.Parse(exponentString, NumberStyles.AllowLeadingSign, formatInfo) + value._exponent; - var mantissa = result.AsSpan()[..(eIndex + 1)]; - string absExponentString = Math.Abs(exponent).ToString(formatInfo); - - if (exponent > 0) - return StringHelper.Concat(mantissa, formatInfo.PositiveSign.AsSpan(), absExponentString.AsSpan()); - - return StringHelper.Concat(mantissa, formatInfo.NegativeSign.AsSpan(), absExponentString.AsSpan()); - } - - string GetDecimalString(BigDecimal value, string wholePartFormat, int? fixedDecimalPlaces) - { - var wholePart = Truncate(value); - string wholeString; - - if (wholePart.IsZero && value.Sign < 0) - wholeString = (-1).ToString(wholePartFormat, formatInfo).Replace('1', '0'); - else - wholeString = GetIntegerString(wholePart, wholePartFormat); - - var decimalPart = Abs(value - wholePart); - int decimalPartShift = -decimalPart._exponent; - int decimalLeadingZeros = decimalPart.IsZero ? 0 : decimalPartShift - decimalPart.Precision; - int decimalTrailingZeros = 0; - - if (fixedDecimalPlaces.HasValue) - decimalTrailingZeros = Math.Max(0, fixedDecimalPlaces.GetValueOrDefault() - decimalPart.Precision - decimalLeadingZeros); - - decimalPart = decimalPart._mantissa; - Debug.Assert(decimalPart._exponent == 0, "unexpected transformed decimal part exponent"); - - string decimalString = GetIntegerString(decimalPart, "G"); - - int insertPoint; - - for (insertPoint = wholeString.Length; insertPoint > 0; insertPoint--) - { - if (char.IsDigit(wholeString[insertPoint - 1])) - break; - } - - string decimalSeparator = wholePartFormat[0] == 'C' ? formatInfo.CurrencyDecimalSeparator : formatInfo.NumberDecimalSeparator; - - var sb = new StringBuilder(wholeString.Length + decimalSeparator.Length + decimalLeadingZeros + decimalString.Length); -#if NETSTANDARD2_0 - sb.Append(wholeString[..insertPoint]); -#else - sb.Append(wholeString.AsSpan()[..insertPoint]); -#endif - sb.Append(decimalSeparator); - sb.Append('0', decimalLeadingZeros); - sb.Append(decimalString); - sb.Append('0', decimalTrailingZeros); -#if NETSTANDARD2_0 - sb.Append(wholeString[insertPoint..]); -#else - sb.Append(wholeString.AsSpan()[insertPoint..]); -#endif - - return sb.ToString(); - } - - string GetIntegerString(BigDecimal value, string format) - { - Debug.Assert(value._exponent >= 0, "value contains decimal digits"); - BigInteger intValue = value._mantissa; - - if (value._exponent > 0) - intValue *= BigIntegerPow10.Get(value._exponent); - - return intValue.ToString(format, formatInfo); - } - - static int PositivePercentagePatternToCurrencyPattern(int positivePercentagePattern) => positivePercentagePattern switch - { - 0 => 3, - 1 => 1, - 2 => 0, - 3 => 2, - _ => Throw.NotSupportedEx("Unsupported positive percentage pattern."), - }; - - static int NegativePercentagePatternToCurrencyPattern(int negativePercentagePattern) => negativePercentagePattern switch - { - 0 => 8, - 1 => 5, - 2 => 1, - 3 => 2, - 4 => 3, - 5 => 6, - 6 => 7, - 7 => 9, - 8 => 10, - 9 => 11, - 10 => 12, - 11 => 13, - _ => Throw.NotSupportedEx("Unsupported negative percentage pattern."), - }; - } - - #endregion - - #region Equality and Comparison Methods - - /// - /// Compares this to another . - /// - public int CompareTo(BigDecimal other) - { - return _exponent > other._exponent ? AlignMantissa(this, other).CompareTo(other._mantissa) : _mantissa.CompareTo(AlignMantissa(other, this)); - } - - /// - int IComparable.CompareTo(object? obj) - { - if (obj == null) - return 1; - - return CompareTo((BigDecimal)obj); - } - - /// - /// Indicates whether this value and the specified other value are equal. - /// - public bool Equals(BigDecimal other) => other._mantissa.Equals(_mantissa) && other._exponent == _exponent; - - /// - /// Indicates whether this value and the specified object are equal. - /// - public override bool Equals(object? obj) => obj is BigDecimal bigDecimal && Equals(bigDecimal); - - /// - /// Returns the hash code for this value. - /// - public override int GetHashCode() => HashCode.Combine(_mantissa, _exponent); - - #endregion - - #region Helper Methods - - /// - /// Returns the mantissa of value, aligned to the reference exponent. Assumes the value exponent is larger than the reference exponent. - /// - private static BigInteger AlignMantissa(BigDecimal value, BigDecimal reference) - { - Debug.Assert(value._exponent >= reference._exponent, "value exponent must be greater than or equal to reference exponent"); - return value._mantissa * BigIntegerPow10.Get(value._exponent - reference._exponent); - } - - #endregion - -#if NET7_0_OR_GREATER - - #region Explicit Generic Math Implementations - - private static BigDecimal _e; - private static BigDecimal _pi; - private static BigDecimal _tau; - - /// - static BigDecimal IFloatingPointConstants.E - { - get { - if (_e.IsZero) - _e = Parse("2.7182818284590452353602874713526624977572470936999"); - - return _e; - } - } - - /// - static BigDecimal IFloatingPointConstants.Pi - { - get { - if (_pi.IsZero) - _pi = Parse("3.1415926535897932384626433832795028841971693993751"); - - return _pi; - } - } - - /// - static BigDecimal IFloatingPointConstants.Tau - { - get { - if (_tau.IsZero) - _tau = Parse("6.2831853071795864769252867665590057683943387987502"); - - return _tau; - } - } - - /// - static BigDecimal ISignedNumber.NegativeOne => MinusOne; - - /// - static int INumberBase.Radix => 10; - - /// - static BigDecimal IAdditiveIdentity.AdditiveIdentity => Zero; - - /// - static BigDecimal IMultiplicativeIdentity.MultiplicativeIdentity => One; - - /// - int IFloatingPoint.GetExponentByteCount() => sizeof(int); - - /// - int IFloatingPoint.GetExponentShortestBitLength() => ((IBinaryInteger)_exponent).GetShortestBitLength(); - - /// - int IFloatingPoint.GetSignificandBitLength() => _mantissa.GetByteCount(false) * 8; - - /// - int IFloatingPoint.GetSignificandByteCount() => _mantissa.GetByteCount(); - - /// - static BigDecimal IFloatingPoint.Round(BigDecimal x, int digits, MidpointRounding mode) => Round(x, digits, (RoundingMode)mode); - - /// - bool IFloatingPoint.TryWriteExponentBigEndian(Span destination, out int bytesWritten) - { - return ((IBinaryInteger)_exponent).TryWriteBigEndian(destination, out bytesWritten); - } - - /// - bool IFloatingPoint.TryWriteExponentLittleEndian(Span destination, out int bytesWritten) - { - return ((IBinaryInteger)_exponent).TryWriteLittleEndian(destination, out bytesWritten); - } - - /// - bool IFloatingPoint.TryWriteSignificandBigEndian(Span destination, out int bytesWritten) - { - return ((IBinaryInteger)_mantissa).TryWriteBigEndian(destination, out bytesWritten); - } - - /// - bool IFloatingPoint.TryWriteSignificandLittleEndian(Span destination, out int bytesWritten) - { - return ((IBinaryInteger)_mantissa).TryWriteLittleEndian(destination, out bytesWritten); - } - - /// - static bool INumberBase.IsCanonical(BigDecimal value) => true; - - /// - static bool INumberBase.IsComplexNumber(BigDecimal value) => false; - - /// - static bool INumberBase.IsFinite(BigDecimal value) => true; - - /// - static bool INumberBase.IsImaginaryNumber(BigDecimal value) => false; - - /// - static bool INumberBase.IsInfinity(BigDecimal value) => false; - - /// - static bool INumberBase.IsNaN(BigDecimal value) => false; - - /// - static bool INumberBase.IsNegativeInfinity(BigDecimal value) => false; - - /// - static bool INumberBase.IsNormal(BigDecimal value) => !value.IsZero; - - /// - static bool INumberBase.IsPositiveInfinity(BigDecimal value) => false; - - /// - static bool INumberBase.IsRealNumber(BigDecimal value) => true; - - /// - static bool INumberBase.IsSubnormal(BigDecimal value) => false; - - /// - static bool INumberBase.IsZero(BigDecimal value) => value.IsZero; - - /// - static BigDecimal INumberBase.MaxMagnitudeNumber(BigDecimal x, BigDecimal y) => MaxMagnitude(x, y); - - /// - static BigDecimal INumberBase.MinMagnitudeNumber(BigDecimal x, BigDecimal y) => MinMagnitude(x, y); - - /// - static bool INumberBase.TryConvertFromChecked(TOther value, out BigDecimal result) => TryConvertFromChecked(value, out result); - - /// - static bool INumberBase.TryConvertFromSaturating(TOther value, out BigDecimal result) => TryConvertFrom(value, out result); - - /// - static bool INumberBase.TryConvertFromTruncating(TOther value, out BigDecimal result) => TryConvertFrom(value, out result); - - /// - static bool INumberBase.TryConvertToChecked(BigDecimal value, [MaybeNullWhen(false)] out TOther result) - { - if (typeof(TOther) == typeof(float)) - { - result = (TOther)(object)(float)value; - return true; - } - else if (typeof(TOther) == typeof(double)) - { - result = (TOther)(object)(double)value; - return true; - } - else if (typeof(TOther) == typeof(decimal)) - { - result = (TOther)(object)(decimal)value; - return true; - } - else if (typeof(TOther).IsAssignableTo(typeof(IBinaryInteger<>))) - { - var intValue = (BigInteger)value; - return TOther.TryConvertFromChecked(intValue, out result); - } - - result = default; - return false; - } - - /// - static bool INumberBase.TryConvertToSaturating(BigDecimal value, [MaybeNullWhen(false)] out TOther result) - { - if (typeof(TOther) == typeof(float)) - { - result = (TOther)(object)(float)value; - return true; - } - else if (typeof(TOther) == typeof(double)) - { - result = (TOther)(object)(double)value; - return true; - } - else if (typeof(TOther) == typeof(decimal)) - { - result = value < decimal.MinValue ? (TOther)(object)decimal.MinValue : - value > decimal.MaxValue ? (TOther)(object)decimal.MaxValue : - (TOther)(object)(decimal)value; - - return true; - } - else if (typeof(TOther).IsAssignableTo(typeof(IBinaryInteger<>))) - { - var intValue = (BigInteger)value; - return TOther.TryConvertFromSaturating(intValue, out result); - } - - result = default; - return false; - } - - /// - static bool INumberBase.TryConvertToTruncating(BigDecimal value, [MaybeNullWhen(false)] out TOther result) - { - if (typeof(TOther) == typeof(float)) - { - result = (TOther)(object)(float)value; - return true; - } - else if (typeof(TOther) == typeof(double)) - { - result = (TOther)(object)(double)value; - return true; - } - else if (typeof(TOther) == typeof(decimal)) - { - result = value < decimal.MinValue ? (TOther)(object)decimal.MinValue : - value > decimal.MaxValue ? (TOther)(object)decimal.MaxValue : - (TOther)(object)(decimal)value; - - return true; - } - else if (typeof(TOther).IsAssignableTo(typeof(IBinaryInteger<>))) - { - var intValue = (BigInteger)value; - return TOther.TryConvertFromTruncating(intValue, out result); - } - - result = default; - return false; - } - - /// - bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) - { - // TODO: Implement better performing option - - string s = ToString(format.ToString(), provider); - - if (destination.Length < s.Length) - { - charsWritten = 0; - return false; - } - - s.CopyTo(destination); - charsWritten = s.Length; - return true; + Debug.Assert(value._exponent >= reference._exponent, "value exponent must be greater than or equal to reference exponent"); + return value._mantissa * BigIntegerPow10.Get(value._exponent - reference._exponent); } #endregion - -#endif } \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal/DecimalData.cs b/Source/Singulink.Numerics.BigDecimal/DecimalData.cs index 9b31bfc..ccfa243 100644 --- a/Source/Singulink.Numerics.BigDecimal/DecimalData.cs +++ b/Source/Singulink.Numerics.BigDecimal/DecimalData.cs @@ -18,7 +18,7 @@ internal readonly struct DecimalData public int Scale => unchecked((byte)(Flags >> ScaleShift)); - public bool IsPositive => (Flags & SignMask) == 0; + public bool IsPositive => (Flags & SignMask) is 0; public DecimalData(int flags, uint hi, ulong lo) { @@ -30,5 +30,5 @@ public DecimalData(int flags, uint hi, ulong lo) Lo = lo; } - public static bool IsValid(int flags) => unchecked((flags & ~(SignMask | ScaleMask)) == 0 && ((uint)(flags & ScaleMask) <= (28 << ScaleShift))); + public static bool IsValid(int flags) => unchecked((flags & ~(SignMask | ScaleMask)) is 0 && ((uint)(flags & ScaleMask) <= (28 << ScaleShift))); } \ No newline at end of file diff --git a/Source/Singulink.Numerics.BigDecimal/FloatConversion.cs b/Source/Singulink.Numerics.BigDecimal/FloatConversion.cs index 16e4517..eb8d55a 100644 --- a/Source/Singulink.Numerics.BigDecimal/FloatConversion.cs +++ b/Source/Singulink.Numerics.BigDecimal/FloatConversion.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Singulink.Numerics; +namespace Singulink.Numerics; /// /// Specifies floating-point conversion modes. diff --git a/Source/Singulink.Numerics.BigDecimal/Singulink.Numerics.BigDecimal.csproj b/Source/Singulink.Numerics.BigDecimal/Singulink.Numerics.BigDecimal.csproj index b469a1c..26df4e7 100644 --- a/Source/Singulink.Numerics.BigDecimal/Singulink.Numerics.BigDecimal.csproj +++ b/Source/Singulink.Numerics.BigDecimal/Singulink.Numerics.BigDecimal.csproj @@ -23,10 +23,6 @@ true - - - - @@ -40,7 +36,7 @@ - + diff --git a/Source/Singulink.Numerics.BigDecimal/Utilities/Throw.cs b/Source/Singulink.Numerics.BigDecimal/Utilities/Throw.cs index 8ed035b..994e2eb 100644 --- a/Source/Singulink.Numerics.BigDecimal/Utilities/Throw.cs +++ b/Source/Singulink.Numerics.BigDecimal/Utilities/Throw.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Text; using RuntimeNullables; namespace Singulink.Numerics.Utilities