Strings Are Bytes
Goose has no string type. A string is an array of u8, and which member of
the array family you choose says what you want to do with it. This turns
out to be a good fit for the memory model: building text, storing text
inside records and cutting text into pieces each get the representation
that suits them, and none of them needs a heap.
#Four kinds of string9.1
| Type | Use it for |
|---|---|
u8[>..] |
building text: appending is a pointer bump |
u8[] |
finished text that owns its bytes, such as a name in a record |
u8[..k] |
short text inside a struct, with room for k bytes |
u8[:] |
a view of bytes that live somewhere else |
A string literal is a view: a const u8[:] of bytes in the program's
read-only data. It is read-only, which is why some slots have to be
declared const before they can hold a literal, as you will see below.
No encoding is enforced. Strings are bytes, and the standard library treats them as UTF-8 by convention where it matters.
#Building text9.2
A u8[>..] is a string builder. append adds the bytes of another string,
and push adds a single byte. The built-in format appends the text form
of any values, exactly as print would write them:
fn main() {
var s: u8[>..] = [];
s.append("honk");
s.push('!');
format(s, " x", 3, ", weight ", 4.5);
print(s, " [", s.len, " bytes]");
}
honk! x3, weight 4.5 [20 bytes]
str(…) takes the same arguments as print, and returns the text as a new
builder. Like every value in Goose it is built directly in its destination,
so names.push(str("goose #", i)) writes the text straight into the new
element:
fn main() {
let msg = str("eggs: ", 12, ", geese: ", 3);
print(msg, " (", msg.len, ")");
}
eggs: 12, geese: 3 (18)
Remember that a character literal is an integer. str('g') is the text
"103", not "g"; to add one byte, use push.
#Literals and escapes9.2.1
String literals use the escapes you know: \n, \t, \r, \0, \\,
\", \', and \xNN for any byte. A raw string is written between
""" delimiters and has no escapes at all. A multi-line raw string has its
closing delimiter's indentation removed from every line, so it can be
indented with the code around it:
fn main() {
print("tab\there, quote \" and \x41");
print("""C:\geese\list.txt""");
print("""
Usage: honk [-n count]
-n how many times
""");
}
tab here, quote " and A C:\geese\list.txt Usage: honk [-n count] -n how many times
#Storing text9.3
Text that is finished and needs a home goes into a u8[] or a u8[..k].
Initializing one from any string, including a literal or a slice, copies
the bytes into it, so the new value owns its text:
fn main() {
var draft: u8[>..] = [];
draft.append("Ada of the pond");
let name: u8[] = draft[..3]; // three bytes of its own
draft.clear(); // fine: name does not point into draft
print(name);
}
Ada
A u8[..k] is the natural choice inside a struct, because it keeps the
struct fixed-size, which in turn keeps an array of such structs indexable
and sortable. A limited array is also a builder up to its capacity, so you
can format into one:
struct Goose { name: u8[..12], eggs: i32 }
fn main() {
var g = Goose { name: "Ada", eggs: 0 };
format(g.name, " Jr.");
print(g.name, " ", g.name.len, "/", g.name.cap);
}
Ada Jr. 7/12
When a struct holds a view rather than its own bytes, its field type says
whether the view may be read-only. A field declared u8[:] holds a
writable view, and a string literal cannot go there. Declare it const
u8[:] and it can:
struct Label { text: const u8[:] }
fn main() {
let l = Label { text: "fragile: contains eggs" };
print(l.text);
}
fragile: contains eggs
#Printing values inside text9.4
Every value has a text form. Strings print as their bytes, but a string inside another value, such as an array or a struct, is quoted and escaped, so you can tell where it begins and ends:
struct Goose { name: u8[..12], eggs: i32 }
fn main() {
let name: u8[] = "Ada";
print(name, " ", [name], " ", Goose { name: "Bea", eggs: 2 });
}
Ada ["Ada"] Goose { "Bea", 2 }
To give your own type a text form of its own, write an overload of format
that takes the type. print, str and format all use it, anywhere the
type appears, including inside arrays:
struct Goose { name: u8[..12], weight: f32 }
fn format(out: u8[>..]&, g: Goose) {
format(out, g.name, " (", g.weight, " kg)");
}
fn main() {
let flock = [Goose { "Ada", 4.5 }, Goose { "Bea", 3.75 }];
print(flock);
print("leader: ", flock[0]);
}
[Ada (4.5 kg), Bea (3.75 kg)] leader: Ada (4.5 kg)
#Cutting text up9.5
The text functions are in the std module, so they need import std;.
Almost all of them take a u8[:] and return slices of it. Cutting text
up copies nothing: the pieces are views into the original bytes.
import std;
fn main() {
let line = " name = Ada Lovelace ";
let body = trim(line);
let eq = find(body, "=");
print("[", trim(body[..eq]), "] [", trim(body[eq + 1..]), "]");
}
[name] [Ada Lovelace]
find returns the position of the first occurrence, or -1. trim,
trim_start and trim_end remove ASCII whitespace by returning a smaller
slice.
split cuts a string at a separator and returns a growable array of
slices; words splits at runs of whitespace and drops the empty pieces:
import std;
fn main() {
let parts = split("Ada,4.2,7", ',');
print(parts.len, " parts: ", parts);
print(words(" waddle to the pond "));
}
3 parts: ["Ada", "4.2", "7"] ["waddle", "to", "the", "pond"]
Each part takes sixteen bytes, a pointer and a length. None of the text was
copied. If you do not need the array at all, each_split and each_word
call a block for each piece instead. The { … } after the call is a
trailing block, a small function passed to each_split, with it as its
argument. Chapter 16 covers them properly.
import std;
fn main() {
each_split("north-east-south", '-') { print("go ", it); };
}
go north go east go south
#Converting to and from numbers9.6
parse_int and parse_flt read a number from the whole of a string, and
return it together with a bool that says whether the text was valid:
import std;
fn main() {
let n, ok = parse_int("7");
let w, wok = parse_flt("4.2");
let bad, bok = parse_int("seven");
print(n * 2, " ", ok, " ", w, " ", wok, " ", bad, " ", bok);
}
14 true 4.2 true 0 false
Going the other way, format writes numbers in their natural form. When
you want control over it, format_int takes a base, a minimum width and a
padding byte, and format_flt a number of decimals:
import std;
fn main() {
var out: u8[>..] = [];
format_int(out, 255, 16, 6, '0');
out.append(" ");
format_flt(out, 3.14159, 2);
print(out);
}
0000ff 3.14
#Comparing and changing text9.7
Strings compare with == like any arrays, and this works across kinds: a
builder against a literal, a u8[..12] field against a slice. For ordering,
compare(a, b) returns -1, 0 or 1. to_lower and to_upper change
ASCII letters in place, so they need a writable string. join writes
several strings into a builder with a separator between them:
import std;
fn main() {
var call: u8[>..] = [];
call.append("Honk");
to_upper(call);
var out: u8[>..] = [];
join(out, ["ada", "bea", "cleo"], ", ");
print(call, " ", call == "HONK", " ", compare("ada", "bea"), " ", out);
}
HONK true -1 ada, bea, cleo
#Bytes and characters9.8
Indexing a string gives a byte, a u8, which prints as a number. A string
literal's .len is its length in bytes, which is not the number of
characters once UTF-8 is involved. each_utf8 decodes the code points for
you, and push_utf8 appends one:
import std;
fn main() {
let word = "gänse";
var points = 0;
each_utf8(word) { points += 1; };
print(word[0], " ", points, " code points in ", word.len, " bytes");
}
103 5 code points in 6 bytes
#Exercises9.9
-
Write a function
initials(full_name: u8[:]) -> u8[>..]that returns the first letter of every word, so thatinitials("ada byron lovelace")is"abl". -
Parse the line
"Ada,4200;Bea,3900;Cleo,4600"into the total weight of all three geese. Usesplittwice, oreach_splittwice. -
Write a
formatoverload forstruct Egg { day: i32, grams: f32 }that prints an egg asday 3: 61.5 g, and print an array of two eggs.