#Text
Status: standard library specification draft.
This chapter defines the part of std.text that lib/std writes in
ordinary hd over the language tier:
- the
stringmethods above the representation intrinsics:trim,lower,split,replace,starts_with,lines, andrepeat; - the splitting and padding methods
split_once,split_whitespace,pad_start,pad_end, andcount; - the
charclassification methods andto_digit; - the UTF-8 conversions
to_utf8andstring::from_utf8, andUtf8Error; - the
Parsetrait,string.parse::[T](), and theParseimpls ofbool,char, andstring; - the raw-text string prefix
r.
The language tier keeps the representation of a string and the methods that expose it (Strings, String Indexing, String Methods):
| Item | Why it stays in the language tier |
|---|---|
len, s[i] |
they read the byte representation that the compiler lays out |
bytes, slice |
intrinsics over that representation; slice shares bytes in constant time |
chars, char_indices |
flow.for.string-explicit, a language rule, names them as the way to loop over a string |
| UTF-8 and byte offsets | module.string.utf8 and module.string.byte-offsets hold for every string method, in both tiers |
The prefix mechanism, @str_prefix and std.ops.Template, is language
tier too (Prefixed Strings).
#String Methods
std gives string these methods, beside the language-tier ones:
| Receiver | Methods |
|---|---|
string |
trim(self) -> string; lower(self) -> string; split(self, separator: string) -> List[string]; replace(self, old: string, replacement: string) -> string; starts_with(self, prefix: string) -> bool; lines(self) -> List[string]; repeat(self, count: usize) -> string |
- std-text.string.lower
loweruses Unicode Default Case Conversion with full mappings. - std-text.string.trim
trimremoves the UnicodeWhite_Spaceproperty at both ends. - std-text.string.split
split(separator)retains empty pieces between adjacent separators and at either end. - std-text.string.split.empty-separatorAn empty separator splits into one-scalar strings, with an empty input producing an empty list.
- std-text.string.split.absentWith a non-empty separator, an input without that separator, including the empty string, yields one piece, so
"".split(",")is[""]. - std-text.string.replace
replacereplaces non-overlapping matches from left to right. - std-text.string.replace.emptyAn empty
oldinserts the replacement at scalar boundaries. - std-text.string.starts-with
starts_withcompares scalar sequences exactly and performs no normalization or case folding. - std-text.string.lines
linesreturns the pieces between\nseparators, and removes a\rdirectly before each\n. - std-text.string.lines.finalA final
\nends the last line and starts no empty one, so"a\nb\n".lines()is["a", "b"]and"".lines()is[]. - std-text.string.repeat
repeat(count)joinscountcopies of the string, so acountof 0 gives"".
fn rows(text: string) -> usize:
text.lines().len() # 2 for "a\r\nb\n", as Rust's str::lines
fn rule(width: usize) -> string:
"-".repeat(width)#Splitting And Padding
std also gives string methods that split text a bounded number of
times, split it into words, pad it to a width, and count matches:
fn setting(line: string) -> string:
match line.split_once("="):
.Some((key, value)) => "$key is $value" # "port is 80=x" for "port=80=x"
.None => "no setting"
fn first_two(path: string) -> List[string]:
path.splitn("/", 2) # ["a", "b/c"] for "a/b/c"
fn cell(text: string) -> string:
text.pad_start(6) # " 42" for "42"
fn word_count(text: string) -> usize:
text.split_whitespace().len() # 2 for " a\tb "| Receiver | Methods |
|---|---|
string |
split_once(self, separator: string) -> (string, string)?; splitn(self, separator: string, n: usize) -> List[string]; split_whitespace(self) -> List[string]; pad_start(self, width: usize, fill: char = ' ') -> string; pad_end(self, width: usize, fill: char = ' ') -> string; count(self, needle: string) -> usize |
- std-text.split-once
split_once(separator)returns.Some((before, after)), with the text before and after the first occurrence ofseparator. - std-text.split-once.absentIt returns
.Nonewhenseparatordoes not occur, so"".split_once("=")is.None. - std-text.split-once.emptyAn empty separator occurs at offset 0, so
"ab".split_once("")is.Some(("", "ab")). - std-text.splitn
splitn(separator, n)returns the firstn - 1pieces thatsplit(separator)returns, followed by the rest of the string after the last of those separators as the final piece. - std-text.splitn.fewA string with at most
n - 1separators gives the pieces ofsplit(separator), so"a=b".splitn("=", 5)is["a", "b"]. - std-text.splitn.smallAn
nof 1 gives the whole string as one piece, and annof 0 gives[]. - std-text.splitn.empty-separatorAn empty separator takes one scalar value per piece, as
splitdoes, so"héy".splitn("", 2)is["h", "éy"]and"".splitn("", 2)is[]. - std-text.split-whitespace
split_whitespacereturns, in order, each maximal run of scalar values that lack the UnicodeWhite_Spaceproperty. - std-text.split-whitespace.no-emptyIt returns no empty piece, so an empty string, or one of whitespace only, gives
[]. - std-text.pad.length
pad_startandpad_endmeasure a string's length in scalar values, not in bytes or display columns. - std-text.pad.start
pad_start(width, fill)puts copies offillbefore the string until its length iswidth. - std-text.pad.end
pad_end(width, fill)puts the copies after the string instead. - std-text.pad.width-at-most-lengthA
widthat or below the string's length gives the string unchanged. - std-text.pad.fill-default
filldefaults to the space U+0020, so"7".pad_end(3)is"7 ". - std-text.count
count(needle)returns the number of non-overlapping matches ofneedle, found from left to right, so"aaaa".count("aa")is 2. - std-text.count.emptyAn empty needle matches at every scalar boundary, so it gives the number of scalar values plus one, and
"".count("")is 1.
| Call | Result |
|---|---|
"a=b=c".split_once("=") |
.Some(("a", "b=c")) |
"abc".split_once(",") |
.None |
"a=b=c".splitn("=", 2) |
["a", "b=c"] |
"a=b".splitn("=", 0) |
[] |
" a b ".split_whitespace() |
["a", "b"] |
"é".pad_start(3, '.') |
"..é", since é is one scalar value |
"abc".pad_end(2) |
"abc" |
"héllo".count("") |
6 |
Why. The names and results are Rust's
splitn,split_onceandsplit_whitespace, Python'sstr.count, and JavaScript'spadStartandpadEnd. A width in scalar values is the unitcharsyields; display columns would need the East Asian Width tables. An empty needle counts the boundaries thatreplacewith an emptyoldfills, as in Python.
#Character Classification
std gives char four classification methods and to_digit, as Rust
does:
fn word_start(c: char) -> bool:
c.is_alphabetic() || c == '_'
fn hex_value(c: char) -> u32?:
c.to_digit(16) # .Some(15) for 'f' and for 'F'| Rule | Method | Result |
|---|---|---|
| std-text.char.ascii-digitASCII digit | is_ascii_digit(self) -> bool |
true for '0' to '9' only |
| std-text.char.alphabeticAlphabetic | is_alphabetic(self) -> bool |
true for a scalar value with the Unicode Alphabetic property |
| std-text.char.alphanumericAlphanumeric | is_alphanumeric(self) -> bool |
true for an Alphabetic scalar value, or one whose general category is Nd, Nl, or No |
| std-text.char.whitespaceWhitespace | is_whitespace(self) -> bool |
true for a scalar value with the Unicode White_Space property, the set that trim removes |
| std-text.char.to-digitTo digit | to_digit(self, radix: u32) -> u32? |
the digit's value in radix, by the rules below |
- std-text.char.unicode-versionThe Unicode properties and general categories that this section names are those of Unicode 17.0.
- std-text.char.to-digit.value
to_digitgives'0'to'9'the values 0 to 9, and gives'a'to'z'and'A'to'Z'the values 10 to 35. - std-text.char.to-digit.resultIt returns
.Someof that value when the value is less thanradix, and.Noneotherwise. - std-text.char.to-digit.ascii-onlyEvery other
chargives.None, so'٣'.to_digit(10)is.None, though'٣'.is_alphanumeric()istrue. - std-text.char.to-digit.radixA
radixbelow 2 or above 36 panics. Panic:explicit-panic.
Why. The names and results are Rust's, so agents and readers already know them.
to_digitreads ASCII only, so a number parser built on it accepts no digit of another script by accident.
#UTF-8 Conversion
to_utf8 copies a string's bytes, and string::from_utf8 checks bytes
and makes a string of them, as
types.string.from-bytes
requires:
use std.text.Utf8Error
fn decode(bytes: List[u8]) -> string:
match string::from_utf8(bytes):
.Ok(text) => text
.Err(Utf8Error.InvalidSequence(position)) => "bad byte at $position"
.Err(Utf8Error.Truncated(position)) => "cut short at $position"| Receiver | Methods |
|---|---|
string |
to_utf8(self) -> List[u8]; the associated function string::from_utf8(bytes: List[u8]) -> Result[string, Utf8Error] |
- std-text.utf8.to-utf8
to_utf8returns a new list of the string's bytes, in order. - std-text.utf8.from-utf8
string::from_utf8(bytes)returns.Okof the string whose bytes arebyteswhen they are well-formed UTF-8, and.Errotherwise. - std-text.utf8.well-formedWell-formed UTF-8 is that of the Unicode Standard, so an overlong encoding, a surrogate code point, and a value above U+10FFFF are not well formed.
- std-text.utf8.round-tripFor every string
s,string::from_utf8(s.to_utf8())is.Ok(s). - std-text.utf8.error.declared-positions
std.textdeclares the enumUtf8Error, with the variantsInvalidSequence(position: usize)andTruncated(position: usize). Code imports it, as inuse std.text.Utf8Error. - std-text.utf8.error.firstThe error describes the first sequence, from the start of
bytes, that is not well formed. - std-text.utf8.error.truncated-positionIt is
Truncated(position)when the bytes end before that sequence has the length its first byte gives, wherepositionis the byte offset at which that sequence starts. That holds only when each byte of it after the first is a continuation byte,0x80to0xBF. - std-text.utf8.error.invalidOtherwise it is
InvalidSequence(position), wherepositionis the byte offset at which that sequence starts. - std-text.utf8.error.traits
Utf8ErrorimplementsEq,Debug, andDisplay. Two errors are equal when they are the same variant with the same position.
| Rule | Error | Display text |
|---|---|---|
| std-text.utf8.error.display.invalidInvalid sequence | InvalidSequence(position) |
invalid UTF-8 sequence at byte followed by position in decimal |
| std-text.utf8.error.display.truncated-positionTruncated | Truncated(position) |
incomplete UTF-8 sequence at byte followed by position in decimal |
Note. So
[0xE2, 0x82]isTruncated(0),[0x68, 0xE2, 0x82]isTruncated(1),[0xC3, 0x28]isInvalidSequence(0), and the overlong[0xC0, 0xAF]isInvalidSequence(0).
#Parsing Text
Parse is the trait of a type that reads itself from text, as Rust's
FromStr. string.parse::[T]() calls it:
use std.text.Parse
fn port(text: string) -> i32:
match text.parse::[i32]():
.Ok(value) => value
.Err(_) => 8080
fn read[T < Parse](text: string) -> Result[T, T::Error]:
T::parse(text)
fn flag(text: string) -> bool:
text.parse::[bool]().unwrap_or(false)The trait and the bool, char, and string impls are declared here.
The number impls are in Integer Parsing.
pub trait Parse:
type Error
fn parse(text: string) -> Result[Self, Self::Error]
pub enum ParseBoolError:
Invalid
pub enum ParseCharError:
Empty
TooMany- std-text.parse.trait
std.textdeclares the traitParseas above. It is not a prelude name; code imports it, as inuse std.text.Parse. - std-text.parse.whole
Self::parse(text)reads the whole oftext. An impl never trims, skips, or ignores any part of it. - std-text.parse.method
stdgivesstringthe methodparse[T < Parse](self) -> Result[T, T::Error], which returnsT::parse(self). Calling it needs no import, and the call names the type, as intext.parse::[i32](). - std-text.parse.string
stringimplementsParsewithErrorset tonever, andparsereturns.Okof the same text. - std-text.parse.bool
boolimplementsParsewithErrorset toParseBoolError. The text"true"gives.Ok(true), and"false"gives.Ok(false). - std-text.parse.bool-otherAny other text gives
.Err(ParseBoolError.Invalid). That includes"","True","1", and text with a space. - std-text.parse.bool-error
std.textdeclaresParseBoolErroras above. It implementsEq,Debug,Display, andError. ItsDisplaytext isprovided string was not `true` or `false`. - std-text.parse.char
charimplementsParsewithErrorset toParseCharError. Text of exactly one Unicode scalar value gives.Okof it. - std-text.parse.char-emptyEmpty text gives
.Err(ParseCharError.Empty). - std-text.parse.char-manyText of two or more scalar values gives
.Err(ParseCharError.TooMany). - std-text.parse.char-error
std.textdeclaresParseCharErroras above. It implementsEq,Debug,Display, andError. ItsDisplaytexts arecannot parse a character from empty textforEmptyandtext has more than one characterforTooMany. - std-text.parse.never-traits
neverimplementsEq,Debug,Display, andError, soResult[string, never]works whereverResult[T, ParseNumberError]does, and a generic reader overT < Parse[Error = E]andE < Displayacceptsstring. No value ofneverexists, so none of their methods runs.
| Call | Result |
|---|---|
"true".parse::[bool]() |
.Ok(true) |
"yes".parse::[bool]() |
.Err(ParseBoolError.Invalid) |
"é".parse::[char]() |
.Ok('é') |
"ab".parse::[char]() |
.Err(ParseCharError.TooMany) |
"ab".parse::[string]() |
.Ok("ab") |
Why.
Parseis Rust'sFromStr: one trait givesstd.cliandstd.hosta typed read without a function per type, and a user type can join it. An impl reads the whole text so that the same text means the same value everywhere, as numericParsedoes.
#Raw Text Prefix
The standard library declares one prefix, in std.text:
| Rule | Declaration | Meaning |
|---|---|---|
| std-text.prefix.std.r.reference-valuesRaw text | @str_prefix pub fn r(t: Template[*dyn Display]) -> string |
the pieces joined with the values' Display text, with no escape processed |
- std-text.prefix.std.r-meaningSo
r"\d+ $n"is the text\d+followed by theDisplaytext ofn, andr"a\"b"keeps its backslash. - std-text.prefix.std.only-r
ris the only standard prefix.stddeclares nob, sob"..."names nothing until a bytes type exists. - std-text.prefix.std.import-text
std.textdeclares the string prefixr. It is not a prelude name; code imports it, as inuse std.text.r.
use std.text.r
fn digits(count: i32) -> string:
r"\d{$count}" # the text \d{ then count, then }See also: Prefixed Strings, which specifies how a prefixed string calls its prefix function.
#More String Methods
std also gives string methods that test, search, and trim text:
fn extension(name: string) -> string?:
match name.rfind("."):
.Some(at) => .Some(name.slice(at + 1, name.len())) # "gz" for "a.tar.gz"
.None => .None
fn header(line: string) -> string?:
if line.is_empty() || !line.contains(":"):
return .None
match line.strip_prefix("# "):
.Some(rest) => .Some(rest.trim_end().upper())
.None => .None| Receiver | Methods |
|---|---|
string |
is_empty(self) -> bool; ends_with(self, suffix: string) -> bool; contains(self, needle: string) -> bool; find(self, needle: string) -> usize?; rfind(self, needle: string) -> usize?; trim_start(self) -> string; trim_end(self) -> string; upper(self) -> string; strip_prefix(self, prefix: string) -> string?; strip_suffix(self, suffix: string) -> string? |
- std-text.more.is-empty
is_emptyis true exactly when the string has no bytes. - std-text.more.ends-with
ends_withcompares scalar sequences exactly, asstarts_withdoes. - std-text.more.contains
contains(needle)is true whenneedleoccurs in the string. An emptyneedleoccurs in every string. - std-text.more.find
find(needle)returns the byte offset of the first occurrence ofneedlein.Some, or.None. An emptyneedleis found at 0. - std-text.rfind
rfind(needle)returns the byte offset of the last occurrence ofneedlein.Some, or.None. Occurrences may overlap, so"aaaa".rfind("aa")is.Some(2). - std-text.rfind.emptyAn empty
needleis found at the length of the string in bytes, as in Rust, so"abc".rfind("")is.Some(3). - std-text.rfind.linear
rfindtakes time linear in the lengths of the string and the needle together. - std-text.more.trim-ends
trim_startremoves the UnicodeWhite_Spaceproperty at the start only, andtrim_endat the end only. - std-text.more.upper
upperuses Unicode Default Case Conversion with full mappings, aslowerdoes. - std-text.more.strip
strip_prefix(prefix)returns the rest of the string afterprefixin.Somewhen the string starts with it, and.Noneotherwise.strip_suffixdoes the same at the end.
| Call | Result |
|---|---|
"héllo".find("llo") |
.Some(3) |
"abc".contains("") |
true |
" a ".trim_start() |
"a " |
"straße".upper() |
"STRASSE" |
"key=1".strip_prefix("key=") |
.Some("1") |
Why. The names are Rust's
strmethods.findgives a byte offset, so its result is a validslicebound.
#Joining And Building
The join method of List[string] and StringBuilder join many
pieces of text:
use std.text.StringBuilder
fn csv(cells: List[string]) -> string:
cells.join(",")
fn report(names: List[string]) -> string:
out := StringBuilder::new()
for name in names:
out.push("- ")
out.push(name)
out.push_char('\n')
out.build()- std-text.join.list-method
std.textgivesList[string]the methodjoin(self, separator: string) -> string. Calling it needs no import. - std-text.join.list-result
joinreturns the parts in order withseparatorbetween each two, so one part gives that part and no part gives"". - std-text.builder.decl-struct
std.textdeclares the structStringBuilder, with private fields, which implementsDebug. Code imports it, as inuse std.text.StringBuilder. - std-text.builder.new-ref
StringBuilder::new() -> *mut StringBuilderreturns an empty builder, a new object. Every copy of the reference pushes to that one builder. - std-text.builder.push-local
StringBuilder.push(&mut self, text: string) -> voidappendstext. - std-text.builder.push-char-local
StringBuilder.push_char(&mut self, c: char) -> voidappends the UTF-8 encoding ofc, one to four bytes. - std-text.builder.push-ascii-local
StringBuilder.push_ascii(&mut self, byte: u8) -> voidappends the bytebyteas an ASCII character. - std-text.builder.push-ascii.rangeA
byteof 128 or more panics. Panic:explicit-panic. - std-text.builder.reserve-local
StringBuilder.reserve(&mut self, count: usize) -> voidmakes room for at leastcountmore bytes without appending text. - std-text.builder.build-local
StringBuilder.build(&mut self) -> stringreturns every text pushed since the builder was made or last built, in push order, joined with nothing between, and empties the builder.
Calls on a new builder b |
b.build() gives |
A second b.build() gives |
|---|---|---|
b.push("ab"), then b.push_char('c') |
"abc" |
"" |
b.push_ascii(104), then b.push_ascii(105) |
"hi" |
"" |
b.reserve(8), then b.push_ascii(33) |
"!" |
"" |
b.push_ascii(200) |
panics explicit-panic |
Why. The method is Rust's
[String].join(sep), and agents wrote it in usability probes. There is no freejoin(parts, separator), since it was a second spelling of the method.trait.inherent.unique-unifying.disjointalready allows an inherent implementation forList[string]alone.A builder is one buffer of bytes.
buildhands it over and starts again, as Go'sstrings.Buildershares its buffer with the string it returns, so the string takes the bytes with no copy.push_asciitakes only ASCII, so every string the builder makes is valid UTF-8 by construction andbuildchecks nothing; other characters go throughpush_charorpush.
#Prefix Helpers
std.text gives a custom string prefix the helpers that r uses:
use std.ops.{Template, str_prefix}
use std.text.{interpolate, process_escapes}
@str_prefix
fn cooked(t: Template[i32]) -> string:
let mut pieces: List[string] = []
for piece in t.raw_parts:
match process_escapes(piece):
.Ok(text) => pieces.push(text)
.Err(error) => pieces.push("?${error.position}")
interpolate(Template { raw_parts: pieces, values: t.values })- std-text.prefix-helper.interpolate
std.textdeclarespub fn interpolate[T < Display](t: Template[T]) -> string, which joins the raw pieces and the values'Displaytext, alternating, starting with the first piece. - std-text.prefix-helper.escapes
std.textdeclarespub fn process_escapes(text: string) -> Result[string, EscapeError], which replaces each escape sequence of Escape Sequences with its meaning. - std-text.prefix-helper.escape-error-value
std.textdeclarespub struct EscapeError, a value with one public field,position: usize, the byte offset of the first invalid escape's backslash. It implementsEqandDebug. - std-text.prefix-helper.final-backslashA lone final backslash is an invalid escape.
| Call | Result |
|---|---|
process_escapes("a\\tb") |
.Ok of a, a tab, and b |
process_escapes("é\\q") |
.Err(EscapeError { position: 2 }) |
Why. The field is
position, as in every other std error. Its byte unit is the oneslicetakes.