Aligning Numbers on the Decimal Separator [Ron Petrusha]

The composite formatting feature in the .NET Framework makes it easy to left-align or right-align a value in a fixed-width field. If the alignment component of a format item in a composite format string is negative, its corresponding argument is left-aligned. If the alignment component is positive, its corresponding argument is right-aligned. For instance, the following example assigns random numbers to an eleven-element array and displays their values. In the output, the array index is left-aligned in a five-character field, and the array value is right-aligned in a twelve-character field.

 [Visual Basic]
Dim rnd As New Random()
Dim values(10) As Double
Console.WriteLine("{0,-5}{1,12}", "Index", "Value")
For ctr As Integer = values.GetLowerBound(0) To values.GetUpperBound(0)
    values(ctr) = rnd.NextDouble()
    Console.WriteLine(" {0,-5} {1,12:F6}", ctr, values(ctr))

Random rnd = new Random();
double[] values = new double[11];
Console.WriteLine("{0,-5}{1,12}", "Index", "Value");
for (int ctr = values.GetLowerBound(0); ctr <= values.GetUpperBound(0); ctr++)
   values[ctr] = rnd.NextDouble();
   Console.WriteLine(" {0,-5} {1,12:F6}", ctr, values[ctr]);

The example produces output similar to the following:

 Index      Value

 0         0.682236
 1         0.827866
 2         0.927542
 3         0.670535
 4         0.023612
 5         0.353050
 6         0.685052
 7         0.938842
 8         0.494007
 9         0.532631
 10        0.978260

The .NET Framework composite formatting feature doesn’t support additional forms of alignment such as centering a value or aligning numeric values in a field on their decimal points. However, such forms of alignment can be easily implemented by dynamically building a composite format string or by modifying the string representation of the value to be formatted. This blog post will illustrate both approaches by showing how to align a series of numeric values on their decimal point.

The first step is to determine the maximum size of the field that is to display the aligned values. In some cases, the field size is static and is known at compile time. In other cases, the precise range of the values to be formatted is based on the format of data that is known only at runtime. However, the alignment component of a format string must consist of a literal numeric value; you cannot supply a variable that represents that value. For example, the composite format string "The value is {0,12}." indicates that the first argument is to be right-aligned in a twelve-character field. But the composite format string "The value is {0,width}." throws a FormatException.

This means that if we cannot supply a numeric value to the alignment component of a composite format string at design time, we must use string concatenation to dynamically create the format string at runtime. This, in turn, requires an initial pass through the data to be formatted. To align numeric data on a decimal point, we need two items of information:

  • The number of characters in the data item that has the largest number of integral digits.
  • The number of characters in the data item that has the largest number of fractional digits.

The sum of these two values, along with the number of characters reserved for the decimal separator, any other formatting styles, and white space, defines the total width of the field. The number of characters in the data item with the largest number of integral digits, along with any formatting styles and leading white space, defines the maximum number of characters that you must accommodate for the value before the decimal separator in the formatted string. For example, to align the values 16.509 and 13052.4 on their decimal point, requires a field width of nine (assuming that we do not want to include any additional styles such as white space or group separators) with a maximum of five integral digits.

The following example illustrates this first pass through an array of numeric values to determine the total field size and the maximum number of integral characters. Note that the example determines the number of characters in a particular numeric value by calling the ToString method with a format specifier.

 [Visual Basic]
Dim numbers() As Decimal = { 3D, 15.4D, 19.008D, 18.62D, 1093.425D }
Dim intDigits, decDigits, decPosition As Integer
Dim decimalSeparator As String = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator
For Each number As Decimal In numbers
   Dim value As String = number.ToString("G")
   decPosition = value.IndexOf(decimalSeparator)
   If intDigits < decPosition Then intDigits = decPosition
   If decDigits < value.Length - (decPosition + 1) Then decDigits = value.Length - (decPosition + 1)

Dim fieldWidth As Integer = intDigits + decimalSeparator.Length + decDigits

Console.WriteLine("Maximum number of integral digits: {0}", intDigits)
Console.WriteLine("Maximum number of decimal digits: {0}", decDigits)
Console.WriteLine("Total number of characters: {0}", fieldWidth)

decimal[] numbers = { 3m, 15.4m, 19.008m, 18.62m, 1093.425m };
int intDigits = 0, decDigits = 0, decPosition = 0;
string decimalSeparator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
foreach (decimal number in numbers)
   string value = number.ToString("G");
   decPosition = value.IndexOf(decimalSeparator);
   if (intDigits < decPosition) intDigits = decPosition;
   if (decDigits < value.Length - (decPosition + 1)) decDigits = value.Length - (decPosition + 1);

int fieldWidth = intDigits + decimalSeparator.Length + decDigits;

Console.WriteLine("Maximum number of integral digits: {0}", intDigits);
Console.WriteLine("Maximum number of decimal digits: {0}", decDigits);
Console.WriteLine("Total number of characters: {0}", fieldWidth);

As the output shows, aligning this data on its decimal separator requires that each data item be displayed in an eight-character field, and that each data item be allocated space for four integral characters.

 Maximum number of integral digits: 4
Maximum number of decimal digits: 3
Total number of characters: 8

Once the first pass through the data determines the total field width and the number of integral characters in each field, a second pass can display the data. To align numeric values on their decimal separator, this second pass must dynamically add the field width to the format string. Since we are aligning the numeric values on their decimal points based on the number of integral characters, the string must define a left-aligned field. When iterating the numeric values in the array, each number must be padded with a sufficient number of leading spaces so that the total length of its integral portion equals the maximum number of integral digits. This allows the numbers to be aligned on their decimal separators.

 [Visual Basic]
Dim fmt As String = "Value: {0,-" + fieldWidth.ToString() + "}"
For Each number As Decimal In numbers
   decPosition = number.ToString().IndexOf(decimalSeparator)
   If decPosition = -1 Then decPosition = number.ToString().Length

   Dim value As String = String.Format("{0}{1}", _
                         New String(" "c, intDigits - decPosition), number)

   Console.WriteLine(fmt, value)

string fmt = "Value: {0,-" + fieldWidth.ToString() + "}";
foreach (decimal number in numbers)
   decPosition = number.ToString().IndexOf(decimalSeparator);
   if (decPosition == -1) decPosition = number.ToString().Length;

   string value = String.Format("{0}{1}", 
                  new String(' ', intDigits - decPosition), number);

   Console.WriteLine(fmt, value);

The example displays the following output:

 Value:    3
Value:   15.4
Value:   19.008
Value:   18.62
Value: 1093.425

Although this example shows how to align numeric values on their decimal point, you can adopt a similar approach to other forms of alignment. To center a numeric value in a field, for example, requires that you know the total field width. Centering the number is then just a matter of determining the length of the string that represents a particular number and padding it with a number of spaces equal to half of the difference between the total field width and the number’s length.