How to use String.Format - and why you should care about it
Is string.Format obsolete? Not at all, it still has cards to play! Let’s see how we can customize format and create custom formatters.
Table of Contents
Just a second! 🫷
If you are here, it means that you are a software developer. So, you know that storage, networking, and domain management have a cost .
If you want to support this blog, please ensure that you have disabled the adblocker for this site. I configured Google AdSense to show as few ADS as possible - I don't want to bother you with lots of ads, but I still need to add some to pay for the resources for my site.
Thank you for your understanding.
- Davide
Formatting strings is one of the basic operations we do in our day-by-day job. Many times we create methods to provide specific formatting for our data, but not always we want to implement additional methods for every type of formatting we need - too many similar methods will clutter our code.
Let’s say that you have this simple class:
class CapturedPokemon{
public string Name { get; set; }
public int PokedexIndex { get; set; }
public decimal Weight { get; set; }
public decimal Height { get; set; }
public DateTime CaptureDate { get; set; }
}
and an instance of that class:
var pkm = new CapturedPokemon
{
Name = "Garchomp",
PokedexIndex = 445,
Height = 1.9m,
Weight = 95.0m,
CaptureDate = new DateTime(2020, 5, 6, 14, 55, 23)
};
How can we format the pkm
variable to provide useful information on our UI?
The most simple ways are using concatenation, formatting, or string interpolation.
Differences between concatenation, formatting, and interpolation
Concatenation is the simplest way: you concatenate strings with the +
operator.
var messageWithConcatenation= "I caught a " + pkm.Name + " on " + pkm.CaptureDate.ToString("yyyy-MM-dd");
There are 2 main downsides:
- it’s hard to read and maintains, with all those open and closed quotes
- it’s highly inefficient, since strings are immutable and, every time you concatenate a string, it creates a whole new string.
Interpolation is the ability to wrap a variable inside a string and, eventually, call methods on it while creating the string itself.
var messageWithInterpolation = $"I caught a {pkm.Name} on {pkm.CaptureDate.ToString("yyyy-MM-dd")}";
As you see, it’s easier to read than simple concatenation.
The downside of this approach is that here you don’t have a visual understanding of what is the expected string, because the variables drive your attention away from the message you are building with this string.
PS: notice the $
at the beginning of the string and the {
and }
used to interpolate the values.
Formatting is the way to define a string using positional placeholders.
var messageWithFormatting = String.Format("I caught a {0} on {1}", pkm.Name, pkm.CaptureDate.ToString("yyyy-MM-dd"));
We are using the Format
static method from the String
class to define a message, set up the position of the elements and the elements themselves.
Now we have a visual clue of the general structure of the string, but we don’t have a hint of which values we can expect.
Even if string.Format
is considered obsolete, there is still a reason to consider it when formatting strings: this class can help you format the values with default and custom formatters.
But first, a quick note on the positioning.
Positioning and possible errors in String.Format
As you may expect, for string.Format
positioning is 0-based. But if it’s true that the numbers must start with zero, it’s also true that the actual position doesn’t count. In fact, the next two strings are the same:
var m1 = String.Format("I caught a {0} on {1}", pkm.Name, pkm.CaptureDate);
var m2 = String.Format("I caught a {1} on {0}", pkm.CaptureDate, pkm.Name);
Of course, if you swap the positioning in the string, you must also swap the order of the parameters.
Since we are only specifying the position, we can use the same value multiple times inside the same string, just by repeating the placeholder:
String.Format("I caught a {0} (YES, {0}!) on {1}", pkm.Name, pkm.CaptureDate);
// I caught a Garchomp (YES, Garchomp!) on 06/05/2020 14:55:23
What happens if the number of position is different from the number of arguments?
If there are more parameters than placeholders, the exceeding ones are simply ignored:
String.Format("I caught a {0} on {1}", pkm.Name, pkm.CaptureDate, pkm.PokedexIndex);
/// I caught a Garchomp on 06/05/2020 14:55:23
On the contrary, if there are more placeholders than parameters, we will get a FormatException
:
String.Format("I caught a {0} on {1}", pkm.Name);
with the message
Index (zero based) must be greater than or equal to zero and less than the size of the argument list_.
How to format numbers
You can print numbers with lots of different formats, you know. Probably you’ve already done it with the ToString
method. Well, here’s almost the same.
You can use all the standard numeric formats as formatting parameters.
For example, you can write a decimal as a currency value by using C
or c
:
String.Format("{0:C}", 12.7885m);
// £12.79
In this way, you can use the symbols belonging to the current culture (in this case, we can see the £
) and round the value to the second decimal.
If you want to change the current culture, you must setup it in a global way or, at least, change the culture for the current thread:
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("it-IT");
Console.WriteLine(String.Format("{0:C}", 12.7885m)); // 12,79 €
If you want to handle numbers with different formats, you can all the formats defined in the official documentation (linked above). Among them we can find, for example, the fixed-point formatter that can manage both the sign and the number of decimal digits:
String.Format("{0:f8}", 12.7885m) //12.78850000
With :f8
here we are saying that we want the fixed-point format with 8 decimal digits.
How to format dates
As per numbers, the default representation of dates is the one provided by the ToString
method.
String.Format("{0}", new System.DateTime(2020,5,8,1,6,0))
// 08/05/2020 01:06:00
This is useful, but not very customizable. Luckily we can use our usual formatting strings to print the date as we want.
For example, if you want to print only the date, you can use :d
in the formatting section:
String.Format("{0:d}", new System.DateTime(2020,5,8,1,6,0))
// 08/05/2020
and you can use :t
if you are interested only in the time info:
String.Format("{0:t}", new System.DateTime(2020,5,8,1,6,0))
// 01:06
Of course, you can define your custom formatting to get the info in the format you want:
String.Format("{0:yyyy-MM-dd hh:mm}", new System.DateTime(2020,5,8,1,6,0))
// 2020-05-08 01:06
PSS! Remember how the current culture impacts the result!
How to define custom formats
As you may imagine, the default value used for formatting is the one defined by ToString
. We can prove it by simply defining the ToString
method in our CapturedPokemon
class
class CapturedPokemon
{
// fields...
public override string ToString()
{
return $"Name: {Name} (#{PokedexIndex})";
}
}
and by passing the whole pkm
variable defined at the beginning of this article:
String.Format("{0}", pkm)
// Name: Garchomp (#445)
But, of course, you may want to use different formatting across your project.
Let’s define a formatter for my CapturedPokemon
class. This class must implement both IFormatProvider
and ICustomFormatter
interfaces.
public class PokemonFormatter : IFormatProvider, ICustomFormatter
{
// todo
}
First of all, let’s implement the GetFormat
method from IFormatProvider
with the default code that works for every custom formatter we are going to build:
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
return null;
}
And then we can define the core of our formatter in the Format
method. It accepts 3 parameters: format
is the string that we pass after the :
symbol, like in :d3
; arg
is a generic object
that references the object to be formatted and formatProvider
is… well, I don’t know! Drop me a comment if you know how to use it and why!
Moving on, and skipping the initial checks, we can write the core of the formatting like this:
switch (format.ToUpper())
{
case "FULL": return $"{pokemon.Name} (#{pokemon.PokedexIndex}) caught on {pokemon.CaptureDate}";
case "POKEDEX": return $"{pokemon.Name} (#{pokemon.PokedexIndex})";
case "NAME": return $"{shortName}";
default:
throw new FormatException($"The format {format} is not valid");
}
So the point is to define different formats, pass one of them in the format
parameter, and apply it to the arg
object.
We can then use the String.Format
method in this way:
String.Format(new PokemonFormatter(), "{0:full}", pkm) // Garchomp (#445) caught on 06/05/2020 14:55:23
String.Format(new PokemonFormatter(), "{0:pokedex}", pkm) // Garchomp (#445)
String.Format(new PokemonFormatter(), "{0:name}", pkm) //Grchmp
If you are interested in the whole code, have find it at the end of the article.
By the way, why should we care about formatters? Because we must always take into account the separation of concerns. Why would the CapturedPokemon
class expose a method for each formatting value? It should be in the scope of the class definition itself, so it’s better to write it somewhere else and use it only when it’s needed.
Conclusion
Using String.Format
is now considered a vintage way to format strings. Even Microsoft itself recommends to use string interpolation because it is more readable (syntax highlighting helps you see better what are the values) and more flexible (because you directly create the string instead of calling an additional method - string.Format itself).
By the way, I think it’s important to get to know even String.Format
because it can be useful not only for readability (because you can see the structure of the returned string even without looking at the actual parameters used) but also because you can create strings dynamically, like in this example:
string en = "My name is {0}";
string it = "Il mio nome è {0}";
var actualString = DateTime.UtcNow.Ticks % 2 == 0 ? it : en;
Console.WriteLine(string.Format(actualString, "Davide"));
If you want to read more about string.Format
, just head to the Microsoft documentation, where you can find lots of examples.
In the end, here’s the full code of the Format
method for our PokemonFormatter
class.
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (!this.Equals(formatProvider)) { return null; }
if (!(arg is CapturedPokemon pokemon)) { return null; }
if (string.IsNullOrWhiteSpace(format))
format = "full";
var shortName = Regex.Replace(pokemon.Name, "a|e|i|o|u", "");
switch (format.ToUpper())
{
case "FULL": return $"{pokemon.Name} (#{pokemon.PokedexIndex}) caught on {pokemon.CaptureDate}";
case "POKEDEX": return $"{pokemon.Name} (#{pokemon.PokedexIndex})";
case "NAME": return $"{shortName}";
default:
throw new FormatException($"The format {format} is not valid");
}
}
Happy coding!