String interpolation
An interpolated string is a string literal with a $ before its opening
quote. Inside it, an expression in braces, {name} or {a + b}, is replaced
by the text of its value:
# Interpolated strings: `$"..."` with expressions in braces.
main(): int {
var name = "Pepper";
var count = 3;
var ready = true;
# Each {name} is replaced by that variable's text.
var line = $"{name} has {count} items, ready: {ready}";
assert(line == "Pepper has 3 items, ready: true", "integers, bool and String");
# Any expression fits between the braces, strings included.
var sizes = [2, 5];
assert($"total {sizes[0] + sizes[1]}" == "total 7", "arithmetic and index");
assert($"{name.size} letters" == "6 letters", "call");
assert($"{count > 1 ? "many" : "one"}" == "many", "ternary with strings");
# Doubled braces are literal braces; escapes work as in any string.
assert($"{{count}} = {count}" == "{count} = 3", "literal braces");
assert($"tab\t{count}\n" == "tab\t3\n", "escapes");
return 0;
}
{expression}holds an expression: a name (a local, a parameter, a global, or inside a method a field), a member access ({p.x}), a call ({f(x)}), an index ({items[0]}), arithmetic ({a + b}), or a ternary ({a > b ? a : b}). Spaces around it are ignored.- The expression changes no variable: an assignment (
=,+=, …),++or--anywhere in it is an error, even inside a call’s arguments ({f(a = 3)}). Only a closure’s body, which does not run in place, may hold one. A function it calls may still have effects of its own. - String literals may appear inside the braces (
{greet("you")}), and so may character literals and other interpolated strings. A}inside them, or inside brackets of the expression’s own, does not end it. - The expression fits on one line and holds no comment: a line break
or a
#inside the braces is an error. - A format specification may follow the expression after a
:, as in{n:>8}or{n:08x}: see below. {{and}}are a literal{and}.- Escapes (
\n,\t,\", …) work as in any string literal. - The result is a new
String. An interpolated string with no braces is an ordinary string.
Which values can be interpolated
Each value is converted with to_string(value), so it can be:
- an integer of any size, signed or unsigned, which gives its decimal text;
- a
floatordouble, which gives the shortest decimal text that reads back as the same number, as Swift prints one:0.1,3.0(a whole number keeps its.0),-2.5, and an exponent outside 1e-4 to 1e16 (1e+16,1.5e-05);nan,infand-inffor the special values; - a
bool, which givestrueorfalse; - a tuple of up to eight values that can each be
interpolated, which gives its elements’ text in parentheses,
(1, two)(aStringelement without quotes); - a
String, which is used as it is; - a value whose type has a
to_string(): Stringmethod, such as a struct that implements the runtime’sToStringtrait, or a value of typeToStringitself; - a value whose type has a
to_string<T>override. Likehash<Point>(see overriding a generic function), an override applies to the whole program, so it serves types that have noto_string()method of their own.
# A struct is interpolated through its to_string() method.
struct Point {
var x: int = 0;
var y: int = 0;
construct(this.x = 0, this.y = 0) {}
}
extend Point with ToString {
to_string(): String {
# Inside a method, fields are names too.
return $"({x}, {y})";
}
}
# A type without the method gets a to_string<T> override instead.
struct Celsius {
var degrees: int = 0;
construct(this.degrees = 0) {}
}
to_string<Celsius>(value: Celsius): String {
return $"{value.degrees} C";
}
main(): int {
var start = Point(1, 2);
var reading = Celsius(21);
assert($"from {start}" == "from (1, 2)", "ToString");
assert($"it is {reading}" == "it is 21 C", "override");
# A trait value converts through the value's own method.
var shown: ToString = start;
assert($"{shown}" == "(1, 2)", "trait value");
return 0;
}
Numbers and tuples, with the format specifications below:
# Floating-point numbers and tuples have text too.
main(): int {
var price = 4.5;
var ratio = 1.0 / 3.0;
assert($"{price}" == "4.5", "the shortest text that reads back");
assert($"{2.0}" == "2.0", "a whole number keeps its point");
assert($"{1.0e20}" == "1e+20", "large numbers use an exponent");
assert($"{ratio:.3}" == "0.333", "a precision rounds to that many digits");
assert($"[{price:8.2}]" == "[ 4.50]", "numbers align right");
assert($"{price:+08.2}" == "+0004.50", "with a sign and zeros");
var point = (3, "north");
assert($"{point}" == "(3, north)", "a tuple's elements in parentheses");
print("{0} at {1:.1}", point, price); # (3, north) at 4.5
return 0;
}
Runtime types such as String, StringBuilder, io.Path, Uri and
regex.Match implement ToString. An enum is
interpolated through a to_string() method it declares (return name();
gives the case’s name).
A plain overload such as to_string(p: Point) is not used: the runtime’s
code does not import your file, so it cannot see yours (see
overloading). Write a
to_string<Point> override or a method instead.
Format specifications
A : after the expression starts a format specification, which pads the
value’s text, writes an integer in another base, or rounds a floating-point
number to a number of digits. Its parts, all optional, come in this order:
{value:[[fill]align][+][0][width][.precision][type]}
| Part | Meaning |
|---|---|
align | < left, ^ centered (leaning left), > right. Without one, numbers go to the right and everything else to the left. |
fill | The character the padding is made of, written before align; a space by default. It can be any character but {, }, ", ' and #. |
+ | Writes a + before a number that is not negative. |
0 | Pads with zeros after the sign (-0042). It cannot be combined with align. |
width | The least number of characters the text takes; longer text is kept whole. |
.precision | For a float or double: the number of digits after the point, rounded to nearest, a tie to the even digit ({pi:.2} is 3.14, {0.125:.2} is 0.12; .0 leaves no point). At most 100. |
type | d decimal, x and X hexadecimal (lower and upper case), b binary, o octal. |
# Format specifications: padding, signs and other bases after a `:`.
main(): int {
var count = 42;
var name = "Pepper";
# Width and alignment pad any value; numbers go right by default.
assert($"[{count:6}]" == "[ 42]", "a number goes right");
assert($"[{name:10}]" == "[Pepper ]", "text goes left");
assert($"[{name:>10}]" == "[ Pepper]", "right");
assert($"[{name:*^10}]" == "[**Pepper**]", "centered with a fill");
# Integers take a sign, zero padding and a base.
assert($"{count:+}" == "+42", "a plus sign");
assert($"{count:05}" == "00042", "zero padding");
assert($"{255:x} {255:X} {5:b} {8:o}" == "ff FF 101 10", "bases");
assert($"0x{255:08x}" == "0x000000ff", "padded hexadecimal");
assert($"{-42:x}" == "-2a", "a negative number keeps its sign");
# The `:` after the whole expression starts the specification.
assert($"[{count > 1 ? "many" : "one":>5}]" == "[ many]", "a ternary");
return 0;
}
- Padding (
fill,alignandwidth) works for any value that can be interpolated: it pads the value’sto_stringtext, counting characters, not bytes. +and0need a number: an integer (of any size, signed or unsigned), or afloatordoublegiven a precision ({x:+08.2}). Atypeneeds an integer, and a precision afloatordouble. Using one with any other value is an error at that character, and a precision cannot be combined with atype.- A negative number keeps its sign in every base:
{-42:x}is-2a, not the bits of its two’s complement. - The
:that starts the specification is the one after the whole expression, so a ternary can be formatted as it is:{n > 1 ? "many" : "one":>5}.
Formatting by position: String.format
String.format takes a string literal whose braces name its other
arguments by position, {0} for the first, each with an optional format
specification, and returns the text:
# String.format: braces name the arguments by position.
main(): int {
var a = 2;
var b = 3;
assert(String.format("{0} + {1} = {2}", a, b, a + b) == "2 + 3 = 5", "in order");
# An index can repeat or come out of order, with its own specification.
assert(String.format("{1} before {0}", "first", "second") == "second before first", "reordered");
assert(String.format("{0} is {0:x} in hex", 255) == "255 is ff in hex", "repeated");
assert(String.format("[{0:>4}] {{literal}}", a) == "[ 2] {literal}", "padding and braces");
return 0;
}
- The format must be a string literal: the compiler reads it, so an
index out of range, an argument no brace names, or a bad specification
is an error at compile time.
{{and}}are literal braces, as in an interpolated string. - Each argument is evaluated once, from left to right, however many braces name it and in whatever order.
- An index can repeat, to show one value twice or in two forms
(
"{0} is {0:x} in hex"). - It is part of the compiler, not a function in the runtime: it cannot be stored in a variable or called through a function value.
printtakes the same arguments:print("{0} of {1}", done, total)prints whatString.formatmakes of them.
Errors
A value with no conversion is reported at its braces, with a note at its type:
main.pep:5:17: error: cannot interpolate a value of type 'Foo': it has no 'to_string()' method and no 'to_string' overload
5 | print($"f = {f}");
| ^~~
main.pep:1:8: note: declare 'to_string(): String' in 'Foo', or implement 'ToString' with 'extend Foo with ToString'
1 | struct Foo {}
| ^~~
A lone brace is an error, since doubling it is how to write one, and so is
anything after the expression but its }:
main.pep:2:15: error: unmatched '}' in interpolated string; write '}}' for a literal '}'
2 | print($"a } b");
| ^
main.pep:3:18: error: expected '}'
3 | print($"a {x b");
| ^
main.pep:3:15: note: to match this '{'
3 | print($"a {x b");
| ^
Empty braces are reported where the expression should start:
main.pep:2:16: error: expected an expression after '{' in interpolated string; write '{{' for a literal '{'
2 | print($"a {} b");
| ^
An error in the expression itself, such as a name that is not declared
(use of undeclared identifier 'x'), is reported like any other, at the
token inside the braces.
A format specification’s errors are reported at the character in it that is wrong:
main.pep:5:16: error: unexpected 'q' in format specification; expected '[[fill]align][+][0][width][.precision][type]'
5 | print($"{n:q}");
| ^
main.pep:5:19: error: cannot format a value of type 'String' as an integer
5 | print($"{name:x}");
| ^
main.pep:6:16: error: cannot format a value of type 'i32' with a precision; only 'float' and 'double' take one
6 | print($"{n:.2}");
| ^
String.format reports an index with no argument at the index, and an
argument with no index at the argument:
main.pep:5:31: error: argument index 2 is out of range for 2 arguments
5 | print(String.format("{0} {2}", n, n));
| ^
main.pep:5:35: error: argument 1 of 'String.format' is not used by its format string
5 | print(String.format("{0}", n, n + 1));
| ^~~~~
Changing a variable inside the braces is reported at the operator:
main.pep:3:16: error: an assignment cannot appear in an interpolated string
3 | print($"{a += 1}");
| ^~
How it works
The compiler turns $"({x}, {y + 1})" into "(" + to_string(x) + ", " + to_string(y + 1) + ")", resolving to_string in the runtime rather than
in your file. So a struct’s own to_string() method can interpolate its
fields without calling itself, and the result costs what the same +
expression does. A value with a format specification goes through a
runtime call that pads or converts its text instead. String.format becomes
the same concatenation, with its arguments bound to hidden names first when
the braces name them out of order or more than once.