Skip to content
47 changes: 46 additions & 1 deletion src/main/java/edu/ie3/util/StringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

/** Some useful functions to manipulate Strings */
public class StringUtils {

private StringUtils() {
throw new IllegalStateException("Utility classes cannot be instantiated.");
}
Expand Down Expand Up @@ -80,7 +81,7 @@ public static String[] camelCaseToSnakeCase(String[] input) {
* @return Quoted String
*/
public static String quote(String input) {
return input.replaceAll("^([^\"])", "\"$1").replaceAll("([^\"])$", "$1\"");
return input.matches("^\".*\"$") ? input : "\"" + input + "\"";
}

/**
Expand All @@ -102,4 +103,48 @@ public static String[] quote(String[] input) {
public static String cleanString(String input) {
return input.replaceAll("[^\\w]", "_");
}

/**
* Quotes a given string that contains special characters to comply with the csv specification RFC
* 4180 (https://tools.ietf.org/html/rfc4180). Double quotes are escaped according to
* specification.
*
* @param inputString string that should be converted to a valid rfc 4180 string
* @param csvSep separator of the csv file
* @return a csv string that is valid according to rfc 4180
*/
public static String csvString(String inputString, String csvSep) {
if (needsCsvRFC4180Quote(inputString, csvSep)) {
/* Get rid of first and last quotation if there is some. */
String inputUnquoted = unquoteStartEnd(inputString);
/* Escape every double quotation mark within the String by doubling it */
String withEscapedQuotes = inputUnquoted.replaceAll("\"", "\"\"");
/* finally add quotes to the strings start and end again */
return quote(withEscapedQuotes);
} else return inputString;
}

/**
* Removes double quotes at start and end position of the provided string, if any
*
* @param input string that should be unquoted
* @return copy of the provided string without start and end double quotes
*/
public static String unquoteStartEnd(String input) {
return input.matches("^\".*\"$") ? input.substring(1, input.length() - 1) : input;
}

/**
* Check if the provided string needs to be quoted according to the csv specification RFC 4180
*
* @param inputString the string that should be checked
* @param csvSep separator of the csv file
* @return true of the string needs to be quoted, false otherwise
*/
private static boolean needsCsvRFC4180Quote(String inputString, String csvSep) {
return inputString.contains(csvSep)
|| inputString.contains(",")
|| inputString.contains("\"")
|| inputString.contains("\n");
}
}
116 changes: 111 additions & 5 deletions src/test/groovy/edu/ie3/util/StringUtilsTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package edu.ie3.util

import spock.lang.Specification

import java.util.stream.Collectors

class StringUtilsTest extends Specification {

def "The StringUtils quote a single String correctly"() {
Expand All @@ -17,11 +19,14 @@ class StringUtilsTest extends Specification {
actual == expected

where:
input || expected
"test" || "\"test\""
"\"test" || "\"test\""
"test\"" || "\"test\""
"\"test\"" || "\"test\""
input || expected
"test" || "\"test\""
"\"test" || "\"\"test\""
"test\"" || "\"test\"\""
"\"test\"" || "\"test\""
"\"This\" is a test" || "\"\"This\" is a test\""
"This is \"a\" test" || "\"This is \"a\" test\""
"This is a \"test\"" || "\"This is a \"test\"\""
}

def "The StringUtils are able to quote each element of an array of Strings"() {
Expand Down Expand Up @@ -211,4 +216,105 @@ class StringUtilsTest extends Specification {
"?ab123" || "_ab123"
"ßab123" || "_ab123"
}

def "The StringUtils converts a given Array of csv header elements to match the csv specification RFC 4180 "() {
given:
def input = [
"4ca90220-74c2-4369-9afa-a18bf068840d",
"{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}",
"node_a",
"2020-03-25T15:11:31Z[UTC] \n 2020-03-24T15:11:31Z[UTC]",
"8f9682df-0744-4b58-a122-f0dc730f6510",
"true",
"1,0",
"1.0",
"Höchstspannung",
"380.0",
"olm:{(0.00,1.00)}",
"cosPhiP:{(0.0,1.0),(0.9,1.0),(1.2,-0.3)}"
]
def expected = [
"4ca90220-74c2-4369-9afa-a18bf068840d",
"\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528],\"\"crs\"\":{\"\"type\"\":\"\"name\"\",\"\"properties\"\":{\"\"name\"\":\"\"EPSG:4326\"\"}}}\"",
"node_a",
"\"2020-03-25T15:11:31Z[UTC] \n 2020-03-24T15:11:31Z[UTC]\"",
"8f9682df-0744-4b58-a122-f0dc730f6510",
"true",
"\"1,0\"",
"1.0",
"Höchstspannung",
"380.0",
"\"olm:{(0.00,1.00)}\"",
"\"cosPhiP:{(0.0,1.0),(0.9,1.0),(1.2,-0.3)}\""] as Set

when:
def actual = input.stream().map({ inputElement -> StringUtils.csvString(inputElement, ",") }).collect(Collectors.toSet()) as Set

then:
actual == expected
}

def "The StringUtils converts a given LinkedHashMap of csv data to match the csv specification RFC 4180 "() {
given:
def input = [
"activePowerGradient": "25.0",
"capex" : "100,0",
"cosphiRated" : "0.95",
"etaConv" : "98.0",
"id" : "test \n bmTypeInput",
"opex" : "50.0",
"sRated" : "25.0",
"uu,id" : "5ebd8f7e-dedb-4017-bb86-6373c4b68eb8",
"geoPosition" : "{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}",
"olm\"characteristic": "olm:{(0.0,1.0)}",
"cosPhiFixed" : "cosPhiFixed:{(0.0,1.0)}"
] as LinkedHashMap<String, String>

def expected = [
"activePowerGradient" : "25.0",
"capex" : "\"100,0\"",
"cosphiRated" : "0.95",
"etaConv" : "98.0",
"id" : "\"test \n bmTypeInput\"",
"opex" : "50.0",
"sRated" : "25.0",
"\"uu,id\"" : "5ebd8f7e-dedb-4017-bb86-6373c4b68eb8",
"geoPosition" : "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528],\"\"crs\"\":{\"\"type\"\":\"\"name\"\",\"\"properties\"\":{\"\"name\"\":\"\"EPSG:4326\"\"}}}\"",
"\"olm\"\"characteristic\"": "\"olm:{(0.0,1.0)}\"",
"cosPhiFixed" : "\"cosPhiFixed:{(0.0,1.0)}\""
] as LinkedHashMap<String, String>

when:
def actualList = input.entrySet().stream().map({ mapEntry ->
return new AbstractMap.SimpleEntry<String, String>(StringUtils.csvString(mapEntry.key, ","), StringUtils.csvString(mapEntry.value, ","))
}) as Set

def actual = actualList.collectEntries {
[it.key, it.value]
}

then:
actual == expected
}

def "The StringUtils converts a given String to match the csv specification RFC 4180 "() {
expect:
StringUtils.csvString(inputString, csvSep) == expect

where:
inputString | csvSep || expect
"activePowerGradient" | "," || "activePowerGradient"
"\"100,0\"" | "," || "\"100,0\""
"100,0" | "," || "\"100,0\""
"100,0" | ";" || "\"100,0\""
"100;0" | ";" || "\"100;0\""
"\"100;0\"" | ";" || "\"100;0\""
"100;0" | "," || "100;0"
"olm:{(0.00,1.00)}" | "," || "\"olm:{(0.00,1.00)}\""
"olm:{(0.00,1.00)}" | ";" || "\"olm:{(0.00,1.00)}\""
"{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528]}" | "," || "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528]}\""
"{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528]}" | ";" || "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528]}\""
"uu,id" | "," || "\"uu,id\""
"uu,id" | ";" || "\"uu,id\""
}
}