Skip to content

Commit

Permalink
Merge branch 'nextversion' into nv-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
lbressler13 committed Jan 23, 2024
2 parents 07f331a + e0c5bef commit 6310d09
Show file tree
Hide file tree
Showing 44 changed files with 860 additions and 829 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ See [here](https://docs.github.com/en/packages/working-with-a-github-packages-re
## Testing

Unit tests are written using the [Kotlin test](https://kotlinlang.org/api/latest/kotlin.test/) framework.
Mocking is done using the [MockK library](https://mockk.io/).
Tests must be written for all logic in the package.

Tests can be run using an IDE, or with the following command:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ internal class ExactFractionImpl private constructor(numerator: BigInteger, deno
this.denominator = denominator
}
else -> {
val simplifiedValues = simplifyFraction(Pair(numerator, denominator))
val simplifiedValues = simplifyFraction(numerator, denominator)
this.numerator = simplifiedValues.first
this.denominator = simplifiedValues.second
}
Expand Down Expand Up @@ -56,14 +56,10 @@ internal class ExactFractionImpl private constructor(numerator: BigInteger, deno
// UNARY NON-OPERATORS

override fun inverse(): ExactFraction {
if (numerator.isZero()) {
throw divideByZero
}

return if (numerator.isNegative()) {
ExactFractionImpl(-denominator, -numerator, fullySimplified = true)
} else {
ExactFractionImpl(denominator, numerator, fullySimplified = true)
return when {
isZero() -> throw divideByZero
isNegative() -> ExactFractionImpl(-denominator, -numerator, fullySimplified = true)
else -> ExactFractionImpl(denominator, numerator, fullySimplified = true)
}
}

Expand All @@ -73,7 +69,10 @@ internal class ExactFractionImpl private constructor(numerator: BigInteger, deno
override fun isWholeNumber(): Boolean = denominator == BigInteger.ONE

override fun roundToWhole(roundingMode: RoundingMode): ExactFraction {
val decimal = numerator.toBigDecimal().divide(denominator.toBigDecimal(), roundingMode)
val mc = MathContext(0, roundingMode)
val decimal = numerator.toBigDecimal()
.divide(denominator.toBigDecimal(), roundingMode)
.round(mc)

return ExactFractionImpl(decimal.toBigInteger(), BigInteger.ONE, fullySimplified = true)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package xyz.lbres.exactnumbers.exactfraction

/**
* [ArithmeticException] specifically for ExactFraction casting overflow.
* Has specific field for value of string that caused overflow
* [ArithmeticException] for ExactFraction overflow.
* Has field for string representation of value that caused overflow
*/
class ExactFractionOverflowException() : ArithmeticException() {
override var message: String? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,35 @@ import java.math.MathContext
import java.math.RoundingMode

/**
* Simplify a pair of numerator and denominator values to smallest values with same ratio, and move all negatives into numerator
* Simplify numerator and denominator values to smallest values with same ratio, and move all negatives into numerator
*
* @param values [TypePair]<BigInteger>: pair to simplify, where the first value represents a numerator and the second represents a denominator
* @param numerator [BigInteger]: numerator of fraction to simplify
* @param denominator [BigInteger]: denominator of fraction to simplify
* @return [TypePair]<BigInteger>: pair where first value represents simplified numerator, and second value represents simplified denominator
*/
internal fun simplifyFraction(values: TypePair<BigInteger>): TypePair<BigInteger> {
var numerator = values.first
var denominator = values.second
internal fun simplifyFraction(numerator: BigInteger, denominator: BigInteger): TypePair<BigInteger> {
var newNumerator = numerator
var newDenominator = denominator

// set denominator to 1 when numerator is 0
if (numerator.isZero()) {
denominator = BigInteger.ONE
if (newNumerator.isZero()) {
newDenominator = BigInteger.ONE
}

// move negatives to numerator
if (denominator.isNegative()) {
numerator = -numerator
denominator = -denominator
if (newDenominator.isNegative()) {
newNumerator = -newNumerator
newDenominator = -newDenominator
}

// simplify using greatest common divisor
if (numerator != BigInteger.ZERO) {
val gcd = getGCD(numerator, denominator)
numerator /= gcd
denominator /= gcd
if (newNumerator != BigInteger.ZERO) {
val gcd = getGCD(newNumerator, newDenominator)
newNumerator /= gcd
newDenominator /= gcd
}

return Pair(numerator, denominator)
return Pair(newNumerator, newDenominator)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ internal fun efAdd(ef1: ExactFraction, ef2: ExactFraction): ExactFraction {
return ExactFraction(newNumerator, ef1.denominator)
}

val scaled1 = ef1.numerator * ef2.denominator
val scaled2 = ef2.numerator * ef1.denominator

val newNumerator = scaled1 + scaled2
val newNumerator = ef1.numerator * ef2.denominator + ef2.numerator * ef1.denominator
val newDenominator = ef1.denominator * ef2.denominator
return ExactFraction(newNumerator, newDenominator)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package xyz.lbres.exactnumbers.exactfraction

import xyz.lbres.kotlinutils.biginteger.ext.isZero
import xyz.lbres.kotlinutils.general.simpleIf
import xyz.lbres.kotlinutils.general.tryOrDefault
import xyz.lbres.kotlinutils.int.ext.isNegative
import xyz.lbres.kotlinutils.int.ext.isZero
import xyz.lbres.kotlinutils.string.ext.countElement
import xyz.lbres.kotlinutils.string.ext.substringTo
import java.math.BigDecimal
import java.math.BigInteger
import kotlin.math.abs

private const val efPrefix = "EF["
private const val efSuffix = "]"

/**
* Parse a string from standard number format into a ExactFraction.
Expand All @@ -24,52 +22,24 @@ internal fun parseDecimal(s: String): ExactFraction {

// remove negative sign
val isNegative = currentState.startsWith("-")
val timesNeg = simpleIf(isNegative, -BigInteger.ONE, BigInteger.ONE)
if (isNegative) {
currentState = currentState.substring(1)
}

// remove e-value
val eIndex = currentState.indexOf('E')
var eValue = 0
if (eIndex != -1) {
eValue = currentState.substring(eIndex + 1).toInt()
currentState = currentState.substringTo(eIndex)
}
val divResult = BigDecimal(currentState).divideAndRemainder(BigDecimal.ONE)
val whole = divResult[0].toBigInteger()

val decimalIndex: Int = currentState.indexOf('.')
val ef: ExactFraction

// generate fraction
if (decimalIndex == -1) {
val numerator = BigInteger(currentState)
ef = ExactFraction(numerator * timesNeg)
} else {
val wholeString = simpleIf(decimalIndex == 0, "0", currentState.substringTo(decimalIndex))
val decimalString = currentState.substring(decimalIndex + 1)
val whole = BigInteger(wholeString)
val decimal = BigInteger(decimalString)

if (decimal.isZero()) {
ef = ExactFraction(whole * timesNeg) // also covers the case where number is 0
} else {
val zeros = "0".repeat(decimalString.length)
val denomString = "1$zeros"

val denominator = BigInteger(denomString)
val numerator = whole * denominator + decimal

ef = ExactFraction(numerator * timesNeg, denominator)
}
}
val rawDecimalString = divResult[1].stripTrailingZeros().toPlainString()
val decimalIndex = rawDecimalString.indexOf('.')
val decimalString = rawDecimalString.substring(decimalIndex + 1) // starts from 0 if decimalIndex == -1

// apply exponentiation
val eMultiplier = BigInteger.TEN.pow(abs(eValue))
return when {
eValue.isZero() -> ef
eValue.isNegative() -> ExactFraction(ef.numerator, eMultiplier * ef.denominator)
else -> ExactFraction(eMultiplier * ef.numerator, ef.denominator)
}
val denomZeroes = "0".repeat(decimalString.length)
val denomString = "1$denomZeroes"

val denominator = BigInteger(denomString)
val numerator = whole * denominator + BigInteger(decimalString)

return simpleIf(isNegative, { ExactFraction(-numerator, denominator) }, { ExactFraction(numerator, denominator) })
}

/**
Expand All @@ -81,30 +51,14 @@ internal fun parseDecimal(s: String): ExactFraction {
private fun validateDecimalString(s: String) {
val exception = NumberFormatException("Error parsing $s")

val eIndex = s.indexOf('E')
val validCharacters = s.all { it.isDigit() || it == '-' || it == '.' || it == 'E' }
val validE = eIndex != 0 && eIndex != s.lastIndex && s.countElement('E') in 0..1
if (!validCharacters || !validE) {
try {
BigDecimal(s)
} catch (_: Exception) {
throw exception
}

val validateMinus: (String) -> Boolean = {
it.indexOf('-') <= 0 && it.countElement('-') <= 1
}

val validateDecimal: (String) -> Boolean = {
it.indexOf('.') != it.lastIndex && it.countElement('.') <= 1
}

if (eIndex == -1 && !validateMinus(s) || !validateDecimal(s)) {
if (s.last() == '.') {
throw exception
} else if (eIndex != -1) {
val preE = s.substringTo(eIndex)
val postE = s.substring(eIndex + 1)

if (!validateMinus(preE) || !validateDecimal(preE) || !validateMinus(postE) || postE.contains('.')) {
throw exception
}
}
}

Expand All @@ -121,10 +75,9 @@ internal fun parseEFString(s: String): ExactFraction {
}

try {
val numbers = s.substring(3, s.lastIndex)
val splitNumbers = numbers.split(' ')
val numString = splitNumbers[0].trim()
val denomString = splitNumbers[1].trim()
val numbers = s.trim().substring(efPrefix.length, s.length - efSuffix.length).split(' ')
val numString = numbers[0].trim()
val denomString = numbers[1].trim()
val numerator = BigInteger(numString)
val denominator = BigInteger(denomString)
return ExactFraction(numerator, denominator)
Expand All @@ -144,24 +97,21 @@ internal fun parseEFString(s: String): ExactFraction {
*/
internal fun checkIsEFString(s: String): Boolean {
val trimmed = s.trim()
val prefix = "EF["
val suffix = "]"

if (!trimmed.startsWith(prefix) || !trimmed.endsWith(suffix)) {
if (!trimmed.startsWith(efPrefix) || !trimmed.endsWith(efSuffix)) {
return false
}

return tryOrDefault(false) {
val split = trimmed.substring(prefix.length, s.length - suffix.length).split(' ')
val numbers = trimmed.substring(efPrefix.length, s.length - efSuffix.length).split(' ')
val validNumber: (String) -> Boolean = {
when {
it.isEmpty() -> false
it.length == 1 -> it[0].isDigit()
it[0] == '-' -> it.substring(1).all(Char::isDigit)
else -> it.all(Char::isDigit)
else -> (it[0] == '-' || it[0].isDigit()) && it.substring(1).all(Char::isDigit)
}
}

split.size == 2 && validNumber(split[0]) && validNumber(split[1])
numbers.size == 2 && validNumber(numbers[0]) && validNumber(numbers[1])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import xyz.lbres.exactnumbers.utils.createHashCode
import xyz.lbres.exactnumbers.utils.divideByZero
import xyz.lbres.kotlinutils.collection.ext.toConstMultiSet
import xyz.lbres.kotlinutils.general.simpleIf
import xyz.lbres.kotlinutils.set.multiset.anyConsistent
import xyz.lbres.kotlinutils.set.multiset.const.ConstMultiSet
import xyz.lbres.kotlinutils.set.multiset.const.emptyConstMultiSet
import xyz.lbres.kotlinutils.set.multiset.mapToSetConsistent
Expand All @@ -36,29 +37,29 @@ internal class TermImpl(coefficient: ExactFraction, factors: ConstMultiSet<Irrat
private var string: String? = null

init {
if (coefficient.isZero() || factors.any { it.isZero() }) {
if (coefficient.isZero() || factors.anyConsistent { it.isZero() }) {
this.coefficient = ExactFraction.ZERO
this.factors = emptyList()
factorSet = emptyConstMultiSet()
factorTypeMapping = emptyMap()
this.factorSet = emptyConstMultiSet()
this.factorTypeMapping = emptyMap()

logs = emptyList()
squareRoots = emptyList()
pis = emptyList()
piCount = 0
this.logs = emptyList()
this.squareRoots = emptyList()
this.pis = emptyList()
this.piCount = 0
} else {
this.coefficient = coefficient
this.factors = factors.toList()
this.factorSet = factors
this.factorTypeMapping = factorSet.groupBy { it.type }

@Suppress("UNCHECKED_CAST")
logs = factorTypeMapping.getOrDefault(Log.TYPE, emptyList()) as List<Log>
this.logs = factorTypeMapping.getOrDefault(Log.TYPE, emptyList()) as List<Log>
@Suppress("UNCHECKED_CAST")
squareRoots = factorTypeMapping.getOrDefault(Sqrt.TYPE, emptyList()) as List<Sqrt>
this.squareRoots = factorTypeMapping.getOrDefault(Sqrt.TYPE, emptyList()) as List<Sqrt>
@Suppress("UNCHECKED_CAST")
pis = factorTypeMapping.getOrDefault(Pi.TYPE, emptyList()) as List<Pi>
piCount = factorSet.getCountOf(Pi()) - factorSet.getCountOf(Pi().inverse())
this.pis = factorTypeMapping.getOrDefault(Pi.TYPE, emptyList()) as List<Pi>
this.piCount = factorSet.getCountOf(Pi()) - factorSet.getCountOf(Pi().inverse())
}
}

Expand All @@ -77,8 +78,8 @@ internal class TermImpl(coefficient: ExactFraction, factors: ConstMultiSet<Irrat
}

other as TermImpl
val newIrrationals = factorSet + other.factorSet.mapToSetConsistent { it.inverse() }
return TermImpl(coefficient / other.coefficient, newIrrationals.toConstMultiSet())
val newFactors = factorSet + other.factorSet.mapToSetConsistent { it.inverse() }
return TermImpl(coefficient / other.coefficient, newFactors.toConstMultiSet())
}

override fun isZero(): Boolean = coefficient.isZero()
Expand Down Expand Up @@ -117,7 +118,7 @@ internal class TermImpl(coefficient: ExactFraction, factors: ConstMultiSet<Irrat
}

/**
* Get list of irrational numbers with a given type
* Get list of factors with a given type
*
* @param irrationalType [String]: type to retrieve numbers for
* @return [List]<IrrationalNumber<*>>: list of irrational numbers, which all have the provided type
Expand All @@ -130,8 +131,8 @@ internal class TermImpl(coefficient: ExactFraction, factors: ConstMultiSet<Irrat
if (string == null) {
val fractionString = coefficient.toFractionString()
val coeffString = simpleIf(fractionString.contains("/"), "[$fractionString]", fractionString)
val numString = factorSet.joinToString("x")
val result = simpleIf(numString.isEmpty(), "<$coeffString>", "<${coeffString}x$numString>")
val factorString = factorSet.joinToString("x")
val result = simpleIf(factorString.isEmpty(), "<$coeffString>", "<${coeffString}x$factorString>")

string = result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import xyz.lbres.exactnumbers.irrationals.pi.Pi
import xyz.lbres.exactnumbers.irrationals.sqrt.Sqrt
import xyz.lbres.kotlinutils.collection.ext.toConstMultiSet
import xyz.lbres.kotlinutils.general.simpleIf
import xyz.lbres.kotlinutils.set.multiset.anyConsistent
import xyz.lbres.kotlinutils.set.multiset.const.ConstMultiSet
import xyz.lbres.kotlinutils.set.multiset.const.ConstMutableMultiSet
import xyz.lbres.kotlinutils.set.multiset.const.constMutableMultiSetOf
Expand Down Expand Up @@ -53,9 +52,8 @@ internal fun createSimplifiedTerm(coefficient: ExactFraction, factorGroups: Map<
* with rational values, and the second is a set of the irrational values
*/
private fun simplifyGenericIrrational(values: ConstMultiSet<IrrationalNumber<*>>): Pair<ExactFraction, ConstMultiSet<IrrationalNumber<*>>> {
when {
values.isEmpty() -> return Pair(ExactFraction.ONE, emptyConstMultiSet())
values.anyConsistent { it.isZero() } -> return Pair(ExactFraction.ZERO, emptyConstMultiSet())
if (values.isEmpty()) {
return Pair(ExactFraction.ONE, emptyConstMultiSet())
}

val distinct = values.mapToSetConsistent { simpleIf(it.isInverted, { it.inverse() }, { it }) }.distinctValues
Expand All @@ -64,6 +62,10 @@ private fun simplifyGenericIrrational(values: ConstMultiSet<IrrationalNumber<*>>
// avoids creating a standard MultiSet for efficiency
val simplifiedValues: ConstMutableMultiSet<IrrationalNumber<*>> = constMutableMultiSetOf()
for (value in distinct) {
if (value.isZero()) {
return Pair(ExactFraction.ONE, emptyConstMultiSet())
}

val diff = values.getCountOf(value) - values.getCountOf(value.inverse())
val simplifiedValue = simpleIf(diff < 0, { value.inverse() }, { value })

Expand Down
Loading

0 comments on commit 6310d09

Please sign in to comment.