snprintf
snprintf is how a C solution turns a number into
text, and in this repository it does two entirely different jobs. Every C solution uses it once at
the end, because the runner's solve() returns a const char * and something has to render the
answer — 336 of the 366 C files here contain the call for that reason alone. The problems below are
the ones where it does algorithmic work: where the Python sibling wrote str(n) and indexed,
sliced or reversed the result, and the port needs the same digit string. That second job is the one
worth understanding, because C gives you no str() and snprintf is the closest thing.
The idea
int snprintf(char *s, size_t n, const char *fmt, ...);
It makes two promises, and the second is the one people forget. It writes at most n bytes
including the terminating NUL — so it never overruns the buffer, and when n > 0 the result is
always NUL-terminated. And it returns the length it would have written, excluding the NUL —
not the length it actually wrote. Both halves are useful:
-
Truncation is detectable, and only via the return. The idiom is
if (ret < 0 || (size_t)ret >= size), with the cast becauseretis a signedintandsizeofis unsigned. The runner header itself does exactly this when it builds a resource path:int pn = snprintf(path, sizeof(path), "%s/resources/%.*s", dirname(prog), (int)name_len, name_start); if (pn < 0 || (size_t)pn >= sizeof(path)) return NULL; -
The return value is a free digit count. Problem 32 needs to know whether
a,bandcconcatenate to exactly nine digits, and gets the length from the same call that produces the string:char buf[20]; int len = snprintf(buf, sizeof(buf), "%d%d%d", a, b, c); if (len != 9) return 0;No
strlen, no separate digit-counting loop. Problems 125, 238 and 845 use the return the same way — to size or bound something before touching the characters.
The older sprintf has neither promise: no bound, and
its return tells you the damage only after it has been done. It survives here in exactly one place —
the hand-rolled bignum printers, which emit the
top limb bare and every following one zero-padded ("%09llu") so the concatenation is the decimal
expansion — and only because those buffers are sized from the limb count first, so overflow is
arithmetically impossible.
str(n) has no C equivalent
This is the whole reason the tag exists. In Python, str(n) gives you a random-access sequence of
digits and the rest is slicing. In C you format into a char array and index it, converting back
with - '0'. The ports below are that translation, four ways:
| Python | C | seen in |
|---|---|---|
str(n)[i] |
format, then buf[i] - '0' |
40 |
str(n)[::-1] |
format, then accumulate r = r*10 + (buf[i]-'0') backwards |
808 |
str(n) split at every position |
format once, recurse over split points on the buffer | 719 |
f"{a}{b}{c}" |
one call, "%d%d%d" |
32, 93 |
Problem 40 is the cleanest: it computes which number of Champernowne's constant contains the requested digit, then formats it and indexes — three lines that replace an entire digit-extraction loop.
char buf[32];
snprintf(buf, sizeof(buf), "%lld", number);
return buf[digit_in_number] - '0';
Problem 719 shows the shape at scale: each candidate square is formatted once, and the
backtracking search over its split points then works on that buffer, never re-formatting. Problem
808 formats a square only to walk it backwards. Problem 34 formats a digit-factorial sum so a
membership test can ask which digits it contains. Problem 17 is the outlier — it formats words,
not digits, composing "%s hundred and %s" from lookup tables into a per-number cache.
The concatenate-and-test pattern deserves one warning. Problem 93 writes four values into
char best_digits[5] with "%d%d%d%d", which fits exactly: four digits plus the NUL. That is
correct because the four values are provably single digits — the bound comes from an invariant of
the data, not from anything the format string enforces. Feed the same call a two-digit value and it
truncates silently, and you will not notice unless you check the return.
Formatting the answer
The runner's contract is explicit about lifetime:
The returned pointer is owned by
solve()and must stay valid until the next call — return a string literal or write into astaticbuffer; do not return a per-call malloc'd buffer (it would leak across--runs).
Hence the one-line tail that ends nearly every C solution here:
static char answer[32];
...
snprintf(answer, sizeof answer, "%lld", total);
return answer;
static because a plain local array dies when solve() returns and the pointer dangles;
32 bytes because the widest long long is 19 digits plus a sign and a NUL. Two solutions in the set
below — 151 and 93 — malloc their buffer instead and never free it, which is harmless at 32 bytes
a run but is exactly what the header asks you not to do.
Match the specifier to the type, and check it yourself: the build line is gcc -O2 -Werror with no
-Wall, and gcc's format checking rides on
-Wformat, which -Wall enables. A
%d handed a long long is undefined behaviour that this toolchain will compile without a word.
| result type | specifier |
|---|---|
long long / unsigned long long |
%lld / %llu |
size_t |
%zu |
GMP mpz_t |
gmp_snprintf with %Zd |
| fixed decimal places | %.6f, %.8f |
| significant digits | %.10g |
Problems 151 and 226 are the fixed-decimal cases: their answers are decimal fractions, %.6f and
%.8f against the Python siblings' f"{result:.6f}" and f"{total:.8f}". Both glibc and CPython
round the exact binary value of the double correctly, so the two strings agree — the risk is not
the formatter but the value. If the C and Python arithmetic differ by one
ULP and the true result sits near a rounding
boundary, the last printed digit flips and the C port fails a test case the Python one passes.
When the answer can be kept exact, keep it exact and place the decimal point yourself: problem 239
formats a scaled integer as "%lu.%0*lu" and problem 252 as "%lld.%d", neither of which can round
at all. (The * there takes the field width as an argument — the way to parameterise precision
without building the format string.)
How to reason about it
It is a runtime interpreter, so keep it out of hot loops. Every call parses the format string
and runs the full stdio conversion machinery — hundreds of nanoseconds, against a couple of
nanoseconds for a % 10 and a /= 10. Problem 206 says so in its own comment: it tests whether a
square matches the pattern 1_2_3_4_5_6_7_8_9 by reading digits with % 10 and skipping the free
ones with /= 100, "no string work", across a few hundred thousand candidates. Problem 719 goes the
other way and formats every candidate — but only after a mod-9 filter has discarded about 78% of
them first. That pairing is the one to imitate: cheap arithmetic filter, then the expensive format
on what survives (see order checks by cost).
Reach for it when the question is genuinely about the digit string. Splitting at arbitrary
positions, reversing, indexing the k-th digit from the left, testing a permutation with strcmp —
these are natural over characters and awkward over arithmetic, because they need the digit count up
front and left-to-right access, and division gives you neither. Counting digits, summing them,
testing divisibility, matching a fixed positional pattern — these are natural over arithmetic. The
tagged problems split cleanly along that line.
sizeof only works on arrays. snprintf(buf, sizeof buf, ...) is right where buf is a local
array and silently wrong where it is a char * parameter, which measures the pointer instead. A
function that writes into a caller's buffer must take the size as an argument — problem 17's word
formatter does exactly that.
Appending is where the bound gets lost. The accumulate idiom, pos += snprintf(out + pos, sizeof out - pos, ...), is correct only while nothing truncates: one truncated call pushes pos
past the end, and sizeof out - pos then underflows to an enormous size_t that defeats the bound
entirely. It appears here in problems 103 and 175, safe in both because the buffers are sized from
the data first. If you cannot prove the size, check each call's return before adding it.
Never format into the buffer you are reading from. snprintf(buf, n, "%s!", buf) is undefined
behaviour, not a clever in-place append.
Problems15 / 15
- ● 0017 — Number Letter Counts
- ● 0032 — Pandigital Products
- ● 0034 — Digit Factorials
- ● 0040 — Champernowne's Constant
- ● 0044 — Pentagon Numbers
- ● 0063 — Powerful Digit Counts
- ● 0093 — Arithmetic Expressions
- ● 0128 — Hexagonal Tile Differences
- ● 0151 — A Preference for A5
- ● 0184 — Triangles Containing the Origin
- ● 0198 — Ambiguous Numbers
- ● 0206 — Concealed Square
- ● 0226 — A Scoop of Blancmange
- ● 0719 — Number Splitting
- ● 0808 — Reversible Prime Squares