Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Aligning strings within string.Format and interpolated strings

$
0
0

I was browsing through the MSDN docs the other day, trying to remind myself of the various standard ToString() format strings, when I spotted something I have somehow missed in all my years of .NET - alignment components.

This post is for those of you who have also managed to miss this feature, looking at how you can use alignment components both with string.Format and when you are using string interpolation.

Right-aligning currencies in format strings

I'm sure the vast majority of people already know how format strings work in general, so I won't dwell on it much here. In this post I'm going to focus on formatting numbers, as formatting currencies seems like the canonical use case for alignment components.

The following example shows a simple console program that formats three decimals as currencies:

class Program
{
    readonly static decimal val1 = 1;
    readonly static decimal val2 = 12;
    readonly static decimal val3 = 1234.12m;

    static void Main(string[] args)
    {
        Console.OutputEncoding = System.Text.Encoding.Unicode;

        Console.WriteLine($"Number 1 {val1:C}");
        Console.WriteLine($"Number 2 {val2:C}");
        Console.WriteLine($"Number 3 {val3:C}");
    }
}

As you can see, we are using the standard c currency formatter in an interpolated string. Even though we are using interpolated strings, the output is identical to the output you get if you use string.Format or pass arguments to Console.WriteLine directly. All of the following are the same:

Console.WriteLine($"Number 1 {val1:C}");
Console.WriteLine("Number 1 {0:C}", val1);
Console.WriteLine(string.Format("Number 1 {0:C}", val1));

When you run the original console app, you'll get something like the following (depending on your current culture):

Number 1 £1.00
Number 2 £12.00
Number 3 £1,234.12

Note that the numbers are slightly hard to read - the following is much clearer:

Number 1      £1.00
Number 2     £12.00
Number 3  £1,234.12

This format is much easier to scan - you can easily see that Number 3 is significantly larger than the other numbers.

To right-align formatted strings as we have here, you can use an alignment component in your string.Format format specifiers. An alignment component specifies the total number of characters to use to format the value.

The formatter formats the number as usual, and then adds the necessary number of whitespace characters to make the total up to the specific alignment component. You specify the alignment component after the number to format and a comma ,. For example, the following format string "{value,5}" when value=1 would give the string " 1": 1 formatted character, 4 spaces, 5 characters in total.

You can use a formatting string (such as standard values like c or custom values like dd-mmm-yyyy and ###) in combination with an alignment component. Simply place the format component after the alignment component and :, for example "value,10:###". The integer after the comma is the alignment component, and the string after the colon is the formatting component.

So, going back to our original requirement of right aligning three currency strings, the following would do the trick, with the values previously presented:

decimal val1 = 1;
decimal val2 = 12;
decimal val3 = 1234.12m;

Console.WriteLine($"Number 1 {val1,10:C}");
Console.WriteLine($"Number 2 {val2,10:C}");
Console.WriteLine($"Number 3 {val3,10:C}");

// Number 1      £1.00
// Number 2     £12.00
// Number 3  £1,234.12

Oversized strings

Now, you may have spotted a slight issue with this alignment example. I specified that the total width of the formatted string should be 10 characters - what happens if the number is bigger that that?

In the following example, I'm formatting a long in the same ways as the previous, smaller, numbers:

class Program
{
    readonly static decimal val1 = 1;
    readonly static decimal val2 = 12;
    readonly static decimal val3 = 1234.12m;
    readonly static long _long = 999_999_999_999;

    static void Main(string[] args)
    {
        Console.OutputEncoding = System.Text.Encoding.Unicode;

        Console.WriteLine($"Number 1 {val1,10:C}");
        Console.WriteLine($"Number 2 {val2,10:C}");
        Console.WriteLine($"Number 3 {val3,10:C}");
        Console.WriteLine($"Number 3 {_long,10:C}");
    }
}

You can see the effect of this 'oversized' number below:

Number 1      £1.00
Number 2     £12.00
Number 3  £1,234.12
Number 3 £999,999,999,999.00

As you can see, when a formatted number doesn't fit in the requested alignment characters, it spills out to the right. Essentially the alignment component indicates the minimum number of characters the formatted value should occupy.

Padding left-aligned strings

You've seen how to left-align currencies, but what if the labels associated with these values were not all the same length, as in the following example:

Console.WriteLine($"A small number {val1,10:C}");
Console.WriteLine($"A bit bigger {val2,10:C}");
Console.WriteLine($"A bit bigger again {val3,10:C}");

Written like this, our good work aligning the currencies is completely undone by the unequal length of our labels:

A small number      £1.00
A bit bigger     £12.00
A bit bigger again  £1,234.12

Now, there's an easy way to fix the problem in this case, just manually pad with whitespace:

Console.WriteLine($"A small number     {val1,10:C}");
Console.WriteLine($"A bit bigger       {val2,10:C}");
Console.WriteLine($"A bit bigger again {val3,10:C}");

But what if these labels were dynamic? In that case, we could use the same alignment component trick. Again, the integer passed to the alignment component indicates the minimum number of characters, but this time we use a negative value to indicate the values should be left aligned:

var label1 = "A small number";
var label2 = "A bit bigger";
var label3 = "A bit bigger again";

Console.WriteLine($"{label1,-18} {val1,10:C}");
Console.WriteLine($"{label2,-18} {val2,10:C}");
Console.WriteLine($"{label3,-18} {val3,10:C}");

With this technique, when the strings are formatted, we get nicely formatted currencies and labels.

A small number          £1.00
A bit bigger           £12.00
A bit bigger again  £1,234.12

Limitations

Now, there's one big limitation when it comes to using alignment components. In the previous example, we had to explicitly set the alignment component to a length of 18 characters. That feels a bit clunky.

Ideally, we'd probably prefer to do something like the following:

var maxLength = Math.Max(label1.Length, label2.Length);
Console.WriteLine($"{label1,-maxLength} {val1,10:C}");
Console.WriteLine($"{label2,-maxLength} {val2,10:C}");

Unfortunately, this doesn't compile - maxLength has to be a constant. Ah well.

Summary

You can use alignment components in your format strings to both right-align and left-align your formatted values. This pads the formatted values with whitespace to either right-align (positive values) or left-align (negative values) the formatted value. This is particularly useful for right-aligning currencies in strings.


Viewing all articles
Browse latest Browse all 743

Trending Articles