csvdocument.pp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. {
  2. CSV Document classes.
  3. Version 0.5 2014-10-25
  4. Copyright (C) 2010-2014 Vladimir Zhirov <[email protected]>
  5. Contributors:
  6. Luiz Americo Pereira Camara
  7. Mattias Gaertner
  8. Reinier Olislagers
  9. This library is free software; you can redistribute it and/or modify it
  10. under the terms of the GNU Library General Public License as published by
  11. the Free Software Foundation; either version 2 of the License, or (at your
  12. option) any later version with the following modification:
  13. As a special exception, the copyright holders of this library give you
  14. permission to link this library with independent modules to produce an
  15. executable, regardless of the license terms of these independent modules,and
  16. to copy and distribute the resulting executable under terms of your choice,
  17. provided that you also meet, for each linked independent module, the terms
  18. and conditions of the license of that module. An independent module is a
  19. module which is not derived from or based on this library. If you modify
  20. this library, you may extend this exception to your version of the library,
  21. but you are not obligated to do so. If you do not wish to do so, delete this
  22. exception statement from your version.
  23. This program is distributed in the hope that it will be useful, but WITHOUT
  24. ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  25. FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
  26. for more details.
  27. You should have received a copy of the GNU Library General Public License
  28. along with this library; if not, write to the Free Software Foundation,
  29. Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  30. }
  31. unit csvdocument;
  32. {$IFDEF FPC}
  33. {$MODE DELPHI}
  34. {$ENDIF}
  35. interface
  36. uses
  37. Classes, SysUtils, Contnrs, csvreadwrite;
  38. type
  39. TCSVChar = csvreadwrite.TCSVChar;
  40. TCSVParser = csvreadwrite.TCSVParser;
  41. TCSVBuilder = csvreadwrite.TCSVBuilder;
  42. {$IFNDEF FPC}
  43. TFPObjectList = TObjectList;
  44. {$ENDIF}
  45. // Random access to CSV document. Reads entire document into memory.
  46. TCSVDocument = class(TCSVHandler)
  47. private
  48. FRows: TFPObjectList;
  49. FParser: TCSVParser;
  50. FBuilder: TCSVBuilder;
  51. // helpers
  52. procedure ForceRowIndex(ARowIndex: Integer);
  53. function CreateNewRow(const AFirstCell: String = ''): TObject;
  54. // property getters/setters
  55. function GetCell(ACol, ARow: Integer): String;
  56. procedure SetCell(ACol, ARow: Integer; const AValue: String);
  57. function GetCSVText: String;
  58. procedure SetCSVText(const AValue: String);
  59. function GetRowCount: Integer;
  60. function GetColCount(ARow: Integer): Integer;
  61. function GetMaxColCount: Integer;
  62. public
  63. constructor Create;
  64. destructor Destroy; override;
  65. // Input/output
  66. // Load document from file AFileName
  67. procedure LoadFromFile(const AFilename: String);
  68. // Load document from stream AStream
  69. procedure LoadFromStream(AStream: TStream);
  70. // Save document to file AFilename
  71. procedure SaveToFile(const AFilename: String);
  72. // Save document to stream AStream
  73. procedure SaveToStream(AStream: TStream);
  74. // Row and cell operations
  75. // Add a new row and a cell with content AFirstCell
  76. procedure AddRow(const AFirstCell: String = '');
  77. // Add a cell at row ARow with data AValue
  78. procedure AddCell(ARow: Integer; const AValue: String = '');
  79. // Insert a row at row ARow with first cell data AFirstCell
  80. // If there is no row ARow, insert row at end
  81. procedure InsertRow(ARow: Integer; const AFirstCell: String = '');
  82. // Insert a cell at specified position with data AValue
  83. procedure InsertCell(ACol, ARow: Integer; const AValue: String = '');
  84. // Remove specified row
  85. procedure RemoveRow(ARow: Integer);
  86. // Remove specified cell
  87. procedure RemoveCell(ACol, ARow: Integer);
  88. // Indicates if there is a row at specified position
  89. function HasRow(ARow: Integer): Boolean;
  90. // Indicates if there is a cell at specified position
  91. function HasCell(ACol, ARow: Integer): Boolean;
  92. // Search
  93. // Return column for cell data AString at row ARow
  94. function IndexOfCol(const AString: String; ARow: Integer): Integer;
  95. // Return row for cell data AString at coloumn ACol
  96. function IndexOfRow(const AString: String; ACol: Integer): Integer;
  97. // Utils
  98. // Remove all data
  99. procedure Clear;
  100. // Copy entire row ARow to row position AInsertPos.
  101. // Adds empty rows if necessary
  102. procedure CloneRow(ARow, AInsertPos: Integer);
  103. // Exchange contents of the two specified rows
  104. procedure ExchangeRows(ARow1, ARow2: Integer);
  105. // Rewrite all line endings within cell data to LineEnding
  106. procedure UnifyEmbeddedLineEndings;
  107. // Remove empty cells at end of rows from entire document
  108. procedure RemoveTrailingEmptyCells;
  109. // Properties
  110. // Cell data at column ACol, row ARow.
  111. property Cells[ACol, ARow: Integer]: String read GetCell write SetCell; default;
  112. // Number of rows
  113. property RowCount: Integer read GetRowCount;
  114. // Number of columns for row ARow
  115. property ColCount[ARow: Integer]: Integer read GetColCount;
  116. // Maximum number of columns found in all rows in document
  117. property MaxColCount: Integer read GetMaxColCount;
  118. // Document formatted as CSV text
  119. property CSVText: String read GetCSVText write SetCSVText;
  120. end;
  121. implementation
  122. //------------------------------------------------------------------------------
  123. type
  124. TCSVCell = class
  125. public
  126. // Value (contents) of cell in string form
  127. Value: String;
  128. end;
  129. TCSVRow = class
  130. private
  131. FCells: TFPObjectList;
  132. procedure ForceCellIndex(ACellIndex: Integer);
  133. function CreateNewCell(const AValue: String): TCSVCell;
  134. function GetCellValue(ACol: Integer): String;
  135. procedure SetCellValue(ACol: Integer; const AValue: String);
  136. function GetColCount: Integer;
  137. public
  138. constructor Create;
  139. destructor Destroy; override;
  140. // cell operations
  141. // Add cell with value AValue to row
  142. procedure AddCell(const AValue: String = '');
  143. // Insert cell with value AValue at specified column
  144. procedure InsertCell(ACol: Integer; const AValue: String);
  145. // Remove cell from specified column
  146. procedure RemoveCell(ACol: Integer);
  147. // Indicates if specified column contains a cell/data
  148. function HasCell(ACol: Integer): Boolean;
  149. // utilities
  150. // Copy entire row
  151. function Clone: TCSVRow;
  152. // Remove all empty cells at the end of the row
  153. procedure TrimEmptyCells;
  154. // Replace various line endings in data with ALineEnding
  155. procedure SetValuesLineEnding(const ALineEnding: String);
  156. // properties
  157. // Value/data of cell at column ACol
  158. property CellValue[ACol: Integer]: String read GetCellValue write SetCellValue;
  159. // Number of columns in row
  160. property ColCount: Integer read GetColCount;
  161. end;
  162. { TCSVRow }
  163. procedure TCSVRow.ForceCellIndex(ACellIndex: Integer);
  164. begin
  165. while FCells.Count <= ACellIndex do
  166. AddCell();
  167. end;
  168. function TCSVRow.CreateNewCell(const AValue: String): TCSVCell;
  169. begin
  170. Result := TCSVCell.Create;
  171. Result.Value := AValue;
  172. end;
  173. function TCSVRow.GetCellValue(ACol: Integer): String;
  174. begin
  175. if HasCell(ACol) then
  176. Result := TCSVCell(FCells[ACol]).Value
  177. else
  178. Result := '';
  179. end;
  180. procedure TCSVRow.SetCellValue(ACol: Integer; const AValue: String);
  181. begin
  182. ForceCellIndex(ACol);
  183. TCSVCell(FCells[ACol]).Value := AValue;
  184. end;
  185. function TCSVRow.GetColCount: Integer;
  186. begin
  187. Result := FCells.Count;
  188. end;
  189. constructor TCSVRow.Create;
  190. begin
  191. inherited Create;
  192. FCells := TFPObjectList.Create;
  193. end;
  194. destructor TCSVRow.Destroy;
  195. begin
  196. FreeAndNil(FCells);
  197. inherited Destroy;
  198. end;
  199. procedure TCSVRow.AddCell(const AValue: String = '');
  200. begin
  201. FCells.Add(CreateNewCell(AValue));
  202. end;
  203. procedure TCSVRow.InsertCell(ACol: Integer; const AValue: String);
  204. begin
  205. FCells.Insert(ACol, CreateNewCell(AValue));
  206. end;
  207. procedure TCSVRow.RemoveCell(ACol: Integer);
  208. begin
  209. if HasCell(ACol) then
  210. FCells.Delete(ACol);
  211. end;
  212. function TCSVRow.HasCell(ACol: Integer): Boolean;
  213. begin
  214. Result := (ACol >= 0) and (ACol < FCells.Count);
  215. end;
  216. function TCSVRow.Clone: TCSVRow;
  217. var
  218. I: Integer;
  219. begin
  220. Result := TCSVRow.Create;
  221. for I := 0 to ColCount - 1 do
  222. Result.AddCell(CellValue[I]);
  223. end;
  224. procedure TCSVRow.TrimEmptyCells;
  225. var
  226. I: Integer;
  227. MaxCol: Integer;
  228. begin
  229. MaxCol := FCells.Count - 1;
  230. for I := MaxCol downto 0 do
  231. begin
  232. if (TCSVCell(FCells[I]).Value = '') then
  233. begin
  234. if (FCells.Count > 1) then
  235. FCells.Delete(I);
  236. end else
  237. break; // We hit the first non-empty cell so stop
  238. end;
  239. end;
  240. procedure TCSVRow.SetValuesLineEnding(const ALineEnding: String);
  241. var
  242. I: Integer;
  243. begin
  244. for I := 0 to FCells.Count - 1 do
  245. CellValue[I] := ChangeLineEndings(CellValue[I], ALineEnding);
  246. end;
  247. { TCSVDocument }
  248. procedure TCSVDocument.ForceRowIndex(ARowIndex: Integer);
  249. begin
  250. while FRows.Count <= ARowIndex do
  251. AddRow();
  252. end;
  253. function TCSVDocument.CreateNewRow(const AFirstCell: String): TObject;
  254. var
  255. NewRow: TCSVRow;
  256. begin
  257. NewRow := TCSVRow.Create;
  258. if AFirstCell <> '' then
  259. NewRow.AddCell(AFirstCell);
  260. Result := NewRow;
  261. end;
  262. function TCSVDocument.GetCell(ACol, ARow: Integer): String;
  263. begin
  264. if HasRow(ARow) then
  265. Result := TCSVRow(FRows[ARow]).CellValue[ACol]
  266. else
  267. Result := '';
  268. end;
  269. procedure TCSVDocument.SetCell(ACol, ARow: Integer; const AValue: String);
  270. begin
  271. ForceRowIndex(ARow);
  272. TCSVRow(FRows[ARow]).CellValue[ACol] := AValue;
  273. end;
  274. function TCSVDocument.GetCSVText: String;
  275. var
  276. StringStream: TStringStream;
  277. begin
  278. StringStream := TStringStream.Create('');
  279. try
  280. SaveToStream(StringStream);
  281. Result := StringStream.DataString;
  282. finally
  283. FreeAndNil(StringStream);
  284. end;
  285. end;
  286. procedure TCSVDocument.SetCSVText(const AValue: String);
  287. var
  288. StringStream: TStringStream;
  289. begin
  290. StringStream := TStringStream.Create(AValue);
  291. try
  292. LoadFromStream(StringStream);
  293. finally
  294. FreeAndNil(StringStream);
  295. end;
  296. end;
  297. function TCSVDocument.GetRowCount: Integer;
  298. begin
  299. Result := FRows.Count;
  300. end;
  301. function TCSVDocument.GetColCount(ARow: Integer): Integer;
  302. begin
  303. if HasRow(ARow) then
  304. Result := TCSVRow(FRows[ARow]).ColCount
  305. else
  306. Result := 0;
  307. end;
  308. // Returns maximum number of columns in the document
  309. function TCSVDocument.GetMaxColCount: Integer;
  310. var
  311. I, CC: Integer;
  312. begin
  313. // While calling MaxColCount in TCSVParser could work,
  314. // we'd need to adjust for any subsequent changes in
  315. // TCSVDocument
  316. Result := 0;
  317. for I := 0 to RowCount - 1 do
  318. begin
  319. CC := ColCount[I];
  320. if CC > Result then
  321. Result := CC;
  322. end;
  323. end;
  324. constructor TCSVDocument.Create;
  325. begin
  326. inherited Create;
  327. FRows := TFPObjectList.Create;
  328. FParser := nil;
  329. FBuilder := nil;
  330. end;
  331. destructor TCSVDocument.Destroy;
  332. begin
  333. FreeAndNil(FBuilder);
  334. FreeAndNil(FParser);
  335. FreeAndNil(FRows);
  336. inherited Destroy;
  337. end;
  338. procedure TCSVDocument.LoadFromFile(const AFilename: String);
  339. var
  340. FileStream: TFileStream;
  341. begin
  342. FileStream := TFileStream.Create(AFilename, fmOpenRead or fmShareDenyNone);
  343. try
  344. LoadFromStream(FileStream);
  345. finally
  346. FileStream.Free;
  347. end;
  348. end;
  349. procedure TCSVDocument.LoadFromStream(AStream: TStream);
  350. var
  351. I, J, MaxCol: Integer;
  352. begin
  353. Clear;
  354. if not Assigned(FParser) then
  355. FParser := TCSVParser.Create;
  356. FParser.AssignCSVProperties(Self);
  357. with FParser do
  358. begin
  359. SetSource(AStream);
  360. while ParseNextCell do
  361. Cells[CurrentCol, CurrentRow] := CurrentCellText;
  362. end;
  363. if FEqualColCountPerRow then
  364. begin
  365. MaxCol := MaxColCount - 1;
  366. for I := 0 to RowCount - 1 do
  367. for J := ColCount[I] to MaxCol do
  368. Cells[J, I] := '';
  369. end;
  370. end;
  371. procedure TCSVDocument.SaveToFile(const AFilename: String);
  372. var
  373. FileStream: TFileStream;
  374. begin
  375. FileStream := TFileStream.Create(AFilename, fmCreate);
  376. try
  377. SaveToStream(FileStream);
  378. finally
  379. FileStream.Free;
  380. end;
  381. end;
  382. procedure TCSVDocument.SaveToStream(AStream: TStream);
  383. var
  384. I, J, MaxCol: Integer;
  385. begin
  386. if not Assigned(FBuilder) then
  387. FBuilder := TCSVBuilder.Create;
  388. FBuilder.AssignCSVProperties(Self);
  389. with FBuilder do
  390. begin
  391. if FEqualColCountPerRow then
  392. MaxCol := MaxColCount - 1;
  393. SetOutput(AStream);
  394. for I := 0 to RowCount - 1 do
  395. begin
  396. if not FEqualColCountPerRow then
  397. MaxCol := ColCount[I] - 1;
  398. for J := 0 to MaxCol do
  399. AppendCell(Cells[J, I]);
  400. AppendRow;
  401. end;
  402. end;
  403. end;
  404. procedure TCSVDocument.AddRow(const AFirstCell: String = '');
  405. begin
  406. FRows.Add(CreateNewRow(AFirstCell));
  407. end;
  408. procedure TCSVDocument.AddCell(ARow: Integer; const AValue: String = '');
  409. begin
  410. ForceRowIndex(ARow);
  411. TCSVRow(FRows[ARow]).AddCell(AValue);
  412. end;
  413. procedure TCSVDocument.InsertRow(ARow: Integer; const AFirstCell: String = '');
  414. begin
  415. if HasRow(ARow) then
  416. FRows.Insert(ARow, CreateNewRow(AFirstCell))
  417. else
  418. AddRow(AFirstCell);
  419. end;
  420. procedure TCSVDocument.InsertCell(ACol, ARow: Integer; const AValue: String);
  421. begin
  422. ForceRowIndex(ARow);
  423. TCSVRow(FRows[ARow]).InsertCell(ACol, AValue);
  424. end;
  425. procedure TCSVDocument.RemoveRow(ARow: Integer);
  426. begin
  427. if HasRow(ARow) then
  428. FRows.Delete(ARow);
  429. end;
  430. procedure TCSVDocument.RemoveCell(ACol, ARow: Integer);
  431. begin
  432. if HasRow(ARow) then
  433. TCSVRow(FRows[ARow]).RemoveCell(ACol);
  434. end;
  435. function TCSVDocument.HasRow(ARow: Integer): Boolean;
  436. begin
  437. Result := (ARow >= 0) and (ARow < FRows.Count);
  438. end;
  439. function TCSVDocument.HasCell(ACol, ARow: Integer): Boolean;
  440. begin
  441. if HasRow(ARow) then
  442. Result := TCSVRow(FRows[ARow]).HasCell(ACol)
  443. else
  444. Result := False;
  445. end;
  446. function TCSVDocument.IndexOfCol(const AString: String; ARow: Integer): Integer;
  447. var
  448. CC: Integer;
  449. begin
  450. CC := ColCount[ARow];
  451. Result := 0;
  452. while (Result < CC) and (Cells[Result, ARow] <> AString) do
  453. Inc(Result);
  454. if Result = CC then
  455. Result := -1;
  456. end;
  457. function TCSVDocument.IndexOfRow(const AString: String; ACol: Integer): Integer;
  458. var
  459. RC: Integer;
  460. begin
  461. RC := RowCount;
  462. Result := 0;
  463. while (Result < RC) and (Cells[ACol, Result] <> AString) do
  464. Inc(Result);
  465. if Result = RC then
  466. Result := -1;
  467. end;
  468. procedure TCSVDocument.Clear;
  469. begin
  470. FRows.Clear;
  471. end;
  472. procedure TCSVDocument.CloneRow(ARow, AInsertPos: Integer);
  473. var
  474. NewRow: TObject;
  475. begin
  476. if not HasRow(ARow) then
  477. Exit;
  478. NewRow := TCSVRow(FRows[ARow]).Clone;
  479. if not HasRow(AInsertPos) then
  480. begin
  481. ForceRowIndex(AInsertPos - 1);
  482. FRows.Add(NewRow);
  483. end else
  484. FRows.Insert(AInsertPos, NewRow);
  485. end;
  486. procedure TCSVDocument.ExchangeRows(ARow1, ARow2: Integer);
  487. begin
  488. if not (HasRow(ARow1) and HasRow(ARow2)) then
  489. Exit;
  490. FRows.Exchange(ARow1, ARow2);
  491. end;
  492. procedure TCSVDocument.UnifyEmbeddedLineEndings;
  493. var
  494. I: Integer;
  495. begin
  496. for I := 0 to FRows.Count - 1 do
  497. TCSVRow(FRows[I]).SetValuesLineEnding(FLineEnding);
  498. end;
  499. procedure TCSVDocument.RemoveTrailingEmptyCells;
  500. var
  501. I: Integer;
  502. begin
  503. for I := 0 to FRows.Count - 1 do
  504. TCSVRow(FRows[I]).TrimEmptyCells;
  505. end;
  506. end.