#{} calls to_s; format is printf¶
Level: 101 · anyone who knows f-strings and wants the Ruby spellings for the same jobs
One line: "#{expr}" evaluates any expression and calls to_s on the result — only double quotes and their relatives do it — while format (alias sprintf, and String#%) speaks printf: %05.2f, %-10s, %x, %p for inspect, %{name} for a Hash, %3$s for positions; Python's f-string is the same idea with the format mini-language after a colon, !r for repr, and one thing Ruby lacks — a built-in thousands separator.
Interpolation is the everyday tool: inside a double-quoted String, a %() literal, a heredoc or a backtick, #{...} holds an expression — an arithmetic, a method call, another interpolated String — and its value's to_s is spliced in, so nil becomes nothing and an Array becomes its inspect (that is what Array#to_s is). Single quotes keep the characters. The result is always a new String. format (sprintf, or the % operator on a String with an Array or a Hash on the right) is C's printf with additions: %s calls to_s and %p calls inspect; a width, a - flag, a + or space sign flag, a 0 pad and a precision behave as in C; %b is binary and %#x adds the prefix; %{name} and %<name>d take a Hash; %1$s picks an argument by position; %*d takes the width from an argument; and a precision on %s truncates. There is no thousands-separator flag — a gsub with a lookahead is the idiom. ljust, rjust and center pad by characters, which is the text library's ljust and %-8s pad by code points ↗; <<~ heredocs strip the common indentation, which is its <<~ strips indentation ↗ page. Numbers have their own page in this library: Number formatting.
Verified output of string_formatting_and_interpolation_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. "x is #{x}" interpolates; 'x is #{x}' does not "x is 5" "x is \#{x}"
2. "#{obj}" calls to_s; nil, an Array, a Symbol, a Hash "Foo!" "|[1, 2]|sym|1.0|{a: 1}"
3. any expression, even a nested string with its own #{} "3" "in 5"
4. format("%05.2f", pi); "%.3e"; "%x %o %b %08b %X" 03.14; 1.235e+05; ff 10 101 00000101 FF
5. "%+d % d" signs; "%5d|%-5d|" width and left-justify +5 5; 42|42 |
6. String#%: "%s and %s" % [a, b]; "%s" % one; "%d%%" % 50 a and b; one; 50%
7. %p is inspect: "%p %s" % ["str", "str"]; %p and %s of nil "str" str; "nil||"
8. named: "%{name} is %<age>d" with a Hash; "%<f>.2f" Ada is 36; 3.14
9. ljust(6, "."), rjust(6), center(6, "*"); "%-10s|"; "%10s|" ab....| ab| **ab**| left | right|
10. 255.to_s(2), to_s(16); "ff".to_i(16), "ff".hex; "%#x %#o %#b" 11111111 ff; 255 255; 0xff 010 0b101
11. format("%.2f", 2.675) and 2.675.round(2); "%.0f" of 2.5, 3.5 2.68 2.68; 2 4
12. "%.10g" % (1/3.0); 1e20.to_s; 100.0.to_s; 1e-5.to_s 0.3333333333; 1.0e+20; 100.0; 1.0e-05
13. positional "%3$s %1$s %2$s"; width from an argument "%*d" c a b; 42|
14. a precision on %s truncates: "%.2s" % "abcdef" ab
15. no thousands flag: "%,d" raises; the gsub idiom ArgumentError: malformed format string - %,; 1,234,567
16. padding counts characters: "%-6s|" % "caf\u{e9}"; ljust café | café..|
17. <<~ strips the common indentation; <<- keeps it "indented\n more\nback\n" " kept\n"
18. "%c" of an Integer and of a String A h
19. format("%s", 3.0); ("%d", 3.99) truncates; ("%f", 3) 3.0; 3; 3.000000
20. an interpolated string is a new object: equal?; == false; true
Reading the output¶
Rows 1–3 are interpolation: double quotes interpolate, single quotes keep #{x} literally (its inspect shows the # escaped), to_s is what gets called — Foo! from the class's own to_s, nothing for nil, [1, 2] for an Array, sym for a Symbol, {a: 1} for a Hash — and any expression works, including a String that interpolates on its own. Rows 4–5 are printf's numbers: zero padding, exponent form, the four radixes with %08b padded and %X upper-case, the sign flags, and width with left-justification. Rows 6–8 are String#% with an Array, a lone value, a literal %%, %p giving "str" with quotes and nil as the word, and the Hash forms %{name} and %<age>d (the angle form takes a format, %<f>.2f). Row 9 pads with ljust/rjust/center and with %-10s/%10s. Row 10 converts radixes without format: to_s(2), to_s(16), to_i(16), hex, and the # flag. Row 11 is rounding: format("%.2f", 2.675) and 2.675.round(2) both give 2.68, while %.0f of 2.5 and 3.5 gives 2 and 4, half to even. The binary double nearest to 2.675 is a hair below it, which is why C's printf and Python round it down; Ruby's Float#round and format treat the shortest decimal that names the double as the value being rounded. (Not machine-checked here.) Row 12: %g and Float#to_s, which writes 1.0e+20 and 1.0e-05 with a mantissa that always has a decimal point. Rows 13–14: positional arguments, a width from an argument, and a precision that truncates a String. Row 15 is the missing feature: %,d is an ArgumentError, and the gsub idiom inserts the commas. Row 16: %-6s and ljust count the four characters of café, not its five bytes. Row 17: <<~ removed the two leading spaces and kept the relative indent; <<- kept everything. Rows 18–19: %c takes an Integer or the first character of a String; %s of 3.0 is 3.0, %d truncates 3.99 to 3, %f of an Integer is 3.000000. Row 20: the interpolated "#{str}" is equal to str but not the same object.
Compared with Python¶
An f-string is "#{}" with the format spec after a colon: f"{x}" calls str() (row 2: None prints as the word None, where Ruby printed nothing), f"{x!r}" calls repr() — Ruby's %p (row 7) — and since Python 3.12 an f-string may nest quotes of the same kind, so f'{f"in {x}"}' works (row 3). Every printf flag in rows 4–5 has a mini-language spelling — :05.2f, :.3e, :x :o :b :08b :X, :+d, : d, :5d, :<5d — and the old % operator still accepts the C forms (row 6), with a tuple where Ruby takes an Array. str.format does named fields from keywords and {2} {0} {1} positions (rows 8 and 13), f"{x=}" prints the expression and its value (row 8), and a nested {w} inside the spec gives a width from a variable (row 13). Radixes are bin/hex/oct and int(s, 16) (row 10). Row 11 is the difference to remember: f"{2.675:.2f}" and round(2.675, 2) give 2.67, because Python rounds the binary value, which is a hair below 2.675, while :.0f of 2.5 and 3.5 gives 2 and 4 just as C does. str(1e20) is 1e+20 with no .0 (row 12). Row 15 is the feature Ruby lacks: f"{n:,}" and f"{n:_}" insert separators. Padding counts characters here too (row 16), the Python library's Padding is not alignment ↗ says why that is still not alignment, and The format mini-language ↗ is the whole grammar, measured. textwrap.dedent is <<~ (row 17). Row 20 differs: f"{s}" of a single str can hand back the very same object — a str is immutable, so nothing is lost — where Ruby always builds a new String.
Verified output of string_formatting_and_interpolation_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. f"x is {x}" interpolates; "x is {x}" does not 'x is 5' 'x is {x}'
2. f"{obj}" calls str; None, a list, a str, a dict 'Foo!' "None|[1, 2]|sym|1.0|{'a': 1}"
3. any expression, even a nested f-string '3' 'in 5'
4. f"{pi:05.2f}"; ":.3e"; ":x :o :b :08b :X" 03.14; 1.235e+05; ff 10 101 00000101 FF
5. ":+d" and ": d" signs; ":5d|" and ":<5d|" width +5 5; 42|42 |
6. "%s and %s" % (a, b); "%s" % one; "%d%%" % 50 a and b; one; 50%
7. !r is repr: f"{'str'!r} {'str'}"; !r and str of None 'str' str; 'None|None|'
8. "{name} is {age:d}".format(**h); f"{x=}" (3.8+) Ada is 36; x=5
9. ljust(6, "."), rjust(6), center(6, "*"); ":<10|"; ":>10|" ab....| ab| **ab**| left | right|
10. bin(255), hex(255); int("ff", 16); ":#x :#o :#b" 0b11111111 0xff; 255; 0xff 0o10 0b101
11. f"{2.675:.2f}" and round(2.675, 2); ":.0f" of 2.5, 3.5 2.67 2.67; 2 4
12. f"{1/3:.10g}"; str(1e20); str(100.0); str(1e-5) 0.3333333333; 1e+20; 100.0; 1e-05
13. "{2} {0} {1}".format(...); a nested width f"{n:{w}d}" c a b; 42|
14. a precision on a str truncates: f"{s:.2}" ab
15. thousands built in: f"{n:,}" and f"{n:_}" 1,234,567; 1_234_567
16. padding counts characters: f"{s:<6}|"; ljust café | café..|
17. textwrap.dedent strips the common indentation 'indented\n more\nback\n'
18. "%c" of an int and of a one-character str A h
19. str(3.0); "%d" % 3.99 truncates; "%f" % 3 3.0; 3; 3.000000
20. an f-string of one str may be the same object: is; == True; True
Try it¶
- Print an aligned table of items — name left-justified in 10 columns, quantity right-justified in 5, price in 12 with two decimals and a thousands separator (the
gsubidiom) — and a total line, usingformatonly. - Write
money(amount, currency: "USD")that formats1234.5asUSD 1,234.50and-0.5asUSD -0.50, then rewrite it withString#%and a Hash (%<sym>s %<amount>s) so the two spellings give identical output for five values.
Solution to 1
string_formatting_and_interpolation_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: an aligned table -- names left-justified, quantities right-justified,
# prices with two decimals and a thousands separator, and a total line.
def money(amount)
whole, cents = format("%.2f", amount).split(".")
"#{whole.gsub(/\B(?=(\d{3})+(?!\d))/, ",")}.#{cents}"
end
items = [["widget", 3, 4.5], ["gadget", 12, 1234.567], ["gizmo", 1, 0.5]]
puts format("%-10s %5s %12s", "item", "qty", "price")
puts "-" * 29
items.each do |name, qty, price|
puts format("%-10s %5d %12s", name, qty, money(price))
end
puts "-" * 29
puts format("%-10s %5d %12s", "total", items.sum { |_, q, _| q }, money(items.sum { |_, q, p| q * p }))
Verified output of string_formatting_and_interpolation_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
putsadds a newline,printdoes not,pshowsinspect—printfisformatplus a write;pis%pfor a whole lineTimeis an instant,Dateis a calendar day —strftime, the third format language- Text lives in the Ruby text library — where padding, encodings and literals are measured in depth
- Number formatting —
%e,%g,round,to_s(16)and the thousands idiom, on numbers alone to_s,inspectandp— the two methods that#{}and%pcallbindingandeval—ERB, the template language that is interpolation over a bindingljustand%-8spad by code points, not columns ↗ — row 16, in depth<<~strips indentation — by a different rule than Perl's ↗ — row 17, in depth- The format mini-language ↗ — Python's grammar after the colon, measured
- Padding is not alignment ↗ — why a padded column still drifts with wide characters