Zahlen formatieren (behind the scenes)

Wie funktioniert nun eigentlich die Anpassung an ein spezifisches Land bei der Ausgabe von Zahlen? Nicht std::locale erledigt das, sondern die darin gespeicherte Facette std::numpunct.

Video

Quelltext

#include <iostream>
#include <locale>
#include <cmath>
#include <climits>
#include <algorithm>
#include <iterator>

std::ostream &print(std::ostream &os, int value)
{
    std::numpunct<char> const &np = std::use_facet<std::numpunct<char>>(os.getloc());
    if (value < 0) {
        os.put('-');
    }
    if (value == 0) {
        os.put('0');
    }
    else {
        int tmp = std::abs(value);
        std::string groups = np.grouping();
        std::string nf;
        std::size_t curr_group = 0;
        std::size_t curr_digit = 0;
        char last_group = groups[0];
        while (tmp != 0) {
            if (last_group != CHAR_MAX && last_group > 0 && 
                curr_digit >= static_cast<std::size_t>(last_group)) {
                ++curr_group;
                if (curr_group < groups.size()) {
                    last_group = groups[curr_group];
                }
                nf += np.thousands_sep();
                curr_digit = 0;
            }
            nf.append(std::to_string(tmp % 10));
            tmp /= 10;
            ++curr_digit;
        }
        std::copy(nf.rbegin(), nf.rend(), std::ostream_iterator<char>(os));
    }
    return os;
}

int main()
{
    print(std::cout, 1234567890) << std::endl;

    std::cout.imbue(std::locale("de_DE.UTF-8"));
    print(std::cout, 1234567890) << std::endl;

    std::cout.imbue(std::locale("en_US.UTF-8"));
    print(std::cout, 1234567890) << std::endl;
}

Erklärung

Eine std::locale mag zwar die Möglichkeit zur Kapselung von länderspezifischem Verhalten bereitstellen, allerdings realisiert sie selbst keinen Aspekt davon. Sie bietet lediglich eine Schnittstelle zum Speichern und Auffinden einzelner Facetten. Diese Facetten realisieren letztlich die Anpassung an eine bestimmte Kultur.

Im Beispiel oben verwenden wir die std::numpunct-Facette, welche die notwendigen Informationen zur länderspezifischen Ausgabe von Zahlen bereitstellt. Mittels std::use_facet<std::numpunct<char>> (Zeile 10) erhalten wir das entsprechende Objekt aus der Locale, die wir gerade betrachten. Diese wiederum bekommen wir von unserem Ausgabestream mittel std::ostream::getloc(). std::use_facet sucht anhand des übergebenen Typs die passende Facette im locale-Objekt und castet sie auf den passenden Typ. Wenn das klappt, erhalten wir eine Referenz und können in Zukunft mit dem Objekt arbeiten. Die eigentliche Schnittstelle der Facette ist spezifisch für deren entsprechende Aufgabe. std::numpunct stellt bspw. Methoden bereit, um den Tausendertrenner zu ermitteln (std::numpunct::thousands_sep(), Zeile 31) oder die Gruppengrößen, die jeweils durch diesen getrennt werden (std::numpunct::grouping(), Zeile 19).

Das Grouping wird in einem std::string zurückgeliefert. Hierbei enthält das erste Zeichen die Länge der ganz rechten Gruppe in der Zahl (also die erste Gruppe links des Kommas), der nächste Eintrag enthält die nächste Gruppe und so weiter. Der Standard spezifiziert hier zwei Spezialfälle bei der Interpretation der Gruppengrößen: ist der Wert kleiner als 0 oder gleich CHAR_MAX, dann ist die Gruppe unendlich groß. Davon macht bspw. die C-Locale gebrauch, die keine Tausendertrennzeichen verwendet. Dort ist nur eine Gruppe unendlicher Größe spezifiziert. Wenn die Gruppen hingegen eine endliche Größe haben, dann sind sie nach und nach abzuarbeiten bei der Ausgabe der Zahl. Kommt man dabei ans Ende der Gruppenliste an, dann erhalten alle weiteren Gruppen die Größe der zuletzt spezifizierte. Das nutzt bspw. die deutsche Locale, indem sie nur eine Gruppe der Länge 3 spezifiziert, die dann bei der Ausgabe immer wieder wiederholt werden muss.

Der Algorithmus oben ermittelt die einzelnen Ziffern der Zahl durch die Division mit Rest. Das Resultat ist dabei in umgekehrter Reihenfolge (die Einerstelle zuerst). Daher muss es vor der eigentlichen Ausgabe noch mit Hilfe der Reverse-Iteratoren rbegin() und rend() in Zeile 38 umgedreht werden.