From 8aad4a876db0dda7da339067f3d06f705362188d Mon Sep 17 00:00:00 2001 From: Ruslan Iushchenko Date: Mon, 15 Jun 2026 13:38:56 +0200 Subject: [PATCH 1/3] #853 Fix IBM single precision floating point (COMP-1) decoder to produce correct results. --- .../decoders/FloatingPointDecoders.scala | 55 ++++-------- .../decoders/FloatingPointDecodersSpec.scala | 84 +++++++++++++++++-- .../source/regression/Test03IbmFloats.scala | 4 +- 3 files changed, 94 insertions(+), 49 deletions(-) diff --git a/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala b/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala index 2972c18f5..c77c1347f 100644 --- a/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala +++ b/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala @@ -117,54 +117,27 @@ object FloatingPointDecoders { /** * Decode IBM single precision big endian encoded number. * - * The source code of this function is based on ibm2ieee NumPy library - * Copyright (c) 2018, Enthought, Inc. - * - * The source code for this method is distributed via 3-clause BSD license. + * The method correctly converts bytes in examples here: + * https://www.ibm.com/docs/en/cobol-zos/6.3.0?topic=data-examples-numeric-internal-representation * * @param bytes An array of bytes * @return a converted single precision floating point number */ def decodeIbmSingleBigEndian(bytes: Array[Byte]): java.lang.Float = { - try { - val IBM32_SIGN_MASK = 0x80000000 - val IBM32_EXPONENT_MASK = 0x80000000 - val IBM32_FRACTURE_MASK = 0x00FFFFFF - val IBM32_MS_NIBBLE = 0x00F00000 + val b0 = bytes(0) & 0xFF + val b1 = bytes(1) & 0xFF + val b2 = bytes(2) & 0xFF + val b3 = bytes(3) & 0xFF - val mantissa = (bytes(0) << 24) | ((bytes(1) & 255) << 16) | ((bytes(2) & 255) << 8) | (bytes(3) & 255) - val sign = mantissa & IBM32_SIGN_MASK - var fracture = mantissa & IBM32_FRACTURE_MASK - var exponent = (mantissa & IBM32_EXPONENT_MASK) >> 22 + val fracInt = (b1 << 16) | (b2 << 8) | b3 + if ((b0 | fracInt) == 0) return 0.0f - if (fracture == 0L) { - 0.0f - } else { - var topNibble = fracture & IBM32_MS_NIBBLE - while (topNibble == 0) { - fracture <<= 4 - exponent -= 4 - topNibble = fracture & IBM32_MS_NIBBLE - } - val leadingZeros = ((BIT_COUNT_MAGIC >> (topNibble >> 19)) & 3).toInt - fracture <<= leadingZeros - val convertedExp = exponent + 131 - leadingZeros - - if (convertedExp >=0 && convertedExp < 254) { - val ieee754Int = sign + (convertedExp << 23) + fracture - java.lang.Float.intBitsToFloat(ieee754Int) - } else if (convertedExp > 254) { - java.lang.Float.POSITIVE_INFINITY - } else if (convertedExp >= -32) { - val mask = ~(0xFFFFFFFD << (-1 - convertedExp)) - val roundUp = if ((fracture & mask) > 0) 1 else 0 - val convertedFract = ((fracture >> (-1 - convertedExp)) + roundUp) >> 1 - val ieee754Int = sign + convertedFract - java.lang.Float.intBitsToFloat(ieee754Int) - } else { - 0.0f - } - } + try { + val fraction = fracInt.toFloat * (1.0f / (1 << 24)) + val sign = if ((b0 & 0x80) != 0) -1.0f else 1.0f + val exp16 = (b0 & 0x7F) - 64 + + sign * Math.scalb(fraction, exp16 * 4) } catch { case NonFatal(_) => null } diff --git a/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecodersSpec.scala b/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecodersSpec.scala index f9e5ab9b1..66db6bb14 100644 --- a/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecodersSpec.scala +++ b/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecodersSpec.scala @@ -29,10 +29,52 @@ class FloatingPointDecodersSpec extends AnyWordSpec { } "decodeIbmSingleBigEndian()" should { - "decode IBM single precision / big-endian FP numbers" in { + "decode IBM single precision / big-endian FP number 1.0" in { val bytes = Array[Byte]( - 0x43.toByte, 0x14.toByte, 0x2E.toByte, 0xFC.toByte) - assertFloatEqual(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes), 5.045883f) + 0x41.toByte, 0x10.toByte, 0x00.toByte, 0x00.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes), 1.0f) + } + + "decode IBM single precision / big-endian FP number 1234.0" in { + val bytes = Array[Byte]( + 0x43.toByte, 0x4D.toByte, 0x20.toByte, 0x00.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes), 1234.0f) + } + + "decode IBM single precision / big-endian FP number -1234.0" in { + val bytes = Array[Byte]( + 0xC3.toByte, 0x4D.toByte, 0x20.toByte, 0x00.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes), -1234.0f) + } + + "decode IBM single precision / big-endian FP number 4.5" in { + val bytes = Array[Byte]( + 0x41.toByte, 0x48.toByte, 0x00.toByte, 0x00.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes), 4.5f) + } + + "decode IBM single precision / big-endian FP number -3.75" in { + val bytes = Array[Byte]( + 0xC1.toByte, 0x3C.toByte, 0x00.toByte, 0x00.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes), -3.75f) + } + + "decode IBM single precision / big-endian FP number 2.5" in { + val bytes = Array[Byte]( + 0x41.toByte, 0x28.toByte, 0x00.toByte, 0x00.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes), 2.5f) + } + + "decode IBM single precision / big-endian FP number 0" in { + val bytes = Array[Byte]( + 0x00.toByte, 0x00.toByte, 0x00.toByte, 0x00.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes), 0f) + } + + "decode IBM single precision / big-endian FP number infinity" in { + val bytes = Array[Byte]( + 0x7F.toByte, 0xFF.toByte, 0xFF.toByte, 0xFF.toByte) + assert(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes).isInfinite) } } @@ -53,10 +95,40 @@ class FloatingPointDecodersSpec extends AnyWordSpec { } "decodeIbmSingleLittleEndian()" should { - "decode IBM single precision / little-endian FP numbers" in { + "decode IBM single precision / little-endian FP number 1.0" in { val bytes = Array[Byte]( - 0xFC.toByte, 0x2E.toByte, 0x14.toByte, 0x43.toByte) - assertFloatEqual(FloatingPointDecoders.decodeIbmSingleLittleEndian(bytes), 5.045883f) + 0x00.toByte, 0x00.toByte, 0x10.toByte, 0x41.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleLittleEndian(bytes), 1.0f) + } + + "decode IBM single precision / little-endian FP number 1234.0" in { + val bytes = Array[Byte]( + 0x00.toByte, 0x20.toByte, 0x4D.toByte, 0x43.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleLittleEndian(bytes), 1234.0f) + } + + "decode IBM single precision / little-endian FP number -1234.0" in { + val bytes = Array[Byte]( + 0x00.toByte, 0x20.toByte, 0x4D.toByte, 0xC3.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleLittleEndian(bytes), -1234.0f) + } + + "decode IBM single precision / little-endian FP number 4.5" in { + val bytes = Array[Byte]( + 0x00.toByte, 0x00.toByte, 0x48.toByte, 0x41.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleLittleEndian(bytes), 4.5f) + } + + "decode IBM single precision / little-endian FP number -3.75" in { + val bytes = Array[Byte]( + 0x00.toByte, 0x00.toByte, 0x3C.toByte, 0xC1.toByte) + assertFloatEqual(FloatingPointDecoders.decodeIbmSingleLittleEndian(bytes), -3.75f) + } + + "decode IBM single precision / big-endian FP number infinity" in { + val bytes = Array[Byte]( + 0xFF.toByte, 0xFF.toByte, 0xFF.toByte, 0x7F.toByte) + assert(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes).isInfinite) } } diff --git a/spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/regression/Test03IbmFloats.scala b/spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/regression/Test03IbmFloats.scala index fc57c6c13..7d5fe747e 100644 --- a/spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/regression/Test03IbmFloats.scala +++ b/spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/regression/Test03IbmFloats.scala @@ -114,7 +114,7 @@ class Test03IbmFloats extends AnyFunSuite with BeforeAndAfterAll with SparkTestB val f = df.select(col("F")).take(1)(0)(0).asInstanceOf[Float] val d = df.select(col("D")).take(1)(0)(0).asInstanceOf[Double] - assertFloatEqual(f, 5.045883f) + assertFloatEqual(f, 322.93652f) assertDoubleEqual(d, 322.936717) } @@ -132,7 +132,7 @@ class Test03IbmFloats extends AnyFunSuite with BeforeAndAfterAll with SparkTestB val f = df.select(col("F")).take(1)(0)(0).asInstanceOf[Float] val d = df.select(col("D")).take(1)(0)(0).asInstanceOf[Double] - assertFloatEqual(f, 5.045883f) + assertFloatEqual(f, 322.93652f) assertDoubleEqual(d, 322.936717) } From 75d513c2959da59bd58be477f7a742e4a9429e61 Mon Sep 17 00:00:00 2001 From: Ruslan Iushchenko Date: Mon, 15 Jun 2026 15:40:52 +0200 Subject: [PATCH 2/3] #853 Fix PR suggestions. --- .../decoders/FloatingPointDecoders.scala | 26 ++++++++++--------- .../decoders/FloatingPointDecodersSpec.scala | 4 +-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala b/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala index c77c1347f..06d778649 100644 --- a/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala +++ b/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala @@ -124,20 +124,22 @@ object FloatingPointDecoders { * @return a converted single precision floating point number */ def decodeIbmSingleBigEndian(bytes: Array[Byte]): java.lang.Float = { - val b0 = bytes(0) & 0xFF - val b1 = bytes(1) & 0xFF - val b2 = bytes(2) & 0xFF - val b3 = bytes(3) & 0xFF - - val fracInt = (b1 << 16) | (b2 << 8) | b3 - if ((b0 | fracInt) == 0) return 0.0f - try { - val fraction = fracInt.toFloat * (1.0f / (1 << 24)) - val sign = if ((b0 & 0x80) != 0) -1.0f else 1.0f - val exp16 = (b0 & 0x7F) - 64 + val b0 = bytes(0) & 0xFF + val b1 = bytes(1) & 0xFF + val b2 = bytes(2) & 0xFF + val b3 = bytes(3) & 0xFF + val fracInt = (b1 << 16) | (b2 << 8) | b3 + + if ((b0 | fracInt) == 0) { + 0.0f + } else { + val fraction = fracInt.toFloat * (1.0f / (1 << 24)) + val sign = if ((b0 & 0x80) != 0) -1.0f else 1.0f + val exp16 = (b0 & 0x7F) - 64 - sign * Math.scalb(fraction, exp16 * 4) + sign * Math.scalb(fraction, exp16 * 4) + } } catch { case NonFatal(_) => null } diff --git a/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecodersSpec.scala b/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecodersSpec.scala index 66db6bb14..96db57388 100644 --- a/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecodersSpec.scala +++ b/cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecodersSpec.scala @@ -125,10 +125,10 @@ class FloatingPointDecodersSpec extends AnyWordSpec { assertFloatEqual(FloatingPointDecoders.decodeIbmSingleLittleEndian(bytes), -3.75f) } - "decode IBM single precision / big-endian FP number infinity" in { + "decode IBM single precision / little-endian FP number infinity" in { val bytes = Array[Byte]( 0xFF.toByte, 0xFF.toByte, 0xFF.toByte, 0x7F.toByte) - assert(FloatingPointDecoders.decodeIbmSingleBigEndian(bytes).isInfinite) + assert(FloatingPointDecoders.decodeIbmSingleLittleEndian(bytes).isInfinite) } } From 69d8325210442ec1b0d7fc65679490cf1bb3a704 Mon Sep 17 00:00:00 2001 From: Ruslan Iushchenko Date: Mon, 15 Jun 2026 16:34:16 +0200 Subject: [PATCH 3/3] #853 Revert IBM COMP-1 decoder to the fastest implementation (measured by microbenchmarking). --- .../decoders/FloatingPointDecoders.scala | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala b/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala index 06d778649..71187e4d7 100644 --- a/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala +++ b/cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/decoders/FloatingPointDecoders.scala @@ -117,28 +117,53 @@ object FloatingPointDecoders { /** * Decode IBM single precision big endian encoded number. * - * The method correctly converts bytes in examples here: - * https://www.ibm.com/docs/en/cobol-zos/6.3.0?topic=data-examples-numeric-internal-representation + * The source code of this function is based on ibm2ieee NumPy library + * Copyright (c) 2018, Enthought, Inc. + * + * The source code for this method is distributed via 3-clause BSD license. * * @param bytes An array of bytes * @return a converted single precision floating point number */ def decodeIbmSingleBigEndian(bytes: Array[Byte]): java.lang.Float = { try { - val b0 = bytes(0) & 0xFF - val b1 = bytes(1) & 0xFF - val b2 = bytes(2) & 0xFF - val b3 = bytes(3) & 0xFF - val fracInt = (b1 << 16) | (b2 << 8) | b3 + val IBM32_SIGN_MASK = 0x80000000 + val IBM32_EXPONENT_MASK = 0x7F000000 + val IBM32_FRACTURE_MASK = 0x00FFFFFF + val IBM32_MS_NIBBLE = 0x00F00000 - if ((b0 | fracInt) == 0) { + val mantissa = (bytes(0) << 24) | ((bytes(1) & 255) << 16) | ((bytes(2) & 255) << 8) | (bytes(3) & 255) + val sign = mantissa & IBM32_SIGN_MASK + var fracture = mantissa & IBM32_FRACTURE_MASK + var exponent = (mantissa & IBM32_EXPONENT_MASK) >> 22 + + if (fracture == 0L) { 0.0f } else { - val fraction = fracInt.toFloat * (1.0f / (1 << 24)) - val sign = if ((b0 & 0x80) != 0) -1.0f else 1.0f - val exp16 = (b0 & 0x7F) - 64 - - sign * Math.scalb(fraction, exp16 * 4) + var topNibble = fracture & IBM32_MS_NIBBLE + while (topNibble == 0) { + fracture <<= 4 + exponent -= 4 + topNibble = fracture & IBM32_MS_NIBBLE + } + val leadingZeros = ((BIT_COUNT_MAGIC >> (topNibble >> 19)) & 3).toInt + fracture <<= leadingZeros + val convertedExp = exponent - 131 - leadingZeros + + if (convertedExp >=0 && convertedExp < 254) { + val ieee754Int = sign + (convertedExp << 23) + fracture + java.lang.Float.intBitsToFloat(ieee754Int) + } else if (convertedExp > 254) { + java.lang.Float.POSITIVE_INFINITY + } else if (convertedExp >= -32) { + val mask = ~(0xFFFFFFFD << (-1 - convertedExp)) + val roundUp = if ((fracture & mask) > 0) 1 else 0 + val convertedFract = ((fracture >> (-1 - convertedExp)) + roundUp) >> 1 + val ieee754Int = sign + convertedFract + java.lang.Float.intBitsToFloat(ieee754Int) + } else { + 0.0f + } } } catch { case NonFatal(_) => null