diff --git a/kotlinx.interval.datetime/src/commonMain/kotlin/DateTimeTypeOperations.kt b/kotlinx.interval.datetime/src/commonMain/kotlin/DateTimeTypeOperations.kt index afcba1d..5583128 100644 --- a/kotlinx.interval.datetime/src/commonMain/kotlin/DateTimeTypeOperations.kt +++ b/kotlinx.interval.datetime/src/commonMain/kotlin/DateTimeTypeOperations.kt @@ -4,6 +4,7 @@ import io.github.whathecode.kotlinx.interval.TypeOperations import kotlinx.datetime.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit internal object InstantOperations : TypeOperations @@ -22,6 +23,23 @@ internal object InstantOperations : TypeOperations a.epochSeconds - b.epochSeconds, a.nanosecondsOfSecond - b.nanosecondsOfSecond ) + + private const val NANOS_IN_SECOND = 1_000_000_000 + + override fun fromDouble( double: Double ): Instant + { + val seconds = double.toLong() + val fraction = double - seconds + val nanoseconds = (NANOS_IN_SECOND * fraction).toLong() + return Instant.fromEpochSeconds( seconds, nanoseconds ) + } + + override fun toDouble( value: Instant ): Double + { + val seconds = value.epochSeconds + val fraction = value.nanosecondsOfSecond.toDouble() / NANOS_IN_SECOND + return seconds + fraction + } } @@ -36,4 +54,7 @@ internal object DurationOperations : TypeOperations override fun unsafeAdd( a: Duration, b: Duration ): Duration = a + b override fun unsafeSubtract( a: Duration, b: Duration ): Duration = a - b + + override fun fromDouble( double: Double ): Duration = double.milliseconds + override fun toDouble( value: Duration ): Double = value.toDouble( DurationUnit.MILLISECONDS ) } diff --git a/kotlinx.interval.testcases/src/commonMain/kotlin/TypeOperationsTest.kt b/kotlinx.interval.testcases/src/commonMain/kotlin/TypeOperationsTest.kt index da20fa6..a600e73 100644 --- a/kotlinx.interval.testcases/src/commonMain/kotlin/TypeOperationsTest.kt +++ b/kotlinx.interval.testcases/src/commonMain/kotlin/TypeOperationsTest.kt @@ -62,4 +62,52 @@ abstract class TypeOperationsTest>( val result = typeOperations.unsafeSubtract( a, a ) assertEquals( typeOperations.additiveIdentity, result ) } + + @Test + fun toDouble_fromDouble_roundtrip_returns_same_value() + { + val original = a + + val toDouble = typeOperations.toDouble( original ) + val fromDouble = typeOperations.fromDouble( toDouble ) + + assertEquals( original, fromDouble ) + } + + @Test + fun toDouble_fromDouble_roundtrip_for_maxima() + { + val maxima = listOf( typeOperations.minValue, typeOperations.maxValue ) + maxima.forEach { + val toDouble = typeOperations.toDouble( it ) + val fromDouble = typeOperations.fromDouble( toDouble ) + assertEquals( it, fromDouble ) + } + } + + @Test + fun fromDouble_overflows_past_max() + { + val max = typeOperations.maxValue + val maxDouble = typeOperations.toDouble( max ) + val pastMaxDouble = maxDouble + 1.0 + + val overflowMax = typeOperations.fromDouble( pastMaxDouble ) + + // Types either overflow, or are coerced to max value (e.g. floating points). + if ( overflowMax != max ) assertEquals( typeOperations.minValue, overflowMax ) + } + + @Test + fun fromDouble_overflows_past_min() + { + val min = typeOperations.minValue + val minDouble = typeOperations.toDouble( min ) + val pastMinDouble = minDouble - 1.0 + + val overflowMin = typeOperations.fromDouble( pastMinDouble ) + + // Types either overflow, or are coerced to min value (e.g. floating points). + if ( overflowMin != min ) assertEquals( typeOperations.maxValue, overflowMin ) + } } diff --git a/kotlinx.interval/src/commonMain/kotlin/BasicTypeOperations.kt b/kotlinx.interval/src/commonMain/kotlin/BasicTypeOperations.kt index 26a7218..b1ca88e 100644 --- a/kotlinx.interval/src/commonMain/kotlin/BasicTypeOperations.kt +++ b/kotlinx.interval/src/commonMain/kotlin/BasicTypeOperations.kt @@ -1,5 +1,7 @@ package io.github.whathecode.kotlinx.interval +import kotlin.math.roundToInt +import kotlin.math.roundToLong import kotlin.reflect.KClass @@ -45,6 +47,9 @@ internal object ByteOperations : TypeOperations override fun unsafeAdd( a: Byte, b: Byte ): Byte = (a + b).toByte() override fun unsafeSubtract( a: Byte, b: Byte ): Byte = (a - b).toByte() + + override fun fromDouble( double: Double ) = double.roundToInt().toByte() + override fun toDouble( value: Byte ) = value.toDouble() } internal object ShortOperations : TypeOperations @@ -56,6 +61,9 @@ internal object ShortOperations : TypeOperations override fun unsafeAdd( a: Short, b: Short ): Short = (a + b).toShort() override fun unsafeSubtract( a: Short, b: Short ): Short = (a - b).toShort() + + override fun fromDouble( double: Double ) = double.roundToInt().toShort() + override fun toDouble( value: Short ) = value.toDouble() } internal object IntOperations : TypeOperations @@ -67,6 +75,9 @@ internal object IntOperations : TypeOperations override fun unsafeAdd( a: Int, b: Int ): Int = a + b override fun unsafeSubtract( a: Int, b: Int ): Int = a - b + + override fun fromDouble( double: Double ): Int = double.roundToInt() + override fun toDouble( value: Int ): Double = value.toDouble() } internal object LongOperations : TypeOperations @@ -78,6 +89,9 @@ internal object LongOperations : TypeOperations override fun unsafeAdd( a: Long, b: Long ): Long = a + b override fun unsafeSubtract( a: Long, b: Long ): Long = a - b + + override fun fromDouble( double: Double ): Long = double.roundToLong() + override fun toDouble( value: Long ): Double = value.toDouble() } internal object FloatOperations : TypeOperations @@ -89,6 +103,9 @@ internal object FloatOperations : TypeOperations override fun unsafeAdd( a: Float, b: Float ): Float = a + b override fun unsafeSubtract( a: Float, b: Float ): Float = a - b + + override fun fromDouble( double: Double ): Float = double.toFloat() + override fun toDouble( value: Float ): Double = value.toDouble() } internal object DoubleOperations : TypeOperations @@ -100,6 +117,9 @@ internal object DoubleOperations : TypeOperations override fun unsafeAdd( a: Double, b: Double ): Double = a + b override fun unsafeSubtract( a: Double, b: Double ): Double = a - b + + override fun fromDouble( double: Double ): Double = double + override fun toDouble( value: Double ): Double = value } internal object UByteOperations : TypeOperations @@ -111,6 +131,9 @@ internal object UByteOperations : TypeOperations override fun unsafeAdd( a: UByte, b: UByte ): UByte = (a + b).toUByte() override fun unsafeSubtract( a: UByte, b: UByte ): UByte = (a - b).toUByte() + + override fun fromDouble( double: Double ): UByte = double.roundToInt().toUByte() + override fun toDouble( value: UByte ): Double = value.toDouble() } internal object UShortOperations : TypeOperations @@ -122,6 +145,9 @@ internal object UShortOperations : TypeOperations override fun unsafeAdd( a: UShort, b: UShort ): UShort = (a + b).toUShort() override fun unsafeSubtract( a: UShort, b: UShort ): UShort = (a - b).toUShort() + + override fun fromDouble( double: Double ): UShort = double.roundToInt().toUShort() + override fun toDouble( value: UShort ): Double = value.toDouble() } internal object UIntOperations : TypeOperations @@ -133,6 +159,9 @@ internal object UIntOperations : TypeOperations override fun unsafeAdd( a: UInt, b: UInt ): UInt = a + b override fun unsafeSubtract( a: UInt, b: UInt ): UInt = a - b + + override fun fromDouble( double: Double ): UInt = double.toUInt() + override fun toDouble( value: UInt ): Double = value.toDouble() } internal object ULongOperations : TypeOperations @@ -144,6 +173,9 @@ internal object ULongOperations : TypeOperations override fun unsafeAdd( a: ULong, b: ULong ): ULong = a + b override fun unsafeSubtract( a: ULong, b: ULong ): ULong = a - b + + override fun fromDouble( double: Double ): ULong = double.toULong() + override fun toDouble( value: ULong ): Double = value.toDouble() } internal object CharOperations : TypeOperations @@ -155,4 +187,7 @@ internal object CharOperations : TypeOperations override fun unsafeAdd( a: Char, b: Char ): Char = a + b.code override fun unsafeSubtract( a: Char, b: Char ): Char = (a - b).toChar() + + override fun fromDouble( double: Double ): Char = double.roundToInt().toChar() + override fun toDouble( value: Char ): Double = value.code.toDouble() } diff --git a/kotlinx.interval/src/commonMain/kotlin/TypeOperations.kt b/kotlinx.interval/src/commonMain/kotlin/TypeOperations.kt index 2c30fa5..b51f683 100644 --- a/kotlinx.interval/src/commonMain/kotlin/TypeOperations.kt +++ b/kotlinx.interval/src/commonMain/kotlin/TypeOperations.kt @@ -39,6 +39,18 @@ interface TypeOperations> */ fun unsafeSubtract( a: T, b: T ): T + /** + * Convert a [double] to the closest corresponding value of [T]. + * If the [double] value is larger than what can be represented by [T] ([minValue]..[maxValue]), the returned value + * will overflow. + */ + fun fromDouble( double: Double ): T + + /** + * Convert [value] to a [Double], which may be lossy. + */ + fun toDouble( value: T ): Double + /** * Determines whether this type can represent negative values.