diff --git a/alchemist-incarnation-protelis/src/test/resources/testbase.yml b/alchemist-incarnation-protelis/src/test/resources/testbase.yml index 706ab5da8b..126e1cd58f 100644 --- a/alchemist-incarnation-protelis/src/test/resources/testbase.yml +++ b/alchemist-incarnation-protelis/src/test/resources/testbase.yml @@ -3,6 +3,8 @@ export: - type: CSVExporter parameters: fileNameRoot: "test_base" + interval: 1.0 + decimalPlacesCount: 3 data: - time diff --git a/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/exporters/CSVExporter.kt b/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/exporters/CSVExporter.kt index 6599ebbbd4..eb69a209dd 100644 --- a/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/exporters/CSVExporter.kt +++ b/alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/exporters/CSVExporter.kt @@ -37,12 +37,14 @@ import org.slf4j.LoggerFactory * @property exportPath the directory to write exported files (temporary folder is used when omitted) * @property appendTime if true a timestamp is appended to the file name to avoid overwriting * @property fileExtension the extension for the exported files (default: 'csv') + * @property decimalPlacesCount the count of places after decimal */ class CSVExporter> @JvmOverloads constructor( private val fileNameRoot: String = "", val interval: Double = DEFAULT_INTERVAL, + val decimalPlacesCount: Int = 2, val exportPath: String = createTempDirectory("alchemist-export") .absolutePathString() @@ -100,17 +102,21 @@ constructor( val data = extractor.extractDataAsText(environment, reaction, time, step) val names = extractor.columnNames when { - data.size <= 1 -> data.values.joinToString(" ") + data.size <= 1 -> + roundValues(data.values, decimalPlacesCount).joinToString(" ") // Labels and keys match data.size == names.size && data.keys.containsAll(names) -> - names.joinToString(" ") { - requireNotNull(data[it]) { - BugReporting.reportBug( - "Bug in ${this::class.simpleName}", - mapOf("key" to it, "data" to data), - ) - } - } + roundValues( + names.map { name -> + requireNotNull(data[name]) { + BugReporting.reportBug( + "Bug in ${this::class.simpleName}", + mapOf("key" to name, "data" to data), + ) + } + }, + decimalPlacesCount, + ).joinToString(" ") // If the labels do not match keys, require predictable iteration order else -> { require(data.hasPredictableIteration) { @@ -122,7 +128,7 @@ constructor( """.trimIndent(), ) } - data.values.joinToString(" ") + roundValues(data.values, decimalPlacesCount).joinToString(" ") } } } @@ -140,6 +146,15 @@ constructor( } } + private fun roundValues(values: Collection, decimalsNumber: Int): Collection = values.map { raw -> + val number = raw.toDoubleOrNull() + if (number == null || !number.isFinite() || number % 1.0 == 0.0) { + raw + } else { + String.format(Locale.US, "%.${decimalsNumber}f", number) + } + } + private companion object { /** * Character used to separate comments from data on export files. diff --git a/alchemist-loading/src/test/kotlin/it/unibo/alchemist/test/TestCSVExporter.kt b/alchemist-loading/src/test/kotlin/it/unibo/alchemist/test/TestCSVExporter.kt index 9f07baa69d..b39ce25da9 100644 --- a/alchemist-loading/src/test/kotlin/it/unibo/alchemist/test/TestCSVExporter.kt +++ b/alchemist-loading/src/test/kotlin/it/unibo/alchemist/test/TestCSVExporter.kt @@ -57,7 +57,7 @@ class TestCSVExporter> : } "should have limited-length decimals" { val limitedDecimalsFile = simulation.csvExporters()[1].dataFile("fixed-decimals_") - val precision2 = """(0\.0*\d\d|\d\.0*\d|\d\.\d|\d\d)(e(-|\+)\d+)?""" + val precision2 = """(\d\d\.\d\d|0\.0*\d\d|\d\.0*\d|\d\.\d|\d\d)(e(-|\+)\d+)?""" val lineRegex = Regex("""^$precision2(\s($precision2))+$""") limitedDecimalsFile.useLines { lines -> lines