F# from the ground up

Text, splitting and format strings

Strings join with `+`, split into arrays and come back together with `String.concat`. A format string with widths is what turns values into columns a reader can scan.

Part 11 of 12

Advanced18 min readBack to Courses

Strings are values with methods

A string is text, and it brings the .NET methods with it: title.ToUpper(), title.Substring(4, 6), csv.Split([| ',' |]). Around those, F# adds plain functions that read better in a pipe: String.length counts the characters and String.concat joins a list with a separator.

Splitting answers an array — a fixed-length .NET collection, written [| 'a'; 'b' |] — so List.ofArray turns it into a list the rest of the course already knows how to walk. String.concat is the inverse: split a line of CSV into fields, work on them, join them back with a different separator.

Format strings line things up

The format string in printfn is checked: %d needs a whole number, %s a string, %A anything at all. Add a width and the column lines up — %6s pads on the left, %-6s pads on the right, %4d right-aligns a number in four characters. That is how a report stays readable without a table library.

sprintf is printfn that returns the text instead of printing it, so a line can be built by a function and printed later. %A on a string keeps the quotes, because it shows the value the way you would type it — which is exactly what you want when the difference between text and "text" matters, and exactly what you do not want in a sentence.

Split, join, and build a line

let csv = "ada,bo,cy"
let parts = csv.Split([| ',' |]) |> List.ofArray

let line = sprintf "%-6s %4d" "ada" 42
printfn "%s" line

A sentence, a slice and three columns

Run it and count the spaces in the last two lines. Then widen the first column to eight and check that `ada` and the number still do not touch.

Output
Click "Run" to execute your code

A format string with the wrong number of arguments is a compile error, and so is %d given a string. That check is the reason to prefer printfn and sprintf to joining text with +: the mistake is caught before the program runs, not in the output.

A width is a minimum, not a limit

%6s means "print this text in a field at least six characters wide", padding on the left so the text sits against the right edge. %-6s pads on the right instead, which is what a name column wants. A number with %6d pads on the left for the same reason: digits line up when their last position matches.

The width never truncates. A seven-character name in a six-character field takes seven characters and pushes the rest of the line one place to the right, which is why a report checks its longest value rather than assuming. The header has to use the same widths as the rows, or the columns look aligned in the source and ragged on screen.

A point after the width adds precision for floats: %8.2f is eight characters wide with two digits after the decimal point. The rounding happens for display only; the value in the binding is unchanged.

String.length counts UTF-16 code units, not user-perceived characters, so a string with an emoji can report a length larger than the number of symbols a reader sees. Substring indexes the same units, which is why slicing text that may contain characters outside the basic plane can split one symbol in half. For the ASCII names and CSV fields in this course the distinction never appears; for user text it does.

Two families of string functions

A string in F# is a .NET string, so it carries instance methods: Trim, ToUpper, Replace, Substring, Split. It also has module functions that read well in a pipe: String.length, String.concat, String.replicate. Use the method when it is a property of one string; use the function when you are building a pipeline.

Split answers an array, which is indexed with .[0] and is not a list. List.ofArray converts it when the list helpers are more convenient. String.concat is the inverse operation: it joins a sequence of strings with one separator between each pair.

sprintf builds a string and printfn prints one, and both use the same checked format language. Building a line first is useful when the line will be stored, sorted or handed to another function; printing it directly is right when the only purpose is the output.

String.replicate builds a rule line, and String.concat joins fields; neither needs a loop. When a line is assembled from several parts, sprintf with widths is usually clearer than concatenation plus padding by hand.

%s accepts only a string, so an int needs %d or an explicit conversion. %6s given a number is a type error, not a padded number. And Split has an overload for one character and one for an array of them, so Split([| ',' |]) uses the array form and returns an array.

Try these next

  • Print a three-row table with a header and check that every column starts at the same place.
  • Build the same line with sprintf and with String.concat, and say which one keeps the columns aligned.
  • Split a small CSV row into fields, double one number, and join the fields back with a semicolon.

A receipt with aligned columns

Run it and count the columns in the header and the rows. Then add a fourth item, widen the name column to ten, and parse a second CSV row with four fields.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does String.concat ", " ["a"; "b"] answer?