Site

Formatting — C# String Formatting

Tutorial S9  •  C# / Learn

S9.0 What This Teaches

This tutorial covers string formatting in C#:

S9.1 String Interpolation

String interpolation with $"..." embeds expressions directly in a string literal. Expressions go inside {} and may include format specifiers after a colon:
// Formatting - string interpolation examples.

string name = "Alice";
int age = 30;
double score = 0.9875;

Console.WriteLine($"Name: {name}, Age: {age}");
Console.WriteLine($"Score: {score:P1}");      // 98.8%
Console.WriteLine($"Score: {score:F4}");      // 0.9875
Console.WriteLine($"Pi:    {Math.PI:F2}");    // 3.14

// Alignment: positive = right-align, negative = left-align
Console.WriteLine($"{"Item",-10} {"Price",8}");
Console.WriteLine($"{"Apple",-10} {1.5,8:C}");
Console.WriteLine($"{"Banana",-10} {0.75,8:C}");
Format specifiers after the colon are the same format strings used in string.Format and ToString.

S9.2 Composite Format Strings

string.Format uses indexed placeholders {0}, {1}, etc. It is the foundation that interpolation compiles to:
string result = string.Format("Hello, {0}! You scored {1:F1}%.", "Bob", 87.5);
Console.WriteLine(result);   // Hello, Bob! You scored 87.5%.

// Console.WriteLine accepts composite format directly
Console.WriteLine("X={0}, Y={1}, Z={2}", 1, 2, 3);

// Same placeholder used twice
Console.WriteLine("{0} * {0} = {1}", 7, 49);

S9.3 Standard Numeric Format Specifiers

int n = 1234567;
double d = 12345.6789;

// N - number with thousands separator
Console.WriteLine(n.ToString("N0"));    // 1,234,567
Console.WriteLine(d.ToString("N2"));    // 12,345.68

// C - currency
Console.WriteLine(d.ToString("C"));     // $12,345.68  (locale-dependent)

// F - fixed-point
Console.WriteLine(d.ToString("F3"));    // 12345.679

// D - decimal integer (integer types only)
Console.WriteLine(n.ToString("D8"));    // 01234567  (zero-padded)

// X - hexadecimal
Console.WriteLine(n.ToString("X"));     // 12D687
Console.WriteLine(n.ToString("x8"));    // 0012d687  (lowercase, padded)

// E - scientific notation
Console.WriteLine(d.ToString("E2"));    // 1.23E+004

// P - percentage
Console.WriteLine(0.1234.ToString("P1"));  // 12.3%

S9.4 Alignment and Field Width

Inside {}, a comma followed by a number sets the minimum field width. Positive aligns right; negative aligns left:
// {index,width:format}
string header = string.Format("{0,-12} {1,8} {2,10}", "Product", "Qty", "Price");
Console.WriteLine(header);

string[] products = { "Widget", "Gadget", "Thingamajig" };
int[] qtys        = { 100, 25, 3 };
double[] prices   = { 9.99, 49.95, 129.00 };

for (int i = 0; i < products.Length; i++)
    Console.WriteLine(string.Format("{0,-12} {1,8} {2,10:C}", products[i], qtys[i], prices[i]));
Expected output:
Product           Qty      Price
Widget            100      $9.99
Gadget             25     $49.95
Thingamajig         3    $129.00

S9.5 Custom Format Strings

Custom format strings give fine-grained control over how numbers and dates appear. Common placeholders: 0 (digit or zero), # (digit or empty), . (decimal point), , (thousands):
double value = 1234.5;

Console.WriteLine(value.ToString("0,000.00"));    // 1,234.50
Console.WriteLine(value.ToString("#,##0.##"));    // 1,234.5
Console.WriteLine(value.ToString("000.000"));     // 1234.500 - leading zeros

// Date formatting
DateTime now = DateTime.Now;
Console.WriteLine(now.ToString("yyyy-MM-dd"));          // 2025-04-01
Console.WriteLine(now.ToString("ddd, dd MMM yyyy"));    // Tue, 01 Apr 2025
Console.WriteLine(now.ToString("HH:mm:ss"));            // 14:32:05

S9.6 IFormattable on Custom Types

Implement IFormattable so your types support format specifiers in interpolated strings and string.Format:
record Point(double X, double Y) : IFormattable
{
    public string ToString(string? format, IFormatProvider? provider)
    {
        return format switch
        {
            "F2" => $"({X:F2}, {Y:F2})",
            "E"  => $"({X:E}, {Y:E})",
            _    => $"({X}, {Y})"
        };
    }
}

var p = new Point(1.23456, 7.89012);
Console.WriteLine($"{p}");       // (1.23456, 7.89012)
Console.WriteLine($"{p:F2}");    // (1.23, 7.89)
Console.WriteLine($"{p:E}");     // (1.234560E+000, 7.890120E+000)

S9.7 Example - All Together

// Formatting - tabular sales report with alignment and numeric specifiers.

var sales = new[]
{
    (Region: "North",  Units: 1500, Revenue: 74250.0),
    (Region: "South",  Units:  820, Revenue: 41000.0),
    (Region: "East",   Units: 2300, Revenue: 115000.0),
    (Region: "West",   Units: 1100, Revenue: 55500.0),
};

Console.WriteLine($"{"Region",-8} {"Units",8} {"Revenue",14} {"Avg/Unit",12}");
Console.WriteLine(new string('-', 44));

foreach (var (region, units, revenue) in sales)
{
    double avg = revenue / units;
    Console.WriteLine($"{region,-8} {units,8:N0} {revenue,14:C} {avg,12:C}");
}

double totalRev = sales.Sum(s => s.Revenue);
Console.WriteLine(new string('-', 44));
Console.WriteLine($"{"Total",-8} {sales.Sum(s => s.Units),8:N0} {totalRev,14:C}");

S9.8 Exercise

Exercise
  • Format the number 1234567.891 five ways: as an integer with thousands separators, as currency, as fixed-point with 3 decimals, in scientific notation, and as a percentage of 2,000,000.
  • Print a 10-row multiplication table using alignment so every column is exactly 5 characters wide and right-aligned.
  • Implement IFormattable on a Temperature record that stores a value in Celsius. Support format "F" for Fahrenheit, "K" for Kelvin, and default for Celsius.

S9.9 Common Mistakes

Currency format is locale-dependent

// On a US machine: $1,234.57
// On a German machine: 1.234,57 €
Console.WriteLine(1234.567.ToString("C"));

// Fix: specify the culture explicitly
using System.Globalization;
Console.WriteLine(1234.567.ToString("C", CultureInfo.InvariantCulture));  // ¤1,234.57
Console.WriteLine(1234.567.ToString("C", new CultureInfo("en-US")));       // $1,234.57

Using D format on a double

double d = 42.0;
// d.ToString("D3") throws FormatException - D is only for integer types!
int n = 42;
n.ToString("D3");   // "042" - correct

String concatenation in tight loops instead of StringBuilder

// Slow: creates a new string object each iteration
string result = "";
foreach (var item in items)
    result += $"{item}, ";

// Fast: StringBuilder amortizes allocations
var sb = new System.Text.StringBuilder();
foreach (var item in items)
    sb.Append($"{item}, ");
string result2 = sb.ToString();

S9.10 Key Terms

TermMeaning
string interpolation$"..." syntax; embeds expressions with optional format specifiers
composite formatstring.Format with {index,width:format} placeholders
NNumber with thousands separator and optional decimal places
CCurrency; locale-dependent symbol and grouping
FFixed-point; specifies decimal places
X / xHexadecimal; uppercase or lowercase digits
EScientific notation
IFormattableInterface enabling custom types to respond to format specifiers