Recent

Author Topic: Convert string with Key-Value pairs to TStrings with Key-Value pairs.  (Read 1339 times)

Bart

  • Hero Member
  • *****
  • Posts: 5640
    • Bart en Mariska's Webstek
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #15 on: November 30, 2025, 04:56:43 pm »
@Bart:
...Or you could write a quick'n'dirty "Finite Automata" yourself  ...state-machines can be quick  :D

Something like this?
Code: Pascal  [Select][+][-]
  1. procedure SplitLine(Line: String; List: TStrings; AllowSpaceAroundEqualSign: Boolean = True);
  2. type
  3.   TState = (stInBetween, stKeyStart, stInKey, stAfterKey, stEqualSign, stAfterEqualSign, stQuoteStart, stInValue, stQuoteEnd);
  4. var
  5.   State: TState;
  6.   i, Len, KeyStart, ValueStart: Integer;
  7.   Ch: Char;
  8.   Key, Value: String;
  9. begin
  10.   List.Clear;
  11.   KeyStart := MaxInt;
  12.   ValueStart := MaxInt;
  13.   i := 1;
  14.   Len := Length(Line);
  15.   if (Len = 0) then
  16.     Exit;
  17.   State := stInBetween;
  18.   i := 1;
  19.   for i := 1 to Len do
  20.   begin
  21.     if not AllowSpaceAroundEqualSign and (State in [stAfterKey, stAfterEqualSign]) then
  22.       Exit;
  23.     Ch := Line[i];
  24.     //write('i=',i:2,'  State=',State:18,' Ch=>',Ch,'<');
  25.     case Ch of
  26.       #32:
  27.         begin
  28.           case  State of
  29.             stInBetween, stAfterKey, stAfterEqualSign, stInValue: ;
  30.             stKeyStart, stInKey:
  31.             begin
  32.               State := stAfterKey;
  33.               Key := Copy(Line, KeyStart, i-KeyStart);
  34.             end;
  35.             stEqualSign: State := stAfterEqualSign;
  36.             stQuoteStart: State := stInValue;
  37.             stQuoteEnd: State := stInBetween;
  38.           end// case State
  39.         end;
  40.       '=':
  41.         begin
  42.           case  State of
  43.             stInBetween: Exit;                            // did not find a Key yet
  44.             stKeyStart, stInKey:
  45.             begin                                         // end of Key
  46.               State := stEqualSign;
  47.               Key := Copy(Line, KeyStart, i-KeyStart);
  48.             end;
  49.             stAfterKey: State := stEqualSign;             //KeyEnd should be marked elsewhere already
  50.             stAfterEqualSign,stEqualSign: Exit;           //cannot have sonsecutive equal signs, even with space between them
  51.             stQuoteStart:
  52.             begin                                         //first char of Value
  53.               State := stInValue;
  54.               ValueStart := i;
  55.             end;
  56.             stInValue: ;                                  //part of Value
  57.             stQuoteEnd: Exit;                             //char directly after closing " can only be space
  58.           end// case State
  59.         end;
  60.       '"':
  61.         begin
  62.           case  State of
  63.             stInBetween: Exit;                             // only start of a Key allowed here
  64.             stKeyStart, stInKey, stAfterKey: Exit;         // did not find equal sign yet
  65.             stEqualSign: State := stQuoteStart;            //Start of Value
  66.             stAfterEqualSign: State := stQuoteStart;       //Start of Value
  67.             stQuoteStart, stInValue:
  68.             begin  //end of Value
  69.               if (Key = '') or (ValueStart = MaxInt) then  //sanity check
  70.               begin
  71.                 //writeln;
  72.                 //writeln('SplitLine Error');
  73.                 //writeln('Key=[',Key,'], ValueStart=',ValueStart);
  74.                 Exit;                                      //at this point Key should never be empty and ValueStart should have been set.
  75.               end;
  76.               State := stQuoteEnd;
  77.               Value := Copy(Line, ValueStart, i-ValueStart);
  78.               List.AddPair(Key, Value);
  79.               Key := '';
  80.               Value := '';
  81.               KeyStart := MaxInt;
  82.               ValueStart := MaxInt;
  83.             end;
  84.             stQuoteEnd: Exit;                              //cannot have 2 consecutive quotes
  85.           end// case State
  86.         end;
  87.       otherwise
  88.       begin //"normal" character
  89.         case  State of
  90.           stInBetween:
  91.           begin
  92.             State := stKeyStart;
  93.             KeyStart := i;
  94.           end;
  95.           stKeyStart: State := stInKey;
  96.           stInKey, stInValue: ;
  97.           stAfterKey: Exit;                                //only space or = is valid after a Key
  98.           stEqualSign, stAfterEqualSign: Exit;             //only space or " is valid after equal sign
  99.           stQuoteStart:
  100.           begin
  101.             State := stInValue;
  102.             ValueStart := i;
  103.           end;
  104.           stQuoteEnd: Exit;                                //there can only be space after closing "
  105.         end// case State
  106.       end;
  107.     end;//case Ch
  108.   //writeln('   State->',State);
  109.   end;//for
  110. end;

Not sure if this is what you would consider to be using a state machine?

It handles spaces around equal signs better than my original code.
Not sure it's faster.
It's a lot more lines of code though...

Was fun coding it this way.

Bart

cdbc

  • Hero Member
  • *****
  • Posts: 2506
    • http://www.cdbc.dk
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #16 on: November 30, 2025, 05:55:27 pm »
Hi Bart
Quote
Not sure if this is what you would consider to be using a state machine?
That is exactly and to the point, what I meant \o/ Well done mate  8)
Yes, it does take some (fun) coding, but I find you're way more flexible, with this style of /Old-School/ parsing... One can implement whatever /rule/ one can think of  ;D
Good on you mate =^
Regards Benny
If it ain't broke, don't fix it ;)
PCLinuxOS(rolling release) 64bit -> KDE6/QT6 -> FPC Release -> Lazarus Release &  FPC Main -> Lazarus Main

Zvoni

  • Hero Member
  • *****
  • Posts: 3163
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #17 on: December 01, 2025, 08:50:49 am »
Regex? https://regex101.com/r/ROiHXL/1

If by hand:
Preconditions:
1) Key-Designations have no blanks in its name
2) if there is no whitespace between Key/Value and Delimiter, otherwise sanitize... replace "Key = Value" (Blanks around "=") to "Key=Value, irrespective of Double quotes.
2a) Sanitize away Doublequotes
3) parse from right to left
Start at the End, looking for "=", when found look for first blank. Since precondition 1, 2, 2a applies, the hit on a blank MUST be a separator to the next pair

That way you avoid "My Values have blanks inside"
« Last Edit: December 01, 2025, 09:14:09 am by Zvoni »
One System to rule them all, One Code to find them,
One IDE to bring them all, and to the Framework bind them,
in the Land of Redmond, where the Windows lie
---------------------------------------------------------------------
Code is like a joke: If you have to explain it, it's bad

Bart

  • Hero Member
  • *****
  • Posts: 5640
    • Bart en Mariska's Webstek
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #18 on: December 02, 2025, 06:26:27 pm »
@wp: your code also breaks on Key="value=off".
KeyAndValue := Pairs.Split('='); will actually split this into 3 parts.

I modified it to this:
Code: Pascal  [Select][+][-]
  1. procedure SplitLine_wp(Line: String; List: TStrings);
  2. var
  3.   Pairs: TStringArray;
  4.   i: Integer;
  5.   Key, Value: String;
  6.   EqualPos: SizeInt;
  7. begin
  8.   List.Clear;
  9.   Pairs := Line.Split([' ', ';', #13, #10], '"','"',TStringSplitOptions.ExcludeEmpty);
  10.   for i := 0 to High(Pairs) do
  11.   begin
  12.     //since Value may also contain an equal sign we cannot use Pairs[i].Split('=')
  13.     begin
  14.       EqualPos := Pos('=', Pairs[i]);
  15.       if (EqualPos > 1) then
  16.       begin
  17.         Key := Copy(Pairs[i], 1, EqualPos-1);
  18.         Value := Copy(Pairs[i], EqualPos+1, Length(Pairs[i])-EqualPos).DequotedString('"');
  19.       end
  20.       else
  21.       begin
  22.         //writeln('SplitLine Error');
  23.         //writeln('Malformed Key-Value pair: Pairs[',i,']=[',Pairs[i],']');
  24.         Exit;
  25.       end;
  26.       if (Pos('"', Key) > 0) then
  27.       begin
  28.         //writeln('SplitLine Error');
  29.         //writeln('Malformed Key: ',Key, '  [Key cannot contain a quote character (")');
  30.         Exit; //invalid Key
  31.       end;
  32.       List.AddPair(Key, Value);
  33.       //writeln('Key=' + keyAndValue[0] + ' Value=' + keyAndValue[1].DequotedString('"'));
  34.     end;
  35.   end;
  36. end;

Since, for now I'm pretty sure that there will be no spaces around the equals sign (like Key = ¨Value"), your method will do nicely.
It may not be as fun as the statemacine approach, but it's better readable.

Bart

Zvoni

  • Hero Member
  • *****
  • Posts: 3163
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #19 on: December 02, 2025, 09:47:52 pm »
Bart,
Any thoughts on my approach?
Though you‘ve introduced a new challenge with your last post (value=off)
Though i already have an idea about that
One System to rule them all, One Code to find them,
One IDE to bring them all, and to the Framework bind them,
in the Land of Redmond, where the Windows lie
---------------------------------------------------------------------
Code is like a joke: If you have to explain it, it's bad

Thausand

  • Sr. Member
  • ****
  • Posts: 439
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #20 on: December 02, 2025, 11:47:30 pm »
KeyAndValue := Pairs.Split('='); will actually split this into 3 parts.
Is logic. Then have make how want  :)

Code: Pascal  [Select][+][-]
  1. begin
  2.   Line:='Key="value=off"';
  3.   Parts:=Line.Split(['='],'"');
  4.   writeln('parts cnt ', Length(Parts));
  5.   for Part in Parts do writeln(Part);
  6. end.
  7.  

Have answer:
Code: [Select]
parts cnt 2
Key
"value=off"

Bart

  • Hero Member
  • *****
  • Posts: 5640
    • Bart en Mariska's Webstek
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #21 on: December 03, 2025, 12:08:45 am »
Is logic. Then have make how want  :)

I really feel stupid right now.

Bart

Bart

  • Hero Member
  • *****
  • Posts: 5640
    • Bart en Mariska's Webstek
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #22 on: December 03, 2025, 12:10:01 am »
@zvoni: I really dislike regex's.
Mostly because I understand only the very minimal utterly small basics of them.

Bart

Zvoni

  • Hero Member
  • *****
  • Posts: 3163
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #23 on: December 03, 2025, 07:33:57 am »
@zvoni: I really dislike regex's.
Mostly because I understand only the very minimal utterly small basics of them.

Bart
I was referring to „parse from right to left“
The critical part is „keys don’t have blanks“

Proof of concept (No sophisticated error-handling)
Code: Pascal  [Select][+][-]
  1. program Project1;
  2. {$mode objfpc}{$H+}
  3. Uses Classes, Sysutils, StrUtils;
  4.  
  5. Const
  6.   Source:String='key1 =Some Value key2= "Value with Quotes" key3="Malformed value missing ending quote key4="Value=off" key5 = Will be the first';
  7.   q:String='"';
  8.   e:String='=';
  9.   b:String=' ';
  10.   Delimiter:String='|#:-_'; //Expand to suit. Note: '=' is not allowed as a Delimiter for the StringList!
  11.  
  12. Function ParseSource(ASource:String;var AStringList:TStringList):Boolean;
  13. Var
  14.     s:String;
  15.     Key,Value:String;
  16.     r1,r2:Integer;
  17.     i:Integer;
  18.     re,rb:Integer;
  19.     SLLocal:TStringList;
  20.     t:Array Of String;
  21. Begin
  22.   s:=ASource;
  23.   //Remove blanks around '='  
  24.   Repeat
  25.     s:=StringReplace(s,e+b,e,[rfReplaceAll],r1);
  26.     s:=StringReplace(s,b+e,e,[rfReplaceAll],r2);
  27.   until r1+r2=0;
  28.   //Remove DoubleQuotes
  29.   s:=StringReplace(s,q,'',[rfReplaceAll]);
  30.   //Set Delimiter for Stringlist to first Character NOT in Source
  31.   For i:=1 to Length(Delimiter) Do
  32.     Begin
  33.       If Pos(Delimiter[i],s)=0 Then
  34.         Begin
  35.           AStringList.Delimiter:=Delimiter[i];
  36.           Break;
  37.         end;
  38.     end;
  39.   SLLocal:=TStringList.Create;
  40.   Repeat
  41.     //Look for equal-sign, start at the end
  42.     re:=RPos(e,s);    
  43.     if re>0 Then    
  44.       begin
  45.       //Look for blank, start at the end, start from the position of '='-sign, then to the left
  46.         rb:=RPosEx(b,s,re-1);
  47.         SLLocal.Add(RightStr(s,Length(s)-rb));  //Add the full token --> KeyValue-Pair
  48.       end;
  49.     s:=Trim(LeftStr(s,rb-1));  //Removes multiple blanks
  50.   until Length(s)=0;//No more equal-signs
  51.   For i:=0 To SLLocal.Count-1 Do
  52.     Begin
  53.       t:=SLLocal.Strings[i].Split([e]);
  54.       Key:=t[0];
  55.       Value:=String.Join(e,t,1,Length(t)-1);  //Whatever is left, catches "Key=Value=Off"
  56.       AStringList.AddPair(Key,Value);
  57.     end;
  58.   SLLocal.Free;
  59. end;
  60.  
  61. Var SL:TStringList;
  62.     i:Integer;
  63. begin
  64.   SL:=TStringList.Create;
  65.   ParseSource(Source,SL);
  66.   For i:=0 To SL.Count-1 Do Writeln('StringlistKey: ',SL.Names[i],' - StringlistValue: ',SL.ValueFromIndex[i]);
  67.   readln;
  68.   SL.Free;
  69. end.
  70.  

Returns:
Quote
StringlistKey: key5 - StringlistValue: Will be the first
StringlistKey: key4 - StringlistValue: Value=off
StringlistKey: key3 - StringlistValue: Malformed value missing ending quote
StringlistKey: key2 - StringlistValue: Value with Quotes
StringlistKey: key1 - StringlistValue: Some Value

As far as i can see, the only situation my algorithm fails is, if you have something like
AKeyName="A Value with Blanks = off"

but in that case i'd say you have a different "Problem" :D
« Last Edit: December 03, 2025, 10:20:06 am by Zvoni »
One System to rule them all, One Code to find them,
One IDE to bring them all, and to the Framework bind them,
in the Land of Redmond, where the Windows lie
---------------------------------------------------------------------
Code is like a joke: If you have to explain it, it's bad

Warfley

  • Hero Member
  • *****
  • Posts: 2028
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #24 on: December 03, 2025, 02:44:15 pm »
Written on my phone and not tested, but sou could simply write some basic matching automatons:
Code: Pascal  [Select][+][-]
  1. function SkipNoise(const s: String; var Start: SizeInt): Boolean; inline;
  2. begin
  3.   while (Start<=Length(s)) and (s[Start]<=#32) do
  4.     Inc(Start);
  5.   Result:=Start<=Length;
  6. end;
  7.  
  8. function MatchIdentifier(const s: String; var Start: SizeInt): SizeInt;
  9. begin
  10.   Result:=0;
  11.   if not SkipNoise(s, Start) then
  12.     Exit;
  13.   while (Start+Result<=Length(s)) and (s[Start+Result] in ['A..Z', 'a..z', '0'..'9']) do
  14.     Inc(Result);
  15. end;
  16.  
  17. function MatchLiteral(const s,l: String; var Start: SizeInt): Boolean;
  18. var
  19.   Len: SizeInt;
  20. begin
  21.   Result:=0;
  22.   if not SkipNoise(s, Start) then
  23.     Exit;
  24.   while (Start+len<=Length(s)) and (len<Length(l)) and (s[Start+len] = l[Result+1]) do
  25.     Inc(len);
  26.   Result:=Len=Length(l);
  27. end;
  28.  
  29. function MatchString(const s: String; var Start: SizeInt): SizeInt;
  30. begin
  31.   Result:=0;
  32.   if not SkipNoise(s, Start) or (s[Result] <> '"') then
  33.     Exit;
  34.   Inc(Result);
  35.   while (Start+Result<=Length(s)) and (s[Start+Result] <> '"') do
  36.     Inc(Result);
  37.   if Start+Result>Length(s) then
  38.     Result:=0
  39.   else
  40.     Inc(Result);
  41. end;
  42.  
  43. function MatchKVPair(const s: String; var p: SizeInt; out KVPair: TKeyValuePair): Boolean;
  44. var
  45.   tokLen: SizeInt;
  46. begin
  47.   Result:=False;
  48.   tokLen:=MatchIdentifier(s, p);
  49.   if tokLen=0 then
  50.     Exit;
  51.   KVPair.Key:=Copy(s,p,tokLen);
  52.   Inc(p,tokLen);
  53.  
  54.   if not MatchLiteral(s, '=', p) then
  55.     Exit;
  56.   Inc(p,Length('='));
  57.  
  58.   tokLen:=MatchString(s, p);
  59.   if tokLen=0 then
  60.     Exit;
  61.   KVPair.Value:=Copy(s,p+1,tokLen-2);
  62.   Inc(p,tokLen);
  63.  
  64.   Result:=True;
  65. end;
  66.  
  67. function KVPairs(const s: String): TKeyValuePairArray;
  68. var
  69.   kvPair: TKeyValuePair;
  70.   p: SizeInt = 1;
  71. begin
  72.   Result:=[];
  73.   while MatchKVPair(s,p,kvPair) do
  74.     Result += [kvPair];
  75.   if p<=Length(s) then
  76.     WriteLn('Syntax Error in string at ', p);
  77. end;
  78.  

It's very clean (just a few functions each with just a few lines of code), efficient (completely single pass matching nearly no overhead) and extensible (easy to add new automatons if needed).

You could do some better error handling than me tho
« Last Edit: December 03, 2025, 02:47:58 pm by Warfley »

Bart

  • Hero Member
  • *****
  • Posts: 5640
    • Bart en Mariska's Webstek
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #25 on: December 03, 2025, 03:07:26 pm »
Proof of concept (No sophisticated error-handling)
<snip>
Returns:
Quote
StringlistKey: key5 - StringlistValue: Will be the first
StringlistKey: key4 - StringlistValue: Value=off
StringlistKey: key3 - StringlistValue: Malformed value missing ending quote
StringlistKey: key2 - StringlistValue: Value with Quotes
StringlistKey: key1 - StringlistValue: Some Value

I think it should not accept malformed pairs.

As far as i can see, the only situation my algorithm fails is, if you have something like
AKeyName="A Value with Blanks = off"
Both wp's and mine (statemachine) parse that though.

Bart

Zvoni

  • Hero Member
  • *****
  • Posts: 3163
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #26 on: December 03, 2025, 04:00:46 pm »
Proof of concept (No sophisticated error-handling)
<snip>
Returns:
Quote
StringlistKey: key5 - StringlistValue: Will be the first
StringlistKey: key4 - StringlistValue: Value=off
StringlistKey: key3 - StringlistValue: Malformed value missing ending quote
StringlistKey: key2 - StringlistValue: Value with Quotes
StringlistKey: key1 - StringlistValue: Some Value

I think it should not accept malformed pairs.

As far as i can see, the only situation my algorithm fails is, if you have something like
AKeyName="A Value with Blanks = off"
Both wp's and mine (statemachine) parse that though.

Bart
Ahh....well then.... just looked convoluted to me, but if you have a solution you're happy with....
One System to rule them all, One Code to find them,
One IDE to bring them all, and to the Framework bind them,
in the Land of Redmond, where the Windows lie
---------------------------------------------------------------------
Code is like a joke: If you have to explain it, it's bad

Bart

  • Hero Member
  • *****
  • Posts: 5640
    • Bart en Mariska's Webstek
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #27 on: December 03, 2025, 07:03:09 pm »
Ahh....well then.... just looked convoluted to me, but if you have a solution you're happy with....

I overengineered it a bit.
I have options to (dis)allow spaces after Key and before Value as well as wether or not to DeQuote Value.
It may look convoluted, but the code flow is rahter east to follow.

B.t.w your code did not compile an after fixing that it just crashed (at severl places).

Bart

Zvoni

  • Hero Member
  • *****
  • Posts: 3163
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #28 on: December 03, 2025, 09:31:56 pm »
Ahh....well then.... just looked convoluted to me, but if you have a solution you're happy with....

I overengineered it a bit.
I have options to (dis)allow spaces after Key and before Value as well as wether or not to DeQuote Value.
It may look convoluted, but the code flow is rahter east to follow.

B.t.w your code did not compile an after fixing that it just crashed (at severl places).

Bart
Well, i did say proof of concept and no error handling :D
One System to rule them all, One Code to find them,
One IDE to bring them all, and to the Framework bind them,
in the Land of Redmond, where the Windows lie
---------------------------------------------------------------------
Code is like a joke: If you have to explain it, it's bad

Bart

  • Hero Member
  • *****
  • Posts: 5640
    • Bart en Mariska's Webstek
Re: Convert string with Key-Value pairs to TStrings with Key-Value pairs.
« Reply #29 on: December 03, 2025, 10:16:17 pm »
Well, i did say proof of concept and no error handling :D
LOL

Bart

 

TinyPortal © 2005-2018