From d58770b36c4677f769de8ec0fb63ec5f57ea9493 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 29 Dec 2024 21:22:22 +0100 Subject: [PATCH 01/35] feature: add IndexedStringBuilder --- .../interpreter/IndexedStringBuilder.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt new file mode 100644 index 000000000..963751368 --- /dev/null +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt @@ -0,0 +1,82 @@ +package com.smeup.rpgparser.interpreter + +/** + * Creates a string builder that allows to replace substrings in a more efficient way. + * The string is divided into chunks of a fixed size. + * When replacing a substring, only the affected chunks are modified. + * This is useful when the string is very large and only a small part of it is modified. + * @param value The initial value of the string builder + * @param chunkSize The size of the chunks + */ +class IndexedStringBuilder(value: String, private val chunkSize: Int) { + + // The string is divided into chunks of a fixed size + private val chunks: List = List((value.length + chunkSize - 1) / chunkSize) { index -> + StringBuilder(value.substring(index * chunkSize, minOf((index + 1) * chunkSize, value.length))) + } + + /*** + * Replace the substring from start to end with the replacing string. + * The length of the replacing string must be equal to the length of the replaced string. + * @param start The start index of the substring to replace + * @param end The end index of the substring to replace + */ + fun replace(start: Int, end: Int, replacingString: String) { + require(end > start) { "End index must be greater than start index." } + require(replacingString.length == end - start) { "Replacing string length must match the length of the substring being replaced." } + + var remaining = replacingString + var currentIndex = 0 + + for (chunk in chunks) { + val chunkStart = currentIndex + val chunkEnd = currentIndex + chunk.length + + if (start < chunkEnd && end > chunkStart) { + val relativeStart = maxOf(0, start - chunkStart) + val relativeEnd = minOf(chunk.length, end - chunkStart) + val replaceStart = maxOf(0, start - chunkStart - relativeStart) + val replaceEnd = replaceStart + (relativeEnd - relativeStart) + + chunk.replace(relativeStart, relativeEnd, remaining.substring(replaceStart, replaceEnd)) + + remaining = remaining.substring(replaceEnd - replaceStart) + } + + currentIndex += chunk.length + } + } + + /*** + * Returns the substring from start to end. + * @param start The start index of the substring + * @param end The end index of the substring + * @return The substring from start to end + */ + fun substring(start: Int, end: Int): String { + require(end >= start) { "End index must be greater than or equal to start index." } + + if (start == end) return "" + + val result = StringBuilder() + var currentIndex = 0 + for (chunk in chunks) { + val chunkStart = currentIndex + val chunkEnd = currentIndex + chunk.length + + if (start < chunkEnd && end > chunkStart) { + val relativeStart = maxOf(0, start - chunkStart) + val relativeEnd = minOf(chunk.length, end - chunkStart) + result.append(chunk.substring(relativeStart, relativeEnd)) + } + + currentIndex += chunk.length + } + + return result.toString() + } + + override fun toString(): String { + return chunks.joinToString(separator = "") { it.toString() } + } +} \ No newline at end of file From a1c1cd46bfef17f6c91397d788c9da49e72a8f73 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 29 Dec 2024 21:22:50 +0100 Subject: [PATCH 02/35] feature: add IndexedStringBuilderTest --- .../interpreter/IndexedStringBuilderTest.kt | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt new file mode 100644 index 000000000..9b9f6f181 --- /dev/null +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt @@ -0,0 +1,175 @@ +package com.smeup.rpgparser.interpreter + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.time.DurationUnit +import kotlin.time.measureTime + +class IndexedStringBuilderTest { + + @Test + fun replaceWithinSingleChunkFirst() { + val builder = IndexedStringBuilder("HelloWorld", 5) + builder.replace(0, 5, "12345") + assertEquals("12345World", builder.toString()) + } + + @Test + fun replaceWithinSingleChunkMiddle() { + val builder = IndexedStringBuilder("HelloWorldHello", 5) + builder.replace(5, 10, "12345") + assertEquals("Hello12345Hello", builder.toString()) + } + + @Test + fun replaceWithinSingleChunkLast() { + val builder = IndexedStringBuilder("HelloWorldHello", 5) + builder.replace(10, 15, "12345") + assertEquals("HelloWorld12345", builder.toString()) + } + + @Test + fun replaceAllChunks() { + val builder = IndexedStringBuilder("HelloWorld", 5) + builder.replace(0, 10, "1234567890") + assertEquals("1234567890", builder.toString()) + } + + @Test + fun replaceAcrossMultipleChunks() { + val builder = IndexedStringBuilder("HelloWorld", 2) + builder.replace(3, 8, "12345") + assertEquals("Hel12345ld", builder.toString()) + } + + @Test + fun replaceWithReplacingStringGreaterThanReplacedRangeThrowsException() { + val builder = IndexedStringBuilder("HelloWorld", 5) + assertFailsWith { + builder.replace(0, 5, "HelloWorld") + } + } + + @Test + fun replaceWithReplacingStringLowerThanReplacedRangeThrowsException() { + val builder = IndexedStringBuilder("HelloWorld", 5) + assertFailsWith { + builder.replace(0, 5, "H") + } + } + + @Test + fun substringWithinSingleChunk() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.substring(0, 5) + assertEquals("Hello", result) + } + + @Test + fun substringAcrossMultipleChunks() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.substring(3, 8) + assertEquals("loWor", result) + } + + @Test + fun substringWithExactLength() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.substring(0, 10) + assertEquals("HelloWorld", result) + } + + @Test + fun substringWithEmptyResult() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.substring(5, 5) + assertEquals("", result) + } + + private fun compareStringBuilderAndIndexedStringBuilderPerformance(descriptionTest: String, stringSize: Int, iterations: Int) { + println("Test: $descriptionTest") + require(stringSize % 10 == 0) { "stringSize must be a multiple of 10" } + val replacingString = "b".repeat(stringSize / 10) + val sbDuration = measureTime { + for (i in 0 until iterations) { + val sb = StringBuilder("a".repeat(stringSize)) + val step = stringSize / 10 + for (j in 0 until step) { + val start: Int = j * step + val end: Int = start + step + sb.replace(start, end, replacingString) + } + } + } + + val indexedSbDuration = measureTime { + for (i in 0 until iterations) { + val indexedSb = IndexedStringBuilder("a".repeat(stringSize), stringSize / 10) + val step = stringSize / 10 + for (j in 0 until step) { + val start: Int = j*step + val end: Int = start + step + indexedSb.replace(start, end, replacingString) + } + } + } + + println("StringBuilder: ${sbDuration.toLong(DurationUnit.MILLISECONDS)}ms") + println("IndexedStringBuilder: ${indexedSbDuration.toLong(DurationUnit.MILLISECONDS)}ms") + } + + @Test + fun littleStringFewLoopPerformance() { + val stringSize = 100_000 + val iterations = 100 + compareStringBuilderAndIndexedStringBuilderPerformance("littleStringFewLoopPerformance", stringSize, iterations) + } + +// @Test +// fun evalStringBuilderPerformances() { +// // DS elements +// val elements = 1_000 +// +// // Fields in each element +// val fields = 100 +// +// // Field size +// val fieldSize = 100 +// +// // DS value as StringBuilder +// val sb = StringBuilder("a".repeat(elements*fields*fieldSize)) +// +// // DS value as Indexed StringBuilder +// val indexedSb = MutableList(elements) { StringBuilder("a".repeat(fields*fieldSize)) } +// +// val replacingString = "b".repeat(fieldSize) +// +// val randomField = List(fields) { (0.. Date: Sun, 29 Dec 2024 22:29:14 +0100 Subject: [PATCH 03/35] improve replace function --- .../com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt index 963751368..8d2fe8998 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt @@ -31,7 +31,9 @@ class IndexedStringBuilder(value: String, private val chunkSize: Int) { for (chunk in chunks) { val chunkStart = currentIndex val chunkEnd = currentIndex + chunk.length - + if (chunkEnd > end) { + break + } if (start < chunkEnd && end > chunkStart) { val relativeStart = maxOf(0, start - chunkStart) val relativeEnd = minOf(chunk.length, end - chunkStart) From ec834082080a2e5fd8b0f0d3824cf1c3e49ddb8a Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 29 Dec 2024 23:04:06 +0100 Subject: [PATCH 04/35] improve replace function that now works only with the chunks implied --- .../smeup/rpgparser/interpreter/IndexedStringBuilder.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt index 8d2fe8998..77de53ffd 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt @@ -25,10 +25,13 @@ class IndexedStringBuilder(value: String, private val chunkSize: Int) { require(end > start) { "End index must be greater than start index." } require(replacingString.length == end - start) { "Replacing string length must match the length of the substring being replaced." } + val firstChunkIndex = start / chunkSize + val lastChunkIndex = (end / chunkSize).let { if (it >= chunks.size) it - 1 else it } + val subChunks = chunks.subList(firstChunkIndex, lastChunkIndex + 1) var remaining = replacingString - var currentIndex = 0 + var currentIndex = firstChunkIndex * chunkSize - for (chunk in chunks) { + for (chunk in subChunks) { val chunkStart = currentIndex val chunkEnd = currentIndex + chunk.length if (chunkEnd > end) { From 422c4b0421e8044f842f19a803da50d8ac570bf6 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 13:08:07 +0100 Subject: [PATCH 05/35] refactor: move IndexedStringBuilder comparison logic --- .../IndexedStringBuilderEvaluator.kt | 79 ++++++++++++++++ .../interpreter/IndexedStringBuilderTest.kt | 90 +------------------ 2 files changed, 80 insertions(+), 89 deletions(-) create mode 100644 rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt new file mode 100644 index 000000000..c08da28b8 --- /dev/null +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt @@ -0,0 +1,79 @@ +package com.smeup.rpgparser.interpreter + +import java.math.RoundingMode +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.measureTime + +private fun compareStringBuilderAndIndexedStringBuilderPerformance( + printHeader: Boolean = false, + debugInformation: Boolean = false, + stringSize: Int, + fields: Int, + iterations: Int, + stringBuilderDone: (duration: Duration) -> Unit, + indexedStringBuilderDone: (duration: Duration) -> Unit +) { + require(stringSize >= fields) { "stringSize must be greater than or equal to elements" } + require(stringSize % fields == 0) { "stringSize: $stringSize must a multiple of fields: $fields" } + + if (printHeader) { + println("stringSize, ${if (debugInformation) "fields, " else "" }chunksSize, ratio${if (debugInformation) ", duration(ms)" else "" }") + } + + print("$stringSize, ${if (debugInformation) "$fields, " else ""}${stringSize / fields}") + + val replacingString = "b".repeat(stringSize / fields) + val sbDuration = measureTime { + val sb = StringBuilder("a".repeat(stringSize)) + for (i in 0 until iterations) { + val replacingChars = stringSize / fields + for (j in 0 until fields) { + val start: Int = j * replacingChars + val end: Int = start + replacingChars + sb.replace(start, end, replacingString) + } + } + } + stringBuilderDone(sbDuration) + + val indexedSbDuration = measureTime { + val indexedSb = IndexedStringBuilder("a".repeat(stringSize), stringSize / fields) + for (i in 0 until iterations) { + val replacingChars = stringSize / fields + for (j in 0 until fields) { + val start: Int = j * replacingChars + val end: Int = start + replacingChars + indexedSb.replace(start, end, replacingString) + } + } + } + + indexedStringBuilderDone(indexedSbDuration) + + val ratio = sbDuration.div(indexedSbDuration).toBigDecimal().setScale(2, RoundingMode.HALF_DOWN).toDouble() + + println(", $ratio${if (debugInformation) ", ${sbDuration.plus(indexedSbDuration).toLong(DurationUnit.MILLISECONDS)}" else ""}") +} + +private fun createPerformanceComparisonDataset() { + for (stringSize in listOf(10, 100, 500, 1000, 2_000, 5_000, 10_000, 50_000, 100_000, 1_000_000, 10_000_000)) { + for (fields in listOf(1, 2, 5, 10, 20, 50, 100, 200, 500, 1000)) { + if (stringSize < fields || ((stringSize % fields) != 0)) { + break + } + compareStringBuilderAndIndexedStringBuilderPerformance( + printHeader = stringSize == 10 && fields == 1, + stringSize = stringSize, + fields = fields, + iterations = if (stringSize*fields < 100_000) 1_000_000 else if (stringSize*fields < 1_000_000) 100_000 else 100, + stringBuilderDone = { }, + indexedStringBuilderDone = { } + ) + } + } +} + +fun main() { + createPerformanceComparisonDataset() +} \ No newline at end of file diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt index 9b9f6f181..7044342d4 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt @@ -3,8 +3,6 @@ package com.smeup.rpgparser.interpreter import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith -import kotlin.time.DurationUnit -import kotlin.time.measureTime class IndexedStringBuilderTest { @@ -86,90 +84,4 @@ class IndexedStringBuilderTest { val result = builder.substring(5, 5) assertEquals("", result) } - - private fun compareStringBuilderAndIndexedStringBuilderPerformance(descriptionTest: String, stringSize: Int, iterations: Int) { - println("Test: $descriptionTest") - require(stringSize % 10 == 0) { "stringSize must be a multiple of 10" } - val replacingString = "b".repeat(stringSize / 10) - val sbDuration = measureTime { - for (i in 0 until iterations) { - val sb = StringBuilder("a".repeat(stringSize)) - val step = stringSize / 10 - for (j in 0 until step) { - val start: Int = j * step - val end: Int = start + step - sb.replace(start, end, replacingString) - } - } - } - - val indexedSbDuration = measureTime { - for (i in 0 until iterations) { - val indexedSb = IndexedStringBuilder("a".repeat(stringSize), stringSize / 10) - val step = stringSize / 10 - for (j in 0 until step) { - val start: Int = j*step - val end: Int = start + step - indexedSb.replace(start, end, replacingString) - } - } - } - - println("StringBuilder: ${sbDuration.toLong(DurationUnit.MILLISECONDS)}ms") - println("IndexedStringBuilder: ${indexedSbDuration.toLong(DurationUnit.MILLISECONDS)}ms") - } - - @Test - fun littleStringFewLoopPerformance() { - val stringSize = 100_000 - val iterations = 100 - compareStringBuilderAndIndexedStringBuilderPerformance("littleStringFewLoopPerformance", stringSize, iterations) - } - -// @Test -// fun evalStringBuilderPerformances() { -// // DS elements -// val elements = 1_000 -// -// // Fields in each element -// val fields = 100 -// -// // Field size -// val fieldSize = 100 -// -// // DS value as StringBuilder -// val sb = StringBuilder("a".repeat(elements*fields*fieldSize)) -// -// // DS value as Indexed StringBuilder -// val indexedSb = MutableList(elements) { StringBuilder("a".repeat(fields*fieldSize)) } -// -// val replacingString = "b".repeat(fieldSize) -// -// val randomField = List(fields) { (0.. Date: Wed, 1 Jan 2025 14:39:58 +0100 Subject: [PATCH 06/35] typo --- .../rpgparser/interpreter/IndexedStringBuilder.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt index 77de53ffd..e55e50778 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt @@ -6,13 +6,13 @@ package com.smeup.rpgparser.interpreter * When replacing a substring, only the affected chunks are modified. * This is useful when the string is very large and only a small part of it is modified. * @param value The initial value of the string builder - * @param chunkSize The size of the chunks + * @param chunksSize The size of the chunks */ -class IndexedStringBuilder(value: String, private val chunkSize: Int) { +class IndexedStringBuilder(value: String, private val chunksSize: Int) { // The string is divided into chunks of a fixed size - private val chunks: List = List((value.length + chunkSize - 1) / chunkSize) { index -> - StringBuilder(value.substring(index * chunkSize, minOf((index + 1) * chunkSize, value.length))) + private val chunks: List = List((value.length + chunksSize - 1) / chunksSize) { index -> + StringBuilder(value.substring(index * chunksSize, minOf((index + 1) * chunksSize, value.length))) } /*** @@ -25,11 +25,11 @@ class IndexedStringBuilder(value: String, private val chunkSize: Int) { require(end > start) { "End index must be greater than start index." } require(replacingString.length == end - start) { "Replacing string length must match the length of the substring being replaced." } - val firstChunkIndex = start / chunkSize - val lastChunkIndex = (end / chunkSize).let { if (it >= chunks.size) it - 1 else it } + val firstChunkIndex = start / chunksSize + val lastChunkIndex = (end / chunksSize).let { if (it >= chunks.size) it - 1 else it } val subChunks = chunks.subList(firstChunkIndex, lastChunkIndex + 1) var remaining = replacingString - var currentIndex = firstChunkIndex * chunkSize + var currentIndex = firstChunkIndex * chunksSize for (chunk in subChunks) { val chunkStart = currentIndex From f73d4f31c703eb689a3f6488f1561053d8cfdf9b Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 14:57:35 +0100 Subject: [PATCH 07/35] refactor: pass chunksSize to compareStringBuilderAndIndexedStringBuilderPerformance --- .../interpreter/IndexedStringBuilderEvaluator.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt index c08da28b8..e67f04f92 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt @@ -10,6 +10,7 @@ private fun compareStringBuilderAndIndexedStringBuilderPerformance( debugInformation: Boolean = false, stringSize: Int, fields: Int, + chunksSize: Int, iterations: Int, stringBuilderDone: (duration: Duration) -> Unit, indexedStringBuilderDone: (duration: Duration) -> Unit @@ -21,7 +22,7 @@ private fun compareStringBuilderAndIndexedStringBuilderPerformance( println("stringSize, ${if (debugInformation) "fields, " else "" }chunksSize, ratio${if (debugInformation) ", duration(ms)" else "" }") } - print("$stringSize, ${if (debugInformation) "$fields, " else ""}${stringSize / fields}") + print("$stringSize, ${if (debugInformation) "$fields, " else ""}$chunksSize") val replacingString = "b".repeat(stringSize / fields) val sbDuration = measureTime { @@ -38,7 +39,7 @@ private fun compareStringBuilderAndIndexedStringBuilderPerformance( stringBuilderDone(sbDuration) val indexedSbDuration = measureTime { - val indexedSb = IndexedStringBuilder("a".repeat(stringSize), stringSize / fields) + val indexedSb = IndexedStringBuilder("a".repeat(stringSize), chunksSize) for (i in 0 until iterations) { val replacingChars = stringSize / fields for (j in 0 until fields) { @@ -56,7 +57,7 @@ private fun compareStringBuilderAndIndexedStringBuilderPerformance( println(", $ratio${if (debugInformation) ", ${sbDuration.plus(indexedSbDuration).toLong(DurationUnit.MILLISECONDS)}" else ""}") } -private fun createPerformanceComparisonDataset() { +private fun createPerformanceComparisonDataset(chunksSize: (stringSize: Int, fields: Int) -> Int = { stringSize, fields -> stringSize / fields }) { for (stringSize in listOf(10, 100, 500, 1000, 2_000, 5_000, 10_000, 50_000, 100_000, 1_000_000, 10_000_000)) { for (fields in listOf(1, 2, 5, 10, 20, 50, 100, 200, 500, 1000)) { if (stringSize < fields || ((stringSize % fields) != 0)) { @@ -66,6 +67,7 @@ private fun createPerformanceComparisonDataset() { printHeader = stringSize == 10 && fields == 1, stringSize = stringSize, fields = fields, + chunksSize = chunksSize(stringSize, fields), iterations = if (stringSize*fields < 100_000) 1_000_000 else if (stringSize*fields < 1_000_000) 100_000 else 100, stringBuilderDone = { }, indexedStringBuilderDone = { } From 24e70049064dd75e4bc9e72d8b9073955851acba Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 15:58:20 +0100 Subject: [PATCH 08/35] refactor: move IndexedStringBuilder to datastruct_value_utils module --- ...xedStringBuilder.kt => datastruct_value_utils.kt} | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) rename rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/{IndexedStringBuilder.kt => datastruct_value_utils.kt} (89%) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt similarity index 89% rename from rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt rename to rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt index e55e50778..3ef19958c 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilder.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt @@ -1,5 +1,11 @@ package com.smeup.rpgparser.interpreter +internal interface DataStructStringValue { + + fun replace(start: Int, end: Int, replacingString: String) + fun substring(start: Int, end: Int): String +} + /** * Creates a string builder that allows to replace substrings in a more efficient way. * The string is divided into chunks of a fixed size. @@ -8,7 +14,7 @@ package com.smeup.rpgparser.interpreter * @param value The initial value of the string builder * @param chunksSize The size of the chunks */ -class IndexedStringBuilder(value: String, private val chunksSize: Int) { +internal class IndexedStringBuilder(value: String, private val chunksSize: Int) : DataStructStringValue { // The string is divided into chunks of a fixed size private val chunks: List = List((value.length + chunksSize - 1) / chunksSize) { index -> @@ -21,7 +27,7 @@ class IndexedStringBuilder(value: String, private val chunksSize: Int) { * @param start The start index of the substring to replace * @param end The end index of the substring to replace */ - fun replace(start: Int, end: Int, replacingString: String) { + override fun replace(start: Int, end: Int, replacingString: String) { require(end > start) { "End index must be greater than start index." } require(replacingString.length == end - start) { "Replacing string length must match the length of the substring being replaced." } @@ -58,7 +64,7 @@ class IndexedStringBuilder(value: String, private val chunksSize: Int) { * @param end The end index of the substring * @return The substring from start to end */ - fun substring(start: Int, end: Int): String { + override fun substring(start: Int, end: Int): String { require(end >= start) { "End index must be greater than or equal to start index." } if (start == end) return "" From 189e952d92b23dff0fcd227474ade3e9415281c9 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 16:06:25 +0100 Subject: [PATCH 09/35] feat: add NotIndexedStringBuilder and DataStructStringValue.create function --- .../interpreter/datastruct_value_utils.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt index 3ef19958c..a943c7d52 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt @@ -1,9 +1,65 @@ package com.smeup.rpgparser.interpreter +import kotlin.math.pow + +/** + * Interface representing a value of a data structure string. + */ internal interface DataStructStringValue { + /** + * Replaces a substring within the data structure string. + * + * @param start The start index of the substring to replace. + * @param end The end index of the substring to replace. + * @param replacingString The string to replace the substring with. + */ fun replace(start: Int, end: Int, replacingString: String) + + /** + * Returns a substring from the data structure string. + * + * @param start The start index of the substring. + * @param end The end index of the substring. + * @return The substring from start to end. + */ fun substring(start: Int, end: Int): String + + companion object { + + /** + * Creates an instance of DataStructStringValue based on the given value and elements. + * + * @param value The initial value of the data structure string. + * @param elements The number of elements to divide the string into. + * @return An instance of DataStructStringValue. The algorithm to create the instance is chosen based on the value and elements. + */ + fun create(value: String, elements: Int): DataStructStringValue { + val chunksSize = value.length / elements + return if (chunksSize <= value.length.toDouble().pow(0.698) / 5102.41) { + IndexedStringBuilder(value, chunksSize) + } else { + NotIndexedStringBuilder(value) + } + } + } +} + +internal class NotIndexedStringBuilder(value: String) : DataStructStringValue { + + private val sb = StringBuilder(value) + + override fun replace(start: Int, end: Int, replacingString: String) { + sb.replace(start, end, replacingString) + } + + override fun substring(start: Int, end: Int): String { + return sb.substring(start, end) + } + + override fun toString(): String { + return sb.toString() + } } /** From e304e699b2343fd240a338bb6ef1bc4a0d0d0454 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 16:10:58 +0100 Subject: [PATCH 10/35] perf: improve substring by searching into subChunks --- .../smeup/rpgparser/interpreter/datastruct_value_utils.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt index a943c7d52..fd58bac75 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt @@ -125,9 +125,13 @@ internal class IndexedStringBuilder(value: String, private val chunksSize: Int) if (start == end) return "" + val firstChunkIndex = start / chunksSize + val lastChunkIndex = (end / chunksSize).let { if (it >= chunks.size) it - 1 else it } + val subChunks = chunks.subList(firstChunkIndex, lastChunkIndex + 1) + val result = StringBuilder() var currentIndex = 0 - for (chunk in chunks) { + for (chunk in subChunks) { val chunkStart = currentIndex val chunkEnd = currentIndex + chunk.length From 65f3635906d285e3cf5dde604dbed98d72f5ba6e Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 16:22:32 +0100 Subject: [PATCH 11/35] refactor: change function name a pass to stringBuilderVsDataStructStringValue a function that allows to create DataStructStringValue --- .../interpreter/IndexedStringBuilderEvaluator.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt index e67f04f92..702c00d1f 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt @@ -5,13 +5,19 @@ import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.measureTime -private fun compareStringBuilderAndIndexedStringBuilderPerformance( +private fun stringBuilderVsDataStructStringValue( printHeader: Boolean = false, debugInformation: Boolean = false, stringSize: Int, fields: Int, chunksSize: Int, iterations: Int, + dataStructStringValue: (value: String, chunksSize: Int) -> DataStructStringValue = { value, chunksSizeParam -> + IndexedStringBuilder( + value, + chunksSizeParam + ) + }, stringBuilderDone: (duration: Duration) -> Unit, indexedStringBuilderDone: (duration: Duration) -> Unit ) { @@ -39,7 +45,7 @@ private fun compareStringBuilderAndIndexedStringBuilderPerformance( stringBuilderDone(sbDuration) val indexedSbDuration = measureTime { - val indexedSb = IndexedStringBuilder("a".repeat(stringSize), chunksSize) + val indexedSb = dataStructStringValue("a".repeat(stringSize), chunksSize) for (i in 0 until iterations) { val replacingChars = stringSize / fields for (j in 0 until fields) { @@ -63,7 +69,7 @@ private fun createPerformanceComparisonDataset(chunksSize: (stringSize: Int, fie if (stringSize < fields || ((stringSize % fields) != 0)) { break } - compareStringBuilderAndIndexedStringBuilderPerformance( + stringBuilderVsDataStructStringValue( printHeader = stringSize == 10 && fields == 1, stringSize = stringSize, fields = fields, From 544ac00de62f5488a4c1f97be19bc25b8f8631f0 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 16:23:28 +0100 Subject: [PATCH 12/35] format --- .../rpgparser/interpreter/IndexedStringBuilderEvaluator.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt index 702c00d1f..9f2153461 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt @@ -63,7 +63,9 @@ private fun stringBuilderVsDataStructStringValue( println(", $ratio${if (debugInformation) ", ${sbDuration.plus(indexedSbDuration).toLong(DurationUnit.MILLISECONDS)}" else ""}") } -private fun createPerformanceComparisonDataset(chunksSize: (stringSize: Int, fields: Int) -> Int = { stringSize, fields -> stringSize / fields }) { +private fun createPerformanceComparisonDataset( + chunksSize: (stringSize: Int, fields: Int) -> Int = { stringSize, fields -> stringSize / fields } +) { for (stringSize in listOf(10, 100, 500, 1000, 2_000, 5_000, 10_000, 50_000, 100_000, 1_000_000, 10_000_000)) { for (fields in listOf(1, 2, 5, 10, 20, 50, 100, 200, 500, 1000)) { if (stringSize < fields || ((stringSize % fields) != 0)) { @@ -74,7 +76,7 @@ private fun createPerformanceComparisonDataset(chunksSize: (stringSize: Int, fie stringSize = stringSize, fields = fields, chunksSize = chunksSize(stringSize, fields), - iterations = if (stringSize*fields < 100_000) 1_000_000 else if (stringSize*fields < 1_000_000) 100_000 else 100, + iterations = if (stringSize * fields < 100_000) 1_000_000 else if (stringSize * fields < 1_000_000) 100_000 else 100, stringBuilderDone = { }, indexedStringBuilderDone = { } ) From 988a0107466135ce77e4a8a5ff5e472568d5bd4e Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 16:52:20 +0100 Subject: [PATCH 13/35] typo --- .../rpgparser/interpreter/datastruct_value_utils.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt index fd58bac75..59a0aa7c2 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt @@ -28,14 +28,14 @@ internal interface DataStructStringValue { companion object { /** - * Creates an instance of DataStructStringValue based on the given value and elements. + * Creates an instance of DataStructStringValue based on the given value and fields. * * @param value The initial value of the data structure string. - * @param elements The number of elements to divide the string into. - * @return An instance of DataStructStringValue. The algorithm to create the instance is chosen based on the value and elements. + * @param fields The number of fields to divide the string into. + * @return An instance of DataStructStringValue. The algorithm to create the instance is chosen based on the value and fields. */ - fun create(value: String, elements: Int): DataStructStringValue { - val chunksSize = value.length / elements + fun create(value: String, fields: Int): DataStructStringValue { + val chunksSize = value.length / fields return if (chunksSize <= value.length.toDouble().pow(0.698) / 5102.41) { IndexedStringBuilder(value, chunksSize) } else { From 4ed74a95a8f8ae0c6970300ebd634f26b39c47bc Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 19:13:23 +0100 Subject: [PATCH 14/35] refactor: change algorithm for choice of StringBuilder rather than IndexedStringBuilder --- .../interpreter/datastruct_value_utils.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt index 59a0aa7c2..e8a38ffbd 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt @@ -1,7 +1,5 @@ package com.smeup.rpgparser.interpreter -import kotlin.math.pow - /** * Interface representing a value of a data structure string. */ @@ -35,13 +33,20 @@ internal interface DataStructStringValue { * @return An instance of DataStructStringValue. The algorithm to create the instance is chosen based on the value and fields. */ fun create(value: String, fields: Int): DataStructStringValue { - val chunksSize = value.length / fields - return if (chunksSize <= value.length.toDouble().pow(0.698) / 5102.41) { - IndexedStringBuilder(value, chunksSize) + val stringSize = value.length + + return if (useIndexedStringBuilder(stringSize = stringSize, fields = fields)) { + IndexedStringBuilder(value = value, chunksSize = stringSize / fields) } else { NotIndexedStringBuilder(value) } } + + private fun useIndexedStringBuilder(stringSize: Int, fields: Int): Boolean { + if (stringSize >= 9000) return true + if (stringSize >= 2000 && fields >= 5) return true + return false + } } } From 2ca4e8ebdd1b4891d202340b1989d2ec3b49a966 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 19:14:50 +0100 Subject: [PATCH 15/35] chore: createPerformanceComparisonDataset compare the ratio both sb/indexed_sb and sb/ds_string_value --- .../IndexedStringBuilderEvaluator.kt | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt index 9f2153461..3391df2cd 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt @@ -1,7 +1,6 @@ package com.smeup.rpgparser.interpreter import java.math.RoundingMode -import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.measureTime @@ -10,25 +9,19 @@ private fun stringBuilderVsDataStructStringValue( debugInformation: Boolean = false, stringSize: Int, fields: Int, - chunksSize: Int, - iterations: Int, - dataStructStringValue: (value: String, chunksSize: Int) -> DataStructStringValue = { value, chunksSizeParam -> - IndexedStringBuilder( - value, - chunksSizeParam - ) - }, - stringBuilderDone: (duration: Duration) -> Unit, - indexedStringBuilderDone: (duration: Duration) -> Unit + iterations: Int ) { require(stringSize >= fields) { "stringSize must be greater than or equal to elements" } require(stringSize % fields == 0) { "stringSize: $stringSize must a multiple of fields: $fields" } + val chunksSize = stringSize / fields + if (printHeader) { - println("stringSize, ${if (debugInformation) "fields, " else "" }chunksSize, ratio${if (debugInformation) ", duration(ms)" else "" }") + println("stringSize, fields${if (debugInformation) ", chunksSize" else ""}, ratio(sb/indexed_sb), " + + "ratio(sb/ds_string_value)${if (debugInformation) ", duration(ms)" else ""}") } - print("$stringSize, ${if (debugInformation) "$fields, " else ""}$chunksSize") + print("$stringSize, $fields${if (debugInformation) ", $chunksSize" else ""}") val replacingString = "b".repeat(stringSize / fields) val sbDuration = measureTime { @@ -42,10 +35,9 @@ private fun stringBuilderVsDataStructStringValue( } } } - stringBuilderDone(sbDuration) val indexedSbDuration = measureTime { - val indexedSb = dataStructStringValue("a".repeat(stringSize), chunksSize) + val indexedSb = IndexedStringBuilder("a".repeat(stringSize), chunksSize) for (i in 0 until iterations) { val replacingChars = stringSize / fields for (j in 0 until fields) { @@ -56,29 +48,44 @@ private fun stringBuilderVsDataStructStringValue( } } - indexedStringBuilderDone(indexedSbDuration) - - val ratio = sbDuration.div(indexedSbDuration).toBigDecimal().setScale(2, RoundingMode.HALF_DOWN).toDouble() + val dataStructStringValueDuration = measureTime { + val dataStructStringValue = DataStructStringValue.create("a".repeat(stringSize), fields) + for (i in 0 until iterations) { + val replacingChars = stringSize / fields + for (j in 0 until fields) { + val start: Int = j * replacingChars + val end: Int = start + replacingChars + dataStructStringValue.replace(start, end, replacingString) + } + } + } - println(", $ratio${if (debugInformation) ", ${sbDuration.plus(indexedSbDuration).toLong(DurationUnit.MILLISECONDS)}" else ""}") + val ratioSbIndexed = sbDuration.div(indexedSbDuration).toBigDecimal().setScale(2, RoundingMode.HALF_DOWN).toDouble() + val ratioSbDataStructStringValue = sbDuration.div(dataStructStringValueDuration).toBigDecimal().setScale(2, RoundingMode.HALF_DOWN).toDouble() + println(", $ratioSbIndexed, $ratioSbDataStructStringValue${if (debugInformation) ", ${sbDuration.plus(indexedSbDuration).toLong(DurationUnit.MILLISECONDS)}" else ""}") } -private fun createPerformanceComparisonDataset( - chunksSize: (stringSize: Int, fields: Int) -> Int = { stringSize, fields -> stringSize / fields } -) { - for (stringSize in listOf(10, 100, 500, 1000, 2_000, 5_000, 10_000, 50_000, 100_000, 1_000_000, 10_000_000)) { +private fun createPerformanceComparisonDataset() { + val stringSizeTests = mutableListOf() + val startStringSizeValue = 1000 + var value = startStringSizeValue + while (value <= 100_000) { + for (i in 1..9) { + stringSizeTests.add(value * i) + } + value *= 10 + } + + for (stringSize in stringSizeTests) { for (fields in listOf(1, 2, 5, 10, 20, 50, 100, 200, 500, 1000)) { if (stringSize < fields || ((stringSize % fields) != 0)) { break } stringBuilderVsDataStructStringValue( - printHeader = stringSize == 10 && fields == 1, + printHeader = stringSize == startStringSizeValue && fields == 1, stringSize = stringSize, fields = fields, - chunksSize = chunksSize(stringSize, fields), - iterations = if (stringSize * fields < 100_000) 1_000_000 else if (stringSize * fields < 1_000_000) 100_000 else 100, - stringBuilderDone = { }, - indexedStringBuilderDone = { } + iterations = if (stringSize * fields < 100_000) 1_000_000 else if (stringSize * fields < 1_000_000) 100_000 else 100 ) } } From 1973e566db705eb48baee6bff650a76f0e982cb3 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 19:48:59 +0100 Subject: [PATCH 16/35] fix: IndexedStringBuilder replace does not work when replacingString is in a sub chunk --- .../rpgparser/interpreter/datastruct_value_utils.kt | 3 --- .../rpgparser/interpreter/IndexedStringBuilderTest.kt | 9 +++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt index e8a38ffbd..ca295b257 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt @@ -101,9 +101,6 @@ internal class IndexedStringBuilder(value: String, private val chunksSize: Int) for (chunk in subChunks) { val chunkStart = currentIndex val chunkEnd = currentIndex + chunk.length - if (chunkEnd > end) { - break - } if (start < chunkEnd && end > chunkStart) { val relativeStart = maxOf(0, start - chunkStart) val relativeEnd = minOf(chunk.length, end - chunkStart) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt index 7044342d4..e96e838e6 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt @@ -27,6 +27,15 @@ class IndexedStringBuilderTest { assertEquals("HelloWorld12345", builder.toString()) } + @Test + fun replaceWithinSubChunkFirst() { + val builder = IndexedStringBuilder("HelloWorld", 5) + builder.replace(0, 1, "1") + assertEquals("1elloWorld", builder.toString()) + builder.replace(1, 2, "1") + assertEquals("11lloWorld", builder.toString()) + } + @Test fun replaceAllChunks() { val builder = IndexedStringBuilder("HelloWorld", 5) From 7a646d520bec053d585461b29dba0700a34f6bc5 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 21:12:56 +0100 Subject: [PATCH 17/35] refactor and add doc --- ...e_utils.kt => datastruct_value_builder.kt} | 47 +++++++++++++++---- .../IndexedStringBuilderEvaluator.kt | 34 ++++++++++++-- 2 files changed, 68 insertions(+), 13 deletions(-) rename rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/{datastruct_value_utils.kt => datastruct_value_builder.kt} (74%) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt similarity index 74% rename from rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt rename to rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt index ca295b257..566c70c66 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_utils.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt @@ -3,7 +3,7 @@ package com.smeup.rpgparser.interpreter /** * Interface representing a value of a data structure string. */ -internal interface DataStructStringValue { +internal interface DataStructValueBuilder { /** * Replaces a substring within the data structure string. @@ -26,22 +26,29 @@ internal interface DataStructStringValue { companion object { /** - * Creates an instance of DataStructStringValue based on the given value and fields. + * Creates an instance of DataStructValueBuilder based on the given value and fields. * * @param value The initial value of the data structure string. * @param fields The number of fields to divide the string into. - * @return An instance of DataStructStringValue. The algorithm to create the instance is chosen based on the value and fields. + * @return An instance of DataStructValueBuilder. The algorithm to create the instance is chosen based on the value and fields. */ - fun create(value: String, fields: Int): DataStructStringValue { + fun create(value: String, fields: Int): DataStructValueBuilder { val stringSize = value.length return if (useIndexedStringBuilder(stringSize = stringSize, fields = fields)) { - IndexedStringBuilder(value = value, chunksSize = stringSize / fields) + IndexedBuilderBuilder(value = value, chunksSize = stringSize / fields) } else { - NotIndexedStringBuilder(value) + StringBuilderWrapper(value) } } + /** + * Determines whether to use IndexedStringBuilder based on the string size and number of fields. + * + * @param stringSize The size of the string. + * @param fields The number of fields to divide the string into. + * @return True if IndexedStringBuilder should be used, false otherwise. + */ private fun useIndexedStringBuilder(stringSize: Int, fields: Int): Boolean { if (stringSize >= 9000) return true if (stringSize >= 2000 && fields >= 5) return true @@ -50,18 +57,42 @@ internal interface DataStructStringValue { } } -internal class NotIndexedStringBuilder(value: String) : DataStructStringValue { +/** + * A wrapper class for StringBuilder that implements the DataStructValueBuilder interface. + * + * @param value The initial value of the string builder. + */ +internal class StringBuilderWrapper(value: String) : DataStructValueBuilder { private val sb = StringBuilder(value) + /** + * Replaces a substring within the string builder. + * + * @param start The start index of the substring to replace. + * @param end The end index of the substring to replace. + * @param replacingString The string to replace the substring with. + */ override fun replace(start: Int, end: Int, replacingString: String) { sb.replace(start, end, replacingString) } + /** + * Returns a substring from the string builder. + * + * @param start The start index of the substring. + * @param end The end index of the substring. + * @return The substring from start to end. + */ override fun substring(start: Int, end: Int): String { return sb.substring(start, end) } + /** + * Returns the string representation of the string builder. + * + * @return The string representation of the string builder. + */ override fun toString(): String { return sb.toString() } @@ -75,7 +106,7 @@ internal class NotIndexedStringBuilder(value: String) : DataStructStringValue { * @param value The initial value of the string builder * @param chunksSize The size of the chunks */ -internal class IndexedStringBuilder(value: String, private val chunksSize: Int) : DataStructStringValue { +internal class IndexedBuilderBuilder(value: String, private val chunksSize: Int) : DataStructValueBuilder { // The string is divided into chunks of a fixed size private val chunks: List = List((value.length + chunksSize - 1) / chunksSize) { index -> diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt index 3391df2cd..e62a276a7 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt @@ -4,7 +4,17 @@ import java.math.RoundingMode import kotlin.time.DurationUnit import kotlin.time.measureTime -private fun stringBuilderVsDataStructStringValue( +/** + * Measures and compares the performance of different string builders. + * + * @param printHeader Whether to print the header for the output. + * @param debugInformation Whether to include debug information in the output. + * @param stringSize The size of the string to be used in the test. + * @param fields The number of fields to divide the string into. + * @param iterations The number of iterations to run the test. + * @throws IllegalArgumentException if stringSize is less than fields or if stringSize is not a multiple of fields. + */ +private fun stringBuilderVsDataStructStringBuilder( printHeader: Boolean = false, debugInformation: Boolean = false, stringSize: Int, @@ -24,6 +34,8 @@ private fun stringBuilderVsDataStructStringValue( print("$stringSize, $fields${if (debugInformation) ", $chunksSize" else ""}") val replacingString = "b".repeat(stringSize / fields) + + // Measure the duration for StringBuilder val sbDuration = measureTime { val sb = StringBuilder("a".repeat(stringSize)) for (i in 0 until iterations) { @@ -36,8 +48,9 @@ private fun stringBuilderVsDataStructStringValue( } } + // Measure the duration for IndexedBuilderBuilder val indexedSbDuration = measureTime { - val indexedSb = IndexedStringBuilder("a".repeat(stringSize), chunksSize) + val indexedSb = IndexedBuilderBuilder("a".repeat(stringSize), chunksSize) for (i in 0 until iterations) { val replacingChars = stringSize / fields for (j in 0 until fields) { @@ -48,14 +61,15 @@ private fun stringBuilderVsDataStructStringValue( } } + // Measure the duration for DataStructValueBuilder val dataStructStringValueDuration = measureTime { - val dataStructStringValue = DataStructStringValue.create("a".repeat(stringSize), fields) + val dataStructValueBuilder = DataStructValueBuilder.create("a".repeat(stringSize), fields) for (i in 0 until iterations) { val replacingChars = stringSize / fields for (j in 0 until fields) { val start: Int = j * replacingChars val end: Int = start + replacingChars - dataStructStringValue.replace(start, end, replacingString) + dataStructValueBuilder.replace(start, end, replacingString) } } } @@ -65,10 +79,16 @@ private fun stringBuilderVsDataStructStringValue( println(", $ratioSbIndexed, $ratioSbDataStructStringValue${if (debugInformation) ", ${sbDuration.plus(indexedSbDuration).toLong(DurationUnit.MILLISECONDS)}" else ""}") } +/** + * Creates a dataset for performance comparison by generating various string sizes and field combinations, + * and then measures the performance of different string builders. + */ private fun createPerformanceComparisonDataset() { val stringSizeTests = mutableListOf() val startStringSizeValue = 1000 var value = startStringSizeValue + + // Generate string sizes in the pattern: 1000, 2000, ..., 9000, 10000, 20000, ..., 90000, 100000 while (value <= 100_000) { for (i in 1..9) { stringSizeTests.add(value * i) @@ -76,12 +96,16 @@ private fun createPerformanceComparisonDataset() { value *= 10 } + // Iterate over each generated string size for (stringSize in stringSizeTests) { + // Test with different numbers of fields for (fields in listOf(1, 2, 5, 10, 20, 50, 100, 200, 500, 1000)) { + // Skip invalid combinations where stringSize is less than fields or not a multiple of fields if (stringSize < fields || ((stringSize % fields) != 0)) { break } - stringBuilderVsDataStructStringValue( + // Measure and compare the performance of different string builders + stringBuilderVsDataStructStringBuilder( printHeader = stringSize == startStringSizeValue && fields == 1, stringSize = stringSize, fields = fields, From 20b7600bd02534c9d863d615b9248427226128cf Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 21:14:20 +0100 Subject: [PATCH 18/35] typo --- .../smeup/rpgparser/interpreter/datastruct_value_builder.kt | 4 ++-- .../rpgparser/interpreter/IndexedStringBuilderEvaluator.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt index 566c70c66..969e7f9a2 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt @@ -36,7 +36,7 @@ internal interface DataStructValueBuilder { val stringSize = value.length return if (useIndexedStringBuilder(stringSize = stringSize, fields = fields)) { - IndexedBuilderBuilder(value = value, chunksSize = stringSize / fields) + IndexedStringBuilder(value = value, chunksSize = stringSize / fields) } else { StringBuilderWrapper(value) } @@ -106,7 +106,7 @@ internal class StringBuilderWrapper(value: String) : DataStructValueBuilder { * @param value The initial value of the string builder * @param chunksSize The size of the chunks */ -internal class IndexedBuilderBuilder(value: String, private val chunksSize: Int) : DataStructValueBuilder { +internal class IndexedStringBuilder(value: String, private val chunksSize: Int) : DataStructValueBuilder { // The string is divided into chunks of a fixed size private val chunks: List = List((value.length + chunksSize - 1) / chunksSize) { index -> diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt index e62a276a7..c85f3de9e 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt @@ -50,7 +50,7 @@ private fun stringBuilderVsDataStructStringBuilder( // Measure the duration for IndexedBuilderBuilder val indexedSbDuration = measureTime { - val indexedSb = IndexedBuilderBuilder("a".repeat(stringSize), chunksSize) + val indexedSb = IndexedStringBuilder("a".repeat(stringSize), chunksSize) for (i in 0 until iterations) { val replacingChars = stringSize / fields for (j in 0 until fields) { From e2563be5ed77590f67eca3f622671033acc8c30c Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 22:19:21 +0100 Subject: [PATCH 19/35] test: add DataStructValueBuilderTest --- .../interpreter/IndexedStringBuilderTest.kt | 142 +++++++++++++++++- 1 file changed, 139 insertions(+), 3 deletions(-) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt index e96e838e6..0c4d1eafa 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt @@ -1,8 +1,8 @@ package com.smeup.rpgparser.interpreter -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith +import kotlin.random.Random +import kotlin.test.* +import kotlin.time.measureTime class IndexedStringBuilderTest { @@ -94,3 +94,139 @@ class IndexedStringBuilderTest { assertEquals("", result) } } + +class DataStructValueBuilderTest { + + private fun replaceAllFields( + dataStructValueBuilder: DataStructValueBuilder, + replacingChar: Char, + fieldsLen: List, + iteration: Int + ) { + for (i in 0 until iteration) { + var start = 0 + var end = 0 + for (fieldLen in fieldsLen) { + end += fieldLen + dataStructValueBuilder.replace(start, end, "$replacingChar".repeat(fieldLen)) + start = end + } + } + } + + @Test + fun inCaseOfLittleDSUseStringBuilderWrapper() { + val littleDS = listOf(10, 20, 20, 4, 5) + val dataStructValueBuilder = DataStructValueBuilder.create(value = "a".repeat(littleDS.sum()), fields = littleDS.size) + assertIs(dataStructValueBuilder) + } + + @Test + fun inCaseOfBigDSUseIndexedStringBuilder() { + val bigDS = listOf(1000, 2000, 3000, 4000, 5000) + val dataStructValueBuilder = DataStructValueBuilder.create(value = "a".repeat(bigDS.sum()), fields = bigDS.size) + assertIs(dataStructValueBuilder) + } + + @Test + fun littleDSBetterStringBuilderWrapper() { + val littleDS = listOf(10, 20, 20, 4, 5) + val value = "a".repeat(littleDS.sum()) + val stringBuilderWrapper = StringBuilderWrapper(value = value) + val indexedStringBuilder = IndexedStringBuilder(value = value, chunksSize = value.length / littleDS.size) + val replacingChar = 'b' + val checkBuiltString = "$replacingChar".repeat(littleDS.sum()) + val iteration = 10_000 + println("In case of little DS StringBuilderWrapper performance must be better than IndexedStringBuilder") + val stringBuilderWrapperDuration = measureTime { + replaceAllFields( + dataStructValueBuilder = stringBuilderWrapper, + replacingChar = replacingChar, + fieldsLen = littleDS, + iteration = iteration + ) + } + val indexedStringBuilderDuration = measureTime { + replaceAllFields( + dataStructValueBuilder = indexedStringBuilder, + replacingChar = replacingChar, + fieldsLen = littleDS, + iteration = iteration + ) + } + println("stringBuilderWrapperDuration: $stringBuilderWrapperDuration") + println("indexedStringBuilderDuration: $indexedStringBuilderDuration") + assertEquals(checkBuiltString, stringBuilderWrapper.toString()) + assertEquals(checkBuiltString, indexedStringBuilder.toString()) + assertTrue { stringBuilderWrapperDuration < indexedStringBuilderDuration } + } + + @Test + fun bigDSBetterIndexedStringBuilder() { + val bigDS = List(100) { Random.nextInt(100, 1000) } + val value = "a".repeat(bigDS.sum()) + val stringBuilderWrapper = StringBuilderWrapper(value = value) + val indexedStringBuilder = IndexedStringBuilder(value = value, chunksSize = value.length / bigDS.size) + val replacingChar = 'b' + val checkBuiltString = "$replacingChar".repeat(bigDS.sum()) + val iteration = 1000 + println("In case of big DS IndexedStringBuilder performance must be better than StringBuilderWrapper") + val stringBuilderWrapperDuration = measureTime { + replaceAllFields( + dataStructValueBuilder = stringBuilderWrapper, + replacingChar = replacingChar, + fieldsLen = bigDS, + iteration = iteration + ) + } + val indexedStringBuilderDuration = measureTime { + replaceAllFields( + dataStructValueBuilder = indexedStringBuilder, + replacingChar = replacingChar, + fieldsLen = bigDS, + iteration = iteration + ) + } + println("stringBuilderWrapperDuration: $stringBuilderWrapperDuration") + println("indexedStringBuilderDuration: $indexedStringBuilderDuration") + assertEquals(checkBuiltString, stringBuilderWrapper.toString()) + assertEquals(checkBuiltString, indexedStringBuilder.toString()) + assertTrue { indexedStringBuilderDuration < stringBuilderWrapperDuration } + } + + @Test + fun overlayingDSMuchBetterIndexedStringBuilder() { + val dsWithOverlay = List(10_000) { Random.nextInt(10, 101) } + val value = "a".repeat(dsWithOverlay.sum()) + val stringBuilderWrapper = StringBuilderWrapper(value = value) + val indexedStringBuilder = IndexedStringBuilder(value = value, chunksSize = value.length / dsWithOverlay.size) + val replacingChar = 'b' + val checkBuiltString = "$replacingChar".repeat(dsWithOverlay.sum()) + val iteration = 5 + println("In case of DS with overlay IndexedStringBuilder performance must better than StringBuilderWrapper at least one order of magnitude.\"") + println("Measure StringBuilderWrapper performance") + val stringBuilderWrapperDuration = measureTime { + replaceAllFields( + dataStructValueBuilder = stringBuilderWrapper, + replacingChar = replacingChar, + fieldsLen = dsWithOverlay, + iteration = iteration + ) + } + println("Measure IndexedStringBuilder performance") + val indexedStringBuilderDuration = measureTime { + replaceAllFields( + dataStructValueBuilder = indexedStringBuilder, + replacingChar = replacingChar, + fieldsLen = dsWithOverlay, + iteration = iteration + ) + } + println("stringBuilderWrapperDuration: $stringBuilderWrapperDuration") + println("indexedStringBuilderDuration: $indexedStringBuilderDuration") + assertEquals(checkBuiltString, stringBuilderWrapper.toString()) + assertEquals(checkBuiltString, indexedStringBuilder.toString()) + val ratio = stringBuilderWrapperDuration.div(indexedStringBuilderDuration) + assertTrue { ratio > 10 } + } +} From 1f222b6f59fea6a01a7beede6c7ba340d30afb68 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Wed, 1 Jan 2025 22:21:23 +0100 Subject: [PATCH 20/35] refactor --- ...derTest.kt => datastruct_value_builder.kt} | 182 +++++++++--------- 1 file changed, 91 insertions(+), 91 deletions(-) rename rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/{IndexedStringBuilderTest.kt => datastruct_value_builder.kt} (100%) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt similarity index 100% rename from rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt rename to rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt index 0c4d1eafa..87b2e79f6 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt @@ -4,97 +4,6 @@ import kotlin.random.Random import kotlin.test.* import kotlin.time.measureTime -class IndexedStringBuilderTest { - - @Test - fun replaceWithinSingleChunkFirst() { - val builder = IndexedStringBuilder("HelloWorld", 5) - builder.replace(0, 5, "12345") - assertEquals("12345World", builder.toString()) - } - - @Test - fun replaceWithinSingleChunkMiddle() { - val builder = IndexedStringBuilder("HelloWorldHello", 5) - builder.replace(5, 10, "12345") - assertEquals("Hello12345Hello", builder.toString()) - } - - @Test - fun replaceWithinSingleChunkLast() { - val builder = IndexedStringBuilder("HelloWorldHello", 5) - builder.replace(10, 15, "12345") - assertEquals("HelloWorld12345", builder.toString()) - } - - @Test - fun replaceWithinSubChunkFirst() { - val builder = IndexedStringBuilder("HelloWorld", 5) - builder.replace(0, 1, "1") - assertEquals("1elloWorld", builder.toString()) - builder.replace(1, 2, "1") - assertEquals("11lloWorld", builder.toString()) - } - - @Test - fun replaceAllChunks() { - val builder = IndexedStringBuilder("HelloWorld", 5) - builder.replace(0, 10, "1234567890") - assertEquals("1234567890", builder.toString()) - } - - @Test - fun replaceAcrossMultipleChunks() { - val builder = IndexedStringBuilder("HelloWorld", 2) - builder.replace(3, 8, "12345") - assertEquals("Hel12345ld", builder.toString()) - } - - @Test - fun replaceWithReplacingStringGreaterThanReplacedRangeThrowsException() { - val builder = IndexedStringBuilder("HelloWorld", 5) - assertFailsWith { - builder.replace(0, 5, "HelloWorld") - } - } - - @Test - fun replaceWithReplacingStringLowerThanReplacedRangeThrowsException() { - val builder = IndexedStringBuilder("HelloWorld", 5) - assertFailsWith { - builder.replace(0, 5, "H") - } - } - - @Test - fun substringWithinSingleChunk() { - val builder = IndexedStringBuilder("HelloWorld", 5) - val result = builder.substring(0, 5) - assertEquals("Hello", result) - } - - @Test - fun substringAcrossMultipleChunks() { - val builder = IndexedStringBuilder("HelloWorld", 5) - val result = builder.substring(3, 8) - assertEquals("loWor", result) - } - - @Test - fun substringWithExactLength() { - val builder = IndexedStringBuilder("HelloWorld", 5) - val result = builder.substring(0, 10) - assertEquals("HelloWorld", result) - } - - @Test - fun substringWithEmptyResult() { - val builder = IndexedStringBuilder("HelloWorld", 5) - val result = builder.substring(5, 5) - assertEquals("", result) - } -} - class DataStructValueBuilderTest { private fun replaceAllFields( @@ -230,3 +139,94 @@ class DataStructValueBuilderTest { assertTrue { ratio > 10 } } } + +class IndexedStringBuilderTest { + + @Test + fun replaceWithinSingleChunkFirst() { + val builder = IndexedStringBuilder("HelloWorld", 5) + builder.replace(0, 5, "12345") + assertEquals("12345World", builder.toString()) + } + + @Test + fun replaceWithinSingleChunkMiddle() { + val builder = IndexedStringBuilder("HelloWorldHello", 5) + builder.replace(5, 10, "12345") + assertEquals("Hello12345Hello", builder.toString()) + } + + @Test + fun replaceWithinSingleChunkLast() { + val builder = IndexedStringBuilder("HelloWorldHello", 5) + builder.replace(10, 15, "12345") + assertEquals("HelloWorld12345", builder.toString()) + } + + @Test + fun replaceWithinSubChunkFirst() { + val builder = IndexedStringBuilder("HelloWorld", 5) + builder.replace(0, 1, "1") + assertEquals("1elloWorld", builder.toString()) + builder.replace(1, 2, "1") + assertEquals("11lloWorld", builder.toString()) + } + + @Test + fun replaceAllChunks() { + val builder = IndexedStringBuilder("HelloWorld", 5) + builder.replace(0, 10, "1234567890") + assertEquals("1234567890", builder.toString()) + } + + @Test + fun replaceAcrossMultipleChunks() { + val builder = IndexedStringBuilder("HelloWorld", 2) + builder.replace(3, 8, "12345") + assertEquals("Hel12345ld", builder.toString()) + } + + @Test + fun replaceWithReplacingStringGreaterThanReplacedRangeThrowsException() { + val builder = IndexedStringBuilder("HelloWorld", 5) + assertFailsWith { + builder.replace(0, 5, "HelloWorld") + } + } + + @Test + fun replaceWithReplacingStringLowerThanReplacedRangeThrowsException() { + val builder = IndexedStringBuilder("HelloWorld", 5) + assertFailsWith { + builder.replace(0, 5, "H") + } + } + + @Test + fun substringWithinSingleChunk() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.substring(0, 5) + assertEquals("Hello", result) + } + + @Test + fun substringAcrossMultipleChunks() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.substring(3, 8) + assertEquals("loWor", result) + } + + @Test + fun substringWithExactLength() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.substring(0, 10) + assertEquals("HelloWorld", result) + } + + @Test + fun substringWithEmptyResult() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.substring(5, 5) + assertEquals("", result) + } +} From 79836370b486fb94df4befc949bd5066be141ca5 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sat, 4 Jan 2025 19:30:59 +0100 Subject: [PATCH 21/35] feature: replace DataStructValue.value type from StringBuilder to DataStructValueBuilder --- .../kotlin/com/smeup/rpgparser/interpreter/values.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt index b356261ea..bcf90c759 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt @@ -12,6 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ package com.smeup.rpgparser.interpreter @@ -592,7 +593,6 @@ data class DateValue(val value: Long, val format: DateFormat) : Value { return when (format) { DateFormat.JUL -> format("yyDDD").asDecimal() DateFormat.ISO -> format("yyyyMMdd").asDecimal() - else -> TODO() } } } @@ -1097,10 +1097,13 @@ fun Type.blank(): Value { * StringValue wrapper */ @Serializable -data class DataStructValue(@Contextual val value: StringBuilder, private val optionalExternalLen: Int? = null) : Value { +data class DataStructValue(@Contextual val value: DataStructValueBuilder, private val optionalExternalLen: Int? = null) : Value { - constructor(value: String) : this(StringBuilder(value)) - constructor(value: String, len: Int) : this(StringBuilder(value), len) + constructor(value: String) : this(StringBuilderWrapper(value = value)) + constructor(value: String, len: Int) : this(StringBuilderWrapper(value = value), len) + constructor(value: String, type: DataStructureType) : this(DataStructValueBuilder.create(value = value, fields = type.fields.size)) { + throw UnsupportedOperationException("Not yet implemented") + } // We can't serialize a class with a var computed from another one because of a bug in the serialization plugin // See https://github.com/Kotlin/kotlinx.serialization/issues/133 From 5cd4fae3341078ed58709be79252a36ae18be301 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sat, 4 Jan 2025 19:32:54 +0100 Subject: [PATCH 22/35] feature: add missing methods to DataStructValueBuilder to make it compile time compliant --- .../interpreter/datastruct_value_builder.kt | 116 ++++++++++++- .../com/smeup/rpgparser/interpreter/sorta.kt | 3 +- .../interpreter/datastruct_value_builder.kt | 161 ++++++++++++++++++ 3 files changed, 274 insertions(+), 6 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt index 969e7f9a2..9a6d83ba2 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt @@ -1,16 +1,44 @@ +/* + * Copyright 2019 Sme.UP S.p.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package com.smeup.rpgparser.interpreter +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable + /** * Interface representing a value of a data structure string. + * This interface has been created to allow different implementations about how to manipulate the data structure content + * in a more efficient way. */ -internal interface DataStructValueBuilder { +@Serializable +sealed interface DataStructValueBuilder { + + /** + * The length of the data structure string. + */ + val length: Int /** * Replaces a substring within the data structure string. * * @param start The start index of the substring to replace. * @param end The end index of the substring to replace. - * @param replacingString The string to replace the substring with. + * @param replacingString The string to replace the substring with, which must have the same length as the substring being replaced. */ fun replace(start: Int, end: Int, replacingString: String) @@ -23,6 +51,36 @@ internal interface DataStructValueBuilder { */ fun substring(start: Int, end: Int): String + /** + * Performs the given action on each character of the data structure string. + * + * @param action The action to be performed on each character. + */ + fun forEach(action: (c: Char) -> Unit) + + /** + * Checks if the data structure string is blank (i.e., empty or contains only whitespace characters). + * + * @return True if the data structure string is blank, false otherwise. + */ + fun isBlank(): Boolean + + /** + * Splits the data structure string into chunks of the specified size. + * + * @param size The size of each chunk. + * @return A list of strings, each representing a chunk of the original string. + */ + fun chunked(size: Int): List + + /** + * Replaces all the characters in the data structure string with the given value. + * @param value The value to replace all the characters with. + * @return An instance of DataStructValueBuilder. + * @throws IllegalArgumentException if value is longer than the initial wrapped value. + */ + fun replaceAll(value: String): DataStructValueBuilder + companion object { /** @@ -62,10 +120,14 @@ internal interface DataStructValueBuilder { * * @param value The initial value of the string builder. */ -internal class StringBuilderWrapper(value: String) : DataStructValueBuilder { +@Serializable +class StringBuilderWrapper(private val value: String) : DataStructValueBuilder { + @Contextual private val sb = StringBuilder(value) + override val length = sb.length + /** * Replaces a substring within the string builder. * @@ -88,6 +150,24 @@ internal class StringBuilderWrapper(value: String) : DataStructValueBuilder { return sb.substring(start, end) } + override fun forEach(action: (c: Char) -> Unit) { + sb.forEach(action) + } + + override fun isBlank(): Boolean { + return sb.isBlank() + } + + override fun chunked(size: Int): List { + return sb.chunked(size) + } + + override fun replaceAll(value: String): DataStructValueBuilder { + require(value.length <= sb.length) { "Value is longer than the initial wrapped value." } + sb.clear().append(value) + return this + } + /** * Returns the string representation of the string builder. * @@ -106,13 +186,17 @@ internal class StringBuilderWrapper(value: String) : DataStructValueBuilder { * @param value The initial value of the string builder * @param chunksSize The size of the chunks */ -internal class IndexedStringBuilder(value: String, private val chunksSize: Int) : DataStructValueBuilder { +@Serializable +class IndexedStringBuilder(private val value: String, val chunksSize: Int) : DataStructValueBuilder { // The string is divided into chunks of a fixed size - private val chunks: List = List((value.length + chunksSize - 1) / chunksSize) { index -> + @Contextual + private val chunks: List<@Contextual StringBuilder> = List((value.length + chunksSize - 1) / chunksSize) { index -> StringBuilder(value.substring(index * chunksSize, minOf((index + 1) * chunksSize, value.length))) } + override val length = value.length + /*** * Replace the substring from start to end with the replacing string. * The length of the replacing string must be equal to the length of the replaced string. @@ -180,6 +264,28 @@ internal class IndexedStringBuilder(value: String, private val chunksSize: Int) return result.toString() } + override fun forEach(action: (c: Char) -> Unit) { + chunks.forEach { it.forEach(action) } + } + + override fun isBlank(): Boolean { + return chunks.all { it.isBlank() } + } + + override fun chunked(size: Int): List { + return toString().chunked(size) + } + + override fun replaceAll(value: String): DataStructValueBuilder { + require(value.length == this.length) { "Value length must be the same of wrapped value." } + chunks.forEachIndexed { index, chunk -> + val start = index * chunksSize + val end = minOf(start + chunksSize, this.length) + chunk.replace(0, chunk.length, value.substring(start, end)) + } + return this + } + override fun toString(): String { return chunks.joinToString(separator = "") { it.toString() } } diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/sorta.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/sorta.kt index 9316e047a..c46782a21 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/sorta.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/sorta.kt @@ -12,6 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ package com.smeup.rpgparser.interpreter @@ -74,7 +75,7 @@ fun sortA(value: Value, arrayType: ArrayType) { val resultList = elementsLeft + sortedElementsToSort + elementsRight // return value - value.container.value.clear().append(resultList.joinToString("")) + value.container.value.replaceAll(resultList.joinToString("")) } } } \ No newline at end of file diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt index 87b2e79f6..657a9c91e 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt @@ -1,5 +1,23 @@ +/* + * Copyright 2019 Sme.UP S.p.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + package com.smeup.rpgparser.interpreter +import com.smeup.rpgparser.parsing.ast.json import kotlin.random.Random import kotlin.test.* import kotlin.time.measureTime @@ -229,4 +247,147 @@ class IndexedStringBuilderTest { val result = builder.substring(5, 5) assertEquals("", result) } + + @Test + fun forEachExecutesActionOnEachCharacter() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = StringBuilder() + builder.forEach { result.append(it) } + assertEquals("HelloWorld", result.toString()) + } + + @Test + fun forEachExecutesActionOnEmptyString() { + val builder = IndexedStringBuilder("", 5) + val result = StringBuilder() + builder.forEach { result.append(it) } + assertEquals("", result.toString()) + } + + @Test + fun forEachExecutesActionOnSingleCharacter() { + val builder = IndexedStringBuilder("A", 5) + val result = StringBuilder() + builder.forEach { result.append(it) } + assertEquals("A", result.toString()) + } + + @Test + fun forEachExecutesActionOnWhitespaceCharacters() { + val builder = IndexedStringBuilder(" \t\n", 5) + val result = StringBuilder() + builder.forEach { result.append(it) } + assertEquals(" \t\n", result.toString()) + } + + @Test + fun isBlankReturnsTrueForEmptyString() { + val builder = IndexedStringBuilder("", 5) + assertTrue(builder.isBlank()) + } + + @Test + fun isBlankReturnsTrueForWhitespaceString() { + val builder = IndexedStringBuilder(" \t\n", 5) + assertTrue(builder.isBlank()) + } + + @Test + fun isBlankReturnsFalseForNonEmptyString() { + val builder = IndexedStringBuilder("HelloWorld", 5) + assertFalse(builder.isBlank()) + } + + @Test + fun isBlankReturnsFalseForMixedString() { + val builder = IndexedStringBuilder(" \t\nHello", 5) + assertFalse(builder.isBlank()) + } + + @Test + fun chunkedSplitsStringIntoEqualChunks() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.chunked(2) + assertEquals(listOf("He", "ll", "oW", "or", "ld"), result) + } + + @Test + fun chunkedSplitsStringWithRemainingCharacters() { + val builder = IndexedStringBuilder("HelloWorld", 5) + val result = builder.chunked(3) + assertEquals(listOf("Hel", "loW", "orl", "d"), result) + } + + @Test + fun chunkedSplitsEmptyString() { + val builder = IndexedStringBuilder("", 5) + val result = builder.chunked(3) + assertEquals(emptyList(), result) + } + + @Test + fun chunkedSplitsStringWithSingleCharacterChunks() { + val builder = IndexedStringBuilder("Hello", 5) + val result = builder.chunked(1) + assertEquals(listOf("H", "e", "l", "l", "o"), result) + } + + @Test + fun chunkedSplitsStringWithChunkSizeGreaterThanStringLength() { + val builder = IndexedStringBuilder("Hello", 5) + val result = builder.chunked(10) + assertEquals(listOf("Hello"), result) + } + + @Test + fun replaceAllReplacesEntireStringWithSameLength() { + val builder = IndexedStringBuilder("HelloWorld", 5) + builder.replaceAll("1234567890") + assertEquals("1234567890", builder.toString()) + } + + @Test + fun replaceAllReplacesStringWithDifferentChunks() { + val builder = IndexedStringBuilder("HelloWorldHelloWorld", 5) + builder.replaceAll("12345123451234512345") + assertEquals("12345123451234512345", builder.toString()) + } + + @Test + fun replaceAllThrowsExceptionWhenValueIsShorter() { + val builder = IndexedStringBuilder("HelloWorld", 5) + assertFailsWith { + builder.replaceAll("12345") + } + } + + @Test + fun replaceAllThrowsExceptionWhenValueIsLonger() { + val builder = IndexedStringBuilder("HelloWorld", 5) + assertFailsWith { + builder.replaceAll("12345678901") + } + } + + @Test + fun replaceAllWithEmptyStringThrowsException() { + val builder = IndexedStringBuilder("HelloWorld", 5) + assertFailsWith { + builder.replaceAll("") + } + } + + @Test + fun serializationWhenValueIsStringBuilderWrapper() { + val dataStruct = DataStructValue(value = StringBuilderWrapper("HelloWorld")) + val serialized = json.encodeToString(DataStructValue.serializer(), dataStruct) + assertEquals(dataStruct, json.decodeFromString(DataStructValue.serializer(), serialized)) + } + + @Test + fun serializationWhenValueIsIndexedStringBuilder() { + val dataStruct = DataStructValue(value = IndexedStringBuilder("HelloWorld", 5)) + val serialized = json.encodeToString(DataStructValue.serializer(), dataStruct) + assertEquals(dataStruct, json.decodeFromString(DataStructValue.serializer(), serialized)) + } } From 3110a8322aae0c47135a9f83aa25368afa4efa17 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 5 Jan 2025 17:26:42 +0100 Subject: [PATCH 23/35] feature: add all stuff related DataStructValueBuilder serialization --- .../interpreter/datastruct_value_builder.kt | 4 ++-- .../serialization/serialization_options.kt | 4 ++++ .../rpgparser/parsing/ast/serialization.kt | 6 ++++-- .../serialization/DSSerializationTest.kt | 18 +++++++++++++++++- .../serialization/SerializationTest.kt | 16 +++++++++++++++- 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt index 9a6d83ba2..06eaf20e4 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt @@ -12,13 +12,13 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ package com.smeup.rpgparser.interpreter import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient /** * Interface representing a value of a data structure string. @@ -123,7 +123,7 @@ sealed interface DataStructValueBuilder { @Serializable class StringBuilderWrapper(private val value: String) : DataStructValueBuilder { - @Contextual + @Transient private val sb = StringBuilder(value) override val length = sb.length diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/serialization/serialization_options.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/serialization/serialization_options.kt index 666faebe8..add5e2054 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/serialization/serialization_options.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/serialization/serialization_options.kt @@ -34,6 +34,10 @@ private val module = SerializersModule { contextual(BigDecimalSerializer) contextual(LocalDateTimeSerializer) contextual(StringBuilderSerializer) + polymorphic(DataStructValueBuilder::class) { + subclass(StringBuilderWrapper::class) + subclass(IndexedStringBuilder::class) + } polymorphic(Value::class) { subclass(IntValue::class) subclass(DecimalValue::class) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/parsing/ast/serialization.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/parsing/ast/serialization.kt index e487beaa1..4df66950f 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/parsing/ast/serialization.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/parsing/ast/serialization.kt @@ -25,13 +25,14 @@ import com.smeup.rpgparser.interpreter.InStatementDataDefinition import com.smeup.rpgparser.parsing.parsetreetoast.LogicalCondition import com.smeup.rpgparser.serialization.BigDecimalSerializer import com.smeup.rpgparser.serialization.LocalDateTimeSerializer +import com.smeup.rpgparser.serialization.StringBuilderSerializer import kotlinx.serialization.cbor.Cbor import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToByteArray import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.subclass import java.math.BigDecimal @@ -232,6 +233,7 @@ private val modules = SerializersModule { polymorphic(DSPFValue::class) { subclass(ConstantValue::class) } + contextual(StringBuilderSerializer) } val json = Json { @@ -258,7 +260,7 @@ enum class SourceProgram(val extension: String, val sourceType: Boolean = true) companion object { fun getByExtension(extension: String): SourceProgram { - return values().first { + return entries.first { it.extension == extension } } diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/serialization/DSSerializationTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/serialization/DSSerializationTest.kt index 944795227..024a2a9a5 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/serialization/DSSerializationTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/serialization/DSSerializationTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Sme.UP S.p.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.smeup.rpgparser.interpreter.serialization import com.smeup.rpgparser.interpreter.DataStructValue @@ -12,7 +28,7 @@ class DSSerializationTest { val stringSerializer = SerializationOption.getSerializer(false) val serializedDS = """ - {"$CLASS_DISCRIMINATOR_TAG":"com.smeup.rpgparser.interpreter.DataStructValue", "value":"JamesBond 7","optionalExternalLen":null} + {"$CLASS_DISCRIMINATOR_TAG":"com.smeup.rpgparser.interpreter.DataStructValue", "value":{"#class":"com.smeup.rpgparser.interpreter.IndexedStringBuilder","value":"JamesBond 7","chunksSize":10},"optionalExternalLen":null} """.trimIndent() val dsValue = stringSerializer.decodeFromString(serializedDS) assertTrue(dsValue is DataStructValue) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/serialization/SerializationTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/serialization/SerializationTest.kt index fa75056df..2900c9f61 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/serialization/SerializationTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/serialization/SerializationTest.kt @@ -94,6 +94,20 @@ class SerializationTest { checkValueSerialization(dsValue, true) } + @Test + fun `DataStructValue with IndexedStringBuilder can be serialized to Json`() { + val rawStringValue = " Hello world 123 " + val dsValue = DataStructValue(value = IndexedStringBuilder(rawStringValue, 2)) + checkValueSerialization(dsValue, true) + } + + @Test + fun `DataStructValue with StringBuilderWrapper can be serialized to Json`() { + val rawStringValue = " Hello world 123 " + val dsValue = DataStructValue(value = StringBuilderWrapper(rawStringValue)) + checkValueSerialization(dsValue, true) + } + @Test fun `a map with Values can be serialized to Json`() { val aLongNumber = 6969L @@ -110,7 +124,7 @@ class SerializationTest { ) val dsValue = DataStructValue(" test 11233 ") - val originalMap = mapOf( + val originalMap = mapOf( "one" to decimalValue, "two" to intValue, "three" to stringValue, From daf1a0e5f2d903d65452b29dd73faf2fd40dedc8 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 5 Jan 2025 18:52:37 +0100 Subject: [PATCH 24/35] refactor: change name of test suite --- ...datastruct_value_builder.kt => DataStructValueBuilderTest.kt} | 1 - 1 file changed, 1 deletion(-) rename rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/{datastruct_value_builder.kt => DataStructValueBuilderTest.kt} (99%) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt similarity index 99% rename from rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt rename to rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt index 657a9c91e..10018f3c5 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt @@ -12,7 +12,6 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ package com.smeup.rpgparser.interpreter From 38219e062bfc34fb96783a94fe13fe7b23f8a24d Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 5 Jan 2025 20:44:29 +0100 Subject: [PATCH 25/35] fix: substring where chunk containing string to replace is not at the start --- .../interpreter/datastruct_value_builder.kt | 2 +- .../interpreter/DataStructValueBuilderTest.kt | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt index 06eaf20e4..fb78bbafb 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt @@ -247,7 +247,7 @@ class IndexedStringBuilder(private val value: String, val chunksSize: Int) : Dat val subChunks = chunks.subList(firstChunkIndex, lastChunkIndex + 1) val result = StringBuilder() - var currentIndex = 0 + var currentIndex = firstChunkIndex * chunksSize for (chunk in subChunks) { val chunkStart = currentIndex val chunkEnd = currentIndex + chunk.length diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt index 10018f3c5..ad2045992 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt @@ -220,12 +220,26 @@ class IndexedStringBuilderTest { } @Test - fun substringWithinSingleChunk() { + fun substringWithinSingleChunkStart() { val builder = IndexedStringBuilder("HelloWorld", 5) val result = builder.substring(0, 5) assertEquals("Hello", result) } + @Test + fun substringWithinSingleChunkMiddle() { + val builder = IndexedStringBuilder("HelloWorld", 2) + val result = builder.substring(2, 4) + assertEquals("ll", result) + } + + @Test + fun substringWithinSingleChunkEnd() { + val builder = IndexedStringBuilder("HelloWorld", 4) + val result = builder.substring(5, 10) + assertEquals("World", result) + } + @Test fun substringAcrossMultipleChunks() { val builder = IndexedStringBuilder("HelloWorld", 5) From be765e292e1f4d97808176c729cf0494f96b6f18 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 5 Jan 2025 20:52:26 +0100 Subject: [PATCH 26/35] feature: add to DataStruct value new blank function The function blank(dataStructureType: DataStructureType) allows to create an instance of DataStructValue that uses the right instance of DataStructValueBuilder based on the provided DataStructureType. --- .../kotlin/com/smeup/rpgparser/interpreter/values.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt index bcf90c759..c0b3a974c 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt @@ -12,7 +12,6 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ package com.smeup.rpgparser.interpreter @@ -1050,7 +1049,8 @@ fun String.asIsoDate(): Date { } fun createBlankFor(dataDefinition: DataDefinition): Value { - val ds = DataStructValue.blank(dataDefinition.type.size) + require(dataDefinition.type is DataStructureType) { "DataDefinition should be a DataStructureType" } + val ds = DataStructValue.blank(dataStructureType = dataDefinition.type as DataStructureType) dataDefinition.fields.forEach { if (it.type is NumberType && (dataDefinition.inz || it.initializationValue != null)) ds.set(it, it.type.toRPGValue()) } @@ -1093,6 +1093,7 @@ fun Type.blank(): Value { } } +private fun List.totalFields() = this.sumOf { if (it.type is ArrayType) it.type.nElements else 1 } /** * StringValue wrapper */ @@ -1101,9 +1102,7 @@ data class DataStructValue(@Contextual val value: DataStructValueBuilder, privat constructor(value: String) : this(StringBuilderWrapper(value = value)) constructor(value: String, len: Int) : this(StringBuilderWrapper(value = value), len) - constructor(value: String, type: DataStructureType) : this(DataStructValueBuilder.create(value = value, fields = type.fields.size)) { - throw UnsupportedOperationException("Not yet implemented") - } + constructor(value: String, type: DataStructureType) : this(DataStructValueBuilder.create(value = value, fields = type.fields.totalFields())) // We can't serialize a class with a var computed from another one because of a bug in the serialization plugin // See https://github.com/Kotlin/kotlinx.serialization/issues/133 @@ -1212,6 +1211,7 @@ data class DataStructValue(@Contextual val value: DataStructValueBuilder, privat companion object { fun blank(length: Int) = DataStructValue(" ".repeat(length)) + fun blank(dataStructureType: DataStructureType) = DataStructValue(value = " ".repeat(dataStructureType.size), type = dataStructureType) /** * Create a new instance of DataStructValue From dec4dc35c6d05be46c0081e093cd01f9a269ac70 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 5 Jan 2025 21:43:09 +0100 Subject: [PATCH 27/35] feature: add or update initializer functions for DataStructValue to handle the new DataStructValue constructors --- .../smeup/rpgparser/interpreter/coercing.kt | 4 +- .../com/smeup/rpgparser/interpreter/values.kt | 60 +++++++++++++++---- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/coercing.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/coercing.kt index 9be3c5068..210393254 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/coercing.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/coercing.kt @@ -308,7 +308,7 @@ fun Type.lowValue(): Value { is ArrayType -> createArrayValue(this.element, this.nElements) { coerce(LowValValue, this.element) } is DataStructureType -> { val fields = this.fields.associateWith { field -> field.type.lowValue() } - DataStructValue.fromFields(fields) + DataStructValue.fromFields(fields = fields, type = this) } is BooleanType -> BooleanValue.FALSE is RecordFormatType -> BlanksValue @@ -323,7 +323,7 @@ fun Type.hiValue(): Value { is ArrayType -> createArrayValue(this.element, this.nElements) { coerce(HiValValue, this.element) } is DataStructureType -> { val fields = this.fields.associateWith { field -> field.type.hiValue() } - DataStructValue.fromFields(fields) + DataStructValue.fromFields(fields = fields, type = this) } is BooleanType -> BooleanValue.TRUE is RecordFormatType -> BlanksValue diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt index c0b3a974c..0040a51f0 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt @@ -1050,7 +1050,7 @@ fun String.asIsoDate(): Date { fun createBlankFor(dataDefinition: DataDefinition): Value { require(dataDefinition.type is DataStructureType) { "DataDefinition should be a DataStructureType" } - val ds = DataStructValue.blank(dataStructureType = dataDefinition.type as DataStructureType) + val ds = DataStructValue.blank(type = dataDefinition.type as DataStructureType) dataDefinition.fields.forEach { if (it.type is NumberType && (dataDefinition.inz || it.initializationValue != null)) ds.set(it, it.type.toRPGValue()) } @@ -1069,8 +1069,8 @@ fun Type.blank(): Value { is ArrayType -> createArrayValue(this.element, this.nElements) { this.element.blank() } - is DataStructureType -> DataStructValue.blank(this.size) - is OccurableDataStructureType -> OccurableDataStructValue.blank(this.size, this.occurs) + is DataStructureType -> DataStructValue.blank(this) + is OccurableDataStructureType -> OccurableDataStructValue.blank(occurs = this.occurs, type = this.dataStructureType) is StringType -> { if (!this.varying) { StringValue.blank(this.size) @@ -1100,8 +1100,30 @@ private fun List.totalFields() = this.sumOf { if (it.type is ArrayTyp @Serializable data class DataStructValue(@Contextual val value: DataStructValueBuilder, private val optionalExternalLen: Int? = null) : Value { + /** + * Constructor for a DataStructValue. This constructor does not implement any kind of optimization. + * In general, it is recommended to use the other constructors. + * Use `DataStructValue(value: String, type: DataStructureType)` instead. + * @param value the value of the data structure + */ constructor(value: String) : this(StringBuilderWrapper(value = value)) + + /** + * Constructor for a DataStructValue. This constructor does not implement any kind of optimization. + * In general, it is recommended to use the other constructors. + * Use `DataStructValue(value: String, type: DataStructureType)` instead. + * @param value the value of the data structure + * @param len the length of the data structure + */ constructor(value: String, len: Int) : this(StringBuilderWrapper(value = value), len) + + /** + * Constructor for a DataStructValue. + * Under the hood, this constructor creates a DataStructValueBuilder with the given value and the total fields of the type. + * This allows to use the better implementation of the DataStructValueBuilder based on the type. + * @param value the value of the data structure + * @param type the type of the data structure + */ constructor(value: String, type: DataStructureType) : this(DataStructValueBuilder.create(value = value, fields = type.fields.totalFields())) // We can't serialize a class with a var computed from another one because of a bug in the serialization plugin @@ -1210,8 +1232,25 @@ data class DataStructValue(@Contextual val value: DataStructValueBuilder, privat } companion object { + + /** + * Creates a blank `DataStructValue` with the specified length. + * This function creates an instance of DataStructValue with no kind of optimization. + * In general, it is recommended to use the function `blank(dataStructureType: DataStructureType)` instead. + * @param length the length of the blank `DataStructValue` + * @return a `DataStructValue` filled with spaces of the given length + * @see blank(dataStructureType: DataStructureType) + */ fun blank(length: Int) = DataStructValue(" ".repeat(length)) - fun blank(dataStructureType: DataStructureType) = DataStructValue(value = " ".repeat(dataStructureType.size), type = dataStructureType) + + /** + * Creates a blank `DataStructValue` with the specified `DataStructureType`. + * It is the better choice to create a blank `DataStructValue` because the knowledge of the type allows + * to use the better implementation of the `DataStructValueBuilder`. + * @param type the `DataStructureType` of the blank `DataStructValue` + * @return a `DataStructValue` filled with spaces + */ + fun blank(type: DataStructureType) = DataStructValue(value = " ".repeat(type.size), type = type) /** * Create a new instance of DataStructValue @@ -1221,7 +1260,7 @@ data class DataStructValue(@Contextual val value: DataStructValueBuilder, privat * */ fun createInstance(compilationUnit: CompilationUnit, dataStructName: String, values: Map): DataStructValue { val dataStructureDefinition = compilationUnit.getDataDefinition(dataStructName) - val newInstance = blank(dataStructureDefinition.type.size) + val newInstance = blank(dataStructureDefinition.type as DataStructureType) values.forEach { newInstance.set( field = dataStructureDefinition.getFieldByName(it.key), @@ -1231,9 +1270,8 @@ data class DataStructValue(@Contextual val value: DataStructValueBuilder, privat return newInstance } - internal fun fromFields(fields: Map): DataStructValue { - val size = fields.entries.fold(0) { acc, entry -> acc + entry.key.type.size } - val newInstance = blank(size) + internal fun fromFields(fields: Map, type: DataStructureType): DataStructValue { + val newInstance = blank(type = type) val fieldDefinitions = fields.map { it.key }.toFieldDefinitions() fields.onEachIndexed { index, entry -> newInstance.set(fieldDefinitions[index], entry.value) } return newInstance @@ -1385,13 +1423,13 @@ data class OccurableDataStructValue(val occurs: Int) : Value { companion object { /** * Create a blank instance of DS - * @param length The DS length (AKA DS element size) * @param occurs The occurrences number + * @param type The data structure type * */ - fun blank(length: Int, occurs: Int): OccurableDataStructValue { + fun blank(occurs: Int, type: DataStructureType): OccurableDataStructValue { return OccurableDataStructValue(occurs).apply { for (index in 1..occurs) { - values[index] = DataStructValue.blank(length) + values[index] = DataStructValue.blank(type) } } } From 127916e10182a34a049056da1938a9ea078aeba7 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Tue, 7 Jan 2025 22:20:46 +0100 Subject: [PATCH 28/35] refactor: remove the dataStructStringValueDuration This measure is no longer necessary due to the addition of tests that assert whether DataStructValueBuilder creates an instance of StringBuilderWrapper instead of IndexedStringBuilder --- .../IndexedStringBuilderEvaluator.kt | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt index c85f3de9e..35ab3d205 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/IndexedStringBuilderEvaluator.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Sme.UP S.p.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.smeup.rpgparser.interpreter import java.math.RoundingMode @@ -61,22 +77,8 @@ private fun stringBuilderVsDataStructStringBuilder( } } - // Measure the duration for DataStructValueBuilder - val dataStructStringValueDuration = measureTime { - val dataStructValueBuilder = DataStructValueBuilder.create("a".repeat(stringSize), fields) - for (i in 0 until iterations) { - val replacingChars = stringSize / fields - for (j in 0 until fields) { - val start: Int = j * replacingChars - val end: Int = start + replacingChars - dataStructValueBuilder.replace(start, end, replacingString) - } - } - } - val ratioSbIndexed = sbDuration.div(indexedSbDuration).toBigDecimal().setScale(2, RoundingMode.HALF_DOWN).toDouble() - val ratioSbDataStructStringValue = sbDuration.div(dataStructStringValueDuration).toBigDecimal().setScale(2, RoundingMode.HALF_DOWN).toDouble() - println(", $ratioSbIndexed, $ratioSbDataStructStringValue${if (debugInformation) ", ${sbDuration.plus(indexedSbDuration).toLong(DurationUnit.MILLISECONDS)}" else ""}") + println(", $ratioSbIndexed${if (debugInformation) ", ${sbDuration.plus(indexedSbDuration).toLong(DurationUnit.MILLISECONDS)}" else ""}") } /** From 227ce49114ce9bfa5d1e0a902184d9f73f311ebb Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Tue, 7 Jan 2025 22:27:25 +0100 Subject: [PATCH 29/35] refactor: `DataStructValueBuilder.create` by adding context more helpful, now it receives both value and DataStructureType All logic of this method has been moved to `JarikoCallback.createDataStructValueBuilder`, this way is totally customizable Add helfpul methods: - DataStructureType.containsOnlyArrays() - DataStructureType.totalFields(): --- .../rpgparser/execution/Configuration.kt | 17 ++++++ .../interpreter/datastruct_value_builder.kt | 52 +++++++++---------- .../com/smeup/rpgparser/interpreter/values.kt | 3 +- .../rpgparser/evaluation/DSPerformanceTest.kt | 36 ++++++++++++- .../interpreter/DataStructValueBuilderTest.kt | 43 +++++++++++---- 5 files changed, 111 insertions(+), 40 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/execution/Configuration.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/execution/Configuration.kt index 7b4e2335a..fc57d846b 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/execution/Configuration.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/execution/Configuration.kt @@ -331,6 +331,23 @@ data class JarikoCallback( */ var finishRpgTrace: (() -> Unit) = { // Defaults to a no-op + }, + + /** + * Creates a `DataStructValueBuilder` based on the provided value and data structure type. + * The default implementation creates an `IndexedStringBuilder` for data structures with 100 or more fields + * and all fields are arrays, elsewhere it creates a `StringBuilderWrapper`. + * @param value The string value to be used for the data structure. + * @param type The data structure type which defines the fields. + * @return A `DataStructValueBuilder` instance. + */ + var createDataStructValueBuilder: ((value: String, type: DataStructureType) -> DataStructValueBuilder) = { value, type -> + val totalFields = type.totalFields() + if (totalFields >= 100 && type.containsOnlyArrays()) { + IndexedStringBuilder(value = value, chunksSize = value.length / totalFields) + } else { + StringBuilderWrapper(value) + } } ) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt index fb78bbafb..7779169fa 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/datastruct_value_builder.kt @@ -16,6 +16,7 @@ package com.smeup.rpgparser.interpreter +import com.smeup.rpgparser.execution.MainExecutionContext import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -84,34 +85,15 @@ sealed interface DataStructValueBuilder { companion object { /** - * Creates an instance of DataStructValueBuilder based on the given value and fields. - * + * Creates an instance of DataStructValueBuilder. + * This method delegates the creation of the instance to the `JarikoCallback.createDataStructValueBuilder` method. * @param value The initial value of the data structure string. - * @param fields The number of fields to divide the string into. - * @return An instance of DataStructValueBuilder. The algorithm to create the instance is chosen based on the value and fields. + * @param type The type of the data structure. + * @return An instance of DataStructValueBuilder. + * @see com.smeup.rpgparser.execution.JarikoCallback.createDataStructValueBuilder */ - fun create(value: String, fields: Int): DataStructValueBuilder { - val stringSize = value.length - - return if (useIndexedStringBuilder(stringSize = stringSize, fields = fields)) { - IndexedStringBuilder(value = value, chunksSize = stringSize / fields) - } else { - StringBuilderWrapper(value) - } - } - - /** - * Determines whether to use IndexedStringBuilder based on the string size and number of fields. - * - * @param stringSize The size of the string. - * @param fields The number of fields to divide the string into. - * @return True if IndexedStringBuilder should be used, false otherwise. - */ - private fun useIndexedStringBuilder(stringSize: Int, fields: Int): Boolean { - if (stringSize >= 9000) return true - if (stringSize >= 2000 && fields >= 5) return true - return false - } + fun create(value: String, type: DataStructureType) = MainExecutionContext.getConfiguration() + .jarikoCallback.createDataStructValueBuilder(value, type) } } @@ -289,4 +271,22 @@ class IndexedStringBuilder(private val value: String, val chunksSize: Int) : Dat override fun toString(): String { return chunks.joinToString(separator = "") { it.toString() } } +} + +/** + * Checks if the data structure type contains only array fields. + * + * @return `true` if all fields in the data structure type are arrays, `false` otherwise. + */ +internal fun DataStructureType.containsOnlyArrays(): Boolean { + return fields.all { it.type is ArrayType } +} + +/** + * Counts the number of fields in the data structure type. + * + * @return The total number of fields, including elements of array fields. + */ +internal fun DataStructureType.totalFields(): Int { + return fields.sumOf { if (it.type is ArrayType) it.type.nElements else 1 } } \ No newline at end of file diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt index 0040a51f0..451e29b3a 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/values.kt @@ -1093,7 +1093,6 @@ fun Type.blank(): Value { } } -private fun List.totalFields() = this.sumOf { if (it.type is ArrayType) it.type.nElements else 1 } /** * StringValue wrapper */ @@ -1124,7 +1123,7 @@ data class DataStructValue(@Contextual val value: DataStructValueBuilder, privat * @param value the value of the data structure * @param type the type of the data structure */ - constructor(value: String, type: DataStructureType) : this(DataStructValueBuilder.create(value = value, fields = type.fields.totalFields())) + constructor(value: String, type: DataStructureType) : this(DataStructValueBuilder.create(value = value, type = type)) // We can't serialize a class with a var computed from another one because of a bug in the serialization plugin // See https://github.com/Kotlin/kotlinx.serialization/issues/133 diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/evaluation/DSPerformanceTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/evaluation/DSPerformanceTest.kt index 996a79591..835d170af 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/evaluation/DSPerformanceTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/evaluation/DSPerformanceTest.kt @@ -19,6 +19,7 @@ package com.smeup.rpgparser.evaluation import com.smeup.rpgparser.AbstractTest import com.smeup.rpgparser.PerformanceTest import com.smeup.rpgparser.execution.Configuration +import com.smeup.rpgparser.interpreter.StringBuilderWrapper import com.smeup.rpgparser.jvminterop.JavaSystemInterface import org.junit.Test import org.junit.experimental.categories.Category @@ -66,8 +67,41 @@ open class DSPerformanceTest : AbstractTest() { } "DSPERF02".outputOf(configuration = configuration) val duration = Duration.between(start.toInstant(), end.toInstant()).toMillis().milliseconds - println(duration) + println("executeDSPERF02 with default useIndexedStringBuilder: $duration ms") // Currently the assertion is really empirical assertTrue(duration.toLong(DurationUnit.SECONDS) < 2, "Duration must be less than 2 second") } + + @Test + @Category(PerformanceTest::class) + fun executeDSPERF02CompareIndexedStringBuilderVsStringBuilder() { + var useIndexedStringBuilder = false + val start = mutableMapOf() + val end = mutableMapOf() + val createDataStructValueBuilderDefaultImpl = Configuration().jarikoCallback.createDataStructValueBuilder + val configuration = Configuration().apply { + jarikoCallback.onEnterPgm = { _, _ -> + start[useIndexedStringBuilder] = Date() + } + jarikoCallback.onExitPgm = { _, _, _ -> + end[useIndexedStringBuilder] = Date() + } + jarikoCallback.createDataStructValueBuilder = { value, type -> + if (useIndexedStringBuilder) { + createDataStructValueBuilderDefaultImpl(value, type) + } else { + StringBuilderWrapper(value) + } + } + } + "DSPERF02".outputOf(configuration = configuration) + val durationNotUseIndexedStringBuilder = Duration.between(start[false]!!.toInstant(), end[false]!!.toInstant()).toMillis().milliseconds + println("executeDSPERF02 with useIndexedStringBuilder=false: $durationNotUseIndexedStringBuilder ms") + useIndexedStringBuilder = true + "DSPERF02".outputOf(configuration = configuration) + val durationUseIndexedStringBuilder = Duration.between(start[true]!!.toInstant(), end[true]!!.toInstant()).toMillis().milliseconds + println("executeDSPERF02 with useIndexedStringBuilder=true: $durationUseIndexedStringBuilder ms") + assertTrue(durationNotUseIndexedStringBuilder / durationUseIndexedStringBuilder >= 10, + "Duration with useIndexedStringBuilder=false must be at least 10 times greater than duration with useIndexedStringBuilder=true") + } } \ No newline at end of file diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt index ad2045992..780ef2fd0 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/interpreter/DataStructValueBuilderTest.kt @@ -41,17 +41,38 @@ class DataStructValueBuilderTest { } @Test - fun inCaseOfLittleDSUseStringBuilderWrapper() { - val littleDS = listOf(10, 20, 20, 4, 5) - val dataStructValueBuilder = DataStructValueBuilder.create(value = "a".repeat(littleDS.sum()), fields = littleDS.size) - assertIs(dataStructValueBuilder) - } - - @Test - fun inCaseOfBigDSUseIndexedStringBuilder() { - val bigDS = listOf(1000, 2000, 3000, 4000, 5000) - val dataStructValueBuilder = DataStructValueBuilder.create(value = "a".repeat(bigDS.sum()), fields = bigDS.size) - assertIs(dataStructValueBuilder) + fun dataStructureUsesStringBuilderWrapper() { + val fields = List(10) { FieldType(name = "name$it", type = StringType(10, false)) } + val type = DataStructureType(fields, fields.sumOf { it.type.size }) + val dataStructValueBuilder = DataStructValueBuilder.create(value = "a".repeat(type.size), type = type) + assertIs( + value = dataStructValueBuilder, + message = "Data structure with total fields less than 100 uses StringBuilderWrapper" + ) + } + + @Test + fun arrayDataStructureUsesStringBuilderWrapper() { + val arrayType = ArrayType(StringType(10, false), 99) + val fields = List(1) { FieldType(name = "name$it", type = arrayType) } + val type = DataStructureType(fields, fields.sumOf { it.type.size }) + val dataStructValueBuilder = DataStructValueBuilder.create(value = "a".repeat(type.size), type = type) + assertIs( + value = dataStructValueBuilder, + message = "Array data structure with total fields less than 100 uses StringBuilderWrapper" + ) + } + + @Test + fun arrayDataStructureUsesIndexedStringBuilder() { + val arrayType = ArrayType(StringType(10, false), 100) + val fields = List(1) { FieldType(name = "name$it", type = arrayType) } + val type = DataStructureType(fields, fields.sumOf { it.type.size }) + val dataStructValueBuilder = DataStructValueBuilder.create(value = "a".repeat(type.size), type = type) + assertIs( + value = dataStructValueBuilder, + message = "Array data structure with total fields equals or greater than 100 uses IndexedStringBuilder" + ) } @Test From e73a31d4e8122afc5c80208810ffa202c8ed034a Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Fri, 10 Jan 2025 23:04:44 +0100 Subject: [PATCH 30/35] perf: in decodeFromDS replaced String with StringBuilder whenever possible --- .../rpgparser/interpreter/data_definitions.kt | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt index 060a1ae5e..973ea08d7 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt @@ -769,45 +769,41 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { fun decodeFromDS(value: String, digits: Int, scale: Int): BigDecimal { val buffer = IntArray(value.length) for (i in value.indices) { - buffer[i] = value[i].toInt() + buffer[i] = value[i].code } - var sign = "" - var number = "" - var nibble = ((buffer[buffer.size - 1]) and 0x0F) + val sign = StringBuilder() + val number = StringBuilder() + var nibble = (buffer[buffer.size - 1] and 0x0F) if (nibble == 0x0B || nibble == 0x0D) { - sign = "-" + sign.append("-") } var offset = 0 while (offset < (buffer.size - 1)) { nibble = (buffer[offset] and 0xFF).ushr(4) - number += Character.toString((nibble or 0x30).toChar()) + number.append((nibble or 0x30).toChar()) nibble = buffer[offset] and 0x0F or 0x30 - number += Character.toString((nibble or 0x30).toChar()) + number.append((nibble or 0x30).toChar()) offset++ } - // read last digit +// read last digit nibble = (buffer[offset] and 0xFF).ushr(4) if (nibble <= 9) { - number += Character.toString((nibble or 0x30).toChar()) + number.append((nibble or 0x30).toChar()) } - // adjust the scale - if (scale > 0 && number != "0") { +// adjust the scale + if (scale > 0 && number.toString() != "0") { val len = number.length - number = buildString { - append(number.substring(0, len - scale)) - append(".") - append(number.substring(len - scale, len)) - } + number.insert(len - scale, ".") } - number = sign + number + number.insert(0, sign) return try { value.toBigDecimal() } catch (e: Exception) { - number.toBigDecimal() + number.toString().toBigDecimal() } } From 4b53be0a96fb989c4f78029965df6a1dc03acc33 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sat, 11 Jan 2025 10:53:15 +0100 Subject: [PATCH 31/35] perf: in decodeFromDS all logic related the conversion when the string is not numeric, must be executed as an exception and not always replace a lot of kotlin deprecated functions --- .../rpgparser/interpreter/data_definitions.kt | 186 +++++++++--------- 1 file changed, 95 insertions(+), 91 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt index 973ea08d7..a8062ca1f 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt @@ -29,6 +29,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import java.math.BigDecimal +import java.util.* @Serializable abstract class AbstractDataDefinition( @@ -112,7 +113,7 @@ enum class ParamOption(val keyword: String) { companion object { fun getByKeyword(keyword: String): ParamOption { - return ParamOption.values().first { + return entries.first { it.keyword == keyword } } @@ -143,7 +144,7 @@ enum class FileType(val keyword: String?) { companion object { // see https://www.ibm.com/docs/sl/i/7.3?topic=statement-position-17-file-type fun getByKeyword(keyword: String): FileType { - return FileType.values().firstOrNull() { + return entries.firstOrNull() { it.keyword == keyword } ?: DB } @@ -166,7 +167,7 @@ data class FileDefinition private constructor( var internalFormatName: String? = null set(value) { - field = value?.toUpperCase() + field = value?.uppercase(Locale.getDefault()) } private var fieldNameToDataDefinitionName = mutableMapOf() @@ -505,7 +506,7 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[0] = (lsb and 0x0000FFFF).toByte() - return buffer[0].toChar().toString() + return buffer[0].toInt().toChar().toString() } if (size == 2) { @@ -513,7 +514,7 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[0] = ((lsb shr 8) and 0x000000FF).toByte() buffer[1] = (lsb and 0x000000FF).toByte() - return buffer[1].toChar().toString() + buffer[0].toChar().toString() + return buffer[1].toInt().toChar().toString() + buffer[0].toInt().toChar().toString() } if (size == 4) { @@ -522,7 +523,8 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[2] = ((lsb shr 8) and 0x0000FFFF).toByte() buffer[3] = (lsb and 0x0000FFFF).toByte() - return buffer[3].toChar().toString() + buffer[2].toChar().toString() + buffer[1].toChar().toString() + buffer[0].toChar().toString() + return buffer[3].toInt().toChar().toString() + buffer[2].toInt().toChar().toString() + buffer[1].toInt() + .toChar().toString() + buffer[0].toInt().toChar().toString() } if (size == 8) { val llsb = inValue.toLong() @@ -535,8 +537,10 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[6] = ((llsb shr 8) and 0x0000FFFF).toByte() buffer[7] = (llsb and 0x0000FFFF).toByte() - return buffer[7].toChar().toString() + buffer[6].toChar().toString() + buffer[5].toChar().toString() + buffer[4].toChar().toString() + - buffer[3].toChar().toString() + buffer[2].toChar().toString() + buffer[1].toChar().toString() + buffer[0].toChar().toString() + return buffer[7].toInt().toChar().toString() + buffer[6].toInt().toChar().toString() + buffer[5].toInt() + .toChar().toString() + buffer[4].toInt().toChar().toString() + + buffer[3].toInt().toChar().toString() + buffer[2].toInt().toChar().toString() + buffer[1].toInt().toChar().toString() + buffer[0].toInt() + .toChar().toString() } TODO("encode binary for $size not implemented") } @@ -552,39 +556,39 @@ fun encodeUnsigned(inValue: BigDecimal, size: Int): String { fun decodeBinary(value: String, size: Int): BigDecimal { if (size == 1) { var number: Long = 0x0000000 - if (value[0].toInt() and 0x0010 != 0) { + if (value[0].code and 0x0010 != 0) { number = 0x00000000 } - number += (value[0].toInt() and 0x00FF) + number += (value[0].code and 0x00FF) return BigDecimal(number.toInt().toString()) } if (size == 2) { var number: Long = 0x0000000 - if (value[1].toInt() and 0x8000 != 0) { + if (value[1].code and 0x8000 != 0) { number = 0xFFFF0000 } - number += (value[0].toInt() and 0x00FF) + ((value[1].toInt() and 0x00FF) shl 8) + number += (value[0].code and 0x00FF) + ((value[1].code and 0x00FF) shl 8) return BigDecimal(number.toInt().toString()) } if (size == 4) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) return BigDecimal(number.toInt().toString()) } if (size == 8) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + - ((value[4].toLong() and 0x00FF) shl 32) + - ((value[5].toLong() and 0x00FF) shl 40) + - ((value[6].toLong() and 0x00FF) shl 48) + - ((value[7].toLong() and 0x00FF) shl 56) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) + + ((value[4].code.toLong() and 0x00FF) shl 32) + + ((value[5].code.toLong() and 0x00FF) shl 40) + + ((value[6].code.toLong() and 0x00FF) shl 48) + + ((value[7].code.toLong() and 0x00FF) shl 56) return BigDecimal(number.toInt().toString()) } @@ -594,35 +598,35 @@ fun decodeBinary(value: String, size: Int): BigDecimal { fun decodeInteger(value: String, size: Int): BigDecimal { if (size == 1) { var number = 0x0000000 - number += (value[0].toByte()) + number += (value[0].code.toByte()) return BigDecimal(number.toString()) } if (size == 2) { var number: Long = 0x0000000 - if (value[1].toInt() and 0x8000 != 0) { + if (value[1].code and 0x8000 != 0) { number = 0xFFFF0000 } - number += (value[0].toInt() and 0x00FF) + ((value[1].toInt() and 0x00FF) shl 8) + number += (value[0].code and 0x00FF) + ((value[1].code and 0x00FF) shl 8) return BigDecimal(number.toInt().toString()) } if (size == 4) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) return BigDecimal(number.toInt().toString()) } if (size == 8) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + - ((value[4].toLong() and 0x00FF) shl 32) + - ((value[5].toLong() and 0x00FF) shl 40) + - ((value[6].toLong() and 0x00FF) shl 48) + - ((value[7].toLong() and 0x00FF) shl 56) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) + + ((value[4].code.toLong() and 0x00FF) shl 32) + + ((value[5].code.toLong() and 0x00FF) shl 40) + + ((value[6].code.toLong() and 0x00FF) shl 48) + + ((value[7].code.toLong() and 0x00FF) shl 56) return BigDecimal(number.toString()) } @@ -633,40 +637,40 @@ fun decodeUnsigned(value: String, size: Int): BigDecimal { if (size == 1) { var number: Long = 0x0000000 - if (value[0].toInt() and 0x0010 != 0) { + if (value[0].code and 0x0010 != 0) { number = 0x00000000 } - number += (value[0].toInt() and 0x00FF) + number += (value[0].code and 0x00FF) return BigDecimal(number.toInt().toString()) } if (size == 2) { var number: Long = 0x0000000 - if (value[1].toInt() and 0x1000 != 0) { + if (value[1].code and 0x1000 != 0) { number = 0xFFFF0000 } - number += (value[0].toInt() and 0x00FF) + ((value[1].toInt() and 0x00FF) shl 8) + number += (value[0].code and 0x00FF) + ((value[1].code and 0x00FF) shl 8) // make sure you count onlu 16 bits number = number and 0x0000FFFF return BigDecimal(number.toString()) } if (size == 4) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) return BigDecimal(number.toString()) } if (size == 8) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + - ((value[4].toLong() and 0x00FF) shl 32) + - ((value[5].toLong() and 0x00FF) shl 40) + - ((value[6].toLong() and 0x00FF) shl 48) + - ((value[7].toLong() and 0x00FF) shl 56) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) + + ((value[4].code.toLong() and 0x00FF) shl 32) + + ((value[5].code.toLong() and 0x00FF) shl 40) + + ((value[6].code.toLong() and 0x00FF) shl 48) + + ((value[7].code.toLong() and 0x00FF) shl 56) return BigDecimal(number.toInt().toString()) } @@ -685,7 +689,7 @@ fun encodeToZoned(inValue: BigDecimal, digits: Int, scale: Int): String { val sign = inValue.signum() inChars.forEachIndexed { index, char -> - val digit = char.toInt() + val digit = char.code buffer[index] = digit } if (sign < 0) { @@ -708,11 +712,11 @@ fun decodeFromZoned(value: String, digits: Int, scale: Int): BigDecimal { when { it.isDigit() -> builder.append(it) else -> { - if (it.toInt() == 0) { + if (it.code == 0) { builder.append('0') } else { builder.insert(0, '-') - builder.append((it.toInt() - 0x0049 + 0x0030).toChar()) + builder.append((it.code - 0x0049 + 0x0030).toChar()) } } } @@ -741,8 +745,8 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { // place all the digits except last one while (inPosition < inChars.size - 1) { - firstNibble = ((inChars[inPosition++].toInt()) and 0x000F) shl 4 - secondNibble = (inChars[inPosition++].toInt()) and 0x000F + firstNibble = ((inChars[inPosition++].code) and 0x000F) shl 4 + secondNibble = (inChars[inPosition++].code) and 0x000F buffer[offset++] = (firstNibble + secondNibble) } @@ -750,7 +754,7 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { firstNibble = if (inPosition == inChars.size) { 0x00F0 } else { - (inChars[inChars.size - 1].toInt()) and 0x000F shl 4 + (inChars[inChars.size - 1].code) and 0x000F shl 4 } if (sign != -1) { buffer[offset] = (firstNibble + 0x000F) @@ -767,42 +771,42 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { } fun decodeFromDS(value: String, digits: Int, scale: Int): BigDecimal { - val buffer = IntArray(value.length) - for (i in value.indices) { - buffer[i] = value[i].code - } + return try { + value.toBigDecimal() + } catch (e: Exception) { + val buffer = IntArray(value.length) + for (i in value.indices) { + buffer[i] = value[i].code + } - val sign = StringBuilder() - val number = StringBuilder() - var nibble = (buffer[buffer.size - 1] and 0x0F) - if (nibble == 0x0B || nibble == 0x0D) { - sign.append("-") - } + val sign = StringBuilder() + val number = StringBuilder() + var nibble = (buffer[buffer.size - 1] and 0x0F) + if (nibble == 0x0B || nibble == 0x0D) { + sign.append("-") + } - var offset = 0 - while (offset < (buffer.size - 1)) { - nibble = (buffer[offset] and 0xFF).ushr(4) - number.append((nibble or 0x30).toChar()) - nibble = buffer[offset] and 0x0F or 0x30 - number.append((nibble or 0x30).toChar()) + var offset = 0 + while (offset < (buffer.size - 1)) { + nibble = (buffer[offset] and 0xFF).ushr(4) + number.append((nibble or 0x30).toChar()) + nibble = buffer[offset] and 0x0F or 0x30 + number.append((nibble or 0x30).toChar()) - offset++ - } + offset++ + } // read last digit - nibble = (buffer[offset] and 0xFF).ushr(4) - if (nibble <= 9) { - number.append((nibble or 0x30).toChar()) - } + nibble = (buffer[offset] and 0xFF).ushr(4) + if (nibble <= 9) { + number.append((nibble or 0x30).toChar()) + } // adjust the scale - if (scale > 0 && number.toString() != "0") { - val len = number.length - number.insert(len - scale, ".") - } - number.insert(0, sign) - return try { - value.toBigDecimal() - } catch (e: Exception) { + if (scale > 0 && number.toString() != "0") { + val len = number.length + number.insert(len - scale, ".") + } + number.insert(0, sign) number.toString().toBigDecimal() } } From d15cff99b9010da4d60577c9cb20495788d04e01 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sat, 11 Jan 2025 11:01:19 +0100 Subject: [PATCH 32/35] test: Add DataDefinitionPerformanceTest tests suite --- .../parsing/ast/DataDefinitionTest.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/parsing/ast/DataDefinitionTest.kt b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/parsing/ast/DataDefinitionTest.kt index 627dbd16a..0436f1bdf 100644 --- a/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/parsing/ast/DataDefinitionTest.kt +++ b/rpgJavaInterpreter-core/src/test/kotlin/com/smeup/rpgparser/parsing/ast/DataDefinitionTest.kt @@ -16,17 +16,16 @@ package com.smeup.rpgparser.parsing.ast -import com.smeup.rpgparser.AbstractTest -import com.smeup.rpgparser.assertDataDefinitionIsPresent -import com.smeup.rpgparser.execute +import com.smeup.rpgparser.* import com.smeup.rpgparser.interpreter.* -import com.smeup.rpgparser.parseFragmentToCompilationUnit import com.smeup.rpgparser.parsing.parsetreetoast.RpgType import com.smeup.rpgparser.parsing.parsetreetoast.ToAstConfiguration import com.smeup.rpgparser.parsing.parsetreetoast.resolveAndValidate +import org.junit.experimental.categories.Category import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue +import kotlin.time.measureTime import org.junit.Test as test open class DataDefinitionTest : AbstractTest() { @@ -357,3 +356,18 @@ open class DataDefinitionTest : AbstractTest() { StringValue("5L")), StringType(2)) as ArrayValue, logValue) } } + +class DataDefinitionPerformanceTest : AbstractTest() { + + @Test(timeout = 10_000) + @Category(PerformanceTest::class) + fun decodeFromDS() { + measureTime { + for (i in 1..100_000_000) { + decodeFromDS(value = "123456789.15", digits = 20, scale = 2) + } + }.also { + println("decodeFromDS: $it") + } + } +} From fbdbd8cb4b2997aa59300cbfe5f041ac0cf0c912 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 12 Jan 2025 20:51:39 +0100 Subject: [PATCH 33/35] Revert "perf: in decodeFromDS all logic related the conversion when the string is not numeric, must be executed as an exception and not always" This reverts commit 4b53be0a96fb989c4f78029965df6a1dc03acc33. --- .../rpgparser/interpreter/data_definitions.kt | 186 +++++++++--------- 1 file changed, 91 insertions(+), 95 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt index a8062ca1f..973ea08d7 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt @@ -29,7 +29,6 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import java.math.BigDecimal -import java.util.* @Serializable abstract class AbstractDataDefinition( @@ -113,7 +112,7 @@ enum class ParamOption(val keyword: String) { companion object { fun getByKeyword(keyword: String): ParamOption { - return entries.first { + return ParamOption.values().first { it.keyword == keyword } } @@ -144,7 +143,7 @@ enum class FileType(val keyword: String?) { companion object { // see https://www.ibm.com/docs/sl/i/7.3?topic=statement-position-17-file-type fun getByKeyword(keyword: String): FileType { - return entries.firstOrNull() { + return FileType.values().firstOrNull() { it.keyword == keyword } ?: DB } @@ -167,7 +166,7 @@ data class FileDefinition private constructor( var internalFormatName: String? = null set(value) { - field = value?.uppercase(Locale.getDefault()) + field = value?.toUpperCase() } private var fieldNameToDataDefinitionName = mutableMapOf() @@ -506,7 +505,7 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[0] = (lsb and 0x0000FFFF).toByte() - return buffer[0].toInt().toChar().toString() + return buffer[0].toChar().toString() } if (size == 2) { @@ -514,7 +513,7 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[0] = ((lsb shr 8) and 0x000000FF).toByte() buffer[1] = (lsb and 0x000000FF).toByte() - return buffer[1].toInt().toChar().toString() + buffer[0].toInt().toChar().toString() + return buffer[1].toChar().toString() + buffer[0].toChar().toString() } if (size == 4) { @@ -523,8 +522,7 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[2] = ((lsb shr 8) and 0x0000FFFF).toByte() buffer[3] = (lsb and 0x0000FFFF).toByte() - return buffer[3].toInt().toChar().toString() + buffer[2].toInt().toChar().toString() + buffer[1].toInt() - .toChar().toString() + buffer[0].toInt().toChar().toString() + return buffer[3].toChar().toString() + buffer[2].toChar().toString() + buffer[1].toChar().toString() + buffer[0].toChar().toString() } if (size == 8) { val llsb = inValue.toLong() @@ -537,10 +535,8 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[6] = ((llsb shr 8) and 0x0000FFFF).toByte() buffer[7] = (llsb and 0x0000FFFF).toByte() - return buffer[7].toInt().toChar().toString() + buffer[6].toInt().toChar().toString() + buffer[5].toInt() - .toChar().toString() + buffer[4].toInt().toChar().toString() + - buffer[3].toInt().toChar().toString() + buffer[2].toInt().toChar().toString() + buffer[1].toInt().toChar().toString() + buffer[0].toInt() - .toChar().toString() + return buffer[7].toChar().toString() + buffer[6].toChar().toString() + buffer[5].toChar().toString() + buffer[4].toChar().toString() + + buffer[3].toChar().toString() + buffer[2].toChar().toString() + buffer[1].toChar().toString() + buffer[0].toChar().toString() } TODO("encode binary for $size not implemented") } @@ -556,39 +552,39 @@ fun encodeUnsigned(inValue: BigDecimal, size: Int): String { fun decodeBinary(value: String, size: Int): BigDecimal { if (size == 1) { var number: Long = 0x0000000 - if (value[0].code and 0x0010 != 0) { + if (value[0].toInt() and 0x0010 != 0) { number = 0x00000000 } - number += (value[0].code and 0x00FF) + number += (value[0].toInt() and 0x00FF) return BigDecimal(number.toInt().toString()) } if (size == 2) { var number: Long = 0x0000000 - if (value[1].code and 0x8000 != 0) { + if (value[1].toInt() and 0x8000 != 0) { number = 0xFFFF0000 } - number += (value[0].code and 0x00FF) + ((value[1].code and 0x00FF) shl 8) + number += (value[0].toInt() and 0x00FF) + ((value[1].toInt() and 0x00FF) shl 8) return BigDecimal(number.toInt().toString()) } if (size == 4) { - val number = (value[0].code.toLong() and 0x00FF) + - ((value[1].code.toLong() and 0x00FF) shl 8) + - ((value[2].code.toLong() and 0x00FF) shl 16) + - ((value[3].code.toLong() and 0x00FF) shl 24) + val number = (value[0].toLong() and 0x00FF) + + ((value[1].toLong() and 0x00FF) shl 8) + + ((value[2].toLong() and 0x00FF) shl 16) + + ((value[3].toLong() and 0x00FF) shl 24) return BigDecimal(number.toInt().toString()) } if (size == 8) { - val number = (value[0].code.toLong() and 0x00FF) + - ((value[1].code.toLong() and 0x00FF) shl 8) + - ((value[2].code.toLong() and 0x00FF) shl 16) + - ((value[3].code.toLong() and 0x00FF) shl 24) + - ((value[4].code.toLong() and 0x00FF) shl 32) + - ((value[5].code.toLong() and 0x00FF) shl 40) + - ((value[6].code.toLong() and 0x00FF) shl 48) + - ((value[7].code.toLong() and 0x00FF) shl 56) + val number = (value[0].toLong() and 0x00FF) + + ((value[1].toLong() and 0x00FF) shl 8) + + ((value[2].toLong() and 0x00FF) shl 16) + + ((value[3].toLong() and 0x00FF) shl 24) + + ((value[4].toLong() and 0x00FF) shl 32) + + ((value[5].toLong() and 0x00FF) shl 40) + + ((value[6].toLong() and 0x00FF) shl 48) + + ((value[7].toLong() and 0x00FF) shl 56) return BigDecimal(number.toInt().toString()) } @@ -598,35 +594,35 @@ fun decodeBinary(value: String, size: Int): BigDecimal { fun decodeInteger(value: String, size: Int): BigDecimal { if (size == 1) { var number = 0x0000000 - number += (value[0].code.toByte()) + number += (value[0].toByte()) return BigDecimal(number.toString()) } if (size == 2) { var number: Long = 0x0000000 - if (value[1].code and 0x8000 != 0) { + if (value[1].toInt() and 0x8000 != 0) { number = 0xFFFF0000 } - number += (value[0].code and 0x00FF) + ((value[1].code and 0x00FF) shl 8) + number += (value[0].toInt() and 0x00FF) + ((value[1].toInt() and 0x00FF) shl 8) return BigDecimal(number.toInt().toString()) } if (size == 4) { - val number = (value[0].code.toLong() and 0x00FF) + - ((value[1].code.toLong() and 0x00FF) shl 8) + - ((value[2].code.toLong() and 0x00FF) shl 16) + - ((value[3].code.toLong() and 0x00FF) shl 24) + val number = (value[0].toLong() and 0x00FF) + + ((value[1].toLong() and 0x00FF) shl 8) + + ((value[2].toLong() and 0x00FF) shl 16) + + ((value[3].toLong() and 0x00FF) shl 24) return BigDecimal(number.toInt().toString()) } if (size == 8) { - val number = (value[0].code.toLong() and 0x00FF) + - ((value[1].code.toLong() and 0x00FF) shl 8) + - ((value[2].code.toLong() and 0x00FF) shl 16) + - ((value[3].code.toLong() and 0x00FF) shl 24) + - ((value[4].code.toLong() and 0x00FF) shl 32) + - ((value[5].code.toLong() and 0x00FF) shl 40) + - ((value[6].code.toLong() and 0x00FF) shl 48) + - ((value[7].code.toLong() and 0x00FF) shl 56) + val number = (value[0].toLong() and 0x00FF) + + ((value[1].toLong() and 0x00FF) shl 8) + + ((value[2].toLong() and 0x00FF) shl 16) + + ((value[3].toLong() and 0x00FF) shl 24) + + ((value[4].toLong() and 0x00FF) shl 32) + + ((value[5].toLong() and 0x00FF) shl 40) + + ((value[6].toLong() and 0x00FF) shl 48) + + ((value[7].toLong() and 0x00FF) shl 56) return BigDecimal(number.toString()) } @@ -637,40 +633,40 @@ fun decodeUnsigned(value: String, size: Int): BigDecimal { if (size == 1) { var number: Long = 0x0000000 - if (value[0].code and 0x0010 != 0) { + if (value[0].toInt() and 0x0010 != 0) { number = 0x00000000 } - number += (value[0].code and 0x00FF) + number += (value[0].toInt() and 0x00FF) return BigDecimal(number.toInt().toString()) } if (size == 2) { var number: Long = 0x0000000 - if (value[1].code and 0x1000 != 0) { + if (value[1].toInt() and 0x1000 != 0) { number = 0xFFFF0000 } - number += (value[0].code and 0x00FF) + ((value[1].code and 0x00FF) shl 8) + number += (value[0].toInt() and 0x00FF) + ((value[1].toInt() and 0x00FF) shl 8) // make sure you count onlu 16 bits number = number and 0x0000FFFF return BigDecimal(number.toString()) } if (size == 4) { - val number = (value[0].code.toLong() and 0x00FF) + - ((value[1].code.toLong() and 0x00FF) shl 8) + - ((value[2].code.toLong() and 0x00FF) shl 16) + - ((value[3].code.toLong() and 0x00FF) shl 24) + val number = (value[0].toLong() and 0x00FF) + + ((value[1].toLong() and 0x00FF) shl 8) + + ((value[2].toLong() and 0x00FF) shl 16) + + ((value[3].toLong() and 0x00FF) shl 24) return BigDecimal(number.toString()) } if (size == 8) { - val number = (value[0].code.toLong() and 0x00FF) + - ((value[1].code.toLong() and 0x00FF) shl 8) + - ((value[2].code.toLong() and 0x00FF) shl 16) + - ((value[3].code.toLong() and 0x00FF) shl 24) + - ((value[4].code.toLong() and 0x00FF) shl 32) + - ((value[5].code.toLong() and 0x00FF) shl 40) + - ((value[6].code.toLong() and 0x00FF) shl 48) + - ((value[7].code.toLong() and 0x00FF) shl 56) + val number = (value[0].toLong() and 0x00FF) + + ((value[1].toLong() and 0x00FF) shl 8) + + ((value[2].toLong() and 0x00FF) shl 16) + + ((value[3].toLong() and 0x00FF) shl 24) + + ((value[4].toLong() and 0x00FF) shl 32) + + ((value[5].toLong() and 0x00FF) shl 40) + + ((value[6].toLong() and 0x00FF) shl 48) + + ((value[7].toLong() and 0x00FF) shl 56) return BigDecimal(number.toInt().toString()) } @@ -689,7 +685,7 @@ fun encodeToZoned(inValue: BigDecimal, digits: Int, scale: Int): String { val sign = inValue.signum() inChars.forEachIndexed { index, char -> - val digit = char.code + val digit = char.toInt() buffer[index] = digit } if (sign < 0) { @@ -712,11 +708,11 @@ fun decodeFromZoned(value: String, digits: Int, scale: Int): BigDecimal { when { it.isDigit() -> builder.append(it) else -> { - if (it.code == 0) { + if (it.toInt() == 0) { builder.append('0') } else { builder.insert(0, '-') - builder.append((it.code - 0x0049 + 0x0030).toChar()) + builder.append((it.toInt() - 0x0049 + 0x0030).toChar()) } } } @@ -745,8 +741,8 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { // place all the digits except last one while (inPosition < inChars.size - 1) { - firstNibble = ((inChars[inPosition++].code) and 0x000F) shl 4 - secondNibble = (inChars[inPosition++].code) and 0x000F + firstNibble = ((inChars[inPosition++].toInt()) and 0x000F) shl 4 + secondNibble = (inChars[inPosition++].toInt()) and 0x000F buffer[offset++] = (firstNibble + secondNibble) } @@ -754,7 +750,7 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { firstNibble = if (inPosition == inChars.size) { 0x00F0 } else { - (inChars[inChars.size - 1].code) and 0x000F shl 4 + (inChars[inChars.size - 1].toInt()) and 0x000F shl 4 } if (sign != -1) { buffer[offset] = (firstNibble + 0x000F) @@ -771,42 +767,42 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { } fun decodeFromDS(value: String, digits: Int, scale: Int): BigDecimal { - return try { - value.toBigDecimal() - } catch (e: Exception) { - val buffer = IntArray(value.length) - for (i in value.indices) { - buffer[i] = value[i].code - } + val buffer = IntArray(value.length) + for (i in value.indices) { + buffer[i] = value[i].code + } - val sign = StringBuilder() - val number = StringBuilder() - var nibble = (buffer[buffer.size - 1] and 0x0F) - if (nibble == 0x0B || nibble == 0x0D) { - sign.append("-") - } + val sign = StringBuilder() + val number = StringBuilder() + var nibble = (buffer[buffer.size - 1] and 0x0F) + if (nibble == 0x0B || nibble == 0x0D) { + sign.append("-") + } - var offset = 0 - while (offset < (buffer.size - 1)) { - nibble = (buffer[offset] and 0xFF).ushr(4) - number.append((nibble or 0x30).toChar()) - nibble = buffer[offset] and 0x0F or 0x30 - number.append((nibble or 0x30).toChar()) + var offset = 0 + while (offset < (buffer.size - 1)) { + nibble = (buffer[offset] and 0xFF).ushr(4) + number.append((nibble or 0x30).toChar()) + nibble = buffer[offset] and 0x0F or 0x30 + number.append((nibble or 0x30).toChar()) - offset++ - } + offset++ + } // read last digit - nibble = (buffer[offset] and 0xFF).ushr(4) - if (nibble <= 9) { - number.append((nibble or 0x30).toChar()) - } + nibble = (buffer[offset] and 0xFF).ushr(4) + if (nibble <= 9) { + number.append((nibble or 0x30).toChar()) + } // adjust the scale - if (scale > 0 && number.toString() != "0") { - val len = number.length - number.insert(len - scale, ".") - } - number.insert(0, sign) + if (scale > 0 && number.toString() != "0") { + val len = number.length + number.insert(len - scale, ".") + } + number.insert(0, sign) + return try { + value.toBigDecimal() + } catch (e: Exception) { number.toString().toBigDecimal() } } From e474f9f59e66f2e34134b4c22186c5c09fa22427 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 12 Jan 2025 20:56:55 +0100 Subject: [PATCH 34/35] perf: in decodeFromDS all logic related the conversion when the string is not numeric, must be executed as an exception and not always --- .../rpgparser/interpreter/data_definitions.kt | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt index 973ea08d7..612f6f19d 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt @@ -767,42 +767,42 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { } fun decodeFromDS(value: String, digits: Int, scale: Int): BigDecimal { - val buffer = IntArray(value.length) - for (i in value.indices) { - buffer[i] = value[i].code - } + return try { + value.toBigDecimal() + } catch (e: Exception) { + val buffer = IntArray(value.length) + for (i in value.indices) { + buffer[i] = value[i].code + } - val sign = StringBuilder() - val number = StringBuilder() - var nibble = (buffer[buffer.size - 1] and 0x0F) - if (nibble == 0x0B || nibble == 0x0D) { - sign.append("-") - } + val sign = StringBuilder() + val number = StringBuilder() + var nibble = (buffer[buffer.size - 1] and 0x0F) + if (nibble == 0x0B || nibble == 0x0D) { + sign.append("-") + } - var offset = 0 - while (offset < (buffer.size - 1)) { - nibble = (buffer[offset] and 0xFF).ushr(4) - number.append((nibble or 0x30).toChar()) - nibble = buffer[offset] and 0x0F or 0x30 - number.append((nibble or 0x30).toChar()) + var offset = 0 + while (offset < (buffer.size - 1)) { + nibble = (buffer[offset] and 0xFF).ushr(4) + number.append((nibble or 0x30).toChar()) + nibble = buffer[offset] and 0x0F or 0x30 + number.append((nibble or 0x30).toChar()) - offset++ - } + offset++ + } // read last digit - nibble = (buffer[offset] and 0xFF).ushr(4) - if (nibble <= 9) { - number.append((nibble or 0x30).toChar()) - } + nibble = (buffer[offset] and 0xFF).ushr(4) + if (nibble <= 9) { + number.append((nibble or 0x30).toChar()) + } // adjust the scale - if (scale > 0 && number.toString() != "0") { - val len = number.length - number.insert(len - scale, ".") - } - number.insert(0, sign) - return try { - value.toBigDecimal() - } catch (e: Exception) { + if (scale > 0 && number.toString() != "0") { + val len = number.length + number.insert(len - scale, ".") + } + number.insert(0, sign) number.toString().toBigDecimal() } } From e535a86c4462e2a3f123abc78022bac4933f0c87 Mon Sep 17 00:00:00 2001 From: lanarimarco Date: Sun, 12 Jan 2025 21:05:54 +0100 Subject: [PATCH 35/35] chore: fix warnings --- .../rpgparser/interpreter/data_definitions.kt | 122 +++++++++--------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt index 612f6f19d..bbe49dfd1 100644 --- a/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt +++ b/rpgJavaInterpreter-core/src/main/kotlin/com/smeup/rpgparser/interpreter/data_definitions.kt @@ -29,6 +29,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import java.math.BigDecimal +import java.util.* @Serializable abstract class AbstractDataDefinition( @@ -166,7 +167,7 @@ data class FileDefinition private constructor( var internalFormatName: String? = null set(value) { - field = value?.toUpperCase() + field = value?.uppercase(Locale.getDefault()) } private var fieldNameToDataDefinitionName = mutableMapOf() @@ -505,7 +506,7 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[0] = (lsb and 0x0000FFFF).toByte() - return buffer[0].toChar().toString() + return buffer[0].toInt().toChar().toString() } if (size == 2) { @@ -513,7 +514,7 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[0] = ((lsb shr 8) and 0x000000FF).toByte() buffer[1] = (lsb and 0x000000FF).toByte() - return buffer[1].toChar().toString() + buffer[0].toChar().toString() + return buffer[1].toInt().toChar().toString() + buffer[0].toInt().toChar().toString() } if (size == 4) { @@ -522,7 +523,8 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[2] = ((lsb shr 8) and 0x0000FFFF).toByte() buffer[3] = (lsb and 0x0000FFFF).toByte() - return buffer[3].toChar().toString() + buffer[2].toChar().toString() + buffer[1].toChar().toString() + buffer[0].toChar().toString() + return buffer[3].toInt().toChar().toString() + buffer[2].toInt().toChar().toString() + buffer[1].toInt() + .toChar().toString() + buffer[0].toInt().toChar().toString() } if (size == 8) { val llsb = inValue.toLong() @@ -535,8 +537,10 @@ fun encodeBinary(inValue: BigDecimal, size: Int): String { buffer[6] = ((llsb shr 8) and 0x0000FFFF).toByte() buffer[7] = (llsb and 0x0000FFFF).toByte() - return buffer[7].toChar().toString() + buffer[6].toChar().toString() + buffer[5].toChar().toString() + buffer[4].toChar().toString() + - buffer[3].toChar().toString() + buffer[2].toChar().toString() + buffer[1].toChar().toString() + buffer[0].toChar().toString() + return buffer[7].toInt().toChar().toString() + buffer[6].toInt().toChar().toString() + buffer[5].toInt() + .toChar().toString() + buffer[4].toInt().toChar().toString() + + buffer[3].toInt().toChar().toString() + buffer[2].toInt().toChar().toString() + buffer[1].toInt().toChar().toString() + buffer[0].toInt() + .toChar().toString() } TODO("encode binary for $size not implemented") } @@ -552,39 +556,39 @@ fun encodeUnsigned(inValue: BigDecimal, size: Int): String { fun decodeBinary(value: String, size: Int): BigDecimal { if (size == 1) { var number: Long = 0x0000000 - if (value[0].toInt() and 0x0010 != 0) { + if (value[0].code and 0x0010 != 0) { number = 0x00000000 } - number += (value[0].toInt() and 0x00FF) + number += (value[0].code and 0x00FF) return BigDecimal(number.toInt().toString()) } if (size == 2) { var number: Long = 0x0000000 - if (value[1].toInt() and 0x8000 != 0) { + if (value[1].code and 0x8000 != 0) { number = 0xFFFF0000 } - number += (value[0].toInt() and 0x00FF) + ((value[1].toInt() and 0x00FF) shl 8) + number += (value[0].code and 0x00FF) + ((value[1].code and 0x00FF) shl 8) return BigDecimal(number.toInt().toString()) } if (size == 4) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) return BigDecimal(number.toInt().toString()) } if (size == 8) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + - ((value[4].toLong() and 0x00FF) shl 32) + - ((value[5].toLong() and 0x00FF) shl 40) + - ((value[6].toLong() and 0x00FF) shl 48) + - ((value[7].toLong() and 0x00FF) shl 56) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) + + ((value[4].code.toLong() and 0x00FF) shl 32) + + ((value[5].code.toLong() and 0x00FF) shl 40) + + ((value[6].code.toLong() and 0x00FF) shl 48) + + ((value[7].code.toLong() and 0x00FF) shl 56) return BigDecimal(number.toInt().toString()) } @@ -594,35 +598,35 @@ fun decodeBinary(value: String, size: Int): BigDecimal { fun decodeInteger(value: String, size: Int): BigDecimal { if (size == 1) { var number = 0x0000000 - number += (value[0].toByte()) + number += (value[0].code.toByte()) return BigDecimal(number.toString()) } if (size == 2) { var number: Long = 0x0000000 - if (value[1].toInt() and 0x8000 != 0) { + if (value[1].code and 0x8000 != 0) { number = 0xFFFF0000 } - number += (value[0].toInt() and 0x00FF) + ((value[1].toInt() and 0x00FF) shl 8) + number += (value[0].code and 0x00FF) + ((value[1].code and 0x00FF) shl 8) return BigDecimal(number.toInt().toString()) } if (size == 4) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) return BigDecimal(number.toInt().toString()) } if (size == 8) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + - ((value[4].toLong() and 0x00FF) shl 32) + - ((value[5].toLong() and 0x00FF) shl 40) + - ((value[6].toLong() and 0x00FF) shl 48) + - ((value[7].toLong() and 0x00FF) shl 56) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) + + ((value[4].code.toLong() and 0x00FF) shl 32) + + ((value[5].code.toLong() and 0x00FF) shl 40) + + ((value[6].code.toLong() and 0x00FF) shl 48) + + ((value[7].code.toLong() and 0x00FF) shl 56) return BigDecimal(number.toString()) } @@ -633,40 +637,40 @@ fun decodeUnsigned(value: String, size: Int): BigDecimal { if (size == 1) { var number: Long = 0x0000000 - if (value[0].toInt() and 0x0010 != 0) { + if (value[0].code and 0x0010 != 0) { number = 0x00000000 } - number += (value[0].toInt() and 0x00FF) + number += (value[0].code and 0x00FF) return BigDecimal(number.toInt().toString()) } if (size == 2) { var number: Long = 0x0000000 - if (value[1].toInt() and 0x1000 != 0) { + if (value[1].code and 0x1000 != 0) { number = 0xFFFF0000 } - number += (value[0].toInt() and 0x00FF) + ((value[1].toInt() and 0x00FF) shl 8) + number += (value[0].code and 0x00FF) + ((value[1].code and 0x00FF) shl 8) // make sure you count onlu 16 bits number = number and 0x0000FFFF return BigDecimal(number.toString()) } if (size == 4) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) return BigDecimal(number.toString()) } if (size == 8) { - val number = (value[0].toLong() and 0x00FF) + - ((value[1].toLong() and 0x00FF) shl 8) + - ((value[2].toLong() and 0x00FF) shl 16) + - ((value[3].toLong() and 0x00FF) shl 24) + - ((value[4].toLong() and 0x00FF) shl 32) + - ((value[5].toLong() and 0x00FF) shl 40) + - ((value[6].toLong() and 0x00FF) shl 48) + - ((value[7].toLong() and 0x00FF) shl 56) + val number = (value[0].code.toLong() and 0x00FF) + + ((value[1].code.toLong() and 0x00FF) shl 8) + + ((value[2].code.toLong() and 0x00FF) shl 16) + + ((value[3].code.toLong() and 0x00FF) shl 24) + + ((value[4].code.toLong() and 0x00FF) shl 32) + + ((value[5].code.toLong() and 0x00FF) shl 40) + + ((value[6].code.toLong() and 0x00FF) shl 48) + + ((value[7].code.toLong() and 0x00FF) shl 56) return BigDecimal(number.toInt().toString()) } @@ -685,7 +689,7 @@ fun encodeToZoned(inValue: BigDecimal, digits: Int, scale: Int): String { val sign = inValue.signum() inChars.forEachIndexed { index, char -> - val digit = char.toInt() + val digit = char.code buffer[index] = digit } if (sign < 0) { @@ -708,11 +712,11 @@ fun decodeFromZoned(value: String, digits: Int, scale: Int): BigDecimal { when { it.isDigit() -> builder.append(it) else -> { - if (it.toInt() == 0) { + if (it.code == 0) { builder.append('0') } else { builder.insert(0, '-') - builder.append((it.toInt() - 0x0049 + 0x0030).toChar()) + builder.append((it.code - 0x0049 + 0x0030).toChar()) } } } @@ -741,8 +745,8 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { // place all the digits except last one while (inPosition < inChars.size - 1) { - firstNibble = ((inChars[inPosition++].toInt()) and 0x000F) shl 4 - secondNibble = (inChars[inPosition++].toInt()) and 0x000F + firstNibble = ((inChars[inPosition++].code) and 0x000F) shl 4 + secondNibble = (inChars[inPosition++].code) and 0x000F buffer[offset++] = (firstNibble + secondNibble) } @@ -750,7 +754,7 @@ fun encodeToDS(inValue: BigDecimal, digits: Int, scale: Int): String { firstNibble = if (inPosition == inChars.size) { 0x00F0 } else { - (inChars[inChars.size - 1].toInt()) and 0x000F shl 4 + (inChars[inChars.size - 1].code) and 0x000F shl 4 } if (sign != -1) { buffer[offset] = (firstNibble + 0x000F)