diff --git a/kotlin/README.md b/kotlin/README.md new file mode 100644 index 0000000..bb6138e --- /dev/null +++ b/kotlin/README.md @@ -0,0 +1,9 @@ +# Quick compilation and testing instructions +```bash +$ kotlinc "./src/com/here/flexiblepolyline/FlexiblePolyline.kt" "./src/com/here/flexiblepolyline/FlexiblePolylineTest.kt" -include-runtime -d FlexiblePolylineText.jar +``` +to run the performance test with the default polyline length of 1000 vertices, or +```bash +$ java -jar FlexiblePolylineText.jar $POLYLINE_LENGTH +``` +to use `$POLYLINE_LENGTH` vertices for the performance test. \ No newline at end of file diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt new file mode 100644 index 0000000..5ee90e3 --- /dev/null +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolyline.kt @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2019 HERE Europe B.V. + * Licensed under MIT, see full license in LICENSE + * SPDX-License-Identifier: MIT + * License-Filename: LICENSE + */ +package com.here.flexiblepolyline + +import kotlin.math.abs +import kotlin.math.roundToLong +import kotlin.math.sign +import kotlin.math.pow + +/** + * The polyline encoding is a lossy compressed representation of a list of coordinate pairs or coordinate triples. + * It achieves that by: + * + * 1. Reducing the decimal digits of each value. + * 1. Encoding only the offset from the previous point. + * 1. Using variable length for each coordinate delta. + * 1. Using 64 URL-safe characters to display the result. + * + * The advantage of this encoding are the following: + * - Output string is composed by only URL-safe characters + * - Floating point precision is configurable + * - It allows to encode a 3rd dimension with a given precision, which may be a level, altitude, elevation or some other custom value + */ +object FlexiblePolyline { + const val VERSION = 1L + + // Base64 URL-safe characters + private val ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray() + private val DECODING_TABLE = intArrayOf( + 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + ) + + /** + * Encode the list of coordinate triples. + * + * The third dimension value will be eligible for encoding only when ThirdDimension is other than ABSENT. + * This is lossy compression based on precision accuracy. + * + * @param coordinates [List] of coordinate triples that to be encoded. + * @param precision Floating point precision of the coordinate to be encoded. + * @param thirdDimension [ThirdDimension] which may be a level, altitude, elevation or some other custom value + * @param thirdDimPrecision Floating point precision for thirdDimension value + * @return URL-safe encoded [String] for the given coordinates. + */ + @JvmStatic + fun encode(coordinates: List?, precision: Int, thirdDimension: ThirdDimension?, thirdDimPrecision: Int): String { + require(!coordinates.isNullOrEmpty()) { "Invalid coordinates!" } + requireNotNull(thirdDimension) { "Invalid thirdDimension" } + val enc = Encoder(precision, thirdDimension, thirdDimPrecision) + coordinates.iterator().forEach { + enc.add(it) + } + return enc.getEncoded() + } + + /** + * Decode the encoded input [String] to [List] of coordinate triples. + * + * @param encoded URL-safe encoded [String] + * @return [List] of coordinate triples that are decoded from input + * + * @see getThirdDimension + * @see LatLngZ + */ + @JvmStatic + fun decode(encoded: String?): List { + require(!encoded.isNullOrBlank()) { "Invalid argument!" } + val result: MutableList = ArrayList() + val dec = Decoder(encoded) + dec.iterator().forEach { + result.add(it) + } + return result + } + + /** + * ThirdDimension type from the encoded input [String] + * + * @param encoded URL-safe encoded coordinate triples [String] + * @return type of [ThirdDimension] + */ + @JvmStatic + fun getThirdDimension(encoded: String): ThirdDimension? { + return Decoder(encoded).thirdDimension + } + + // Decode a single char to the corresponding value + private fun decodeChar(charValue: Char): Int { + val pos = charValue.code - 45 + return if (pos < 0 || pos > 77) { + -1 + } else DECODING_TABLE[pos] + } + + // Single instance for configuration, validation and encoding for an input request. + private class Encoder(precision: Int, private val thirdDimension: ThirdDimension, thirdDimPrecision: Int) { + private val result: StringBuilder = StringBuilder() + private val latConverter: Converter = Converter(precision) + private val lngConverter: Converter = Converter(precision) + private val zConverter: Converter = Converter(thirdDimPrecision) + + init { + encodeHeader(precision, this.thirdDimension.num, thirdDimPrecision) + } + + private fun encodeHeader(precision: Int, thirdDimensionValue: Int, thirdDimPrecision: Int) { + // Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char + require(!(precision < 0 || precision > 15)) { "precision out of range" } + require(!(thirdDimPrecision < 0 || thirdDimPrecision > 15)) { "thirdDimPrecision out of range" } + require(!(thirdDimensionValue < 0 || thirdDimensionValue > 7)) { "thirdDimensionValue out of range" } + val res = ((thirdDimPrecision shl 7) or (thirdDimensionValue shl 4) or precision).toLong() + Converter.encodeUnsignedVarInt(VERSION, result) + Converter.encodeUnsignedVarInt(res, result) + } + + private fun add(lat: Double, lng: Double) { + latConverter.encodeValue(lat, result) + lngConverter.encodeValue(lng, result) + } + + private fun add(lat: Double, lng: Double, z: Double) { + add(lat, lng) + if (thirdDimension != ThirdDimension.ABSENT) { + zConverter.encodeValue(z, result) + } + } + + fun add(tuple: LatLngZ?) { + requireNotNull(tuple) { "Invalid LatLngZ tuple" } + add(tuple.lat, tuple.lng, tuple.z) + } + + fun getEncoded(): String { + return result.toString() + } + } + + // Single instance for decoding an input request. + private class Decoder(encoded: String) : Iterator { + private val encoded: CharIterator = encoded.iterator() + private val latConverter: Converter + private val lngConverter: Converter + private val zConverter: Converter + var thirdDimension: ThirdDimension? = null + + init { + val header = decodeHeader() + val precision = header and 0x0f + thirdDimension = ThirdDimension.fromNum(((header shr 4) and 0x07).toLong()) + val thirdDimPrecision = ((header shr 7) and 0x0f) + latConverter = Converter(precision) + lngConverter = Converter(precision) + zConverter = Converter(thirdDimPrecision) + } + + private fun hasThirdDimension(): Boolean { + return thirdDimension != ThirdDimension.ABSENT + } + + private fun decodeHeader(): Int { + val version = Converter.decodeUnsignedVarInt(encoded) + require(version == VERSION) { "Invalid format version :: encoded.$version vs FlexiblePolyline.$VERSION" } + // Decode the polyline header + return Converter.decodeUnsignedVarInt(encoded).toInt() + } + + override fun next(): LatLngZ { + val lat = latConverter.decodeValue(encoded) + val lng = lngConverter.decodeValue(encoded) + + if (hasThirdDimension()) { + val z = zConverter.decodeValue(encoded) + return LatLngZ(lat, lng, z) + } + return LatLngZ(lat, lng) + } + + override fun hasNext(): Boolean { + return encoded.hasNext() + } + } + + /** + * Stateful instance for encoding and decoding on a sequence of Coordinates part of an request. + * Instance should be specific to type of coordinates (e.g. Lat, Lng) + * so that specific type delta is computed for encoding. + * Lat0 Lng0 3rd0 (Lat1-Lat0) (Lng1-Lng0) (3rdDim1-3rdDim0) + * + * @param precision [Int] + */ + class Converter(precision: Int) { + private val multiplier = (10.0.pow(precision.toDouble())).toLong() + private var lastValue: Long = 0 + + fun encodeValue(value: Double, result: StringBuilder) { + /* + * Round-half-up + * round(-1.4) --> -1 + * round(-1.5) --> -2 + * round(-2.5) --> -3 + */ + val scaledValue = abs(value * multiplier).roundToLong() * sign(value).roundToLong() + var delta = scaledValue - lastValue + val negative = delta < 0 + lastValue = scaledValue + + // make room on lowest bit + delta = delta shl 1 + + // invert bits if the value is negative + if (negative) { + delta = delta.inv() + } + encodeUnsignedVarInt(delta, result) + } + + // Decode single coordinate (say lat|lng|z) starting at index + fun decodeValue(encoded: CharIterator): Double { + var l = decodeUnsignedVarInt(encoded) + if ((l and 1L) != 0L) { + l = l.inv() + } + l = l shr 1 + lastValue += l + + return lastValue.toDouble() / multiplier + } + + companion object { + fun encodeUnsignedVarInt(value: Long, result: StringBuilder) { + var number = value + while (number > 0x1F) { + val pos = (number and 0x1F or 0x20).toByte() + result.append(ENCODING_TABLE[pos.toInt()]) + number = number shr 5 + } + result.append(ENCODING_TABLE[number.toByte().toInt()]) + } + + fun decodeUnsignedVarInt(encoded: CharIterator): Long { + var shift: Short = 0 + var result: Long = 0 + + encoded.withIndex().forEach { + val value = decodeChar(it.value).toLong() + if (value < 0) { + throw IllegalArgumentException("Unexpected value found :: '${it.value}' at ${it.index}") + } + result = result or ((value and 0x1FL) shl shift.toInt()) + if ((value and 0x20L) == 0L) { + return result + } else { + shift = (shift + 5).toShort() + } + } + return result + } + } + } + + /** + * 3rd dimension specification. + * Example a level, altitude, elevation or some other custom value. + * ABSENT is default when there is no third dimension en/decoding required. + */ + enum class ThirdDimension(val num: Int) { + ABSENT(0), LEVEL(1), ALTITUDE(2), ELEVATION(3), RESERVED1(4), RESERVED2(5), CUSTOM1(6), CUSTOM2(7); + + companion object { + fun fromNum(value: Long): ThirdDimension? { + for (dim in entries) { + if (dim.num.toLong() == value) { + return dim + } + } + return null + } + } + } + + /** + * Coordinate triple + */ + data class LatLngZ( + val lat: Double, + val lng: Double, + val z: Double = 0.0 + ){} +} \ No newline at end of file diff --git a/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt new file mode 100644 index 0000000..e2f23b0 --- /dev/null +++ b/kotlin/src/com/here/flexiblepolyline/FlexiblePolylineTest.kt @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2019 HERE Europe B.V. + * Licensed under MIT, see full license in LICENSE + * SPDX-License-Identifier: MIT + * License-Filename: LICENSE + */ +import com.here.flexiblepolyline.FlexiblePolyline +import com.here.flexiblepolyline.FlexiblePolyline.Converter +import com.here.flexiblepolyline.FlexiblePolyline.ThirdDimension +import com.here.flexiblepolyline.FlexiblePolyline.LatLngZ +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* + +/** + * Validate polyline encoding with different input combinations. + */ +class FlexiblePolylineTest { + private fun testInvalidCoordinates() { + + // Null coordinates + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.encode(null, 5, ThirdDimension.ABSENT, 0) } + + + // Empty coordinates list test + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.encode(ArrayList(), 5, ThirdDimension.ABSENT, 0) } + } + + private fun testInvalidThirdDimension() { + val pairs: MutableList = ArrayList() + pairs.add(LatLngZ(50.1022829, 8.6982122)) + val invalid: ThirdDimension? = null + + // Invalid Third Dimension + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.encode(pairs, 5, invalid, 0) } + } + + private fun testConvertValue() { + val conv: Converter = Converter(5) + val result = StringBuilder() + conv.encodeValue(-179.98321, result) + assertEquals(result.toString(), "h_wqiB") + } + + private fun testSimpleLatLngEncoding() { + val pairs: MutableList = ArrayList() + pairs.add(LatLngZ(50.1022829, 8.6982122)) + pairs.add(LatLngZ(50.1020076, 8.6956695)) + pairs.add(LatLngZ(50.1006313, 8.6914960)) + pairs.add(LatLngZ(50.0987800, 8.6875156)) + val expected = "BFoz5xJ67i1B1B7PzIhaxL7Y" + val computed: String = FlexiblePolyline.encode(pairs, 5, ThirdDimension.ABSENT, 0) + assertEquals(computed, expected) + } + + private fun testComplexLatLngEncoding() { + val pairs: MutableList = ArrayList() + pairs.add(LatLngZ(52.5199356, 13.3866272)) + pairs.add(LatLngZ(52.5100899, 13.2816896)) + pairs.add(LatLngZ(52.4351807, 13.1935196)) + pairs.add(LatLngZ(52.4107285, 13.1964502)) + pairs.add(LatLngZ(52.38871, 13.1557798)) + pairs.add(LatLngZ(52.3727798, 13.1491003)) + pairs.add(LatLngZ(52.3737488, 13.1154604)) + pairs.add(LatLngZ(52.3875198, 13.0872202)) + pairs.add(LatLngZ(52.4029388, 13.0706196)) + pairs.add(LatLngZ(52.4105797, 13.0755529)) + val expected = "BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e" + val computed: String = FlexiblePolyline.encode(pairs, 5, ThirdDimension.ABSENT, 0) + assertEquals(computed, expected) + } + + private fun testLatLngZEncode() { + val tuples: MutableList = ArrayList() + tuples.add(LatLngZ(50.1022829, 8.6982122, 10.0)) + tuples.add(LatLngZ(50.1020076, 8.6956695, 20.0)) + tuples.add(LatLngZ(50.1006313, 8.6914960, 30.0)) + tuples.add(LatLngZ(50.0987800, 8.6875156, 40.0)) + val expected = "BlBoz5xJ67i1BU1B7PUzIhaUxL7YU" + val computed: String = FlexiblePolyline.encode(tuples, 5, ThirdDimension.ALTITUDE, 0) + assertEquals(computed, expected) + } + + /** + * Decoder test starts + */ + private fun testInvalidEncoderInput() { + + // Null coordinates + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.decode(null) } + + + // Empty coordinates list test + assertThrows( + IllegalArgumentException::class.java + ) { FlexiblePolyline.decode("") } + } + + private fun testThirdDimension() { + assertTrue(FlexiblePolyline.getThirdDimension("BFoz5xJ67i1BU") === ThirdDimension.ABSENT) + assertTrue(FlexiblePolyline.getThirdDimension("BVoz5xJ67i1BU") === ThirdDimension.LEVEL) + assertTrue(FlexiblePolyline.getThirdDimension("BlBoz5xJ67i1BU") === ThirdDimension.ALTITUDE) + assertTrue(FlexiblePolyline.getThirdDimension("B1Boz5xJ67i1BU") === ThirdDimension.ELEVATION) + } + + private fun testDecodeConvertValue() { + val encoded = ("h_wqiB").iterator() + val expected = -179.98321 + val conv = Converter(5) + val computed = conv.decodeValue(encoded) + assertEquals(computed, expected) + } + + private fun testSimpleLatLngDecoding() { + val computed: List = FlexiblePolyline.decode("BFoz5xJ67i1B1B7PzIhaxL7Y") + val expected: MutableList = ArrayList() + expected.add(LatLngZ(50.10228, 8.69821)) + expected.add(LatLngZ(50.10201, 8.69567)) + expected.add(LatLngZ(50.10063, 8.69150)) + expected.add(LatLngZ(50.09878, 8.68752)) + assertEquals(computed.size, expected.size) + for (i in computed.indices) { + assertEquals(computed[i], expected[i]) + } + } + + private fun testComplexLatLngDecoding() { + val computed: List = FlexiblePolyline.decode("BF05xgKuy2xCx9B7vUl0OhnR54EqSzpEl-HxjD3pBiGnyGi2CvwFsgD3nD4vB6e") + val pairs: MutableList = ArrayList() + pairs.add(LatLngZ(52.51994, 13.38663)) + pairs.add(LatLngZ(52.51009, 13.28169)) + pairs.add(LatLngZ(52.43518, 13.19352)) + pairs.add(LatLngZ(52.41073, 13.19645)) + pairs.add(LatLngZ(52.38871, 13.15578)) + pairs.add(LatLngZ(52.37278, 13.14910)) + pairs.add(LatLngZ(52.37375, 13.11546)) + pairs.add(LatLngZ(52.38752, 13.08722)) + pairs.add(LatLngZ(52.40294, 13.07062)) + pairs.add(LatLngZ(52.41058, 13.07555)) + assertEquals(computed.size, pairs.size) + for (i in computed.indices) { + assertEquals(computed[i], pairs[i]) + } + } + + private fun testLatLngZDecode() { + val computed: List = FlexiblePolyline.decode("BlBoz5xJ67i1BU1B7PUzIhaUxL7YU") + val tuples: MutableList = ArrayList() + tuples.add(LatLngZ(50.10228, 8.69821, 10.0)) + tuples.add(LatLngZ(50.10201, 8.69567, 20.0)) + tuples.add(LatLngZ(50.10063, 8.69150, 30.0)) + tuples.add(LatLngZ(50.09878, 8.68752, 40.0)) + assertEquals(computed.size, tuples.size) + for (i in computed.indices) { + assertEquals(computed[i], tuples[i]) + } + } + + private fun encodingSmokeTest() { + TestCaseReader("original.txt", "round_half_up/encoded.txt").testCases.forEach { + val original = parseTestDataFromLine(it.testInput); + val encodedComputed: String = FlexiblePolyline.encode(original.latLngZs, original.precision, original.thirdDimension, original.thirdDimensionPrecision) + + assertEquals(encodedComputed, it.testResult) + } + } + + private fun decodingSmokeTest() { + TestCaseReader("round_half_up/encoded.txt", "round_half_up/decoded.txt").testCases.forEach { + val expected = parseTestDataFromLine(it.testResult); + + // Validate thirdDimension + val computedDimension: ThirdDimension = FlexiblePolyline.getThirdDimension(it.testInput)!! + assertEquals(computedDimension, expected.thirdDimension) + + // Validate LatLngZ + val computedLatLngZs: List = FlexiblePolyline.decode(it.testInput) + + assertEquals(computedLatLngZs.size, expected.latLngZs?.size) + expected.latLngZs?.let { + for (i in computedLatLngZs.indices) { + assertEquals(computedLatLngZs[i], expected.latLngZs[i]) + } + } ?: throw Exception("Error parsing expected results") + } + } + + private fun testVeryLongLine(lineLength: Int) { + val PRECISION = 10 + val random = Random() + val coordinates: MutableList = ArrayList() + for (i in 0..lineLength) { + val nextPoint = LatLngZ(random.nextDouble(), random.nextDouble(), random.nextDouble()) + coordinates.add(nextPoint) + } + val encoded: String = FlexiblePolyline.encode(coordinates, PRECISION, ThirdDimension.ALTITUDE, PRECISION) + val startTime = System.nanoTime() + val decoded: List = FlexiblePolyline.decode(encoded) + val duration = System.nanoTime() - startTime + println("duration: " + duration / 1000 + "us") + println("FlexiblePolyline.decoded total number of LatLngZ: " + decoded.size) + } + + companion object { + const val TEST_FILES_RELATIVE_PATH = "../test/" + + /* + * Helper for parsing DecodeLines file + * Line Format: {(precision, thirdDimPrecision?, thirdDim?); [(c1Lat, c1Lng, c1Alt), ]} + */ + private fun parseTestDataFromLine(line: String): TestData { + var precision = 0 + var thirdDimensionPrecision = 0 + var hasThirdDimension = false + var thirdDimension: ThirdDimension? = FlexiblePolyline.ThirdDimension.ABSENT + + // .substring gets rid of { and } + val splitBySemicolon = line.substring(1, line.length - 1).split(";").toTypedArray(); + val leftPart = splitBySemicolon[0]; + val meta = leftPart.split(",").toTypedArray(); + precision = Integer.valueOf(meta[0]) + if (meta.size > 1) { + thirdDimension = ThirdDimension.fromNum(Integer.valueOf(meta[2].trim()).toLong()) + hasThirdDimension = true + thirdDimensionPrecision = Integer.valueOf(meta[1].trim()) + } + + val latLngZs = extractLatLngZ(splitBySemicolon[1], hasThirdDimension) + + return TestData( + precision = precision, + thirdDimensionPrecision = thirdDimensionPrecision, + thirdDimension = thirdDimension, + latLngZs = latLngZs + ) + } + + private fun extractLatLngZ(line: String, hasThirdDimension: Boolean): List { + val latLngZs: MutableList = ArrayList() + val coordinates = line.trim().substring(1, line.trim().length - 1).split(",").toTypedArray() + var itr = 0 + while (itr < coordinates.size && coordinates[itr].isNotBlank()) { + val lat = coordinates[itr++].trim().replace("(", "").toDouble() + val lng = coordinates[itr++].trim().replace(")", "").toDouble() + var z = 0.0 + if (hasThirdDimension) { + z = coordinates[itr++].trim().replace(")", "").toDouble() + } + latLngZs.add(FlexiblePolyline.LatLngZ(lat, lng, z)) + } + return latLngZs + } + + private fun assertEquals(lhs: Any, rhs: Any?) { + if (lhs !== rhs) { + if (lhs != rhs) { + throw RuntimeException("Assert failed, $lhs != $rhs") + } + } + } + + private fun assertTrue(value: Boolean) { + if (!value) { + throw RuntimeException("Assert failed") + } + } + + private fun assertThrows(expectedType: Class, runnable: Runnable) { + try { + runnable.run() + } catch (actualException: Throwable) { + if (!expectedType.isInstance(actualException)) { + println("Working Directory = " + actualException.javaClass.name + " "+ actualException); + + throw RuntimeException("Assert failed, Invalid exception found!") + } + return + } + throw RuntimeException("Assert failed, No exception found!") + } + + @JvmStatic + fun main(args: Array) { + println("Working Directory = " + System.getProperty("user.dir")); + + val DEFAULT_LINE_LENGTH = 1000 + var lineLength = DEFAULT_LINE_LENGTH + if (args.size > 0) { + lineLength = args[0].toInt() + } + val test = FlexiblePolylineTest() + test.testInvalidCoordinates() + test.testInvalidThirdDimension() + test.testConvertValue() + test.testSimpleLatLngEncoding() + test.testComplexLatLngEncoding() + test.testLatLngZEncode() + test.encodingSmokeTest() + + // Decode test + test.testInvalidEncoderInput() + test.testThirdDimension() + test.testDecodeConvertValue() + test.testSimpleLatLngDecoding() + test.testComplexLatLngDecoding() + test.testLatLngZDecode() + test.decodingSmokeTest() + test.testVeryLongLine(lineLength) + } + } +} + +private data class TestData( + val precision: Int = 0, + val thirdDimensionPrecision: Int = 0, + val thirdDimension: ThirdDimension? = null, + val latLngZs: List? = null +){} + +private data class TestCase( + val testInput: String, + val testResult: String +){} + +private class TestCaseReader(testInputFile: String, testResultFile: String) { + val testCases: List + + init { + var totalLines = 0 + val testCaseList = mutableListOf() + try { + Files.newBufferedReader(Paths.get(FlexiblePolylineTest.TEST_FILES_RELATIVE_PATH + testInputFile)).use { input -> + Files.newBufferedReader(Paths.get(FlexiblePolylineTest.TEST_FILES_RELATIVE_PATH + testResultFile)).use { result -> + // read line by line and validate the test + while (true) { + val regex = "\\s|\\(|\\)".toRegex() + val testInputFileLine = input.readLine(); + val testResultFileLine = result.readLine(); + + if (testInputFileLine != null && testInputFileLine.isNotBlank() && testResultFileLine != null && testResultFileLine.isNotBlank()) { + testCaseList.add( + TestCase( + testInput = testInputFileLine.replace(regex, ""), + testResult = testResultFileLine.replace(regex, "") + ) + ) + totalLines++ + } else { + if(totalLines==0) { + System.err.format("TestCaseReader - 0 test cases in file $testInputFile or $testResultFile:") + } + break + } + } + } + } + } catch (e: Exception) { + e.printStackTrace() + System.err.format("TestCaseReader - exception reading test case $testInputFile and $testResultFile at LineNo: $totalLines: %s%n", e) + throw RuntimeException("TestCaseReader - exception reading test case $testInputFile and $testResultFile at LineNo: $totalLines", e) + } + testCases = testCaseList.toList() + } +} \ No newline at end of file