I have a text file with user information stored in it line by line. Each line is in the format: UserID#UserEmail#UserPassword with '#' being the delimiter.
I have tried to use this coding to perform the task:
var sl:TStringList;
begin
sl:=TStringList.Create;
sl.LoadFromFile('filename');
sl.Delete(Index);
sl.SaveToFile('filename');
sl.free;
end;
But I'm not sure what to put in the "index" space.
Is there any way I can receive the User ID as input and then delete the line of text from the text file that has this user ID in? Any help would be appreciated.
You can set the NameValueSeparator to # then use IndexOfName to find the user, as long as the username is the first value in the file.
sl.NameValueSeparator := '#';
Index := sl.IndexOfName('455115')
So in your example, like so
var sl:TStringList;
begin
sl:=TStringList.Create;
sl.LoadFromFile('filename');
sl.NameValueSeparator := '#';
Index := sl.IndexOfName('455115')
if (Index <> -1) then
begin
sl.Delete(Index);
sl.SaveToFile('filename');
end;
sl.free;
end;
This may be slow on large files as IndexOfName loops though each line in the TStringList and checks each string in turn until it finds a match.
Disclaimer: Tested/ works with Delphi 2007, Delphi 7 may be diffrent.
I don't see why so many people make this so hard. It is quite simple:
function ShouldDeleteLine(const UserID, Line: string): Boolean;
begin
// Remember: Pos(Needle, Haystack)
Result := Pos(UserID + '#', Line) = 1; // always 1-based!
end;
procedure DeleteLinesWithUserID(const FileName, UserID: string);
var
SL: TStringList;
I: Integer;
begin
if not FileExists(FileName) then
Exit;
SL := TStringList.Create;
try
SL.LoadFromFile(FileName); // Add exception handling for the
// case the file does not load properly.
// Always work backward when deleting items, otherwise your index
// may be off if you really delete.
for I := SL.Count - 1 downto 0 do
if ShouldDeleteLine(SL[I], UserID) then
begin
SL.Delete(I);
// if UserID is unique, you can uncomment the following line.
// Break;
end;
SL.SaveToFile(FileName);
finally
SL.Free;
end;
end;
As Arioch'The says, if you save to the same file name, you risk losing your data when the save fails, so you can do something like
SL.SaveToFile(FileName + '.dup');
if FileExists(FileName + '.old') then
DeleteFile(FileName + '.old');
RenameFile(FileName, FileName + '.old');
RenameFile(FileName + '.dup', FileName);
That keeps a backup of the original file as FileName + '.old'.
Explanations
Working backward
Why work backward? Because if you have the following items
A B C D E F G
^
And you delete the item at ^, then the following items will shift downward:
A B C E F G
^
If you iterate forward, you will now point to
A B C E F G
^
and E is never examined. If you go backward, then you will point to:
A B C E F G
^
Note that E, F and G were examined already, so now you will indeed examine the next item, C, and you won't miss any. Also, if you go upward using 0 to Count - 1, and delete, Count will become one less and at the end, you will try to access past the boundary of the list. This can't happen if you work backwards using Count - 1 downto 0.
Using + '#'
If you append '#' and test for Pos() = 1, you will be sure to catch the entire UserID up to the delimiter, and not a line with a user ID that only contains the UserID you are looking for. IOW, if UserID is 'velthuis', you don't want to delete lines like 'rudyvelthuis#rvelthuis01#password' or 'velthuisresidence#vr#password2', but you do want to delete 'velthuis#bla#pw3'.
E.g. when looking for a user name, you look for '#' + UserName + '#' for the same reason.
There is the only way to actually "delete a line from the text file" - that is to create a new file with changed content, to REWRITE it.
So you better just do it explicitly.
And don't you forget about protecting from errors. Your current code might just destroy the file and leak memory, if any error occurs...
var sl: TStringList;
s, prefix: string;
i: integer; okay: Boolean;
fs: TStream;
begin
prefix := 'UserName' + '#';
okay := false;
fs := nil;
sl:=TStringList.Create;
Try /// !!!!
sl.LoadFromFile('filename');
fs := TFileStream.Create( 'filename~new', fmCreate or fmShareExclusive );
for i := 0 to Prev(sl.Count) do begin
s := sl[ i ];
if AnsiStartsStr( prefix, Trim(s) ) then
continue; // skip the line - it was our haunted user
s := s + ^M^J; // add end-of-line marker for saving to file
fs.WriteBuffer( s[1], length(s)*SizeOf(s[1]) );
end;
finally
fs.Free;
sl.Free;
end;
// here - and only here - we are sure we successfully rewritten
// the fixed file and only no are able to safely delete old file
if RenameFile( 'filename' , 'filename~old') then
if RenameFile( 'filename~new' , 'filename') then begin
okay := true;
DeleteFile( 'filename~old' );
end;
if not okay then ShowMessage(' ERROR!!! ');
end;
Note 1: See if check for username should be case-sensitive or case-ignoring:
http://www.freepascal.org/docs-html/rtl/strutils/ansistartsstr.html
http://www.freepascal.org/docs-html/rtl/strutils/ansistartstext.html
Note 2: in Delphi 7 SizeOf( s[1] ) is always equal to one because string is an alias to AnsiString. But in newer Delphi version it is not. It might seems tedious and redundant - but it might save a LOT of headache in future. Even better would be to have a temporary AnsiString type variable like a := AnsiString( s + ^m^J ); fs.WriteBuffer(a[1],Length(a));
So far everyone has been suggesting the use for a For..Then Loop but can I suggest a Repeat..While.
The traditional For..Loop is a good option but could be inefficient if you have a long list of Usernames (they are usually unique). Once found and deleted the For Loop continues until the end of the list. That's ok if you have a small list but if you have 500,000 Usernames and the one you want is at position 10,000 there is no reason to continue beyond that point.
Therefore, try this.
Function DeleteUser(Const TheFile: String; Const TheUserName: String): Boolean;
Var
CurrentLine: Integer;
MyLines: TStringlist;
Found: Boolean;
Eof: Integer;
Begin
MyLines := TStringlist.Create;
MyLines.LoadFromFile(TheFile);
CurrentLine := 0;
Eof := Mylines.count - 1;
Found := false;
Repeat
If Pos(UpperCase(TheUserName), UpperCase(MyLines.Strings[CurrentLine])) = 1 Then
Begin
MyLines.Delete(CurrentLine);
Found := True;
End;
Inc(CurrentLine);
Until (Found) Or (CurrentLine = Eof); // Jump out when found or End of File
MyLines.SaveToFile(TheFile);
MyLines.Free;
result := Found;
End;
Once called the function returns True or False indicating the Username was deleted or not.
If Not DeleteUsername(TheFile,TheUsername) then
ShowMessage('User was not found, what were you thinking!');
Just for fun, here's a compact solution, which I like for its readability.
const fn = 'myfile.txt';
procedure DeleteUser(id: integer);
var s:string; a:TStringDynArray;
begin
for s in TFile.ReadAllLines(fn) do
if not s.StartsWith(id.ToString + '#') then
a := a + [s];
TFile.WriteAllLines(fn, a);
end;
Obviously it's not the most efficient solution. This could run faster by not appending single items to the array, or by caching the search string.
And to search for other fields, you could use s.split(['#'])[0] to find the username, s.split(['#'])[1] for email, etc.
For those who like one-liners. This works too:
const fn = 'users.txt';
procedure DeleteUserRegExp(id: string);
begin
TFile.WriteAllText(fn,TRegEx.Replace(TFile.ReadAllText(fn),''+id+'\#.*\r\n',''))
end;
Explanation
It loads the content of a file into a string.
The string is sent to TRegEx.Replace
The regular expression searches for the username followed by the hash sign, then any character, and then a CRLF. It replaces it with an empty string.
The resulting string is then written to the original file
This is just for fun though, because I saw long code where I thought that this would be possible with a single line of code.
Related
So i have a 'downloads' folder where i put everything i download in my day by day work. You know we always automate everything, so I'm trying to build a simply app to run everyday to delete files older than 30 days, as i have to do this manually from time to time to avoid the folder become too big.
Here is my code :
function TForm1.deleteOldDownloads: boolean;
var
f: string;
i, d: Integer;
var
sl: tstringlist;
begin
try
FileListBox1.Directory := '\\psf\home\downloads';
FileListBox1.refresh;
sl := tstringlist.create;
for i := 0 to FileListBox1.items.count - 1 do
begin
f := FileListBox1.Directory + '\' + FileListBox1.items[i];
if fileexists(f) then
d := daysbetween(FileAge(f), now)
else
d := 0;
if d > 30 then // problem is here, d is always a big number, not the actually age of file
sl.Add(f);
end;
if sl.count > 0 then
begin
for i := 0 to sl.count do
begin
f := sl[i];
deletefile(f);
end;
end;
sl.Free;
except
on e: Exception do
begin
end;
end;
Problem is "d" variable is returning very big numbers like 1397401677, even if the file has only 1 day.
The only detail here is i run Windows in a Parallels virtual machine and the "\psf\home\downloads" folder is on Mac, but i can access this folderl normally using Windows Explorer, so for Delphi is like a regular local folder.
What am i missing ?
Did you read the documentation for FileAge? The first day in programming school, you are taught "When you start using a new function or API, you begin by reading its documentation." In this case, the function's documentation says
The [one-argument] overloaded version of FileAge is deprecated.
So you are using a function you shouldn't be using.
Still, this function should probably still work.
But what do you expect it to return? Well, obviously the thing that the docs say it should return:
The first overload returns an integer that represents the OS time stamp of the file. The result can be later converted to a TDateTime using the FileDateToDateTime function.
But when you use this in DaysBetween, you assume it already is a TDateTime!
Why is FileAge returning unexpected values?
It isn't. It is probably returning exactly the thing its documentation says it should return.
You are using the older version of FileAge() that returns a timestamp in DOS numeric format, but you are treating it as a TDateTime, which it is not. As the FileAge documentation says:
The first overload returns an integer that represents the OS time stamp of the file. The result can be later converted to a TDateTime using the FileDateToDateTime() function.
So, do what the documentation says to do, eg:
var
age: Integer;
age := FileAge(f);
if age <> -1 then
d := DaysBetween(FileDateToDateTime(age), Now)
Otherwise, use the newer version of FileAge() that outputs a TDateTime to begin with, eg:
var
dt: TDateTime;
if FileAge(f, dt) then
d := DaysBetween(dt, Now)
This is NOT a direct answer to your question, but I cannot post it as a comment.
So, the thing is, you should never delete user files directly. What if you make a mistake? What if the user of your program makes a mistake?
Always delete files to Recycle Bin:
{--------------------------------------------------------------------------------------------------
DELETE FILE
Deletes a file/folder to RecycleBin.
Old name: Trashafile
Note related to UNC: The function won't move a file to the RecycleBin if the file is UNC. MAYBE it was moved to the remote's computer RecycleBin
--------------------------------------------------------------------------------------------------}
function RecycleItem(CONST ItemName: string; CONST DeleteToRecycle: Boolean= TRUE; CONST ShowConfirm: Boolean= TRUE; CONST TotalSilence: Boolean= FALSE): Boolean;
VAR
SHFileOpStruct: TSHFileOpStruct;
begin
FillChar(SHFileOpStruct, SizeOf(SHFileOpStruct), #0);
SHFileOpStruct.wnd := Application.MainForm.Handle; { Others are using 0. But Application.MainForm.Handle is better because otherwise, the 'Are you sure you want to delete' will be hidden under program's window }
SHFileOpStruct.wFunc := FO_DELETE;
SHFileOpStruct.pFrom := PChar(ItemName+ #0); { ATENTION! This last #0 is MANDATORY. See this for details: http://stackoverflow.com/questions/6332259/i-cannot-delete-files-to-recycle-bin - Although this member is declared as a single null-terminated string, it is actually a buffer that can hold multiple null-delimited file names. Each file name is terminated by a single NULL character. The last file name is terminated with a double NULL character ("\0\0") to indicate the end of the buffer }
SHFileOpStruct.pTo := NIL;
SHFileOpStruct.hNameMappings := NIL;
if DeleteToRecycle
then SHFileOpStruct.fFlags:= SHFileOpStruct.fFlags OR FOF_ALLOWUNDO;
if TotalSilence
then SHFileOpStruct.fFlags:= SHFileOpStruct.fFlags OR FOF_NO_UI
else
if NOT ShowConfirm
then SHFileOpStruct.fFlags:= SHFileOpStruct.fFlags OR FOF_NOCONFIRMATION;
Result:= SHFileOperation(SHFileOpStruct)= 0;
//DEBUG ONLY if Result<> 0 then Mesaj('last error: ' + IntToStr(Result)+ CRLF+ 'last error message: '+ SysErrorMessage(Result));
//if fos.fAnyOperationsAborted = True then Result:= -1;
end;
Also, you don't need that obsolete control to get the files in a folder. You can use this:
{ FIND FILES }
function ListFilesOf(CONST aFolder, FileType: string; CONST ReturnFullPath, DigSubdirectories: Boolean): TStringList;
{ If DigSubdirectories is false, it will return only the top level files,
else it will return also the files in subdirectories of subdirectories.
If FullPath is true the returned files will have full path.
FileType can be something like '*.*' or '*.exe;*.bin'
Will show also the Hidden/System files.
Source Marco Cantu Delphi 2010 HandBook
// Works with UNC paths}
VAR
i: Integer;
s: string;
SubFolders, filesList: TStringDynArray;
MaskArray: TStringDynArray;
Predicate: TDirectory.TFilterPredicate;
procedure ListFiles(CONST aFolder: string);
VAR strFile: string;
begin
Predicate:=
function(const Path: string; const SearchRec: TSearchRec): Boolean
VAR Mask: string;
begin
for Mask in MaskArray DO
if System.Masks.MatchesMask(SearchRec.Name, Mask)
then EXIT(TRUE);
EXIT(FALSE);
end;
// Long paths will raise an EPathTooLongexception exception, so we simply don't process those folders
if Length(aFolder) > MAXPATH
then exit;
filesList:= TDirectory.GetFiles (aFolder, Predicate);
for strFile in filesList DO
if strFile<> '' { Bug somewhere here: it returns two empty entries ('') here. Maybe the root folder? }
then Result.Add(strFile);
end;
begin
{ I need this in order to prevent the EPathTooLongexception (reported by some users) }
if aFolder.Length >= MAXPATH then
begin
MesajError('Path is longer than '+ IntToStr(MAXPATH)+ ' characters!');
EXIT(NIL);
end;
if NOT System.IOUtils.TDirectory.Exists (aFolder)
then RAISE exception.Create('Folder does not exist! '+ CRLF+ aFolder);
Result:= TStringList.Create;
{ Split FileType in subcomponents }
MaskArray:= System.StrUtils.SplitString(FileType, ';');
{ Search the parent folder }
ListFiles(aFolder);
{ Search in all subfolders }
if DigSubdirectories then
begin
SubFolders:= TDirectory.GetDirectories(aFolder, TSearchOption.soAllDirectories, NIL);
for s in SubFolders DO
begin
if ccIO.DirectoryExists(s) { This solves the problem caused by broken 'Symbolic Link' folders }
then ListFiles(s);
end;
end;
{ Remove full path }
if NOT ReturnFullPath then
for i:= 0 to Result.Count-1 DO
Result[i]:= TPath.GetFileName(Result[i]);
end;
The code above is from: https://github.com/GodModeUser/Delphi-LightSaber
I'm adding strings with objects (also strings) to a TComboBox, but getting corrupted strings when trying to retrieve them later.
This is how I'm adding them:
var
i: Integer;
sl: TStringList;
c: Integer;
s: PChar;
begin
for i := 1 to tblCalls.FieldCount do
if tblCalls.Fields[i - 1].Tag = 1 then
ListBox1.Items.Append(tblCalls.Fields[i - 1].FieldName);
sl := TStringList.Create;
try
LoadStyles(TStrings(sl));
for c := 0 to sl.Count - 1 do
begin
s := PChar(sl.Values[sl.Names[c]]);
ComboBox1.Items.AddObject(sl.Names[c], TObject(s));
end;
finally
sl.Free;
end;
end;
procedure LoadStyles(var AStylesList: TStrings);
var
f, n: String;
filelist: TStringDynArray;
begin
f := ExtractFilePath(ParamStr(0)) + 'Styles';
if (not DirectoryExists(f)) then
Exit;
filelist := TDirectory.GetFiles(f);
for f in filelist do
begin
n := ChangeFileExt(ExtractFileName(f), EmptyStr);
AStylesList.Add(n + '=' + f);
end;
end;
..and this is where I'm trying to retrieve a string object:
procedure TfrmOptions.ComboBox1Change(Sender: TObject);
var
si: TStyleInfo;
i: Integer;
s: String;
begin
i := TComboBox(Sender).ItemIndex;
s := PChar(TComboBox(Sender).Items.Objects[i]);
Showmessage(s); // --> Mostly shows a corrupted string (gibberish chars)
if (TStyleManager.IsValidStyle(s, si)) then
begin
if (not MatchStr(s, TStyleManager.StyleNames)) then
TStyleManager.LoadFromFile(s);
TStyleManager.TrySetStyle(si.Name);
end;
end;
I suspect that its something with the way I'm adding them. Perhaps I need to allocate memory at:
s := PChar(sl.Values[sl.Names[c]]);
Not sure. Looking at the help on StrNew, NewStr and StrAlloc, it says that those functions are deprecated. Can you help point out whats wrong?
There's nothing to keep the string alive. When you write:
s := PChar(sl.Values[sl.Names[c]]);
an implicit local variable of type string is created to hold whatever sl.Values[sl.Names[c]] evaluates to. That local variable goes out of scope, as far as the compiler is aware, nothing references it, and the string object is destroyed.
In fact, it's even worse than that. Because the assignment above happens in a loop, there is only one implicit local variable. Each time round the loop, the string that you asked the combo box to remember is destroyed.
You need to find a way to extend the lifetime of the string. You could do it like this:
var
StrPtr: ^string;
....
for c := 0 to sl.Count - 1 do
begin
New(StrPtr);
StrPtr^ := sl.Values[sl.Names[c]];
ComboBox1.Items.AddObject(sl.Names[c], TObject(StrPtr));
end;
Then when you need to access the string you can do so like this:
var
StrPtr: ^string;
....
TObject(StrPtr) := TComboBox(Sender).Items.Objects[i];
// do something with StrPtr^
When you clear the combo box you must also run through each item and call Dispose on the pointer.
Having said that, it's going to be much easier not to do it that way. Stop trying to force strings into the TObject data associated with each item. Instead keep a parallel string list containing these strings. When you need to look up a name look it up in that list rather than in the combo box.
I know this is an old question but I came across this problem again and rather than use the separate string list I used an object with a string value (I think someone suggested it in a comment) as follows:
Declare a type as TObject with a string value:
TStringObject = class(TObject)
StringValue : string;
end;
Then when adding your items declare a local var of TStringObject and create a new instance for each item:
var
strObj : TStringObject
begin
...
for c := 0 to sl.Count - 1 do
begin
strObj := TStringObject.Create;
strObj.StringValue := sl.Values[sl.Names[c]];
ComboBox1.Items.AddObject(sl.Names[c], strObj);
end;
And when retrieving the values:
s := TStringObject(TComboBox(Sender).Items.Objects[i]).StringValue;
As #Dejan Dozet mentions in the comments - you should always free the data objects before freeing the TStringList!
Well, I have an edit that takes only numbers (no letters, no symbols, only numbers). The problem is that i don't want the user to put 0 as the first digit, for example (0239847).
I was thinking of making a variable "x" --> byte which will count the lenght of the edit and delete the first digit, if it is 0:
var l:length; number:string100;
begin
l:length(edit1.text);
Now if the first digit is 0 (0239847), then delete it, BUT if there are not other numbers (only 0), leave it as it is.
Omg, i finally found the easiest way to do this:
procedure TForm1.Edit1Change(Sender: TObject);
var digit1:string;
begin
digit1:=edit1.Text;
if (digit1='00') or (digit1='01') or (digit1='02') or (digit1='03') or (digit1='04') or
(digit1='05') or (digit1='06') or (digit1='07') or (digit1='08') or (digit1='09') then
edit1.Text:=clear; //or edit1.text:=0; it's the same
end;
end.
The can still copy and paste numbers, but i don't mind
It was a pretty silly question, because the answer was easy and obvious, but if you are way too tired, it is hard to find it, i am sorry for getting you busy for this, Andreas, thanks for your help and tips.
It is hard to do this well. The most ambitious approach would be to do a lot of logic on key down, paste, etc., like in this answer. You'd be surprised how many cases there are to handle!
This works pretty well, I think:
type
TEdit = class(StdCtrls.TEdit)
...
procedure TEdit.KeyPress(var Key: Char);
function InvalidKey: boolean;
begin
InvalidKey :=
(
(Key = '0') and
(
((SelStart = 0) and (Length(Text) - SelLength <> 0))
or
((SelStart = 1) and (Text[1] = '0'))
)
)
or
(
not (Key in ['0'..'9', #8, ^Z, ^X, ^C, ^V])
)
end;
begin
inherited;
if InvalidKey then
begin
beep;
Key := #0;
end else if (SelStart = 1) and (Text[1] = '0') then
begin
Text := Copy(Text, 2);
end;
end;
procedure TEdit.WMPaste(var Message: TWMPaste);
var
ClipbrdText: string;
function ValidText: boolean;
var
i: Integer;
begin
result := true;
for i := 1 to Length(ClipbrdText) do
if not (ClipbrdText[i] in ['0'..'9']) then
begin
result := false;
break;
end;
end;
function RemoveLeadingZeros(const S: string): string;
var
NumLeadingZeros: integer;
i: Integer;
begin
NumLeadingZeros := 0;
for i := 1 to Length(S) do
if S[i] = '0' then
inc(NumLeadingZeros)
else
break;
result := Copy(S, NumLeadingZeros + 1);
end;
begin
if Clipboard.HasFormat(CF_TEXT) then
begin
ClipbrdText := Clipboard.AsText;
if not ValidText then
begin
beep;
Exit;
end;
if SelStart = 0 then
ClipbrdText := RemoveLeadingZeros(ClipbrdText);
SelText := ClipbrdText;
end
else
inherited;
end;
This looks complicated, but all this logic is necessary. For instance, the code handles all these cases well:
You can only enter digits.
If SelStart = 0 and not every character is selected, you cannot enter 0, because then you would get a leading zero.
If SelStart = 0 and every character is selected, you can enter 0. Because you should be able to enter this valid number.
A special case: If there are zero characters, then of course zero characters are selected, so by simply logic you will be able to enter 0 from the state of the empty string!
If SelStart = 1 and the Text[1] = '0' [if SelStart = 1, then Text[1] exists, so we rely on boolean short-circuit evaluation here], then you cannot enter 0.
If the contents of the edit field is 0 and you add a non-zero character after the zero, then the zero is removed (and there is no interference with the caret position).
You can only paste digits. If SelStart = 0, leading zeros (in the clipboard data) are discarded.
In fact, to make the behaviour perfect, you need even more code. For instance, suppose that the contents of the edit field is 123000456. As it is now, you can select the first three characters (123) and press Backspace or Delete to obtain the invalid text 000456. The solution is to add extra code that removes leading zeroes resulting from either of these two operations. Update: I have been doing some thinking, and have come to realise that it is probably best not to implement this (why?).
One shortcut, however, would be to 'prettify' the contents of the edit when it isn't focused, because when it isn't, you are much less likely to shoot the user in the foot by doing something with the contents of the edit.
For instance, you can do something like
function PrettifyNumber(const S: string): string;
var
NumLeadingZeros: integer;
i: Integer;
begin
NumLeadingZeros := 0;
for i := 1 to Length(S) do
if S[i] = '0' then
inc(NumLeadingZeros)
else
break;
result := Copy(S, NumLeadingZeros + 1);
if length(result) = 0 then
result := S;
end;
and then replace the contents of the edit field by its prettified version when it loses keyboard focus. Of course, you should consider adding this functionality to a derived edit class, but for testing you can simply use the OnExit event:
procedure TForm1.Edit1Exit(Sender: TObject);
begin
Edit1.Text := PrettifyNumber(Edit1.Text);
end;
I suppose that you want this functionality in a lot of edits in a major application (or not so major). So you will in fact write your own subclassed control. Then I suppose you will also need other types of verification than simply removal of leading zeros? You thus have to integrate this code nicely with the rest of the code. If in fact you will use this edit 'subclass' everywhere in a big application, you could also consider adding a shortcut, to the edit control class, so that you can prettify/verify the text on demand, and not only on exit (which is particularly difficult if the edit control is alone on its form!)
Update
On request, this is a simple solution that works badly:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if (Key = '0') and (Edit1.SelStart = 0) then Key := #0;
end;
I leave it as an exercise to figure out the issues with this approach. If you fix the issues, one by one, as you discover them, you will end up with my long code above!
You could use TmaskEdit. This force the user to input the numbers. After add eventchange for delete first digit if 0.
Example.dfm
.
.
object MaskEdit1: TMaskEdit
Left = 352
Top = 8
Width = 120
Height = 21
EditMask = '0999999;0;_'
MaxLength = 7
TabOrder = 3
OnChange = MaskEdit1Change
end
.
.
Example.pas
.
.
Procedure TForm1.MaskEdit1Change(Sender: TObject);
Begin
If Length(TMaskEdit(Sender).Text) > 1 Then
TMaskEdit(Sender).Text := IntToStr(StrToInt(TMaskEdit(Sender).Text));
End;
.
.
I have written a Delphi function that loads data from a .dat file into a string list. It then decodes the string list and assigns to a string variable. The contents of the string use the '#' symbol as a separator.
How can I then take the contents of this string and then assign its contents to local variables?
// Function loads data from a dat file and assigns to a String List.
function TfrmMain.LoadFromFile;
var
index, Count : integer;
profileFile, DecodedString : string;
begin
// Open a file and assign to a local variable.
OpenDialog1.Execute;
profileFile := OpenDialog1.FileName;
if profileFile = '' then
exit;
profileList := TStringList.Create;
profileList.LoadFromFile(profileFile);
for index := 0 to profileList.Count - 1 do
begin
Line := '';
Line := profileList[Index];
end;
end;
After its been decoded the var "Line" contains something that looks like this:
example:
Line '23#80#10#2#1#...255#'.
Not all of the values between the separators are the same length and the value of "Line" will vary each time the function LoadFromFile is called (e.g. sometimes a value may have only one number the next two or three etc so I cannot rely on the Copy function for strings or arrays).
I'm trying to figure out a way of looping through the contents of "Line", assigning it to a local variable called "buffer" and then if it encounters a '#' it then assigns the value of buffer to a local variable, re-initialises buffer to ''; and then moves onto the next value in "Line" repeating the process for the next parameter ignoring the '#' each time.
I think I have been scratching around with this problem for too long now and I cannot seem to make any progress and need a break from it. If anyone would care to have a look, I would welcome any suggestions on how this might be achieved.
Many Thanks
KD
You need a second TStringList:
lineLst := TStringList.Create;
try
lineLst.Delimiter := '#';
lineLst.DelimitedText := Line;
...
finally
lineLst.Free;
end;
Depending on your Delphi version you can set lineLst.StrictDelimiter := true in case the line contains spaces.
You can do something like this:
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, StrUtils;
var
S : string;
D : string;
begin
S := '23#80#10#2#1#...255#';
for D in SplitString(S,'#') do //SplitString is in the StrUtils unit
writeln(D);
readln;
end.
You did not tag your Delphi version, so i don't know if it applies or not.
That IS version-specific. Please do!
In order of my personal preference:
1: Download Jedi CodeLib - http://jcl.sf.net. Then use TJclStringList. It has very nice split method. After that you would only have to iterate through.
function Split(const AText, ASeparator: string; AClearBeforeAdd: Boolean = True): IJclStringList;
uses JclStringLists;
...
var s: string; js: IJclStringList.
begin
...
js := TJclStringList.Create().Split(input, '#', True);
for s in js do begin
.....
end;
...
end;
2: Delphi now has somewhat less featured StringSplit routine. http://docwiki.embarcadero.com/Libraries/en/System.StrUtils.SplitString
It has a misfeature that array of string type may be not assignment-compatible to itself. Hello, 1949 Pascal rules...
uses StrUtils;
...
var s: string;
a_s: TStringDynArray;
(* aka array-of-string aka TArray<string>. But you have to remember this term exactly*)
begin
...
a_s := SplitString(input, '#');
for s in a_s do begin
.....
end;
...
end;
3: Use TStringList. The main problem with it is that it was designed that spaces or new lines are built-in separators. In newer Delphi that can be suppressed. Overall the code should be tailored to your exact Delphi version. You can easily Google for something like "Using TStringlist for splitting string" and get a load of examples (like #Uwe's one).
But you may forget to suppress here or there. And you may be on old Delphi,, where that can not be done. And you may mis-apply example for different Delphi version. And... it is just boring :-) Though you can make your own function to generate such pre-tuned stringlists for you and carefully check Delphi version in it :-) But then You would have to carefully free that object after use.
I use a function I've written called Fetch. I think I stole the idea from the Indy library some time ago:
function Fetch(var VString: string; ASeperator: string = ','): string;
var LPos: integer;
begin
LPos := AnsiPos(ASeperator, VString);
if LPos > 0 then
begin
result := Trim(Copy(VString, 1, LPos - 1));
VString := Copy(VString, LPos + 1, MAXINT);
end
else
begin
result := VString;
VString := '';
end;
end;
Then I'd call it like this:
var
value: string;
line: string;
profileFile: string;
profileList: TStringList;
index: integer;
begin
if OpenDialog1.Execute then
begin
profileFile := OpenDialog1.FileName;
if (profileFile = '') or not FileExists(profileFile) then
exit;
profileList := TStringList.Create;
try
profileList.LoadFromFile(profileFile);
for index := 0 to profileList.Count - 1 do
begin
line := profileList[index];
Fetch(line, ''''); //discard "Line '"
value := Fetch(line, '#')
while (value <> '') and (value[1] <> '''') do //bail when we get to the quote at the end
begin
ProcessTheNumber(value); //do whatever you need to do with the number
value := Fetch(line, '#');
end;
end;
finally
profileList.Free;
end;
end;
end;
Note: this was typed into the browser, so I haven't checked it works.
I have a submenu that list departments. Behind this each department have an action who's name is assigned 'actPlan' + department.name.
Now I realize this was a bad idea because the name can contain any strange character in the world but the action.name cannot contain international characters. Obviously Delphi IDE itself call some method to validate if a string is a valid componentname. Anyone know more about this ?
I have also an idea to use
Action.name := 'actPlan' + department.departmentID;
instead. The advantage is that departmentID is a known format, 'xxxxx-x' (where x is 1-9), so I have only to replace '-' with for example underscore. The problem here is that those old actionnames are already persisted in a personal textfile. It will be exceptions if I suddenly change from using departments name to the ID.
I could of course eat the exception first time and then call a method that search replace that textfile with the right data and reload it.
So basically I search the most elegant and futureproof method to solve this :)
I use D2007.
Component names are validated using the IsValidIdent function from SysUtils, which simply checks whether the first character is alphabetic or an underscore and whether all subsequent characters are alphanumeric or an underscore.
To create a string that fits those rules, simply remove any characters that don't qualify, and then add a qualifying character if the result starts with a number.
That transformation might yield the same result for similar names. If that's not something you want, then you can add something unique to the end of the string, such as a checksum computed from the input string, or your department ID.
function MakeValidIdent(const s: string): string;
var
len: Integer;
x: Integer;
c: Char;
begin
SetLength(Result, Length(s));
x := 0;
for c in s do
if c in ['A'..'Z', 'a'..'z', '0'..'9', '_'] then begin
Inc(x);
Result[x] := c;
end;
SetLength(Result, x);
if x = 0 then
Result := '_'
else if Result[1] in ['0'..'9'] then
Result := '_' + Result;
// Optional uniqueness protection follows. Choose one.
Result := Result + IntToStr(Checksum(s));
Result := Result + GetDepartment(s).ID;
end;
In Delphi 2009 and later, replace the second two in operators with calls to the CharInSet function. (Unicode characters don't work well with Delphi sets.) In Delphi 8 and earlier, change the first in operator to a classic for loop and index into s.
I have written a routine
// See SysUtils.IsValidIdent:
function MakeValidIdent(const AText: string): string;
const
Alpha = ['A'..'Z', 'a'..'z', '_'];
AlphaNumeric = Alpha + ['0'..'9'];
function IsValidChar(AIndex: Integer; AChar: Char): Boolean;
begin
if AIndex = 1 then
Result := AChar in Alpha
else
Result := AChar in AlphaNumeric;
end;
var
i: Integer;
begin
Result := AText;
for i := 1 to Length(Result) do
if not IsValidChar(i, Result[i]) then
Result[i] := '_';
end;
which makes Pascal identifiers from strings.
You might also want to copy FindUniqueName from Classes.pas and apply that to the result from MakeValidIdent.
Here is my routine:
function MakeValidIdent(const s: string): string;
begin
Result := 'clm'; //Prefix
for var c in s do
if CharInSet(c, ['A'..'Z', 'a'..'z', '0'..'9', '_']) then
Result := Result + c;
end;