From fe3c6cb79c28fcb6f636585c2e45130327903cfa Mon Sep 17 00:00:00 2001 From: Hakanai Date: Thu, 26 May 2022 17:37:21 +1000 Subject: [PATCH] Added some series-based functions (exp, cos, sin, ...) Added: - exp (by infinite series) - cos (by infinite series) - sin (by infinite series) - tan (by cos/sin) - cosh (by infinite series) - sinh (by infinite series) - tanh (by cosh/sinh) --- .../kotlin/bignum/decimal/BigDecimal.kt | 72 ++++++++++++++ .../bignum/decimal/util/CosineCalculator.kt | 24 +++++ .../decimal/util/ExponentialCalculator.kt | 17 ++++ .../util/HyperbolicCosineCalculator.kt | 19 ++++ .../decimal/util/HyperbolicSineCalculator.kt | 19 ++++ .../bignum/decimal/util/SeriesCalculator.kt | 44 +++++++++ .../bignum/decimal/util/SineCalculator.kt | 24 +++++ .../bignum/decimal/BigDecimalExpTest.kt | 28 ++++++ .../bignum/decimal/BigDecimalTrigTest.kt | 97 +++++++++++++++++++ 9 files changed, 344 insertions(+) create mode 100644 bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/CosineCalculator.kt create mode 100644 bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/ExponentialCalculator.kt create mode 100644 bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/HyperbolicCosineCalculator.kt create mode 100644 bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/HyperbolicSineCalculator.kt create mode 100644 bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/SeriesCalculator.kt create mode 100644 bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/SineCalculator.kt create mode 100644 bignum/src/commonTest/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimalExpTest.kt create mode 100644 bignum/src/commonTest/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimalTrigTest.kt diff --git a/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimal.kt b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimal.kt index 8a996f91..925a2652 100644 --- a/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimal.kt +++ b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimal.kt @@ -20,6 +20,11 @@ package com.ionspin.kotlin.bignum.decimal import com.ionspin.kotlin.bignum.BigNumber import com.ionspin.kotlin.bignum.CommonBigNumberOperations import com.ionspin.kotlin.bignum.NarrowingOperations +import com.ionspin.kotlin.bignum.decimal.util.CosineCalculator +import com.ionspin.kotlin.bignum.decimal.util.ExponentialCalculator +import com.ionspin.kotlin.bignum.decimal.util.HyperbolicCosineCalculator +import com.ionspin.kotlin.bignum.decimal.util.HyperbolicSineCalculator +import com.ionspin.kotlin.bignum.decimal.util.SineCalculator import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.Platform import com.ionspin.kotlin.bignum.integer.RuntimePlatform @@ -1696,6 +1701,73 @@ class BigDecimal private constructor( */ override fun signum(): Int = significand.signum() + /** + * Exponential function (e^x) + * @param decimalMode the decimal mode to use. Precision and rounding mode must be specified. + * @return Result of exponential function for this BigDecimal. + */ + fun exp(decimalMode: DecimalMode): BigDecimal { + return ExponentialCalculator.calculate(this, decimalMode) + } + + /** + * Sine function + * @param decimalMode the decimal mode to use. Precision and rounding mode must be specified. + * @return Result of sine function for this BigDecimal. + */ + fun sin(decimalMode: DecimalMode): BigDecimal { + return SineCalculator.calculate(this, decimalMode) + } + + /** + * Cosine function + * @param decimalMode the decimal mode to use. Precision and rounding mode must be specified. + * @return Result of cosine function for this BigDecimal. + */ + fun cos(decimalMode: DecimalMode): BigDecimal { + return CosineCalculator.calculate(this, decimalMode) + } + + /** + * Tangent function + * @param decimalMode the decimal mode to use. Precision and rounding mode must be specified. + * @return Result of tangent function for this BigDecimal. + */ + fun tan(decimalMode: DecimalMode): BigDecimal { + val higherPrecisionMode = decimalMode.copy(decimalPrecision = decimalMode.decimalPrecision + 3) + return sin(higherPrecisionMode) + .divide(cos(higherPrecisionMode), decimalMode) + } + + /** + * Hyperbolic sine function + * @param decimalMode the decimal mode to use. Precision and rounding mode must be specified. + * @return Result of hyperbolic sine function for this BigDecimal. + */ + fun sinh(decimalMode: DecimalMode): BigDecimal { + return HyperbolicSineCalculator.calculate(this, decimalMode) + } + + /** + * Hyperbolic cosine function + * @param decimalMode the decimal mode to use. Precision and rounding mode must be specified. + * @return Result of hyperbolic cosine function for this BigDecimal. + */ + fun cosh(decimalMode: DecimalMode): BigDecimal { + return HyperbolicCosineCalculator.calculate(this, decimalMode) + } + + /** + * Hyperbolic tangent function + * @param decimalMode the decimal mode to use. Precision and rounding mode must be specified. + * @return Result of hyperbolic tangent function for this BigDecimal. + */ + fun tanh(decimalMode: DecimalMode): BigDecimal { + val higherPrecisionMode = decimalMode.copy(decimalPrecision = decimalMode.decimalPrecision + 3) + return sinh(higherPrecisionMode) + .divide(cosh(higherPrecisionMode), decimalMode) + } + /** * The next group of functions are implementations of the NarrowingOperations interface */ diff --git a/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/CosineCalculator.kt b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/CosineCalculator.kt new file mode 100644 index 00000000..f67a3f10 --- /dev/null +++ b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/CosineCalculator.kt @@ -0,0 +1,24 @@ +package com.ionspin.kotlin.bignum.decimal.util + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import com.ionspin.kotlin.bignum.decimal.DecimalMode + +object CosineCalculator : SeriesCalculator() { + private val MINUS_ONE = BigDecimal.parseString("-1") + + override fun createTermSequence(x: BigDecimal, decimalMode: DecimalMode): Sequence { + // 1, x^2, x^4, x^6, ... + val powerSequence = createAllPowersSequence(x, decimalMode) + .filterIndexed { i, _ -> i % 2 == 0 } + + // 1/0!, -1/2!, 1/4!, -1/6!, ... + val factorSequence = createAllFactorialsSequence() + .filterIndexed { i, _ -> i % 2 == 0 } + .mapIndexed { i, n -> + val sign = if (i % 2 == 0) BigDecimal.ONE else MINUS_ONE + sign.divide(n, decimalMode) + } + + return powerSequence.zip(factorSequence) { a, b -> a.multiply(b, decimalMode) } + } +} \ No newline at end of file diff --git a/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/ExponentialCalculator.kt b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/ExponentialCalculator.kt new file mode 100644 index 00000000..a9513819 --- /dev/null +++ b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/ExponentialCalculator.kt @@ -0,0 +1,17 @@ +package com.ionspin.kotlin.bignum.decimal.util + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import com.ionspin.kotlin.bignum.decimal.DecimalMode + +object ExponentialCalculator : SeriesCalculator() { + override fun createTermSequence(x: BigDecimal, decimalMode: DecimalMode): Sequence { + // 1, x, x^2, x^3, ... + val powerSequence = createAllPowersSequence(x, decimalMode) + + // 1/0!, 1/1!, 1/2!, 1/3!, ... + val factorSequence = createAllFactorialsSequence() + .map { n -> BigDecimal.ONE.divide(n, decimalMode) } + + return powerSequence.zip(factorSequence) { a, b -> a.multiply(b, decimalMode) } + } +} \ No newline at end of file diff --git a/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/HyperbolicCosineCalculator.kt b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/HyperbolicCosineCalculator.kt new file mode 100644 index 00000000..193037c0 --- /dev/null +++ b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/HyperbolicCosineCalculator.kt @@ -0,0 +1,19 @@ +package com.ionspin.kotlin.bignum.decimal.util + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import com.ionspin.kotlin.bignum.decimal.DecimalMode + +object HyperbolicCosineCalculator : SeriesCalculator() { + override fun createTermSequence(x: BigDecimal, decimalMode: DecimalMode): Sequence { + // 1, x^2, x^4, x^6, ... + val powerSequence = createAllPowersSequence(x, decimalMode) + .filterIndexed { i, _ -> i % 2 == 0 } + + // 1/0!, 1/2!, 1/4!, 1/6!, ... + val factorSequence = createAllFactorialsSequence() + .filterIndexed { i, _ -> i % 2 == 0 } + .map { n -> BigDecimal.ONE.divide(n, decimalMode) } + + return powerSequence.zip(factorSequence) { a, b -> a.multiply(b, decimalMode) } + } +} \ No newline at end of file diff --git a/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/HyperbolicSineCalculator.kt b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/HyperbolicSineCalculator.kt new file mode 100644 index 00000000..b03ecc29 --- /dev/null +++ b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/HyperbolicSineCalculator.kt @@ -0,0 +1,19 @@ +package com.ionspin.kotlin.bignum.decimal.util + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import com.ionspin.kotlin.bignum.decimal.DecimalMode + +object HyperbolicSineCalculator : SeriesCalculator() { + override fun createTermSequence(x: BigDecimal, decimalMode: DecimalMode): Sequence { + // x, x^3, x^5, x^7, ... + val powerSequence = createAllPowersSequence(x, decimalMode) + .filterIndexed { i, _ -> i % 2 != 0 } + + // 1/1!, 1/3!, 1/5!, 1/7!, ... + val factorSequence = createAllFactorialsSequence() + .filterIndexed { i, _ -> i % 2 != 0 } + .map { n -> BigDecimal.ONE.divide(n, decimalMode) } + + return powerSequence.zip(factorSequence) { a, b -> a.multiply(b, decimalMode) } + } +} \ No newline at end of file diff --git a/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/SeriesCalculator.kt b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/SeriesCalculator.kt new file mode 100644 index 00000000..e686f1b9 --- /dev/null +++ b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/SeriesCalculator.kt @@ -0,0 +1,44 @@ +package com.ionspin.kotlin.bignum.decimal.util + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import com.ionspin.kotlin.bignum.decimal.DecimalMode + +/** + * Utility abstract class for implementing infinite series summations. + * + * Subclasses must ensure that the series actually converges (at least for + * all values you plan to calculate.) + */ +abstract class SeriesCalculator { + fun calculate(x: BigDecimal, decimalMode: DecimalMode): BigDecimal { + val higherPrecisionMode = decimalMode.copy(decimalPrecision = (decimalMode.decimalPrecision * 1.1).toLong() + 2) + val epsilon = BigDecimal.ONE.moveDecimalPoint(-higherPrecisionMode.decimalPrecision) + + return createTermSequence(x, higherPrecisionMode) + .takeWhile { step -> step.abs() > epsilon } + .fold(BigDecimal.ZERO) { acc, step -> acc.add(step) } + .roundSignificand(decimalMode) + } + + /** + * Implemented by subclasses to create an appropriate sequence of terms for the series. + */ + abstract fun createTermSequence(x: BigDecimal, decimalMode: DecimalMode): Sequence + + /** + * Utility function for subclasses to use which produces a sequence of all powers of `x`, + * starting at 0. + */ + protected fun createAllPowersSequence(x: BigDecimal, decimalMode: DecimalMode): Sequence { + return generateSequence(BigDecimal.ONE) { n -> n.multiply(x, decimalMode) } + } + + /** + * Utility function for subclasses to use which produces a sequence of all factorials, + * starting at 0! = 1, then 1! = 1, 2! = 2, 3! = 6, etc. + */ + protected fun createAllFactorialsSequence(): Sequence { + return generateSequence(1) { n -> n + 1 } + .runningFold(BigDecimal.ONE) { acc, n -> acc * n } + } +} \ No newline at end of file diff --git a/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/SineCalculator.kt b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/SineCalculator.kt new file mode 100644 index 00000000..a9a9d93b --- /dev/null +++ b/bignum/src/commonMain/kotlin/com/ionspin/kotlin/bignum/decimal/util/SineCalculator.kt @@ -0,0 +1,24 @@ +package com.ionspin.kotlin.bignum.decimal.util + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import com.ionspin.kotlin.bignum.decimal.DecimalMode + +object SineCalculator : SeriesCalculator() { + private val MINUS_ONE = BigDecimal.parseString("-1") + + override fun createTermSequence(x: BigDecimal, decimalMode: DecimalMode): Sequence { + // x, x^3, x^5, x^7, ... + val powerSequence = createAllPowersSequence(x, decimalMode) + .filterIndexed { i, _ -> i % 2 != 0 } + + // 1/1!, -1/3!, 1/5!, -1/7!, ... + val factorSequence = createAllFactorialsSequence() + .filterIndexed { i, _ -> i % 2 != 0 } + .mapIndexed { i, n -> + val sign = if (i % 2 == 0) BigDecimal.ONE else MINUS_ONE + sign.divide(n, decimalMode) + } + + return powerSequence.zip(factorSequence) { a, b -> a.multiply(b, decimalMode) } + } +} \ No newline at end of file diff --git a/bignum/src/commonTest/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimalExpTest.kt b/bignum/src/commonTest/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimalExpTest.kt new file mode 100644 index 00000000..10f31080 --- /dev/null +++ b/bignum/src/commonTest/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimalExpTest.kt @@ -0,0 +1,28 @@ +package com.ionspin.kotlin.bignum.decimal + +import kotlin.test.Test +import kotlin.test.assertEquals + +class BigDecimalExpTest { + + @Test + fun expTest() { + val decimalMode = DecimalMode(decimalPrecision = 20, roundingMode = RoundingMode.TOWARDS_ZERO) + val examples = listOf( + "0" to "1", + "0.2" to "1.2214027581601698339", + "0.4" to "1.4918246976412703178", + "0.6" to "1.8221188003905089748", + "0.8" to "2.2255409284924676045", + "1" to "2.7182818284590452353", + "2" to "7.3890560989306502272", + "-0.2" to "0.81873075307798185866", + "-0.4" to "0.67032004603563930074", + "-0.6" to "0.54881163609402643262", + ) + + for ((input, expected) in examples) { + assertEquals(BigDecimal.parseString(expected), BigDecimal.parseString(input).exp(decimalMode)) + } + } +} \ No newline at end of file diff --git a/bignum/src/commonTest/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimalTrigTest.kt b/bignum/src/commonTest/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimalTrigTest.kt new file mode 100644 index 00000000..66a3d709 --- /dev/null +++ b/bignum/src/commonTest/kotlin/com/ionspin/kotlin/bignum/decimal/BigDecimalTrigTest.kt @@ -0,0 +1,97 @@ +package com.ionspin.kotlin.bignum.decimal + +import kotlin.test.Test +import kotlin.test.assertEquals + +class BigDecimalTrigTest { + + @Test + fun sinTest() { + val decimalMode = DecimalMode(decimalPrecision = 20, roundingMode = RoundingMode.TOWARDS_ZERO) + val examples = listOf( + "0" to "0", + "0.2" to "0.19866933079506121545", + "0.4" to "0.38941834230865049166", + "0.6" to "0.56464247339503535720", + ) + + for ((input, expected) in examples) { + assertEquals(BigDecimal.parseString(expected), BigDecimal.parseString(input).sin(decimalMode)) + } + } + + @Test + fun cosTest() { + val decimalMode = DecimalMode(decimalPrecision = 20, roundingMode = RoundingMode.TOWARDS_ZERO) + val examples = listOf( + "0" to "1", + "0.2" to "0.98006657784124163112", + "0.4" to "0.92106099400288508279", + "0.6" to "0.82533561490967829724", + ) + + for ((input, expected) in examples) { + assertEquals(BigDecimal.parseString(expected), BigDecimal.parseString(input).cos(decimalMode)) + } + } + + @Test + fun tanTest() { + val decimalMode = DecimalMode(decimalPrecision = 20, roundingMode = RoundingMode.TOWARDS_ZERO) + val examples = listOf( + "0" to "0", + "0.2" to "0.20271003550867248332", + "0.4" to "0.42279321873816176198", + "0.6" to "0.68413680834169231707", + ) + + for ((input, expected) in examples) { + assertEquals(BigDecimal.parseString(expected), BigDecimal.parseString(input).tan(decimalMode)) + } + } + + @Test + fun sinhTest() { + val decimalMode = DecimalMode(decimalPrecision = 20, roundingMode = RoundingMode.TOWARDS_ZERO) + val examples = listOf( + "0" to "0", + "0.2" to "0.20133600254109398762", + "0.4" to "0.41075232580281550854", + "0.6" to "0.63665358214824127112", + ) + + for ((input, expected) in examples) { + assertEquals(BigDecimal.parseString(expected), BigDecimal.parseString(input).sinh(decimalMode)) + } + } + + @Test + fun coshTest() { + val decimalMode = DecimalMode(decimalPrecision = 20, roundingMode = RoundingMode.TOWARDS_ZERO) + val examples = listOf( + "0" to "1", + "0.2" to "1.0200667556190758462", + "0.4" to "1.0810723718384548092", + "0.6" to "1.1854652182422677037", + ) + + for ((input, expected) in examples) { + assertEquals(BigDecimal.parseString(expected), BigDecimal.parseString(input).cosh(decimalMode)) + } + } + + @Test + fun tanhTest() { + val decimalMode = DecimalMode(decimalPrecision = 20, roundingMode = RoundingMode.TOWARDS_ZERO) + val examples = listOf( + "0" to "0", + "0.2" to "0.19737532022490400073", + "0.4" to "0.37994896225522488526", + "0.6" to "0.53704956699803528586", + ) + + for ((input, expected) in examples) { + assertEquals(BigDecimal.parseString(expected), BigDecimal.parseString(input).tanh(decimalMode)) + } + } +}