Short Strings in a Variant Record? - string
I'd like to be able to access sections of a short string as part of a record
Something like
TMyRecord = record
case Boolean of
True:
(
EntireString: String[20];
);
False
(
StringStart: String[8];
StringMiddle: String[4];
StringEnd: String[8];
);
end;
Is this possible or would I have to declare each char individually
TMyRecord = record
private
Chars: Array[1..20] of Char;
Function GetStringStart:String;
Procedure SetStringStart(Value: String);
public
Property StringStart: String read GetStringStart write SetStringStart; // Can I have properties on a record?
end;
Function GetStringStart: String;
begin
Result := Chars[1] + Char[2]....;
end;
Procedure SetStringStart(Value: String);
begin
for i := 1 to 8 do
begin
Chars[i] := Value[i];
end;
end;
Is this possible / worth the effort?
A Delphi short string contains more than just the string contents. The initial byte in the data structure contains the length of the string. This is why short strings are limited to 255 characters.
So, you can't use short strings in your variant array the way you propose.
What you could do is adapt your second approach based on getter and setter methods to be a bit more readable.
For example:
function TMyRecord.GetStringStart: string;
begin
SetString(Result, #Chars[1], 8);
end;
You might consider using a string rather than a char array, but it's a little hard to be 100% sure of that advice without knowing exactly what your underlying problem is.
As a final thought, why not turn the problem around? Store 3 strings: StartString, MiddleString and EndString. Then have a property backed with a getter and setter called EntireString. When you read EntireString it pieces it together from the 3 individual parts, and when you write to it it pulls the individual parts out. I suspect it would be easier that way around.
Your first sample doesn't consider the length byte. The memory layout looks like this:
case True:
L12345678901234567890
^....................
case False:
L12345678L1234L12345678
^........^....^........
(L = length byte).
Depending on your requirements (e.g.: Are the partial strings always 8, 4 and 8 Chars?) I'd try storing the partial strings and make EntireString the property, using System.Copy, StrUtils.LeftStr etc.
ShortString has an implied length, so your first example will map the length parts of the substrings on top of the main string.
Your second sample is the way to start, with these notes:
properties on records are possible
you should think of the length of each sub-string (or is it always a fixed array of 20 characters?)
Edit
It totally depend on the reason you want this, and mixing character arrays and strings will get you into trouble because strings can be shorter than the array length.
Small example:
program VariantRecordsWithCharactersAndStrings;
{$APPTYPE CONSOLE}
uses
SysUtils,
Math;
const
Size20 = 20;
Size8 = 8;
Size4 = 4;
type
TChar20 = array[0..Size20-1] of Char;
TChar8 = array[0..Size8-1] of Char;
TChar4 = array[0..Size4-1] of Char;
TMyRecord = record
class var FillCharValue: Byte;
function GetEntireString: string;
function GetStringStart: string;
function GetStringMiddle: string;
function GetStringEnd: string;
procedure SetEntireString(const Value: string);
procedure SetStringStart(const Value: string);
procedure SetStringMiddle(const Value: string);
procedure SetStringEnd(const Value: string);
property EntireString: string read GetEntireString write SetEntireString;
property StringStart: string read GetStringStart write SetStringStart;
property StringMiddle: string read GetStringMiddle write SetStringMiddle;
property StringEnd: string read GetStringEnd write SetStringEnd;
procedure SetCharArray(const CharArrayPointer: PChar; const CharArraySize: Integer; const Value: string);
case Boolean of
True:
(
CharFull: TChar20;
);
False:
(
CharStart: TChar8;
CharMiddle: TChar4;
CharEnd: TChar8;
);
end;
function TMyRecord.GetEntireString: string;
begin
Result := CharFull;
end;
function TMyRecord.GetStringStart: string;
begin
Result := CharStart;
end;
function TMyRecord.GetStringMiddle: string;
begin
Result := CharMiddle;
end;
function TMyRecord.GetStringEnd: string;
begin
Result := CharEnd;
end;
procedure TMyRecord.SetEntireString(const Value: string);
begin
SetCharArray(CharFull, SizeOf(CharFull), Value);
end;
procedure TMyRecord.SetCharArray(const CharArrayPointer: PChar; const CharArraySize: Integer; const Value: string);
begin
FillChar(CharArrayPointer^, CharArraySize, FillCharValue);
Move(Value[1], CharArrayPointer^, Min(CharArraySize, SizeOf(Char)*Length(Value)));
end;
procedure TMyRecord.SetStringStart(const Value: string);
begin
SetCharArray(CharStart, SizeOf(CharStart), Value);
end;
procedure TMyRecord.SetStringMiddle(const Value: string);
begin
SetCharArray(CharMiddle, SizeOf(CharMiddle), Value);
end;
procedure TMyRecord.SetStringEnd(const Value: string);
begin
SetCharArray(CharEnd, SizeOf(CharEnd), Value);
end;
var
MyRecord: TMyRecord;
procedure Dump();
begin
Writeln(MyRecord.EntireString);
Writeln(MyRecord.StringStart);
Writeln(MyRecord.StringMiddle);
Writeln(MyRecord.StringEnd);
end;
procedure TestWithFillCharValue(const FillCharValue: Byte);
begin
Writeln('Testing with FillCharValue ', FillCharValue);
TMyRecord.FillCharValue := FillCharValue;
MyRecord.EntireString := '123456789001234567890';
Dump();
MyRecord.StringStart := 'AAA';
MyRecord.StringMiddle := 'BBB';
MyRecord.StringEnd := 'CCC';
Dump();
end;
begin
try
TestWithFillCharValue(0); // this will truncated all the sub arrays when you pass strings that are too short
TestWithFillCharValue(20); // when using Unicode, this fails even more horribly
Write('Press <Enter>');
Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
This class does more or less what you want:
it has overlapping data structures
when you assign the arrays: no problem
when you assign the strings: be aware when strings get to short
As other stated, it won't work, because the variant-sized record will add some lengths for StringStart/StringMiddle/StringEnd in the middle of the EntireString type.
You are confusing the *char type of C with the pascal shortstring type. There is an hidden character at position [0] which is the shortstring length.
You could use regular string type, then split in on purpose:
procedure StringSplit(const EntireString: string; out StringStart, StringMiddle, StringEnd: string);
begin
if length(EntireString)<>20 then
exit;
StringStart := copy(EntireString,1,8);
StringMiddle := copy(EntireString,9,4);
StringEnd := copy(EntireString,13,8);
end;
Note that the out parameter type will set all output String* variables into '' before calling the function.
This version will expect entering entire string of 20 chars long.
You could use shortstrings, but with custom types of the exact length, if you want to avoid hidden copies from/to string[255] (which occur when you use a shortstring type and work with string[n] with n<255):
type
String20 = string[20];
String4 = string[4];
String8 = string[8];
procedure StringSplit(const EntireString: String20; out StringStart: String8;
out StringMiddle: String4; out StringEnd: String8);
begin
if length(EntireString)<>20 then
exit;
StringStart := copy(EntireString,1,8);
StringMiddle := copy(EntireString,9,4);
StringEnd := copy(EntireString,13,8);
end;
Related
Custom Sort Stringlist
I am an intermediate Delphi programmer that needs to learn a lot so I hope my question here is not to dumb. I have a file with 1546 strings that I need to place in a StringList and do a custom sort. The strings look like this: 2:X,X,2,2,2,X<A>11 7:5,7,7,6,5,5<A>08 3:3,X,0,0,1,0<C/D>11 5:X,2,4,2,5,2<Asus2/Gb>02 3:0,3,2,0,3,0<C/D>02 4:X,0,4,4,0,0<Asus2/Gb>11 4:X,X,4,4,4,2<B>01 3:3,2,1,0,0,3<B#5>11 I need them to look like this: 2:X,X,2,2,2,X<A>11 7:5,7,7,6,5,5<A>08 5:X,2,4,2,5,2<Asus2/Gb>11 4:X,0,4,4,0,0<Asus2/Gb>02 4:X,X,4,4,4,2<B>01 3:3,2,1,0,0,3<B#5>11 3:3,X,0,0,1,0<C/D>11 3:0,3,2,0,3,0<C/D>02 They need to be sorted by the portion of the string between the <...> and the last 2 chars. Any help would be much appreciated. OK...done, Works quite well. Sorts a list with over 1500 strings in 62ms. Constructive criticism will be appreciated. function SortChords(List:TStringList; idx1,idx2:integer): integer; var s1,s2:string; begin s1:=List[idx1]; s1:=copy(s1,pos('<',s1)+1,pos('>',s1)-pos('<',s1)-1); s2:=List[idx2]; s2:=copy(s2,pos('<',s2)+1,pos('>',s2)-pos('<',s2)-1); if s1 < s2 then result:=-1 else if s1 > s2 then result:=1 else result:=0; end;
You can write your own custom sort procedure and use TStringList.CustomSort to sort in the desired order. The following demonstrates using the custom sort. It does not produce the exact output you describe, because you're not clear how you determine the precedence of two items that have the same value between the <> (as in lines 1 and 2, or 3 and 4, of your expected output; you can add code to decide the final order where I've indicated in the code comment. The sample is a complete console application that demonstrates sorting the values you've provided. It's slightly verbose in variable declarations for clarity. program Project1; {$APPTYPE CONSOLE} uses SysUtils, Classes; function ListSortProc(List: TStringList; Index1, Index2: Integer): Integer; var StartPosA, EndPosA: Integer; StartPosB, EndPosB: Integer; TestValA, TestValB: string; Comp: Integer; begin StartPosA := Pos('<', List[Index1]) + 1; EndPosA := Pos('>', List[Index1]); TestValA := Copy(List[Index1], StartPosA, EndPosA - StartPosA); StartPosB := Pos('<', List[Index2]) + 1; EndPosB := Pos('>', List[Index2]); TestValB := Copy(List[Index2], StartPosB, EndPosB - StartPosB); Result := CompareStr(TestValA, TestValB); { To do further processing for lines with the same value, add code here. if Result = 0 then // Decide on the order of the equal values with whatever // criteria you want. } end; var SL: TStringList; s: String; begin SL := TStringList.Create; try SL.Add('2:X,X,2,2,2,X<A>11'); SL.Add('7:5,7,7,6,5,5<A>08'); SL.Add('3:3,X,0,0,1,0<C/D>11'); SL.Add('5:X,2,4,2,5,2<Asus2/Gb>02'); SL.Add('3:0,3,2,0,3,0<C/D>02'); SL.Add('4:X,0,4,4,0,0<Asus2/Gb>11'); SL.Add('4:X,X,4,4,4,2<B>01'); SL.Add('3:3,2,1,0,0,3<B#5>11'); SL.CustomSort(ListSortProc); for s in SL do WriteLn(s); ReadLn; finally SL.Free; end; end. The code above produces this output: 7:5,7,7,6,5,5<A>08 2:X,X,2,2,2,X<A>11 4:X,0,4,4,0,0<Asus2/Gb>11 5:X,2,4,2,5,2<Asus2/Gb>02 4:X,X,4,4,4,2<B>01 3:3,2,1,0,0,3<B#5>11 3:0,3,2,0,3,0<C/D>02 3:3,X,0,0,1,0<C/D>11
Add String of TSearchrec to Memo
I want to add the files in the selected folder to the memobox or in a stringlist and show the results. In both ways, i can add them but i can't show the files from the folder in the memo or from the stringlist in a ShowMessage-dialog. function CountFilesInFolder(AFolder: String; AMask: String): Integer; var tmp1: TSearchRec; ergebnis: Integer; memo1: string; list : TStringList; begin result := 0; if (AFolder <> '') then begin if AFolder[length(AFolder)] <> '\' then AFolder := AFolder + '\'; ergebnis := FindFirst(AFolder + AMask, faArchive + faReadOnly + faHidden + faSysFile, tmp1); while ergebnis = 0 do begin Inc(result); ergebnis := FindNext(tmp1); while ((tmp1.Name = '|*_tif.tif')) and (ergebnis <> 0) do ergebnis := FindNext(tmp1); end; list.Add(tmp1.Name); FindClose(tmp1); end; end; thank you for your time and sorry for my bad english.
A low-level function like this should not directly add items to a memo. Instead pass a TStrings (an abstraction of a string list) into the function and fill it: function CountFilesInFolder(AFolder: String; AMask: String; FileNames: TStrings): Integer; begin // do your file enumeration // for each file call FileNames.Add(FileName); end; Since the Lines property of a memo is also of type TStrings you can use it directly like this: CountFilesInFolder('D:\', '*.TXT', Memo1.Lines); If you wanted to have the filenames in a string list, the usual pattern goes like this: FileNames := TStringList.Create; try CountFilesInFolder('D:\', '*.TXT', FileNames); finally FileNames.Free; end; The important point is that the caller creates and destroys the TStringList passed into CountFilesInFolder - an important principle in Delphi.
How do I store and load a list of key-value pairs in a string?
I have a list of strings and the values they are to be replaced with. I'm trying to combine them in a list like 'O'='0',' .'='.', ... so it's easy for me to edit it and add more pairs of replacements to make. Right now the best way I could think of it is: var ListaLimpeza : TStringList; begin ListaLimpeza := TStringList.Create; ListaLimpeza.Delimiter := '|'; ListaLimpeza.QuoteChar := '"'; ListaLimpeza.DelimitedText := 'O=0 | " .=."'; ShowMessage('1o Valor = '+ListaLimpeza.Names[1]+' e 2o Valor = '+ListaLimpeza.ValueFromIndex[1]); This works, but it's not good for visuals, since I can't code the before string (for ex ' .') like that (which is very visual for the SPACE character), only like (" .) so that the = works to assign a name and value in the TStringList.
The Names and Values by default have to be separated by =, in the style of Windows INI files. There's no way AFAICT to change that separator. As #SirRufo indicates in the comment (and which I had never noticed), you can change that using the TStringList.NameValueSeparator property. This will give you an idea of what Delphi thinks is in your TStringList, which is not what you think it is: procedure TForm1.FormCreate(Sender: TObject); var SL: TStringList; Temp: string; i: Integer; begin SL := TStringList.Create; SL.Delimiter := '|'; SL.QuoteChar := '"'; SL.StrictDelimiter := True; SL.DelimitedText := 'O=0 | ! .!=!.!'; Temp := 'Count: ' + IntToStr(SL.Count) + #13; for i := 0 to SL.Count - 1 do Temp := Temp + Format('Name: %s Value: %s'#13, [SL.Names[i], SL.ValueFromIndex[i]]); ShowMessage(Temp); end; This produces this output: TStringList Names/Values probably isn't going to do what you need. It's not clear what your actual goal is, but it appears that a simple text file with a simple list of text|replacement and plain parsing of that file would work, and you can easily use TStringList to read/write from that file, but I don't see any way to do the parsing easily except to do it yourself. You could use an array to store the pairs when you parse them: type TReplacePair = record TextValue: string; ReplaceValue: string; end; TReplacePairs = array of TReplacePair; function GetReplacementPairs: TReplacePairs; var ConfigInfo: TStringList; i, Split: Integer; begin ConfigInfo := TStringList.Create; try ConfigInfo.LoadFromFile('ReplacementPairs.txt'); SetLength(Result, ConfigInfo.Count); for i := 0 to ConfigInfo.Count - 1 do begin Split := Pos('|`, ConfigInfo[i]; Result[i].TextValue := Copy(ConfigInfo[i], 1, Split - 1); Result[i].ReplaceValue := Copy(ConfigInfo[i], Split + 1, MaxInt); end; finally ConfigInfo.Free; end; end; You can then populate whatever controls you need to edit/add/delete the replacement pairs, and just reverse the read operation to write them back out to save.
Defining a string type to be a certain format
I was wondering if there's a way to define a type of string or similar in delphi 7 which is intended to be in a particular format, or matching certain specifications? For example, I'd like to define a TSizeString type which accepts values such as 4x6 or 9x12 or maybe even 2.5x10.75. It should require the x as the only deliminator between two numbers. So there should never be anything like x9 or 65 or 2-4 or 4-6x6-2 and not even 4 x 6. Just INTEGER + 'x' + INTEGER or SINGLE + 'x' + SINGLE. Similar I guess to how like a TFilename works, standard filenames may look like C:\MyPath\MyFile.txt or \\Storage\SomeDir\SomeFile.doc
In newer versions of Delphi, advanced records and operator overloading are very handy in this case: type TSizeString = record x, y: single; public class operator Implicit(const S: string): TSizeString; class operator Implicit(const S: TSizeString): string; end; implementation class operator TSizeString.Implicit(const S: string): TSizeString; var DelimPos: integer; begin DelimPos := Pos('x', S); if (DelimPos = 0) or (not TryStrToFloat(Copy(S, 1, DelimPos-1), result.X)) or (not TryStrToFloat(Copy(S, DelimPos + 1), result.y)) then raise Exception.CreateFmt('Invalid format of size string "%s".', [S]); end; class operator TSizeString.Implicit(const S: TSizeString): string; begin result := FloatToStr(S.x) + 'x' + FloatToStr(S.y); end; Now you can do procedure TForm1.Button1Click(Sender: TObject); var S: TSizeString; begin S := '20x30'; // works ShowMessage(S); S := 'Hello World!'; // exception raised ShowMessage(S); end; In older versions of Delphi, you simply have to write a class, or create a basic record to hold your size (and then, of course, you can create functions that convert between such records and formatted strings).
Special types, like TFileName and TCaption are nothing special, like Andreas mentioned, but they can be used to register a specific property editor in the IDE. This will help entering such values through the object inspector. To really enforce such a value, if your string is a property of an object, you can write a setter for it. Otherwise, I should make a TSize class that has properties for the two integers, and an AsString property that combines its properties to a string. type TSize = class private FLeftInt, FRightInt: Integer; function GetString: string; procedure SetString(Value: string); public property LeftInt: Integer read FLeftInt write FLeftInt; property RightInt: Integer read FRightInt write FRightInt; property AsString: string read GetString write SetString; end; function TSize.GetString: string; begin Result := Format('%dx%d', [FLeftInt, FRightInt]); end; function TSize.SetString(Value: string); begin // Validate and parse Value. Set LeftInt and RightInt. end;
Simplest way is just to use a function, and always use it when defining your strings... function MyString(numA, numB: single) : string; begin Result := FloatToStr(numA) + 'x' + FloatToStr(numB) end; If you want to get fancier, you can do it as a class which allows a direct string assignment as a property, but which parses the string for compliance.
Getting the whole word (link) from a phrase, when I know some of it
Lets say I have a String: Go to this page: http://mysite.com/?page=1 , and I have a string page. I would like to create a function like so: MyBoolean := IsLink('Go to this page: http://mysite.com/?page=1','page',sLink); // sLink is a Var, so it would return http://mysite.com/?page=1 Basically it is supposed to check if the word "page" is part of a link or not. However I just can't figure it out. Any tips?
You could do something like function GetLinkContaining(const Str, SubStr: string; out URL: string): boolean; const ValidURLSpecialChars = ['.', ':', '/', '?', '=', '&', '%']; Prefixes: array[0..4] of string = ('http://', 'https://', 'ftp://', 'mailto:', 'www.'); function IsValidURLChar(const Char: char): boolean; begin result := IsLetterOrDigit(Char) or (Char in ValidURLSpecialChars); end; var SubStrPos: integer; Start, &End: integer; i: Integer; URLBegin: integer; begin result := false; URLBegin := 0; for i := low(Prefixes) to High(Prefixes) do begin URLBegin := Pos(Prefixes[i], Str); if URLBegin > 0 then break; end; if URLBegin = 0 then Exit(false); SubStrPos := PosEx(SubStr, Str, URLBegin); if SubStrPos = 0 then Exit(false); Start := SubStrPos; for i := SubStrPos - 1 downto 1 do if IsValidURLChar(Str[i]) then dec(Start) else break; &End := SubStrPos + length(SubStr); for i := SubStrPos + length(SubStr) to length(Str) do if IsValidURLChar(Str[i]) then inc(&End) else break; URL := Copy(Str, Start, &End - Start); result := true; end; To test it: procedure TForm1.FormCreate(Sender: TObject); var s: string; begin if GetLinkContaining('Go to this page: http://mysite.com/?page=1 (right now!)', 'page', s) then ShowMessage(s); if GetLinkContaining('This is my favourite site (www.bbc.co.uk).', 'bbc', s) then ShowMessage(s); end;
To check if 'page' is part of a string you can use the function Pos. function Pos(Str, Source : string): integer; Pos returns an integer specifying the position of the first occurrence of one string within another. Pos looks for the first complete occurence of Str in Source. If it finds one, it returns the character position in Source of the first character in Str as an integer value, otherwise it returns 0. Pos is case sensitive. So mybe you have to deal with upper- and lowe-case situations. To extracrt the URL is (maybe) not so easy, you have to define more conditions. If the URL is always at the end of your string, you can copy everything from the http on (also use Pos and Copy!)
Among the more powerful string matching algorithms there are regular expressions. They allow for very complex matches without writing much code, although mastering them may require a little time. Latest versions of Delphi have already regular expression libraries, but you can find some for earlier versions as well.