From 9cc5553f59ed270da5ffc12f8fd455c1c898380b Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Tue, 1 Aug 2023 11:44:36 +0200 Subject: [PATCH 1/2] Added API for reading and writing of floating point numbers Implemented extension functions for reading and writing values with types Float and Double Resolves #167 --- core/api/kotlinx-io-core.api | 8 ++++ core/common/src/Sinks.kt | 64 ++++++++++++++++++++++++++ core/common/src/Sources.kt | 52 +++++++++++++++++++++ core/common/test/AbstractSinkTest.kt | 28 +++++++++++ core/common/test/AbstractSourceTest.kt | 64 ++++++++++++++++++++++++++ core/common/test/samples/samples.kt | 61 ++++++++++++++++++++++++ 6 files changed, 277 insertions(+) diff --git a/core/api/kotlinx-io-core.api b/core/api/kotlinx-io-core.api index 3c6532c9e..0d0f64df9 100644 --- a/core/api/kotlinx-io-core.api +++ b/core/api/kotlinx-io-core.api @@ -113,6 +113,10 @@ public final class kotlinx/io/SinksJvmKt { public final class kotlinx/io/SinksKt { public static final fun writeDecimalLong (Lkotlinx/io/Sink;J)V + public static final fun writeDouble (Lkotlinx/io/Sink;D)V + public static final fun writeDoubleLe (Lkotlinx/io/Sink;D)V + public static final fun writeFloat (Lkotlinx/io/Sink;F)V + public static final fun writeFloatLe (Lkotlinx/io/Sink;F)V public static final fun writeHexadecimalUnsignedLong (Lkotlinx/io/Sink;J)V public static final fun writeIntLe (Lkotlinx/io/Sink;I)V public static final fun writeLongLe (Lkotlinx/io/Sink;J)V @@ -158,6 +162,10 @@ public final class kotlinx/io/SourcesKt { public static final fun readByteArray (Lkotlinx/io/Source;)[B public static final fun readByteArray (Lkotlinx/io/Source;I)[B public static final fun readDecimalLong (Lkotlinx/io/Source;)J + public static final fun readDouble (Lkotlinx/io/Source;)D + public static final fun readDoubleLe (Lkotlinx/io/Source;)D + public static final fun readFloat (Lkotlinx/io/Source;)F + public static final fun readFloatLe (Lkotlinx/io/Source;)F public static final fun readHexadecimalUnsignedLong (Lkotlinx/io/Source;)J public static final fun readIntLe (Lkotlinx/io/Source;)I public static final fun readLongLe (Lkotlinx/io/Source;)J diff --git a/core/common/src/Sinks.kt b/core/common/src/Sinks.kt index de9387752..f8bb80519 100644 --- a/core/common/src/Sinks.kt +++ b/core/common/src/Sinks.kt @@ -275,6 +275,70 @@ public fun Sink.writeULongLe(long: ULong) { writeLongLe(long.toLong()) } +/** + * Writes four bytes of a bit representation of [float], in the big-endian order, to this sink. + * Bit representation of the [float] corresponds to the IEEE 754 floating-point "single format" bit layout. + * + * To obtain a bit representation, the [Float.toBits] function is used. + * + * @param float the floating point number to be written. + * + * @throws IllegalStateException when the sink is closed. + * + * @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.writeFloat + */ +public fun Sink.writeFloat(float: Float) { + writeInt(float.toBits()) +} + +/** + * Writes eight bytes of a bit representation of [double], in the big-endian order, to this sink. + * Bit representation of the [double] corresponds to the IEEE 754 floating-point "double format" bit layout. + * + * To obtain a bit representation, the [Double.toBits] function is used. + * + * @param double the floating point number to be written. + * + * @throws IllegalStateException when the sink is closed. + * + * @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.writeDouble + */ +public fun Sink.writeDouble(double: Double) { + writeLong(double.toBits()) +} + +/** + * Writes four bytes of a bit representation of [float], in the little-endian order, to this sink. + * Bit representation of the [float] corresponds to the IEEE 754 floating-point "single format" bit layout. + * + * To obtain a bit representation, the [Float.toBits] function is used. + * + * @param float the floating point number to be written. + * + * @throws IllegalStateException when the sink is closed. + * + * @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.writeFloatLe + */ +public fun Sink.writeFloatLe(float: Float) { + writeIntLe(float.toBits()) +} + +/** + * Writes eight bytes of a bit representation of [double], in the little-endian order, to this sink. + * Bit representation of the [double] corresponds to the IEEE 754 floating-point "double format" bit layout. + * + * To obtain a bit representation, the [Double.toBits] function is used. + * + * @param double the floating point number to be written. + * + * @throws IllegalStateException when the sink is closed. + * + * @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.writeDoubleLe + */ +public fun Sink.writeDoubleLe(double: Double) { + writeLongLe(double.toBits()) +} + /** * Provides direct access to the sink's internal buffer and hints its emit before exit. * diff --git a/core/common/src/Sources.kt b/core/common/src/Sources.kt index 76b5df53a..3a748714a 100644 --- a/core/common/src/Sources.kt +++ b/core/common/src/Sources.kt @@ -357,6 +357,58 @@ public fun Source.readUIntLe(): UInt = readIntLe().toUInt() */ public fun Source.readULongLe(): ULong = readLongLe().toULong() +/** + * Removes four bytes from this source and returns a floating point number with type [Float] composed of it + * according to the big-endian order. + * + * The [Float.Companion.fromBits] function is used for decoding bytes into [Float]. + * + * @throws EOFException when there are not enough data to read an unsigned int value. + * @throws IllegalStateException when the source is closed. + * + * @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.readFloat + */ +public fun Source.readFloat(): Float = Float.fromBits(readInt()) + +/** + * Removes eight bytes from this source and returns a floating point number with type [Double] composed of it + * according to the big-endian order. + * + * The [Double.Companion.fromBits] function is used for decoding bytes into [Double]. + * + * @throws EOFException when there are not enough data to read an unsigned int value. + * @throws IllegalStateException when the source is closed. + * + * @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.readDouble + */ +public fun Source.readDouble(): Double = Double.fromBits(readLong()) + +/** + * Removes four bytes from this source and returns a floating point number with type [Float] composed of it + * according to the little-endian order. + * + * The [Float.Companion.fromBits] function is used for decoding bytes into [Float]. + * + * @throws EOFException when there are not enough data to read an unsigned int value. + * @throws IllegalStateException when the source is closed. + * + * @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.readFloatLe + */ +public fun Source.readFloatLe(): Float = Float.fromBits(readIntLe()) + +/** + * Removes eight bytes from this source and returns a floating point number with type [Double] composed of it + * according to the little-endian order. + * + * The [Double.Companion.fromBits] function is used for decoding bytes into [Double]. + * + * @throws EOFException when there are not enough data to read an unsigned int value. + * @throws IllegalStateException when the source is closed. + * + * @sample kotlinx.io.samples.KotlinxIoCoreCommonSamples.readDoubleLe + */ +public fun Source.readDoubleLe(): Double = Double.fromBits(readLongLe()) + /** * Return `true` if the next byte to be consumed from this source is equal to [byte]. * Otherwise, return `false` as well as when the source is exhausted. diff --git a/core/common/test/AbstractSinkTest.kt b/core/common/test/AbstractSinkTest.kt index 22e048d60..1c2aaa4fe 100644 --- a/core/common/test/AbstractSinkTest.kt +++ b/core/common/test/AbstractSinkTest.kt @@ -449,6 +449,34 @@ abstract class AbstractSinkTest internal constructor( assertEquals("Buffer(size=8 hex=efcdab9078563412)", data.toString()) } + @Test + fun writeFloat() { + sink.writeFloat(12345.678F) + sink.flush() + assertEquals(12345.678F.toBits(), data.readInt()) + } + + @Test + fun writeFloatLe() { + sink.writeFloatLe(12345.678F) + sink.flush() + assertEquals(12345.678F.toBits(), data.readIntLe()) + } + + @Test + fun writeDouble() { + sink.writeDouble(123456.78901) + sink.flush() + assertEquals(123456.78901.toBits(), data.readLong()) + } + + @Test + fun writeDoubleLe() { + sink.writeDoubleLe(123456.78901) + sink.flush() + assertEquals(123456.78901.toBits(), data.readLongLe()) + } + @Test fun writeByteString() { sink.write("təˈranəˌsôr".encodeToByteString()) diff --git a/core/common/test/AbstractSourceTest.kt b/core/common/test/AbstractSourceTest.kt index c27f665f6..da9dbe53c 100644 --- a/core/common/test/AbstractSourceTest.kt +++ b/core/common/test/AbstractSourceTest.kt @@ -1464,6 +1464,70 @@ abstract class AbstractBufferedSourceTest internal constructor( assertEquals(0x78563412u, source.readUIntLe()) } + @Test + fun readFloat() { + sink.write(byteArrayOf(70, 64, -26, -74)) + sink.flush() + assertEquals(12345.678F.toBits(), source.readFloat().toBits()) + } + + @Test + fun readDouble() { + sink.write(byteArrayOf(64, -2, 36, 12, -97, -56, -13, 35)) + sink.flush() + assertEquals(123456.78901, source.readDouble()) + } + + @Test + fun readFloatLe() { + sink.write(byteArrayOf(-74, -26, 64, 70)) + sink.flush() + assertEquals(12345.678F.toBits(), source.readFloatLe().toBits()) + } + + @Test + fun readDoubleLe() { + sink.write(byteArrayOf(35, -13, -56, -97, 12, 36, -2, 64)) + sink.flush() + assertEquals(123456.78901, source.readDoubleLe()) + } + + @Test + fun readTooShortFloatThrows() { + assertFailsWith { source.readFloat() } + sink.writeByte(0) + sink.flush() + assertFailsWith { source.readFloat() } + assertTrue(source.request(1)) + } + + @Test + fun readTooShortDoubleThrows() { + assertFailsWith { source.readDouble() } + sink.writeByte(0) + sink.flush() + assertFailsWith { source.readDouble() } + assertTrue(source.request(1)) + } + + @Test + fun readTooShortFloatLeThrows() { + assertFailsWith { source.readFloatLe() } + sink.writeByte(0) + sink.flush() + assertFailsWith { source.readFloatLe() } + assertTrue(source.request(1)) + } + + @Test + fun readTooShortDoubleLeThrows() { + assertFailsWith { source.readDoubleLe() } + sink.writeByte(0) + sink.flush() + assertFailsWith { source.readDoubleLe() } + assertTrue(source.request(1)) + } + @Test fun readTooShortUnsignedIntThrows() { assertFailsWith { source.readUInt() } diff --git a/core/common/test/samples/samples.kt b/core/common/test/samples/samples.kt index 988670e3e..b3f04ee78 100644 --- a/core/common/test/samples/samples.kt +++ b/core/common/test/samples/samples.kt @@ -488,6 +488,21 @@ class KotlinxIoCoreCommonSamples { assertEquals(18446744073709551615UL, buffer.readULong()) } + @Test + fun readFloat() { + val buffer = Buffer() + buffer.write(byteArrayOf(70, 64, -26, -74)) + assertEquals(12345.678F.toBits(), buffer.readFloat().toBits()) + } + + @Test + fun readDouble() { + val buffer = Buffer() + buffer.write(byteArrayOf(64, -2, 36, 12, -97, -56, -13, 35)) + + assertEquals(123456.78901, buffer.readDouble()) + } + @Test fun writeUByte() { val buffer = Buffer() @@ -520,6 +535,22 @@ class KotlinxIoCoreCommonSamples { assertContentEquals(byteArrayOf(-1, -1, -1, -1, -1, -1, -1, -1), buffer.readByteArray()) } + @Test + fun writeFloat() { + val buffer = Buffer() + buffer.writeFloat(12345.678F) + + assertContentEquals(byteArrayOf(70, 64, -26, -74), buffer.readByteArray()) + } + + @Test + fun writeDouble() { + val buffer = Buffer() + buffer.writeDouble(123456.78901) + + assertContentEquals(byteArrayOf(64, -2, 36, 12, -97, -56, -13, 35), buffer.readByteArray()) + } + @Test fun flush() { val rawSink = object : RawSink { @@ -650,6 +681,20 @@ class KotlinxIoCoreCommonSamples { assertEquals(0xF0DEBC9A78563412U, buffer.readULongLe()) } + @Test + fun readFloatLe() { + val buffer = Buffer() + buffer.write(byteArrayOf(-74, -26, 64, 70)) + assertEquals(12345.678F.toBits(), buffer.readFloatLe().toBits()) + } + + @Test + fun readDoubleLe() { + val buffer = Buffer() + buffer.write(byteArrayOf(35, -13, -56, -97, 12, 36, -2, 64)) + assertEquals(123456.78901, buffer.readDoubleLe()) + } + @Test fun writeUShortLe() { val buffer = Buffer() @@ -670,4 +715,20 @@ class KotlinxIoCoreCommonSamples { buffer.writeULongLe(0x123456789ABCDEF0U) assertEquals(0xF0DEBC9A78563412U, buffer.readULong()) } + + @Test + fun writeFloatLe() { + val buffer = Buffer() + buffer.writeFloatLe(12345.678F) + + assertContentEquals(byteArrayOf(-74, -26, 64, 70), buffer.readByteArray()) + } + + @Test + fun writeDoubleLe() { + val buffer = Buffer() + buffer.writeDoubleLe(123456.78901) + + assertContentEquals(byteArrayOf(35, -13, -56, -97, 12, 36, -2, 64), buffer.readByteArray()) + } } From 40ed6cdfe399bde71c4956fe0e12f87fe840aa6c Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Mon, 7 Aug 2023 08:18:10 +0200 Subject: [PATCH 2/2] ~added comment about NaN --- core/common/src/Sinks.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/common/src/Sinks.kt b/core/common/src/Sinks.kt index f8bb80519..e4b32e10e 100644 --- a/core/common/src/Sinks.kt +++ b/core/common/src/Sinks.kt @@ -281,6 +281,8 @@ public fun Sink.writeULongLe(long: ULong) { * * To obtain a bit representation, the [Float.toBits] function is used. * + * Should be used with care when working with special values (like `NaN`) as bit patterns obtained for [Float.NaN] may vary depending on a platform. + * * @param float the floating point number to be written. * * @throws IllegalStateException when the sink is closed. @@ -297,6 +299,8 @@ public fun Sink.writeFloat(float: Float) { * * To obtain a bit representation, the [Double.toBits] function is used. * + * Should be used with care when working with special values (like `NaN`) as bit patterns obtained for [Double.NaN] may vary depending on a platform. + * * @param double the floating point number to be written. * * @throws IllegalStateException when the sink is closed. @@ -313,6 +317,8 @@ public fun Sink.writeDouble(double: Double) { * * To obtain a bit representation, the [Float.toBits] function is used. * + * Should be used with care when working with special values (like `NaN`) as bit patterns obtained for [Float.NaN] may vary depending on a platform. + * * @param float the floating point number to be written. * * @throws IllegalStateException when the sink is closed. @@ -329,6 +335,8 @@ public fun Sink.writeFloatLe(float: Float) { * * To obtain a bit representation, the [Double.toBits] function is used. * + * Should be used with care when working with special values (like `NaN`) as bit patterns obtained for [Double.NaN] may vary depending on a platform. + * * @param double the floating point number to be written. * * @throws IllegalStateException when the sink is closed.