D from the ground up

Text: strings, slices and to!string

Measuring and cutting text, the verbs std.string and std.array provide, and the conversion that turns any value into a string and a string back into a number.

Part 4 of 12

Intermediate17 min readBack to Courses

A string is a slice of characters

A string in D is an array of characters that cannot be modified. word.length is the number of characters, word[0] is the first, and word[0 .. 4] is a slice of the first four. Inside the brackets $ stands for the length, so word[$ - 3 .. $] is the last three characters.

A slice does not copy. It is a window onto the same characters, which makes cutting text cheap no matter how long the original is. Reading through the window is fine; writing through it is not, because the characters behind a string are immutable.

Because a slice carries only an address and a length, it does not know where the original array ends. word[0 .. 100] on a five-letter word is not clamped: the bounds check compares against the slice length, and a range that runs past the end is refused at run time rather than silently shortened.

The verbs live in std.string and std.array

std.string holds the operations that treat text as text: strip removes whitespace from both ends, toUpper and toLower change case, indexOf finds the position of a substring and returns -1 when it is absent, and replace returns a new string with matches swapped.

std.array holds the ones that work on any array, text included: split cuts a string into an array of pieces, and join puts an array back together around a separator. Both are called with dot notation, as in csv.split(",") and parts.join(" + ").

The module names matter because D does not import them for you. strip without import std.string; is an undefined identifier, and the compiler names the symbol rather than the missing import — a message that sends a beginner looking for a typo in a name that is spelled correctly.

Turning values into text and back

to!string(value) from std.conv turns any value into its text form, and to!int(text) and to!double(text) go the other way. The ! marks a template argument, which is covered properly in a later chapter; for now read to!string(x) as "convert x to a string".

writefln is the other half of the story: a format string such as "%-5s %3d" lines columns up, and "%.2f" fixes a number at two decimals. The format function itself lives in a module this course does not import, because writefln covers the cases a report needs.

Bytes, not letters

A D string is a sequence of UTF-8 bytes, so .length counts bytes rather than visible characters. An ASCII letter is one byte; a letter such as é is two. Slicing at an arbitrary byte can land in the middle of a character, which is why slicing text is simple for ASCII and needs care for anything else.

foreach (char c; text) walks the bytes, while foreach (dchar c; text) decodes one whole code point at a time. char is one byte, wchar is two and dchar is four; only dchar can hold any character on its own. Nothing in this course leaves ASCII, so the two loops agree on every example here.

The distinction becomes visible the moment you slice by a fixed count. Cutting the first three bytes of a name that begins with an accented letter can split that letter in two, and the result prints as a replacement character. Count bytes when the text is ASCII, and decode when it is not.

Conversions that can fail

to!int("42") succeeds, and to!int("forty-two") throws a ConvException while the program runs. The conversion cannot be checked at compile time because the text is not known then. to!string in the other direction cannot fail for the types this course uses: every value has a text form.

indexOf answers with a position or -1, and replace answers with a new string because the original cannot be written. strip, toUpper and replace all return values; none of them changes the variable you called them on, so the result has to be kept or printed.

Split, join, convert

import std.stdio;
import std.array;
import std.conv;

void main() {
    string csv = "tea,cake,fig";
    auto parts = csv.split(",");
    writeln(parts.length, " ", parts[1]);
    writeln(parts.join(" + "));
    writeln(to!int("12") * 2);
}

Clean it, cut it, convert it

Run it and read the eight lines against the code that produced them. Then change raw to your own name, print the first three characters with a slice, and add a line that joins the split pieces with a comma and a space.

Output
Click "Run" to execute your code

indexOf counts from zero and returns -1 for no match, so a test for a missing piece of text is a comparison with -1 rather than with 0. Printing the position is the quickest way to check what a search really found.

Build initials from a name

Run it and count the parts and the bytes. Then add a middle name and check that the initials grow by one letter, and change the join separator to a plus sign.

Output
Click "Run" to execute your code

Three mistakes to expect: reading word[word.length] is one past the end and stops the program with a range error; comparing word[0] with "a" compares a char with a string and does not compile, so compare with char literal 'a'; and ignoring -1 from indexOf turns not-found into a number that looks like a position.

Try these next

  • Add a middle name and check that the initials grow by one letter.
  • Print the position of "hopper" with indexOf, then search for a word that is absent and print -1.
  • Join the parts with a plus sign and print the result in a field eight columns wide.

Check what you learned

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

Part 1 of 5

What does word[0 .. 4] give you?