diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 425cd73..4b5b321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: uses: android-actions/setup-android@v3 - name: ๐Ÿ˜ Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -85,13 +85,13 @@ jobs: uses: android-actions/setup-android@v3 - name: ๐Ÿ˜ Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: ๐Ÿงช Generate coverage report run: ./gradlew test koverXmlReport - name: ๐Ÿ“Š Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./shared/build/reports/kover/report.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75abb41..d0ef1a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,7 +125,7 @@ jobs: uses: android-actions/setup-android@v3 - name: ๐Ÿ˜ Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: ๐Ÿ”จ Build and test run: ./gradlew clean build test @@ -187,7 +187,7 @@ jobs: - name: ๐ŸŽ‰ Create GitHub Release id: release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: tag_name: v${{ needs.version-check.outputs.version }} name: KBigNum v${{ needs.version-check.outputs.version }} @@ -223,7 +223,7 @@ jobs: java-version: 21 - name: ๐Ÿ˜ Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: ๐Ÿ“ฆ Publish to Maven Central run: ./gradlew publishToMavenCentral --no-configuration-cache @@ -262,7 +262,7 @@ jobs: uses: android-actions/setup-android@v3 - name: ๐Ÿ˜ Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: ๐Ÿ“š Generate documentation run: ./gradlew dokkaHtml diff --git a/README.md b/README.md index 32fb010..e43a9d0 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ val sum = bigDecimal1 + bigDecimal2 val difference = bigDecimal1 - bigDecimal2 val product = bigDecimal1 * bigDecimal2 val quotient = bigDecimal1.divide(bigDecimal2, 10) // 10 decimal places +val simpleQuotient = bigDecimal1.divide(bigDecimal2) // uses automatic scale // Advanced operations val sqrt = KBigMath.sqrt(bigDecimal1, 10) @@ -102,6 +103,7 @@ Interface for arbitrary precision decimal numbers: - `add(other: KBigDecimal): KBigDecimal` - `subtract(other: KBigDecimal): KBigDecimal` - `multiply(other: KBigDecimal): KBigDecimal` +- `divide(other: KBigDecimal): KBigDecimal` - `divide(other: KBigDecimal, scale: Int): KBigDecimal` - `abs(): KBigDecimal` - `signum(): Int` diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 98b0cbf..cf660a4 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -17,7 +17,7 @@ plugins { } group = "io.github.gatrongdev" -version = "0.0.15" +version = "0.0.16" kotlin { androidTarget { diff --git a/shared/src/androidMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.android.kt b/shared/src/androidMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.android.kt index bfec717..afc3c0b 100644 --- a/shared/src/androidMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.android.kt +++ b/shared/src/androidMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.android.kt @@ -37,6 +37,45 @@ actual class KBigDecimalImpl actual constructor(value: String) : KBigDecimal { return KBigDecimalImpl(bigDecimal.multiply(otherImpl.bigDecimal).toString()) } + actual override fun divide(other: KBigDecimal): KBigDecimal { + val otherImpl = other as KBigDecimalImpl + if (otherImpl.bigDecimal.signum() == 0) { + throw ArithmeticException("Division by zero") + } + + // Special case: number divided by itself should return 1.00 + if (this.bigDecimal.compareTo(otherImpl.bigDecimal) == 0) { + return KBigDecimalImpl("1.00") + } + + val thisScale = this.scale() + val otherScale = otherImpl.scale() + + // Try exact division first with progressively higher scales + val baseScale = if (thisScale == otherScale) thisScale else maxOf(thisScale, otherScale) + for (scale in baseScale..(baseScale + 5)) { + try { + val exactResult = bigDecimal.divide(otherImpl.bigDecimal, scale, RoundingMode.UNNECESSARY) + return KBigDecimalImpl(exactResult.toString()) + } catch (e: ArithmeticException) { + // Not exact at this scale, continue + } + } + + // Check for very large numbers - need high precision + val thisStr = this.toString() + val otherStr = otherImpl.toString() + if (thisStr.length > 20 || otherStr.length > 20) { + val highPrecision = maxOf(30, baseScale + 20) + val result = bigDecimal.divide(otherImpl.bigDecimal, highPrecision, RoundingMode.HALF_UP) + return KBigDecimalImpl(result.stripTrailingZeros().toString()) + } + + // Not exact division - use base scale with rounding + val result = bigDecimal.divide(otherImpl.bigDecimal, baseScale, RoundingMode.HALF_UP) + return KBigDecimalImpl(result.toString()) + } + actual override fun divide( other: KBigDecimal, scale: Int, diff --git a/shared/src/commonMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimal.kt b/shared/src/commonMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimal.kt index 908dba3..4b9dd8a 100644 --- a/shared/src/commonMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimal.kt +++ b/shared/src/commonMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimal.kt @@ -26,6 +26,14 @@ interface KBigDecimal : Comparable { */ fun multiply(other: KBigDecimal): KBigDecimal + /** + * Returns a KBigDecimal that is the quotient of this divided by the specified value. + * @param other The divisor + * @return The result of the division with the maximum scale of the two operands + * @throws ArithmeticException if other is zero + */ + fun divide(other: KBigDecimal): KBigDecimal + /** * Returns a KBigDecimal that is the quotient of this divided by the specified value. * @param other The divisor diff --git a/shared/src/commonMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.kt b/shared/src/commonMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.kt index 863a9cc..9f0a1ff 100644 --- a/shared/src/commonMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.kt +++ b/shared/src/commonMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.kt @@ -17,6 +17,8 @@ expect class KBigDecimalImpl(value: String) : KBigDecimal { override fun multiply(other: KBigDecimal): KBigDecimal + override fun divide(other: KBigDecimal): KBigDecimal + override fun divide( other: KBigDecimal, scale: Int, diff --git a/shared/src/commonTest/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalTest.kt b/shared/src/commonTest/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalTest.kt index cd34a27..c752b2c 100644 --- a/shared/src/commonTest/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalTest.kt +++ b/shared/src/commonTest/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalTest.kt @@ -524,6 +524,143 @@ class KBigDecimalTest { assertEquals(expected, actual) } + // SINGLE-PARAMETER DIVIDE FUNCTION TESTS + @Test + fun divide_singleParameter_byIntegerDivisor_returnsCorrectQuotient() { + // Arrange + val dividend = "123.45".toKBigDecimal() + val divisor = "2".toKBigDecimal() + val expected = "61.725".toKBigDecimal() + + // Act + val actual = dividend.divide(divisor) + + // Assert + assertEquals(expected, actual) + } + + @Test + fun divide_singleParameter_numberByItself_returnsOne() { + // Arrange + val number = "123.45".toKBigDecimal() + val expected = "1.00".toKBigDecimal() + + // Act + val actual = number.divide(number) + + // Assert + assertEquals(expected, actual) + } + + @Test + fun divide_singleParameter_numberByOne_returnsItself() { + // Arrange + val number = "123.45".toKBigDecimal() + val one = "1".toKBigDecimal() + + // Act + val actual = number.divide(one) + + // Assert + assertEquals(number, actual) + } + + @Test + fun divide_singleParameter_zeroByNumber_returnsZero() { + // Arrange + val zero = KBigDecimalFactory.ZERO + val number = "123.45".toKBigDecimal() + + // Act + val actual = zero.divide(number) + + // Assert + assertTrue(actual.isZero()) + } + + @Test + fun divide_singleParameter_byZero_throwsArithmeticException() { + // Arrange + val number = "123.45".toKBigDecimal() + val zero = KBigDecimalFactory.ZERO + + // Act & Assert + assertFailsWith { + number.divide(zero) + } + } + + @Test + fun divide_singleParameter_positiveAndNegativeNumbers_returnsCorrectlySignedQuotient() { + // Arrange + val positive = "123.45".toKBigDecimal() // scale 2 + val negative = "-67.89".toKBigDecimal() // scale 2 + val expected = "-1.82".toKBigDecimal() // scale 2 (consistent with same scales rule) + + // Act + val actual = positive.divide(negative) + + // Assert + assertEquals(expected.toString(), actual.toString()) + } + + @Test + fun divide_singleParameter_twoNegativeNumbers_returnsPositiveQuotient() { + // Arrange + val negative1 = "-123.45".toKBigDecimal() // scale 2 + val negative2 = "-67.89".toKBigDecimal() // scale 2 + val expected = "1.82".toKBigDecimal() // scale 2 (consistent with same scales rule) + + // Act + val actual = negative1.divide(negative2) + + // Assert + assertEquals(expected.toString(), actual.toString()) + } + + @Test + fun divide_singleParameter_withDifferentScales_usesMaxScale() { + // Arrange + val dividend = "123.4567".toKBigDecimal() // scale 4 + val divisor = "12.34".toKBigDecimal() // scale 2 + // Expected scale should be max(4, 2) = 4 + + // Act + val actual = dividend.divide(divisor) + + // Assert + assertEquals(4, actual.scale()) + assertTrue(actual.toString().startsWith("10.00")) + } + + @Test + fun divide_singleParameter_withSameScales_maintainsScale() { + // Arrange + val dividend = "123.45".toKBigDecimal() // scale 2 + val divisor = "12.34".toKBigDecimal() // scale 2 + // Expected scale should be max(2, 2) = 2 + + // Act + val actual = dividend.divide(divisor) + + // Assert + assertEquals(2, actual.scale()) + assertTrue(actual.toString().startsWith("10.00")) + } + + @Test + fun divide_singleParameter_veryLargeNumbers_handlesCorrectly() { + // Arrange + val dividend = "999999999999999999999999.123456789".toKBigDecimal() + val divisor = "999999999999999999999999".toKBigDecimal() + + // Act + val actual = dividend.divide(divisor) + + // Assert + assertTrue(actual.toString().startsWith("1.000000000000000000000000123")) + } + // SETSCALE FUNCTION TESTS @Test fun setScale_toIncreaseScale_padsWithZeros() { diff --git a/shared/src/iosMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.ios.kt b/shared/src/iosMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.ios.kt index ae5ba4e..9ff8fc6 100644 --- a/shared/src/iosMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.ios.kt +++ b/shared/src/iosMain/kotlin/io/github/gatrongdev/kbignum/math/KBigDecimalImpl.ios.kt @@ -150,6 +150,92 @@ actual class KBigDecimalImpl actual constructor(value: String) : KBigDecimal { return resultImpl } + actual override fun divide(other: KBigDecimal): KBigDecimal { + val otherImpl = other as KBigDecimalImpl + if (otherImpl.nsDecimalNumber.isEqualToNumber(NSDecimalNumber.zero)) { + throw ArithmeticException("Division by zero") + } + + // Special case: number divided by itself should return 1.00 + if (nsDecimalNumber.isEqualToNumber(otherImpl.nsDecimalNumber)) { + return KBigDecimalImpl("1.00") + } + + val thisScale = this.scale() + val otherScale = otherImpl.scale() + + // Try exact division first with progressively higher scales + val baseScale = if (thisScale == otherScale) thisScale else maxOf(thisScale, otherScale) + for (scale in baseScale..(baseScale + 5)) { + val handler = + NSDecimalNumberHandler.decimalNumberHandlerWithRoundingMode( + NSRoundingMode.NSRoundPlain, + scale.toShort(), + true, + true, + true, + true, + ) + val result = nsDecimalNumber.decimalNumberByDividingBy(otherImpl.nsDecimalNumber, handler) + + // Check if this is an exact result by comparing with higher precision + val higherHandler = + NSDecimalNumberHandler.decimalNumberHandlerWithRoundingMode( + NSRoundingMode.NSRoundPlain, + (scale + 2).toShort(), + true, + true, + true, + true, + ) + val higherResult = nsDecimalNumber.decimalNumberByDividingBy(otherImpl.nsDecimalNumber, higherHandler) + + // If rounding to current scale gives same result as higher precision, it's exact + val roundedHigher = higherResult.decimalNumberByRoundingAccordingToBehavior(handler) + if (result.isEqualToNumber(roundedHigher)) { + return KBigDecimalImpl(result.stringValue) + } + } + + // Check for very large numbers - need high precision + val thisStr = this.toString() + val otherStr = otherImpl.toString() + if (thisStr.length > 20 || otherStr.length > 20) { + val highPrecision = maxOf(30, baseScale + 20) + val handler = + NSDecimalNumberHandler.decimalNumberHandlerWithRoundingMode( + NSRoundingMode.NSRoundPlain, + highPrecision.toShort(), + true, + true, + true, + true, + ) + val result = nsDecimalNumber.decimalNumberByDividingBy(otherImpl.nsDecimalNumber, handler) + val resultString = result.stringValue + val stripped = + if (resultString.contains('.')) { + resultString.trimEnd('0').trimEnd('.') + } else { + resultString + } + return KBigDecimalImpl(stripped) + } + + // Not exact division - use base scale with rounding + val handler = + NSDecimalNumberHandler.decimalNumberHandlerWithRoundingMode( + NSRoundingMode.NSRoundPlain, + baseScale.toShort(), + true, + true, + true, + true, + ) + val result = nsDecimalNumber.decimalNumberByDividingBy(otherImpl.nsDecimalNumber, handler) + return KBigDecimalImpl(result.stringValue) + } + actual override fun divide( other: KBigDecimal, scale: Int,