Mert Özen Her Satırda Daha İleri
Backend C# ile Günlük .NET API'leri — #02

C#'ta Format String'leri: Standart ve Custom Sayı/Tarih Formatları, Alignment ve Composite Formatting

Mert Özen 15 Kas 2026 15 dk 5 görüntülenme
C#'ta Format String'leri: Standart ve Custom Sayı/Tarih Formatları, Alignment ve Composite Formatting

Format string'i değeri culture'ın kurallarıyla string'e çevirir: N, C, d ve D culture'a göre değişir, o, s ve InvariantCulture ise makineye giden çıktıyı sabit tutar. Bookstore'un fatura satırı üzerinden standart ve custom sayı/tarih formatlarını, alignment'ı, para birimini veriden almayı, ICU'da N'nin 3 ondalık yazmasını ve Isbn tipini ISpanFormattable ile yazdırılabilir yapmayı gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; hex floating-point örneği .NET 11 SDK ile çalıştırılmıştır.

Önce Tahmin Et

Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) sipariş servisinde üç satırlık bir log kodu var. Aynı program önce tr-TR, sonra en-US culture'ında çalışıyor. Ekrana ne yazılır?

decimal price = 1234.5m;
DateTime placed = new(2026, 11, 14, 9, 5, 0);

Console.WriteLine(price.ToString("C"));
Console.WriteLine(placed.ToString("yyyy-mm-dd"));
Console.WriteLine(placed.ToString("dd/MM/yyyy"));
dotnet run --project samples/Bookstore.Bcl.Samples -- format-guess --culture tr-TR
₺1.234,50
2026-05-14
14.11.2026

dotnet run --project samples/Bookstore.Bcl.Samples -- format-guess --culture en-US
$1,234.50
2026-05-14
14/11/2026

Üç sürpriz var. Türk lirası olarak kaydettiğimiz tutar en-US'de dolar işaretiyle yazıldı; "C" para birimini veriden değil culture'dan alıyor. mm ay değil dakika olduğu için Kasım ayındaki sipariş "05. ay" oldu. "dd/MM/yyyy" içindeki / ise sabit bir karakter değil, culture'ın tarih ayırıcısı için yer tutucu; tr-TR'de noktaya dönüştü.

Serinin ilk yazısında string'leri arayıp böldük. Bu yazıda değerleri string'e çevirmenin kurallarını görüyoruz: standart ve custom sayı formatları, tarih formatları, alignment, composite formatting, culture'dan bağımsız çıktı ve kendi tipimizi format string'leriyle yazdırılabilir yapmak. Parse tarafı, yani string'den değere dönüşüm, C# Temelleri serisinin "C# Tip Dönüşümleri: Cast, Parse, TryParse ve Convert" yazısında; tarih parse'ının ileri kısmı bu serinin 6. yazısında.

Standart Sayı Formatları

Standart format string'i bir harf ve isteğe bağlı bir sayıdan oluşur: N2'de harf format'ı, sayı precision'ı (çoğu format için ondalık basamak sayısını) belirler. tr-TR altında çalışan örnek:

decimal price = 1234.5m;
Row("C", price.ToString("C"));
Row("C0", price.ToString("C0"));
Row("N", price.ToString("N"));
Row("N0", price.ToString("N0"));
Row("F3", price.ToString("F3"));
Row("E2", price.ToString("E2"));
Row("G", price.ToString("G"));
Row("P1 (0.125)", 0.125m.ToString("P1"));
Row("D6 (42)", 42.ToString("D6"));
Row("X4 (255)", 255.ToString("X4"));
Row("x (255)", 255.ToString("x"));
Row("B8 (5)", 5.ToString("B8"));
Row("N2 (-1234.5)", (-1234.5m).ToString("N2"));

double sum = 0.1 + 0.2;
Row("double default", sum.ToString());
Row("double R", sum.ToString("R"));
Row("double G17", sum.ToString("G17"));
Row("double F2", sum.ToString("F2"));
C              ₺1.234,50
C0             ₺1.235
N              1.234,500
N0             1.235
F3             1234,500
E2             1,23E+003
G              1234,5
P1 (0.125)     %12,5
D6 (42)        000042
X4 (255)       00FF
x (255)        ff
B8 (5)         00000101
N2 (-1234.5)   -1.234,50
double default 0,30000000000000004
double R       0,30000000000000004
double G17     0,30000000000000004
double F2      0,30

Kısaca: C para birimi, N binlik ayırıcılı sayı, F binlik ayırıcısız sabit noktalı sayı, P yüzde (değeri 100 ile çarpar), E bilimsel gösterim, G en kısa gösterim. D (sıfırla doldurma), X (hex) ve .NET 8 ile gelen B (binary) yalnız tamsayılarda çalışır; X'in büyük ya da küçük harfi çıktıdaki harflerin büyüklüğünü belirler. Türkçede yüzde işaretinin sayının önüne geldiğine (%12,5) dikkat et; bu da culture verisinden geliyor.

En önemli satır N. Precision vermediğinde ondalık basamak sayısı NumberFormatInfo.NumberDecimalDigits'ten gelir ve ICU'nun (International Components for Unicode; .NET'in culture verisini aldığı Unicode library'si) culture verisinde bu değer hem tr-TR hem en-US için 3; invariant culture'da ise 2. .NET runtime'ının kaynak kodunda bu değer ICU'nun ondalık sayı pattern'indeki en fazla kesir basamağından okunuyor (#,##0.###). Eski dokümanlarda ve NLS (National Language Support; Windows'un eski yerel ayar altyapısı) moduna geçirilmiş uygulamalarda 2 görürsün. Sonuç: precision'ı hiçbir zaman varsayılana bırakma, N2, F2, C2 yaz.

R (round-trip) format'ı .NET Core 3.0'dan beri pratikte gereksiz: double.ToString() artık değeri geri parse edildiğinde aynı sayıyı verecek en kısa string'i üretiyor; üç satırın aynı çıkması bunu gösteriyor. Microsoft dokümanı R'yi yalnız BigInteger için öneriyor, double'ı her bitiyle saklaman gerekiyorsa G17 diyor. Precision'ın değeri yuvarlamadığını, yalnız gösterimi kısalttığını da unutma: F2 çıktısı 0,30 ama değişkendeki sayı hâlâ 0.30000000000000004.

.NET 11: double için hex formatı

.NET 10'da double'a "X" verirsen FormatException: Format specifier was invalid. alırsın. .NET 11 double, float ve Half için IEEE (Institute of Electrical and Electronics Engineers; elektrik ve elektronik mühendisleri derneği) 754 standardındaki hex gösterimi ekledi; bu gösterim her biti korur. Repo'daki Bookstore.Bcl.Net11 projesinde:

string hex = price.ToString("X", invariant);
double back = double.Parse(hex, System.Globalization.NumberStyles.HexFloat, invariant);

Console.WriteLine($"G17 (invariant): {price.ToString("G17", invariant)}");
Console.WriteLine($"X (invariant)  : {hex}");
Console.WriteLine($"X (tr-TR)      : {price.ToString("X", tr)}");
Console.WriteLine($"Round-trip equal: {back == price}");
G17 (invariant): 0.30000000000000004
X (invariant)  : 0X1.3333333333334P-2
X (tr-TR)      : 0X1,3333333333334P-2
Round-trip equal: True

Hex gösterim de culture'ın ondalık ayırıcısını kullanıyor; dosyaya ya da teste yazarken invariant culture vermek burada da şart.

Custom Sayı Formatları

Standart formatlar yetmediğinde format'ı karakter karakter tarif edersin. Aynı dört değeri sekiz custom format'la tr-TR'de yazdırdım (en-US'de yalnız ayırıcılar yer değiştiriyor):

decimal[] values = [1234.5m, -1234.5m, 0m, 0.075m];
string[] formats = ["0000.00", "#,##0.00", "#.##", "0.##", "#,##0,K", "0.0%", "#,##0.00;(#,##0.00);-", "\\#0"];

foreach (string format in formats)
{
    string cells = string.Join(" | ", values.Select(v => v.ToString(format, culture).PadLeft(12)));
    Console.WriteLine($"{format,-24}{cells}");
}
0000.00                      1234,50 |     -1234,50 |      0000,00 |      0000,08
#,##0.00                    1.234,50 |    -1.234,50 |         0,00 |         0,08
#.##                          1234,5 |      -1234,5 |              |          ,08
0.##                          1234,5 |      -1234,5 |            0 |         0,08
#,##0,K                           1K |          -1K |           0K |           0K
0.0%                       123450,0% |   -123450,0% |         0,0% |         7,5%
#,##0.00;(#,##0.00);-       1.234,50 |   (1.234,50) |            - |         0,08
\#0                            #1235 |       -#1235 |           #0 |           #0

0 o basamakta mutlaka bir rakam yazar, yoksa sıfır koyar; # ise yalnız anlamlı bir rakam varsa yazar. Bu yüzden "#.##" sıfırı boş string olarak yazdı ve 0.075'i baştaki sıfır olmadan ,08 yaptı. Ekranda tutar gösterirken bu boşluk fark edilmez, bir PDF faturada boş hücre olarak çıkar. Format'ta noktanın solunda en az bir 0 bırakmak güvenli tercihtir.

Virgül iki iş yapar: rakam yer tutucularının arasındaysa binlik ayırıcıdır, ondalık noktanın hemen solundaysa sayıyı 1000'e böler (scaling). "#,##0,K" 1234,5'i 1K yaptı; raporlarda "bin TL" sütunu için kullanışlı. Format string'inde nokta ve virgül her zaman invariant anlamıyla yazılır, çıktıdaki karakter culture'dan gelir. % değeri 100 ile çarpar. Noktalı virgül format'ı bölümlere ayırır: pozitif, negatif ve sıfır; muhasebe raporlarındaki parantezli negatif bu şekilde yapılır. Özel bir karakteri olduğu gibi yazmak için önüne ters bölü koyar ya da tırnak içine alırsın.

Son satıra bak: 1234,5 #1235 oldu. Custom format'lar ve decimal'ın standart formatları orta noktayı sıfırdan uzağa yuvarlar, Math.Round ise varsayılan olarak çifte yuvarlar (Math.Round(2.5m) 2 döner). Fatura satırlarını format'la, toplamı Math.Round ile yuvarlarsan kuruş farkı çıkabilir; yuvarlama modlarını serinin 4. yazısında ayrıntılı göreceğiz.

Tarih Formatları: Standart ve Custom

Sipariş zamanını DateTimeOffset olarak tutuyoruz: 14 Kasım 2026, 09:05:07, +03:00. Standart tarih formatları tek harf, custom olanlar desen:

string[] formats = ["d", "D", "f", "g", "M", "Y", "o", "s", "u", "R", "yyyy-MM-dd", "dd.MM.yyyy HH:mm", "d MMMM yyyy, dddd", "hh:mm tt", "HH:mm:ss zzz"];
foreach (string format in formats)
    Console.WriteLine($"{format,-20}{OrderPlaced.ToString(format)}");
d                   14.11.2026
D                   14 Kasım 2026 Cumartesi
f                   14 Kasım 2026 Cumartesi 09:05
g                   14.11.2026 09:05
M                   14 Kasım
Y                   Kasım 2026
o                   2026-11-14T09:05:07.0000000+03:00
s                   2026-11-14T09:05:07
u                   2026-11-14 06:05:07Z
R                   Sat, 14 Nov 2026 06:05:07 GMT
yyyy-MM-dd          2026-11-14
dd.MM.yyyy HH:mm    14.11.2026 09:05
d MMMM yyyy, dddd   14 Kasım 2026, Cumartesi
hh:mm tt            09:05 ÖÖ
HH:mm:ss zzz        09:05:07 +03:00

Standart formatların ikiye ayrıldığını görmek önemli. d, D, f, g, M, Y culture'a göre değişir ve kullanıcıya gösterilecek metin içindir. o, s, u, R her culture'da aynıdır ve makineler içindir. o (round-trip) ISO (International Organization for Standardization; uluslararası standart kuruluşu) 8601 formatında, offset ve yedi basamak saniye kesriyle yazar; API'lere ve log'lara giden tarih için doğru seçim budur. u ve R DateTimeOffset'i önce UTC'ye (Coordinated Universal Time; saat dilimlerinin referans aldığı evrensel saat) çevirdi (06:05:07); DateTime'da bu çeviriyi yapmazlar, değeri zaten UTC kabul ederler. R ise HTTP header'larında kullanılan biçimdir; tanımı RFC (Request for Comments; internet standartlarının yayımlandığı belge serisi) 1123'te.

Custom tarafta en çok karışan harfler: MM ay, mm dakika; HH 24 saat, hh 12 saat; tt ÖÖ/ÖS ya da AM/PM; zzz offset. Tuzakları tek programda topladım:

DateTime placed = new(2026, 11, 14, 9, 5, 0);
CultureInfo tr = CultureInfo.GetCultureInfo("tr-TR");

Console.WriteLine($"yyyy-mm-dd          {placed.ToString("yyyy-mm-dd", tr)}");
Console.WriteLine($"yyyy-MM-dd          {placed.ToString("yyyy-MM-dd", tr)}");
Console.WriteLine($"dd/MM/yyyy          {placed.ToString("dd/MM/yyyy", tr)}");
Console.WriteLine($"dd'/'MM'/'yyyy      {placed.ToString("dd'/'MM'/'yyyy", tr)}");
Console.WriteLine($"hh:mm (21:35)       {placed.AddHours(12.5).ToString("hh:mm", tr)}");
Console.WriteLine($"HH:mm (21:35)       {placed.AddHours(12.5).ToString("HH:mm", tr)}");
Console.WriteLine($"o (Kind={placed.Kind})  {placed.ToString("o")}");
Console.WriteLine($"o (Kind=Utc)          {DateTime.SpecifyKind(placed, DateTimeKind.Utc).ToString("o")}");
yyyy-mm-dd          2026-05-14
yyyy-MM-dd          2026-11-14
dd/MM/yyyy          14.11.2026
dd'/'MM'/'yyyy      14/11/2026
hh:mm (21:35)       09:35
HH:mm (21:35)       21:35
o (Kind=Unspecified)  2026-11-14T09:05:00.0000000
o (Kind=Utc)          2026-11-14T09:05:00.0000000Z

/ ve : culture'ın tarih ve saat ayırıcısını temsil eder; harfiyen istiyorsan tek tırnak içine al. hh akşam dokuzu sabah dokuzdan ayırt edilemez yapar. Son iki satır DateTime'ın Kind'ının o çıktısına nasıl yansıdığını gösteriyor: Unspecified bir tarih offset'siz yazılır ve karşı taraf onu kendi yerel saati sanabilir. Zaman dilimi ve Kind konusu serinin 6. yazısında.

Aynı decimal ve tarih değerinin üç culture'daki çıktısını tek tabloda topladım. Kullanıcıya gösterilen formatların culture'a göre nasıl değiştiğini ve makine formatlarının (s) nasıl sabit kaldığını bu tablodan takip edebilirsin:

02-format-cultures-tr.png

Alignment ve Composite Formatting

Composite formatting, string.Format ve interpolation'ın ortak syntax'ı: {index[,alignment][:format]}. Alignment, alanın en az kaç karakter genişliğinde olacağını söyler; pozitifse sağa, negatifse sola yaslar. Bookstore'un console'dan çıkardığı sipariş özeti:

(string Title, int Quantity, decimal Price)[] lines = [("Dune", 2, 349.90m), ("Middlemarch", 1, 1249.50m), ("The Hobbit", 12, 89m)];

Console.WriteLine($"{"Title",-14}{"Qty",4}{"Price",12}{"Total",12}");
foreach (var (title, quantity, price) in lines)
    Console.WriteLine($"{title,-14}{quantity,4}{price,12:N2}{quantity * price,12:N2}");
Console.WriteLine($"{"",-14}{"",4}{"",12}{lines.Sum(l => l.Quantity * l.Price),12:N2}");

Console.WriteLine(string.Format("[{0,8}] [{0,-8}] [{1,8:P0}]", "Dune", 0.1));
Console.WriteLine($"[{"Middlemarch",6}]");
Title          Qty       Price       Total
Dune             2      349,90      699,80
Middlemarch      1    1.249,50    1.249,50
The Hobbit      12       89,00    1.068,00
                                  3.017,30
[    Dune] [Dune    ] [     %10]
[Middlemarch]

Metin sütunlarını sola, sayıları sağa yaslamak tabloyu okunur yapar. Alignment bir üst sınır değildir: değer daha uzunsa kesilmez, tablo kayar (son satırda 6 istedik, 11 karakter yazıldı). Aynı index'i format içinde birden çok kez kullanabilirsin ({0,8} ve {0,-8}). Metnin içinde süslü parantez gerekiyorsa çift yazarsın: {{ ve }}. Format string'ini tekrar tekrar kullanacaksan önceki yazıdaki CompositeFormat onu bir kez parse eder.

Bookstore: Fatura Satırı ve Para Birimi

Bookstore'da tutarlar decimal, para birimi siparişin bir alanı (TRY, gerektiğinde EUR). Önce "C"'nin ne yaptığına bakalım, sonra doğrusuna:

// Para birimi siparişten gelir; culture yalnız sayının yazılışını belirler.
public static string FormatMoney(decimal amount, string currencyCode, CultureInfo culture, bool symbolAfter = false)
{
    var numberFormat = (NumberFormatInfo)culture.NumberFormat.Clone();
    if (symbolAfter)
    {
        numberFormat.CurrencyPositivePattern = 3; // "n $"
        numberFormat.CurrencyNegativePattern = 8; // "-n $"
    }
    numberFormat.CurrencySymbol = currencyCode switch
    {
        "TRY" => "₺",
        "EUR" => "€",
        _ => currencyCode,
    };
    return amount.ToString("C2", numberFormat);
}

public static string FormatWithCode(decimal amount, string currencyCode, CultureInfo culture) =>
    string.Create(culture, $"{currencyCode} {amount:N2}");
tr-TR C       : ₺1.234,50
en-US C       : $1,234.50
de-DE C       : 1.234,50 €
tr-TR invoice : ₺1.234,50
en-US invoice : ₺1,234.50
en-US code    : TRY 1,234.50
de-DE EUR     : 1.234,50 €
tr-TR EUR     : €1.234,50
tr-TR n $     : 1.234,50 ₺
tr-TR -n $    : -1.234,50 ₺
RegionInfo TR : TRY ₺ Turkish Lira

İlk üç satır sorunun kendisi: aynı 1234,50 TL üç sunucuda üç farklı para birimiyle yazılıyor. FormatMoney culture'ın NumberFormatInfo'sunu kopyalayıp yalnız para sembolünü siparişteki para biriminden alıyor; böylece İngilizce arayüzde de tutar ₺1,234.50 görünüyor. Culture'dan gelen NumberFormatInfo salt okunurdur, değiştirmeden önce Clone şart. Uluslararası müşteriye giden e-postada sembol yerine kodu tercih edersen FormatWithCode TRY 1,234.50 üretir.

ICU'nun tr-TR verisi sembolü sayının önüne koyuyor (₺1.234,50). Faturada alışılmış yazım 1.234,50 ₺ ise CurrencyPositivePattern = 3 ve CurrencyNegativePattern = 8 ile bu sırayı kendin belirlersin. Bu sayılar NumberFormatInfo dokümanındaki pattern tablosundan; Windows'un NLS verisi ile ICU arasında varsayılan pattern farklı olabildiği için faturada varsayılana güvenme, açıkça ver. RegionInfo bir ülkenin ISO 4217 para birimi kodunu ve sembolünü verir; ama kullanıcının ülkesi ile siparişin para birimi aynı şey değildir, bu yüzden para birimi her zaman veride durmalı. Para tipini Money value object'i olarak modellemek Çözüm Desenleri serisinin "Para ve Yuvarlama" yazısında.

Culture'dan Bağımsız Çıktı: Invariant

Log'a, dosyaya, başka bir sisteme ya da URL'e yazılan her sayı ve tarih makine tarafından okunacak. Bunlar sunucunun culture'ından etkilenmemeli:

decimal price = 1234.5m;
DateTime placed = new(2026, 11, 14, 9, 5, 0);
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR");

string line = $"{placed:d};{price}";
Console.WriteLine($"Current (tr-TR)            : {line}");
Console.WriteLine($"FormattableString.Invariant: {FormattableString.Invariant($"{placed:yyyy-MM-dd};{price}")}");
Console.WriteLine($"string.Create(Invariant)   : {string.Create(CultureInfo.InvariantCulture, $"{placed:yyyy-MM-dd};{price}")}");

string written = price.ToString();
decimal readBack = decimal.Parse(written, CultureInfo.GetCultureInfo("en-US"));
Console.WriteLine($"Written by tr-TR \"{written}\", read by en-US: {readBack}");
Current (tr-TR)            : 14.11.2026;1234,5
FormattableString.Invariant: 2026-11-14;1234.5
string.Create(Invariant)   : 2026-11-14;1234.5
Written by tr-TR "1234,5", read by en-US: 12345

Son satır gerçek bir veri hatası: Türkçe sunucunun yazdığı 1234,5'i İngilizce sunucu exception atmadan 12345 olarak okudu, çünkü en-US'de virgül binlik ayırıcı. On kat fiyat farkı hiçbir hata log'u bırakmaz.

İki çözüm de aynı sonucu veriyor ama farkları var. FormattableString.Invariant eski ve okunaklı; interpolation'ı önce bir FormattableString object'ine çevirir, sonra format'lar. string.Create(IFormatProvider, ref DefaultInterpolatedStringHandler) .NET 6 ile geldi ve interpolated string handler'ı doğrudan verilen culture ile çalıştırır; ara object ve argümanların boxing'i olmaz. Yeni kodda ikincisini tercih et. Tek bir değer için price.ToString(CultureInfo.InvariantCulture) yeterli. Analyzer tarafında CA1305 kuralı culture almayan ToString, Parse ve string.Format çağrılarını yakalar.

Kendi Tipini Formatlanabilir Yapmak: IFormattable ve ISpanFormattable

Önceki yazıda ISBN (International Standard Book Number; kitaplara verilen uluslararası standart numara) girdisini 13 haneye indiren bir IsbnNormalizer yazmıştık. Şimdi onu bir tipe çevirip {isbn:P} gibi format'larla yazdırılabilir yapıyoruz. IFormattable format string'ini alan ToString overload'unu, .NET 6 ile gelen ISpanFormattable ise allocation'sız TryFormat'ı tanımlar:

public readonly struct Isbn : ISpanFormattable, IEquatable<Isbn>
{
    private readonly string _digits;

    private Isbn(string digits) => _digits = digits;

    public static Isbn Parse(string input) =>
        TryParse(input, out Isbn isbn) ? isbn : throw new FormatException($"'{input}' is not a valid ISBN-13.");

    public static bool TryParse([NotNullWhen(true)] string? input, out Isbn isbn)
    {
        bool ok = IsbnNormalizer.TryNormalize(input, out string? digits);
        isbn = ok ? new Isbn(digits!) : default;
        return ok;
    }

    public override string ToString() => ToString(null, null);

    public string ToString(string? format, IFormatProvider? formatProvider) => format switch
    {
        null or "" or "G" => _digits ?? "",
        "P" => "ISBN " + _digits,
        _ => throw new FormatException($"The '{format}' format is not supported. Use G or P."),
    };

    public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
    {
        ReadOnlySpan<char> prefix = format switch
        {
            "" or "G" => "",
            "P" => "ISBN ",
            _ => throw new FormatException($"The '{format}' format is not supported. Use G or P."),
        };

        string digits = _digits ?? "";
        if (destination.Length < prefix.Length + digits.Length)
        {
            charsWritten = 0;
            return false;
        }

        prefix.CopyTo(destination);
        digits.CopyTo(destination[prefix.Length..]);
        charsWritten = prefix.Length + digits.Length;
        return true;
    }

    // Equals, GetHashCode ve == operatörleri ordinal karşılaştırma yapar.
}
Isbn dune = Isbn.Parse("ISBN 978-0-441-17271-9");

Console.WriteLine($"{dune}");
Console.WriteLine($"{dune:P}");
Console.WriteLine($"[{dune,22:P}]");
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0:G} / {0:P}", dune));

Span<char> buffer = stackalloc char[32];
if (dune.TryFormat(buffer, out int written, "P", null))
    Console.WriteLine($"TryFormat wrote {written} chars: {buffer[..written]}");

Span<char> tooSmall = stackalloc char[8];
Console.WriteLine($"TryFormat into 8 chars: {dune.TryFormat(tooSmall, out _, "P", null)}");
9780441172719
ISBN 9780441172719
[    ISBN 9780441172719]
9780441172719 / ISBN 9780441172719
TryFormat wrote 18 chars: ISBN 9780441172719
TryFormat into 8 chars: False
FormatException: The 'X' format is not supported. Use G or P.

Birkaç kural var. "G" genel format'tır ve null ya da boş format ile aynı sonucu vermelidir; parametresiz ToString() da onu çağırır. Desteklenmeyen format'ta FormatException atmak .NET'teki sözleşmedir (son satır {dune:X} denemesinden). TryFormat buffer yetmediğinde exception atmaz, false döner; çağıran taraf daha büyük bir buffer ile tekrar dener. Interpolation handler'ı tipin ISpanFormattable olduğunu görünce TryFormat'ı çağırıp doğrudan kendi buffer'ına yazar, yani $"{dune:P}" için arada ayrı bir string oluşmaz. Alignment ({dune,22:P}) senin kodun olmadan çalışır, onu composite formatting yapar. ISBN için culture önemli olmadığından IFormatProvider'ı kullanmadık; bir para tipi yazsaydın sayıyı onunla format'lardın. ISBN'in ya da telefon numarasının geçerli bir pattern'e uyup uymadığını regex ile kontrol etmek ise Regular Expressions serisinin konusu.

Üretimde Dikkat

Kullanıcıya gösterilen ve makineye giden format'ları kodda ayır. Ekrana giden her şey kullanıcının culture'ı ile, log'a, dosyaya, API'ye ve veritabanına giden her şey CultureInfo.InvariantCulture ve sabit bir format ile (o, yyyy-MM-dd, F2) yazılmalı. ASP.NET Core'da kullanıcının culture'ını request bazında belirlemek ASP.NET Core MVC (Model-View-Controller; sayfaları sunucuda üreten ASP.NET Core yaklaşımı) serisinin localization yazısında anlatılıyor; arka planda çalışan job'larda CurrentCulture sunucunun ayarıdır ve her ortamda farklı olabilir.

Container image'ı değiştirmek format çıktısını değiştirebilir. Invariant globalization mode açık bir image'da (bazı küçük image'larda varsayılan) tr-TR istediğinde invariant verilerle çalışırsın: tarih 11/14/2026, para sembolü ¤ olur. Bunu ve PredefinedCulturesOnly ayarını bir sonraki yazıda gerçek çıktılarla göreceğiz. Fatura ve e-posta şablonlarının çıktısını bu yüzden snapshot testleriyle (beklenen çıktıyı dosyada tutup karşılaştıran test) sabitlemek iyi bir alışkanlık.

ICU sürümü değiştiğinde culture verisi de değişebilir: para sembolünün yeri, ay adlarının yazımı, N'nin varsayılan precision'ı. Bunlara bağımlı bir test yazıyorsan değerleri test içinde açıkça ver (N2, custom NumberFormatInfo) ki işletim sistemi güncellemesinde testler kırılmasın.

Sık Düşülen Tuzaklar

MM ile mm'i karıştırmak. Tarih "2026-05-14" olur, hata da ancak ay dakikaya denk gelmediğinde fark edilir. Aynı şekilde HH yerine hh yazınca 21:35 ile 09:35 aynı görünür.

Culture vermeden ToString() ile dosyaya sayı yazmak. Türkçe sunucu 1234,5 yazar, başka bir sunucu onu 12345 olarak okur ve hiçbir exception oluşmaz. Makineye giden her değerde CultureInfo.InvariantCulture ver.

Para için "C"'ye güvenmek. C para birimini culture'dan alır; TL tutarı İngilizce sunucuda dolar olarak yazılır. Para birimi veride durmalı, sembolü ondan üretmelisin.

Precision'ı varsayılana bırakmak. N ICU'lu ortamda 3, invariant'ta 2 ondalık yazar; N2 her yerde aynıdır.

/ ve :'ın culture'a göre değiştiğini unutmak. Sabit bir ayırıcı istiyorsan tırnağa al ya da invariant culture ver.

Küçük Bir Deneme

★☆☆ format-custom örneğine "#,##0.00 ₺;-#,##0.00 ₺;Ücretsiz" format'ını ekle ve dört değer için çıktıya bak. Sıfır tutarlı bir kampanya kitabı faturada nasıl görünür?

★★☆ format-dates örneğini ja-JP ve ar-SA culture'larıyla çalıştır. D ve yyyy çıktılarındaki takvim farkına bak ve API'ye neden o göndermemiz gerektiğini bir cümleyle yaz.

★★★ Bookstore için readonly record struct Money(decimal Amount, string Currency) tipi yaz ve ISpanFormattable uygula: "G" 1.234,50 ₺ gibi culture'a göre, "I" TRY 1234.50 gibi invariant çıktı versin. TryFormat'ı decimal.TryFormat ile allocation'sız yaz ve küçük buffer'da false döndüğünü kontrol et.

Bu yazıda her şey culture'a dayandı: ondalık ayırıcı, ay adları, para sembolü, hatta N'nin basamak sayısı. Bir sonraki yazıda culture'ın kendisine bakıyoruz: CultureInfo hiyerarşisi, CurrentCulture ile CurrentUICulture farkı, Türkçe sıralama ve ı/İ dönüşümü ve aynı kodun ICU'lu ve invariant mode'lu container'larda nasıl farklı davrandığı.