I want to create a function that receive multiples strings as parameters.
Like the function printf("Hello %s",name); of C. but I don't want to pass a ready array, it wouldn't be readable.
Edit1.text:=lang('Hello');
Edit2.text:=lang('Welcome to {1} guest',place);
Edit3.text:=lang('Hi {1}, is your {2} time in {3}','Victor','first','Disney');
output should be:
Hello
Welcome to Disney guest
Hi Victor is your first time in Disney
how I create the function TForm1.lang(parameters:String):String;, I did a research, but I can't get it work.
I need to access the parameters[] and the parameters.length also.
I'm needing this to turn my App to multilang.
Here's an example function of how you can do this:
function TForm1.lang(s: String; params: array of String): String;
var
i: Integer;
begin
for i := 0 to High(params) do
begin
ShowMessage(params[i]);
end;
end;
Call it like this:
lang('My format string', ['this', 'that']);
or like this:
var
b: String;
begin
b := 'this';
lang('My format string', [b, 'that']);
end;
Not sure what you mean by not readable
DoSomething(['Param1','Param2']);
for
procedure DoSomething(args : Array of String);
Var
Index : Integer;
Begin
for index := Low(args) to High(args) Do
ShowMessage(args[Index]);
End;
Seems okay to me. Course if you want to call it from outside delphi then you have an issue.
Quick fix is just to pass in a delimited string and then user TStringList to split it.
You could write a wee function to do that, don't forget to free it when you are done.
All your three examples could be fixed by using SysUtils.Format:
Edit1.text := format('%s',['Hello']));
Edit1.text := format('Welcome to %s guest',[place]));
Edit1.text := format('Hi %s, is your %s time in %s',['Victor','first','Disney']));
Personally I think it's quite readable. If you can have what you need from a basic sysutils function, you should seriously consider doing that, rather than to write your own version. On the other hand, you may need more complex functionality that doesn't show in your question. If that's the case, I think paulsm4's suggestion of using a stringlist seems like a good way to go.
Delphi does not support CREATING functions withvararg-style parameters that work exactly like printf() does. It only supports CONSUMING such functions from external libraries. The closest Delphi comes to supporting the creation of functions with variable parameter lists is to use "open array" parameters, like what SysUtils.Format() uses.
As Tony mentions above, I also recommend using a deliminated string. Except, a little more than just deliminating, but using more of a parsing technique. If I understand right, this function you're making for formatting shall NOT include an array in the parameters, but technically, that doesn't mean we can't use arrays anywhere at all (arrays are very ideal to use for this scenario for fast performance).
This method will allow virtually anything to be passed in the parameters, including the deliminator, without affecting the output. The idea is to do A) Size of parameter string, B) Deliminator between size and parameter, and C) parameter string... And repeat...
const
MY_DELIM = '|'; //Define a deliminator
type
TStringArray = array of String;
/////////////////////////////////
//Convert an array of string to a single parsable string
// (Will be the first step before calling your format function)
function MakeParams(const Params: array of String): String;
var
X: Integer;
S: String;
begin
Result:= '';
for X:= 0 to Length(Params)-1 do begin
S:= Params[X];
Result:= Result + IntToStr(Length(S)) + MY_DELIM + S;
end;
end;
//Convert a single parsable string to an array of string
// (Will be called inside your format function to decode)
// This is more or less called parsing
function ExtractParams(const Params: String): TStringArray;
var
S: String; //Used for temporary parsing
T: String; //Used for copying temporary data from string
P: Integer; //Used for finding positions
C: Integer; //Used for keeping track of param count
Z: Integer; //Used for keeping track of parameter sizes
begin
S:= Params; //Because we'll be using 'Delete' command
C:= 0; //Set count to 0 to start
SetLength(Result, 0); //Prepare result array to 0 parameters
while Length(S) > 0 do begin //Do loop until nothing's left
P:= Pos(MY_DELIM, S); //Get position of next deliminator
if P > 1 then begin //If deliminator was found...
C:= C + 1; //We have a new parameter
SetLength(Result, C); //Set array length to new parameter count
T:= Copy(S, 1, P-1); //Get all text up to where deliminator was found
Delete(S, 1, P); //Delete what we just copied, including deliminator
Z:= StrToIntDef(T, 0); //Convert T:String to Z: Integer for size of parameter
T:= Copy(S, 1, Z); //Get all text up to 'Z' (size of parameter)
Delete(S, 1, Z); //Delete what we just copied
Result[C-1]:= T; //Assign the new parameter to end of array result
end else begin //If deliminator was NOT found...
S:= ''; //Clear S to exit loop (possible bad format if this happens)
end;
end;
end;
//Main formatting routine
function MyFormat(const Input: String; const Params: String): String;
var
A: TStringArray;
X: Integer;
S: String;
P: Integer;
R: String;
begin
R:= Input;
A:= ExtractParams(Params);
//At this point, A contains all the parameters parsed from 'Params'
for X:= 0 to Length(A)-1 do begin
S:= A[X];
P:= Pos('%s', R);
if P > 0 then begin
Delete(R, P, 2);
Insert(S, R, P);
end;
end;
Result:= R;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Pars: String;
begin
Pars:= MakeParams(['this', 'that', 'something else']);
Edit1.Text:= MyFormat('%s is %s but not %s', Pars);
end;
As you probably know, SysUtils.Format() implements "varargs" by using a set.
In your case, however, why not just pass a TStringList? The function will simply check "list.Count". Voila - you're done!
Related
procedure ReverseArray(var A : array of string);
var I,J,L : integer;
begin
for I := Low(A) to High(A) do
begin
L := length(A[I]);
for J := L downto 1 do M := M + A[I];
end;
writeln(M);
end;
begin
for I := 1 to 4 do readln(T[I]);
ReverseArray(T);
sleep(40000);
end.
What I'm trying to do here basically is reverse every string in the array but I'm unable to do it , what the code above do is basically repeat the words depends on their length (I write 'bob' in the array , the procedure will give me 'bob' three times because the length is 3) ... not sure why it's not working properly and what I'm missing
Delphi has a ReverseString() function in the StrUtils unit.
uses
StrUtils;
type
TStrArray = array of string;
procedure ReverseArray(var A : TStrArray);
var
I: integer;
begin
for I := Low(A) to High(A) do
A[I] := ReverseString(A[I]);
end;
var
T: TStrArray;
I: Integer
begin
SetLength(T, 4);
for I := 1 to 4 do Readln(T[I]);
ReverseArray(T);
...
end.
A string is an array of char with some extra bells and whistles added.
So an array of string is a lot like an array of array of char.
If you want to reverse the string, you'll have to access every char and reverse it.
procedure ReverseArray(var A : array of string);
var
i,j,Len : integer;
B: string;
begin
for i := Low(A) to High(A) do begin
Len := length(A[i]);
SetLength(B, Len); //Make B the same length as A[i].
//B[Len] = A[i][1]; B[Len-1]:= A[i][2] etc...
for j := Len downto 1 do B[j]:= A[i][(Len-J)+1];
//Store the reversed string back in the array.
A[i]:= B;
//Because A is a var parameter it will be returned.
//Writeln(B); //Write B for debugging purposes.
end;
end;
var
i: integer;
Strings: array [0..3] of string;
begin
for i := 0 to 3 do readln(Strings[i]);
ReverseArray(Strings);
for i := 0 to 3 do writeln(Strings[i]);
WriteLn('Done, press a key...');
ReadLn;
end.
Some tips:
Do not use global variables like M but declare a local variable instead.
Don't do AStr:= AStr + AChar in a loop, if you can avoid it. If you know how long the result is going to be use the SetLength trick as shown in the code. It's generates much faster code.
Instead of a Sleep you can use a ReadLn to halt a console app. It will continue as soon as you press a key.
Don't put the writeln in your working routine.
Note the first element in a string is 1, but the first element in a array is 0 (unless otherwise defined); Dynamic arrays always start counting from zero.
Note that array of string in a parameter definition is an open array; a different thing from a dynamic array.
Single uppercase identifiers like T, K, etc are usually used for generic types, you shouldn't use them for normal variables; Use a descriptive name instead.
Come on! 'bob' is one of those words you shouldn't try to test a reverse routine. But the problem goes beyond that.
Your problem is in here
for J := L downto 1 do
M := M + A[I];
You are trying to add the whole string to the M variable instead of the character you are trying to access. So, it should be
for J := L downto 1 do
M := M + A[I][J];
Also you need to set M := '' inside the first loop where it will have nothing when you start accumulating characters in to it.
Third, move the writing part, WriteLn(M), inside the first loop where you get a nice, separated outputs.
Putting together, it is going to be:
for I := Low(A) to High(A) do
begin
L := length(A[I]);
M := '';
for J := L downto 1 do
M := M + A[I][J];
writeln(M);
end;
My preferred solution for this is
type
TStringModifier = function(const s: string): string;
procedure ModifyEachOf( var aValues: array of string; aModifier: TStringModifier );
var
lIdx: Integer;
begin
for lIdx := Low(aValues) to High(aValues) do
aValues[lIdx] := aModifier( aValues[lIdx] );
end;
and it ends up with
var
MyStrings: array[1..3] of string;
begin
MyStrings[1] := '123';
MyStrings[2] := '456';
MyStrings[3] := '789';
ModifyEachOf( MyStrings, SysUtils.ReverseString );
end;
uses
System.SysUtils, System.StrUtils;
var
Forwards, backwards : string;
begin
forwards:= 'abcd';
backwards:= ReverseString(forwards);
Writeln(backwards);
Readln;
end;
// dcba
I know, I can write
if C in ['#', ';'] then ...
if C is an AnsiChar.
But this
function CheckValid(C: Char; const Invalid: array of Char; OtherParams: TMyParams): Boolean;
begin
Result := C in Invalid; // <-- Error because Invalid is an array not a set
//maybe other tests...
//Result := Result and OtherTestsOn(OtherParams);
end;
yields E2015: Operator not applicable to this operand type.
Is there an easy way to check if a character is contained in an array of characters (other than iterate through the array)?
I know you don't want to, but this is one of those cases where iterating through the array really is your best option, for performance reasons:
function CheckValid(C: Char; const Invalid: array of Char): Boolean;
var
I: Integer;
begin
Result := False;
for I := Low(Invalid) to High(Invalid) do begin
if Invalid[I] = C then begin
Result = True;
Exit;
end;
end;
end;
Or:
function CheckValid(C: Char; const Invalid: array of Char): Boolean;
var
Ch: Char;
begin
Result := False;
for Ch in Invalid do begin
if Ch = C then begin
Result = True;
Exit;
end;
end;
end;
Converting the input data to strings just to search it can cause huge performance bottlenecks, especially if the function is called often, such as in a loop.
If one tries to avoid iterating through the array and if speed is of no concern then IndexOfAny can be helpful:
function CheckValid(C: Char; const Invalid: array of Char; OtherParams: TMyParams): Boolean;
begin
Result := string(C).IndexOfAny(Invalid) >= 0;
//maybe other test...
//....
end;
From the Delphi documentation:
[IndexOfAny r]eturns an integer indicating the position of the first given character found in the 0-based string. [It returns -1 if the character is not found.]
If speed is of concern, this should be avoided as #RemyLebeau explains in the comments:
Casting C to String to call IndexOfAny() will create 1 temp String. [...] if CheckValid() is called often, those conversions can be a BIG performance bottleneck, not to mention a waste of memory.
In this case #RemyLebeau's answer is the better solution.
I have a TStringList in Delphi.
after the items are inserted i call .sort procedure to sort the items.
the Items are first names followed by last names. for example: "John Smith".
I want to sort the items by last name. I mean by the first character after the space.
how can I do this?
and also the items may be unicode strings like persian characters.
I'd use the CustomSort method of TStringList to supply a custom compare function. First of all, let's imagine that we have already got the compare function:
function NameCompareFunc(List: TStringList; Index1, Index2: Integer): Integer;
begin
Result := ...;
end;
This function will (in due course) return negative to mean less than, positive to mean greater than and zero to mean equal.
Then we sort the list like this:
List.CustomSort(NameCompareFunc);
So, that's the easy bit done. But how do we implement NameCompareFunc? First of all let's split the name into last name and the rest.
procedure SplitName(const Name: string; out Last, Rest: string);
var
P: Integer;
begin
P := Pos(' ', Name);
if P = 0 then begin
Last := Trim(Name);
Rest := '';
end else begin
Last := Trim(Copy(Name, P+1, MaxInt));
Rest := Trim(Copy(Name, 1, P-1));
end;
end;
This is a pretty naive way to split a name. You'd probably want to search for separators starting from the end of the name, but I'll let you decide how to do that.
Now we can implement the compare function:
function NameCompareFunc(List: TStringList; Index1, Index2: Integer): Integer;
var
Last1, Last2, Rest1, Rest2: string;
begin
SplitName(List[Index1], Last1, Rest1);
SplitName(List[Index2], Last2, Rest2);
Result := AnsiCompareText(Last1, Last2);
if Result = 0 then begin
Result := AnsiCompareText(Rest1, Rest2);
end;
end;
Some notes:
I'm assuming that name comparison should always be case-insensitive.
We use AnsiCompareText to perform locale aware comparison.
If we encounter two names that have the same last name, then we implement a secondary comparison o the rest of the name.
You could use the CustomSort methos of Stringlist:
function LastNameCompareStrings(List: TStringList; Index1, Index2: Integer): Integer;
var
S1, S2: string;
SpaceIndex: Integer;
begin
S1 := List[Index1];
SpaceIndex := Pos(' ', S1);
if SpaceIndex <> 0 then
S1 := Copy(S1, 1, SpaceIndex - 1);
S2 := List[Index2];
SpaceIndex := Pos(' ', S2);
if SpaceIndex <> 0 then
S2 := Copy(S2, 1, SpaceIndex - 1);
if List.CaseSensitive then
Result := AnsiCompareStr(S1, S2)
else
Result := AnsiCompareText(S1, S2);
end;
procedure TForm7.ButtonFirstNameClick(Sender: TObject);
begin
NameBuffer.Sort;
Memo1.Lines.Assign(NameBuffer);
end;
procedure TForm7.ButtonLastNameClick(Sender: TObject);
begin
NameBuffer.CustomSort(#LastNameCompareStrings);
Memo1.Lines.Assign(NameBuffer);
end;
I my example I have all your names in a StringList called NameBuffer. Then I've added two buttons to a form where I sort mylist, and display the result on the Screen.
You could iterate through each item of your StringList (lets call it FullNames),
split each string and put the "splits" in two new separate stringlists which you could call
FirstNameList and LastNameList.
Now create a third stringlist which you can call ReverseFirstLast,
and combine the items from LastNameList with FirstNameList and put them in ReverseNames.
Now you have all names in reverse order. Last name first, and first name last.
You can now sort the ReverseFirstLast-list and do a split&combine method again to reverse orders again and maintain the sorting.
That is one way to do it to get a rough method up and running.
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 huge list of users and every user has it's id , but it id numbers are messed up so if anyone can show me how can I sort users by numbers , every value has this form
1:Stackoverflow
or
145000:Google
If I do that manually I think I will lose my mind since tehere are more than 700000 records.Thanks for your time and help....
Extract the number like this:
function ID(const str: string): Integer;
var
p: Integer;
begin
p := Pos(':', str);
if p=0 then
raise Exception.CreateFmt('Invalid string format: %s', [str]);
Result := StrToInt(Copy(str, 1, p-1));
end;
Once you can extract the ID as an integer you can then write a compare function. Like this:
function CompareIDs(List: TStringList; Index1, Index2: Integer): Integer;
begin
Result := CompareValue(
ID(List[Index1]),
ID(List[Index2])
);
end;
CompareValue is an RTL function that returns -1, 0 or 1 depending on the relative values of the two operands.
Feed these building blocks into TStringList.CustomSort and your job is done.
MyStringList.CustomSort(CompareIDs);