csvreadwrite.pp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. {
  2. CSV Parser, Builder 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 csvreadwrite;
  32. {$mode objfpc}
  33. {$H+}
  34. interface
  35. uses
  36. Classes, SysUtils, strutils;
  37. Type
  38. TCSVChar = Char;
  39. { TCSVHandler }
  40. TCSVHandler = class(TPersistent)
  41. private
  42. function GetDelimiter: TCSVChar;
  43. function GetLineEnding: String;
  44. function GetQuoteChar: TCSVChar;
  45. procedure SetDelimiter(const AValue: TCSVChar);
  46. procedure SetLineEnding(AValue: String);
  47. procedure SetQuoteChar(const AValue: TCSVChar);
  48. procedure UpdateCachedChars;
  49. protected
  50. // special chars
  51. FDelimiter: AnsiChar;
  52. FQuoteChar: AnsiChar;
  53. FLineEnding: AnsiString;
  54. // cached values to speed up special chars operations
  55. FSpecialChars: TSysCharSet;
  56. FDoubleQuote: AnsiString;
  57. // parser settings
  58. FIgnoreOuterWhitespace: Boolean;
  59. // builder settings
  60. FQuoteOuterWhitespace: Boolean;
  61. // document settings
  62. FEqualColCountPerRow: Boolean;
  63. public
  64. constructor Create; virtual;
  65. procedure Assign(ASource: TPersistent); override;
  66. procedure AssignCSVProperties(ASource: TCSVHandler);
  67. // Delimiter that separates the field, e.g. comma, semicolon, tab
  68. property Delimiter: TCSVChar read GetDelimiter write SetDelimiter;
  69. // Character used to quote "problematic" data
  70. // (e.g. with delimiters or spaces in them)
  71. // A common quotechar is "
  72. property QuoteChar: TCSVChar read GetQuoteChar write SetQuoteChar;
  73. // String at the end of the line of data (e.g. CRLF)
  74. property LineEnding: String read GetLineEnding write SetLineEnding;
  75. // Ignore whitespace between delimiters and field data
  76. property IgnoreOuterWhitespace: Boolean read FIgnoreOuterWhitespace write FIgnoreOuterWhitespace;
  77. // Use quotes when outer whitespace is found
  78. property QuoteOuterWhitespace: Boolean read FQuoteOuterWhitespace write FQuoteOuterWhitespace;
  79. // When reading and writing: make sure every line has the same column count, create empty cells in the end of row if required
  80. property EqualColCountPerRow: Boolean read FEqualColCountPerRow write FEqualColCountPerRow;
  81. end;
  82. // Sequential input from CSV stream
  83. { TCSVParser }
  84. TCSVByteOrderMark = (bomNone, bomUTF8, bomUTF16LE, bomUTF16BE);
  85. TCSVParser = class(TCSVHandler)
  86. private
  87. FFreeStream: Boolean;
  88. // fields
  89. FSourceStream: TStream;
  90. FStrStreamWrapper: TStringStream;
  91. FBOM: TCSVByteOrderMark;
  92. FDetectBOM: Boolean;
  93. // parser state
  94. EndOfFile: Boolean;
  95. EndOfLine: Boolean;
  96. FCurrentChar: AnsiChar;
  97. FCurrentRow: Integer;
  98. FCurrentCol: Integer;
  99. FMaxColCount: Integer;
  100. // output buffers
  101. FCellBuffer: RawByteString;
  102. FWhitespaceBuffer: RawByteString;
  103. procedure ClearOutput;
  104. function GetCurrentCell: String;
  105. // basic parsing
  106. procedure SkipEndOfLine;
  107. procedure SkipDelimiter;
  108. procedure SkipWhitespace;
  109. procedure NextChar;
  110. // complex parsing
  111. procedure ParseCell;
  112. procedure ParseQuotedValue;
  113. // simple parsing
  114. procedure ParseValue;
  115. public
  116. constructor Create; override;
  117. destructor Destroy; override;
  118. // Source data stream
  119. procedure SetSource(AStream: TStream); overload;
  120. // Source data string.
  121. procedure SetSource(const AString: String); overload;
  122. // Rewind to beginning of data
  123. procedure ResetParser;
  124. // Read next cell data; return false if end of file reached
  125. function ParseNextCell: Boolean;
  126. // Current row (0 based)
  127. property CurrentRow: Integer read FCurrentRow;
  128. // Current column (0 based); -1 if invalid/before beginning of file
  129. property CurrentCol: Integer read FCurrentCol;
  130. // Data in current cell
  131. property CurrentCellText: String read GetCurrentCell;
  132. // The maximum number of columns found in the stream:
  133. property MaxColCount: Integer read FMaxColCount;
  134. // Does the parser own the stream ? If true, a previous stream is freed when set or when parser is destroyed.
  135. Property FreeStream : Boolean Read FFreeStream Write FFreeStream;
  136. // Return BOM found in file
  137. property BOM: TCSVByteOrderMark read FBOM;
  138. // Detect whether a BOM marker is present. If set to True, then BOM can be used to see what BOM marker there was.
  139. property DetectBOM: Boolean read FDetectBOM write FDetectBOM default false;
  140. end;
  141. // Sequential output to CSV stream
  142. TCSVBuilder = class(TCSVHandler)
  143. private
  144. FOutputStream: TStream;
  145. FDefaultOutput: TMemoryStream;
  146. FNeedLeadingDelimiter: Boolean;
  147. function GetDefaultOutputAsString: String;
  148. protected
  149. procedure AppendStringToStream(const AString: String; AStream: TStream);
  150. function QuoteCSVString(const AValue: String): String;
  151. public
  152. constructor Create; override;
  153. destructor Destroy; override;
  154. // Set output/destination stream.
  155. // If not called, output is sent to DefaultOutput
  156. procedure SetOutput(AStream: TStream);
  157. // If using default stream, reset output to beginning.
  158. // If using user-defined stream, user should reposition stream himself
  159. procedure ResetBuilder;
  160. // Add a cell to the output with data AValue
  161. procedure AppendCell(const AValue: String);
  162. // Write end of row to the output, starting a new row
  163. procedure AppendRow;
  164. // Default output as memorystream (if output not set using SetOutput)
  165. property DefaultOutput: TMemoryStream read FDefaultOutput;
  166. // Default output in string format (if output not set using SetOutput)
  167. property DefaultOutputAsString: String read GetDefaultOutputAsString;
  168. end;
  169. function ChangeLineEndings(const AString, ALineEnding: String): String;
  170. implementation
  171. const
  172. CsvCharSize = SizeOf(TCSVChar);
  173. CR = #13;
  174. LF = #10;
  175. HTAB = #9;
  176. SPACE = #32;
  177. WhitespaceChars = [HTAB, SPACE];
  178. LineEndingChars = [CR, LF];
  179. Procedure AppendStr(Var Dest : RawByteString; Src : RawByteString); inline;
  180. begin
  181. Dest:=Dest+Src;
  182. end;
  183. procedure RemoveTrailingChars(VAR S: RawByteString; const CSet: TSysCharset);
  184. VAR I,J: LONGINT;
  185. Begin
  186. I:=Length(S);
  187. IF (I>0) Then
  188. Begin
  189. J:=I;
  190. While (j>0) and (S[J] IN CSet) DO DEC(J);
  191. IF J<>I Then
  192. SetLength(S,J);
  193. End;
  194. End;
  195. // The following implementation of ChangeLineEndings function originates from
  196. // Lazarus CodeTools library by Mattias Gaertner. It was explicitly allowed
  197. // by Mattias to relicense it under modified LGPL and include into CsvDocument.
  198. function ChangeLineEndings(const AString, ALineEnding: String): String;
  199. var
  200. I: Integer;
  201. Src: PChar;
  202. Dest: PChar;
  203. DestLength: Integer;
  204. EndingLength: Integer;
  205. EndPos: PChar;
  206. begin
  207. if AString = '' then
  208. Exit(AString);
  209. EndingLength := Length(ALineEnding);
  210. DestLength := Length(AString);
  211. Src := PChar(AString);
  212. EndPos := Src;
  213. Inc(EndPos,DestLength);
  214. while Src < EndPos do
  215. begin
  216. if (Src^ = CR) then
  217. begin
  218. Inc(Src);
  219. if (Src^ = LF) then
  220. begin
  221. Inc(Src);
  222. Inc(DestLength, EndingLength - 2);
  223. end else
  224. Inc(DestLength, EndingLength - 1);
  225. end else
  226. begin
  227. if (Src^ = LF) then
  228. Inc(DestLength, EndingLength - 1);
  229. Inc(Src);
  230. end;
  231. end;
  232. SetLength(Result, DestLength);
  233. Src := PChar(AString);
  234. Dest := PChar(Result);
  235. EndPos := Dest + DestLength;
  236. while (Dest < EndPos) do
  237. begin
  238. if Src^ in LineEndingChars then
  239. begin
  240. for I := 1 to EndingLength do
  241. begin
  242. Dest^ := ALineEnding[I];
  243. Inc(Dest);
  244. end;
  245. if (Src^ = CR) and (Src[1] = LF) then
  246. Inc(Src, 2)
  247. else
  248. Inc(Src);
  249. end else
  250. begin
  251. Dest^ := Src^;
  252. Inc(Src);
  253. Inc(Dest);
  254. end;
  255. end;
  256. end;
  257. { TCSVHandler }
  258. function TCSVHandler.GetDelimiter: TCSVChar;
  259. begin
  260. Result:=FDelimiter;
  261. end;
  262. function TCSVHandler.GetLineEnding: String;
  263. begin
  264. Result:=UTF8Decode(FLineEnding);
  265. end;
  266. function TCSVHandler.GetQuoteChar: TCSVChar;
  267. begin
  268. Result:=FQuoteChar;
  269. end;
  270. procedure TCSVHandler.SetDelimiter(const AValue: TCSVChar);
  271. begin
  272. if FDelimiter <> AValue then
  273. begin
  274. FDelimiter := AValue;
  275. UpdateCachedChars;
  276. end;
  277. end;
  278. procedure TCSVHandler.SetLineEnding(AValue: String);
  279. begin
  280. FLineEnding:=UTF8ENcode(AValue)
  281. end;
  282. procedure TCSVHandler.SetQuoteChar(const AValue: TCSVChar);
  283. begin
  284. if FQuoteChar <> AValue then
  285. begin
  286. FQuoteChar := AValue;
  287. UpdateCachedChars;
  288. end;
  289. end;
  290. procedure TCSVHandler.UpdateCachedChars;
  291. begin
  292. FDoubleQuote := FQuoteChar + FQuoteChar;
  293. FSpecialChars := [CR, LF, FDelimiter, FQuoteChar];
  294. end;
  295. constructor TCSVHandler.Create;
  296. begin
  297. inherited Create;
  298. FDelimiter := ',';
  299. FQuoteChar := '"';
  300. FLineEnding := sLineBreak;
  301. FIgnoreOuterWhitespace := False;
  302. FQuoteOuterWhitespace := True;
  303. FEqualColCountPerRow := True;
  304. UpdateCachedChars;
  305. end;
  306. procedure TCSVHandler.Assign(ASource: TPersistent);
  307. begin
  308. if (ASource is TCSVHandler) then
  309. AssignCSVProperties(ASource as TCSVHandler)
  310. else
  311. inherited Assign(ASource);
  312. end;
  313. procedure TCSVHandler.AssignCSVProperties(ASource: TCSVHandler);
  314. begin
  315. FDelimiter := ASource.FDelimiter;
  316. FQuoteChar := ASource.FQuoteChar;
  317. FLineEnding := ASource.FLineEnding;
  318. FIgnoreOuterWhitespace := ASource.FIgnoreOuterWhitespace;
  319. FQuoteOuterWhitespace := ASource.FQuoteOuterWhitespace;
  320. FEqualColCountPerRow := ASource.FEqualColCountPerRow;
  321. UpdateCachedChars;
  322. end;
  323. { TCSVParser }
  324. procedure TCSVParser.ClearOutput;
  325. begin
  326. FCellBuffer := '';
  327. FWhitespaceBuffer := '';
  328. FCurrentRow := 0;
  329. FCurrentCol := -1;
  330. FMaxColCount := 0;
  331. end;
  332. function TCSVParser.GetCurrentCell: String;
  333. begin
  334. Result:=FCellBuffer
  335. end;
  336. procedure TCSVParser.SkipEndOfLine;
  337. begin
  338. // treat LF+CR as two linebreaks, not one
  339. if (FCurrentChar = CR) then
  340. NextChar;
  341. if (FCurrentChar = LF) then
  342. NextChar;
  343. end;
  344. procedure TCSVParser.SkipDelimiter;
  345. begin
  346. if FCurrentChar = FDelimiter then
  347. NextChar;
  348. end;
  349. procedure TCSVParser.SkipWhitespace;
  350. begin
  351. while FCurrentChar = SPACE do
  352. NextChar;
  353. end;
  354. procedure TCSVParser.NextChar;
  355. begin
  356. if FSourceStream.Read(FCurrentChar, SizeOf(FCurrentChar)) < SizeOf(FCurrentChar) then
  357. begin
  358. FCurrentChar := #0;
  359. EndOfFile := True;
  360. end;
  361. EndOfLine := FCurrentChar in LineEndingChars;
  362. end;
  363. procedure TCSVParser.ParseCell;
  364. begin
  365. FCellBuffer := '';
  366. if FIgnoreOuterWhitespace then
  367. SkipWhitespace;
  368. if FCurrentChar = FQuoteChar then
  369. ParseQuotedValue
  370. else
  371. ParseValue;
  372. end;
  373. procedure TCSVParser.ParseQuotedValue;
  374. var
  375. QuotationEnd: Boolean;
  376. begin
  377. NextChar; // skip opening quotation AnsiChar
  378. repeat
  379. // read value up to next quotation AnsiChar
  380. while not ((FCurrentChar = FQuoteChar) or EndOfFile) do
  381. begin
  382. if EndOfLine then
  383. begin
  384. AppendStr(FCellBuffer, FLineEnding);
  385. SkipEndOfLine;
  386. end else
  387. begin
  388. AppendStr(FCellBuffer, FCurrentChar);
  389. NextChar;
  390. end;
  391. end;
  392. // skip quotation AnsiChar (closing or escaping)
  393. if not EndOfFile then
  394. NextChar;
  395. // check if it was escaping
  396. if FCurrentChar = FQuoteChar then
  397. begin
  398. AppendStr(FCellBuffer, FCurrentChar);
  399. QuotationEnd := False;
  400. NextChar;
  401. end else
  402. QuotationEnd := True;
  403. until QuotationEnd;
  404. // read the rest of the value until separator or new line
  405. ParseValue;
  406. end;
  407. procedure TCSVParser.ParseValue;
  408. begin
  409. while not ((FCurrentChar = FDelimiter) or EndOfLine or EndOfFile or (FCurrentChar = FQuoteChar)) do
  410. begin
  411. AppendStr(FCellBuffer, FCurrentChar);
  412. NextChar;
  413. end;
  414. if FCurrentChar = FQuoteChar then
  415. ParseQuotedValue;
  416. // merge whitespace buffer
  417. if FIgnoreOuterWhitespace then
  418. RemoveTrailingChars(FWhitespaceBuffer, WhitespaceChars);
  419. AppendStr(FWhitespaceBuffer,FCellBuffer);
  420. FWhitespaceBuffer := '';
  421. end;
  422. constructor TCSVParser.Create;
  423. begin
  424. inherited Create;
  425. ClearOutput;
  426. FStrStreamWrapper := nil;
  427. EndOfFile := True;
  428. end;
  429. destructor TCSVParser.Destroy;
  430. begin
  431. if FFreeStream and (FSourceStream<>FStrStreamWrapper) then
  432. FreeAndNil(FSourceStream);
  433. FreeAndNil(FStrStreamWrapper);
  434. inherited Destroy;
  435. end;
  436. procedure TCSVParser.SetSource(AStream: TStream);
  437. begin
  438. If FSourceStream=AStream then exit;
  439. if FFreeStream and (FSourceStream<>FStrStreamWrapper) then
  440. FreeAndNil(FSourceStream);
  441. FSourceStream := AStream;
  442. ResetParser;
  443. end;
  444. procedure TCSVParser.SetSource(const AString: String); overload;
  445. begin
  446. FreeAndNil(FStrStreamWrapper);
  447. FStrStreamWrapper := TStringStream.Create(AString);
  448. SetSource(FStrStreamWrapper);
  449. end;
  450. procedure TCSVParser.ResetParser;
  451. var
  452. b: packed array[0..2] of byte;
  453. n: Integer;
  454. begin
  455. B[0]:=0; B[1]:=0; B[2]:=0;
  456. ClearOutput;
  457. FSourceStream.Seek(0, soFromBeginning);
  458. if FDetectBOM then
  459. begin
  460. if FSourceStream.Read(b[0], 3)<3 then
  461. begin
  462. n:=0;
  463. FBOM:=bomNone;
  464. end
  465. else if (b[0] = $EF) and (b[1] = $BB) and (b[2] = $BF) then begin
  466. FBOM := bomUTF8;
  467. n := 3;
  468. end else
  469. if (b[0] = $FE) and (b[1] = $FF) then begin
  470. FBOM := bomUTF16BE;
  471. n := 2;
  472. end else
  473. if (b[0] = $FF) and (b[1] = $FE) then begin
  474. FBOM := bomUTF16LE;
  475. n := 2;
  476. end else begin
  477. FBOM := bomNone;
  478. n := 0;
  479. end;
  480. FSourceStream.Seek(n, soFromBeginning);
  481. end;
  482. EndOfFile := False;
  483. NextChar;
  484. end;
  485. // Parses next cell; returns True if there are more cells in the input stream.
  486. function TCSVParser.ParseNextCell: Boolean;
  487. var
  488. LineColCount: Integer;
  489. begin
  490. if EndOfLine or EndOfFile then
  491. begin
  492. // Having read the previous line, adjust column count if necessary:
  493. LineColCount := FCurrentCol + 1;
  494. if LineColCount > FMaxColCount then
  495. FMaxColCount := LineColCount;
  496. end;
  497. if EndOfFile then
  498. Exit(False);
  499. // Handle line ending
  500. if EndOfLine then
  501. begin
  502. SkipEndOfLine;
  503. if EndOfFile then
  504. Exit(False);
  505. FCurrentCol := 0;
  506. Inc(FCurrentRow);
  507. end else
  508. Inc(FCurrentCol);
  509. // Skipping a delimiter should be immediately followed by parsing a cell
  510. // without checking for line break first, otherwise we miss last empty cell.
  511. // But 0th cell does not start with delimiter unlike other cells, so
  512. // the following check is required not to miss the first empty cell:
  513. if FCurrentCol > 0 then
  514. SkipDelimiter;
  515. ParseCell;
  516. Result := True;
  517. end;
  518. { TCSVBuilder }
  519. function TCSVBuilder.GetDefaultOutputAsString: String;
  520. var
  521. StreamSize: Integer;
  522. begin
  523. Result := '';
  524. StreamSize := FDefaultOutput.Size;
  525. if StreamSize > 0 then
  526. begin
  527. SetLength(Result, StreamSize);
  528. FDefaultOutput.Position:=0;
  529. FDefaultOutput.ReadBuffer(Result[1], StreamSize);
  530. end;
  531. end;
  532. procedure TCSVBuilder.AppendStringToStream(const AString: String; AStream: TStream);
  533. var
  534. StrLen: Integer;
  535. S : AnsiString;
  536. begin
  537. S:=aString;
  538. StrLen := Length(S);
  539. if StrLen > 0 then
  540. AStream.WriteBuffer(S[1], StrLen);
  541. end;
  542. function TCSVBuilder.QuoteCSVString(const AValue: String): String;
  543. var
  544. I: Integer;
  545. ValueLen: Integer;
  546. NeedQuotation: Boolean;
  547. S : String;
  548. begin
  549. ValueLen := Length(AValue);
  550. NeedQuotation := (AValue <> '') and FQuoteOuterWhitespace
  551. and ((AValue[1] in WhitespaceChars) or (AValue[ValueLen] in WhitespaceChars));
  552. if not NeedQuotation then
  553. for I := 1 to ValueLen do
  554. begin
  555. if AValue[I] in FSpecialChars then
  556. begin
  557. NeedQuotation := True;
  558. Break;
  559. end;
  560. end;
  561. if NeedQuotation then
  562. begin
  563. // double existing quotes
  564. Result := FDoubleQuote;
  565. S:=StringReplace(AValue, FQuoteChar, FDoubleQuote, [rfReplaceAll]);
  566. Insert(S,Result, 2);
  567. end else
  568. Result := AValue;
  569. end;
  570. constructor TCSVBuilder.Create;
  571. begin
  572. inherited Create;
  573. FDefaultOutput := TMemoryStream.Create;
  574. FOutputStream := FDefaultOutput;
  575. end;
  576. destructor TCSVBuilder.Destroy;
  577. begin
  578. FreeAndNil(FDefaultOutput);
  579. inherited Destroy;
  580. end;
  581. procedure TCSVBuilder.SetOutput(AStream: TStream);
  582. begin
  583. if Assigned(AStream) then
  584. FOutputStream := AStream
  585. else
  586. FOutputStream := FDefaultOutput;
  587. ResetBuilder;
  588. end;
  589. procedure TCSVBuilder.ResetBuilder;
  590. begin
  591. if FOutputStream = FDefaultOutput then
  592. FDefaultOutput.Clear;
  593. // Do not clear external FOutputStream because it may be pipe stream
  594. // or something else that does not support size and position.
  595. // To clear external output is up to the user of TCSVBuilder.
  596. FNeedLeadingDelimiter := False;
  597. end;
  598. procedure TCSVBuilder.AppendCell(const AValue: String);
  599. var
  600. CellValue: String;
  601. begin
  602. if FNeedLeadingDelimiter then
  603. FOutputStream.WriteBuffer(FDelimiter, SizeOf(FDelimiter));
  604. CellValue := ChangeLineEndings(AValue, FLineEnding);
  605. CellValue := QuoteCSVString(CellValue);
  606. AppendStringToStream(CellValue, FOutputStream);
  607. FNeedLeadingDelimiter := True;
  608. end;
  609. procedure TCSVBuilder.AppendRow;
  610. begin
  611. AppendStringToStream(FLineEnding, FOutputStream);
  612. FNeedLeadingDelimiter := False;
  613. end;
  614. end.