A Groovy class to generate CSV output (with tests)

I needed to write a class to output a report in comma-delimited format, and I decided to use the opportunity to practice a little Test Driven Development.

The CsvBuffer class is modeled upon StringBuffer. Instantiate one, load it with data one row at a time using append(), and then dump the output to whatever writer you want using toString() or toByteArray().

Since there is no formal specification for the comma-separated values (CSV) format, I used Wikipedia’s CSV article to define my requirements. Then I wrote a unit test:


import common.format.CsvBuffer

class CsvBufferTests extends GroovyTestCase {

String fromCodePoints(List list) {
StringBuffer buf = new StringBuffer()
list.each() {aChar ->
buf.appendCodePoint(aChar)
}
return buf.toString()
}

void testEscape() {
String neutral = fromCodePoints([115, 111, 109, 101, 32, 116, 101, 120,
116])
String containsComma = fromCodePoints([34, 116, 101, 120, 116, 44, 32,
115, 111, 109, 101, 34])
String containsDoubleQuote = fromCodePoints([34, 115, 111, 109, 101, 32,
34, 34, 116, 101, 120, 116, 34, 34, 32, 104, 101, 114, 101, 34])
String containsNewLine = fromCodePoints([34, 115, 111, 109, 101, 32,
116, 101, 120, 116, 32, 10, 104, 101, 114, 101, 34])
String containsSpaces = fromCodePoints([34, 32, 32, 115, 111, 109, 101,
32, 116, 101, 120, 116, 32, 32, 34])

CsvBuffer buf = new CsvBuffer()

/* Fields containing no special characters should not be enclosed in
* double quotes. */
assertEquals("Normal text", neutral, buf.escape("some text"))
/* Fields containing a comma should be enclosed */
assertEquals("Text containing a comma", containsComma,
buf.escape("text, some"))
/* Fields with embedded double-quote characters must be enclosed within
* double-quote characters, and each of the the embedded double-quote
* characters must be represented by a pair of double-quote
* characters. */
assertEquals("Text containing a double quote",
containsDoubleQuote, buf.escape("some \"text\" here"))
/* Fields containing a newline should be enclosed */
assertEquals("Text containing newline", containsNewLine,
buf.escape("some text \nhere"))
/* Fields with leading or trailing spaces must be enclosed within
* double-quote characters. */
assertEquals("Text containing leading or trailing spaces",
containsSpaces, buf.escape(" some text "))
}

void testAppend() {
CsvBuffer buf = new CsvBuffer()
buf.append(["String", null, 1, 2.3])
assertEquals(fromCodePoints([83, 116, 114, 105, 110, 103, 44, 44, 49,
44, 50, 46, 51, 13, 10]),
buf.toString())
}

}

With a test in hand, I then wrote a method to format output to my specification.


package common.format

class CsvBuffer {

private StringBuffer content = new StringBuffer()

/**
* Constructors. Setting column names on a header row is optional.
*/
CsvBuffer(List colnames) {
append(colnames)
}
CsvBuffer() {
// without header row
}

/**
* Returns a field value escaped for special characters
* @param input A String to be evaluated
* @return A properly formatted String
*/
String escape(String input) {
String output = new String()

if (input.contains(",") || input.contains("\n") ||
(!input.trim().equals(input))) {
output = "\"${input}\""
} else if (input.contains("\"")) {
output = "\"${input.replace("\"","\"\"")}\""
} else {
output = input
}

return output
}

/**
* Appends a row of values to the output
* @param values A list of values
* @return this CsvBuffer instance
*/
CsvBuffer append(List values) {
values.eachWithIndex() {value, i ->
// Insert a comma to delimit each field after the first
if (i > 0) {
content.append(",")
}
if (value != null) {
content.append(escape(value.toString()))
} // else null becomes ',,' - an empty string
}
content.append("\r\n")
return this
}

/**
* Outputs the contents of the buffer.
* @return Buffer contents as a String
*/
String toString() {
return content.toString()
}

/**
* Outputs the contents of the buffer.
* @return Buffer contents as a byte array
*/
byte[] toByteArray() {
return content.toString().getBytes()
}

}

I’m pretty happy with the tests for the escape() method. I’m curious – how would you recommend testing the append and output methods?

Advertisements

#csv, #groovy, #test-driven-development