I have some files (3-5) that i need to compare:
File 1.txt have 1 million strings.
File 2.txt have 10 million strings.
File 3.txt have 5 million strings.
All these files are compared with file keys.txt (10 thousand strings). If line from currently opened file is the same as one of lines from keys.txt, write this line into output.txt (I hope you understand what i mean).
Now i have:
function Thread.checkKeys(sLine: string): boolean;
var
SR: TStreamReader;
line: string;
begin
Result := false;
SR := TStreamReader.Create(sKeyFile); // sKeyFile - Path to file keys.txt
try
while (not(SR.EndOfStream)) and (not(Result))do
begin
line := SR.ReadLine;
if LowerCase(line) = LowerCase(sLine) then
begin
saveStr(sLine);
inc(iMatch);
Result := true;
end;
end;
finally
SR.Free;
end;
end;
procedure Thread.saveStr(sToSave: string);
var
fOut: TStreamWriter;
begin
fOut := TStreamWriter.Create('output.txt', true, TEncoding.UTF8);
try
fOut.WriteLine(sToSave);
finally
fOut.Free;
end;
end;
procedure Thread.updateFiles;
begin
fmMain.flDone.Caption := IntToStr(iFile);
fmMain.flMatch.Caption := IntToStr(iMatch);
end;
And loop with
fInput := TStreamReader.Create(tsFiles[iCurFile]);
while not(fInput.EndOfStream) do
begin
sInput := fInput.ReadLine;
checkKeys(sInput);
end;
fInput.Free;
iFile := iCurFile + 1;
Synchronize(updateFiles);
So, if i compare these 3 files with file key.txt it takes about 4 hours. How to decrease compare time?
An easy solution is to use an associative container to store your keys. This can provide efficient lookup.
In Delphi you can use TDictionary<TKey,TValue> from Generics.Collections. The implementation of this container hashes the keys and provides O(1) lookup.
Declare the container like this:
Keys: TDictionary<string, Boolean>;
// doesn't matter what type you use for the value, we pick Boolean since we
// have to pick something
Create and populate it like this:
Keys := TDictionary<string, Integer>.Create;
SR := TStreamReader.Create(sKeyFile);
try
while not SR.EndOfStream do
Keys.Add(LowerCase(SR.ReadLine), True);
// exception raised if duplicate key found
finally
SR.Free;
end;
Then your checking function becomes:
function Thread.checkKeys(const sLine: string): boolean;
begin
Result := Keys.ContainsKey(LowerCase(sLine));
if Result then
begin
saveStr(sLine);
inc(iMatch);
end;
end;
First of all you should load Keys.txt into for example TStringList. Don't read keys each time from file. The second in such high count loop you shouldn't use procedure/functions calls you should do all checks inline.
Something like this:
Keys:=TStringList.Create;
Keys.LoadFromFile('keys.txt');
fInput := TStreamReader.Create(tsFiles[iCurFile]);
fOut := TStreamWriter.Create('output.txt', true, TEncoding.UTF8);
while not(fInput.EndOfStream) do
begin
sInput := fInput.ReadLine;
if Keys.IndexOf(sInput)>=0 then
begin
fOut.WriteLine(sInput);
inc(iMatch);
end;
end;
fInput.Free;
fOut.Free;
iFile := iCurFile + 1;
Synchronize(updateFiles);
Keys.Free;
Related
I'm trying to make progressbar while deleting files here is my code:
procedure TForm1.Timer1Timer(Sender: TObject);
var
i:Integer;
begin
i:=i+1;
ProgressBar.Max:=DeleteList.Count - i ; //Files = 8192
DeleteFile(GetIniString('List', 'File' + IntToStr(i),'','FileLists.ini'));
ProgressBar.Position:=ProgressBar.Position+1;
end;
Using threads or IFileOperation both involve fairly steep learning curves. Here are a couple of possibilities:
TDirectory method
At Jerry Dodge's prompting I decided to add an example of using TDirectory to
get a list of files and process it in some way, e.g. delete files in the list.
It displays a periodic progress message - see the if i mod 100 = 0 then statement
in the ProcessFiles method. Unfortunately I couldn't find a way to show
a periodic message during the list-building stage because AFAIC TDirectory
doesn't expose the necessary hook to do so.
procedure TForm2.ProcessFileList(FileList : TStringList);
var
i : Integer;
S : String;
begin
for i := 0 to FileList.Count - 1 do begin
// do something with FileList[i], e.g. delete it
S := FileList[i];
DeleteFile(S);
// Display progress
if i mod 100 = 0 then // do something to show progress
Caption := Format('Files processed: %d ', [i]);
// OR, you could use i and FileList.Count to set a trackbar % complete
end;
Caption := Format('Processed: %d files', [FileList.Count]);
end;
procedure TForm2.GetFileList(const Path : String; FileList : TStringList);
var
Files : Types.TStringDynArray;
i : Integer;
begin
Files := TDirectory.GetFiles('C:\Temp');
FileList.BeginUpdate;
try
for i:= 0 to Length(Files) - 1 do
FileList.Add(Files[i]);
finally
FileList.EndUpdate;
end;
end;
procedure TForm2.Button1Click(Sender: TObject);
var
FileList : TStringList;
begin
FileList := TStringList.Create;
try
GetFileList('C:\Temp', FileList);
ProcessFileList(FileList);
Memo1.Lines.Text := FileList.Text;
finally
FileList.Free;
end;
end;
It should be evident that this way of doing it is a lot simpler than using the
traditional, Windows-specific method below, at the expense of loss of some flexibility,
and has the advantage of being cross-platform.
IFileOperation method (Windows-specific)
The Windows API has functionality to retrieve and process a list of files e.g. in a directory and there used to be a trivially-simple-to-use wrapper around this, including a progress animation, in the (antique) v.3 of SysTools library from TurboPower S/Ware, but I'm not sure this wrapper ever made it into the later public domain version. On the face if it, it could also be done using the IFileOperation interface but google has yet to conjure a simple example. Note that an SO answer about this contains the comment "this is a very complex API and you do need to read the documentation carefully".
I attempted to do this myself but soon got out of my depth. Remy Lebeau's answer here to the q I posted when I got stuck shows how to do it, but the TDirectory method above seems vastly easier at my skill level.
Traditional (D7) method (Windows-specific)
In my experience, if you are only looking to process a few hundred thousand files, you should be able to do it, displaying progress as you go, by adding the files to a TStringList and then processing that, with code along the following lines:
procedure GetFileList(const Path : String; Recurse : Boolean; FileList : TStringList);
// Beware that the following code is Windows-specific
var
FileCount : Integer;
procedure GetFilesInner(sPath : String);
var
Path,
AFileName,
Ext: String;
Rec: TSearchRec;
Done: Boolean;
begin
Path := IncludeTrailingBackslash(sPath);
if FindFirst(Path + '*.*', faAnyFile, Rec) = 0 then begin
Done := False;
while not Done do begin
if (Rec.Name <> '.') and (Rec.Name <> '..') then begin
AFileName := Path + Rec.Name;
Ext := LowerCase(ExtractFileExt(AFileName));
if not ((Rec.Attr and faDirectory) = faDirectory) then begin
inc(FileCount);
if FileCount mod 100 = 0 then
//show progress in GUI
;
FileList.Add(AFileName)
end
else begin
if Recurse then
GetFilesInner(AFileName);
end;
end;
Done := FindNext(Rec) <> 0;
end;
FindClose(Rec);
end;
end;
begin
FileCount := 0;
FileList.BeginUpdate;
FileList.Sorted := True;
FileList.Duplicates := dupIgnore; // don't add duplicate filenames to the list
GetFilesInner(Path);
FileList.EndUpdate;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
FileList : TStringList;
FileName : String;
i : Integer;
begin
FileList := TStringList.Create;
try
GetFileList('d:\aaad7', True, FileList);
for i := 0 to FileList.Count - 1 do begin
FileName := FileList[i];
// do something with FileName, e.g. delete the file
if i mod 100 = 0 then
// display progess e.g. by
Caption := IntToStr(i);
end;
Memo1.Lines := FileList;
finally
FileList.Free;
end;
end;
The if [...] mod [...] = 0 then statements are where you can show the two phases' progress howver you want.
Btw, this code was olny intended to get you started. I'm obliged to Jerry Dodge for reminding me that in recent versions of Delphi, there is similar functionality built-in, by way of the TDirectory.GetFiles method so if you are interested in cross-platform and/or accommodate Unicode, you would do better to study the ins and outs of TDirectory and non-Windows-specific routines like TrailingPathDelim.
When you really want to show some progress in a UI when deleting files, you should use threads:
create a thread, which deletes the files
then poll the progress of the deletion thread from the UI
Be careful when using threads, not to access UI parts (like the progressbar) from within the deletion thread. Such things should at least be synchronized.
Is there a way to merge entries from one TIniFile instance to another?
There's no single method to do so. You can do it yourself like this:
Load the INI file, let's call them A and B.
Enumerate the sections in B.
For each section in B, enumerate the name/value pairs in that section.
Add each name/value pair from B into the corresponding section in A.
When complete, save file A, which contains the entries from both files.
The methods that you'll use to enumerate file A are ReadSections and ReadSectionValues.
You'll need to decide what to do about any clashes. That is any names that appear in both files.
Here's a procedure which can merge two INI files together into a new output INI file:
procedure MergeIniFiles(const FromFilename, ToFilename, OutputFilename: String;
const Overwrite: Boolean);
var
IniFrom, IniTo, IniOut: TIniFile;
Sec: TStringList;
Val: TStringList;
X, Y: Integer;
S, N, V: String;
begin
IniFrom:= TIniFile.Create(FromFilename);
IniTo:= TIniFile.Create(ToFilename);
IniOut:= TIniFile.Create(OutputFilename);
Sec:= TStringList.Create;
Val:= TStringList.Create;
try
IniFrom.ReadSections(Sec);
for X := 0 to Sec.Count-1 do begin
S:= Sec[X];
IniFrom.ReadSection(S, Val);
for Y := 0 to Val.Count-1 do begin
N:= Val[Y];
V:= IniFrom.ReadString(S, N, '');
IniOut.WriteString(S, N, V);
end;
end;
IniTo.ReadSections(Sec);
for X := 0 to Sec.Count-1 do begin
S:= Sec[X];
IniTo.ReadSection(S, Val);
for Y := 0 to Val.Count-1 do begin
N:= Val[Y];
V:= IniTo.ReadString(S, N, '');
if Overwrite then begin
IniOut.WriteString(S, N, V);
end else begin
if not IniOut.ValueExists(S, N) then
IniOut.WriteString(S, N, V);
end;
end;
end;
finally
Val.Free;
Sec.Free;
IniOut.Free;
IniTo.Free;
IniFrom.Free;
end;
end;
What I wanted to achieve is to have an ini file in my setup program, which would be placed along with the main executable in the 'Program Files'. This ini will contain the default values for many properties of the application. So the user's actual ini file (ex. in home folder) will read the "factory" defaults from there. This approach is something like OSX's NSUserDefaults. I think that in some cases this is useful instead of just using the default value in inifile.readString(). Thank you all for your answers, I just post the final functions for this purpose...
procedure inifileLoadDefaults(const defaults: TFileName; destination:TIniFile);
var inif: TIniFile;
begin
inif := TIniFile.Create(defaults);
try
inifileLoadDefaults(inif, destination);
finally
inif.Free;
end;
end;
procedure inifileLoadDefaults(const defaults: TIniFile; destination:TIniFile);
var secs, secsVal: TStrings;
i, k: Integer;
begin
secs := TStringList.Create;
secsVal := TStringList.Create;
try
defaults.ReadSections(secs);
for i:=0 to secs.Count -1 do begin
defaults.ReadSection(secs[i], secsVal);
for k:=0 to secsVal.Count -1 do
if not(destination.ValueExists(secs[i], secsVal[k])) then
destination.WriteString(secs[i], secsVal[k], defaults.ReadString(secs[i], secsVal[k], ''));
end;
finally
secsVal.Free;
secs.Free;
end;
end;
This program raises an I/O 104 error on EoF when first entering the while loop.
The purpose of the program is to look up if a username is already taken. The existing usernames are stored in a text file.
procedure TForm1.btnRegisterClick(Sender: TObject);
begin
sCUser := edtUserName.Text;
AssignFile(tNames, 'Names.txt');
begin
try
Reset(tNames);
except
ShowMessage('File not found');
Exit;
end;
end;
rewrite(tNames);
while not EoF(tNames) do // I get a I/O 104 Error here `
begin
Readln(tNames, sLine);
iPosComme := Pos(',', sLine);
sUser := Copy(sLine, 1, iPosComme - 1);
Delete(sLine, 1, iPosComme - 1);
if sCUser = sUser then begin
ShowMessage('Username taken');
end
else
begin
rewrite(tNames);
Writeln(tNames, sCUser + ',' + '0');
CloseFile(tNames);
end;
end;
end;
Remove the call to Rewrite()before Eof(). Even if you were not getting an IO error, your code would still fail because Rewrite() closes the file you opened with Reset() and then it creates a new bank file, so Eof() would always be True.
Update: error 104 is file not open for input, which means Reset() is not opening the file but is not raising an exception (which sounds like an RTL bug if Eof() is raising an exception, indicating that {I+} is active).
In any case, using AssignFile() and related routines is the old way to do file I/O. You should use newer techniques, like FileOpen() with FileRead(), TFileStream with TStreamReader, TStringList, etc...
Update: your loop logic is wrong. You are comparing only the first line. If it does not match the user, you are wiping out the file, writing the user to a new file, closing the file, and then continuing the loop. EoF() will then fail at that point. You need to rewrite your loop to the following:
procedure TForm1.btnRegisterClick(Sender: TObject
var
SCUser, sUser: String;
tNames: TextFile;
iPosComme: Integer;
Found: Boolean;
begin
sCUser := edtUserName.Text;
AssignFile(tNames,'Names.txt');
try
Reset(tNames);
except
ShowMessage('File not found');
Exit;
end;
try
Found := False;
while not EoF(tNames) do
begin
Readln(tNames,sLine);
iPosComme := Pos(',', sLine);
sUser := Copy(sLine ,1,iPosComme -1);
if sCUser = sUser then
begin
ShowMessage('Username taken') ;
Found := True;
Break;
end;
end;
if not Found then
Writeln(tNames,sCUser + ',0');
finally
CloseFile(tNames);
end;
end;
For the sake of completeness, this Version works for me, but it is hard to guess what the code is intended to do. Especially the while loop seems a bit displaced, since the file will contain exactly one line after the rewrite-case has ben hit once.
program wtf;
{$APPTYPE CONSOLE}
{$I+}
uses
SysUtils;
procedure Sample( sCUser : string);
var sUser, sLine : string;
iPosComme : Integer;
tnames : textfile;
begin
AssignFile(tNames,'Names.txt');
try
Reset(tNames);
except
Writeln('File not found');
Exit;
end;
while not EoF(tNames) do
begin
Readln(tNames,sLine);
iPosComme := Pos(',', sLine);
sUser := Copy(sLine ,1,iPosComme -1);
Delete( sLine,1, iPosComme -1);
if sCuser = sUser then begin
Writeln('Username taken') ;
end
else begin
Rewrite(tNames);
Writeln(tNames,sCUser + ',' + '0');
CloseFile(tNames);
Break; // file has been overwritten and closed
end;
end;
end;
begin
try
Sample('foobar');
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
end.
I wrote a version of this method that uses the newer TStreamReader and TStreamWriter classes.
This won't work with Delphi 7 of course, it's just to show how this could be done in newer versions of Delphi.
The code was heavily inspired by Remys answer.
procedure TForm1.btnRegisterClick(Sender: TObject);
var
Stream: TStream;
Reader: TStreamReader;
Writer: TStreamWriter;
Columns: TStringList;
UserName: string;
Found: Boolean;
FileName: string;
Encoding: TEncoding;
begin
FileName := ExpandFileName('Names.txt'); // An absolute path would be even better
UserName := edtUsername.Text;
Found := False;
Encoding := TEncoding.Default; // or another encoding, e.g. TEncoding.Unicode for Unicode
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Reader := TStreamReader.Create(Stream, Encoding);
try
Columns := TStringList.Create;
try
Columns.Delimiter := ',';
Columns.StrictDelimiter := True; // or False, depending on the file format
while not Reader.EndOfStream do
begin
Columns.DelimitedText := Reader.ReadLine;
if Columns.Count > 0 then
begin
if AnsiSameStr(Columns[0], UserName) then // or AnsiSameText if UserName is not case-sensitive
begin
ShowMessage('Username taken') ;
Found := True;
Break;
end;
end;
end;
finally
Columns.Free;
end;
finally
Reader.Free;
end;
finally
Stream.Free;
end;
if not Found then
begin
Writer := TStreamWriter.Create(FileName, True, Encoding);
try
// Warning: This will cause problems when the file does not end with a new line
Writer.WriteLine(UserName + ',0');
finally
Writer.Free;
end;
end;
end;
If performance and memory usage are not a concern:
procedure TForm1.btnRegisterClick(Sender: TObject);
var
Rows: TStringList;
Columns: TStringList;
UserName: string;
Found: Boolean;
FileName: string;
Encoding: TEncoding;
Row: string;
begin
FileName := ExpandFileName('Names.txt'); // An absolute path would be even better
UserName := edtUsername.Text;
Found := False;
Encoding := TEncoding.Default; // or another encoding, e.g. TEncoding.Unicode for Unicode
Rows := TStringList.Create;
try
Rows.LoadFromFile(FileName, Encoding);
Columns := TStringList.Create;
try
Columns.Delimiter := ',';
Columns.StrictDelimiter := True; // or False, depending on the file format
for Row in Rows do
begin
Columns.DelimitedText := Row;
if Columns.Count > 0 then
begin
if AnsiSameStr(Columns[0], UserName) then // or AnsiSameText if UserName is not case-sensitive
begin
ShowMessage('Username taken') ;
Found := True;
Break;
end;
end;
end;
finally
Columns.Free;
end;
if not Found then
begin
Rows.Add(UserName + ',0');
Rows.SaveToFile(FileName, Encoding);
end;
finally
Rows.Free;
end;
end;
This solution can be adapted to Delphi 7 by removing the Encoding variable.
If it's part of a bigger database it should be stored in a real database management system rather than a text file.
I implemented this code but again i am not able to search through the subdirectories .
procedure TFfileSearch.FileSearch(const dirName:string);
begin
//We write our search code here
if FindFirst(dirName,faAnyFile or faDirectory,searchResult)=0 then
begin
try
repeat
ShowMessage(IntToStr(searchResult.Attr));
if (searchResult.Attr and faDirectory)=0 then //The Result is a File
//begin
lbSearchResult.Items.Append(searchResult.Name)
else
begin
FileSearch(IncludeTrailingBackSlash(dirName)+searchResult.Name);
//
end;
until FindNext(searchResult)<>0
finally
FindClose(searchResult);
end;
end;
end;
procedure TFfileSearch.btnSearchClick(Sender: TObject);
var
filePath:string;
begin
lbSearchResult.Clear;
if Trim(edtMask.Text)='' then
MessageDlg('EMPTY INPUT', mtWarning, [mbOK], 0)
else
begin
filePath:=cbDirName.Text+ edtMask.Text;
ShowMessage(filePath);
FileSearch(filePath);
end;
end;
I am giving the search for *.ini files in E:\ drive. so initially filePath is E:*.ini.
But the code does not search the directories in E:\ drive. How to correct it?
Thanks in Advance
You can't apply a restriction to the file extension in the call to FindFirst. If you did so then directories do not get enumerated. Instead you must check for matching extension in your code. Try something like this:
procedure TMyForm.FileSearch(const dirName:string);
var
searchResult: TSearchRec;
begin
if FindFirst(dirName+'\*', faAnyFile, searchResult)=0 then begin
try
repeat
if (searchResult.Attr and faDirectory)=0 then begin
if SameText(ExtractFileExt(searchResult.Name), '.ini') then begin
lbSearchResult.Items.Append(IncludeTrailingBackSlash(dirName)+searchResult.Name);
end;
end else if (searchResult.Name<>'.') and (searchResult.Name<>'..') then begin
FileSearch(IncludeTrailingBackSlash(dirName)+searchResult.Name);
end;
until FindNext(searchResult)<>0
finally
FindClose(searchResult);
end;
end;
end;
procedure TMyForm.FormCreate(Sender: TObject);
begin
FileSearch('c:\windows');
end;
I'd recommend doing as follows:
uses
System.Types,
System.IOUtils;
procedure TForm7.Button1Click(Sender: TObject);
var
S: string;
begin
Memo1.Lines.Clear;
for S in TDirectory.GetFiles('C:\test', '*.bmp', TSearchOption.soAllDirectories) do
Memo1.Lines.Add(S);
Showmessage('Finished!');
end;
I hate those recursive solutions with FindFirst/FindNext and I consider it troublesome that some even forget to use FindClose to clean up resources. So, for the fun of it, a non-recursive solution that should be practical to use...
procedure FindDocs(const Root: string);
var
SearchRec: TSearchRec;
Folders: array of string;
Folder: string;
I: Integer;
Last: Integer;
begin
SetLength(Folders, 1);
Folders[0] := Root;
I := 0;
while (I < Length(Folders)) do
begin
Folder := IncludeTrailingBackslash(Folders[I]);
Inc(I);
{ Collect child folders first. }
if (FindFirst(Folder + '*.*', faDirectory, SearchRec) = 0) then
begin
repeat
if not ((SearchRec.Name = '.') or (SearchRec.Name = '..')) then
begin
Last := Length(Folders);
SetLength(Folders, Succ(Last));
Folders[Last] := Folder + SearchRec.Name;
end;
until (FindNext(SearchRec) <> 0);
FindClose(SearchRec);
end;
{ Collect files next.}
if (FindFirst(Folder + '*.doc', faAnyFile - faDirectory, SearchRec) = 0) then
begin
repeat
if not ((SearchRec.Attr and faDirectory) = faDirectory) then
begin
WriteLn(Folder, SearchRec.Name);
end;
until (FindNext(SearchRec) <> 0);
FindClose(SearchRec);
end;
end;
end;
While it seems to eat a lot of memory because it uses a dynamic array, a recursive method will do exactly the same but recursion happens on the stack! Also, with a recursive method, space is allocated for all local variables while my solution only allocates space for the folder names.
When you check for speed, both methods should be just as fast. The recursive method is easier to remember, though. You can also use a TStringList instead of a dynamic array, but I just like dynamic arrays.
One additional trick with my solution: It can search in multiple folders! I Initialized the Folders array with just one root, but you could easily set it's length to 3, and set Folders[0] to C:\, Folders[1] to D:\ and Folders[2] to E:\ and it will search on multiple disks!
Btw, replace the WriteLn() code with whatever logic you want to execute...
This is worked for me with multi-extension search support:
function GetFilesPro(const Path, Masks: string): TStringDynArray;
var
MaskArray: TStringDynArray;
Predicate: TDirectory.TFilterPredicate;
begin
MaskArray := SplitString(Masks, ',');
Predicate :=
function(const Path: string; const SearchRec: TSearchRec): Boolean
var
Mask: string;
begin
for Mask in MaskArray do
if MatchesMask(SearchRec.Name, Mask) then
exit(True);
exit(False);
end;
Result := TDirectory.GetFiles(Path, Predicate);
end;
Usage:
FileList := TStringList.Create;
FileSearch(s, '.txt;.tmp;.exe;.doc', FileList);
The problem with this file search is that it will loop infinitely, FindClose is like it does not exist.
procedure FindFilePattern(root:String;pattern:String);
var
SR:TSearchRec;
begin
root:=IncludeTrailingPathDelimiter(root);
if FindFirst(root+'*.*',faAnyFile,SR) = 0 then
begin
repeat
Application.ProcessMessages;
if ((SR.Attr and faDirectory) = SR.Attr ) and (pos('.',SR.Name)=0) then
FindFilePattern(root+SR.Name,pattern)
else
begin
if pos(pattern,SR.Name)>0 then Form1.ListBox1.Items.Add(Root+SR.Name);
end;
until FindNext(SR)<>0;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
FindFilePattern('C:\','.exe');
end;
This searches recursively to all folders displaying filenames that contain a certain pattern.
I want to export content of a TQuery to a CSV file without using a 3d part component(Delphi 7). From my knowledge this can not be accomplished with Delphi standard components.
My solution was to save the content in a StringList with a CSV format, and save it to a file.
Is there any comfortable solution?
PS:I don't want to use JvCsvDataSet or any component. Question is: can this be accomplished only with Delphi 7 or higher standard components?
Thank you in advance!
Of course it can.
You just have to do the work to properly output the CSV content (quoting properly, handling embedded quotes and commas, etc.). You can easily write the output using TFileStream, and get the data using the TQuery.Fields and TQuery.FieldCount properly.
I'll leave the fancy CSV quoting and special handling to you. This will take care of the easy part:
var
Stream: TFileStream;
i: Integer;
OutLine: string;
sTemp: string;
begin
Stream := TFileStream.Create('C:\Data\YourFile.csv', fmCreate);
try
while not Query1.Eof do
begin
// You'll need to add your special handling here where OutLine is built
OutLine := '';
for i := 0 to Query.FieldCount - 1 do
begin
sTemp := Query.Fields[i].AsString;
// Special handling to sTemp here
OutLine := OutLine + sTemp + ',';
end;
// Remove final unnecessary ','
SetLength(OutLine, Length(OutLine) - 1);
// Write line to file
Stream.Write(OutLine[1], Length(OutLine) * SizeOf(Char));
// Write line ending
Stream.Write(sLineBreak, Length(sLineBreak));
Query1.Next;
end;
finally
Stream.Free; // Saves the file
end;
end;
The original question asked for a solution using a StringList. So it would be something more like this. It will work with any TDataSet, not just a TQuery.
procedure WriteDataSetToCSV(DataSet: TDataSet, FileName: String);
var
List: TStringList;
S: String;
I: Integer;
begin
List := TStringList.Create;
try
DataSet.First;
while not DataSet.Eof do
begin
S := '';
for I := 0 to DataSet.FieldCount - 1 do
begin
if S > '' then
S := S + ',';
S := S + '"' + DataSet.Fields[I].AsString + '"';
end;
List.Add(S);
DataSet.Next;
end;
finally
List.SaveToFile(FileName);
List.Free;
end;
end;
You can add options to change the delimiter type or whatever.
This is like the Rob McDonell solution but with some enhancements: header, escape chars, enclosure only when required, and ";" separator.
You can easily disable this enhancements if not required.
procedure SaveToCSV(DataSet: TDataSet; FileName: String);
const
Delimiter: Char = ';'; // In order to be automatically recognized in Microsoft Excel use ";", not ","
Enclosure: Char = '"';
var
List: TStringList;
S: String;
I: Integer;
function EscapeString(s: string): string;
var
i: Integer;
begin
Result := StringReplace(s,Enclosure,Enclosure+Enclosure,[rfReplaceAll]);
if (Pos(Delimiter,s) > 0) OR (Pos(Enclosure,s) > 0) then // Comment this line for enclosure in every fields
Result := Enclosure+Result+Enclosure;
end;
procedure AddHeader;
var
I: Integer;
begin
S := '';
for I := 0 to DataSet.FieldCount - 1 do begin
if S > '' then
S := S + Delimiter;
S := S + EscapeString(DataSet.Fields[I].FieldName);
end;
List.Add(S);
end;
procedure AddRecord;
var
I: Integer;
begin
S := '';
for I := 0 to DataSet.FieldCount - 1 do begin
if S > '' then
S := S + Delimiter;
S := S + EscapeString(DataSet.Fields[I].AsString);
end;
List.Add(S);
end;
begin
List := TStringList.Create;
try
DataSet.DisableControls;
DataSet.First;
AddHeader; // Comment if header not required
while not DataSet.Eof do begin
AddRecord;
DataSet.Next;
end;
finally
List.SaveToFile(FileName);
DataSet.First;
DataSet.EnableControls;
List.Free;
end;
end;
Delphi does not provide any built-in access to .csv data.
However, following the VCL TXMLTransform paradigm, I wrote a TCsvTransform class helper that will translate a .csv structure to /from a TClientDataSet.
As for the initial question that was to export a TQuery to .csv, a simple TDataSetProvider will make the link between TQuery and TClientDataSet.
For more details about TCsvTransform, cf http://didier.cabale.free.fr/delphi.htm#uCsvTransform