Exercises: String Handling and Text Processing

These exercises make you use the three tools of the chapter — deferred-length strings, the character intrinsics, and internal files — on the kind of text real scientific code handles: config lines, data fields, and output filenames. Predict every result before you compile; the intrinsics reward hand-tracing, and the off-by-one in a tokenizer is caught by tracing, not by hoping.

Difficulty: ⭐ warm-up · ⭐⭐ standard · ⭐⭐⭐ deeper. Solutions: worked solutions to the daggered (†) and odd-numbered problems are in appendices/answers-to-selected.md; the programs among them are in code/exercise-solutions.f90. Try every problem before you look.


Part A — Warm-ups ⭐

12.1 † For character(len=8) :: t = 'grid', state len(t), len_trim(t), and the value and length of trim(t).

12.2 Explain in one sentence what character(:), allocatable :: s declares, and name the two ways s can acquire a length.

12.3 † Give the value and the length of 'heat' // '_' // 'map'.

12.4 In one sentence each, distinguish trim(s) from adjustl(s). Which one changes the string's length?

12.5 † Predict the output of these five calls, then check with code/exercise-solutions.f90: index('banana','ana'), index('banana','ana',back=.true.), scan('banana','n'), verify('banana','ban'), verify('banana','ba').


Part B — Type, Compile, and Run ⭐⭐

Write the program, predict its output in a comment, then compile and run to confirm.

12.6 Write a program that stores x = 2.5_dp as text with the f8.2 descriptor via an internal write, then prints trim(adjustl(...)). What exactly is in the buffer before the trim, and after?

12.7 † Write the deferred-length growth loop: start s = 'ab', print len(s), then three times do s = s // 'ab' and print len(s). Predict the four lengths and the final string.

12.8 Write a program that declares character(len=10) :: a = 'cat', b = 'cat ' (three trailing blanks in b) and prints the logical value of a == b. Explain the result using Fortran's rule for comparing strings of equal length.


Part C — Port It ⭐⭐

Translate the Python to Fortran, and note where the semantics differ.

12.9 † Port the Python name = f"heat_{step:06d}.vtk" to Fortran using an internal-file write. What edit descriptor plays the role of :06d?

12.10 Port these three Python expressions to Fortran and note the base/sentinel differences: s.strip(), s.rstrip(), and s.find('x') (be explicit about what each returns when 'x' is absent).

12.11 Port parts = line.split() (split on runs of whitespace). You do not have to write the whole tokenizer — describe, in two or three sentences, how the chapter's scan/verify loop reproduces split()'s behavior, including its handling of multiple spaces.


Part D — Find the Bug ⭐⭐

Each snippet compiles-but-misbehaves or won't compile. Diagnose and fix it.

12.12 A path comes out wrong:

character(len=20) :: dir = 'output'
character(len=40) :: path
path = dir // '/run.dat'

trim(path) prints output /run.dat. What went wrong, and what is the one-token fix?

12.13 † This line draws a compiler error:

if (index(line, ',')) call handle_csv(line)

Why won't it compile, and what did the author almost certainly mean?

12.14 A frame writer emits filenames full of asterisks (heat_****.vtk) once the run passes step 9999:

write(fname, '(a, i4.4, a)') 'heat_', step, '.vtk'

Explain the asterisks and give two possible fixes.

12.15 † A tokenizer built by "split at every blank" returns an empty token between words that are separated by two spaces. Why does that happen, and how does searching with verify (rather than testing one character at a time) avoid it?


Part E — Internal Files and Parsing ⭐⭐

12.16 Write a function to_int(text) result(n) that returns the integer encoded in a string, using an internal read. Test it on ' 42 ' and '-17'. What happens (and what should you add) if text is '12x'?

12.17 † Write parse_config(line) that splits a line like alpha = 0.25 on its =, extracts the key as a trimmed string and the value as a real(dp), and prints both. Which intrinsic finds the =, and why do you wrap the key in trim(adjustl(...))?

12.18 Write ntokens(line) result(n) that returns the number of blank-separated tokens in line. Confirm it returns 5 for 'one two three four five' and 3 for 'the quick brown' (note the double space).


Part F — Design It (Extend the Heat Solver) ⭐⭐

12.19 † Generalize the Project Checkpoint's frame_name into frame_name(prefix, step, ext) result(name) that returns a deferred-length string such as temp_000042.dat. Show the single format string you use.

12.20 Your parallel solver (Part VIII) will write one file per MPI rank per step. Design a naming scheme and the internal-file write that produces, for rank 3 and step 123, the name heat_r003_000123.vtk. How many integer fields does the format have, and what are their widths?

12.21 † Design a one-line progress log for the time loop: given step (integer), t (the simulated time, real(dp)), and tmax (the current maximum temperature, real(dp)), build a message like step 000123 t=1.230 Tmax=99.5 with a single internal write. Give the format and the hand-computed output for those inputs.


Part G — Back of the Envelope and Interleaved ⭐⭐⭐

12.22 Back of the envelope. A run writes 1,000,000 frames, each named heat_NNNNNN.vtk (15 characters). Estimate the total bytes if you cached every name (not the field data) as fixed-length character(len=15). Is caching the names worth it, or should the output routine just rebuild each name with frame_name when it writes? Justify with the numbers.

12.23 (Interleaved — Chapter 5.) Compare character(len=16) :: names(100) with character(len=:), allocatable :: names(:) where each element is a deferred-length string. What does the first waste, what does the second cost, and when is each the right choice? (Hint: all elements of an array share one length.)

12.24 † (Interleaved — Chapter 7.) Sketch, in a few lines, how an internal read with iostat= lets you validate that a text field really is a number without crashing when it is not. What value does iostat take on a bad conversion, and what do you do with it?

12.25 (Interleaved — Chapter 3 and Chapter 11.) The rule "prefer allocatable" from Chapter 11 applies to strings too: why is character(:), allocatable almost always the right string type, rather than a character, pointer? Name two of the same advantages you gave for allocatable arrays.


Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the programs among them (12.5, 12.7, 12.9, 12.17, 12.19, 12.21, and the parsing helpers) are in code/exercise-solutions.f90. The port and design problems have more than one good answer — the appendix gives a model, not the only one.