/**
|
* http://frombelvideres4thfloor.blogspot.com/2010/10/ietf-rfc-4180-compliant-csv-reader-for.html
|
* CSVReader contains static methods used for reading a CSV file and returning
|
* the contained values in a nested List of Strings.
|
*
|
*/
|
public class CSVReader {
|
/**
|
* Returns a List containing Lists of Strings that represents
|
* the values contained in an IETF RFC 4180-compliant CSV file.
|
*
|
* Each element in the outer list represents a row in the CSV file.
|
* Each element in the inner list is the value in the field specified
|
* by the row-column combination.
|
*
|
* @param file the CSV file to read
|
* @return the List<List<String>> containing values read from the
|
* CSV file
|
*/
|
public static List<List<String>> readIETFRFC4180CSVFile(Blob file) {
|
String fileString = file.toString();
|
System.debug('fileString' + fileString);
|
if (!fileString.endsWith(Parser.CRLF)) {
|
fileString = fileString + Parser.CRLF;
|
}
|
|
List<List<String>> fileValues = new List<List<String>>();
|
List<String> rowValues = new List<String>();
|
CSVValue csvValue = new CSVValue();
|
|
Boolean eod = false; // Whether end of CSV data is reached
|
while (!eod) {
|
System.debug(fileString);
|
|
csvValue = readIETFRFC4180CSVValue(fileString);
|
rowValues.add(csvValue.value);
|
if (csvValue.delimiter == Parser.CRLF) {
|
fileValues.add(rowValues);
|
System.debug(rowValues);
|
if (fileValues.size() > 0) {
|
System.assertEquals(fileValues.get(0).size(),
|
rowValues.size());
|
}
|
rowValues = new List<String>();
|
}
|
|
if (csvValue.biteSize() == fileString.length()) {
|
eod = true;
|
}
|
else {
|
fileString = fileString.substring(csvValue.biteSize());
|
}
|
}
|
System.debug('fileValues' + fileValues.size());
|
return fileValues;
|
}
|
|
/**
|
* Returns the first String value read from a String representation of
|
* data contained in an IETF RFC 4180-compliant CSV file.
|
*
|
* The data is assumed to be terminated with a CRLF.
|
*
|
* @param data the textual CSV data in one long string
|
* @return the first CSV value read from <code>data</code>.
|
* null is returned if no value is discerned.
|
*/
|
public static CSVValue readIETFRFC4180CSVValue(String data) {
|
System.assert(data.endsWith(Parser.CRLF));
|
CSVValue csvValue = new CSVValue();
|
|
if (data.startsWith(Parser.DQUOTE)) {
|
csvValue.enclosed = true;
|
Integer searchIndex = 1; // starting index to search
|
Integer dquoteIndex = -1; // index of DQUOTE
|
Integer dquotesIndex = -1; // index of DQUOTEDQUOTE
|
|
Boolean closerFound = false;
|
while (!closerFound) {
|
dquoteIndex = data.indexOf(Parser.DQUOTE, searchIndex);
|
dquotesIndex = data.indexOf(Parser.DQUOTEDQUOTE,
|
searchIndex);
|
System.assert(dquoteIndex != -1);
|
|
if (dquoteIndex == dquotesIndex) {
|
searchIndex = dquotesIndex
|
+ Parser.DQUOTEDQUOTE.length();
|
}
|
else {
|
closerFound = true;
|
}
|
}
|
|
csvValue.value = data.substring(
|
Parser.DQUOTE.length(), dquoteIndex)
|
.replaceAll(Parser.DQUOTEDQUOTE, Parser.DQUOTE);
|
|
Integer commaIndex = data.indexOf(Parser.COMMA, dquoteIndex);
|
Integer crlfIndex = data.indexOf(Parser.CRLF, dquoteIndex);
|
|
if (commaIndex != -1 && commaIndex < crlfIndex) {
|
csvValue.delimiter = Parser.COMMA;
|
}
|
else {
|
csvValue.delimiter = Parser.CRLF;
|
}
|
}
|
else {
|
csvValue.enclosed = false;
|
Integer commaIndex = data.indexOf(Parser.COMMA);
|
Integer crlfIndex = data.indexOf(Parser.CRLF);
|
|
if (commaIndex != -1 && commaIndex < crlfIndex) {
|
csvValue.value = data.substring(0, commaIndex);
|
csvValue.delimiter = Parser.COMMA;
|
}
|
else {
|
csvValue.value = data.substring(0, crlfIndex);
|
csvValue.delimiter = Parser.CRLF;
|
}
|
}
|
System.debug('Returning: ' + csvValue);
|
return csvValue;
|
}
|
|
/**
|
* CSVValue is a class structure containing information about a CSV
|
* value that was read from a CSV file, including such information as
|
* whether the value was encapsulated in double-quotes.
|
*/
|
@TestVisible
|
private class CSVValue {
|
/**
|
* The field value that was read from the CSV file.
|
*/
|
public String value;
|
|
/**
|
* Whether the value was surrounded by double-quotes.
|
*/
|
public Boolean enclosed;
|
|
/**
|
* The comma or CRLF delimiter that identified the end of the CSV value.
|
*/
|
public String delimiter;
|
|
/**
|
* Default constructor, setting all members to null.
|
*/
|
public CSVValue() {
|
this(null, null, null);
|
}
|
|
/**
|
* Constructor.
|
*
|
* @param value the field value
|
* @param enclosed whether the value was surrounded by double-quotes
|
* @param delimiter the delimiter that identified the end
|
* of the CSV value
|
*/
|
public CSVValue(String value, Boolean enclosed, String delimiter) {
|
this.value = value;
|
this.enclosed = enclosed;
|
this.delimiter = delimiter;
|
}
|
|
/**
|
* Returns the number of characters to remove from the data
|
* String which produced the CSVValue in order to reach the next
|
* value in the data String.
|
*/
|
public Integer biteSize() {
|
Integer biteSize = value.replaceAll(Parser.DQUOTE, Parser.DQUOTEDQUOTE).length()
|
+ delimiter.length();
|
if (enclosed) {
|
biteSize += Parser.DQUOTE.length() * 2;
|
}
|
System.debug('biteSize: ' + biteSize);
|
return biteSize;
|
}
|
|
/**
|
* Returns whether a CSVValue has the same <code>value</code> and
|
* <code>enclosed</code> as another CSVValue.
|
*/
|
public Boolean equals(CSVValue compCSVValue) {
|
return this.value.equals(compCSVValue.value)
|
&& this.enclosed == compCSVValue.enclosed
|
&& this.delimiter == compCSVValue.delimiter;
|
}
|
|
/**
|
* Asserts that two <code>CSVValue</code> instances have the same
|
* <code>value</code> and <code>enclosed</code>.
|
*/
|
public void assertEquals(CSVValue compCSVValue) {
|
System.assertEquals(value, compCSVValue.value);
|
System.assertEquals(enclosed, compCSVValue.enclosed);
|
System.assertEquals(delimiter, compCSVValue.delimiter);
|
}
|
}
|
}
|