When I join an array (of strings), I will get a delimiter in between every element of the array
Writeln(string.Join('-', ['a','b','c']));
-> 'a-b-c'
However I would like to add delimiters also to the start and end of the string. I know I can do it like this
program Project1;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
function JoinAndAddDelimitersToStartAndEnd(const Delimiter: string; const SArr: TArray<string>): string;
begin
Result := Delimiter + string.Join(Delimiter, SArr) + Delimiter;
end;
begin
Writeln(JoinAndAddDelimitersToStartAndEnd('-', ['a','b','c']));
//-> '-a-b-c-'
Readln;
end.
Is there a better (built-in?) way to do this?
Another "ugly" solution would be adding empty elements to the beginning and end of the array, if the array is more joined than written to. By adding the first empty element before array population and one after, it won't have much overhead and the benefit is 1 (relatively expensive) string concatenation instead of 3.
How about:
var
s : string;
SArr : TArray<string>;
Delim : string;
begin
Delim := '-';
SArr := ['a','b','c'];
s := format('%s%s%s',[Delim,string.Join(Delim,SArr),Delim]);
end;
Related
I use the StrUtils in to split a string into a TStringDynArray, but the output was not as expected. I will try to explain the issue:
I have a string str: 'a'; 'b'; 'c'
Now I called StrUtils.SplitString(str, '; '); to split the string and I expected an array with three elements: 'a', 'b', 'c'
But what I got is an array with five elements: 'a', '', 'b', '', 'c'.
When I split with just ';' instead of '; ' I get three elements with a leading blank.
So why do I get empty strings in my first solution?
This function is designed not to merge consecutive separators. For instance, consider splitting the following string on commas:
foo,,bar
What would you expect SplitString('foo,,bar', ',') to return? Would you be looking for ('foo', 'bar') or should the answer be ('foo', '', 'bar')? It's not clear a priori which is right, and different use cases might want different output.
If your case, you specified two delimiters, ';' and ' '. This means that
'a'; 'b'
splits at ';' and again at ' '. Between those two delimiters there is nothing, and hence an empty string is returned in between 'a' and 'b'.
The Split method from the string helper introduced in XE3 has a TStringSplitOptions parameter. If you pass ExcludeEmpty for that parameter then consecutive separators are treated as a single separator. This program:
{$APPTYPE CONSOLE}
uses
System.SysUtils;
var
S: string;
begin
for S in '''a''; ''b''; ''c'''.Split([';', ' '], ExcludeEmpty) do begin
Writeln(S);
end;
end.
outputs:
'a'
'b'
'c'
But you do not have this available to you in XE2 so I think you are going to have to roll your own split function. Which might look like this:
function IsSeparator(const C: Char; const Separators: string): Boolean;
var
sep: Char;
begin
for sep in Separators do begin
if sep=C then begin
Result := True;
exit;
end;
end;
Result := False;
end;
function Split(const Str, Separators: string): TArray<string>;
var
CharIndex, ItemIndex: Integer;
len: Integer;
SeparatorCount: Integer;
Start: Integer;
begin
len := Length(Str);
if len=0 then begin
Result := nil;
exit;
end;
SeparatorCount := 0;
for CharIndex := 1 to len do begin
if IsSeparator(Str[CharIndex], Separators) then begin
inc(SeparatorCount);
end;
end;
SetLength(Result, SeparatorCount+1); // potentially an over-allocation
ItemIndex := 0;
Start := 1;
CharIndex := 1;
for CharIndex := 1 to len do begin
if IsSeparator(Str[CharIndex], Separators) then begin
if CharIndex>Start then begin
Result[ItemIndex] := Copy(Str, Start, CharIndex-Start);
inc(ItemIndex);
end;
Start := CharIndex+1;
end;
end;
if len>Start then begin
Result[ItemIndex] := Copy(Str, Start, len-Start+1);
inc(ItemIndex);
end;
SetLength(Result, ItemIndex);
end;
Of course, all of this assumes that you want a space to act as a separator. You've asked for that in the code, but perhaps you actually want just ; to act as a separator. In that case you probably want to pass ';' as the separator, and trim the strings that are returned.
SplitString is defined as
function SplitString(const S, Delimiters: string): TStringDynArray;
One would thought that Delimiters denote single delimiter string used for splitting string, but it actually denotes set of single characters used to split string. Each character in Delimiters string will be used as one of possible delimiters.
SplitString
Splits a string into different parts delimited by the specified
delimiter characters. SplitString splits a string into different parts
delimited by the specified delimiter characters. S is the string to be
split. Delimiters is a string containing the characters defined as
delimiters.
It is because the second parameter of SplitString is a list of single character delimiters, so '; ' means split at a ';' OR split at a ' '. So the string is split at every ';' and at every space, and between the ';' and the ' ' there is nothing, hence the empty strings.
How can I set the specified character by index to empty character in Delphi6?
procedure TMainForm.Button1Click(Sender: TObject);
var i: integer;
s_ord_account : String[10];
begin
s_ord_account := '0930002930' ;
i := 1;
REPEAT
IF s_ord_account[i] = '0' THEN
s_ord_account[i] := '';
INC(i);
UNTIL (i=5) OR (s_ord_account[i] <> ' ');
MessageDlg(s_ord_account,mtError, mbOKCancel, 0);
yend;
When I try to execute this code I get an error
[Error] Main.pas(30): Incompatible types: 'Char' and 'String'
First of all it would make a lot of sense for you to stop using Turbo Pascal strings and use the native Delphi string type, string.
There is no such thing as an empty character. You can use the Delete function to remove a character from the string. A simpler approach would be to use the StringReplace function. That renders your code entirely needless.
{$APPTYPE CONSOLE}
uses
SysUtils;
var
s: string;
begin
s := StringReplace('0930002930', '0', '', [rfReplaceAll]);
Writeln(s);
end.
Output
93293
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 want to add 30 different strings into a stringList . I do not want to add AList.Items.Add 30 times. Nor do i want to keep the strings in an array and run a loop. I was thinking may be i could write a single AList.Add ( not in a loop) where the strings to be added were seperated by a Delimiter .
e.g.
AList.Add('Data1' + <Delim> + 'Data2' ...)
How to do that ?
Please note that i am just curious as to if it can be done this way. It is quite ok if not as there are better ways to accomplish this. ( keeping the strings in an array and using a loop to add data is my idea)
Thanks in Advance
You can write a procedure that does this:
procedure SLAddStrings(SL: TStrings; S: array of string);
var
i: Integer;
begin
SL.BeginUpdate;
for i := low(S) to high(S) do
SL.Add(S[i]);
SL.EndUpdate;
end;
Try it:
var
SL: TStringList;
begin
SL := TStringList.Create;
SLAddStrings(SL, ['car', 'cat', 'dog']);
Create a temporary TStringList, assign the string to its DelimitedText property, pass the temporary to the AddStrings() method of the destination TStringList, and then free the temporary.
var
Temp: TStringList;
begin
Temp := TStringList.Create;
try
Temp.Delimiter := <Delim>;
// if using a Delphi version that has StrictDelimiter available:
// Temp.StrictDelimiter := True;
Temp.DelimitedText := 'Data1' + <Delim> + 'Data2' ...;
AList.AddStrings(Temp);
finally
Temp.Free;
end;
end;
Just use DelimitedText property. E.g. if your delimiter is set to , (default in TStringList) then you can write this code:
AList.DelimitedText := 'Data1,Data2';
you can use TStringList.DelimitedText property to add text, wich uses your Delimiter char. TStringList will split your text and then you can access each string separately using strings property;
program Project3;
{$APPTYPE CONSOLE}
uses classes;
const DATA = 'one,two,three';
var sl : TStringList;
s : string;
begin
sl := TStringList.Create();
try
sl.Delimiter := ',';
sl.DelimitedText := DATA;
for s in sl do begin
writeln(s);
end;
readln;
finally
sl.Free();
end;
end.
and result is
one
two
three
const
states : array [0..49,0..1] of string =
(
('Alabama','AL'),
('Montana','MT'),
('Alaska','AK'),
('Nebraska','NE'),
('Arizona','AZ'),
('Nevada','NV'),
('Arkansas','AR'),
('New Hampshire','NH'),
('California','CA'),
('New Jersey','NJ'),
('Colorado','CO'),
('New Mexico','NM'),
('Connecticut','CT'),
('New York','NY'),
('Delaware','DE'),
('North Carolina','NC'),
('Florida','FL'),
('North Dakota','ND'),
('Georgia','GA'),
('Ohio','OH'),
('Hawaii','HI'),
('Oklahoma','OK'),
('Idaho','ID'),
('Oregon','OR'),
('Illinois','IL'),
('Pennsylvania','PA'),
('Indiana','IN'),
('Rhode Island','RI'),
('Iowa','IA'),
('South Carolin','SC'),
('Kansas','KS'),
('South Dakota','SD'),
('Kentucky','KY'),
('Tennessee','TN'),
('Louisiana','LA'),
('Texas','TX'),
('Maine','ME'),
('Utah','UT'),
('Maryland','MD'),
('Vermont','VT'),
('Massachusetts','MA'),
('Virginia','VA'),
('Michigan','MI'),
('Washington','WA'),
('Minnesota','MN'),
('West Virginia','WV'),
('Mississippi','MS'),
('Wisconsin','WI'),
('Missouri','MO'),
('Wyoming','WY')
);
function getabb(state:string):string;
var
I:integer;
begin
for I := 0 to length(states) -1 do
if lowercase(state) = lowercase(states[I,0]) then
begin
result:= states[I,1];
end;
end;
function getstate(state:string):string;
var
I:integer;
begin
for I := 0 to length(states) -1 do
if lowercase(state) = lowercase(states[I,1]) then
begin
result:= states[I,0];
end;
end;
procedure TForm2.Button1Click(Sender: TObject);
begin
edit1.Text:=getabb(edit1.Text);
end;
procedure TForm2.Button2Click(Sender: TObject);
begin
edit1.Text:=getstate(edit1.Text);
end;
end.
Is there a bette way to do this?
Should this kind of data be hard coded?
Wouldn't it be better to use something like a XML file or even just a CSV.
Or Name Value Pairs, i.e. IA=Iowa
then loaded into a TStringList to get
States.Values['IA'] = 'Iowa';
Then you just need to write something to search the Values to work backwards like
//***Untested***
//Use: NameOfValue(States, 'Iowa') = 'IA'
function NameOfValue(const strings: TStrings; const Value: string): string;
var
i : integer;
P: Integer;
S: string;
begin
for i := 0 to strings.count - 1 do
begin
S := strings.ValueFromIndex[i];
P := AnsiPos(strings.NameValueSeparator, S);
if (P <> 0) and (AnsiCompareText(Copy(S, 1, P - 1), Value) = 0) then
begin
Result := strings.Names[i];
Exit;
end;
end;
Result := '';
end;
I'm fairly sure its case insensitive too
If you're on D2009 or D2010, use a TDictionary<string, string> from Generics.Collections. Declare the array of constants like you have it, then set up your dictionary by putting each pair in to the dictionary. Then just use the dictionary's default property to do your lookups.
Notice that lowercase(a) = lowercase(b) is slower than sameText(a, b).
In addition, you can speed up the procedure further by storing the strings in the array as lower-case only, and then in the look-up routine start with converting the input to lower-case as well. Then you can use the even faster function sameStr(a, b). But of course, when a match is found, you then need to format it by capitalizing the initial letters. This speed-up approach is probably not very important for such a small list of strings. After all, there are not too many states in the US.
Also, you should declare the functions using const arguments, i.e. write
function getabb(const state:string):string;
instead of
function getabb(state:string):string;
(unless you want to change state in the routine).
Finally, you could make the code more compact and readable by omitting the begin and end of the for loops.
I would have your lists sorted. That way you can use a binary search to cut the lookup times down. It all depends on the number of iterations you will be exercising. Around 50 items doesn't seem like much, until your iterating over the list a few thousand times looking for the last item in the list.
Also you should ALWAYS bail from your loops as soon as you get get a match if you know the rest of the list will not match.
Arrays are fine, and depending on how your using the data, you might need to add some of the "territories" that also have abbreviations (PR = PUERTO RICO, GU = GUAM, etc.).