Delphi 12 besitzt ja leider immer noch keine Interpolation innerhalb von Zeichenketten. Für alle die grad nicht wissen was ich meine, hier ein paar Beispiele:

//CSharp:

Console.WriteLine($"Hello, {name}! Today is {date.DayOfWeek}, it's {date:HH:mm} now.");
// Hello, Mark! Today is Wednesday, it's 19:40 now.

 

# Python

print(f"Hello, {name}! Today is {date.weekday()}, it's {date.strftime('%H:%M')} now.");
# Hello, Mark! Today is Wednesday, it's 19:40 now.

selbst Java hat es mitterweile geschafft:

//Java 21 (JEP 430)

STR. "Today's weather is \{ feelsLike }, with a temperature of \{ temperature } degrees \{ unit }" ;

//Today's weather is warm, with a temperature of 22 degrees C

In Delphi schreibt man aber nur:

//Delphi

writeln('Hello, ' + name + '! Today is ' + FormatDateTime('ddd', date) + ', it's ' + FormatDateTime('hh:mm', date) + ' now.');

//Hello, Mark! Today is Wednesday, it's 19:40 now.

//oder mit Format
writeln(Format('Hello %s! Today is %s, its %s now', name , FormatDateTime('ddd', date), FormatDateTime('hh:mm', date)));

das ist natürlich nicht sehr schön, zumal der Format-Befehl nicht mal das Datum unterstützt, und somit separat über FormatDateTime erst in ein String umgewandelt werden muss, um es dann an Format zu übergeben. Schön wäre doch aber die Variante die C# / Python (und viele andere Sprachen) bietet.

NetFormat

Ich habe mal einen Code geschrieben, welcher folgendes ermöglicht:

//NetFormat:

writeln(TNetFormat.ToString('Hello, {0}! Today is {1:ddd}, it's {1:HH:mm} now.', name, date);

// Hello, Mark! Today is Wednesday, it's 19:40 now.

Neue Version, zusätzlich:

//NetFormat:

writeln(FStr('Hello, {0}! Today is {1:ddd}, it's {1:HH:mm} now.').Params(name, date);

// Hello, Mark! Today is Wednesday, it's 19:40 now.

Coool oder?

Folgendes bitte als Delphi-Programmierer beachten: In meiner Variante kann man einfach String, Datum, Zahlen direkt übergeben, die Funktion wandelt einfach alles in eine Zeichenkette um. Für die Formatierung habe ich mich an CSharp gehalten, was bedeutet:

Spezifizierer

Zahlen:

nach der Ziffer in den geschweiften Klammer einen ':' machen, und nun entweder C,D, F oder X schreiben (Währung, Ganzzahl, Fließkommazahl, Hexstring). Danach folgt eine Ziffer für die Länge: Bei C und F ist das die Anzahl der Nachkommastellen, bei D und X wird die Länge mit führenden Nullen formatiert. 

Datum:


Es muss ein TDateTime verwendet werden. Das Format ist ebenfalls identisch mit CSharp.
Das bedeutet das es von FormatDateTime folgendermaßen abweicht:

  • Groß/Kleinschreibung der Buchstaben werden beachtet
  • H=24h Stunden, h=12 Stunden ( ap - Spezifizierer wird nicht unterstützt)
  • M ist Monat, m ist Minute (n wird also nicht benötigt)

Wen die technischen Hintergründe nicht interessieren, der scrollt einfach ganz nach unten zum Downloadlink.

Technischer Hintergrund

Ich hatte mir lange Zeit Gedanken gemacht, wie man mit den eingeschränkten Möglichkeiten von Delphi etwas realisieren kann, was bequem ist, und einen echten Fortschritt zur bestehenden Situation bringt. Das direkte interpolieren der Variablen geht leider nicht, da müsste Embarcadero den Compiler anpassen. Also blieb nur noch die Möglichkeit irgendwie die Datentypen zu dynamisieren, und ein Format zu finden welches ein wenig intuitiver als das welches Delphi verwendet. (Der Ursprung liegt hier übrigens in sprintf von C und C++)

Variante Parameter, und dynamische Parametersignatur

Das man die Parameter übergeben kann, ohne vorher umzuwandeln, hat die meiste Überlegung gebraucht. Variant währe eine Möglichkeit gewesen, nur ich fürchte dann wäre es mit Klassen und Records schwierig geworden. Ich wollte keine Pointer verwenden, sondern modernes Delphi. Also habe ich an Generics gedacht. Die kann man leider nicht direkt in einer Unit realisieren, es muss immer eine Klasse (oder Record) sein. Viele wissen gar nicht, das man den Typ bei einem generischen Aufruf nicht anzugeben braucht, der typ wird vom Compiler ermittelt und übertragen. Deshalb reicht ein einfacher Aufruf.

Zu den Parametern: Mit Generics, fällt die Möglichkeit der optionalen Parametern schonmal weg. Also bleibt standardmäßig nur noch das überladen von Funktionen. Hierfür habe ich mich entschieden, und 9 Funktionen, für 9 Parameter angelegt. Das sollte reichen.

Es gäbe noch eine wenig bekannte Möglichkeit über CDECL etwas zu erreichen, was wie bei "writeln" funktioniert. Allerdings befürchte ich hier Schwierigkeiten, das es hier um DLL-Kompatibilität geht und ggf Generics eingeschränkt wären.

So siehts aus:

//Auszug aus dem Interface-Part der Klasse

class function ToString<T1>(const msg: string; val1: T1): string; reintroduce; overload;
class function ToString<T1, T2>(const msg: string; val1: T1; val2: T2): string; reintroduce; overload;
class function ToString<T1, T2, T3>(const msg: string; val1: T1; val2: T2; val3: T3): string; reintroduce; overload;

Das 'reintroduce' wird hier übrigens benötigt, um die Vererbungskette von ToString zu unterbrechen. Die Funktion "schattiert" also das originale ToString

Explizite Operator Überladung

Und noch ein Kniff kommt beim Aufruf in der neuen Version 2 dazu:

TStr('Test'), ist nicht einfach eine Funktion, sondern eine überladenes Typcast. Das war erforderlich, da Delphi Generics nur für Klassen und Records unterstützt. Das Ergebnis des Typcasts ist ein TStr-Record, welcher dann die Methoden ToString() und Params() zur Verfügungs stellt, welche dann die eigentliche Arbeit übernehmen.

Die Innerreien von NetFormat 

Um nun nach der Parameterübergabe weiterarbeiten zu können, brauchts erstmal einen varianten Typ, welcher die extrem verschiedenen Typen alle abdecken kann. Delphi bietet hierfür TValue. Was sich harmlos anhört, ist ein Typ, welcher die Daten beliebiger Typen aufnimmt, und auch wieder zurückgeben kann. Zudem kann man hier TypenInfos ermitteln. Dank der Typeninfos, kann nun je nach Typ reagiert werden, und mit einer passenden Strategie nach String umgewandelt werden. In vielen Fällen reicht dann auch ein simples ToString(). Wenn es um Zahlen und Datums-Werten handelt greift dann aber eine Strategie welche FloatToStrF, IntToStr, Round und natürlich DateToStr und FormatDateTime verwendet. Für die Länge kommt zudem Format zum Einsatz. Also im wesentlichen nichts geheimnisvolles, nur Delphi-Standard. Die Ausnahme bildet FormatDateTime. Die angelieferten Format-Spezifizierer müssen nochmal übersetzt werden.

Hier mal ein kleiner Überblick:

//verarbeiten von 'C' und 'D' welche jeweils als Integer oder Float-Typ vorliegen können

if self.fType.ToLower() = 'c' then
begin;
   tmpdbl:= TValue.From<T>(val).AsExtended();
   result:=FloatToStrF(tmpdbl, ffCurrency, 10, fLength);
end
else if self.fType.ToLower() = 'd' then
begin;
     if ttypeInfo.Kind = tkFloat then
        tmpint:= Round(TValue.From<T>(val).AsExtended())
     else
        tmpint:= TValue.From<T>(val).AsInt64();

Mögliche Verbesserungen

Ich werde hier noch ein wenig herumexperimentieren: Möglicherweise geht ja eine "Default-Property" und als Name für die Klasse nimmt man was kurzes, z.B. TStr, wie in Java, also vielleicht ein TStr('Hello {0}', 'world'). Und vielleicht erlaube ich ja auch benannte Spezifizierer, also TStr('Hello {world}', 'world'). Ihr könnt gespannt sein.

So nun aber die Links, auf die Ihr den ganzen Artikel lang "hingelesen" habt:

Download und Source

https://github.com/VoSs2o0o/NetFormat

https://github.com/VoSs2o0o/NetFormat/archive/refs/tags/V1.0.0.zip

 

 


Keine Kommentare zu “Delphi: Besserer Format-Befehl”

Kommentar hinterlassen

Als Antwort auf Some User