Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Strings reference

The string type str (built in, immutable), plus the text-facing traits Display, Debug, and Into.

str

Concatenate with +. Interpolate with i"…{expr}…" (see Values and types) — the two are one construct, so both take the same right-hand values: a str, a number, or a bool. Everything else needs to_string() first.

impl str {
	fun len(self): i32
	fun is_empty(self): bool
	fun trim(self): str
	fun to_uppercase(self): str                      // full Unicode; see below
	fun to_lowercase(self): str                      // full Unicode; see below
	fun to_uppercase_ascii(self): str                // A-Z only, length-preserving
	fun to_lowercase_ascii(self): str                // A-Z only, length-preserving
	fun contains(self, needle: str): bool
	fun starts_with(self, prefix: str): bool
	fun ends_with(self, suffix: str): bool
	fun replace(self, from: str, to: str): str       // all occurrences
	fun repeat(self, count: i32): str
	fun split(self, separator: str): List<str>
	fun substring(self, start: i32, end: i32): str   // end-exclusive; see below
	fun code_at(self, index: i32): u32               // UTF-16 code unit
	fun index_of(self, needle: str): Option<i32>     // declared in std::option
	fun last_index_of(self, needle: str): Option<i32> // likewise
	fun strip_prefix(self, prefix: str): Option<str> // likewise
	fun strip_suffix(self, suffix: str): Option<str> // likewise
	fun parse_i32(self): Option<i32>                 // likewise
	fun parse_f64(self): Option<f64>                 // likewise
}

Case: the full pair and the ASCII pair

There are two case mappings here, and which one a call site wants is a question about the text, not about convenience.

to_uppercase / to_lowercase are the host’s full Unicode mapping, locale-independent. Non-ASCII letters map, and one-to-many mappings apply — so the result can be a different length than the receiver, and an index into one is not an index into the other. This is the pair for human text.

to_uppercase_ascii / to_lowercase_ascii shift exactly the 26 ASCII letters and touch nothing else. ß, DZ, İ, É and every other code unit come back unchanged, so the length is always preserved and indices survive. This is the pair for protocol text — file extensions, mime rows, HTTP header names, HTML tag names, hex — where “case-insensitive” is defined over ASCII to begin with.

fun main() {
	print("Straße".to_uppercase());          // "STRASSE" — ß became SS
	print("Straße".to_uppercase().len());    // 7, from a 6-unit receiver
	print("Straße".to_uppercase_ascii());    // "STRAßE" — ß untouched
	print("Straße".to_uppercase_ascii().len());  // 6, always
	print("STRASSE-DZ".to_lowercase());       // "strasse-dz" — U+01F1 mapped
	print("STRASSE-DZ".to_lowercase_ascii()); // "strasse-DZ" — U+01F1 kept
}

Reaching for the full fold on protocol text is a bug, not a nicety. U+212A KELVIN SIGN lowers to a plain k under to_lowercase, so a full fold makes a character that is not an ASCII letter compare equal to one — which is how a tag-name test in std::process came to accept it. The _ascii pair cannot do that, and its length guarantee is what lets a scanner lowercase a whole document and keep using the indices it already computed.

Locating: index_of and last_index_of

contains answers whether; these answer where. They are what substring needs and nothing else computed — without them, text is taken apart with split and put back together. The index is zero-based and counts UTF-16 code units — substring’s own unit, so a bound one of these returns feeds substring directly, non-BMP text included.

import std::option::{ Some, None };

fun main() {
	let line = "key: value";
	match line.index_of(": ") {
		Some(let at) => {
			print(line.substring(0, at));                 // "key"
			print(line.substring(at + 2, line.len()));    // "value"
		}
		None => print("no separator"),
	}
	print("a.b.c".last_index_of(".").unwrap_or(-1));      // 3 — the final one
}

Absence is None, never -1. That is the whole reason these are not the host’s indexOf renamed: -1 is an index nothing in the type system tells apart from a real one, and it would be reported one call later, by substring, about a number this call produced. The Option makes “not found” a case you have to answer.

The empty needle sits at each end, as it does in the host: s.index_of("") is Some(0) and s.last_index_of("") is Some(s.len()).

Slicing: substring refuses, it does not correct

substring(start, end) requires

0 <= start <= end <= len()

and panics on anything else — a negative bound, an inverted pair, or an end past the length. It never clamps and never swaps.

That rule is worth stating plainly because the obvious host behavior is the opposite one. JavaScript’s substring clamps a negative argument to 0 and swaps the pair when start > end, so s.substring(offset, -1) there returns s[0..offset] — the prefix, the exact complement of the suffix the caller was reaching for, with no error at any point. Vilan refuses instead, on the same principle that makes an out-of-range list[i] a panic rather than a silent undefined.

Two consequences worth knowing:

  • “To the end” is spelled s.len(), not -1 and not an over-long end.
  • The bound is checked, not guessedsubstring(0, 100) on a short string is an error, not a truncation.

Where both bounds are literals the refusal happens at compile time:

error: substring end -1 is negative — the range must satisfy
       0 <= start <= end <= len, and substring never clamps or swaps

Empty ranges are legal, so the natural boundary cases all work: substring(0, 0), substring(len(), len()), and substring(0, len()).

Cutting a known affix

Reaching for substring to drop a leading or trailing marker is what invited the arithmetic in the first place. Prefer the verbs, which return Option<str> so that “absent” is distinguishable from “present but empty”:

import std::option::{ Some, None };

fun main() {
	match "data: 42".strip_prefix("data: ") {
		Some(let body) => print(body),
		None => print("not a data line"),
	}
	match "report.md".strip_suffix(".md") {
		Some(let stem) => print(stem),
		None => print("not markdown"),
	}
	// `starts_with`/`ends_with` test; these two cut.
	print("ab".strip_prefix("ab").unwrap_or("?"));   // "" — present, empty
	print("ab".strip_prefix("zz").unwrap_or("?"));   // "?" — absent
}
fun main() {
	let path = "/w/3/task/7";
	let parts = path.split("/").filter(|part| !part.is_empty());
	print(parts.len());
	print(parts[0].to_uppercase());
	print("task".repeat(2));
}

On a path, cut with std::path, not with these. strip_prefix and starts_with compare text, and text is the wrong unit for a path: "/a/bc".starts_with("/a/b") is true while /a/bc is not inside /a/b. path::starts_with and path::relative compare components, and path::basename/path::extname cut the affixes a filename actually has.

str also implements PartialEq/Ord (lexicographic ==, <) and Default ("").

Display: user-facing text

trait Display {
	fun to_string(self): str;
}
fun format<T: Display>(value: T): str

Implement Display for values that have a natural user-facing rendering; format(value) (from std::display) is the generic entry point. Concatenation and interpolation accept only what already has a string form — a str, a number, a bool — and refuse everything else by name, so format/to_string on a custom type is a call you write rather than one the compiler guesses at.

Debug: developer-facing text

trait Debug {
	fun debug(self): str;
}

[derive(Debug)] generates a structural rendering (Point { x: 1, y: 2 } style) for structs and enums: the standard tool for logging and error paths (error.debug() on an RpcError).

Into: conversions

trait Into<T> {
	fun into(self): T;
}

The generic conversion seam: implement Into<Target> on a source type, bound helpers as T: Into<Target>. (Numeric width conversions don’t use this: they’re the as_* methods on the numbers; see numbers.)