高章伟
2022-02-24 2aa8da8af66aa8ae00f25831aed6bb0364176e7b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
/**
 * 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);
        }
    }
}