String Formatting
Raw numbers rarely look right on screen: you want two decimal places for prices, thousands separators for big numbers, percentages, padded columns and dates. C# handles all of this with format specifiers inside interpolated strings, and it adapts the result to the user's culture.
Format specifiers
Inside an interpolation hole, add a colon and a format after the value: {value:format}.
double ratio = 0.4567; double big = 1234567.891; decimal price = 1299.5m; int id = 42; Console.WriteLine($"{big:N2}"); // number with separators Console.WriteLine($"{big:N0}"); Console.WriteLine($"{ratio:F1}"); // fixed-point, 1 decimal Console.WriteLine($"{ratio:P1}"); // percentage Console.WriteLine($"{price:C}"); // currency Console.WriteLine($"{id:D5}"); // integer padded with zeros Console.WriteLine($"{255:X}"); // hexadecimal Console.WriteLine($"{big:E2}"); // scientific
1,234,567.89 1,234,568 0.5 45.7% $1,299.50 00042 FF 1.23E+006
| Specifier | Name | Example | Result (en-US) |
|---|---|---|---|
Nn | Number | {1234.5:N2} | 1,234.50 |
Fn | Fixed-point | {1234.5:F3} | 1234.500 |
Cn | Currency | {9.5:C} | $9.50 |
Pn | Percent (×100) | {0.25:P0} | 25% |
Dn | Decimal integer | {7:D3} | 007 |
Xn | Hexadecimal | {255:X4} | 00FF |
En | Scientific | {1500.0:E1} | 1.5E+003 |
The number after the letter is the precision: decimal places for N, F, C, P and E, or minimum digits for D and X.
Custom formats
When the standard specifiers don't fit, build a pattern: 0 is a required digit, # an optional one, , a thousands separator and . the decimal point. Text in quotes appears as-is:
double value = 1234.5; Console.WriteLine($"{value:0.00}"); Console.WriteLine($"{value:#,##0.0}"); Console.WriteLine($"{0.5:0.###}"); Console.WriteLine($"{7:000}"); Console.WriteLine($"{-3.2:+0.0;-0.0;zero}"); // positive;negative;zero sections Console.WriteLine($"{0:+0.0;-0.0;zero}"); Console.WriteLine($"{98.6:0.0' °F'}");
1234.50 1,234.5 0.5 007 -3.2 zero 98.6 °F
Alignment
A comma and a number after the value set a minimum width: positive right-aligns, negative left-aligns. Combined with a format, it makes tidy columns:
var rows = new (string Item, int Qty, decimal Price)[] { ("Keyboard", 1, 49.99m), ("Mouse", 2, 19.5m), ("USB cable", 10, 3.25m), }; Console.WriteLine($"{"Item",-12}{"Qty",5}{"Total",10}"); Console.WriteLine(new string('-', 27)); foreach (var (item, qty, price) in rows) { Console.WriteLine($"{item,-12}{qty,5}{qty * price,10:F2}"); }
Item Qty Total --------------------------- Keyboard 1 49.99 Mouse 2 39.00 USB cable 10 32.50
The full form of a hole is {value,alignment:format}. (The (string Item, int Qty, decimal Price) type is a tuple — lesson 40 shows similar data with records.)
Expressions and braces
Any expression can go in the braces, including method calls and the conditional operator (wrap it in parentheses because of the colon). To print a literal brace, double it:
int count = 3; string[] names = ["Ana", "Ben", "Cy"]; Console.WriteLine($"{count} file{(count == 1 ? "" : "s")} selected"); Console.WriteLine($"Names: {string.Join(", ", names)} ({names.Length})"); Console.WriteLine($"{{ braces }} and a value: {count * 2}");
3 files selected
Names: Ana, Ben, Cy (3)
{ braces } and a value: 6Culture
Formats follow the current culture — the user's language and region settings — so the same code prints 1,234.50 in the US and 1.234,50 in Germany. Pass a specific culture to ToString, or use string.Create for a whole interpolated string:
using System.Globalization; decimal amount = 1234.5m; var de = new CultureInfo("de-DE"); var ja = new CultureInfo("ja-JP"); Console.WriteLine(amount.ToString("C", de)); Console.WriteLine(amount.ToString("C", ja)); Console.WriteLine(amount.ToString("N2", CultureInfo.InvariantCulture)); Console.WriteLine(string.Create(de, $"Total: {amount:N2}"));
1.234,50 € ¥1,235 1,234.50 Total: 1.234,50
Show users their own culture. For log files, file formats and data sent to other programs, use CultureInfo.InvariantCulture so the output is stable everywhere. (The examples in this course run with the US English culture.)
string.Format and composite formatting
Older code uses string.Format with numbered placeholders. The format rules inside the braces are the same:
string s = string.Format("{0} scored {1:P0} ({2}/{3})", "Lee", 0.875, 35, 40); Console.WriteLine(s); Console.WriteLine("{0,-6}|{1,6}|", "left", "right");
Lee scored 88% (35/40) left | right|
Console.WriteLine accepts the same placeholder style directly. Prefer interpolation in new code — it's easier to read and the compiler checks the expressions.
Formatting dates
Dates use their own specifiers, covered properly in lesson 56. A taste:
var launch = new DateTime(2026, 3, 14, 9, 5, 0); Console.WriteLine($"{launch:yyyy-MM-dd}"); Console.WriteLine($"{launch:dd/MM/yyyy HH:mm}"); Console.WriteLine($"{launch:dddd, MMMM d}"); Console.WriteLine($"{launch:d}"); Console.WriteLine($"{launch:t}");
2026-03-14 14/03/2026 09:05 Saturday, March 14 3/14/2026 9:05 AM
Try it yourself
Print a formatted receipt. Try changing the culture by adding using System.Globalization; and CultureInfo.CurrentCulture = new CultureInfo("fr-FR"); at the top.
Sandwich $6.50 Juice $2.75 Cookie $1.80 ================== Subtotal $11.05 Tax 8% $0.88 Total $11.93