Derselbe Betrag von 1234,56 sieht in Deutschland, Frankreich und der Schweiz unterschiedlich aus. Trennzeichen, Stellung des Währungszeichens und sogar der Abstand davor unterscheiden sich. Wer das von Hand pflegt, pflegt es irgendwann falsch.
Im Bild geht ein Zahlenwert in einen Formatierer und kommt in mehreren Schreibweisen heraus. Diese Aufgabe erledigt die Intl-Erweiterung von PHP.
Was PHP numfmt_create() macht
Der Aufruf von PHP numfmt_create erzeugt einen Formatierer für einen Sprachraum und einen Typ. Die Signatur lautet numfmt_create(string $locale, int $style, ?string $pattern = null). Zurück kommt ein Objekt, das anschließend beliebig oft verwendet werden kann.
<?php
$betrag = 1234.567;
foreach (['de_DE', 'fr_FR', 'de_CH'] as $sprache) {
$fmt = numfmt_create($sprache, NumberFormatter::CURRENCY);
echo $sprache . ': ' . numfmt_format_currency($fmt, $betrag, 'EUR') . PHP_EOL;
}
/* de_DE: 1.234,57 €
fr_FR: 1 234,57 €
de_CH: € 1'234.57 */
Drei Sprachräume, drei Schreibweisen, ein einziger Codepfad. Die Schweiz nutzt den Apostroph als Tausendertrennung und stellt das Währungszeichen voran, Frankreich trennt mit einem schmalen Leerzeichen. Keine dieser Regeln steht im eigenen Code.
Wie bei den übrigen Intl-Funktionen gibt es auch hier eine Klassenform. new NumberFormatter($sprache, $stil) ist gleichbedeutend mit dem Aufruf oben.
Die englische Beschreibung aus dem Handbuch ist knapp, nennt aber die Begriffe, die in der weiteren Dokumentation immer wieder auftauchen:
numfmt_create(string $locale, int $style, ?string $pattern = null): ?NumberFormatter
/* locale - one of the supported locales, e.g. de_DE or fr_FR
style - the formatting style, see the constants below
pattern - an optional pattern, syntax depends on the style
return - a formatter object, or null on failure
DECIMAL, CURRENCY and PERCENT use ICU DecimalFormat.
SPELLOUT and ORDINAL use ICU RuleBasedNumberFormat,
which is why their output reads like written words.
See the ICU documentation and the PHP manual for the
full pattern syntax. */
Die Unterscheidung in der letzten Hälfte erklärt ein Verhalten, das sonst willkürlich wirkt: Die zahlenorientierten Typen und die wortorientierten stammen aus zwei verschiedenen ICU-Formatierern, und deshalb wirken Attribute wie die Zahl der Nachkommastellen nur auf die erste Gruppe.
Die Regeln selbst stammen nicht aus PHP, sondern aus den ICU-Daten, einer Bibliothek mit Sprach- und Länderinformationen, die auch hinter der Datumsformatierung steckt. Deshalb kennt der Formatierer Schreibweisen, die man selbst nie pflegen wollte, und deshalb kann das Ergebnis zwischen zwei PHP-Installationen minimal abweichen, wenn dort unterschiedliche ICU-Versionen liegen.
number_format() oder numfmt_create()?
Die eingebaute Funktion number_format() schreibt die Trennzeichen so, wie man sie ihr übergibt. Sie weiß nichts von Sprachen. PHP numfmt_create kennt für jeden Sprachraum die richtigen Trennzeichen, die übliche Zahl der Nachkommastellen und die Stellung des Währungszeichens.
Daraus folgt eine einfache Entscheidungsregel. Eine Oberfläche, die es nur in einer Sprache gibt, kommt mit der einfachen Funktion bestens zurecht, und PHP number_format() zum sicheren Formatieren von Zahlen zeigt, wie das geht. Sobald eine zweite Sprache dazukommt, wird die Pflege der Trennzeichen von Hand zur Fehlerquelle, und dann lohnt der Umstieg.
Die fünf Typen von PHP numfmt_create()
| Typ | Zweck | Ausgabe auf Deutsch |
DECIMAL | gewöhnliche Zahlen | 1.234,567 |
CURRENCY | Beträge mit Währung | 1.234,57 € |
PERCENT | Anteile | 42 % |
SPELLOUT | Zahl als Wort | eintausendzweihundert… |
ORDINAL | Ordnungszahl | 3. |
Dazu kommt CURRENCY_ACCOUNTING, das negative Beträge in Klammern setzt, wie es die Buchhaltung erwartet. Der Typ wird beim Erzeugen festgelegt und lässt sich danach nicht mehr ändern; für einen anderen Zweck entsteht ein zweiter Formatierer.
Preise mit Währungssymbol ausgeben
Neben PHP numfmt_create gibt es für Beträge einen eigenen Aufruf, der den Währungscode entgegennimmt. Das ist wichtig, denn ein Shop zeigt französischen Kunden Preise in Euro und nicht in einer Währung, die sich aus der Sprache ableitet.
<?php
$fmt = numfmt_create('de_DE', NumberFormatter::CURRENCY);
echo numfmt_format_currency($fmt, 1234.5, 'EUR'); /* 1.234,50 € */
echo numfmt_format_currency($fmt, 1234.5, 'USD'); /* 1.234,50 $ */
echo numfmt_format_currency($fmt, 1234.5, 'CHF'); /* 1.234,50 CHF */
/* Fuer den Buchhaltungsexport wird statt des Symbols
der dreistellige Code gebraucht. */
$export = numfmt_create('de_DE', NumberFormatter::CURRENCY);
numfmt_set_symbol($export, NumberFormatter::CURRENCY_SYMBOL, 'EUR');
echo numfmt_format_currency($export, 1234.5, 'EUR'); /* 1.234,50 EUR */
Die Anzeige bekommt also das Symbol, der Export den Code, und beides stammt aus derselben Quelle. Umgerechnet wird dabei nichts: Der Formatierer stellt einen Betrag dar, er kennt keine Wechselkurse.
Prozentwerte richtig formatieren
Hier lauert der häufigste Denkfehler im Umgang mit PHP numfmt_create. Der Prozent-Typ multipliziert selbst mit hundert, weil er einen Anteil erwartet.
<?php
$fmt = numfmt_create('de_DE', NumberFormatter::PERCENT);
/* Falsch: der Wert ist schon in Prozent gerechnet */
echo numfmt_format($fmt, 42); /* 4.200 % */
/* Richtig: der Anteil wird uebergeben */
echo numfmt_format($fmt, 0.42); /* 42 % */
Wer eine bereits umgerechnete Zahl anzeigen will, teilt sie also vorher wieder durch hundert oder greift zum Dezimaltyp und hängt das Zeichen selbst an. Der erste Weg ist sauberer, weil auch das Prozentzeichen und der Abstand davor sprachabhängig sind.
Zahlen als Text ausschreiben
Für Sprachausgaben, Schecks und barrierefreie Oberflächen wird die ausgeschriebene Form gebraucht, und PHP numfmt_create liefert sie ohne eigene Wortliste. Selbst gebaute Funktionen dafür sind lang und scheitern regelmäßig an großen Zahlen.
<?php
$fmt = numfmt_create('de_DE', NumberFormatter::SPELLOUT);
echo numfmt_format($fmt, 1234);
/* eintausendzweihundertvierunddreissig */
$ordnung = numfmt_create('de_DE', NumberFormatter::ORDINAL);
echo numfmt_format($ordnung, 3); /* 3. */
Die Sprachdaten dafür bringt die Erweiterung mit, es ist also keine eigene Wortliste nötig. Bei sehr großen Zahlen wird das Ergebnis lang, aber es bleibt korrekt.
Nachkommastellen und Rundung in PHP numfmt_create()
Standardmäßig richtet sich die Zahl der Nachkommastellen bei PHP numfmt_create nach dem Typ und dem Sprachraum. Über Attribute lässt sich das genau festlegen.
<?php
$fmt = numfmt_create('de_DE', NumberFormatter::DECIMAL);
numfmt_set_attribute($fmt, NumberFormatter::FRACTION_DIGITS, 3);
echo numfmt_format($fmt, 2.5); /* 2,500 */
/* Tausendertrennung abschalten, etwa fuer Jahreszahlen */
numfmt_set_attribute($fmt, NumberFormatter::GROUPING_USED, 0);
echo numfmt_format($fmt, 2026); /* 2026 */
/* Rundungsverhalten festlegen */
numfmt_set_attribute($fmt, NumberFormatter::ROUNDING_MODE,
NumberFormatter::ROUND_HALFUP);
Ein wichtiger Hinweis dazu: Das Rundungsattribut betrifft ausschließlich die Anzeige. Wer mit Geld rechnet, rechnet vorher in kleinsten Einheiten oder mit einer dafür gebauten Bibliothek. Eine Differenz zwischen angezeigtem und gebuchtem Betrag lässt sich mit einem Formatierer nicht beheben, sie entsteht eine Ebene tiefer.
Zahlen aus Text einlesen
Eine deutsche Eingabe wie 1.234,56 in einen Zahlenwert zu verwandeln, ist mit Bordmitteln unangenehm. Die einfache Typumwandlung verliert dabei stillschweigend Daten.
<?php
$eingabe = '1.234,56';
/* Bordmittel: liefert 1.234 und verwirft den Rest */
var_dump((float) $eingabe); /* float(1.234) */
/* Sauber: der Formatierer kennt die Schreibweise */
$fmt = numfmt_create('de_DE', NumberFormatter::DECIMAL);
$wert = numfmt_parse($fmt, $eingabe);
if (numfmt_get_error_code($fmt) !== 0) {
exit('Keine gueltige Zahl: ' . numfmt_get_error_message($fmt));
}
var_dump($wert); /* float(1234.56) */
Die Fehlerprüfung gehört dazu, weil ein fehlgeschlagener Versuch false liefert und sich das nicht sicher vom Wert null unterscheiden lässt. Wer anschließend prüfen will, ob wirklich eine Ganzzahl vorliegt, findet die Feinheiten in PHP is_int() und die Typprüfung mit Integer. Für Beträge mit Währungszeichen gibt es zusätzlich numfmt_parse_currency(), das den erkannten Code über einen Referenzparameter zurückgibt.
flowchart TD
A[Zahl ausgeben] --> B{Nur eine Sprache}
B -->|ja| C[number_format genuegt]
B -->|nein| D{Welche Art}
D -->|Betrag| E[CURRENCY]
D -->|Anteil| F[PERCENT]
D -->|Wort| G[SPELLOUT]
D -->|Zahl| H[DECIMAL]
Die erste Verzweigung ist die wichtigste. Sie verhindert, dass eine einsprachige Anwendung eine Erweiterung voraussetzt, die sie gar nicht braucht.
Verfügbarkeit und Wiederverwendung
Zwei praktische Punkte zu PHP numfmt_create zum Schluss. Der erste betrifft Server, auf denen die Intl-Erweiterung fehlt: dort bricht jeder Aufruf ab, weshalb eine Prüfung mit extension_loaded('intl') und eine schlichte Rückfallebene über number_format() sinnvoll sind.
Der zweite betrifft die Geschwindigkeit. Beim Erzeugen lädt der Formatierer die Sprachdaten, und genau das ist der teure Schritt. Das Formatieren selbst kostet fast nichts.
<?php
/* So nicht: in jeder Zeile ein neuer Formatierer */
foreach ($positionen as $p) {
$fmt = numfmt_create('de_DE', NumberFormatter::CURRENCY);
echo numfmt_format_currency($fmt, $p['preis'], 'EUR');
}
/* Besser: einmal erzeugen, tausendfach nutzen */
$fmt = numfmt_create('de_DE', NumberFormatter::CURRENCY);
foreach ($positionen as $p) {
echo numfmt_format_currency($fmt, $p['preis'], 'EUR');
}
Bei einer Tabelle mit fünftausend Zeilen macht dieser Unterschied sich im Seitenaufbau bemerkbar. Datumsangaben verhalten sich übrigens genauso, wie das Schwestertutorial zu datefmt_create() zeigt.
Fazit
PHP numfmt_create nimmt einem alle Regeln ab, die zwischen Sprachräumen unterschiedlich sind, von der Tausendertrennung bis zur Stellung des Währungszeichens. Fünf Typen decken vom einfachen Zahlenwert bis zur ausgeschriebenen Form alles ab, und mit numfmt_parse() gibt es endlich einen verlustfreien Weg zurück. Wichtig bleibt die Trennung im Kopf: Der Formatierer ist für die Anzeige zuständig, gerechnet wird woanders.