Skip to content

Commit 983bed8

Browse files
authored
Merge pull request #33 from ie3-institute/ds/#141-csv-converter-method
CSV data converter to comply with the CSV specification RFC 4180
2 parents c76ba8c + 75416a1 commit 983bed8

File tree

2 files changed

+157
-6
lines changed

2 files changed

+157
-6
lines changed

src/main/java/edu/ie3/util/StringUtils.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
/** Some useful functions to manipulate Strings */
1111
public class StringUtils {
12+
1213
private StringUtils() {
1314
throw new IllegalStateException("Utility classes cannot be instantiated.");
1415
}
@@ -80,7 +81,7 @@ public static String[] camelCaseToSnakeCase(String[] input) {
8081
* @return Quoted String
8182
*/
8283
public static String quote(String input) {
83-
return input.replaceAll("^([^\"])", "\"$1").replaceAll("([^\"])$", "$1\"");
84+
return input.matches("^\".*\"$") ? input : "\"" + input + "\"";
8485
}
8586

8687
/**
@@ -102,4 +103,48 @@ public static String[] quote(String[] input) {
102103
public static String cleanString(String input) {
103104
return input.replaceAll("[^\\w]", "_");
104105
}
106+
107+
/**
108+
* Quotes a given string that contains special characters to comply with the csv specification RFC
109+
* 4180 (https://tools.ietf.org/html/rfc4180). Double quotes are escaped according to
110+
* specification.
111+
*
112+
* @param inputString string that should be converted to a valid rfc 4180 string
113+
* @param csvSep separator of the csv file
114+
* @return a csv string that is valid according to rfc 4180
115+
*/
116+
public static String csvString(String inputString, String csvSep) {
117+
if (needsCsvRFC4180Quote(inputString, csvSep)) {
118+
/* Get rid of first and last quotation if there is some. */
119+
String inputUnquoted = unquoteStartEnd(inputString);
120+
/* Escape every double quotation mark within the String by doubling it */
121+
String withEscapedQuotes = inputUnquoted.replaceAll("\"", "\"\"");
122+
/* finally add quotes to the strings start and end again */
123+
return quote(withEscapedQuotes);
124+
} else return inputString;
125+
}
126+
127+
/**
128+
* Removes double quotes at start and end position of the provided string, if any
129+
*
130+
* @param input string that should be unquoted
131+
* @return copy of the provided string without start and end double quotes
132+
*/
133+
public static String unquoteStartEnd(String input) {
134+
return input.matches("^\".*\"$") ? input.substring(1, input.length() - 1) : input;
135+
}
136+
137+
/**
138+
* Check if the provided string needs to be quoted according to the csv specification RFC 4180
139+
*
140+
* @param inputString the string that should be checked
141+
* @param csvSep separator of the csv file
142+
* @return true of the string needs to be quoted, false otherwise
143+
*/
144+
private static boolean needsCsvRFC4180Quote(String inputString, String csvSep) {
145+
return inputString.contains(csvSep)
146+
|| inputString.contains(",")
147+
|| inputString.contains("\"")
148+
|| inputString.contains("\n");
149+
}
105150
}

src/test/groovy/edu/ie3/util/StringUtilsTest.groovy

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ package edu.ie3.util
77

88
import spock.lang.Specification
99

10+
import java.util.stream.Collectors
11+
1012
class StringUtilsTest extends Specification {
1113

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

1921
where:
20-
input || expected
21-
"test" || "\"test\""
22-
"\"test" || "\"test\""
23-
"test\"" || "\"test\""
24-
"\"test\"" || "\"test\""
22+
input || expected
23+
"test" || "\"test\""
24+
"\"test" || "\"\"test\""
25+
"test\"" || "\"test\"\""
26+
"\"test\"" || "\"test\""
27+
"\"This\" is a test" || "\"\"This\" is a test\""
28+
"This is \"a\" test" || "\"This is \"a\" test\""
29+
"This is a \"test\"" || "\"This is a \"test\"\""
2530
}
2631

2732
def "The StringUtils are able to quote each element of an array of Strings"() {
@@ -211,4 +216,105 @@ class StringUtilsTest extends Specification {
211216
"?ab123" || "_ab123"
212217
"ßab123" || "_ab123"
213218
}
219+
220+
def "The StringUtils converts a given Array of csv header elements to match the csv specification RFC 4180 "() {
221+
given:
222+
def input = [
223+
"4ca90220-74c2-4369-9afa-a18bf068840d",
224+
"{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}",
225+
"node_a",
226+
"2020-03-25T15:11:31Z[UTC] \n 2020-03-24T15:11:31Z[UTC]",
227+
"8f9682df-0744-4b58-a122-f0dc730f6510",
228+
"true",
229+
"1,0",
230+
"1.0",
231+
"Höchstspannung",
232+
"380.0",
233+
"olm:{(0.00,1.00)}",
234+
"cosPhiP:{(0.0,1.0),(0.9,1.0),(1.2,-0.3)}"
235+
]
236+
def expected = [
237+
"4ca90220-74c2-4369-9afa-a18bf068840d",
238+
"\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528],\"\"crs\"\":{\"\"type\"\":\"\"name\"\",\"\"properties\"\":{\"\"name\"\":\"\"EPSG:4326\"\"}}}\"",
239+
"node_a",
240+
"\"2020-03-25T15:11:31Z[UTC] \n 2020-03-24T15:11:31Z[UTC]\"",
241+
"8f9682df-0744-4b58-a122-f0dc730f6510",
242+
"true",
243+
"\"1,0\"",
244+
"1.0",
245+
"Höchstspannung",
246+
"380.0",
247+
"\"olm:{(0.00,1.00)}\"",
248+
"\"cosPhiP:{(0.0,1.0),(0.9,1.0),(1.2,-0.3)}\""] as Set
249+
250+
when:
251+
def actual = input.stream().map({ inputElement -> StringUtils.csvString(inputElement, ",") }).collect(Collectors.toSet()) as Set
252+
253+
then:
254+
actual == expected
255+
}
256+
257+
def "The StringUtils converts a given LinkedHashMap of csv data to match the csv specification RFC 4180 "() {
258+
given:
259+
def input = [
260+
"activePowerGradient": "25.0",
261+
"capex" : "100,0",
262+
"cosphiRated" : "0.95",
263+
"etaConv" : "98.0",
264+
"id" : "test \n bmTypeInput",
265+
"opex" : "50.0",
266+
"sRated" : "25.0",
267+
"uu,id" : "5ebd8f7e-dedb-4017-bb86-6373c4b68eb8",
268+
"geoPosition" : "{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}",
269+
"olm\"characteristic": "olm:{(0.0,1.0)}",
270+
"cosPhiFixed" : "cosPhiFixed:{(0.0,1.0)}"
271+
] as LinkedHashMap<String, String>
272+
273+
def expected = [
274+
"activePowerGradient" : "25.0",
275+
"capex" : "\"100,0\"",
276+
"cosphiRated" : "0.95",
277+
"etaConv" : "98.0",
278+
"id" : "\"test \n bmTypeInput\"",
279+
"opex" : "50.0",
280+
"sRated" : "25.0",
281+
"\"uu,id\"" : "5ebd8f7e-dedb-4017-bb86-6373c4b68eb8",
282+
"geoPosition" : "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528],\"\"crs\"\":{\"\"type\"\":\"\"name\"\",\"\"properties\"\":{\"\"name\"\":\"\"EPSG:4326\"\"}}}\"",
283+
"\"olm\"\"characteristic\"": "\"olm:{(0.0,1.0)}\"",
284+
"cosPhiFixed" : "\"cosPhiFixed:{(0.0,1.0)}\""
285+
] as LinkedHashMap<String, String>
286+
287+
when:
288+
def actualList = input.entrySet().stream().map({ mapEntry ->
289+
return new AbstractMap.SimpleEntry<String, String>(StringUtils.csvString(mapEntry.key, ","), StringUtils.csvString(mapEntry.value, ","))
290+
}) as Set
291+
292+
def actual = actualList.collectEntries {
293+
[it.key, it.value]
294+
}
295+
296+
then:
297+
actual == expected
298+
}
299+
300+
def "The StringUtils converts a given String to match the csv specification RFC 4180 "() {
301+
expect:
302+
StringUtils.csvString(inputString, csvSep) == expect
303+
304+
where:
305+
inputString | csvSep || expect
306+
"activePowerGradient" | "," || "activePowerGradient"
307+
"\"100,0\"" | "," || "\"100,0\""
308+
"100,0" | "," || "\"100,0\""
309+
"100,0" | ";" || "\"100,0\""
310+
"100;0" | ";" || "\"100;0\""
311+
"\"100;0\"" | ";" || "\"100;0\""
312+
"100;0" | "," || "100;0"
313+
"olm:{(0.00,1.00)}" | "," || "\"olm:{(0.00,1.00)}\""
314+
"olm:{(0.00,1.00)}" | ";" || "\"olm:{(0.00,1.00)}\""
315+
"{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528]}" | "," || "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528]}\""
316+
"{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528]}" | ";" || "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528]}\""
317+
"uu,id" | "," || "\"uu,id\""
318+
"uu,id" | ";" || "\"uu,id\""
319+
}
214320
}

0 commit comments

Comments
 (0)