Practical .NET · Lesson 18

Building a deterministic text report

Separate calculation, ordering, and repeated text assembly.

How it works

A report combines several responsibilities: selecting data, calculating values, choosing an order, and formatting text. Keeping those steps visible makes the result easier to test. StringBuilder is useful when a loop assembles many pieces into one final string, but the builder itself should not become the place where unrelated validation and business rules are hidden.

A deterministic report specifies its ordering and formatting. Numeric text should use the culture required by the report contract, and line endings should be chosen deliberately when exact exported text matters. This example appends a newline explicitly rather than relying on an operating-system-specific line ending. The final result can therefore be compared as a complete string across environments.

Walk through the example. The rows are ordered by their ordinal product codes before formatting. Each row contributes a code, a colon, a two-decimal amount, and one newline. The total is calculated independently with Sum and appended as the final row. Console.Write prints the complete prepared report without adding an extra newline, so the output structure comes entirely from the builder.

Watch the boundary. StringBuilder does not choose a culture for every value automatically, and it does not guarantee that a report is efficient merely because it is present. For a few fixed fragments, ordinary interpolation is clearer. Also remember that appending a mutable object's default ToString may produce a debugging representation rather than a stable export format. Format each field according to the report's intended reader.

Practice independently. Build a stock report with code, quantity, and a final quantity total. Require ordinal code ordering and a fixed field separator. Test an empty report and a report containing equal quantities under different codes. Compare the entire generated string with a hand-written expected value. Then introduce a display-culture version separately, so a human-oriented report does not silently change a machine-readable export.

A working example

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var rows = new[] { (Code: "B", Amount: 2.5m), (Code: "A", Amount: 3m) };
var builder = new StringBuilder();
foreach (var row in rows.OrderBy(x => x.Code, StringComparer.Ordinal))
    builder.Append(row.Code).Append(':').Append(row.Amount.ToString("F2", CultureInfo.InvariantCulture)).Append('\n');
builder.Append("total:").Append(rows.Sum(x => x.Amount).ToString("F2", CultureInfo.InvariantCulture)).Append('\n');
Console.Write(builder.ToString());

Expected output

A:3.00
B:2.50
total:5.50
Try this
Specify order, numeric culture, and line endings as parts of the report contract.