Addressing some compilation warnings:
[qwerkisync] / CSV.cpp
1 /*
2  * Copyright (C) 2011, Jamie Thompson
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public
6  * License as published by the Free Software Foundation; either
7  * version 3 of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public
15  * License along with this program; If not, see
16  * <http://www.gnu.org/licenses/>.
17  */
18
19 #include "CSV.h"
20
21 #include <QDebug>
22
23 #include <QFile>
24 #include <QHash>
25 #include <QString>
26 #include <QTextStream>
27
28 class SortByValueDesc
29 {
30 public:
31         inline bool operator()(const QPair<QChar, uint> &a, const QPair<QChar, uint> &b) const
32         {
33                 return b.second < a.second;
34         }
35 };
36
37 CSV::CSV()
38         : m_IsValid(false), m_File(NULL), m_Stream(NULL), m_LineNumber(0), m_RecordNumber(0)
39 {
40 }
41
42 CSV::CSV(QChar delimiter, int numColumnsPerRecord, const ColumnIndicesHash &headingIndices)
43         : m_IsValid(false), m_File(NULL), m_Stream(NULL), m_LineNumber(0), m_RecordNumber(0)
44 {
45         Delimiter(delimiter);
46         NumColumnsPerRecord(numColumnsPerRecord);
47
48         UpdateHeadings(headingIndices);
49
50         IsValid(true);
51 }
52
53 CSV::~CSV()
54 {
55 }
56
57 void CSV::Open(QFile &file)
58 {
59         // Ready the file...
60         LineNumber(0);
61         RecordNumber(0);
62         File(&file);
63         File()->seek(0);
64
65         // Read the first line
66         Stream(new QTextStream(&file));
67
68         // Set up the properties...
69         if(!IsValid())
70         {
71                 QString firstLineContent(Stream()->readLine());
72                 DetermineDelimiter(firstLineContent);
73                 GetHeadings(firstLineContent);
74         }
75         // We accept we've already done the hard work, so advance to the first
76         // actual record (i.e. the 2nd row)
77         else
78                 ReadRecord();
79 }
80
81 void CSV::Open(QFile &file, QChar delimiter, int numColumnsPerRecord, const ColumnIndicesHash &headingIndices)
82 {
83         // Set up the properties...
84         Delimiter(delimiter);
85         NumColumnsPerRecord(numColumnsPerRecord);
86         UpdateHeadings(headingIndices);
87         IsValid(true);
88
89         // Ready the file...
90         File(&file);
91         File()->seek(0);
92
93         // Advance to the first actual record (i.e. the 2nd row)
94         ReadRecord();
95 }
96
97 void CSV::Close()
98 {
99         IsValid(false);
100         File(NULL);
101 }
102
103 bool CSV::AtEnd() const
104 {
105         return Stream()->atEnd();
106 }
107
108 QHash<QString, QString> CSV::ReadRecord()
109 {
110         // If we have something more to read...
111         if(LineValues().count() < NumColumnsPerRecord() && !Stream()->atEnd())
112         {
113                 // ...read a line's worth but make sure we have enough columns (i.e. handle newlines in values)
114                 while(LineValues().count() < NumColumnsPerRecord())
115                 {
116                         QStringList nextValues(QString(Stream()->readLine()).split(Delimiter()));
117                         if(LineValues().count() > 0)
118                         {
119                                 // Merge the first value of the next line with the last of the previous...
120                                 LineValues().last().append('\n');
121                                 nextValues.removeAt(0);
122                         }
123                         LineValues().append(nextValues);
124                         ++LineNumber();
125                 }
126         }
127
128         // The extract enough values to complete a record
129         QHash<QString, QString> recordValues;
130         for(int i(NumColumnsPerRecord() - 1); i >= 0 && LineValues().count() >= 0; --i)
131         {
132                 recordValues.insert(HeadingNames().value(i), LineValues().value(i));
133                 LineValues().removeAt(i);
134         }
135         return recordValues;
136 }
137
138 void CSV::GetHeadings(const QString &firstLineContent)
139 {
140         QStringList headingsRaw(QString(firstLineContent).split(Delimiter(), QString::KeepEmptyParts, Qt::CaseSensitive));
141
142         // We have this many fields per record
143         NumColumnsPerRecord(headingsRaw.count());
144
145         // Grab each column heading, and tidy it up.
146         ColumnIndicesHash indices;
147         indices.reserve(headingsRaw.count());
148         for(QStringList::size_type i(0); i < headingsRaw.count(); ++i)
149         {
150                 QString heading(ExtractString(headingsRaw.value(i)));
151                 qDebug() << headingsRaw.value(i) << " : " << heading;
152
153                 indices[heading] = i;
154         }
155
156         UpdateHeadings(indices);
157 }
158
159 const QStringList CSV::HasRequiredHeadings(const QStringList &requiredHeadings)
160 {
161         QStringList missingRequiredHeadings(requiredHeadings);
162
163         // Check over the required headings
164         foreach(const QString requiredHeading, requiredHeadings)
165         {
166                 if(HeadingIndices().contains(requiredHeading.toLower()))
167                         missingRequiredHeadings.removeOne(requiredHeading);
168         }
169
170         return missingRequiredHeadings;
171 }
172
173 void CSV::DetermineDelimiter(const QString &firstLineContent)
174 {
175         // Count the non-alphanumeric characters used
176         QHash<QChar, uint> counts;
177         foreach(const QChar c, firstLineContent)
178                 ++counts[c];
179
180         QList<QPair<QChar, uint> > orderedCounts;
181         orderedCounts.reserve(counts.size());
182         foreach(const QChar c, counts.keys())
183                 if(!QChar(c).isLetterOrNumber())
184                         orderedCounts.append(QPair<QChar, uint>(c, counts.value(c)));
185
186         qSort(orderedCounts.begin(), orderedCounts.end(), SortByValueDesc());
187
188         // Work around Q_FOREACH macro limitation when dealing with
189         // multi-typed templates (comma issue)
190         typedef QPair<QChar, uint> bodge;
191         foreach(bodge count, orderedCounts)
192                 qDebug() << count.first << " = " << count.second;
193
194         // No-one would be mad enough to use quotation marks or apostrophes
195         // as their delimiter,but just in case, check the second most
196         // frequent character is present the right number of times for
197         // the quotation marks to be present on every column heading (two
198         // per heading, less one as they're seperators)
199         if((orderedCounts.value(0).first == '"' || orderedCounts.value(0).first == '\'')
200                 && ((orderedCounts.value(0).second / 2) - 1 == orderedCounts.value(1).second ))
201         {
202                 // We're good.
203                 Delimiter(orderedCounts.value(1).first);
204         }
205         else
206                 Delimiter(orderedCounts.value(0).first);
207 }
208
209 const QString CSV::ExtractString(const QString &originalString)
210 {
211         QRegExp content("^[\"\']?(.*)?[\"\']?$");
212         content.indexIn(originalString.trimmed());
213         return content.cap(1);
214 }
215
216 void CSV::UpdateHeadings(const ColumnIndicesHash &headingIndices)
217 {
218         HeadingIndices().clear();
219         HeadingIndices().reserve(headingIndices.count());
220         foreach(QString columnName, headingIndices.keys())
221                 HeadingIndices().insert(columnName.toLower(), headingIndices.value(columnName));
222
223         // ..and prepare the bidirectional hash (toLower not needed as above
224         // value reused)
225         HeadingNames().clear();
226         HeadingNames().reserve(headingIndices.count());
227         foreach(QString columnName, HeadingIndices().keys())
228                 HeadingNames().insert(HeadingIndices().value(columnName), columnName);
229 }