Rev 1221 | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
671 | werner | 1 | /******************************************************************************************** |
2 | ** iLand - an individual based forest landscape and disturbance model |
||
3 | ** http://iland.boku.ac.at |
||
4 | ** Copyright (C) 2009- Werner Rammer |
||
5 | ** |
||
6 | ** This program is free software: you can redistribute it and/or modify |
||
7 | ** it under the terms of the GNU General Public License as published by |
||
8 | ** the Free Software Foundation, either version 3 of the License, or |
||
9 | ** (at your option) any later version. |
||
10 | ** |
||
11 | ** This program is distributed in the hope that it will be useful, |
||
12 | ** but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
13 | ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
14 | ** GNU General Public License for more details. |
||
15 | ** |
||
16 | ** You should have received a copy of the GNU General Public License |
||
17 | ** along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
18 | ********************************************************************************************/ |
||
19 | |||
243 | werner | 20 | #include "csvfile.h" |
21 | #include <QtCore> |
||
22 | #include "helper.h" |
||
23 | |||
248 | werner | 24 | /** @class CSVFile |
697 | werner | 25 | @ingroup tools |
248 | werner | 26 | Provides access to table data stored in text files (CSV style). |
245 | werner | 27 | Tables have optionally headers in first line (hasCaptions()) and can use various |
28 | delimiters ("tab",";",","," "). If separated by spaces, consecuteive spaces are merged. |
||
29 | Table dimensions can be accessed with colCount() and rowCount(), cell values as QVariant are retrieved |
||
30 | by value(). full rows are retrieved using row(). |
||
31 | Files are loaded by loadFile() or by passing a filename to the constructor: |
||
32 | @code |
||
33 | CSVFile file(fileName); |
||
34 | for (int row=0; row<file.rowCount(); row++) |
||
35 | for (int col=0; col<file.colCount(); col++) |
||
36 | value = file.value(row, col); |
||
37 | @endcode |
||
38 | Planned is also a "streaming" mode for large files (loadFile(), while(file.next()) file.value(x) ), but not finsihed yet. |
||
39 | |||
40 | */ |
||
793 | werner | 41 | #include <QJSEngine> |
42 | #include <QJSValue> |
||
43 | //Q_SCRIPT_DECLARE_QMETAOBJECT(CSVFile, QObject*) |
||
44 | void CSVFile::addToScriptEngine(QJSEngine &engine) |
||
243 | werner | 45 | { |
1032 | werner | 46 | Q_UNUSED(engine); // remove this code? |
245 | werner | 47 | // about this kind of scripting magic see: http://qt.nokia.com/developer/faqs/faq.2007-06-25.9557303148 |
793 | werner | 48 | //QJSValue cc_class = engine.scriptValueFromQMetaObject<CSVFile>(); |
49 | |||
50 | // TODO: solution for creating objects (on the C++ side...) |
||
51 | |||
434 | werner | 52 | // the script name for the object is "CSVFile". |
793 | werner | 53 | //engine.globalObject().setProperty("CSVFile", cc_class); |
245 | werner | 54 | } |
55 | |||
766 | werner | 56 | CSVFile::CSVFile(QObject *) |
245 | werner | 57 | { |
1208 | werner | 58 | mIsEmpty = true; |
244 | werner | 59 | mHasCaptions = true; |
250 | werner | 60 | mFlat = false; |
585 | werner | 61 | mFixedWidth=false; |
243 | werner | 62 | clear(); |
63 | } |
||
64 | |||
65 | void CSVFile::clear() |
||
66 | { |
||
67 | mColCount = mRowCount = -1; |
||
68 | mCaptions.clear(); |
||
69 | mRows.clear(); |
||
1208 | werner | 70 | mIsEmpty = true; |
71 | |||
243 | werner | 72 | } |
73 | |||
258 | werner | 74 | bool CSVFile::loadFromString(const QString &content) |
243 | werner | 75 | { |
76 | clear(); |
||
585 | werner | 77 | // split into rows: use either with windows or unix style delimiter |
78 | if (content.left(1000).contains("\r\n")) |
||
79 | mRows = content.split("\r\n", QString::SkipEmptyParts); |
||
80 | else |
||
81 | mRows = content.split("\n", QString::SkipEmptyParts); |
||
82 | |||
243 | werner | 83 | if (mRows.count()==0) |
84 | return false; |
||
85 | |||
1208 | werner | 86 | mIsEmpty = false; |
585 | werner | 87 | // trimming of whitespaces is a problem |
88 | // when having e.g. tabs as delimiters... |
||
89 | // if (!mFixedWidth) { |
||
90 | // for (int i=0;i<mRows.count();i++) |
||
91 | // mRows[i] = mRows[i].trimmed(); |
||
92 | // } |
||
289 | werner | 93 | // drop comments (i.e. lines at the beginning that start with '#', also ignore '<' (are in tags of picus-ini-files) |
94 | while (!mRows.isEmpty() && (mRows.front().startsWith('#') || mRows.front().startsWith('<'))) |
||
282 | werner | 95 | mRows.pop_front(); |
383 | werner | 96 | while (!mRows.isEmpty() && mRows.last().isEmpty()) |
288 | werner | 97 | mRows.removeLast(); |
282 | werner | 98 | |
250 | werner | 99 | mSeparator = ";"; // default |
243 | werner | 100 | QString first = mRows.first(); |
250 | werner | 101 | if (!mFlat) { |
102 | // detect separator |
||
103 | int c_tab = first.count('\t'); |
||
104 | int c_semi = first.count(';'); |
||
105 | int c_comma = first.count(','); |
||
106 | int c_space = first.count(' '); |
||
107 | if (c_tab+c_semi+c_comma+c_space == 0) { |
||
108 | qDebug() << "CSVFile::loadFile: cannot recognize separator. first line:" << first; |
||
109 | return false; |
||
110 | } |
||
111 | mSeparator=" "; |
||
112 | if (c_tab > c_semi && c_tab>c_comma) mSeparator="\t"; |
||
113 | if (c_semi > c_tab && c_semi>c_comma) mSeparator=";"; |
||
114 | if (c_comma > c_tab && c_comma>c_semi) mSeparator=","; |
||
500 | werner | 115 | // if (mSeparator==" ") { |
116 | // for (int i=0;i<mRows.count();i++) |
||
117 | // mRows[i] = mRows[i].simplified(); |
||
118 | // first = mRows.first(); |
||
119 | // } |
||
250 | werner | 120 | } // !mFlat |
243 | werner | 121 | |
122 | // captions |
||
123 | if (mHasCaptions) { |
||
1048 | werner | 124 | mCaptions = first.split(mSeparator, QString::KeepEmptyParts).replaceInStrings("\"", ""); // drop "-characters |
243 | werner | 125 | mRows.pop_front(); // drop the first line |
126 | } else { |
||
127 | // create pseudo captions |
||
1048 | werner | 128 | mCaptions = first.split(mSeparator, QString::KeepEmptyParts); |
243 | werner | 129 | for (int i=0;i<mCaptions.count();i++) |
130 | mCaptions[i] = QString("c%1").arg(i); |
||
131 | } |
||
250 | werner | 132 | |
243 | werner | 133 | mColCount = mCaptions.count(); |
134 | mRowCount = mRows.count(); |
||
135 | mStreamingMode = false; |
||
136 | return true; |
||
258 | werner | 137 | |
243 | werner | 138 | } |
258 | werner | 139 | bool CSVFile::loadFile(const QString &fileName) |
140 | { |
||
141 | QString content = Helper::loadTextFile(fileName); |
||
142 | if (content.isEmpty()) { |
||
143 | qDebug() << "CSVFile::loadFile" << fileName << "does not exist or is empty."; |
||
1208 | werner | 144 | mIsEmpty = true; |
258 | werner | 145 | return false; |
146 | } |
||
147 | return loadFromString(content); |
||
148 | } |
||
340 | werner | 149 | QVariantList CSVFile::values(const int row) const |
150 | { |
||
151 | QVariantList list; |
||
152 | if (row<0 || row>=mRowCount) { |
||
153 | qDebug() << "CSVFile::values: invalid row:" << row; |
||
154 | return list; |
||
155 | } |
||
156 | QStringList line = mRows[row].split(mSeparator); |
||
157 | foreach(QString item, line) |
||
158 | list.append(item); |
||
159 | return list; |
||
160 | } |
||
243 | werner | 161 | |
280 | werner | 162 | QVariant CSVFile::value(const int row, const int col) const |
243 | werner | 163 | { |
164 | if (mStreamingMode) |
||
165 | return QVariant(); |
||
166 | |||
585 | werner | 167 | if (row<0 || row>=mRowCount || col<0 || col>=mColCount) { |
245 | werner | 168 | qDebug() << "CSVFile::value: invalid index: row col:" << row << col << ". Size is:" << mRowCount << mColCount; |
243 | werner | 169 | return QVariant(); |
170 | } |
||
500 | werner | 171 | |
172 | if (mFixedWidth) { |
||
173 | // special case with space (1..n) as separator |
||
174 | QString s = mRows[row]; |
||
175 | QChar sep=mSeparator.at(0); |
||
176 | QVariant result; |
||
177 | if (col==mColCount-1) { |
||
178 | // last element: |
||
179 | result = s.mid(s.lastIndexOf(sep)+1); |
||
180 | return result; |
||
181 | } |
||
182 | int sepcount=0; |
||
183 | int lastsep=0; |
||
184 | int i=0; |
||
185 | while (s.at(i) == sep && i<s.size()) i++; // skip initial spaces |
||
186 | for (;i<s.size();i++) { |
||
187 | if (s.at(i) == sep) { |
||
188 | // skip all spaces |
||
189 | while (s.at(i)==sep) |
||
190 | i++; |
||
191 | i--; // go back to last separator |
||
192 | // count the separators up to the wanted column |
||
193 | if (sepcount==col) { |
||
194 | result = s.mid(lastsep,i-lastsep); |
||
195 | return result; |
||
196 | } |
||
197 | sepcount++; |
||
198 | lastsep=i+1; |
||
199 | } |
||
200 | } |
||
201 | qDebug() << "CSVFile::value: found no result:" << row << col << ". Size is:" << mRowCount << mColCount; |
||
202 | return QVariant(); |
||
203 | } |
||
204 | |||
205 | // one-character separators.... |
||
206 | if (mSeparator.length()==1) { |
||
207 | QString s = mRows[row]; |
||
208 | QChar sep=mSeparator.at(0); |
||
209 | QVariant result; |
||
210 | if (col==mColCount-1) { |
||
211 | // last element: |
||
585 | werner | 212 | if (s.count(sep)==mColCount-1) { |
213 | result = s.mid(s.lastIndexOf(sep)+1); |
||
938 | werner | 214 | if (result.toString().startsWith('\"') && result.toString().endsWith('\"')) |
215 | result = result.toString().mid(1, result.toString().length()-2); |
||
585 | werner | 216 | } |
217 | // if there are less than colcount-1 separators, then |
||
218 | // the last columns is empty |
||
500 | werner | 219 | return result; |
220 | } |
||
221 | int sepcount=0; |
||
222 | int lastsep=0; |
||
223 | for (int i=0;i<s.size();i++) { |
||
224 | if (s.at(i) == sep) { |
||
225 | // count the separators up to the wanted column |
||
226 | if (sepcount==col) { |
||
585 | werner | 227 | if (s.at(lastsep)=='\"' && s.at(i-1)=='\"') |
228 | result = s.mid(lastsep+1,i-lastsep-2); // ignore " |
||
229 | else |
||
230 | result = s.mid(lastsep,i-lastsep); |
||
231 | |||
500 | werner | 232 | return result; |
233 | } |
||
234 | sepcount++; |
||
235 | lastsep=i+1; |
||
236 | } |
||
237 | } |
||
585 | werner | 238 | if (sepcount==col) |
239 | result = s.mid(s.lastIndexOf(sep)+1); |
||
240 | //qDebug() << "CSVFile::value: found no result:" << row << col << ". Size is:" << mRowCount << mColCount; |
||
969 | werner | 241 | return result; |
500 | werner | 242 | |
243 | } |
||
244 | |||
245 | // fallback, if separator is more than one character. This is very slow approach.... (old) |
||
243 | werner | 246 | QStringList line = mRows[row].split(mSeparator); |
247 | QVariant result; |
||
248 | if (col<line.count()) { |
||
249 | result = line[col]; |
||
250 | } |
||
251 | return result; |
||
252 | } |
||
245 | werner | 253 | QVariant CSVFile::row(const int row) |
254 | { |
||
255 | if (mStreamingMode) |
||
256 | return QVariant(); |
||
243 | werner | 257 | |
245 | werner | 258 | if (row<0 || row>=mRowCount) { |
259 | qDebug() << "CSVFile::row: invalid index: row " << row << ". Size is:" << mRowCount ; |
||
260 | return QVariant(); |
||
261 | } |
||
262 | |||
263 | QVariant result = mRows[row]; |
||
264 | return result; |
||
265 | } |
||
266 | |||
243 | werner | 267 | bool CSVFile::openFile(const QString &fileName) |
268 | { |
||
766 | werner | 269 | (void)fileName; // silence compiler warning; the function makes no sense, nonetheless. |
243 | werner | 270 | mStreamingMode = true; |
271 | return false; |
||
272 | } |
||
280 | werner | 273 | |
274 | QStringList CSVFile::column(const int col) const |
||
275 | { |
||
276 | QStringList result; |
||
277 | for (int row=0;row<rowCount();row++) |
||
278 | result+=value(row,col).toString(); |
||
279 | return result; |
||
280 | } |
||
281 | |||
500 | werner | 282 | void CSVFile::setValue(const int row, const int col, QVariant value) |
283 | { |
||
284 | if (row<0 || row>=mRowCount || col<0 || col>mColCount) { |
||
285 | qDebug() << "CSVFile::setValue: invalid index: row col:" << row << col << ". Size is:" << mRowCount << mColCount; |
||
286 | return; |
||
287 | } |
||
288 | if (!mFixedWidth) { |
||
289 | QStringList line = mRows[row].split(mSeparator); |
||
290 | if (col<line.count()) { |
||
291 | line[col] = value.toString(); |
||
292 | } |
||
293 | mRows[row] = line.join(mSeparator); |
||
294 | } else { |
||
295 | // if sep=space, we assume a fixed format... and try to insert the new data right-padded |
||
296 | QString data = value.toString(); |
||
297 | QString &s = mRows[row]; |
||
298 | int field=mColCount-1; |
||
299 | QChar sep = mSeparator[0]; |
||
300 | for (int i=s.size()-1; i>=0; --i) { |
||
301 | if (field == col) { |
||
302 | // replace: first, replace data with spaces... |
||
303 | int r = i; // position of last character of the data |
||
304 | while (s.at(i)!=sep) { |
||
305 | s[i] = sep; |
||
306 | i--; |
||
307 | } |
||
308 | // insert data |
||
309 | for (int j=0;j<data.size();j++) |
||
310 | s[r - data.size() + j + 1] = data.at(j); |
||
311 | return; // finished |
||
312 | } |
||
313 | if (s.at(i)==sep) { |
||
314 | field--; |
||
315 | while (s.at(i)==sep && i>=0) i--; // skip multiple spaces |
||
316 | i++; |
||
317 | } |
||
318 | } |
||
319 | qDebug() << "CSVFile::saveFile: save value with fixed width: col not found."; |
||
320 | } |
||
321 | } |
||
322 | |||
323 | /// save the contents of the CSVFile back to a file. |
||
324 | /// this removes all comments and uses the system line-end |
||
325 | void CSVFile::saveFile(const QString &fileName) |
||
326 | { |
||
327 | QFile file(fileName); |
||
328 | if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { |
||
329 | qDebug() << "CSVFile::saveFile: could not create file" + fileName; |
||
330 | return; |
||
331 | } |
||
332 | QTextStream str(&file); |
||
333 | if (mHasCaptions) |
||
334 | str << mCaptions.join(mSeparator) << endl; |
||
335 | foreach(const QString s, mRows) |
||
336 | str << s << endl; |
||
337 | } |
||
338 |