String operations

Strings are immutable. len(text) returns the number of bytes, and text[i] returns the byte at an integer index as a char. Bounds are checked at runtime. The current behavior is byte-oriented rather than Unicode-code-point-oriented.

slice(text, start, end) returns a new string using a half-open range: start is included and end is excluded. Invalid ranges terminate the program with a runtime error.

Two strings can be joined with +. == and != compare their contents rather than their addresses. += provides the assignment form of concatenation:

str prefix = slice("cuarzo", 0, 3)
str name = prefix + "rzo"
name += "!"
print(name == "quarzo!")

join(parts, separator) concatenates a str[], inserting the separator only between elements; joining an empty array returns "". It builds the result in one allocation, so collecting pieces in an array and joining once is the efficient way to build large text.

f-strings

A string written with f before it holds values in braces:

str name = "Ana"
f64 height = 1.6789
print(f"{name} is {height:2} m tall and {len(name)} letters long")
// Ana is 1.68 m tall and 3 letters long

Each {expression} becomes to_str(expression), and {expression:N} becomes to_str(expression, N), with N decimals. Any other spec after the : becomes format(expression, "spec"), so {price:>10,.2f} is the price with two decimals and thousands separated by commas, to the right of ten columns. The expression can be any expression, including calls and strings of its own. {{ and }} stand for the braces themselves. The values are evaluated from left to right, and the result is a new string.

format(value, spec) lays out a number or any value to_str takes. A spec is [[fill]align][sign][#][0][width][grouping][.precision][type], as in Python:

Part Meaning
<, >, ^ to the left, right, or center of the width; numbers go right and other values left unless told
= numbers only: the fill between the sign and the digits
fill any one character before the alignment, as *^9; a space unless given
+, -, space a sign for every number, for negative ones only (the default), or a space before positive ones
# 0x, 0X, 0o, or 0b before the digits of x, X, o, and b, and a point kept by f, e, and g
0 numbers padded with zeros after the sign, as 08
width the least number of characters
, or _ the thousands separated, or for _ with x, o, and b each four digits
.precision the decimals of f, e, and %, the significant digits of g, and the most characters of text
type d whole numbers; x, X, o, b hexadecimal, octal, and binary; f, e, E, g, G fixed, scientific, and either, % a percentage; s text
print(format(1234567, ","), format(255, "#x"), format(0.256, ".1%"))    // 1,234,567 0xff 25.6%
print(f"[{42:>6}] [{42:<6}] [{"ok":*^6}] [{-3.14159:+.2f}] [{1e6:.2e}]")
// [    42] [42    ] [**ok**] [-3.14] [1.00e+06]

A float without a type is written as to_str writes it, or with precision decimals when one is given; a whole number with e, f, g, or % is written as a float. Widths count characters. A spec written in the code is checked when the program is compiled; one made while it runs and not valid stops it with an error.

to_str(value) returns exactly the text that print(value) would write for an integer, float, bool, char, str, or enum value:

str[] parts = ["n=", to_str(42), " ok=", to_str(true)]
print(join(parts, "")) // n=42 ok=true

Text built-ins

These built-ins search and edit text. Strings never change, so the editing ones return a new string:

Built-in Result Behavior
split(text, separator) str[] the pieces between each separator
find(text, part) i64 the index of the first part, or -1
find(text, part, start) i64 the same, searching from start
replace(text, old, new) str text with every old replaced by new
trim(text) str text without spaces, tabs, or line ends at either end
starts_with(text, prefix) bool whether text begins with prefix
ends_with(text, suffix) bool whether text ends with suffix
upper(text), lower(text) str ASCII letters in upper or lower case
parse_int(text) Result[int] a whole decimal integer, with an optional sign
parse_float(text) Result[f64] a decimal number such as -0.5 or 1.5e3
ord(c) int the byte code of a char, from 0 to 255
chr(code) char the char with a byte code from 0 to 255
str[] fields = split("ana, 31, Rosario", ", ")
print(len(fields), upper(fields[0]))          // 3 ANA
print(find("banana", "na"), find("banana", "na", 3))  // 2 4
print(chr(ord('a') + 1))                     // b

parse_int and parse_float accept the whole text and nothing else: spaces, hexadecimal, and words such as inf are errors, as is an integer that does not fit in an int. They return an error Result with a message such as '12abc' is not an integer.

split keeps empty pieces, so split("a,,b", ",") has three elements and split("", ",") has one. An empty separator for split, an empty old for replace, a find start outside the text, and a chr code outside 0 to 255 are runtime errors.

Concatenation, slicing, joining, and conversion allocate new strings, which the collector reclaims once nothing refers to them. An indexed string assignment is rejected because strings are immutable.