Skip to content
Merged

Dev #35

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ plugins {
}

group = "io.github.gatrongdev"
version = "0.0.15"
version = "0.0.16"

kotlin {
androidTarget {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ interface KBigDecimal : Comparable<KBigDecimal> {
*/
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArithmeticException> {
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down