Sisu

Reference / Strings and formatting

Strings and formatting

A string is text in UTF-8. A program joins strings with +, looks at their bytes and characters with methods, and builds text with interpolation.

fn main() {
    let name = "häst"
    print(name.len())                        // 5
    print(name.chars().len())                // 4
    print("{name} has {name.chars().len()} letters")   // häst has 4 letters
}

This page tells how a string works, the methods of a string, how a value becomes text, and the format specs of an interpolation. Literals tells how to write a string literal and its escapes.

Text and bytes

A string always holds valid UTF-8. A character of a string is one Unicode scalar value, a char (Types). Each character takes 1 to 4 bytes. e and a combining accent are two characters.

fn main() {
    let s = "häst 😀"
    print(s.len())                           // 10
    print(s.chars().len())                   // 6
    print(s.bytes()[1])                      // 195
}

The rules:

  • A string is a value. Binding, passing and storing it behaves as a copy (Values and memory).
  • + joins two strings (Arithmetic). += joins a string to a place that can change (Compound assignment). Another type beside a string is an error. Use interpolation.
  • ==, !=, < and the other comparison operators compare byte by byte, which is the order of the code points (Comparison).
  • Sisu does not normalize text. "é" as one character and "e" with a combining accent are not equal.
  • A string has no index (E0068), no range index (E0023) and no fields. Use chars() or bytes() to get an array.
  • A string can hold the null character \0. It counts as one byte.
fn main() {
    print("é" == "e\u{301}")                 // false
    print("ant" < "bee")                     // true
    print("ab" < "abc")                      // true
    let mut text = "sau"
    text += "na"
    print(text + "!")                        // sauna!
}
fn main() {
    let s = "sauna"
    print(s[0])                              // error[E0068]
}

E0068: what this error means

Methods

A string has these methods. None of them takes an argument.

Method Result Meaning
s.len() i64 The number of bytes in s, not the number of characters
s.chars() [char] The characters of s, in order
s.bytes() [u8] The UTF-8 bytes of s, in order
s.words() [string] The words of s, in order

string(cs) is the reverse of chars(). It builds a string from a [char] (Types).

fn main() {
    let s = "ab"
    let mut cs = s.chars()
    cs[0] = 'z'
    print(string(cs))                        // zb
    print(s)                                 // ab
    print(string(cs).len())                  // 2
}

The rules:

  • chars(), bytes() and words() make a new array each time. A change to the array does not change the string.
  • A word is a run of characters that are not white space. White space is any character with the Unicode property White_Space, such as a space, a tab, a line break and a no-break space.
  • A string that is empty, or only white space, has no words. A word is never empty.
  • A string has no other methods (E0063). A char has no methods either. Convert it with i64(c) (Types).
  • A method of an optional string is called with ?. (Optional chaining).
fn main() {
    for w in "  one two\tthree\u{A0}four  ".words() {
        print("[{w}]")
    }
    print("".words().len())                  // 0
    print(" \t ".words().len())              // 0
}
output
[one]
[two]
[three]
[four]
0
0

Text of a value

Interpolation and print write a value as text in the same way. They take an i64, a u8, an f64, a bool, a char or a string (E0098, E0059).

Type Text
i64, u8 The decimal digits, with a - for a negative number
f64 The shortest decimal text that reads back as the same number
bool true or false
char The character
string The string, as it is

The text of an f64 has at least one digit after the point, such as 100.0. A number with an absolute value of 1e16 or more has an exponent instead, such as 1e21. So has a number whose absolute value is below 1e-4 but not 0, such as 1.5e-7. The exponent has no + sign and no leading zero. The text of the special values is inf, -inf and nan, and -0.0 keeps its sign.

fn main() {
    print(100.0)                             // 100.0
    print(0.1 + 0.2)                         // 0.30000000000000004
    print(0.0001)                            // 0.0001
    print(0.00001)                           // 1e-5
    print(9999999999999998.0)                // 9999999999999998.0
    print(1e16)                              // 1e16
    print(1.5e-7)                            // 1.5e-7
    print(-1.0 / 0.0)                        // -inf
    print(-0.0)                              // -0.0
}

Text of other types In development

print and interpolation also take a value of any type that can be a type argument. An array prints as [1, 2]. A class prints its name. A class can write its own text with a method to_string (Display, Eq and Ord).

In development
class Sauna {
    heat: i64
}

fn main() {
    print([1, 2])                      // [1, 2]
    print(Sauna(heat: 80))             // Sauna
}

Interpolation

{expression} in a string literal writes the value of the expression as text (Literals). A : after the expression adds a format spec.

fn main() {
    let n = 7
    let b: u8 = 200
    let x = 2.5
    let ok = n > 3
    let c = 'ä'
    let s = "sauna"
    print("{n} {b} {x} {ok} {c} {s}")        // 7 200 2.5 true ä sauna
    print("{n + 1} {s + "!"} {n * 2 > 10}")  // 8 sauna! true
}

Interpolation does not convert a value: it writes the text of the type that the value has. To write an optional, use ?? or if let. To write an array, use a for loop.

The program calculates each expression once, from left to right, before it builds the text. This is so also when the parts have format specs.

fn part(n: i64) -> i64 {
    print("part {n}")
    n
}

fn main() {
    print("{part(1)}:{part(2):>3}:{part(3)}")
}
output
part 1
part 2
part 3
1:  2:3

Format specs

A format spec sets the width of the text of a part, how it aligns, and how many decimals an f64 has. It follows the expression and a :, as in {x:>8.2}.

fn main() {
    let x = 2.675
    let name = "Väinö"
    print("[{x:>8.2}]")                      // [    2.67]
    print("[{name:<8}]")                     // [Väinö   ]
    print("[{name:^9}]")                     // [  Väinö  ]
    print("[{42:6}]")                        // [    42]
}

A spec is [align][width][.precision]. A spec needs a width or a precision. The align is optional.

Part Form Meaning
align <, > or ^ Left, right or centre
width 1 to 1000 The least width of the text, in characters
precision . and 0 to 30 The number of decimals of an f64

The rules:

  • A part that is shorter than its width gets spaces. A part that is longer is not cut.
  • Without an align, a number (i64, u8 or f64) is on the right, with the spaces on its left. Every other type is on the left, with the spaces on its right.
  • The width counts characters, not bytes. ä and 😀 are each one column.
  • ^ puts half of the spaces on each side. When the number of spaces is odd, the extra space is on the right.
  • A precision rounds half to even from the exact value of the f64, and never uses an exponent. A precision of 0 writes no point. inf, -inf and nan have no decimals.
  • A precision on a value that is not an f64 is an error (E0111).
  • A width above 1000, or a precision above 30, is an error (E0110).
  • A spec with no width and no precision, or a . with no digits, is an error (E0108).
  • Other parts, such as zero padding, a sign, a fill character, a type letter, or a width from a variable, are errors (E0109). The section The # flag is in development, and it changes this rule.
  • A : inside a string or character literal in the expression does not start a spec.
fn main() {
    let t = 3.14159
    let big = 1e21
    print("[{t:.2}][{t:.0}][{t:8.3}]")       // [3.14][3][   3.142]
    print("[{2.5:.0}][{0.5:.0}][{1.5:.0}]")   // [2][0][2]
    print("[{big:.1}]")                      // [1000000000000000000000.0]
    print("[{"a:b"}]")                       // [a:b]
}
fn main() {
    let n = 7
    print("{n:.2}")                          // error[E0111]
}

E0111: what this error means

The # flag In development

{x:#} lays a value out on several lines when it does not fit on one line. A value that fits stays on one line. A part that does not fit breaks into one element per line.

In development
fn main() {
    let xs = [1, 2, 3]
    print("{xs:#}")                    // [1, 2, 3]
    print("{7:#}")                     // 7
}

The rules:

  • A hand-written to_string gives one piece of text that the layout does not break.
  • # does not combine with a width, an alignment or a precision. {x:#>8} is an error.
  • # on an i64, a u8, an f64, a bool, a char or a string changes nothing.
  • There is no function for it: print("{x:#}") is the one way.