Skip to content
Draft
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
57 changes: 56 additions & 1 deletion src/main/kotlin/com/cognite/units/UnitService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import java.net.URL
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.log10
import kotlin.math.nextDown
import kotlin.math.pow
import kotlin.math.roundToLong

Expand Down Expand Up @@ -231,12 +232,42 @@ class UnitService(units: String, systems: String) {
return roundToSignificantDigits(targetUnitValue, 12)
}

// Find the range of numbers that would convert to the given valueTo in the target unit
fun convertBetweenUnitsInverseRange(unitFrom: TypedUnit, unitTo: TypedUnit, valueTo: Double): Pair<Double, Double> {
if (unitFrom == unitTo) {
return valueTo to valueTo // avoid rounding errors
}
verifyIsConvertible(unitFrom, unitTo)
val roundingValues = roundingNeighbors(valueTo, 12)
// roundedTarget is the aim: we want to find everything that would convert and round to this value
val roundedTarget = roundingValues.rounded
// Find the bounds for the target value. Anything between these bounds would round to roundedTarget
val lowerBoundTo = (roundingValues.nextLower + roundedTarget) / 2
val upperBoundTo = (roundedTarget + roundingValues.nextUpper) / 2
// Do the reverse conversion to find the corresponding bounds in the source unit
val lowerBaseUnit = convertBetweenUnits(unitTo, unitFrom, lowerBoundTo)
val upperBaseUnit = convertBetweenUnits(unitTo, unitFrom, upperBoundTo)
// Because the bounds can not be represented exactly, we do one more check to see if we are correct.
// If they do not convert to the exact roundedTarget, we must use the neighbor number instead.
val adjustedLower = if (convertBetweenUnits(unitFrom, unitTo, lowerBaseUnit) == roundedTarget) {
lowerBaseUnit
} else {
roundingNeighbors(lowerBaseUnit, 12).nextUpper
}
val adjustedUpper = if (convertBetweenUnits(unitFrom, unitTo, upperBaseUnit) == roundedTarget) {
upperBaseUnit
} else {
roundingNeighbors(upperBaseUnit, 12).nextLower
}
return adjustedLower to adjustedUpper
}

/*
* Conversion factors can't always be represented exactly in floating point. Also, some arithmetics may result
* in numbers like 0.9999999999999999 which should be rounded to 1.0.
* This function rounds to the specified number of significant digits.
*/
private fun roundToSignificantDigits(value: Double, significantDigits: Int): Double {
fun roundToSignificantDigits(value: Double, significantDigits: Int): Double {
if (value == 0.0 || !value.isFinite()) {
return value
}
Expand All @@ -247,6 +278,30 @@ class UnitService(units: String, systems: String) {
return shifted / magnitude
}

data class RoundingValues(
val nextLower: Double,
val rounded: Double,
val nextUpper: Double,
)

/* Find the rounding neighbors. Eg. with 2 significant digits,
* the neighbors of 1.234 are (1.22, 1.24). (First round to 1.23, then find neighbors.)
*/
fun roundingNeighbors(value: Double, significantDigits: Int) : RoundingValues{
if (value == 0.0 || !value.isFinite()) {
return RoundingValues(value, value, value)
}
val digits = ceil(log10(abs(value)))
val power = significantDigits - digits
val magnitude = 10.0.pow(power)
val shifted = (value * magnitude).roundToLong()
return RoundingValues(
(shifted - 1) / magnitude,
shifted / magnitude,
(shifted + 1) / magnitude,
)
}

fun isValidUnit(unitExternalId: String): Boolean {
return unitsByExternalId.containsKey(unitExternalId)
}
Expand Down
78 changes: 78 additions & 0 deletions src/test/kotlin/UnitTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.net.URL
import kotlin.math.pow
import kotlin.random.Random
import kotlin.test.DefaultAsserter
import kotlin.test.assertNotEquals

class UnitTest {

Expand Down Expand Up @@ -55,6 +58,81 @@ class UnitTest {
assertEquals(0.555555555556, unitService.convertBetweenUnits(unitFahrenheit, unitCelsius, 33.0))
}

@Test
fun testConversionRanges() {
val unitService = UnitService.service
val unitCelsius = unitService.getUnitByExternalId("temperature:deg_c")
val unitFahrenheit = unitService.getUnitByExternalId("temperature:deg_f")

// Generate random numbers with a wide range of magnitudes
fun randomTemperature() : Double {
val power = Random.nextInt(-10, 12)
val targetTemperature = Random.nextDouble(-1.0, 1.0) * 10.0.pow(power.toDouble())
val roundedTarget = unitService.roundToSignificantDigits(targetTemperature, 12)
return roundedTarget
}

repeat (100) {
val temperature = randomTemperature()
println("Testing temperature: $temperature")
for ((targetUnit, sourceUnit) in listOf(
Pair(unitCelsius, unitFahrenheit),
Pair(unitFahrenheit, unitCelsius),
)) {
val (lowerBound, upperBound) = unitService
.convertBetweenUnitsInverseRange(sourceUnit, targetUnit, temperature)
if (lowerBound > upperBound) {
println("Not possible to reach temperature. SourceUnit: ${sourceUnit.name} Lower bound: $lowerBound, Upper bound: $upperBound, Temperature: $temperature")
continue
}
assertEquals(
temperature,
unitService.convertBetweenUnits(sourceUnit, targetUnit, lowerBound),
)
assertEquals(
temperature,
unitService.convertBetweenUnits(sourceUnit, targetUnit, upperBound),
)
assertNotEquals(
temperature,
unitService.convertBetweenUnits(
sourceUnit,
targetUnit,
unitService.roundingNeighbors(lowerBound, 12).nextLower),
0.0,
"Failed for temperature $temperature, lowerBound $lowerBound, sourceUnit ${sourceUnit.name}"
)
assertNotEquals(
temperature,
unitService.convertBetweenUnits(
sourceUnit,
targetUnit,
unitService.roundingNeighbors(upperBound, 12).nextUpper),
0.0,
"Failed for temperature $temperature, upperBound $upperBound, sourceUnit ${sourceUnit.name}",
)
println("OK conversion from ${sourceUnit.name} for temperature $temperature")
}
}
}

@Test
fun blah() {
val unitService = UnitService.service
val unitCelsius = unitService.getUnitByExternalId("temperature:deg_c")
val unitFahrenheit = unitService.getUnitByExternalId("temperature:deg_f")

val temperature = -196.593259651
val upperTemp = -196.5932596505
val upperBound = -321.867867372
val nextUpperB = -321.867867371
println("Converted temperature: ${unitService.convertBetweenUnits(unitCelsius, unitFahrenheit, temperature)}")
println("Converted upperTemp: ${unitService.convertBetweenUnits(unitCelsius, unitFahrenheit, upperTemp)}")
println("Converted upperBound: ${unitService.convertBetweenUnits(unitFahrenheit, unitCelsius, upperBound)}")
println("Converted nextUpperB: ${unitService.convertBetweenUnits(unitFahrenheit, unitCelsius, nextUpperB)}")
}


@Test
fun convertToSystem() {
val unitService = UnitService.service
Expand Down
Loading