From 5bfce898c9f509fac026eba4a31734133b46e1de Mon Sep 17 00:00:00 2001 From: Shaun Moss Date: Wed, 26 Oct 2022 17:48:26 +1000 Subject: [PATCH 1/4] Updated log and pow methods. --- .gitignore | 4 + DecimalEx/DecimalEx.cs | 230 +++++++++++++++++++++++------------------ 2 files changed, 132 insertions(+), 102 deletions(-) diff --git a/.gitignore b/.gitignore index b248083..c004aef 100644 --- a/.gitignore +++ b/.gitignore @@ -347,3 +347,7 @@ healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ + +# JetBrains settings +.idea + diff --git a/DecimalEx/DecimalEx.cs b/DecimalEx/DecimalEx.cs index 9adaa65..2201cf1 100644 --- a/DecimalEx/DecimalEx.cs +++ b/DecimalEx/DecimalEx.cs @@ -202,143 +202,169 @@ public static decimal Exp(decimal d) } /// - /// Returns the natural (base e) logarithm of a specified number. + /// Calculate the natural logarithm of a decimal. + /// Algorithm from: + /// + /// I prefer this method name to "Log", but I've provided Log() as an alias. + /// + /// /// - /// A number whose logarithm is to be found. - /// - /// I'm still not satisfied with the speed. I tried several different - /// algorithms that you can find in a historical version of this - /// source file. The one I settled on was the best of mediocrity. - /// - public static decimal Log(decimal d) + /// A decimal value. + /// The natural logarithm of the given value. + /// + /// If the argument is less than or equal to 0. + /// + public static decimal Ln(decimal m) { - if (d < 0) throw new ArgumentException("Natural logarithm is a complex number for values less than zero!", "d"); - if (d == 0) throw new OverflowException("Natural logarithm is defined as negative infinity at zero which the Decimal data type can't represent!"); - - if (d == 1) return 0; - - if (d >= 1) + // Guards. + if (m == 0) { - var power = 0m; - - var x = d; - while (x > 1) - { - x /= 10; - power += 1; - } + throw new ArgumentOutOfRangeException(nameof(m), + "The logarithm of 0 is undefined. Math.Log(0) returns -Infinity, but the decimal type doesn't provide a way to represent this."); + } - return Log(x) + power * Ln10; + if (m < 0) + { + throw new ArgumentOutOfRangeException(nameof(m), + "The logarithm of a negative value is a complex number. Use DecimalComplex.Ln()."); } - - // See http://en.wikipedia.org/wiki/Natural_logarithm#Numerical_value - // for more information on this faster-converging series. - decimal y; - decimal ySquared; + // Optimizations. + switch (m) + { + case 1: return 0; + case 2: return DecimalEx.Ln2; + case 10: return DecimalEx.Ln10; + case DecimalEx.E: return 1; + } - var iteration = 0; - var exponent = 0m; - var nextAdd = 0m; - var result = 0m; + // Scale the value to the range (0..1) so the Taylor series converges + // quickly and to avoid overflow. Using double methods for speed. + int scale = (int)Math.Floor(Math.Log10((double)m)) + 1; + decimal x; - y = (d - 1) / (d + 1); - ySquared = y * y; + // Some cleverness to avoid overflow if scale == 29. + if (scale <= 28) + { + x = m / Pow10(scale); + } + else + { + x = m / 1e28m / Pow10(scale - 28); + } + // Use the Taylor series. + x--; + decimal xx = x; + int n = 1; + int s = 1; + decimal oldValue = 0; + decimal newValue = 0; while (true) { - if (iteration == 0) - { - exponent = 2 * y; - } - else + // Calculate the next term in the series. + decimal term = s * xx / n; + + // Check if done. + if (term == 0m) { - exponent = exponent * ySquared; + break; } - nextAdd = exponent / (2 * iteration + 1); - - if (nextAdd == 0) break; - - result += nextAdd; - - iteration += 1; + // Add the term. + newValue = oldValue + term; + + // Prepare to calculate the next term. + s = -s; + xx *= x; + n++; + oldValue = newValue; } - return result; - + // Scale back. + return newValue + scale * DecimalEx.Ln10; } /// - /// Returns the logarithm of a specified number in a specified base. + /// Calculate the natural logarithm. + /// Alias for Ln() to match method names used elsewhere. + /// + /// /// - /// A number whose logarithm is to be found. - /// The base of the logarithm. - /// - /// This is a relatively naive implementation that simply divides the - /// natural log of by the natural log of the base. - /// - public static decimal Log(decimal d, decimal newBase) - { - // Short circuit the checks below if d is 1 because - // that will yield 0 in the numerator below and give us - // 0 for any base, even ones that would yield infinity. - if (d == 1) return 0m; - - if (newBase == 1) throw new InvalidOperationException("Logarithm for base 1 is undefined."); - if (d < 0) throw new ArgumentException("Logarithm is a complex number for values less than zero!", nameof(d)); - if (d == 0) throw new OverflowException("Logarithm is defined as negative infinity at zero which the Decimal data type can't represent!"); - if (newBase < 0) throw new ArgumentException("Logarithm base would be a complex number for values less than zero!", nameof(newBase)); - if (newBase == 0) throw new OverflowException("Logarithm base would be negative infinity at zero which the Decimal data type can't represent!"); - - return Log(d) / Log(newBase); - } + /// A decimal value. + /// The natural log of the given value. + /// + /// If the argument is less than or equal to 0. + /// + public static decimal Log(decimal m) => Ln(m); /// - /// Returns the base 10 logarithm of a specified number. + /// Logarithm of a decimal in a specified base. + /// + /// /// - /// A number whose logarithm is to be found. - public static decimal Log10(decimal d) + /// The decimal value. + /// The base. + /// The logarithm of z in base b. + /// + /// If the number is less than or equal to 0. + /// + /// + /// If the base is less than or equal to 0, or + /// equal to 1. + public static decimal Log(decimal m, decimal b) { - if (d < 0) throw new ArgumentException("Logarithm is a complex number for values less than zero!", nameof(d)); - if (d == 0) throw new OverflowException("Logarithm is defined as negative infinity at zero which the Decimal data type can't represent!"); - - // Shrink precision from the input value and get bits for analysis - var parts = decimal.GetBits(d / 1.000000000000000000000000000000000m); - var scale = (parts[3] >> 16) & 0x7F; - - // Handle special cases of .1, .01, .001, etc. - if (parts[0] == 1 && parts[1] == 0 && parts[2] == 0) + if (b == 1) { - return -1 * scale; + throw new ArgumentOutOfRangeException(nameof(b), + "Logarithms are undefined for a base of 1."); } - // Handle special cases of powers of 10 - // Note: A binary search was actually found to be faster on average probably because it takes fewer iterations to find no match. - // It's even faster than doing a modulus 10 check first. - if (scale == 0) + // 0^0 == 1. Mimics Math.Log(). + if (m == 1 && b == 0) { - var powerOf10 = Array.BinarySearch(PowersOf10, d); - if (powerOf10 >= 0) - { - return powerOf10; - } + return 0; } - return Log(d) / Ln10; + // This will throw if m <= 0 || b <= 0. + return Ln(m) / Ln(b); } /// - /// Returns the base 2 logarithm of a specified number. + /// Logarithm of a decimal in base 10. + /// + /// /// - /// A number whose logarithm is to be found. - public static decimal Log2(decimal d) - { - if (d < 0) throw new ArgumentException("Logarithm is a complex number for values less than zero!", nameof(d)); - if (d == 0) throw new OverflowException("Logarithm is defined as negative infinity at zero which the Decimal data type can't represent!"); + /// The decimal value. + /// The logarithm of the number in base 10. + /// + /// If the number is less than or equal to 0. + /// + public static decimal Log10(decimal m) => Log(m, 10); - return Log(d) / Ln2; - } + /// + /// Calculate 10 raised to a decimal power. + /// + /// A decimal value. + /// 10^d + public static decimal Pow10(decimal m) => DecimalEx.Pow(10, m); + + /// + /// Logarithm of a decimal in base 2. + /// + /// The decimal value. + /// The logarithm of the number in base 2. + /// + /// If the number is less than or equal to 0. + /// + public static decimal Log2(decimal m) => Log(m, 2); + + /// + /// Calculate 2 raised to a decimal power. + /// + /// A decimal value. + /// 2^d + public static decimal Pow2(decimal m) => DecimalEx.Pow(2, m); /// /// Returns the factorial of a number n expressed as n!. Factorial is From 3f121d4119920f1ff618dac66d1a799cabaf0e3a Mon Sep 17 00:00:00 2001 From: Shaun Moss Date: Wed, 26 Oct 2022 17:56:11 +1000 Subject: [PATCH 2/4] Added some tests for the new Ln() method. --- DecimalEx.Tests/DecimalExTests/LogTests.cs | 69 +++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/DecimalEx.Tests/DecimalExTests/LogTests.cs b/DecimalEx.Tests/DecimalExTests/LogTests.cs index 0888b5c..fead95e 100644 --- a/DecimalEx.Tests/DecimalExTests/LogTests.cs +++ b/DecimalEx.Tests/DecimalExTests/LogTests.cs @@ -192,6 +192,71 @@ public void Log2RejectZeroValue() } #endregion - } + + [Test] + public void LnThrowsIfArgZero() + { + Assert.Throws(() => DecimalEx.Ln(0)); + } + + [Test] + public void LnThrowsIfArgNegative() + { + Assert.Throws(() => DecimalEx.Ln(-1)); + } + + [Test] + public void LnTest() + { + decimal m; + + m = 1; + Assert.AreEqual(0, DecimalEx.Ln(m)); + + m = 2; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); + + m = 10; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); + + m = DecimalEx.E; + Assert.AreEqual(1, DecimalEx.Ln(m)); + + m = decimal.MaxValue; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); -} \ No newline at end of file + m = DecimalEx.SmallestNonZeroDec; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); + + m = 1.23456789m; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); + + m = 9.87654321m; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); + + m = 123456789m; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); + + m = 9876543210m; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); + + m = 0.00000000000000000123456789m; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); + + m = 0.00000000000000000987654321m; + Assert.AreEqual(Math.Log((double)m), DecimalEx.Ln(m)); + } + + [Test] + public void Log1Base0Returns0() + { + Assert.AreEqual(0, DecimalEx.Log(1, 0)); + } + + [Test] + public void LogThrowsIfBase1() + { + Assert.Throws(() => DecimalEx.Log(1.234m, 1)); + } + } +} From 6c5d133b63f4173a6576c271034f1ff842cd54a7 Mon Sep 17 00:00:00 2001 From: Shaun Moss Date: Wed, 26 Oct 2022 18:02:35 +1000 Subject: [PATCH 3/4] Removed redundant qualifiers. --- DecimalEx/DecimalEx.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/DecimalEx/DecimalEx.cs b/DecimalEx/DecimalEx.cs index 2201cf1..91207e0 100644 --- a/DecimalEx/DecimalEx.cs +++ b/DecimalEx/DecimalEx.cs @@ -233,9 +233,9 @@ public static decimal Ln(decimal m) switch (m) { case 1: return 0; - case 2: return DecimalEx.Ln2; - case 10: return DecimalEx.Ln10; - case DecimalEx.E: return 1; + case 2: return Ln2; + case 10: return Ln10; + case E: return 1; } // Scale the value to the range (0..1) so the Taylor series converges @@ -282,7 +282,7 @@ public static decimal Ln(decimal m) } // Scale back. - return newValue + scale * DecimalEx.Ln10; + return newValue + scale * Ln10; } /// @@ -347,7 +347,7 @@ public static decimal Log(decimal m, decimal b) /// /// A decimal value. /// 10^d - public static decimal Pow10(decimal m) => DecimalEx.Pow(10, m); + public static decimal Pow10(decimal m) => Pow(10, m); /// /// Logarithm of a decimal in base 2. @@ -364,7 +364,7 @@ public static decimal Log(decimal m, decimal b) /// /// A decimal value. /// 2^d - public static decimal Pow2(decimal m) => DecimalEx.Pow(2, m); + public static decimal Pow2(decimal m) => Pow(2, m); /// /// Returns the factorial of a number n expressed as n!. Factorial is From 242dd0a429c4db69885135d5bfccf53a53c5fe7e Mon Sep 17 00:00:00 2001 From: Shaun Moss Date: Wed, 26 Oct 2022 18:12:53 +1000 Subject: [PATCH 4/4] Fixed comment format. --- DecimalEx/DecimalEx.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DecimalEx/DecimalEx.cs b/DecimalEx/DecimalEx.cs index 91207e0..b10ad9c 100644 --- a/DecimalEx/DecimalEx.cs +++ b/DecimalEx/DecimalEx.cs @@ -310,8 +310,8 @@ public static decimal Ln(decimal m) /// If the number is less than or equal to 0. /// /// - /// If the base is less than or equal to 0, or - /// equal to 1. + /// If the base is less than or equal to 0, or equal to 1. + /// public static decimal Log(decimal m, decimal b) { if (b == 1)