Fortschritte des Tntnet-Witzard

Standard

Aktuell arbeite ich an dem webbasierten Wizard-Tool für Tntnet-Projekte. Wie vor einiger Zeit in dem Artikel „Proof of concept für ein alternatives Build-System für Tntnet“ angekündigt, habe ich das Build-Tool reinimplementiert, und zwar in Python. Ausschlaggebend war die Erfahrung, dass mein Ruby-Code unter RHEL6/CentOS6 nicht so einfach zum Laufen zu bekommen ist. Das führte dazu, dass ich meinen Tntnet-Wizard nicht auf den Server übersetzen und installieren konnte. Die Reimplementierung ging einigermaßen zügig,  hat mich jedoch trotzdem einige Tage Zeit gekostet.

Mit dem eigentlichen Programm bin ich jetzt soweit, dass man den Kern einer Tntnet-Application zusammenklicken und herunterladen kann. Derzeit arbeite ich daran, das der Projekt-Kern durch Komponenten erweitert werden kann. Dies soll ebenfalls graphisch über eine Weboberfläche gehen. Wenn der Schritt fertig ist, plane ich eine GitHub-Unterstützung zu intrigieren. So das man Code aus GitHub in das Programm laden, neue Komponenten hinzufügen, den generierten Code herunterladen, den Code in der IDE, die man sonst verwendet, erneut prüfen und wieder in GitHub commiten.

Das Buildtool, das ich mir gebaut habe, heißt Tntmake und ist hier zu bekommen: https://github.com/OlafRadicke/tntmake

Das Tool zum Generieren von Tntnet-Projekten trägt den Namen Tntwebwizard und ist hier zu finden: https://github.com/OlafRadicke/tntwebwizard

Tntmake verwende ich seit ein paar Wochen und es arbeitet (für meine Zwecke) sehr gut. Woran ich mir ein bisschen die Zähne ausgerissenen habe, war das Thema „uppercase in c++„. In dem Artikel umlautproblemen in Tntnet-Applicationen mit Hilfe von cxxtools aus dem weg gehen habe ich bereits festgestellt, dass meine Funktion, die aus Kleinbuchstaben Großbuchstaben machen soll, mit Umlauten nicht funktioniert. Jetzt fand ich mich an dem Punkt wieder, dass ich für meinen Tntwebwizard erneut eine Funktion brauchte, die Strings in uppercase formatiert.

Also habe ich versucht das Problem mit cxxtools::toupper() zu lösen. Es ist, mir leider nicht gelungen. Mangels Dokumentation habe ich nicht herausgefunden, ob und wie es geht. Ich habe mir die Headerdateien von der Classe cxxtools::Char angeschaut und zwei Stunden herumprobiert, jedoch keine Lösung gefunden. Mir ist es zwar gelungen, alle Zeichen in Großbuchstaben umzuwandeln, ohne dass die Umlaute zerbrechen, aber die Umlaute werden leider nicht in Großbuchstaben umgewandelt. Ein“ä“ bleibt ein „ä“ und wird nicht zu einem „Ä“.

Der Code sieht so aus:


#include <cxxtools/utf8codec.h>
#include <cxxtools/string.h>
#include <cxxtools/char.h>
[...]
01| std::string NewModelData::toUpper( std::string _mixedString ){
02|     std::ostringstream upperString;
03|     cxxtools::String lastLetter;
04|     cxxtools::String uMixedString = cxxtools::Utf8Codec::decode( _mixedString );
05|     for ( cxxtools::String::size_type i=0;
06|         i < uMixedString.length();
07|         ++i
08|     ){
09|         upperString << cxxtools::toupper(uMixedString[i]);
10|     }
11|     return upperString.str();
12| }

Dann habe ich nach einer Lösung mit reinem C++Standard-Libs gesucht, und bin dann auf diese kompakte Lösung gekommen:


01| std::string toUpper( std::string _mixedString ){
02|     std::ostringstream upperString;
03|     std::locale loc;
04|     for (std::string::size_type i=0; i<_mixedString.length(); ++i)
05|         upperString << std::toupper(_mixedString[i],loc);
06|     return upperString.str();
07| }

Die String-Verarbeitung in C++ ist immer ziemlich krampfig, zwar nicht ganz so schlimm wie in C, aber trotzdem unbefriedigend. Die cxxtools bringen hier leider auch keine befriedigenden Lösung.

Ich habe gesehen, dass es wohl noch eine Lösung mit Boost gibt. Allerdings würde ich die Abhängigkeit zu Boost gerne vermeiden. Vielleicht komme ich noch auf eine andere Lösung, dann werde ich es berichten.


Creative Commons Lizenzvertrag

Advertisements

Umlautproblemen in Tntnet-Applicationen mit Hilfe von cxxtools aus dem Weg gehen

Standard

In der Vergangenheit hatte ich, dank der strenge Typisierung, mit C++ selten Probleme mit UTF-8 und Umlauten. Ganz anders in Python2.x, Ruby und Bash. Hier habe ich schon Stunden und Tage damit zugebracht, Fälle abzufangen, in denen ich einen unerwarteten String-Codierung bekam. Die – vermeintliche – Zeitersparnis, die ich durch Duck-Typing habe, bezahle ich später durch Laufzeit-Typ-Prüfung doppelt und dreifach wieder zurück. Zum Einen ist der Code mit Duck-Typing ineffizienter und langsamer, zum Anderen schreibe ich dann doch wieder mehr Code, weil ich Duck-Types auf ihren jeweiligen Type testen muss, um ggf nicht ASCII-Code mit Unicode vergleichen bzw. verarbeiten zu müssen. So wird Duck-Typing zu einer „Informatik-Schuldenfalle“, in der man am Ende soviel drauf zahlt, das nie wieder erwirtschaften kann. Zur Ehrenrettung von Skriptsprache: Für Prototyping haben Python, Ruby(-on Rails) und Co ihre Berechtigung, aber für den produktiven Betrieb von Software, die einen längeren Lebenszyklus hat, ist C++ nach wie vor ökonomischer.

Jetzt  zu C++ und meinem Ausgangsproblem. In meiner eccp-View-Datei durchlaufe ich in einer for-Schleife eine Liste mit Objekten die eine Eigenschaft „Name“ haben. Diese Namen sind alphabetisch in der Liste vorsortiert. Ich will sie als Liste auf einer Website ausgeben.

Um die Liste etwas übersichtlicher zu gestalten, habe ich mir überlegt, dass ich für jeden Anfangsbuchstaben einen Abschnitt mit einer Überschrift anlegen möchte, so das alle Namen, die mit „A“ anfangen, die Überschrift „A“ bekommen und die mit „C“ beginnen eine Überschrift „C“ erhalten… Der Algorithmus ist recht überschaubar und leicht verständlich:


01| % string lastLetter = "";
02| % for ( unsigned int i=0; i<s_keywordTitlesCounts.size(); i++) {
03| %     if ( lastLetter != s_keywordTitlesCounts[i].Name.substr (0,1) ) {
04| %         lastLetter = s_keywordTitlesCounts[i].Name.substr (0,1);
05|          <h3 id="letter_" >
08| % } 

In Zeile 01 definiere ich eine Variable, in der ich den letzten Anfangsbuchstaben speichere. In Zeile 02 beginnt der Schleifendurchlauf. In Zeile 03 überprüfe ich, ob der Anfangsbuchstabe schon aufgetaucht ist. Die Methode ".substr(0,1)" gehört zur Klasse std::string und gibt das erste Zeichen des String zurück. Wurde der Anfangsbuchstabe noch nicht verwendet, wird er in Zeile 04 in der Variable "lastLetter" gespeichert. Danach wird in Zeile 05-07 die Überschrift angelegt.

Der Algorithmus funktioniert recht gut, bis ein Name mit einen Umlaut oder Sonderzeichen beginnt. Dann sieht man an der Stelle, an der das
Sonderzeichen stehen sollte ein ? oder ein anderes sonderbares Zeichen. Was ist passiert?

Ein "std::string" ist de facto ein "std::basic_string<char>" und der „char" ist der da runter liegende Basis-Datentyp. Der char-Type hat nur 1 Byte. Der Buchstabe „ä“ braucht jedoch in der utf-8 Form 2 Bytes, um gespeichert werden zu können, nämlich „\xc3\xa4„. Mit der string-Funktion ".substr(0,1)" wird aber nur das 1. Zeichen, bzw. das 1. Byte zurückgegeben, also der char, mit dem Bytewert „\xc3„. Das ist aber nur die Hälfte von dem Umlaut. Die zeichenweise Verarbeitung inklusive substr-Funktion macht bei einem std::string deshalb keinen Sinn, wenn man utf-8 verwendet. Genauso verhält es sich übrigens auch mit stringstreams.

Es gibt eine ganze Reihe von Lösungsansätzen für dieses Problem. Dabei kann auf die Boost-Lib zurückgegriffen werden;  oder C++11-Fatures genutzt werden;  das Qt-Framework hat ebenfalls eine eigene String-Implementiierung namens „QString„, die ich auch ziemlich cool finde und mit der sich das Problem in den Griff kriegen lässt.

Eine weitere Lösung für Unicode-Strings findet sich im cxxtools-Framwork. Dazu gibt es die „cxxtools::String", welche auf std::basic_string<cxxtools::Char> basieren. Und die „cxxtools::Char" können auch Unicode Zeichen repräsentieren und somit auch ein ‚ä‘ beinhalten. Mit der Funktion „cxxtools::utf8codec::encode(cxxtools::String)" bekommt man den Utf-8 Repräsentanten des Strings. Mit „cxxtools::utf8codec::decode(std::string)" wird ein utf-8-kodierten String in „cxxtools::String" konvertiert.

Wenn man Tntnet verwendet, hat man cxxtools automatisch dabei, von daher wäre es naheliegend cxxtools::String zu verwenden. Das sähe in meinem Beispiel so aus:


01| 
02|     #include <cxxtools/utf8codec.h>
03|     #include <cxxtools/string.h> 
04| 
[...]
05| %     cxxtools::String lastLetter;
06| %     cxxtools::String uName = cxxtools::Utf8Codec::decode( s_keywordTitlesCounts[i].Name );
07| %     if ( lastLetter != uName.substr (0,1) ) {
08| %         lastLetter = uName.substr (0,1);
09|          <h3 id="letter_" >
12| % } 

In Zeile 01-04 werden die nötigen Header-Datein eingebunden. In Zeile 05 kommt cxxtools::String zu ersten Mal zum Einsatz. In der nächsten Zeile 06 konvertiere ich den normalen C++-String in einen UTF8-Cxxtools-String. Jetzt bekomme ich auch in Zeile 10 den Wert bzw. den Buchstaben zurück,, den ich erwartet habe. Wird ein cxxtools::String auf einem std::ostream ausgegeben, dann wird er automatisch utf-8-kodiert. Genau das passiert in tntnet in den -Tags (Zeile 10).

Idealerweise verwendet man in der ganzen Applikation cxxtools::Strings. In meiner eigenen Anwendung ist mir jetzt aufgefallen, dass dieser Algorithmus ebenfalls nicht mit Umlauten funktioniert:


01| std::string OString::LowerCase ( std::string keywords ) {
02|     keywords = StrReplace ( "A", "a", keywords);
03|     keywords = StrReplace ( "B", "b", keywords);
04|     keywords = StrReplace ( "C", "c", keywords);
[...]
05|     keywords = StrReplace ( "X", "x", keywords);
06|     keywords = StrReplace ( "Y", "y", keywords);
07|     keywords = StrReplace ( "Z", "z", keywords);
08|     keywords = StrReplace ( "Ä", "ä", keywords);
09|     keywords = StrReplace ( "Ö", "ö", keywords);
10|     keywords = StrReplace ( "Ü", "ü", keywords);
11|     return keywords;
12| }
13| 
14| std::string OString::StrReplace (
15|     const std::string s_string,
16|     const std::string r_string,
17|     const std::string i_string
18| ) {
19|   int pos;
20|   // return string
21|   std::string b_string = i_string;
22|   while (true) {
23|     pos = b_string.find(s_string);
24|     if (pos == -1) {
25|       break;
26|     } else {
27|       b_string.erase(pos, s_string.length());
28|       b_string.insert(pos, r_string);
29|     }
30|   }
31|   return b_string;
32| }

Das Problem lauert hier wieder in Zeile 27-28. Alternativ könnte hier wieder cxxtools::String helfen. Ich habe jedoch gesehen, dass es noch eine undokumentierte Funktion namens CXXTOOLS_API Char cxxtools::toupper( const Char & ch) gibt. Möglicherweise brauche ich meinen eigenen Algorithmus gar nicht. Wenn ich es herausgefunden habe, werde ich es hier berichten.

Siehe auch:


Creative Commons Lizenzvertrag