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

Advertisements

Ein Gedanke zu “Umlautproblemen in Tntnet-Applicationen mit Hilfe von cxxtools aus dem Weg gehen

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s