/** * 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> containing values read from the * CSV file */ public static List> readIETFRFC4180CSVFile(Blob file) { String fileString = file.toString(); System.debug('fileString' + fileString); if (!fileString.endsWith(Parser.CRLF)) { fileString = fileString + Parser.CRLF; } List> fileValues = new List>(); List rowValues = new List(); 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(); } 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 data. * 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 value and * enclosed 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 CSVValue instances have the same * value and enclosed. */ public void assertEquals(CSVValue compCSVValue) { System.assertEquals(value, compCSVValue.value); System.assertEquals(enclosed, compCSVValue.enclosed); System.assertEquals(delimiter, compCSVValue.delimiter); } } }