This is a follow-up to my original answer in order
to make sure this is the intended behavior
As far as an official source is concerned, we should refer to the Interpolated Strings from msdn.
The structure of an interpolated string is
$ " <text> { <interpolation-expression> <optional-comma-field-width> <optional-colon-format> } <text> ... } "
and each single interpolation is formally defined with a syntax
single-interpolation:
interpolation-start
interpolation-start : regular-string-literal
interpolation-start:
expression
expression , expression
What counts here is that
- the
optional-colon-format
is defined as a regular-string-literal
syntax => i.e. it can contains an escape-sequence
, according to the paragraph 2.4.4.5 String literals
of the C# Language Specification 5.0
- You can use an interpolated string anywhere you can use a
string literal
- To include a curly brace (
{
or }
) in an interpolated string use two curly braces, {{
or }}
=> i.e. the compiler escapes two curly braces in the optional-colon-format
- the compiler scans the contained interpolation
expressions
as balanced text until it finds a comma, colon, or close curly brace => i.e. a colon breaks the balanced text as well as a close curly brace
Just to be clear, this explains the difference between $"{{{date}}}"
where date
is an expression
and so it is tokenized until the first curly brace versus $"{{{date:o}}}"
where date
is again an expression
and now it is tokenized until the first colon, after which a regular string literal begins and the compiler resumes escaping two curly braces, etc...
There is also the String Formatting FAQ from msdn, where this case was explicitly treated.
int i = 42;
string s = String.Format(“{{{0:N}}}”, i); //prints ‘{N}’
The question is, why did this last attempt fail? There’s two things
you need to know in order to understand this result:
When providing a format specifier, string formatting takes these
steps:
Determine if the specifier is longer than a single character: if so,
then assume that the specifier is a custom format. A custom format
will use suitable replacements for your format, but if it doesn’t know
what to do with some character, it will simply write it out as a
literal found in the format Determine if the single character
specifier is a supported specifier (such as ‘N’ for number
formatting). If it is, then format appropriately. If not, throw an
ArgumnetException
When attempting to determine whether a curly bracket should be
escaped, the curly brackets are simply treated in the order they are
received. Therefore, {{{
will escape the first two characters and
print the literal {
, and the the third curly bracket will begin the
formatting section. On this basis, in }}}
the first two curly
brackets will be escaped, therefore a literal }
will be written to
the format string, and then the last curly bracket will be assumed to
be ending a formatting section With this information, we now can
figure out what’s occurring in our {{{0:N}}}
situation. The first
two curly brackets are escaped, and then we have a formatting section.
However, we then also escape the closing curly bracket, before closing
the formatting section. Therefore, our formatting section is actually
interpreted as containing 0:N}
. Now, the formatter looks at the
format specifier and it sees N}
for the specifier. It therefore
interprets this as a custom format, and since neither N or } mean
anything for a custom numeric format, these characters are simply
written out, rather than the value of the variable referenced.