code it

Martins Tech Blog

Lesbare Bytes

Das Problem ist einfach umrissen: Man hat die Größe einer Datei oder (auch gern genommen) eine Größenbeschränkung in Bytes vorliegen und möchte diese dem Benutzer anzeigen. Aber nur die wenigsten Benutzer können mit der Größenangabe 1.048.576 Bytes etwas anfangen. Also möchte man die Größe dem Benutzer in ihm bekannten Größenangaben anzeigen - so wie man es von Windows kennt. Das .NET Framework selbst kennt meines Wissens eine solche Funktion nicht und daher ist es jedem Entwickler selbst überlassen, wie er diese Umrechnung vornimmt.

Auf meiner Suche nach einer Lösung für genau dieses Problem bin ich über einige Lösungsvorschläge bei stackoverflow gestolpert und einen davon fand ich extrem gut, so dass ich dem Problem und möglichen Lösungen diesen Blogpost widmen möchte.

1. Lösung: While-Loop

Dieser Ansatz wird gern genommen und liegt ja in der prozeduralen Denke auch auf der Hand: Man dividiert so lange durch 1024, bis eine Zahl übrig bleibt, die kleiner als 1024 ist. Hat man sich nun noch gemerkt, wie häufig man dies angewandt hat, so weiß man auch was man als Einheit dahinter schreiben muss.

var unit = new[] { "B", "KB", "MB", "GB", "TB", "PB" };
var index = 0;
var value = bytes;

while (value >= 1024)
{
 index++;
 value /= 1024;
}

var readable = string.Format("{0} {1}", value, unit[index]);

 

2. Lösung: Windows bemühen

Wie ich schon einleitend beschrieben habe, kann Windows diese Berechnung ja. Warum also nicht mit unmanaged Code auf die passende Windows-Funktion zugreifen? Die Shlwapi.dll stellt eine entsprechende Funktion bereit, mit der diese Formatierung vorgenommen werden kann.

[DllImport("Shlwapi.dll", CharSet = CharSet.Auto)]
public static extern long StrFormatByteSize(
 long fileSize,
 [MarshalAs(UnmanagedType.LPTStr)] StringBuilder buffer,
 int bufferSize);


var sb = new StringBuilder(11);
StrFormatByteSize(bytes, sb, sb.Capacity);
var readable = sb.ToString();

OK, es ist eine Möglichkeit, aber keine die ich präferieren würde.

 

3. Lösung: Mathematisch

Und nun zu der Lösung, auf die ich zugegebenermaßen nicht gekommen bin, die ich aber sehr charmant finde. Mit Hilfe nur weniger mathematischer Funktionen lässt sich die Schleife aus der Lösung 1 ersetzen. Durch eine geschickte Kombination von Logarithmus-, Potenz- und Rundungsfunktionen lässt sich sowohl der gerundete Wert als auch die anzuzeigende Einheit ermitteln.

var unit = new[] { "B", "KB", "MB", "GB", "TB", "PB" };
var index = Convert.ToInt32(
    Math.Floor(Math.Log(Math.Abs(bytes), 1024)));
var value = Math.Round(bytes / Math.Pow(1024, index), 2);
var readable = string.Format("{0} {1}", value,  unit[index]);

Einziger Wert für den diese Variante nicht funktioniert ist 0 Byte.

Wie ganz häufig, so gibt es auch hier viele Wege, die zum Ziel führen und es bleibt jedem selbst überlassen, welchen Weg er wählt.

back to basics: Geschweifte Klammern in string.Format

Geschweifte Klammern verwendet man in string.Format, um Parameter in Strings unterzubringen und diese ggf. noch zu formatieren. Den meisten sollten Ausdrücke wie dieser bekannt vorkommen:

int i = 23;
string s = string.Format("{0}", i); // Ergebnis: "23"

Wie geht man nun vor, wenn man wirklich einmal geschweifte Klammern ausgeben möchte? Die Escapesequenz für geschweifte Klammern sind 2 geschweifte Klammern:

int i = 23;
string s = string.Format("{{{0}}}", i); // Ergebnis: "{23}"..

So weit ist das alles ganz einfach. Interessant wird es, wenn man den Ausdruck etwas komplexer gestaltet und Formatoptionen verwendet:

int i = 23;
string s = string.Format("{0:N}", i); // Ergebnis: "23.00"

int i = 23;
string s = string.Format("{{{0:N}}}", i); // Ergebnis: "{N}"

Das erste Beispiel erschließt sich von selbst. Mittels "N" wird spezifiziert, dass es sich um eine Zahl handelt, die in Standardformatierung angezeigt wird. Warum aber klappt es im zweiten Beispiel nicht, um diese Zahl geschweifte Klammern zu setzen?

Dazu muss man sich anschauen, wie string.Format vorgeht: Wird ein Format angegeben, wird zunächst geprüft, ob die Formatzeichenfolge länger als ein Zeichen ist. Ist das der Fall, geht die Methode davon aus, dass es sich um eine benutzerdefinierte Formatzeichenfolge handelt. Dabei wird versucht, diese bestmöglich zu interpretieren und Ersetzungen vorzunehmen. Kann ein Zeichen nicht interpretiert werden, wird es einfach ausgedruckt. Wird eine Formatzeichenfolge mit einer Länge von 1 Zeichen angegeben (wie P für Prozent- oder N für Zahlenformatierung), wird die Zeichenfolge entsprechend formatiert oder - wenn ein unbekanntes Zeichen übergeben wird - eine ArgumentException geworfen.

Beim Versuch, geschweifte Klammern zu ersetzen, geht die Methode in Reihenfolge der Vorkommen vor.

Gerüstet mit diesen Informationen können wir uns das zweite Beispiel nochmal näher anschauen: Die ersten beiden Zeichen werden zu "{" ersetzt. Die dritte geschweifte Klammer zeigt an, dass hier die Formatzeichenfolge beginnt. Die ersten beiden schließenden geschweiften Klammern werden wieder durch "}" ersetzt und die letzte schließende Klammer zeigt an, dass die Formatzeichenfolge hier endet. Damit bleibt folgender Ausdruck übrig "{0:N}}", wobei "N}" die Formatzeichenfolge ist. Da es sich hier um mehr als 1 Zeichen handelt, wird nun von einem benutzerdefinierten Format ausgegangen - und dort haben weder "N" noch "}" eine besondere Bedeutung. Daher werden diese beiden Zeichen ausgedruckt. Zusammen mit der schon anfangs ersetzten "{" kommt das Ergebnis "{N}" zustande.

Um das Ganze an einem einfacheren Beispiel ohne Escapesequenz zu verdeutlichen:

int i = 23;
string s = string.Format("{0:N*}", i); // Ergebnis: "N*"

Auch hierbei handelt es sich um ein benutzerdefiniertes Format, dass nicht interpretiert werden kann und deshalb ausgegeben wird.